project from jirengu.com
实现Labels.vue的功能:
- 先初步写一版页面:
//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>
进一步优化HTML,并实现SCSS样式:
//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;
&-wrapper { //&-xx xx为其父元素
text-align: center;
padding: 16px;
margin-top: 44-16px;
}
}
</style>
实现新建标签的功能:
- 新增的标签存在什么位置?参考之前写的
Money.vue中写的标签初始值是固定的,如果把新增标签的数据放在tags初始值上,那么刷新时,就会再次出现初始值,并没有体现变化,所以还需要新建一个tagListModel.ts来存Labels.vue的新建标签:
<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 {
tags = tagList; //初始值改为重构后的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.vue 功能实现
- 把当前的标签名获取到,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)">