# 我是精明的小卖家

# 我是精明的小卖家(一)

# 课程目标

这次的任务将会分解为好几个子任务,是我们以后在工作中会经常遇到的项目类型 -- MIS 系统,我们今天将会以 MIS 系统中的一个简单的页面示例,来让大家尝试一下稍微复杂的页面功能,我们如何进行拆解。

# 课程描述

最终需求描述

  • 这是一个数据报表定制页面,有许多筛选表单,有一个表格,以及几个图表,需要我们在不使用任何框架的情况下,完成这个页面的开发
  • 页面初始化的时候,显示默认的数据及图表
  • 根据用户的表单选择,可以进行数据和图表的切换,但这些切换动作都是在当前页面中完成
  • 用户可以在当前页面上做一些数据修改,我们先设定这些修改仅对自己有效,将数据存在浏览器本地
  • 当用户选择了某种数据视图(选择了某些选项所展示的数据及图表)后,可以复制当前 URL 给其他人,其他人也能看见同样的视图,而不是回到初始化效果
  • 我们会提供一份数据用例:链接: https://pan.baidu.com/s/1ckhyN9_vyKnWgB9icJmBpg 密码: tbwy

任务拆解

和上一个任务画流程图不一样的是,这个任务不仅要画流程图,还需要画一个整体的模块关系图,整个任务可以大致分为几个模块:路由模块,表单模块,数据处理模块,表格模块,图表模块。

  • 路由模块负责指挥页面的其它部分,该以什么样的状态来呈现页面
  • 表单模块负责承接用户的交互及告诉后面的模块如何组装数据,展示图表
  • 数据处理模块,则根据用户的输入,对数据进行组合,提供给页面展现
  • 表格模块负责用表格的形式展现数据
  • 图表模块负责用图表的方式展现数据

# 从简单的开始

需求

  • 表单:我们先从最简单的表单开始,我们的数据维度有:月、地区、商品种类,表单选项的任务就是做这几个维度的筛选或者组合的设置。我们先只做一个,用来做示例,比如我们选择用地区,请用一个 Select 或者单选,让用户可以选择地区。选项应该包括华东、华南、华北三个地区
  • 数据处理:接下来,我们根据用户选择的地区表单,从完整数据中,把对应选择地区的数据取出来
  • 表格:最后,将上一步取出来数据渲染成表格,表格有一个表头,用于显示数据标题:商品、地区、1 月、2 月……12 月,然后共有 14 列:商品、地区、以及 12 个月的销售情况

设计

上面需求还是一个比较简单的逻辑,表单的变更事件触发表格的更新,而表格更新依赖的数据来源于表单的选择。

伪代码类似:

HTML:

<select id="region-select">
    <option>....</option>
</select>
或者
<input type="radio">
……

.....

<div id="table-wrapper">
</div>

JS: 这是一种实现思路,渲染表格的方法接受数据参数,但不关注数据怎么来的

region-select的change事件 = function() {
    渲染新的表格(根据select选项获取数据)
}

function 根据select选项获取数据() {
    dosomething
    返回数据
}

function 渲染新的表格(data) {
    输出表头:商品、地区、1月、2月、…… 12月
    遍历数据 {
        输出每一行的表格HTML内容
    }
    把生成的HTML内容赋给table-wrapper
}

这是另外一种实现思路,表单变化时通知表格进行渲染,但不关注他用什么数据渲染

region-select的change事件 = function() {
    渲染新的表格()
}

function 根据select选项获取数据() {
    遍历数据 {
        向要返回的数据list中添加符合表单所选项的数据
    }
    返回数据
}

function 渲染新的表格() {
    根据表单选项获取数据
    渲染表格
}

两种思路在这个例子中,还不是那么有明显区别,随便选择一种。

# 稍微复杂一些

需求

我们现在加入第二个表单,商品种类,依然是 select 或者 radio ,自选。 两个表单项都存在,做并集的选择,比如选了华北,和手机,表示要看华北地区手机的销售情况 两个表单项的选择互相不干扰,即改变其中一个时候,不会导致另外一个的选项的变化 设计 很明显,这里要调整的是“根据 select 选项获取数据()”这个方法

function 根据select选项获取数据() {
遍历数据 {
向要返回的数据list中添加符合两个表单项所选的数据
}
返回数据
}

# 再复杂一些

需求

  • 我们发现,有时候我们不止要看某个地区的数据,我们可能会想同时看好几个地区的数据,或者我们想看某个地区所有商品的销售情况,所以我们需要把地区及商品从单选改成多选
  • 同时,为了方便多选,我们提供了一个功能叫做全部选择,分别给地区和商品各增加一个全选的CheckBox,全选有如下状态和逻辑:
    • 点击全选时,如果单个选项中只要有一个不是被选上的状态,则进行全选操作
    • 点击全选时,如果单个选项中所有选项都已经是被选上的状态,则无反应
    • 点击最后一个未被选中的单个选项后,全选CheckBox也要置为被勾选状态
    • 如果当前是全选状态,取消任何一个子选项,则全选CheckBox也要置为未勾选状态
    • 不允许一个都不勾选,所以当用户想取消唯一一个被勾选的子选项时,无交互反应,不允许取消勾选

设计

我们先不管数据是咋回事,我们先把全选这块逻辑整理一下。首先把 radio,select 换成 CheckBox 。

我们有两组 CheckBox ,从全选的逻辑是一致的,只是文案和具体值不一样,所以在这里,我们要尽可能想办法让两组CheckBox的逻辑能复用,而不要写两遍。

我们先来看看只有一组的情况下,逻辑是如何的:

  • 分别给全选的CheckBox和各个单选的CheckBox绑定上点击事件
  • 对于全选的CheckBox的点击事件,要做的事情很简单,让所有的CheckBox全部勾选上
  • 对于单个的CheckBox,每次点击要做如下判断:
    • 在点击之前它是不是唯一一个被勾选的?如果是的话,阻止这次点击默认事件,或者立马又将其checked状态置为真
    • 点击之后,是不是满足了全选状态,并对应修改全选CheckBox的状态 上面是整个完整一组CheckBox的基本逻辑,大家也可以深入看还有哪些可优化的地方或缺失的逻辑。

接下来我们看如何进行复用代码,先介绍一种思路,抛砖引玉,大家可以自己引申或从其它优秀框架类库中去学习如何封装组件

我们可以 HTML 部分只留容器,具体的CheckBox由JS生成

<div id="region-radio-wrapper"></div>
<div id="product-radio-wrapper"></div>

JS,我们暂时不介绍面向对象,设计模式等方式,先用很基础的方式来讲解,后续会有专门的练习,但如果有经验的同学也可以直接运用你的经验,面向对象方面的知识来进行封装

function 生成一组CheckBox( CheckBox容器, CheckBox选项的参数对象或者数组 ) {
    生成全选checkbox的html,给一个自定义属性表示为全选checkbox,比如checkbox-type="all"
    遍历参数对象 {
        生成各个子选项checkbox的html,给一个自定义属性表示为子选项
    }
    // bca-disable-line
    容器innerHTML = 生成好的html集合

    给容器做一个事件委托 = function() {
        if 是checkbox
            读取自定义属性
            if 全选
                做全选对应的逻辑
            else
                做子选项对应的逻辑
    }
}

// 对象或数组自己根据喜好实现均可
生成一组CheckBox(容器1, [{
    value: 1,
    text: "XXXX"
}, {
    value: 2,
    text: "YYYY"
}]);

生成一组CheckBox(容器2, [{
    value: 1,
    text: "AAAA"
}, {
    value: 2,
    text: "BBBB"
}]);

// 生成一组CheckBox({
//    1: "XXXX",
//    2: "YYYY"
// });

这样分别调用两次函数,把容器 ID 和对应 checkbox 的数据传入即可。

当然,如果选项不是很多,我们书写自定义属性的成本不大,我们也可以把生成HTML那部分省略,直接把HTML写好在容器中,JS部分只需要做事件处理即可。本例我们推荐把checkbox相关代码还是写在HTML中。这样JS代码会更加简洁,调用生成checkbox逻辑函数时,只需要传入容器ID即可。

这里我们某种程度上解决了代码复用的问题,以后我们会再学习如何将其封装成一个真正意义上的组件。

交互解决了,接下来需要解决数据处理的问题,根据表单选择进行数据多维度的筛选

function 获取数据 {
    遍历原始数据 {
        判断是否在商品维度 或者 地区维度的选中范围内 {
            添加到返回数据list中
        }
    }
    返回数据
}

# 多选的表格渲染

需求

现在,我们给表格也提出了更复杂的需求,之前我们仅仅需要单纯的遍历数据,然后一行一行,一格一格输出即可,但当突然出现多选的情况下,我们会期望有更好的阅读体验:

  • 当商品选择了一个,地区选择了多个的时候,商品作为第一列,地区作为第二列,并且把商品这一列的单元格做一个合并,只保留一个商品名称
  • 当地区选择了一个,商品选择了多个的时候,地区作为第一列,商品作为第二列,并且把地区这一列的单元格做一个合并,只保留一个地区名称
  • 当商品和地区都选择了多于一个的情况下,以商品为第一列,地区为第二列,商品列对同样的商品单元格进行合并
  • 当商品和地区都只选择一个的情况下,以商品为第一列,地区为第二列

设计思路

选择谁做第几列并不是难事,判断一下两组 CheckBox 的选择数量即可,稍微有难度的是做单元格合并。

我们如何在通过 JS 输出表格 HTML 的时候,在合适的行输出时把 rowspan 属性加进去,并且在其他行的时候又跳过这行,请大家仔细思考。这个点,我们就不留示例代码,留个大家发挥的空间。

# 文件拆分

第一个子任务最后一步,我们提出一个新的需求,后续我们代码可能会越来越多,现在表格,表单,数据处理等一堆东西的代码都放在一个文件中,实在是不方便代码维护,所以,请你把你的代码进行拆分,在你的项目根目录下,建立一个 js 目录,分别创建类似 checkbox.js ,table.js 这样的文件,把对应的代码放入。对了,页面入口主流程的代码,比如一些初始化的工作,放到一个叫做 app.js 的文件中

如果你有余力,可以开始使用 webpack 来进行打包,如果没有,或者你发现看了几眼就看不下去了,那么就只在你的 html 里多添加几个 js 即可,注意 app.js 应该放在几个 js 文件引用的最后一个。

# 我是精明的小卖家(二)

# 课程目标

今天我们将学习 SVG 和 Canvas ,来继续丰富我们的销售报表。

# 课程描述

当上一课任务完成时,面对一个复杂数据的表格,估计没有人会仔细看这份数据的内容是什么,看也看不出什么。所以我们希望找到某种方式,让数据变得更加易读和易懂,而数据可视化正式解决这一问题的最佳答案。

在表格上方增加两个图表,一个折线图和一个柱状图,用于展现不同数据在12个月的销售情况。

先了解一下数据可视化 简要阅读下面两个文章了解数据可视化

# 小练习

阅读 我们将通过 SVG 及 Canvas 来分别实现折线图及柱状图,我们先来看看 SVG ,使用 SVG 来实现我们的柱状图

小练习编码 这个练习的代码和报表页面无关,单独新开一个页面来实现,通过 SVG 实现如下需求

  • 画一条线
  • 画一个矩形
  • 画一个圆形
  • 显示一些文字
  • 画一朵花
  • 画一颗小树

# 先来个柱状图

需求详细描述 没有过多复杂的要求,我们先把焦点聚焦在图形的绘制上。使用 SVG ,来实现这个柱状图,包括以下元素:

  • 横轴
  • 纵轴
  • 数据项
  • 固定只显示华东地区手机 12 个月的数据
  • 图表样式不做限制,可以参考 ECharts
  • 暂时不在图表上做任何文字显示

实现思路 当你看完 SVG ,你会发现,大部分工作量都是在计算每个图形元素的位置及高宽

所以,找一个计算器很重要

新建一个 js 文件叫做:bar.js ,把柱状图的代码放在这个文件里。

画每个柱子其实很简单,就是绘制一个矩形,但在绘制的过程中,我们需要考虑数据对应的柱子高度是多少,而这个比例我们在这个练习中,需要以所有数据的最大值来进行参考和计算

function 绘制一个柱状图(柱状图数据) {
    定义好柱状图绘制区域的高度,宽度,轴的高度,宽度
    定义好每一个柱子的宽度及柱子的间隔宽度
    定义好柱子颜色,轴的颜色

    拿到柱状图中的最大值Max
    根据Max和你用来绘制柱状图图像区域的高度,进行一个数据和像素的折算比例

    绘制横轴及纵轴
    遍历数据 {
        计算将要绘制柱子的高度和位置
        绘制每一个柱子
    }    
}

# Canvas 小练习

小练习编码 这个练习的代码和报表页面无关,单独新开一个页面来实现,通过Canvas实现如下需求

  • 画一条线
  • 画一个矩形
  • 画一个圆形
  • 显示一些文字
  • 画一个时钟
  • 画一朵云

再来个折线图 我们再学习使用 HTML5 中第二个重要的绘图方式 Canvas 来绘制折线图

需求详细描述 和柱状图一样,包括以下元素:

  • 横轴
  • 纵轴
  • 每个数据对应在坐标中的数据点,可以用一个直径为 5 的实心圆
  • 每个数据点之间连接的直线
  • 固定只显示华东地区手机 12 个月的数据

实现思路 请准备好计算器。

新建一个 js 文件叫做:line.js ,把折现图的代码放在这个文件里。

function 绘制一个折线图(折线图数据) {
    定义好折线图绘制区域的高度,宽度,轴的高度,宽度
    定义好每一个数据点的直径,颜色,线的颜色,宽度    
    定义好没两个数据点之间的横向间隔距离

    拿到折线图中的最大值Max
    根据Max和你用来绘制折线图图像区域的高度,进行一个数据和像素的折算比例

    绘制横轴及纵轴
    遍历数据 {
        计算将要绘制数据点的坐标
        绘制数据点        
        if 不是第一个点 {
            绘制这个数据点和上一个数据点的连线
        }
        记录下当前数据点的数据用于下一个点时绘制连线
    }    
}

让图表数据可变 上面我们的数据都是用的固定的,接下来我们需要让图表的数据可变。

我们给表格增加一个鼠标滑过的事件响应,当鼠标滑过任何一行时,把这一行的数据在两个图表中进行呈现

把两个图表做成左右布局,各占一半宽度,表格在图表下方,表单在图表上方,几个内容保证在一个屏幕中能够完整呈现

设计思路 我们不妨将你的两个图表进行一下调整,让它们和数据解耦的组件,提供一些方法(如果已经有面向对象基础的同学可以进行类的封装)

折线图 = {

    图表数据: Array

    相关各种定义

    ……

    绘制图表: function() {

    }

    ……

    设置数据: function(数据) {
        根据新的数据从新 绘制图表()
    }

    ……   
};

这样,在鼠标滑动过某一行表格时,获取到对应行的数据,然后调用图表的设置数据方法。顺便提供一下鼠标滑过的设计思路,当然我相信到这里大部分同学已经知道怎么做了

实现思路一:

// 在绘制表格的时候,给对应的td或者tr添加一个自定义属性,这一格数据属于哪个商品哪个区域

表格添加mouseover事件 = function () {
    获取对应tr或者td的商品及区域的自定义属性
    根据上面两个属性在数据中获取对应的12个月的数据
    调用图表的设置数据方式
}

实现思路二:

表格添加mouseover事件 = function () {
    拿到响应事件对应的tr,然后依次遍历其中的td,获取其中的数据    
    调用图表的设置数据方式
}

不妨两种都试试,看看自己觉得哪种好

# 绘制多条折线图

需求描述 单独看一行数据,只能看一个指标自己的趋势,我们往往还需要在不同数据之间进行比较,所以我们需要在折线图中绘制多条数据。

  • 根据表单的选择,在折线图中显示相应的折现,和对应表格中的数据对应
  • 每一条线选择不同的颜色
  • 另外保留上面的鼠标 hover 某一行时显示某一行数据的图表,但鼠标移开表格后,再恢复到显示表单对应的所有数据

设计思路

对于折线图而言,其实画一条线和画多条线没有本质区别,需要我们额外考虑的点是:

  • 纵坐标的最大值取决于所有数据的最大值,而不仅仅是一个数据

  • 颜色的管理,管理一个颜色序列,分配给每一条线 我们把整个流程再做一下拆解和梳理,对于画每一条线而言,需要的包括:

  • 数据

  • 颜色

  • 两个点之间的间隔

  • 数据点直径

  • …… 上面这些有的数据对于每一条线是不一样的,有些是一样的,不一样的我们通过参数传递,一样的可以直接读取对象的属性

整体流程变为:

  • 计算整体的数据与像素的比例
  • 遍历数据,渲染每一条折线

# 绘制多个柱状图(可选)

需求描述 这个是个附加需求,有余力的同学可以尝试:

柱状图稍微复杂一些,我们先看个例子 (opens new window)

  • 以每个月为第一个维度进行柱状图的聚合
  • 然后再按照表格同样的分类方式对数据进行如上方例子类似的聚合
  • 每行数据有一个颜色,每一类数据用同系颜色

# 我是精明的小卖家(三)

# 课程目标

今天我们将学习 LocalStorage ,基于它来实现对于数据的写操作

# 课程描述

我们在做这种系统时,经常会有在数据表格中同时进行数据编辑的需求,这里面涉及两个工作,一个是实现在数据编辑的交互问题,另一个是解决数据编辑完成后的传输问题。今天我们就来学习一下。

# 编辑

需求

  • 我们首先为所有表格增加一个编辑功能,在原来的表格输出的数据单元格,全部变成 input 输入框,里面为数据。
  • 在页面中增加一个保存按钮,点击保存后将数据保存到 LocalStorage 中
  • 页面加载的时候,优先从 LocalStorage 读取数据

阅读

我们需要阅读一些基础的背景知识点:

设计

在学习完 LocalStorage 后,我们需要梳理一下业务流程,首先是数据读取,原来是直接使用提供的 JS 文件,现在我们需要先判断 LocalStorage 中是否有数据,有的话从 LocalStorage 中读取,没有的话再使用 JS 文件中的数据。

对于数据的编辑和保存,可以如下实现:

  • 给所有 input 输入框增加一个 onblur 事件,在事件中增加对于输入内容的判断,是否为正确的数字,是的话什么都不做,不是的话弹出提示框(alert)
  • 点击保存的时候,遍历所有 input ,按照一定顺序,把数据写入 LocalStorage 中。

# 体验更好的编辑

需求

如果对于一个强编辑需求的场景,上面的方案可能合适,甚至可以直接上一个 Web 电子表格组件,但如果对于数据的编辑是小部分的需求场景时,一堆 input 框看上去就不是那么优雅了。

我们希望你实践一下,看上去不是输入框,但鼠标移动上去或者点击就变成一个可编辑的状态。需求如下:

  • 把表格恢复成没有 input 的状态
  • 当鼠标滑动过某一个数字的单元格时,数字旁边显示一个铅笔的icon,或-者显示灰色的小小的编辑两个字
  • 当鼠标点击某个数字的单元格时,这个数字进入编辑状态,单元格内容变成一个输入框,输入框右边是取消和确定
  • 点击取消,输入框消失,恢复出原来数字
  • 点击确定,输入框消失,数字变成编辑的,这个过程中需要判断输入的正确性,如果输入的不是数字,则弹出提示
  • 点击该单元格以外的页面其他任何地方,除了响应对应行为外,同时等同于点击了取消,输入状态消失
  • 理论上,同一时刻,只有一个单元格处于编辑状态
  • 在输入框中,按 ESC 键等同于按取消
  • 在输入框中,按回车键等同于按确认

# Ajax 可选需求

需求

真正项目中,大部分是通过 Ajax 和 Server 通过接口来完成上面的事情,有余力的同学可以通过自己写一个简单的服务代码来学习前后端数据通信的主要方式 Ajax 。

阅读

# 我是精明的小卖家(四)

# 课程目标

今天我们将进行这个任务系列的最后一课,学习 Location、Hash 等相关知识,来实现最后一个需求

# 课程描述

先来点简单的

阅读

需求

<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<div id="cont"></div>
  • 基于以上 HTML ,点击对应按钮时候,改变 div 中的内容为按钮的文字。
  • 刷新页面的时候,保持 div 中的显示
  • 页面 URL copy 再打开后,保留渲染状态
  • 通过 location.hash 来实现

设计

按照传统思路,我们会给3个按钮绑定一个事件,事件中来改变 id 为 cont 的 div 中的内容,但在这个小任务中,我们需要改变一下逻辑。

这个需求中的关键,其实是在于通过 URL 中 # 后面的部分,来记录页面的状态,页面的渲染是由这个状态来驱动的。所以,点击按钮的时候,应该去做的事情,是更新这个状态。

然后对于状态改变这件事情,增加一个事件响应,来进行渲染。所以整个伪代码变为:

function 解析Hash,获取状态参数() {
    取到需要的值,并返回
}

function 渲染函数() {
    内容 = 解析Hash,获取状态参数()
    // bca-disable-line
    cont的innerHTML = 内容
}

按钮点击事件 = function() {
    设置新的hash
}

window.onhashchange = 渲染函数

进来先执行一次渲染函数()

按照上面的基本思想进行代码的编写吧

# 稍微复杂一点

需求

<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>

<button id="d">D</button>
<button id="e">E</button>
<button id="f">F</button>


<div id="contABC"></div>
<div id="contDEF"></div>

现在我们需要记录两个状态

  • abc 点击了,把按钮文字显示在 contABC 中
  • def 点击了,把按钮文字显示在 contDEF 中
  • 同样在页面刷新时,保留之前的渲染状态
  • 页面 URL copy 再打开后,保留渲染状态

设计

思路和前面一样,只是在获取状态参数的地方稍微复杂一点点,相信你能搞定的

# 正式来做报表的事情了

现在通过 hash 的方式

  • 把用户的一些交互状态通过某种方式记录在URL中
  • 分享或再次打开某个URL,需要从URL中读取到数据状态,并且进行页面呈现的还原
  • 需要记录的状态包括:产品的选择以及地域的选择

# 来学习一下 pushState

阅读

编码 用 pushState 等代替前面直接操作 hash 的方式,来实现前面2个小需求及报表页面的需求

# 提交

把你的代码放在Github后进行提交

# 总结

依然把今天的学习用时,收获,问题进行记录

# 预告

下一个任务将是全新的一个系列任务,敬请期待