用Vue Router实现底部导航
1. 确定页面URL
1.1 编写router
import Vue from 'vue';
import VueRouter, {RouteConfig} from 'vue-router';
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path:'/money',
component:Money
},
const router = new VueRouter({
routes
});
export default router;
1.2 将router写到全局变量里
new Vue({
router: router,
render: h => h(App)
}).$mount('#app');
1.3 在App.vue中用展示router
<template>
<div>
<router-view/> //展示对应路由
<div>
</template>
1.4 在Nav.vue中写对应路由
<template>
<div>
<router-link to="/money">记账</router-link>
|
<router-link to="/labels">标签</router-link>
|
<router-link to="/statistics">统计</router-link>
</div>
</div>
</template>
1.5 分别建4个vue文件(Money、Labels、Statistics、NotFound),分别引入Nav.vue
<template>
<div>money.vue
<Nav/>
<div>
<template>
1.6 将Nav写到全局变量里
Vue.component('Nav',Nav)
2. 给导航栏添加样式
2.1 新增一个存放共同样式的文件Layout.vue
2.2 使用<slot>插槽来自动获取<Layout>里面的内容
Money实现渲染的过程为:先是解析出<Layout>,可以再main.js中找到索引,找到Layout.vue这个文件,它首先是渲染一个<div class="nav-wrapper">,再渲染<div class="content">,接着解析<slot/>,可以把Money.vue里的<Layout>内容<p>Money.vue</p>,直接填到里面.
2.3 引入SVG Icon图标
- 新建
icon.vue,用来封装重复的导航代码
2.4 引入Icon全局组件
2.5 给icon加样式
2.6 路由激活
- 即点击导航栏图标,图标即高亮显示:在需要高亮显示的标签中,加入属性
active-class="x"
第一部分 写Money.vue组件
3. 写Money.vue组件
3.1 写Money.vue的页面
它是由
Tags、Notes、Tags、NumberPad这四个子模块组成的,可以先把这四个子模块写在一起,然后再分模块写
3.2 写Money.vue的SCSS样式
- 全局样式重置
assets/style/reset.scss - 全局样式设置
assets/style/helper.scss,放变量和函数
3.3 写Money.vue的TypeScript
在精简后的Money.vue中,分别再去引入刚才分出去的组件:
<template>
<!-- class前缀-->
<Layout class-prefix="layout">
<!-- 由于scss样式写的column-reverse布局,所以组件由下往上堆叠-->
<NumberPad/>
<Types/>
<Notes/>
<Tags/>
</Layout>
</template>
<script lang="ts">
import Notes from '@/components/Money/Notes.vue';
import NumberPad from '@/components/Money/NumberPad.vue';
import Tags from '@/components/Money/Tags.vue';
import Types from '@/components/Money/Types.vue';
export default {
name: 'Money.vue',
components: {Types, Tags, NumberPad, Notes},
// components: {Nav}
};
</script>
<style lang="scss">
.layout-content {
display: flex;
flex-direction: column-reverse; //从下面往上布局
}
</style>
4. 写Types.vue组件
JS是用构造选项
options来写组件,构造选项没有类型,任意添加东西;
<template>
<div>
<ul class="types">
<li :class="type === '-' && 'selected'"
@click="selectType('-')">支出
</li>
<li :class="type === '+' && 'selected'"
@click="selectType('+')">收入
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Types',
props:['xxx'], //外部接收的属性
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
data(){ //用来保存当前选中的类型,内部属性
return{
type:'-' //'-'表示支出,'+'表示收入
}
},
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
mounted() {
console.log(this.xxx);
},
methods:{ //用于切换 Type 的方法
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
selectType(type){ //type 只能是'-'和'+'中的一个
if(type!=='-' && type !=='+'){
throw new Error('type is unknown')
}
this.type= type
}
}
};
</script>
TS是用class类、装饰器写组件;
<template>
<div>
<ul class="types">
<li :class="type === '-' && 'selected'"
@click="selectType('-')">支出
</li>
<li :class="type === '+' && 'selected'"
@click="selectType('+')">收入
</li>
</ul>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {Component} from 'vue-property-decorator'; //自第三方库引入Component装饰器
//声明以下为 TS 写的组件,type='-'会自动处理为data,selectType会自动处理为methods
@Component //将此装饰器修饰到class上;@Component由vue-class-component官方库提供
export default class Types extends Vue { // TS 写组件的引入
type = '-'
selectType(type: string){ // 此行为 TS 语法
if (type !== '-' && type !== '+'){
throw new Error('type is unknown')
}
this.type = type;
}
}
</script>
5. 写NumberPad.vue组件
5.1 写页面
页面就只有2层结构,1层是输出框output,另一层是14个按钮button,代码为:
<template>
<div class="numberPad">
<div class="output">{{output}}</div>
<div class="buttons">
<button>1</button>
<button>2</button>
<button>3</button>
<button>删除</button>
<button>4</button>
<button>5</button>
<button>6</button>
<button>清空</button>
<button>7</button>
<button>8</button>
<button>9</button>
<button>OK</button>
<button>0</button>
<button>.</button>
</div>
</div>
</template>
5.2 用class类、装饰器写组件
<script lang="ts">
import Vue from 'vue'; //ts行间加分号';'
import {Component} from 'vue-property-decorator';
@Component
export default class NumberPad extends Vue{
}
功能实现。总体的响应是点击14个按钮和输出框显示内容(默认显示字符串0),都会有对应的响应。分为不同的功能实现:
- 数字区0-9,和小数点
.要实现点击每个数字显示在输出框中,且数字区的细节流程控制有不可以输入两次0,两次小数点.,还有输出框的最大输出长度; - 删除,实现每次点击,输出框数字都缩进1个数字;
- 清空,实现点击后,数据清空显示
0; - ok,实现记账后,把数据提交到本地存储,功能后面实现。
事件绑定有:@click、inputContent
6. 数据处理:收集四个组件的value
需求是把用户填的东西,包括新增标签、备注、支出或收入下的金额输入信息,收集起来,单击ok键的时候,变成一个大的对象。
6.1 收集tags组件的value
- 怎么知道用户选中的哪个tag呢?比如监听它的一个事件(@update:value),触发这个事件时,就得到要收集的
value,再把这个value放在对应的方法(onUpdateTags)上。
//Money.vue
<template>
<Layout class-prefix="layout">
<NumberPad/>
<Types/>
<Notes/>
<Tags :data-source.sync="tags" @update="onUpdateTags"/> //监听xxx事件
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import NumberPad from '@/components/Money/NumberPad.vue';
import Types from '@/components/Money/Types.vue';
import Notes from '@/components/Money/Notes.vue';
import Tags from '@/components/Money/Tags.vue';
import {Component} from 'vue-property-decorator';
@Component({
components: {Tags, Notes, Types, NumberPad}
})
export default class Money extends Vue {
tags = ['餐饮', '购物', '交通', '娱乐', '医疗'];
yyy() { //获得内容放在yyy方法中
}
};
</script>
6.2 收集Notes、Types、NumberPad组件的value
- 先完成获取用户输入的
value值的事件绑定:
//Money.vue
<NumberPad @update:value="onUpdateAmount"/> //触发update:value事件
<Types @update:value="onUpdateType"/> //触发update:value事件
<Notes @update:value="onUpdateNotes"/> //触发update:value事件
...
onUpdateAmount(value:string) { //获得内容放在onUpdateAmount方法中
}
onUpdateType(value:string) { //获得内容放在onUpdateType方法中
}
onUpdateNotes(value:string) { //获得内容放在onUpdateNotes方法中
}
- 写对应的方法:
//Notes.vue
<script lang="ts">
import Vue from 'vue';
import {Component, Watch} from 'vue-property-decorator';
@Component
export default class Notes extends Vue {
value = '';
@Watch('value') //Watch适用于发生变化时触发事件
onValueChange(value: string, oldValue: string) {
this.$emit('update:value', value);
}
}
</script>
//Types.vue
<script lang="ts">
import Vue from 'vue';
import {Component, Prop,Watch} from 'vue-property-decorator';
@Component
export default class Types extends Vue {
type = '-';
@Prop(Number) xxx: number | undefined
selectType(type: string) {
if (type !== '-' && type !== '+') {
throw new Error('type is unknown');
}
this.type = type;
}
@Watch('type')
onTypeChange(value:string){
this.$emit('update:value',value)
}
}
</script>
6.3 把value收集到一个对象中
收集到的value命名为record,并做类型声明,写对应的方法:
//Money.vue
<script lang="ts">
import Vue from 'vue';
import NumberPad from '@/components/Money/NumberPad.vue';
import Types from '@/components/Money/Types.vue';
import Notes from '@/components/Money/Notes.vue';
import Tags from '@/components/Money/Tags.vue';
import {Component} from 'vue-property-decorator';
type Record = { //TS类型声明只写类型,JS类型声明写具体内容
tags: string[]
notes: string
type: string
amount: number
}
@Component(
{
components: {Tags, Notes, Types, NumberPad}
})
export default class Money extends Vue {
tags = ['餐饮', '购物', '交通', '娱乐', '医疗'];
record: Record = {
tags: [], notes: '', type: '-', amount: 0
};
onUpdateAmount(value: string) {
this.record.amount = parseFloat(value);
}
onUpdateType(value: string) {
this.record.type = value;
}
onUpdateNotes(value: string) {
this.record.notes = value;
}
onUpdateTags(value: string[]) {
this.record.tags = value;
}
};
</script>
6.4 使用LocalStorage 保存用户数据
初步实现记账,不能接续排列为一个数组:
在用户点击ok时,把数据放到
LocalStorage,绑定一个@submit事件,并监听,提交时触发saveRecord方法,在NumberPad.vue组件中添加ok的saveRecord方法,此步骤为初步实现记一笔账,点击ok提交后,就把这条账目推到数组中,每次都是保存相同的内存地址,将上一次的覆盖掉。
//NumberPad.vue
ok() {
this.$emit('update:value', this.output);
this.$emit('submit', this.output);
this.output = '0';
}
//Money.vue
export default class Money extends Vue {
tags = ['餐饮', '购物', '交通', '娱乐', '医疗'];
recordList: Record[] = []; //创建一个recordList,类型是Record数组,默认为空数组
record: Record = {
tags: [], notes: '', type: '-', amount: 0
};
saveRecord() {
this.recordList.push(this.record); //触发saveRecord方法时,把this.record推到recordList数组里
}
@Watch('recordlist')
onRecordListChange(){
window.LocalStorage.setItem('recordList',JSON.stringfy(this.recordList));
// JSON.stringfy() 用于从一个对象解析出字符串
}
用深拷贝的方法实现接续列出数组的每次push数据,把 record 的复制保存到recordlist,List初始化时,不能赋值为空数组,要赋值,然后用插入语法在html页面中引入{{recordList}}:
recordList: Record[] = JSON.parse(window.localStorage.getItem('recordList') || '[]');
saveRecord(){
const record2 = JSON.parse(JSON.stringify(this.record));
// JSON.parse() 用于从一个字符串中解析出json对象
this.recordList.push(record2);
}
7. MVC重构
所有数据层面的集中到一个Model文件,所有视图View放在一起。
- 视图层面View,也就是每个组件中
<template>的内容,用于页面渲染; - 控制层面Control,也就是所有的方法API接口,如何更新数据等;
- 数据层面Model,都分散在各处,没有集中到一起:
//数据层面
//初始化数据,把数据从数据库中拿出来
const recordList:Record[] = JSON.parse(window.localStorage.getItem('recordList')||'[]');
//更新数据,往数据库中添加
this.recordList.push(record2);
//保存数据
window.localStorage.setItem('recordList',JSON.stringify(this.recordList));
7.1 用JS重构数据层面的东西 model.js
- 新建
src/model.js(先尝试用js写数据,然后在组件中引用):
const model = {
fetch(){ //获取数据
return JSON.parse(window.localStorage.getItem('recordList')||'[]');
},
save(){ //保存数据
window.localStorage.setItem('recordList',JSON.stringify(this.recordList));
},
//可写成传参data的形式
save(data){ //保存数据,传参data,序列化以后放在recordList里,
window.localStorage.setItem('recordList',JSON.stringify(data));
},
//把recordList定义为一个常量
const localStorageKeyName = 'recordList';
}
export default model;
- 在
Money.vue中引入model,有两种写法,一种是:
//model.js
export default model
//Money.vue
const model = require('@/model.js').default;
- 另一种写法是具名导出,到导入的时候就不需要加
.default了,而是直接写model;还有一种写法更屌,就是用析构语法,连后面的model都省了:
//model.js
export {model}
//Money.vue
const model = require('@/model.js').model;
const {model} = require('@/model.js')
- 然后获取数据:
const recordList:Record[] = model.fetch();
7.2 用TS重构数据层面的东西 mdoel.ts
//Money.vue
const recordList: Record[] = [] //recordList是一个大对象,收纳Record的所有内容
type Record = { //声明Record的类型
tags:string[] //最上部的标签,每次新增都push到数组中
notes:string //备注内容
type:string //支出 or 收入的类型
amount:number //数字区输出的结果
createAt?:Date
}
//model.ts
const localStorageKeyName = 'recordList';
const model = {
fetch(){ //获取数据
return JSON.parse(window.localStorage.getItem('recordList')||'[]');
},
save(data:Record[]){ //
window.localStorage.setItem('recordList',JSON.stringify(data));
},
//把recordList定义为一个常量
- 声明
data数据类型为Record:改完ts类型以后,save(data)中的data会报错,ts要声明数据类型,也就是一个大对象,里面是空数组。 - 再去声明
Record的类型:由于Record是默认就有的全局变量类型(已经被占用了),需要重定向为别的名:RecordItem,然后去根目录创建一个custom.d.ts自定义全局声明:
type RecordItem = { //TS类型声明只写类型,JS类型声明写具体内容
tags: string[]
notes: string
type: string
amount: number
createdAt?: Date
}
- 声明
fetch()的返回值类型,用断言语句as强制类型声明为RecordItem数组:
fetch(){
const x: RecordItem[] = JSON.parse(window.localStorage.getItem('recordList')||[]);
return x
//可简写为
return JSON.parse(window.localStorage.getItem('recordList')||[]) as RecordItem[];
}
- 直接就可以在组件中处理数据了:
//Money.vue
const recordList = model.fetch(); //类型可以直接省略了,因为已经全局声明了类型
第二部分 写Label.vue组件
8.1 写Label.vue的HTML页面
<template>
<Layout>
<ol>
<li><span>衣 </span><Icon name="right"></li>
<li><span>食 </span><Icon name="right"></li>
<li><span>住 </span><Icon name="right"></li>
<li><span>行 </span><Icon name="right"></li>
<ol>
<div>
<button>新建标签</button>
</div>
<Layout>
</template>
8.2 写Label.vue的SCSS样式
8.3 写新建标签功能:
- 实现的功能与之前的
model.ts不同,是用来处理Label.vue的数据文件。新建model文件夹,把原来的model.ts重构名为recordListModel.ts,再单独新建tagsListModel.ts
//tagsListModel.ts
const localStorageKeyName = 'tagList';
const tagListModel = {
fetch(){ //获取数据
return JSON.parse(window.localStorage.getItem('tagList')||'[]');
},
save(data:Record[]){ //
window.localStorage.setItem('tagList',JSON.stringify(data));
},
//把recordList定义为一个常量
export default tagListmodel;
- 在组件中引用:
//Money.vue
const recordList = recordListModel.fetch();
const tagList = tagListModel.fetch();
export default class Money extends Vue{
tags:tagList;
recordList:RecordItem[] = recordList;
record:RecordItm[] = {
tags:[], notes:'', type:'-', amount:0
};
}
//Labels.vue
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel';
const tags = tagListModel.fetch();
@Component
export default class Money extends Vue{
}
- 删掉页面中的固定的内容:
<template>
<Layout>
<ol>
<li v-for:"tag in tags" :key="tag">
<span>{{tag}} </span>
<Icon name="right"></li>
<ol>
<div>
<button class="createTag" @click="createTag">
新建标签</button>
</div>
<Layout>
</template>
...
tagListModel.fetch();
@Component
export default class Money extends Vue{
tags = tagListModel.data;
createTag(){
const name = window.prompt('请输入标签名');
if(name){
tagListModel.create(name)
}
}
//tagListModel.ts
const localStorageKeyName = 'tagList';
type TagListModel {
data:string[]
fetch:()=>string[]
create:(name:string)=>string
save:()=>void
}
const tagListModel:TagListModel = {
data:[],
fetch(){ //获取数据
return JSON.parse(window.localStorage.getItem('tagList')||'[]');
},
create(name){
this.data.push(name)
this.save();
return name;
}
save(){
window.localStorage.setItem('tagList',JSON.stringify(this.data));
},
export default tagListmodel;
8.4 写删除标签功能:
- 新建一个路由,获取页面的id:
router/index.ts
import EditLabel from '@/views/EditLabel.vue';
{
path:'/labels/edit/:id', //占位符
component:EditLabel
}
-
写
EditLabel的页面: -
用VueRouter实现EditLabel获取路由path的id内容:
create(){
const id = this.$route.params.id; //获取路由path的id内容
tagListModel.fetch();
const tags = tagListModel.data;
const tag = tags.filter(t=>t.id === id)[0]
if(tag){
}else{
this.$router.replace('/404');
}
}
- 封装通用组件,因为EditLabel.vue和Money.vue两个组件都要用到Notes,请输入备注信息,这个组件,也就可以把Notes这个组件封装一下,变为通用组件
FormItem.vue:
第三部分 全局数据管理之【手写store】
背景是Money.vue和Labels.vue要实现同步数据显示,其优点如下:
- 解耦:将所有数据相关的逻辑放入 store(也就是 MVC 中的 Model)
- 数据读写更方便:任何组件不管在哪里,都可以直接读写数据
- 控制力更强:组件对数据的读写只能使用 store 提供的 API 进行
9. 手写store的实现:
- 再次封装recordListModel
- 用window来容纳数据
- 用window来封装API
- 消除对window的依赖
- 将model融合进store
第四部分 全局数据管理之【Vuex】
- 用Vuex重构组件
第五部分 写统计页面
- 页面效果图:由两个tab标签和数据结构组成
5.1 首先是第一个type组件,支出收入在Money页面做过这个,可以拿来用;
- 具体的样式需要使用deep语法
::v-deep(可被sass识别)或/deep/; - 通过
classPrefix来灵活定义类; 原来的写法为:
<li :class="value ==='-' && 'selected'"
@click="selectType('-')">支出
</li>
改为对象的形式,即为表驱动,把所有的类都用表的形式写下来,如果为true,那就要这个class:
<li :class="{selected:value ==='-'}"
</li>
再次写为完整格式:
<li :class="{'xxx-item':classPrefix, selected:value ==='-'}"
</li>
还可以写成ES6的新语法,如果key('xxx-item')里面有变量,就用中括号把这个key包起来:
<li :class="{[classPrefix+'-item']:classPrefix, selected:value ==='-'}"
</li>
引用的组件Statistics.vue写为:
<Types class-prefix="xxx" :value.sync="yyy">
得到的选中元素,就是li.xxx-item.selected,样式选择器就可以写为:::v-deep .xxx-item
5.2 第二个type组件
//Staistics.vue
<Types class-prefix="zzz" :value.sync="yyy">
<Types class-prefix="zzz3" :value.sync="yyy">
-这时候就重复使用相似组件了,可以新建一个tabs.vue组件,先来写实现按天 按周 按月type效果:
<template>
<ul class="types">
<li></li>
</ul>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
@Component
export default class Tabs extends Vue{
@Prop({required:true, type:Array})
dataSource!:{text:string, value:string}[]
}
ISO 8601 日期时间表示法
- 屎一样的Date( ) API:一般人不用
var d = new Date()
d.toISOString() // '2022-08-19T07:45:15.081Z' 零时区标准时间
Date.parse('2022-08-19T07:45:15.081Z') // 1660895115081
new Date(1660895115081) // Fri Aug 19 2022 15:45:15 GMT+0800 (GMT+08:00)
-
moment.js:体积16k过大,也不常用
-
day.js:轻量化的moment.js,常用
dayjs()
.startOf('month')
.add(1, 'day')
.set('year', 2018)
.format('YYYY-MM-DD HH:mm:ss')