【项目】记账App(五)----写标签页面

276 阅读6分钟

标签页面Label.vue

编辑标签页面

一、基本需求

  1. 点击标签会跳转到编辑标签页面
  2. 编辑标签页面的删除标签会跳回标签页面
  3. 新建标签按钮和记账页面的新增标签按钮功能一模一样

二、HTML和CSS

<template>
  <div>
    <Layout>
      <ol class="tags">
        <li>
          <span></span>
          <Icon iconId="right"/>
        </li>
        <li>
          <span></span>
          <Icon iconId="right"/>
        </li>
        <li>
          <span></span>
          <Icon iconId="right"/>
        </li>
        <li>
          <span></span>
          <Icon iconId="right"/>
        </li>
      </ol>
      <div class="creatTag">
        <button>新建标签</button>
      </div>
    </Layout>
  </div>
</template>

<script lang="ts">

  export default {
    name: 'Labels',
  };
</script>

<style lang="scss" scoped>
  @import "~@/assets/style/helper.scss";

  .tags {
    font-size: 18px;
    padding: 0 8px;
    background: white;

    > li {
      border-bottom: 1px solid #e6e6e6;
      min-height: 44px;
      display: flex;
      align-items: center;
      justify-content: space-between;

      > svg {
        color: #999999;
        width: 26px;
        height: 26px;
      }
    }
  }

  .creatTag {
    text-align: center;
    margin-top: 44px;
    > button {
      background: $color-four;
    padding: 10px 18px;
      border-radius: 5px;
      border: none;
    }
  }

</style>

三、TypeScript

(一)新建标签功能

1、需求描述

  • 回顾Money组件里的标签。被我们写死在Money组件的tags数据里,然后作为Tags组件的外部数据dataSource的值传给Tags组件的

<Tags :data-source.sync="tags" @update:tags="onUpdateTags"/>

  • Money组件和Label组件里的标签应该全都是从像recordList一样,从数据库中读出来(然后渲染),可以修改,可以保存进数据库

2、步骤

(1)新建src/models目录,把recordListModel.ts放进去,也在新建一个tagsListModel.ts

(2)tagsListModel.ts里面写对标签的所有操作。

  • 标签列表这个数据写在了这个文件夹中,只暴露了API供别人使用
  • 标签列表应该像这样,每个标签有id和标签名name
const localStorageKeyName = 'tagList';
type Tag = {
  id: string;
  name: string;
}
type TagListModel = {
  data: Tag[]
  fetch: () => Tag[]
  create: (name: string) => 'success' | 'duplicated' // 联合类型
  save: () => void
}
const tagListModel: TagListModel = {
  //声明标签列表data
  data: [],
  //从数据库中获取最新的标签列表
  fetch() {
    this.data = JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]');
    return this.data;
  },
  //往标签列表里面新增标签 ①如果已经有了就返回duplicated ②没有就往标签列表里面新增标签,保存,返回success
  create(name) {
    // this.data = [{id:'1', name:'1'}, {id:'2', name:'2'}]
    const names = this.data.map(item => item.name);
    if (names.indexOf(name) >= 0) {return 'duplicated';};
    this.data.push({id:name, name: name});
    this.save();
    return 'success';
  },
  //把标签列表保存到数据库里
  save() {
    window.localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));
  }
};
export {tagListModel};

(3)Label组件如何使用

  • 首先需要从数据库获取最新的标签列表data
  • 其次本组件用tags表示标签列表,内容就是tagListModel里的data
  • 把标签渲染到页面
  • 当点击新建标签按钮,事件处理函数createTag会让用户输入标签名,然后执行tagListModel的create函数,返回值命名为message
  • 如果message是duplicated,那肯定就是重复了,那就提醒用户重复了;如果message是success,那就加入成功,提醒用户成功了
<template>
  <div>
    <Layout>
      <ol class="tags">
<!--        把标签渲染到页面-->
        <li v-for="tag in tags" :key="tag.id">
          <span>{{tag.name}}</span>
          <Icon iconId="right"/>
        </li>
      </ol>
      <div class="creatTag">
        <button @click="createTag">新建标签</button>
      </div>
    </Layout>
  </div>
</template>

<script lang="ts">
  import Vue from 'vue';
  import {Component} from 'vue-property-decorator';
  import {tagListModel} from '@/models/tagListModel';
  //首先需要从数据库获取最新的标签列表data
  tagListModel.fetch();
  @Component
  export default class Labels extends Vue {
    //其次本组件用tags表示标签列表,内容就是tagListModel里的data
    tags = tagListModel.data;
    //当点击新建标签按钮,事件处理函数createTag会让用户输入标签名,然后执行create函数,返回值命名为message
    //如果message是duplicated,那肯定就是重复了,那就提醒用户重复了;如果message是success,那就加入成功,提醒用户成功了
    createTag() {
      const name = window.prompt('请输出标签名');
      if (name) {
        const message = tagListModel.create(name);
        if (message === 'duplicated') {
          window.alert('标签名重复了');
        } else if (message === 'success') {
          window.alert('添加成功');
        }
      }
    }
  }
</script>

面试官问你遇到最难的技术性问题是什么?

我们曾经对于创建一个东西成功返回什么存在一些疑问。一开始觉得只要返回true和false;但是失败的情况有很多种,所以又想着能不能用数字表示。于是列了一个数组列表定义了不同的数组分别代表不同的错误。最后发现数字还是容易忘记。最后直接返回字符串表示错误。那又想着中文也是字符串啊,所以干脆返回中文表示错误得了。

四、编辑标签页面

(一)新建路由

import EditLabel from '@/views/EditLabel.vue';
{
    path: '/label/edit/:id',     //编辑标签页面
    component: EditLabel        
  },
  • :id就是用来占位,之后会被替换成字符串
  • 如果访问/label/edit/1,那么在对应组件EditLabel里就可以用this.$route.params获取到当前路由的信息{id:'1'}

(二)新建 编辑标签页面 的 组件view/EditLabel.vue

  • 由上面可知如果访问/label/edit/x,那么在对应组件EditLabel里就可以用this.$route.params获取到当前路由的信息{id:'x'}
  • 那么就去标签列表里面找看看有没有id为x的标签,有就是成功了;没有那就说明访问的/label/edit/x根本不是东西,那就调到404页面去
<template>
  <Layout>
    编辑页面
  </Layout>
</template>

<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;
        tagListModel.fetch();
        const tags = tagListModel.data;
        const tag = tags.filter(t=>t.id===id)[0];
        if(tag){
           console.log(tag);  //这样就获得了这个编辑标签页面所对应的标签了!
         }else {
          this.$router.replace('/404')
            }
}
  }
</script>

<style lang="scss" scoped>

</style>

(三)修改标签页面

因为是从标签页面点击标签跳转到对应编辑标签页面的。

<template>
  <div>
    <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 iconId="right"/>
        </router-link>
      </div>
      <div class="creatTag">
        <button @click="createTag">新建标签</button>
      </div>
    </Layout>
  </div>
</template>

小结:举例描述全过程

  1. 在标签页面点击id为1的标签,那么会跳转到/label/edit/1路由
  2. 在对应组件EditLabel里就可以用this.$route.params获取到当前路由的信息{id:'1'}
  3. 从标签列表里找有没有id为1的标签,有就是成功了(没有那就说明访问的/label/edit/x根本不是东西,那就调到404页面去)

(四)写HTML和CSS

1、先来个小插曲:封装通用组件

(1)把Money/Notes.vue封装成通用组件,这样编辑标签页面也可以用了

① 原来代码

<template>
  <div>
    <label class="notes">
      <span class="name">备注</span>    //备注应该是外部数据由外面传过来
      <input type="text"
             v-model="value"
             placeholder="在这里输入备注">   //应该是外部数据 由外面传过来
    </label>
  </div>
</template>

<script lang="ts">
  import Vue from 'vue';
  import {Component,Watch} from 'vue-property-decorator';
  @Component
  export default class Notes extends Vue {
    value = '';  //还是得声明一个value,用户不写当做写了个空字符串
  @Watch('value')
    onValueChanged(newValue: string,oldValue: string){
     this.$emit('update:notes',newValue)
  }
  }
</script>

② 添加两个外部数据

   @Prop({required: true}) fieldName!: string;  //{required: true}表示必须给值   !表示我就不给你初始值 
   @Prop() placeholder?: string;      //!表示有可能不存在

③ 修改<template>标签

<template>
  <div>
    <label class="formItem">
      <span class="name">{{this.fieldName}}</span>
      <input type="text"
             :value="value"
             @input="onValueChanged($event.target.value)"
             :placeholder="this.placeholder">
    </label>
  </div>
</template>

④ Money组件用到了这个组件,所以得修改

<Notes fieldName="备注" placeholder="请在这里输入备注" @update:notes="onUpdateNotes"/>

⑤ 既然成为了通用组件,那叫Notes.vue就不合适了。改个名字吧(好怕怕有bug)

  • 先改名字FormItem.vue,再把文件里的export default class FormItem extends Vue改了
  • 用到这个组件的父组件都要改一下!修改Money.vue

(2)把标签页面里的按钮封装成通用组件,这样编辑标签页面也可以用了

① 新建components/Button ② 把标签页面里的按钮代码剪切进去

<template>
  <button class="button" @click="$emit('click',$event)" > 
  //之后在使用本组件时,肯定要有点击事件。但是Button组件的点击事件其实没有的,所以加上这句话
  //button被点击了,就会触发Button组件的点击事件
    <slot/>
  </button>
</template>

<script lang="ts">
  import Vue from 'vue';
  import {Component} from 'vue-property-decorator';
  @Component
  export default class Button extends Vue {
  }
</script>

<style lang="scss" scoped>
  @import "~@/assets/style/helper.scss";
  .button {
    background: $color-four;
    padding: 10px 18px;
    border-radius: 5px;
    border: none;
    color: #4d424c;
    margin-top: 44px;
    margin-left: 37%;
  }
</style>

③ 修改标签页面,使用Button组件

//原代码
 <div>
   <button class="creatTag" @click="createTag">新建标签</button>
 <div>
 
//新代码
<div>
   <Button @click="createTag">新建标签</Button>
</div>

2、HTML和CSS

有个向左的箭头icon,应该和标签页面的向右的箭头icon完全对称。所以自己做一下吧

  • 在iconfont里把向右的箭头icon(right.svg)上传(重命名left),然后编辑的时候旋转好保存,最后svg下载就行了

<template>
  <Layout>
    <div class="navBar">
      <Icon class="leftIcon" iconId="left"/>
      <span class="title">编辑标签</span>
      <span class="leftIcon"> </span>   //用来space-between的
    </div>
    <FormItem fieldName="标签名" placeholder="请在这里输入标签名"/>
    <Button>删除标签</Button>
  </Layout>
</template>

<script lang="ts">
  import Vue from 'vue';
  import {Component} from 'vue-property-decorator';
  import {tagListModel} from '@/models/tagListModel';
  import FormItem from '@/components/Money/FormItem.vue';
  import Button from '@/components/Button.vue';

  @Component({
    components: {Button, FormItem}
  })
  export default class EditLabel extends Vue {
    created() {
      const id = this.$route.params.id;
      tagListModel.fetch();
      const tags = tagListModel.data;
      const tag = tags.filter(t => t.id === id)[0];
      if (tag) {
        console.log(tag);
      } else {
        this.$router.replace('/404');
      }
    }
  }
</script>

<style lang="scss" scoped>
  .navBar {
    text-align: center;
    font-size: 18px;
    padding: 12px;
    background: white;
    display: flex;
    align-items: center;
    justify-content: space-between;

    .leftIcon {
      width: 28px;
      height: 28px;
    }
    .title {
    }
  }
</style>

(五)TypeScript

1、需要让标签名后面默认就展示当前的标签名

(1)修改FormItem组件

  • 把value改成外部属性。又因为input的value属性等于我们的外部属性value。所以编辑标签页面在使用这个组件的时候传标签名给外部属性value就行
<template>
  <div>
    <label class="notes">
      <span class="name">{{this.fieldName}}</span>
      <input type="text"
      
             //EditLabel父组件专属
             :value="value"   //input的value属性等于我们的value
             
             //Money父组件专属
             //当用户输入的时候就触发了input事件,那么这个函数onValueChanged($event.target.value)就执行了,这个函数就是把用户输入的内容传给父组件Moeny
             @input="onValueChanged($event.target.value)"  
             
             :placeholder="placeholder">
    </label>
  </div>
</template>

<script lang="ts">
  import Vue from 'vue';
  import {Component,Watch,Prop} from 'vue-property-decorator';
  @Component
  export default class FormItem extends Vue {
    @Prop({default: ''}) readonly value!: string; //value是外部属性,将被我们当成input标签的value属性。所以得给他个默认值空字符串,因为在Money父组件不用

    @Prop({required: true}) fieldName!: string;
    @Prop() placeholder?: string;

  onValueChanged(newValue: string){          //这个函数就是把用户输入的内容传给父组件Moeny
     this.$emit('update:notes',newValue)
  }
  }
</script>

(2)修改EditLabel组件

  • 首先声明一个我们自己的tag
  • 把找到的tag赋值给我们的tag
  • 使用FormItem组件时把标签名当成value传进去就行了
<template>
  <Layout>
    <div class="navBar">
      <Icon class="leftIcon" iconId="left"/>
      <span class="title">编辑标签</span>
      <span class="leftIcon"> </span>
    </div>
    <FormItem :value="tag.name" fieldName="标签名" placeholder="请在这里输入标签名"/>    
    //使用FormItem组件时把标签名当成value传进去就行了
    <Button>删除标签</Button>
  </Layout>
</template>

<script lang="ts">
  import Vue from 'vue';
  import {Component} from 'vue-property-decorator';
  import {tagListModel} from '@/models/tagListModel';
  import FormItem from '@/components/Money/FormItem.vue';
  import Button from '@/components/Button.vue';

  @Component({
    components: {Button, FormItem}
  })
  export default class EditLabel extends Vue {
    tag?: {id: string; name: string}=undefined;  //首先声明一个tag
    created() {
      const id = this.$route.params.id;
      tagListModel.fetch();
      const tags = tagListModel.data;
      const tag = tags.filter(t => t.id === id)[0];
      if (tag) {
        this.tag=tag;                   //找到的tag赋值给我们的tag
      } else {
        this.$router.replace('/404');
      }
    }
  }
</script>

2、修改标签名

(1)在FormItem子组件中,用户在修改标签名肯定就是在input里面输入嘛,当用户输入的时候就触发了input事件,那么函数onValueChanged($event.target.value)就执行了,这个函数就是把用户输入的内容传给父组件

(2)EditLabel父组件监听update:notes事件,那就可以拿到用户输入的内容了。 就可以把新的标签名保存到标签列表里去了

<FormItem :value="tag.name"
              fieldName="标签名"
              placeholder="请在这里输入标签名"
              @update:notes="updateTag"/>
              
              
updateTag(value: string) {  //value就是用户输入的内容(新标签名)
      if (this.tag) {
        tagListModel.update(this.tag.id, value);
      }
    }

(3)在标签列表的文件里新加一个函数用于更新标签名

type TagListModel = {
  data: Tag[];
  fetch: () => Tag[];
  create: (name: string) => 'success' | 'duplicated'; // 联合类型
  save: () => void;
  update: (id: string, name: string) => 'success' | 'not found' | 'duplicated'
}


 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';
    }
  },

3、删除标签

(1)在标签列表的文件里新加一个函数用于删除标签

remove: (id: string) => boolean

 remove(id: string) {
      let index = -1;
      
      //遍历标签列表中的每一个标签
      for (let i = 0; i < this.data.length; i++) {
      //如果有一个标签的id和我们给的id一样
        if (this.data[i].id === id) {
        //就记下他的下标,退出遍历
          index = i;
          break;
        }
      }
      //把这个标签删掉,然后保存
      this.data.splice(index, 1);
      this.save();
      return true;
    },

(2)监听Button组件的点击事件,事件处理函数是removeTag

<Button @click="removeTag">删除标签</Button>


 removeTag(){
      if (this.tag) {
        tagListModel.remove(this.tag.id);
      }
    }

4、点击icon返回标签页面

<Icon class="leftIcon" iconId="left" @click(.native)="goBack"/>


goBack(){
      this.$router.back()
    }

但是注意!Icon子组件是没有点击事件的。

所以加个.native( .native主要是给自定义的组件添加原生事件。)

或者要在Icon子组件的svg标签里透传点击事件

<template>
  <svg class="icon" @click="$emit('click',$event)" >
    <use v-bind:xlink:href="'#'+iconId"/>
  </svg>
</template>

优化id:新建id生成器

需求描述

  • 因为还没有使用数据库,所以id之前和name同名所以会出现bug。
  • ID的原则:①一旦给了id就不能修改 ②id不能重复
  • 因为先暂时写一个id生成函数缓一缓

步骤

  1. 新建src/lip/createId.ts
//每次刷新后都是从localStorage读出id
let id: number = parseInt(window.localStorage.getItem('_idMax') || '0') || 0;

//creatId()函数会把id增加一个然后返回最新的id
function createId() {
  id++;
  window.localStorage.setItem('_idMax',id.toString());
  return id;
}

export default createId;
  1. 那么修改tagListModel.ts里的create函数
 //往标签列表里面新增标签 ①如果已经有了就返回duplicated ②没有就往标签列表里面新增标签,保存,返回success
    create(name) {
      // this.data = [{id:'1', name:'1'}, {id:'2', name:'2'}]
      const names = this.data.map(item => item.name);
      if (names.indexOf(name) >= 0) {return 'duplicated';}
      const id = createId().toString();           //每次新增标签都生成一个新id
      this.data.push({id: id, name: name});  
      this.save();
      return 'success';
    },

小插曲

  1. 把recordListModel.ts改造成和tagListModel一样的
import clone from '@/lib/clone';

const localStorageKeyName = 'recordList';

const recordListModel = {
  data: [] as RecordItem[],
  
  fetch() {
    this.data= JSON.parse(window.localStorage.getItem(localStorageKeyName) || '[]') as RecordItem[];
    return this.data
  },  //获取数据
  
  create(record: RecordItem){
    const record2: RecordItem = clone(record);
    record2.createdAt = new Date();
    this.data.push(record2);
    this.save()
  },
 
 
  save() { return window.localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));} //保存数据
};
  export {recordListModel}
  1. Money组件用到了这个,所以需要修改