vue

✍️ Tangxt ⏳ 2020-10-05 🏷️ 列表页

13-列表展示怎么做

★封装 Tabs,使用 deep 语法

知识点

1)二次封装

Types 组件的基础上再封装一层!

代码:Demo

💡:如何更改子组件的样式?

/deep/::v-deep 的区别 -> 后者可以被 sass 认出来,而前者则不能!

deep

可以不用 .x !直接 ::v-deep

v-deep

这个 v-deep 的存在,让我们使用组件标签更像是在使用一个原生标签了 -> 在一些组件库里边,我们要改某个组件的样式是需要透过添加 prop 来改的!而现在我们只要追加一个 class-prefix ,然后写上 CSS 样式就行了!

我们加前缀的目的是让子组件知道我们是用什么类型的样式来处理它:

前缀

效果:

效果

💡:如果子组件有多个 li ,如何精准选中某个 li

最佳实践是使用 classPrefix ! -> 子组件的 template 里边有多个 li ,我们在 Types 组件标签上写一个 class-prefix="zzz"

样式前缀

用表驱动姿势为元素添加 class

表驱动

ES6 语法 -> key 有变量的话,那就用 [] 💡:再次强调 ! & ? 的意思?

ts 语法

💡:如何完成「按天」、「按周」、「按月」?

需求

需要封装一个 Tabs 组件! -> 内容基本与 Types 组件一致,只是有 3 个 tab 罢了! -> 需要二次封装 Types ! -> 把组件的内容交给父组件去控制! -> 总之,我们可以把 Types 变为更通用的 Tabs

属性定义:

属性

变量的起名:

变量名

改名时,可以用 webstorm 提供的重构功能! -> 变量名改完后要测试一下,看看能否正常运行!

💡:写 v-for 必须加 key ,这是潜规则!

💡:加 class 的代码好长?

class

class 封装处理:

封装

除了函数姿势,你也可以写成是一个 data 的姿势! -> 当然,优先使用 methods 姿势的!

2)不要 `Types` 组件了

Tabs.vue 放到 Money.vue 里边 -> Types 能做的, Tabs 也能做!

代码:Demo

💡:模块化处理 intervalList & typeList 把一些枚举东西扔到一个目录里边,其它模块文件需要用就直接导入就好了!

freeze

我们无法往这个数组里边 push 东西!

原先的做法:让我们无法改变值,也无法改变值的内容

值与内容

💡:设计稿中的高度?

高度

可以同时有 class & :class -> 会自动合并,但不能同时用 class/:classclass/:class 优先级处理:

优先级

但这有问题呀!我们无法确定这是不是 li 元素,万一是 div 呢?

那如果加 !important 呢? -> 这是万不得已的做法!

另一种做法,降低组件内部的优先级:

降低优先级

我没有想到 scss 这种 &-item 语法的结果居然直接 .tabs-item 了,而不是 .tabs > .tabs-item 这是方方的经验之谈……

对了, ::deep 的编译结果居然是这样的:

::deep 编译结果

代码:Demo

★用 JS 配置 height

方方推荐用之前的 CSS 姿势!

1)试试用 JS 搞 CSS

代码:Demo

第一步:

@Prop({type:String,default:'64'}) height!: string

必须加 ! ,不加的话,那么 tsc 就会报错说「你要初始化这个 height 」 -> 可这个 height 是外边传过来的呀! -> 所以初始化你妹啊!

第二步:

css

第三步(使用):

使用


更改子组件的样式 -> 我们要么传属性,要么写 CSS! -> 推荐写 CSS

对了, ::deep 可以优化成这样(不用写两次 ::deep 了):

::v-deep {
  .type-tabs-item {
    background: white;
    &.selected {
      background: #c4c4c4;
      &::after {
        display: none;
      }
    }
  }

  .interval-tabs-item {
    height: 48px;
  }
}

★用列表展示数据

1)需求 & 思路

我们要做的:

需求

用数据结构的思维去分析列表展示,它就是一颗无根之树,也就是所谓的森林:

森林

那么如何表示森林呢?

很简单,搞一个数组,里边的一个个元素是一个个对象:

数组 & 对象

公式:

公式

1 + 2 = 3 -> 就是在计算,所以就可以联想到「计算属性」了!

由于自身的数据结构不太好,抽象能力不太好,我们可以用其它奇奇怪怪地方式得到森林,但最最简单的方式还是用计算属性!

2)实现需求

💡:拿到 recordList ,就分组?

分组

💡:关于时间, ISO 8601

每个前端都要懂!但 99% 的前端都不懂!

💡:这行代码: (this.$store.state as RootState).recordList

为什么要用 as RootState ? -> 不用的话, recordList 就是 any 类型了,我们想要的是 RecordItem[]

这是 TS 与 Vue2 配合的 bug!

💡: Date 类型?

Date

我们存到本地数据库边,是经过一层序列化的操作的,也就是 JSON.stringify(state.recordList) ,而当我们:

{
  fetchRecords(state) {
    state.recordList = JSON.parse(
      window.localStorage.getItem("recordList") || "[]"
    ) as RecordItem[];
  }
}

拿数据的时候,是透过 JSON.parse 拿的!而它的返回值是 any 类型,然后被我们强制成了 RecordItem[] 类型!

JSON 不支持 Date 类型,面对Date,会转化成 string

string

所以我们拿到createdAt的值时,它就是个 string 值,而不是我们声明的 Date 类型 -> 我们强制断言JSON.parse()的结果是RecordItem[]类型,但这只是骗过了 tsc 的检查,当代码运行的时候,我们才确切知道 createdAt 的结果居然是个 string!而不是我们以为的 Date

Date & string

于是我们把createdAt类型改成string了:

type RecordItem = {
  tags: string[];
  notes: string;
  type: string;
  amount: number; // 数据类型 object | string
  createdAt?: string; // 类 / 构造函数
};

因此,之前用到 createdAt的地方都得toISOString()这样一下:

record2.createdAt = new Date().toISOString();

tsc即便检查到类型错误,也会让代码在浏览器里边正常地运行起来!

有很多toXxxString的 API,我们只要 ISO 8601String

💡:如何声明一个空对象{}的类型?

type HashTableValue = { title: string; items: RecordItem[] };
const hashTable: { [key: string]: HashTableValue } = {}

这个key可以是Key,也可以是中文等…… -> 名字随便! -> value值就直接写类型就好了,不用[value: { title: string }]这样

数据

💡:计算属性的执行时机要比mounted早!

所以我们用了beforeCreate这个钩子!

★可抄袭:添加 CSS

代码:Demo

💡:localStorage最多存 5 M 到 10 M 的数据?

如果数据多了,我们会如何处理呢? -> 数据库版……

每次只加载 7 天的数据,滚动一下,再加载 7 天的数据

★ISO 8601 和 dayjs

内容

1)ISO 8601 是什么?

国际标准 ISO 8601,是国际标准化组织的日期和时间(Date and time)的表示方法

日期指的是年月日,而是时间则指的是时分秒!

我们要关注的是「日期和时间的组合表示法」:

合并表示时,要在时间前面加一大写字母 T(一般较大的年份放在最前边,所以就是年月日,时分秒这样表示了),如此时东八区:

时间表示

带有小数的当地时间的完全表示:152735.5 或 15:27:35.5

话说,如何把 ISO 8601 表示的日期和时间转化成「中国标准时间」的日期对象?

日期对象

我们 toISOString() 拿到的是绝对的时间,也就是 0 时区的时间,如果你想得到 东八区这个相对的时间,那么你可以把这绝对的时间透过Date.parse(ISO 8601)得到「时间戳」,然后再new Date(xxx)一下,就能拿到东八区的时间了!

时间戳是一个数字,定义为格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数。注意,同一时刻,不同时区获得的时间戳是相同的(是从 0 时区开始计算的时间戳,不是基于东八区,东九区等这样计算时间戳的)。以前很多用来记录时间的字段,在数据库中往往不会存储为 Datetime 类型,而是直接存储为无符号整形,存放时间戳的值。

我们一般都不用时间戳,因为我们并不能看出此时是啥日期和时间,我们一般都用 ISO 8601,虽然结果是 0 时区的,但我们加 8 一下就知道,此时的日期和时间是多少了!

我们一般如何操作时间和日期呢? -> 不用原生 Date 提供的 API -> 因为这 API 像屎一样……

我们会用 Moment.js -> 非常好用,但体积贼鸡儿大!(moment.min.js 18.2k gz)而 Vue 20KB min+gzip!(k 是 KB 的简写,都是描述文件大小的,就像是这儿有几个苹果?你说 10 也行,你说 10 个 也行)

可以看到,我们只是为了处理一个日期和时间居然要用一个比 Vue 小一点点的库,而这显然是划不来的!

所以我们就用了 Day.js

Moment.js 的 2kB 轻量化方案,拥有同样强大的 API -> 和 Moment.js 的 API 设计保持完全一样

API

2)如何学习 Day.js?

CRM 学习大法!

它的兼容性很好!

安装:

npm install dayjs --save

使用:

import dayjs from "dayjs"

const api = dayjs()
console.log(api)

简单使用

💡:如何把「9 号」改成是「今天」?

日期显示

我们透过beautify(group.title)这个 API,可以把2020-10-09这样的日期改成是「今天/昨天/前天」这个样子!

原生 API 姿势:

原生 API

可以看到我们要写很多行代码

dayjs 姿势:

它提供了一个叫isSame的 API -> 可以判断一个「xxx日期」与「今天的这个日期」是否一样,如果一样,那就会返回true,否则,则返回false

如何计算减一天?

减一天,我们可以用 new Date().valueof() - 86400*1000

但这样做忒麻烦了,我们可以这样做:

做法

日是咩有0的,月同样是没有0的 -> 不会出现09月、09天!

关于时分秒,一笔账的统计精确到何时记的需要自己去发挥自己的创意去弄,当然,你也可以参照支付宝的!

💡:Date.prototype.valueOf() & Date.parse()的区别?

居然不一致

Date.prototype.valueOf()

Date.parse(dateString)

误差消除

总之,使用new Date()都得toISOString()一下!

💡:为了测试代码,localStorage里边的value是可以被我修改滴!

测试今天、昨天、前天的效果!

💡:目前存在的问题?

排序

3)小结

★数据排序

遍历对象

当我们遍历一个对象的时候,我们遍历的顺序是固定的吗?

如:

遍历的顺序

顺序是不固定的,可能是abc,也可能是bca等 -> 反正不管顺序是这样,这遍历操作都是对的!

更何况如果之后我们可以选择日期插入数据的话,那么所谓的对象的key顺序就是个笑话了!

所以我们要把result改成是数组!

那么我们得要一个什么样的数组呢?

type Result = { title: string; total?: number; items: RecordItem[] }[]

💡:字符串可以比较大小吗?

可以,但比较结果的返回值只有true or false,而我们要的比较结果可是有三种情况呀:小于 等于 大于

而且,一个字符串减去一个字符串的结果是NaN

字符串比较

所以我们就把createdAt给数字化了! -> 这是valueOf()的结果!

排序

顺序是相反的,那就换个位置减 -> 不用理会这是为啥!

上边的做法有个弊端,那就是我们改变recordList的值,万一其它地方有用到recordList,那就不好了!所以我们需要对其深 clone 一下!

clone

然而,原先的clone API,是返回 any 类型的,导致callback里的a、b形参报错了,解决报错姿势:

错误

clone(recordList)的返回值类型,可以透过recordList这个类型给推出来! -> 推出来之后,sort里边的每一项ts也可以自动推出来!

★数据排序后分组

同一天的就一个组!

分组思路:

思路

注意,如果recordList为空,你得处理一下!

分组结构

排序后的分组代码:

分组代码

💡:为什么叫groupedList

因为这代表这是一个数组呀!如果直接叫result的话,你是无法知道这是一个对象还是数组的!

💡:TS 也会检查 template

检查

我一直以为它不会去检查!

★完成统计页面

我们把「按周、按月」删了 -> 因为支付宝就咩有这样做,可见这并不是一个好的设计!

白色是选中的状态! -> 因为数据列表显示的是白色的!

1)支出与收入的切换

用了计算属性groupedList后,result依赖的this.type

type

2)得到一天的累积收入或累积支出

map

测试map

map

我们没有用到返回值,只是用了计算过程,所以这并不影响计算结果

整个计算过程是这样的:

遍历每一组,从每一组里边再reduce一下每条记账记录的amount! -> 每条记账记录统计完,那么一组就多了一个total字段!

在一片土地(数组)里边找到一颗树,在一颗树里边找到一个鸟巢(数组) -> 我们要统计鸟蛋数量 -> 从而得出这棵树上边总共有几颗鸟蛋!

💡:map & forEach

map是有返回值的forEach

相较于forEach,我们使用map可以少写几个字!

map

➹:译-图解 Map、Reduce 和 Filter 数组方法 - 知乎

➹:深入理解 Array.prototype.map() - 知乎

💡:amount的类型问题?

类型问题

所以tsc没有对this.output这个字符串值进行报错的提示!

3)最终的统计页面代码

代码:Demo


接下来看一下还有那些小功能需要去优化的!