Labels 组件
写 结构
//Labels.vue
<template>
<Layout>
<ol class="tagList">
<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 class="createTag-wrapper">
<button class="createTag" @click="createTag">新建标签</button>
</div>
</Layout>
</template>
写 样式
//Labels.vue
<template>
<Layout>
<ol class="tags">
<li v-for="tag in tags" :key="tag">
<span>{{tag}}</span>
<Icon name="right"/>
</li>
</ol>
<div class="createTag-wrapper">
<button class="createTag" @click="createTag">新建标签</button>
</div>
</Layout>
</template>
<style scoped lang="scss">
.tags {
background: white;
font-size: 16px;
padding-left: 16px;
> li {
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e6e6e6;
svg {
width: 18px;
height: 18px;
color: #666;
margin-right: 16px;
}
}
}
.createTag {
background: #767676;
color: white;
border-radius: 4px;
border: none;
height: 40px;
padding: 0 16px;
/* &-xx xx为其父元素 */
&-wrapper {
text-align: center;
padding: 16px;
margin-top: 44-16px;
}
}
</style>
写 行为
- 新增的标签存在什么位置?参考之前写的
Money.vue
中写的标签初始值是固定的,如果把新增标签的数据放在tags初始值上,那么刷新时,就会再次出现初始值,并没有体现变化,所以还需要新建一个tagListModel.ts
来存Labels.vue
的新建标签:
// tagListModel.ts
<Tags :data-source.sync="tags" @update:value="onUpdateTags"/>
//tags初始值
tags = ['衣','食','住','行']
- 新建一个初步的model,主体内容保持不变,看能否与之前的model合并:
// src/models/tagListModel.ts
const localStorageKeyName = 'tagList';
const recordListModel = {
fetch() { //获取数据
return JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]') as RecordItem[];
},
save(data: RecordItem[]) {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(data));
}
};
export default recordListModel;
封装之前的Money.vue
中的record初始值
之前MVC时创建的
model.ts
重构为recordListModel.ts
,重构时注意勾选重构的操作步骤为:要重构的文件上单击右键-> 选refactor-> 选rename,勾选
Search for references
选项)
//Money.vue
recordList: RecordItem[] = JSON.parse(window.localStorage.getItem(recordList) || '[]');
//封装recordList为如下代码:
const recordList = recordlist.fetch();
//封装recordList为如下代码:
const tagList = tagListModel.fetch();
//recordListModel.ts
const localStorageKeyName = 'recordList';
const recordListModel = {
...
fetch() {
return JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]') as RecordItem[];
},
...
};
export default recordListModel;
//tagListModel
const localStorageKeyName = 'tagList';
const tagListModel = {
fetch() {
return JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]');
},
...
};
export default tagListModel;
- 之前的初始值改为:
export default class Money extends Vue {
//初始值改为重构后的tagList
tags = tagList;
recordList: RecordItem[] = JSON.parse(window.localStorage.getItem(recordList) || '[]');
record: RecordItem = {
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';
tagListModel.fetch(); //封装获取数据
@Component
export default class Labels extends Vue {
tags = tagListModel.data; //引用data
createTag() {
const name = window.prompt('请输入标签名');
if (name){
tagListModel.create(name) //需要进一步实现封装a
}
}
};
</script>
- 实现上一步的封装a,需要在
tagListModel
中写入create
方法,这就涉及到怎么拿数据,可以在开始时声明data:[]
,但是因为ts需要赋给data
数据类型,而因为data
数据类型为联合类型,就要再声明一下TagListModel
的类型,用tagListModel: TagListModel
,把声明与类型关联起来,从而实现data
完全自己维护,外部要创建和声明,直接调用暴露在外的API即可。
tagListModel
的data完全自己维护,与之前的recordListModel
实现思路不一样:之前的recordListModel
没有保存自己的data,所有的东西需要用参数和返回值的形式来实现。
//tagListModel.ts
const localStorageKeyName = 'tagList';
type TagListModel = { //声明TagListModel类型
data: string[] //声明data为字符串数组
//对于函数声明类型,箭头=>前为输入的参数类型,箭头后为输出结果类型
fetch: () => string[] //声明fetch为函数,返回值为字符串数组
create: (name: string) => string
save: () => void //save函数不返回
}
//tagListModel的实现方法,用冒号:TagListModel,使方法与声明类型关联
const tagListModel: TagListModel = {
data: [],
fetch() { //获取数据
return JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]');
},
create() {
this.data.push(name);
this.save();
return name;
},
save() {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));
}
};
-
以上的代码总结一下:首先
Labels.vue
中的数据,Label是不管的,践行MVC设计模式,所有涉及数据的操作都由Model模块
自己提供,这样data可以直接拿来用,就可以放在tags
里面,以后创建tag时,调用Model来创建,涉及到保存的Label还是不插手,依然由Model模块
去实现,实现了简洁独立的设计模式。 -
避免用户创建标签时,存在相同的名字,在
tagListModel
中实现:
可以叙述遇到的技术难题的解决路径,比如:创建成功要返回什么类型存在疑问:一开始认为只要返回布尔值
true
或false
就行,但是发现失败的情况有很多种,于是考虑用数字来判定,列了一个数字列表从0-100,分别表示什么错误,最后发现文档在运行一段时间以后,根本不记得不同数字对应的是什么错误类型,最终就统一改为字符串了,在字符串里不管字符串有多长,都可以接受,只要把错误描述清楚就行;也用了中文这种特殊字符串来判定。
//声明类型时
create(name: string)=> 'success'| 'duplicated' //联合类型
//方法实现:
create(name: string) {
if (this.data.indexOf(name)>=0){ //如果命名重复
return 'duplicated';
}
this.data.push(name);
this.save();
return 'success';
},
- 对应修改页面元素:
<template>
<Layout>
<ol class="tags">
<li v-for="tag in tags" :key="tag">
<span>{{tag}}</span>
<Icon name="right"/>
</li>
</ol>
<div class="createTag-wrapper">
<button class="createTag" @click="createTag">新建标签</button>
</div>
</Layout>
</template>
写 删除标签项
- 首先,新建一个路由
EditLabel.vue
:
//router/index.ts
path:'/labels/edit',
component:EditLabel
},
- 加底部导航栏,直接加
<Layout>
//EditLabel.vue
<template>
<Layout>
<div>
<Icon name="left"/>
<span>编辑标签</span>
</div>
</Layout>
</template>
- 明确编辑的哪个标签,有个索引
ID
,查看创建标签的代码,因为没有数字的自增长,唯一能保证的是name
不重复,所以可以把name
当做id
,对应地去修改tagListModel.ts
中的内容:
//Labels.vue
createTag() {
const name = window.prompt('请输入标签名');
if (name) {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success'){
window.alert('标签添加成功');
}
}
//tagListModel.ts
type Tag = {
id:string;
name:string;
}
type TagListModel = { //声明TagListModel类型
data: Tag[] //data为字符串数组
fetch: () => Tag[]
create(name: string) {
//this.data = [{id:'1', name:'1'}.{id:'2',name:'2'}]
const names = this.data.map(item => item.name)
if (names.indexOf(name)>=0){ //names里面包含了新建的name,返回重复
return 'duplicated';
}
this.data.push({id:name, name:name});
this.save();
return 'success';
}
- 对应的改
Labels.vue
里面的内容:
//Labels.vue
<template>
<Layout>
<ol class="tags">
<li v-for="tag in tags" :key="tag.id"
<span>{{tag.name}}</span>
<Icon name="right"/>
</li>
</ol>
<div class="createTag-wrapper">
<button class="createTag" @click="createTag">新建标签</button>
</div>
</Layout>
</template>
- 如何获取页面的中的
id=1
?
//router/index.ts
path:'/labels/edit/:id', //意思是edit后面有个字符串但是未知,写个占位符
component:EditLabel
},
EditLabel
怎么知道上面的路由edit
后面的是什么呢? 可以通过vue router
文档,用于收集信息的:
$router
$route
- 添加Vue的两个属性
$router
和$route
,来源为vue.d.ts
,声明了在Vue中添加的属性。
//vue.d.ts
declare module 'vue/types/vue' {
interface Vue {
$router: VueRouter
$route: Route
}
}
//EditLabel.vue
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel';
@Component
export default class EditLabel extends Vue {
created() {
const id = this.$route.params.id; //$route 用于获取路由信息
tagListModel.fetch();
const tags = tagListModel.data;
const tag = tags.filter(tag => tag.id === id)[0];
if (tag) {
console.log(tag);
} else {
this.$router.replace('/404');
}
}
}
</script>
- 再把标签名对应的编辑页面关联起来:用户点击其中一个标签时,跳转到编辑页面,就需要改
Labels.vue
里面的内容,页面中的<li>
改为<router-link>
,外面的<ol>
改为<div>
,同时<router-link>
要有to
属性:
//Labels.vue
<template>
<Layout>
<div class="tags">
<router-link class="tag" v-for="tag in tags" :key="tag.id"
:to="/labels/edit/${tag.id}"
<span>{{tag.name}}</span>
<Icon name="right"/>
</router-link>
</div>
<div class="createTag-wrapper">
<button class="createTag" @click="createTag">新建标签</button>
</div>
</Layout>
</template>
- 对应的样式也改为:
<style scoped lang="scss">
.tags {
background: white;
font-size: 16px;
padding-left: 16px;
> .tag {
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e6e6e6;
svg {
width: 18px;
height: 18px;
color: #666;
margin-right: 16px;
}
}
}
.createTag {
background: #767676;
color: white;
border-radius: 4px;
border: none;
height: 40px;
padding: 0 16px;
&-wrapper { //父元素选择器
text-align: center;
padding: 16px;
margin-top: 44-16px;
}
}
</style>
- 关联建好以后,就可以写编辑页面的内容了:
//EditLabel.vue
<template>
<Layout>
<div>
<Icon name="left"/>
<span>编辑标签</span>
</div>
</Layout>
</template>
- 把
Notes.vue
中用过的input
组件改造为通用组件,可以任意复用到各处:
//EditLabels.vue
<template>
<Layout>
<div>
<Icon name="left"/>
<span>编辑标签</span>
</div>
<Notes/> //引入Notes组件
</Layout>
</template>
//Notes.vue
<template>
<div>
{{value}}
<label class="notes">
<span class="name">{{this.fieldName}}}</span> //改动
<input type="text"
v-model="value"
:placeholder="this.placeholder">
</label>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component, Prop, Watch} from 'vue-property-decorator';
@Component
export default class Notes extends Vue {
value = '';
@Prop({required: true}) fieldName!: string; //改动
@Prop() palceholder?: string;
@Watch('value')
onValueChange(value: string, oldValue: string) {
this.$emit('update:value', value);
}
}
</script>
- 也要把
Money.vue
中引用的Notes
也做一下增加:
<Notes field-name="备注"
placeholder="在这里输入备注"
@update:value="onUpdateNotes"
/>
- 这样就可以在其他任何页面使用这个通用组件了,比如在
EditLabel
中使用:
<template>
<Layout>
<div>
<Icon name="left"/>
<span>编辑标签</span>
</div>
<Notes field-name="标签名" placeholder="请输入标签名"/> //用此直接引用
</Layout>
</template>
- 文件名为
Notes.vue
重构为FormItem.vue
- 类名为
Notes
重构为FormItem
- 全局搜索一下,还有那些地方用到了
Notes
,匹配大小写Match case
,匹配单词Words
以上就实现了通用组件的复用,还可以把按钮复用,也就是把这个button改为通用组件
新建一个组件为Button.vue
:
//component/Button.vue
<template>
<button class="button">新建标签</button>
</template>
<style scoped lang="scss">
.button {
background: #767676;
color: white;
border-radius: 4px;
border: none;
height: 40px;
padding: 0 16px;
&-wrapper { //父元素选择器
text-align: center;
padding: 16px;
margin-top: 44-16px;
}
}
</style>
- 新建标签,用插槽引入,
<slot>
:
//component/Button.vue
<template>
<button class="button">
<slot/>
</button>
</template>
- 把
Label.vue
中的按钮用<Button>引入
:
<div class="createTag-wrapper">
<Button class="createTag"
@click="createTag">新建标签
</Button>
</div>
- 添加
<button>
的点击事件触发<Button>
:
<template>
<button class="button"
@click="$emit('click',$event)">
<slot/>
</button>
</template>
写 EditLabel组件
- 把当前的标签名获取到,FormItem的数据要从外部获取:
//FormItem.vue
@Prop({default:''}) readonly value!:string;
- 查看用到他的地方是否还能用:
//Tag.vue
<li v-for="tag in dataSource" :key="tag.id" //原来是"tag"
:class="{selected:selectedTags.indexOf(tag)>=0}"
@click="toggle(tag)">{{tag.name}} //原来是{{tag}}
</li>
- 因
v-model
会造成理解上的困难,所以把v-model
改为通用写法:
//FormItem.vue
<input type="text"
:value="value"
@change="value = $event.target.value"
:placeholder="this.placeholder">
- 有一个外部数据
value
,把value
的值传给input
,当用户input
进行变更时,不会对value
进行变更,只是把value
的值,通过onValueChange()
这个函数,回传给组件,只负责中转功能,不会对value
进行任何的读写。
//FormItem.vue
<input type="text"
:value="value"
@input="onValueChange($event.target.value)"
:placeholder="this.placeholder">
- 如何传标签名的值:
//EditLabel.vue
<FormItem :value="tag.name" field-name="标签名" placeholder="请输入标签名"/>
tag?:{id:string, name:string} = undefined; //把tag的初始值赋值为undefined
created() {
const id = this.$route.params.id; // 获取到url里面的id
tagListModel.fetch();
const tags = tagListModel.data; //通过id在所有tags里面找到tag
const tag = tags.filter(t => t.id === id)[0]; //把tag赋值到刚开始未定义的tag
if (tag) {
this.tag = tag;
} else {
this.$router.replace('/404');
}
}
- 用户对标签修改,监听
FormItem
的value
事件:
//EditLabel.vue
<div class="form-wrapper">
<FormItem :value="tag.name"
@update:value="updateTag" //监听
field-name="标签名" placeholder="请输入标签名"/>
</div>
- 通过
tagListModel.data
拿到tags
,
//tagListModel.ts
type TagListModel = {
data: Tag[]
fetch: () => Tag[]
create: (name: string) => 'success' | 'duplicated' //重复
update:(id:string, name:string)=> 'success'| 'not found' |'duplicated'
save: () => void
}
update(id, name) {
const idList = this.data.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const names = this.data.map(item => item.name);
if (names.indexOf(name) >= 0) {
return 'duplicated';
} else {
const tag = this.data.filter(item => item.id === id)[0];
tag.name = name;
this.save();
return 'success';
}
} else {
return 'not found';
}
},
- 对应的改
EditLabel.vue
内容:
update(name:string){
if (this.tag){
tagListModel.update(this.tag.id, name)
}
}
- 实现删除功能:
//EditLabel.vue
<div class="button-wrapper">
<Button @click="remove">删除标签</Button>
</div>
update(id, name) {
const idList = this.data.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const names = this.data.map(item => item.name);
if (names.indexOf(name) >= 0) {
return 'duplicated';
} else {
const tag = this.data.filter(item => item.id === id)[0];
tag.name = tag.id = name;
this.save();
return 'success';
}
} else {
return 'not found';
}
},
remove(){
if (this.tag){
tagListModel.remove(this.tag.id)
}
}
//tagListModel.ts
type TagListModel = {
remove: (id: string) => boolean;
}
remove(id: string) {
let index = -1;
for (let i = 0; i < this.data.length; i++) {
if (this.data[i].id === id){
index = i;
break;
}
}
this.data.splice(index, 1);
this.save();
return true;
},
- 实现返回按钮的功能:
//EditLabel.vue
<Icon class="leftIcon" name="left" @click="goBack"/>
goBack() {
this.$router.back();
}
//Icon.vue
<svg class="icon" @click="$emit('click',$event)">
- 写ID生成器
// lib/createdId.ts
let id: number = parseInt(window.localStorage.getItem('_idMax') || '0');
function createId() {
id++;
return id;
}
export default createId;
//tagListModel.ts
create(name: string) {
const names = this.data.map(item => item.name);
if (names.indexOf(name) >= 0) {
return 'duplicated';
}
const id = createId().toString(); //新增
this.data.push({id, name: name}); //新增
this.save();
return 'success';
},
目前项目中实现标签动态的删减,与记账页面的标签显示不同步,因为两个组件都分别做了一次
fetch、window.localStorage.getItem
,解决此问题,需要先把两个存数据的Model统一一下。
const recordList = recordListModel.fetch();
const tagList = tagListModel.fetch();
- 重新封装
recordListModel.ts
,重新封装create
方法,同时封装clone
方法(在lib目录下新建clone.ts
):
//src/lib/clone.ts
function clone(data:any){
return JSON.parse(JSON.stringify(data));
}
export default clone;
//recordListModel.ts
import clone from '@/lib/clone';
const localStorageKeyName = 'recordList';
const recordListModel = {
data: [] as RecordItem[],
create(record:RecordItem) {
const record2: RecordItem = clone(record);
record2.createdAt = new Date();
this.data.push(record2);
},
fetch() {
this.data = JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]');
return this.data;
},
save() {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));
}
};
export default recordListModel;
- 使用封装好的API,对数据的任何操作都可以用通过
recordListModel
来搞定:
const recordList = recordListModel.fetch();
saveRecord() {
recordListModel.create(this.record)
}
onRecordListChange() {
recordListModel.save();
}
全局数据管理
解决标签显示不同步问题,需要先把两个存数据的Model统一一下,找到
Money.vue
和Labels.vue
的共同上一级组件:App.vue
,或者说提到main.ts
用于渲染app的这层,在此处获取到数据,在此之前需要去custom.d.ts
中提前声明:
//custom.d.ts
type RecordItem = { //TS类型声明只写类型,JS类型声明写具体内容
tags: string[]
notes: string
type: string
amount: number
createdAt?: Date
}
type Tag = {
id: string;
name: string;
}
type TagListModel = {
data: Tag[]
fetch: () => Tag[]
create: (name: string) => 'success' | 'duplicated'
update: (id: string, name: string) => 'success' | 'not found' | 'duplicated'
remove: (id: string) => boolean;
save: () => void
}
interface Window { //声明window属性
tagList: Tag[];
}
- 声明全局变量,用window来容纳数据:
//main.ts
import tagListModel from '@/models/tagListModel';
window.tagList = tagListModel.fetch();
- 分别直接从window上拿到数据:
//Money.vue
export default class Money extends Vue {
tags = window.tagList;
//Labels.vue
export default class Labels extends Vue {
tags = window.tagList;
- 把
create
也封装一下,在此之前需要去custom.d.ts
中提前声明:
interface Window { //声明window属性
tagList: Tag[];
createTag:(name:string)=>void
}
- 声明全局变量,用window来容纳数据:
//main.ts
window.createTag = (name:string)=>{
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success'){
window.alert('标签添加成功');
}
}
- 使用这个API:
//Labels.vue
createTag() {
const name = window.prompt('请输入标签名');
if (name) {
window.createTag(name);
}
}
- 继续封装
remove
、update
:
//custom.d.ts
interface Window { //声明window属性
tagList: Tag[];
createTag:(name:string)=>void
removeTag:(id:string)=>boolean
updateTag: (id: string, name: string) => 'success' | 'not found' | 'duplicated'
}
//main.ts
window.removeTag = (id: string) => {
return tagListModel.remove(id);
};
window.updateTag = (id:string, name:string)=>{
return tagListModel.update(id, name);
}
封装record
//custom.d.ts
interface Window {
recordList:RecordItem[];
createRecord:(record:RecordItem)=>void
}
//main.ts
// record store
window.recordList = recordListModel.fetch();
window.createRecord = (record: RecordItem) => recordListModel.create(record);
// tag store
window.tagList = tagListModel.fetch();
window.findTag = (id: string) => {
return window.tagList.filter(t => t.id === id)[0];
};
window.createTag = (name: string) => {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('标签添加成功');
}
};
window.removeTag = (id: string) => {
return tagListModel.remove(id);
};
window.updateTag = (id: string, name: string) => {
return tagListModel.update(id, name);
};
//Money.vue
saveRecord() {
window.createRecord(this.record)
}
-
目前存在的问题一是用了过多的全局变量,二是严重依赖window
-
修改注释的样式:
- 先把过多的全局变量挂到一个对象上,来解决用了过多的全局变量,先声明类型:
//custom.d.ts
interface Window { //声明window属性
store:{
tagList: Tag[];
createTag: (name: string) => void
findTag:(id:string)=> Tag | undefined;
removeTag: (id: string) => boolean
updateTag: (id: string, name: string) => 'success' | 'not found' | 'duplicated'
//也可以写为 updateTag: TagListModel['update']
recordList:RecordItem[];
createRecord:(record:RecordItem)=>void
}
}
- 在
main.ts
中把全局变量都挂在store
上:
window.store = {
// record store
recordList: recordListModel.fetch(),
createRecord: (record: RecordItem) => recordListModel.create(record),
// tag store
tagList: tagListModel.fetch(),
findTag(id: string) {
return this.tagList.filter(t => t.id === id)[0];
},
createTag: (name: string) => {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('标签添加成功');
}
},
removeTag: (id: string) => {
return tagListModel.remove(id);
},
updateTag: (id: string, name: string) => {
return tagListModel.update(id, name);
}
};
- 再解决严重依赖window:
- 新建一个
index2.ts
在store
文件夹下,把刚才的main.ts
中的store内容都粘贴到index2.ts
里,然后把window删掉:
//index2.ts
import recordListModel from '@/models/recordListModel';
import tagListModel from '@/models/tagListModel';
const store = {
// record store
recordList: recordListModel.fetch(),
createRecord: (record: RecordItem) => recordListModel.create(record),
// tag store
tagList: tagListModel.fetch(),
findTag(id: string) {
return this.tagList.filter(t => t.id === id)[0];
},
createTag: (name: string) => {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('标签添加成功');
}
},
removeTag: (id: string) => {
return tagListModel.remove(id);
},
updateTag: (id: string, name: string) => {
return tagListModel.update(id, name);
}
};
export default store;
- 此时,任何想用数据的就可以直接用,比如在
Money.vue
或其他组件中使用数据:
//Money.vue
import store from '@/store/index2';
export default class Money extends Vue {
tags = store.tagList;
recordList = store.recordList;
saveRecord() {
store.createRecord(this.record)
}
//Labels.vue
tags = store.tagList;
createTag() {
const name = window.prompt('请输入标签名');
if (name) {
store.createTag(name);
}
}
//EditLabels.vue
tag = store.findTag(this.$route.params.id);
created() {
this.tag = store.findTag(this.$route.params.id);
if (!this.tag) {
this.$router.replace('404');
}
}
update(name: string) {
if (this.tag) {
store.updateTag(this.tag.id, name);
}
}
remove() {
if (this.tag) {
if (store.removeTag(this.tag.id)) {
this.$router.back();
} else {
window.alert('删除失败');
}
}
}
手写store
来实现数据集中管理
- 继续优化
store
,index2实际上分两个部分,一部分是recordList
,另一部分是tagList
,可以分为两个文件recordStore.ts
和tagStore.ts
:
//recordStore.ts
import recordListModel from '@/models/recordListModel';
export default {
// record store
recordList: recordListModel.fetch(),
createRecord: (record: RecordItem) => recordListModel.create(record),
}
//tagStore.ts
import tagListModel from '@/models/tagListModel';
export default {
// tag store
tagList: tagListModel.fetch(),
findTag(id: string) {
return this.tagList.filter(t => t.id === id)[0];
},
createTag: (name: string) => {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('标签添加成功');
}
},
removeTag: (id: string) => {
return tagListModel.remove(id);
},
updateTag: (id: string, name: string) => {
return tagListModel.update(id, name);
}
};
- 如何在
index2.ts
中引入上面的两个对象:
import recordStore from '@/store/recordStore';
import tagStore from '@/store/tagStore';
const store = {
...recordStore, //浅拷贝(只复制地址)、模块化
...tagStore, //浅拷贝(只复制地址)、模块化
};
export default store;
- 将 model 融合进 store,把共性的功能合并,直接把model中的代码拷贝到store中,合并后,就可以删掉原model文件.
- 先合并record:
//recordStore.ts
import clone from '@/lib/clone';
const localStorageKeyName = 'recordList';
let data: RecordItem[] | undefined = undefined;
function fetch() {
data = JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]') as RecordItem[];
return data;
}
function save() {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(data));
}
export default {
fetch: fetch,
save: save,
recordList: fetch(),
createRecord: (record: RecordItem) => {
const record2: RecordItem = clone(record);
record2.createdAt = new Date();
data && data.push(record2);
save();
},
};
- 进一步封装上面的代码:
//recordStore.ts
import clone from '@/lib/clone';
const localStorageKeyName = 'recordList';
let data: RecordItem[] | undefined = undefined;
const recordStore = {
fetch() {
data = JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]') as RecordItem[];
return data;
},
save() {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(data));
},
recordList: data,
createRecord: (record: RecordItem) => {
const record2: RecordItem = clone(record);
record2.createdAt = new Date();
data && data.push(record2);
recordStore.save();
},
};
recordStore.fetch();
export default recordStore;
-
搜索引用此
recordStore.ts
的index2.ts
引用了recordStore
的fetch
、save
属性,可能与另外的数据可能也使用fetch
、save
相冲突,所以需要重命名为fetchRecord
、saveRecord
-
再合并tag:
//tagStore.ts
import createId from '@/lib/createId';
const localStorageKeyName = 'tagList';
const tagStore = {
tagList: [] as Tag[],
fetchTags() {
this.tagList = JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]');
return this.tagList;
},
findTag(id: string) {
return this.tagList.filter(t => t.id === id)[0];
},
createTag(name: string) {
const names = this.tagList.map(item => item.name);
if (names.indexOf(name) >= 0) {
window.alert('标签名重复了');
return 'duplicated';
}
const id = createId().toString();
this.tagList.push({id, name: name});
this.saveTags();
window.alert('添加成功');
return 'success';
},
removeTag(id: string) {
let index = -1;
for (let i = 0; i < this.tagList.length; i++) {
if (this.tagList[i].id === id) {
index = i;
break;
}
}
this.tagList.splice(index, 1);
this.saveTags();
return true;
},
updateTag(id: string, name: string) {
const idList = this.tagList.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const names = this.tagList.map(item => item.name);
if (names.indexOf(name) >= 0) {
return 'duplicated';
} else {
const tag = this.tagList.filter(item => item.id === id)[0];
tag.name = name;
this.saveTags();
return 'success';
}
} else {
return 'not found';
}
},
saveTags() {
window.localStorage.setItem(localStorageKeyName, JSON.stringify(this.tagList));
}
};
tagStore.fetchTags();
export default tagStore;
-
解决创建标签的bug:
-
优化前代码:
//Tags.vue
@Prop() readonly dataSource: string[] | undefined;
create() {
const name = window.prompt('请输入标签名');
if (name === '') {
window.alert('标签名不能为空');
} else if (this.dataSource) {
this.$emit('update:dataSource',
[...this.dataSource, name]);
}
- 优化后代码:
//Tags.vue
<template>
<div class="tags">
<div class="new">
<button @click="create">添加标签</button>
</div>
<ul class="current">
<li v-for="tag in tagList" :key="tag.id"
:class="{selected:selectedTags.indexOf(tag)>=0}"
@click="toggle(tag)">{{tag.name}}
</li>
</ul>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component, Prop} from 'vue-property-decorator';
import store from '@/store/index2';
@Component
export default class Tags extends Vue {
tagList = store.fetchTags();
selectedTags: string[] = [];
toggle(tag: string) {
const index = this.selectedTags.indexOf(tag);
if (index >= 0) {
this.selectedTags.splice(index, 1);
} else {
this.selectedTags.push(tag);
}
this.$emit('update:value', this.selectedTags);
}
create() {
const name = window.prompt('请输入标签名');
if (!name) {
return window.alert('标签名不能为空');
}
store.createTag(name);
}
}
</script>
Vuex 全局状态管理
- state 存放数据的地方
- mutation 用于写数据的地方,同步数据
- action 用于调用method,异步操作,发AJAX请求
Vuex的常用方法:
//index.ts
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
//给它构造参数,常用的是state和mutations
state:{
count:0;
tag:undefined;
},
mutations:{
addCount(state){
state.count
}
}
})
export default store;
- 在组件中使用Vuex来读写数据:
//money.vue
import {Component} from 'vue-property-decorator';
import store from '@/store/index.ts';
@Component({
components: {Tags, FormItem, Types, NumberPad},
computed: {
recordList() {
return this.$store.state.count; //对象语法,读数据
}
}
...
export default class Money extends Vue {
created(){
this.$store.commit('fetchRecords') //写数据,可以传一个参数payload
}
get count(){
//JS或TS类语法,读数据
}
setTag(){
store.state.tag //获取commit的返回值,读数据
}
}
})
Vuex 重构 Money组件
- 安全删除
store/index2.ts
,有多个文件用到此文件内容 - 分别去用到的文件中做暂时修改,赋空值或直接注释掉,标记
//TODO
,直到yarn serve
页面控制台不报错为止,接下来再分别用Vuex重构以上的组件。 - 让
recordList
一开始就有数据,recordList
初始化:
//store.index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import clone from '@/lib/clone';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
recordList:[] as RecordItem[]
},
mutations: { //不需要return
fetchRecords(state) { //`recordList`初始化
state.recordList = JSON.parse(window.localStorage.getItem('recordList') || '[]') as RecordItem[];
},
createRecord(state,record){
const record2: RecordItem = clone(record);
record2.createdAt = new Date();
state.recordList.push(record2);
console.log(state.recordList);
// TO DO
// recordStore.saveRecord();
},
saveRecord(state) {
window.localStorage.setItem('recordList', JSON.stringify(state.recordList));
},
}
});
export default store;
- 只需要
created
的时候,调用fetchRecords
即可获取到用户添加的数据
//Money.vue
created(){
this.$store.commit('fetchRecords')
}
- 保存用户添加的数据:
//index.ts
...
createRecord(state, record) {
const record2: RecordItem = clone(record);
record2.createdAt = new Date();
state.recordList.push(record2);
store.commit('saveRecord'); //保存用户添加的数据
Vuex 重构 Tags组件
- 第一步,读取tag列表:
//Tags.vue
@Component({
computed: {
tagList() {
return this.$store.state.tagList;
}
}
})
- 第二步,声明
tagList
,并初始化:
//store/index.ts
const store = new Vuex.Store({
state: {
recordList: [] as RecordItem[],
tagList:[] as Tag[] //声明tagList
},
mutations:{
fetchTags: function (state) { //初始化
return state.tagList = JSON.parse(window.localStorage.getItem('tagList') || '[]');
}
}
}
- 第三步,使用
tagList
:
//Tags.vue
export default class Tags extends Vue {
created() {
this.$store.commit('fetchTags');
}
}
- 第四步,把原来的
tagStore
中的createTag
和saveTags
粘贴到index.ts
中:
//
const store = new Vuex.Store({
state: {
recordList: [] as RecordItem[],
tagList: [] as Tag[]
},
mutations: {
createTag(state, name: string) {
const names = state.tagList.map(item => item.name);
if (names.indexOf(name) >= 0) {
window.alert('标签名重复了');
return 'duplicated';
}
const id = createId().toString();
state.tagList.push({id, name: name});
store.commit('saveTags');
window.alert('添加成功');
return 'success';
},
saveTags(state) {
window.localStorage.setItem('tagList',
JSON.stringify(state.tagList));
}
}
}
- 第五步,在
Tags.vue
中调用create
:
create() {
const name = window.prompt('请输入标签名');
if (!name) {
return window.alert('标签名不能为空');
}
this.$store.commit('createTag', name);
}
Vuex 重构 Labels组件
@Component({
components: {Button},
computed: {
tags() { //新增
return this.$store.state.tagList;
}
}
})
export default class Labels extends Vue {
beforeCreate() { //新增fetch,每次点击Labels时都fetch最新
this.$store.commit('fetchTags');
}
createTag() { //与Money.vue中的create()相同的代码,可以用mixin
const name = window.prompt('请输入标签名');
if (!name) {
return window.alert('标签名不能为空');
}
this.$store.commit('createTag', name);
}
}
-
用
mixin
来优化createtag
,把多个组件用到的方法归拢到一个mixins组件,然后用到的时候直接继承: -
官方文档为:Vue Class Component
-
直接把
mixin
声明为组件
//TagHelper.ts
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export class TagHelper extends Vue {
createTag() {
const name = window.prompt('请输入标签名');
if (!name) {
return window.alert('标签名不能为空');
}
this.$store.commit('createTag', name);
}
}
export default TagHelper;
- 在
labels.vue
中引入这个mixin
组件:
import {mixins} from 'vue-class-component';
const tagHelper:any = require('@/mixins/TagHelper') ;
@Component({
components: {Button},
mixins:[tagHelper],
computed: {
tags() {
return this.$store.state.tagList;
}
}
export default class Labels extends mixins(TagHelper) {
beforeCreate() {
this.$store.commit('fetchTags');
}
}
})
- 在
Tags.vue
中引入这个mixin
组件:
<button @click="createTag">添加标签</button>
export default class Tags extends mixins(TagHelper) {
}
Vuex 重构 EditLabel组件
store.commit没有返回值,通过在store上添加一个currentTag来获取commit('setCurrentTag')的返回值
- 先声明数据类型:
//store/index.ts
type RootState = {
recordList: RecordItem[],
tagList: Tag[],
currentTag?: Tag
}
const store = new Vuex.Store({
state: {
recordList: [] ,
tagList: [] ,
currentTag:undefined
} as RootState,
- 用原生自带的
set
和get
,基于多次尝试如下代码无法读取到数据,比如Tags.vue
中:
//Tags.vue
//此组件只是没有在下面引用到tagList;其实computed部分可以删掉,然后直接在下面用get tagList就行。
@Component({
computed: {
tagList() {
return this.$store.state.tagList;
}
}
})
export default class Tags extends mixins(TagHelper) {
- 再比如在
Labels.vue
中也存在这个情况:
//Labels.vue
@Component({
components: {Button},
computed: { //可以删掉
tags() { //可以删掉
return this.$store.state.tagList;
}
}
})
export default class Labels extends mixins(TagHelper) {
}
- 按照上面的思路去改造之前的写法:
//Tag.vue
@Component({}) //删掉之前的computed
//Labels.vue
@Component({
components: {Button},
})
export default class Labels extends mixins(TagHelper) {
get tags() {
return this.$store.state.tagList; //新增内容
}
//EditLabel.vue
export default class EditLabel extends Vue {
get tag() {
return this.$store.state.currentTag;
}
//Money.vue
@Component({
components: {Tags, FormItem, Types, NumberPad},
})
export default class Money extends Vue {
get recordList() {
return this.$store.state.recordList;
}
mutations里面的函数只能接受两个参数
- 实现修改标签名:
//index.ts
updateTag(state, payload: { id: string, name: string }) { //只能接受两个参数
const {id, name} = payload;
const idList = state.tagList.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const names = state.tagList.map(item => item.name);
if (names.indexOf(name) >= 0) {
window.alert('标签名重复了')
} else {
const tag = state.tagList.filter(item => item.id === id)[0];
tag.name = name;
store.commit('saveTags');
}
}
},
//EditLabel.vue
update(name: string) {
if (this.tag) {
this.$store.commit('updateTag',
{id:this.tag.id,name});
}
}
- 实现删除功能
//index.ts
removeTag(state, id: string) {
let index = -1;
for (let i = 0; i < state.tagList.length; i++) {
if (state.tagList[i].id === id) {
index = i;
break;
}
}
state.tagList.splice(index, 1);
store.commit('saveTags');
},
//EditLabel.vue
remove() {
if (this.tag) {
this.$store.commit('removeTag',this.tag.id);
}
}
Statistics 组件
- 页面效果图:
第一个 type 组件
- 具体的样式需要使用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
第二个 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')
部署到 github
源代码的仓库和dist打包仓库分两个;前者是手动push,后者用
sh deploy.sh
推送
- 本地预览
$ yarn build //生成dist目录,提示部署的文档
$ yarn global add serve //本地预览用于检查dist是否正确打包
$ serve -s dist //本地预览,打开一个5000端口的网页,文件已打包
- GitHub新建仓库并关联
git remote set-url origin git@github.com:jianlong5296/morney-9.git
git branch -M main
git push -u origin main
-
GitHub新建dist打包的新仓库
morney-9-website
-
GitHub Page手动推送更新
//vue-config-js中设置正确的publicPath
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/morney-9-website/' //注意填写dist打包新仓库项目名
: '/'
}
- 在项目目录下,创建内容如下的
deploy.sh
(可以适当地取消注释) 并运行它以进行部署
# !/usr/bin/env sh
# 当发生错误时中止脚本
set -e
# 构建
yarn build
# cd 到构建输出的目录下
cd dist
# 部署到自定义域域名
# echo 'www.example.com' > CNAME
git init
git add -A
git commit -m 'deploy'
# 部署到 https://<USERNAME>.github.io
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master
# 部署到 https://<USERNAME>.github.io/<REPO>
git push -f git@github.com:jianlong5296/morney-1-website.git master:gh-pages
cd -
- 首次提交或更改源代码后都需要重新deploy
$ sh deploy.sh //sh即为shell; deploy部署
- 刷新仓库,打开自动生成的page地址,即可访问。
小bug:遇到修改代码但未呈现出来的情况,就换个浏览器查看效果。