vue.js开发记账(6)

112 阅读5分钟

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中实现:

可以叙述遇到的技术难题的解决路径,比如:创建成功要返回什么类型存在疑问:一开始认为只要返回布尔值truefalse就行,但是发现失败的情况有很多种,于是考虑用数字来判定,列了一个数字列表从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');
    }
}
  • 用户对标签修改,监听FormItemvalue事件:
//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)">