一、封装Tags组件
(一)背景
发现记账页面和统计页面的这三个地方其实是一样的,所以封装成一个Tags组件


(二)src/component/Tags.vue
<template>
<ul class="tabs" :class="{[classPrefix+'-tabs']: classPrefix}">
<li v-for="item in dataSource" :key="item.value" class="tabs-item"
:class="{
[classPrefix + '-tabs-item']: classPrefix,
selected: item.value === value
}"
@click="select(item)">{{item.text}}
</li>
</ul>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component, Prop} from 'vue-property-decorator';
type DataSourceItem = { text: string;value: string }
@Component
export default class Tabs extends Vue {
//外部数据dataSource用于接受到底有几个块,每个块的名字和符号是啥
@Prop({required: true, type: Array}) dataSource!: DataSourceItem[];
//外部数据value用于表示选中了哪个块(的符号)
@Prop(String) readonly value!: string;
//外部数据 classPrefix用于给加上class的前缀。父组件传值后就会成为唯一的选择器
@Prop(String) classPrefix?: string;
liClass(item: DataSourceItem) {
return {
[this.classPrefix + '-tabs-item']: this.classPrefix,
selected: item.value === this.value
};
}
//若是点击了一个块,就会把这个块的符号给父组件,父组件会让这个符号成为新的value。selected: item.value === value因此这个块就会有class:selected了
select(item: DataSourceItem) {
this.$emit('update:value', item.value);
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/style/helper.scss";
.tabs {
background: $color-three;
display: flex;
font-size: 24px;
> li {
/*border: 1px solid black;*/
width: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 12px 0;
position: relative;
&.selected::after {
/*不可以用border,不然消失的时候整个会动,那就加一个伪元素,让伪元素绝对定位在它下面就行了*/
/*border-bottom: 4px solid;*/
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background: #333333;
}}}
</style>
(三)统计页面就可以使用Tags组件了
<template>
<Layout>
<Tabs class-prefix="type" :data-source="recordTypeList" :value.sync="type"/>
<Tabs class-prefix="interval" :data-source="intervalList" :value.sync="interval"/>
<div>
type: {{type}}
<br/>
interval: {{interval}}
</div>
</Layout>
</template>
<style scoped lang="scss">
@import "~@/assets/style/helper.scss";
::v-deep .type-tabs-item {
background: white;
&.selected {
background: $color-three;
&::after {
display: none;
}
}
}
::v-deep .interval-tabs-item {
height: 48px;
}
</style>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import Tabs from '@/components/Tabs.vue';
import intervalList from '@/constants/intervalList';
import recordTypeList from '@/constants/recordTypeList';
@Component({
components: {Tabs},
})
export default class Statistics extends Vue {
type = '-';
interval = 'day';
intervalList = intervalList;
recordTypeList = recordTypeList;
}
</script>
新建src/constants/intervalList.ts和recoreType.ts来当做dataSource
intervalList.ts
export default Object.freeze([
{text: '按天', value: 'day'},
{text: '按周', value: 'week'},
{text: '按月', value: 'month'},
]);
recoreType.ts
export default Object.freeze([
{text: '支出', value: '-'},
{text: '收入', value: '+'},
]);
二、统计页面的数据结构
- 我们要用的是数据是recordList里的每个record



[
{title:3.3, items:[record4,record5,record6], total: 100},
{title:3.2, items:[record3], total: 100},
{title:3.1, items:[record1,record2], : 100},
]
- 为什么是数组?遍历这个数组的时候会按照顺序依次遍历,而对象可能会乱遍历。
- 所以我们按照从最新的时间开始到以前的时间(倒序)这个顺序把这个数组写好,到时候按照顺序遍历

get groupedList() {
//一、获得数据recordList
const {recordList} = this;
//二、从recordList中选出一个只有支出或者只有收入,并且要按照时间倒序排序的新数组newList
const newList = clone(recordList)
.filter(r => r.type === this.type)
.sort((a, b) => dayjs(b.createdAt).valueOf() - dayjs(a.createdAt).valueOf());
if (newList.length === 0) {return [];}
//三、定义森林的类型是个数组,数组的每个元素都是个对象,每个对象有title和items和total属性
type Result = { title: string, items: RecordItem[], total?: number }[]
//四、首先result中就只有最新的一笔账的对象
const result: Result = [{title: dayjs(newList[0].createdAt).format('YYYY-MM-DD'), items: [newList[0]]}];
//其次,从第二个record开始遍历newList中每一个元素
for (let i = 1; i < newList.length; i++) {
//当前的record
const current = newList[i];
//result中最后一个对象
const last = result[result.length - 1
//因为newList中已经按照时间倒序排好了。比较当前的record和result中最后一个对象的时间
//如果一样,那就把record加入result中最后一个对象的items中
if (dayjs(last.title).isSame(dayjs(current.createdAt), 'day')) {
last.items.push(current);
//如果不一样,说明result没有这个时间的对象,那就新建一个这个时间的对象,并且这个对象的items里就暂时是这个record一个。
} else {
result.push({title: dayjs(current.createdAt).format('YYYY-MM-DD'), items: [current]});
}
}
//五、算总和
result.map(group => {
group.total = group.items.reduce((sum, item) => {
console.log(sum);
console.log(item);
return sum + item.amount;
}, 0);
});
return result;
}

三、完整代码:src/views/Statistics.vue

<template>
<Layout>
<Tabs class-prefix="type" :data-source="recordTypeList" :value.sync="type"/>
<ol>
<li v-for="(group, index) in groupedList" :key="index">
<h3 class="title">{{beautify(group.title)}} <span>¥{{group.total}}</span></h3>
<ol>
<li v-for="item in group.items" :key="item.id"
class="record"
>
<span>{{tagString(item.tags)}}</span>
<span class="notes">{{item.notes}}</span>
<span>¥{{item.amount}} </span>
</li>
</ol>
</li>
</ol>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import Tabs from '@/components/Tabs.vue';
import recordTypeList from '@/constants/recordTypeList';
import dayjs from 'dayjs';
import clone from '@/lib/clone';
@Component({
components: {Tabs},
})
export default class Statistics extends Vue {
tagString(tags: Tag[]) {
if(tags.length === 0) {return '无'}
else{
return tags.map(item=>item.name).join(' ')
}
}
beautify(string: string) {
const day = dayjs(string);
const now = dayjs();
if (day.isSame(now, 'day')) {
return '今天';
} else if (day.isSame(now.subtract(1, 'day'), 'day')) {
console.log('hi');
return '昨天';
} else if (day.isSame(now.subtract(2, 'day'), 'day')) {
return '前天';
} else if (day.isSame(now, 'year')) {
return day.format('M月D日');
} else {
return day.format('YYYY年M月D日');
}
}
get recordList() {
return (this.$store.state as RootState).recordList;
}
get groupedList() {
const {recordList} = this;
if (recordList.length === 0) {return [];}
const newList = clone(recordList)
.filter(r => r.type === this.type)
.sort((a, b) => dayjs(b.createdAt).valueOf() - dayjs(a.createdAt).valueOf());
type Result = { title: string; total?: number;items: RecordItem[] }[]
const result: Result = [{title: dayjs(newList[0].createdAt).format('YYYY-MM-DD'), items: [newList[0]]}];
for (let i = 1; i < newList.length; i++) {
const current = newList[i];
const last = result[result.length - 1];
if (dayjs(last.title).isSame(dayjs(current.createdAt), 'day')) {
last.items.push(current);
} else {
result.push({title: dayjs(current.createdAt).format('YYYY-MM-DD'), items: [current]});
}
}
result.map(group => {
group.total = group.items.reduce((sum, item) => {
console.log(sum);
console.log(item);
return sum + item.amount;
}, 0);
});
return result;
}
beforeCreate() {
this.$store.commit('fetchRecords');
}
type = '-';
recordTypeList = recordTypeList;
}
</script>
<style scoped lang="scss">
@import "~@/assets/style/helper.scss";
::v-deep .type-tabs-item {
background: $color-five;
&.selected {
background: $color-three;
&::after {
display: none;
}
}
}
%item {
padding: 8px 16px;
line-height: 24px;
display: flex;
justify-content: space-between;
align-content: center;
}
.title {
@extend %item;
background: $color-four;
}
.record {
background: white;
@extend %item;
}
.notes {
margin-right: auto;
margin-left: 16px;
color: #999;
}
</style>