vue.js开发记账(7)

144 阅读5分钟

project from jirengu.com

  • 写ID生成器(在lib目录下新建createId.ts):
//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.vueLabels.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);
    }
}
  • 继续封装removeupdate
//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

  • 修改注释的样式:

222.png

  • 先把过多的全局变量挂到一个对象上,来解决用了过多的全局变量,先声明类型:
//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.tsstore文件夹下,把刚才的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.tstagStore.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.tsindex2.ts引用了recordStorefetchsave属性,可能与另外的数据可能也使用fetchsave相冲突,所以需要重命名为fetchRecordsaveRecord

  • 再合并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>