[发财账簿] V1.0 开发日志
前言
简单的记录一下在发财账簿开发过程中所用的和遇到的一些技术和问题。
这一版的发财账簿RichAccount是由Vue2+TypeScript+SCSS编写的SPA应用,同时主要数据都存放在LocalStorage中,暂无任何线上功能。
主要功能:
-
记账的标签、备注、时间
-
新增和删除标签
-
记录按日统计
-
图表呈现
-
……
链接
发财账簿-Pixso
源码仓库-GitHub
预览链接
主要运用的技术
开发思路
(指截至文章撰写时,后续小改动不再更新。)最后更新:2022-07-19
-
底部的导航栏导航到不同的页面
-
新增记账时可以选择标签、填写备注、选择时间
-
标签可以新增、改名、删除
-
记录可以查看、按日期统计、通过图表呈现
Vue Rooter 使用
主要逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const routes: Array<RouteConfig> = [ { path: '/', redirect: '/money', }, { path: '/money', component: Money, }, { path: '/labels', component: Labels, }, { path: '/statistics', component: Statistics, }, { path: '/labels/edit/:id', component: EditLabel, }, { path: '*', component: notFound, }, ];
|
一个类似于select case的结构,Vue Router会从上至下依次匹配
使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <nav> <router-link to="/labels" class="item" active-class="selected"> 标签 </router-link>
<router-link to="/money" class="item" active-class="selected"> 记账 </router-link>
<router-link to="/statistics" class="item" active-class="selected"> 统计 </router-link> </nav> </template>
|
在template中通过router-link标签来实现跳转
获取id
1 2 3 4 5 6 7 8 9 10
| { path: '/labels/edit/:id', component: EditLabel, },
created() { const id = this.$route.params.id; }
|
通过id可以精确指向到对应的标签
用TS跳转
1 2 3 4 5
| this.$router.back();
this.$router.replace('/404');
|
这里要注意, **.back()
**相当于浏览器中的后退,浏览器会返回到上一个页面,可能是别的网站
Vuex 使用
Vuex在本项目中主要用于做全局数据管理,好处是所有组件的数据都是同步的、统一的,所有操作数据的方法也都是在Vuex中声明的,规范且统一
Vue会自动将@/store/index.ts
中的store作为$store
挂载到当前的Vue实例(也就是this)上,我们可以通过this.$store
访问到Vuex中的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const store = new Vuex.Store({ state: { recordList: [], tagList: [], currentTag: undefined } as RootState, mutations: { createRecord(state, record: RecordItem) { ... store.commit('saveRecords'); }, ... createTag(state, name: string) { ... store.commit('saveTags'); }, ... updateTag(state, {id, name}: { id: string, name: string }) { ... }, } });
|
Vuex中使用到了两个概念,state
和mutations
,其实对应的是Vue实例中的data
和methods
注意点:
-
在store中,Vuex会给所有的mutations
传一个state
参数,通过state
来访问到store中的数据
-
mutations
只接受两个参数,state
和参数2
,如果想要传多个参数,需要用对象的形式将多个参数合并为一个对象,这个对象被称之为payload
-
有时候我们会需要在一个mutation中调用另一个mutation方法,这时需要使用store.commit('方法名', 参数)
在组件中使用Vuex
1 2 3 4 5 6 7 8 9 10 11 12
| get recordList() { return this.$store.state.recordList; }
created() { this.$store.commit('fetchRecords'); }
saveRecord() { this.$store.commit('createRecord', this.record); ... }
|
装饰器 vue-property-decorator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script lang="ts"> import Vue from 'vue'; import {Component, Prop, Watch} from 'vue-property-decorator';
...
@Component export default class Chart extends Vue { @Prop() options?: EChartsOption;
@Watch('options', {immediate: true}) onOptionsChanged(newValue: EChartsOption) { ... } get recordList() { return (this.$store.state as RootState).recordList; } } </script>
|
在线SVG symbols
在本项目中,我主要使用的是SVG图片,通过在线导入的方式引入,没有在本地保存SVG图片。
1 2 3 4 5 6 7 8 9
| <body>
<script src="IconPark生成的js链接"></script>
<svg class="icon"> <use href="#consume"></use> </svg> </body>
|
Echarts使用
通过Echarts来做数据可视化。我封装了一个Chart.vue组件,从外部接受一个option
,然后渲染图表
本项目中使用的是折线图,展示从今天起往前推30天的每日收入/支出情况,同时将图表的样式根据我的实际需要进行了调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <template> <div class="wrapper" ref="wrapper">chart</div> </template>
<script lang="ts"> import Vue from 'vue'; import {Component, Prop, Watch} from 'vue-property-decorator'; import * as echarts from 'echarts';
type EChartsOption = echarts.EChartsOption;
@Component export default class Chart extends Vue { @Prop() options?: EChartsOption;
@Watch('options', {immediate: true}) onOptionsChanged(newValue: EChartsOption) { setTimeout(() => { echarts.init(this.$refs.wrapper as HTMLDivElement).setOption(newValue); }, 0); } } </script>
<style lang="scss" scoped> .wrapper { height: 400px; } </style>
|
dayjs使用
在本项目中,因为要对时间进行记录和格式化,因此选择了更为好用的dayjs取代原生的Date()
api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| formatDate(isoString: string) { return dayjs(isoString).format('YYYY-MM-DD'); }
beautify(date: string) { const day = dayjs(date); const now = dayjs(); if (day.isSame(now, 'day')) { return '今天'; } else if (day.isSame(now.subtract(1, 'day'), 'day')) { return '昨天'; } else if (day.isSame(now.subtract(2, 'day'), 'day')) { return '前天'; } else { return day.format('YYYY年M月D日'); } }
|
SCSS使用
因为组件比较多,为了更好的管理样式,本次项目使用了SCSS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @import '~@/assets/style/helper.scss'; //引入变量
%item { ... }
.title { @extend %item; //@extend 语法 }
.noResultWrapper { ... > .noResult { // '>' 操作符可以获取子元素 ... $color-noResult: #bbbbbb; color: $color-noResult; background: darken($color-noResult, 8%); //通过darken加深颜色 } }
::v-deep { .type-tabs-item { ...
&.selected { // '&' 操作符,复制自己,这里相当于 .type-tabs-item.selected ...
&-wrapper { //可以选中父元素 ... } } } }
|
遇到的一些小问题
两位小数
记账时只需要记录两位小数,输入一个两位小数后应该无法继续输入
我采取的方法是在每次输入数字时,检查小数点的位置,如果小数点是倒数第三个,说明已经是一个两位小数了
1
| if (this.output.indexOf('.') === this.output.length - 3 ) { return; }
|
出现了一个bug,只能输入两位数了,因为在没有输入小数点时,this.output.indexOf('.') === -1
,所以添加一个条件 this.output.indexOf('.') ≥ 0
1 2 3 4 5 6
| if ( this.output.indexOf('.') === this.output.length - 3 && this.output.indexOf('.') >= 0 ) { return; }
|
更新图表
第一版是在chart挂载时就先初始化,然后监听options的变化,但是两处代码重复,考虑使用watch
的immediate
参数进行修改
1 2 3 4 5 6 7 8 9 10
| mounted() { if (this.options === undefined) {return;} const chart = echarts.init(this.$refs.wrapper as HTMLDivElement); chart.setOption(this.options); }
@Watch('options') echarts.init(this.$refs.wrapper as HTMLDivElement).setOption(newValue); }
|
报错,提示setOption of undefined
,后续通过在mounted()
和@Watch
中进行log,发现@Watch
的时机比mounted()
更早,因此想到使用setTimeout()
方法
1 2 3 4 5
| @Watch('options',{immediate:true}) echarts.init(this.$refs.wrapper as HTMLDivElement).setOption(newValue); }
|
成功!
1 2 3 4 5 6 7
| @Watch('options', {immediate: true}) onOptionsChanged(newValue: EChartsOption) { setTimeout(() => { echarts.init(this.$refs.wrapper as HTMLDivElement).setOption(newValue); }, 0); }
|
倒序数组
在为图表准备数据的过程中,需要用到桶排序,将所有的记录通过时间分类,将日期和金额存在一个数组内,但是生成的是一个日期从现在到过去的数组
1 2 3 4 5 6 7 8 9 10 11 12 13
| get chartArray() { const today = new Date(); const array = []; for (let i = 0; i <= 29; i++) { const dateString = dayjs(today).subtract(i, 'day'); const found = _.find(this.groupedList, {title: dateString.format('YYYY-MM-DD')}); array.push({ key: dateString, value: found ? found.total : 0 }); } return array }
|
首先想到的是将数组重新排序,过去在前,现在在后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| get chartArray() { const today = new Date(); const array = []; for (let i = 0; i <= 29; i++) { const dateString = day(today).subtract(i, 'day').format('YYYY-MM-DD'); const found = _.find(this.groupedList, { title: dateString }); array.push({ key: dateString, value: found ? found.total : 0 }); } array.sort((a, b) => { if (a.key > b.key) { return 1; } else if (a.key === b.key) { return 0; } else { return -1; } }); return array; }
|
后来又重新思考,可以让i递减,从29减到0,精简了代码,于是做出了最终版,同样的效果,更少的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| get chartArray() { const today = new Date(); const array = []; for (let i = 29; i >= 0; i--) { const dateString = dayjs(today).subtract(i, 'day'); const found = _.find(this.groupedList, {title: dateString.format('YYYY-MM-DD')}); array.push({ key: dateString, value: found ? found.total : 0 }); } return array; }
|
后记
本项目还有很多不足之处,后续还会继续调整,如果你发现了什么bug,也可以留言给我哦 (≖ᴗ≖)✧