【项目-喵内记账-meoney-03】Labels 组件
大纲链接 §
[toc]
知识点
Surround with Emmet
快捷键Ctrl Alt t
给每项添加标签li * n
iconfont
小技巧,编辑 SVG
custom.d.ts
怎么用
@click.native
怎么用
Vue Router
怎么用
props
怎么用
- 每次完成一小节都要提交代码
Labels.vue
之 HTML
- 去
IconFont
下载一个右箭头图标,使用封装好的<Icon/>
全局组件
- 先写死大致的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
<template>
<Layout>
<ol class="tags">
<li>
<span>衣</span>
<Icon name="money_right"/>
</li>
<li>
<span>食</span>
<Icon name="money_right"/>
</li>
<li>
<span>住</span>
<Icon name="money_right"/>
</li>
<li>
<span>行</span>
<Icon name="money_right"/>
</li>
</ol>
<div>
<button>新建标签</button>
</div>
</Layout>
</template>
|
Labels.vue
之 SCSS 样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
@import "~@/assets/style/global.scss";
.labels {
.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;
> span {
// text overflow mixins
@include multiline-ellipsis(1, 44px, 4em);
}
> svg {
font-size: 24px;
color: #666;
margin-right: 16px;
}
}
}
.createTag {
background: #767676;
color: #fff;
border-radius: 4px;
height: 40px;
padding: 0 16px;
&-wrapper {
text-align: center;
padding: 16px;
margin-top:44-16px;
}
}
/*
.createTag-wrapper{
text-align: center;
padding: 16px;
margin-top:44-16px;
.createTag {
background: #767676;
color: #fff;
border-radius: 4px;
height: 40px;
padding: 0 16px;
&-wrapper {
// 以下样式无效
text-align: center;
padding: 16px;
margin-top:44-16px;
}
}
}
*/
}
|
<button></button>
为内联元素,居中,text-align: center;
加到父元素上
padding
没有margin
的上下合并问题,但会影响background
的范围
SCSS
可以在子类中写父类的样式 .child { &-father {...}}
,但如果子类已经写在父类的嵌套中,这种写法无效
- 使用最小高度限制每行的高度
- 显示字数最多3个,多余的用
...
global.scss
中写@mixin multiline-ellipsis
单行超出文字省略样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// ...
// mixins text overflow
@mixin multiline-ellipsis($line: 2, $line-height: 1.6em, $max-width: 5em) {
//white-space: nowrap;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
line-height: $line-height;
max-height: $line-height * $line;
max-width: $max-width;
}
|
reset.scss
- 由于多次用到
border: none;
,写到reset.scss
新建标签功能
- 原来存储的标签是写死在数据中的
tags = ['衣', '食', '住', '行', '理财'];
- 需要另外的
Model
来存储添加的标签
- 新建
models
目录,存放所有Model
相关数据:recordListModel.ts
、tagListModel.ts
- 两个
Model
文件结构几乎一样,可提取Mixins
tagListModel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const localStorageKeyName = 'tagList';
const tagListModel = {
clone(data: RecordItem | RecordItem[]) {
return JSON.parse(JSON.stringify(data)) as RecordItem;
},
fetchData() {
return JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]') as RecordItem[];
},
saveData(data: RecordItem[]) {
localStorage.setItem(localStorageKeyName, JSON.stringify(data));
}
};
export default tagListModel;
|
修改Money.vue
- 引入 Model 数据
import tagListModel from '@/models/tagListModel';
- 声明标签列表
const tagList = tagListModel.fetchData();
- 替换原来写死的数据
tags = tagList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
// ...
<script lang="ts">
import Vue from 'vue';
import Tags from '@/components/Money/Tags.vue';
import Notes from '@/components/Money/Notes.vue';
import Types from '@/components/Money/Types.vue';
import Numpad from '@/components/Money/Numpad.vue';
import {Component, Watch} from 'vue-property-decorator';
import recordListModel from '@/models/recordListModel';
import tagListModel from '@/models/tagListModel';
const recordList = recordListModel.fetchData();
const tagList = tagListModel.fetchData();
@Component({
components: {
Numpad, Types, Notes, Tags
}
})
export default class Money extends Vue {
tags = tagList;
record: RecordItem = {
tags: [],
notes: '',
type: '-',
amount: 0,
createdAt: new Date(),
};
onUpdate(selectedTags: string[]) {
this.record.tags = selectedTags;
}
onUpdateNotes(notesValue: string) {
this.record.notes = notesValue;
}
saveRecord() {
const clonedRecord = recordListModel.clone(this.record);
clonedRecord.createdAt = new Date();
this.recordList.push(clonedRecord);
}
@Watch('recordList')
onRecordeChange() {
recordListModel.saveData(this.recordList);
}
}
</script>
// ...
|
同样地引入到Lables.vue
- 先获取标签数据
tagListModel.fetchData();
- 内部数据引用
tagListModel.data
,不用去维护操作tags
,所有数据让Model
控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
//...
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel.ts';
tagListModel.fetchData();
@Component
export default class Labels extends Vue {
tags = tagListModel.data;
createTag() {
const name = window.prompt('请输入标签名');
if (name) {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('添加成功');
}
}
}
}
</script>
|
- 判断标签的内容是否存在
- 判断标签是否唯一
- 通过返回提示字符串来判断
- TS 联合类型(字符串的子类型 枚举)可以通过具体的可能值来声明类型,来防止写错
tagListModel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
const localStorageKeyName = 'tagList';
type TagListModel = {
data: string[];
fetchData: () => string[];
create: (name: string) => 'success' | 'duplicated';
saveData: () => void;
}
const tagListModel: TagListModel = {
data: [],
fetchData() {
this.data = JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]');
return this.data;
},
saveData() {
localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));
},
create(name: string) {
if (this.data.indexOf(name) >= 0) { return 'duplicated'; }
this.data.push(name);
this.saveData();
return 'success';
}
};
export default tagListModel;
|
编辑/删除标签功能
- 在另一个编辑标签页面中实现
- 实现点击
Label.vue
中的标签可跳转至标签编辑页面
- 新建路由
/labels/edit
- 新建
src/view/EditLabel.vue
组件
src/router/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import Vue from 'vue';
import VueRouter, {RouteConfig} from 'vue-router';
import Money from '@/views/Money.vue';
import Labels from '@/views/Labels.vue';
import Statistics from '@/views/Statistics.vue';
import NotFound from '@/views/NotFound.vue';
import EditLabel from '@/views/EditLabel.vue';
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path: '/',
redirect: '/money'
},
{
path: '/money',
component: Money
},
{
path: '/labels',
component: Labels
},
{
path: '/statistics',
component: Statistics
},
{
path: '/labels/edit',
component: EditLabel
},
{
path: '*',
component: NotFound
}
];
const router = new VueRouter({
routes
});
export default router;
|
EditLabel.vue
& Vue Router
- 每个循环渲染的标签需要一个
id
,通常是取自数据库中的,本地版将不重复的name
当做id
- 在
tagListModel.ts
中声明Tag
类型,data
变为Tag[]
,fetchData
获取Tag[]
tagListModel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
const localStorageKeyName = 'tagList';
type Tag = {
id: string;
name: string;
}
type TagListModel = {
data: Tag[];
fetchData: () => Tag[];
create: (name: string) => 'success' | 'duplicated';
saveData: () => void;
}
const tagListModel: TagListModel = {
data: [],
fetchData() {
this.data = JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]');
return this.data;
},
saveData() {
localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));
},
create(name: string) {
// this.data = [{id: '1', name: '1'}, {id: '2', name: '2'}]
const names = this.data.map(d => d.name);
if (names.indexOf(name) >= 0) { return 'duplicated'; }
this.data.push({id: name, name: name}); // 将不重复的数据推入,后期改为id生成器
this.saveData();
return 'success';
}
};
export default tagListModel;
|
Lables.vue
中相应的模板数据{{tag}}
和:key
也改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<template>
<Layout class="labels">
<ul class="tags">
<li v-for="tag in tags" :key="tag.id">
<span>{{ tag.name }}</span>
<Icon name="money_right"/>
</li>
</ul>
<div class="createTag-wrapper">
<button class="createTag" @click="createTag">新建标签</button>
</div>
</Layout>
</template>
...
|
动态子路由
- 点击
Label.vue
中的标签跳转至编辑页面
- 路由到
../labels/edit/1
'/labels/edit/:id'
中的:id
表示占位
Vue-Router 数据获取
文档
src/router/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import ...;
import EditLabel from '@/views/EditLabel.vue';
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
...
{
path: '/labels/edit/:id',
component: EditLabel
},
...
];
const router = new VueRouter({
routes
});
export default router;
|
EditLable.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
<template>
<Layout>
<div class="edit">
<Icon name="money_right"/>
编辑标签
</div>
</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.fetchData();
const tags = tagListModel.data;
// const tag = tags.filter(t => t.id === id)[0];
const tag = tags.find(t => t.id === id);
if (tag) {
console.log(tag);
} else {
this.$router.replace('/404');
}
}
}
</script>
<style lang="scss" scoped>
.edit {
> svg {
transform: rotate3d(0, 1, 0, 180deg);
font-size: 24px;
color: #666;
margin-right: 16px;
}
}
</style>
|
- 钩子
created(){}
this.$route
路由相关:可获取路由相关信息
- 是由库中
vue/types/vue.d.ts
中声明而来 ctrl
+ 点击查看源文件
- 查看
console.log(this.$route.params)
- 浏览器地址栏输入
http://localhost:8080/#/labels/edit/1
- 控制台显示
{id: "1"}
- 将路由改为
path: '/labels/edit/:fuck',
- 控制台显示
{fuck: "1"}
- 获取
id
(动态无需声明类型)const id = this.$route.params.id;
- 获取标签数据
- 更新数据
tagListModel.fetchData();
- 获取
const tags = tagListModel.data;
- 获取
id
对应的标签const tag = tags.filter(t => t.id === id)[0];
- 也可用
const tag = tags.find(t => t.id === id);
,但可能会遍历到稀疏数组的未定义值,效率没filter
高-
this.$router
路由器相关:进行路由的相关操作(转发、重定向等)
- 重定向
this.$router.push('/404');
不太好,回退时会重复重定向
- 改为
this.$router.replace('/404');
如何封装通用组件
- 封装原本
Notes.vue
中的<input>
标签
- 封装
Button.vue
按钮组件
1
2
3
4
5
6
7
8
9
10
|
<template>
<div class="div_notes">
<label class="notes">
<span class="name">备注</span>
<input type="text"
v-model="inputValue"
placeholder="在这里输入备注"/>
</label>
</div>
</template>
|
- 封装为外部数据
@Prop()
自动引入import {Component, Prop, Watch} from 'vue-property-decorator';
- 字段名
@Prop({required: true}) fieldName!: string;
(必要,无初始值,类型为字符串)
- 模板中改为
<span class="name">{{this.fieldName}}</span>
- 值由外部组件传入
<Notes field-name="备注" @update:value="onUpdateNotes"/>
\
placeholder
占位符 @Prop({default: ''}) placeholder?: string;
(非必要,可以无初始值,类型为字符串,可能为空值)
- 模板中改为
<input type="text" v-model="inputValue" :placeholder="this.placeholder"/>
- 值由外部组件传入
<Notes field-name="标签名" placeholder="在这里输入标签名"/>
- 由于
Notes.vue
组件的作用已经改变,需重命名为FormItem.vue
- 重构之重命名
FormItem.vue
- 重构之组件内部的类
export default class FormItem extends Vue {...}
- 注意原来引用
Notes.vue
的组件中@Component({components: {Notes: FormItem} })
做了映射
- 手动全局搜索
Notes
,全部改为FormItem
- 注意到一个Bug: 在
Numpad
中输入完所有信息后点击 OK
未清空 备注信息
- 创建
src/components/Button.vue
- 点击事件失效:目前绑定事件在
<Button>
上,用户点击的是<button>
- 需要传递点击事件,点击
<button>
时触发<Button>
上的事件
Button.vue
中的<button>
标签传递触发事件到外部@click="$emit('click', $event)"
- 当
<button>
被点击时,事件传递至<Button>
src/components/Button.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<template>
<button class="button"
@click="$emit('click', $event)">
<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>
.button {
background: #767676;
color: #fff;
border-radius: 4px;
height: 40px;
padding: 0 16px;
&-wrapper {
text-align: center;
padding: 16px;
margin-top: 44-16px;
}
}
</style>
|
- 处理
<button>
其他事件
- 怎么用自定义组件模拟一个真实
<button>
- 可以不用一个一个地暴露事件出去
- 在父组件上使用
.native
修饰符 <Button class="createTag" @click.native="createTag">新建标签</Button>
- 可省略以上子组件内传递原生事件的过程:
@click="$emit('click', $event)"
相应地修改Lable.vue
- 原来的
<button class="createTag" @click="createTag">新建标签</button>
改为<Button class="createTag" @click="createTag">新建标签</Button>
- 注意
webStorm
的代码提示与自动引入import Button from '@/components/Button.vue';
- 自动引入装饰器
@Component({ components: {Button} })
- 首先删除小写的字符
button
,在删除的地方输入Button
,按Tab
自动补全
- 删除原来
<button>
的样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
<template>
<Layout class="labels">
<ul class="tags">
<li v-for="tag in tags" :key="tag.id">
<router-link class="tag" :to="`/labels/edit/${tag.id}`">
<span class="tag-text">{{ tag.name }}</span>
<Icon class="tag-icon" name="money_right"/>
</router-link>
</li>
</ul>
<div class="createTag-wrapper">
<Button @click.native="createTag">新建标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel.ts';
import Button from '@/components/Button.vue';
tagListModel.fetchData();
@Component({
components: {Button}
})
export default class Labels extends Vue {
tags = tagListModel.data; // [{id: '1', name: '1'}, {id: '2', name: '2'}]
createTag() {
const name = window.prompt('请输入标签名');
if (name) {
const message = tagListModel.create(name);
if (message === 'duplicated') {
window.alert('标签名重复了');
} else if (message === 'success') {
window.alert('添加成功');
}
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/style/global.scss";
.labels {
.tags {
background: white;
font-size: 16px;
padding-left: 16px;
> li {
> .tag {
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e6e6e6;
> span {
// text overflow mixins
@include multiline-ellipsis(1, 44px, 4em);
}
> svg {
font-size: 24px;
color: #666;
margin-right: 16px;
}
}
}
}
}
|
EditLabel.vue
添加返回功能
this.$router.back()
- 相同原理,点击事件失效:目前绑定事件在
<Icon/>
上,用户点击的是<svg>
- 需要传递点击事件,点击
<svg>
时触发组件<Icon/>
上的事件
Icon.vue
中的<svg>
标签传递触发事件到外部@click="$emit('click', $event)"
- 当
<svg>
被点击时,事件传递至组件<Icon/>
- 处理
<svg>
其他事件
- 可以不用一个一个地暴露不同类型的事件出去
- 在父组件上使用
.native
修饰符 <Icon @click.native="goBack"/>
- 可省略以上子组件内传递原生事件的过程:
@click="$emit('click', $event)"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
<template>
<Layout>
<div class="headerBar">
<Icon class="left-icon"
name="money_right"
@clcik.native="goBack"/>
<span class="title">编辑标签</span>
</div>
<FormItem class="form-item"
field-name="标签名"
placeholder="在这里输入标签名"/>
<div class="button-wrapper">
<Button>删除标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel.ts';
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.fetchData();
const tags = tagListModel.data;
// const tag = tags.filter(t => t.id === id)[0];
const tag = tags.find(t => t.id === id);
if (tag) {
console.log(tag);
} else {
this.$router.replace('/404');
}
}
goBack() {
this.$router.back();
}
}
</script>
// ...
|
EditLabel.vue
之 SCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
.headerBar {
text-align: center;
font-size: 16px;
padding: 12px 16px;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
> .left-icon {
transform: rotate3d(0, 1, 0, 180deg);
color: #666;
font-size: 24px;
}
> .title {
}
&::after {
content: '';
display: inline;
width: 24px;
height: 24px;
}
}
.form-item {
margin-top: 8px;
background: #fff;
-webkit-box-shadow: 0 1px 1px 0 #BCBBC1;
-moz-box-shadow: 0 1px 1px 0 #BCBBC1;
box-shadow: 0 1px 1px 0 #BCBBC1;
}
.button-wrapper {
text-align: center;
padding: 16px;
margin-top: 44-16px;
}
|
- 之后需要抽取顶部栏组件
headerBar
- 布局: 让图标绝对定位,让标题居中
- 顶部栏高度用
line-height
撑开,并不好
- 图标绝对定位,难以居中
- 改用
flex
,加border: 1px solid red;
确认位置
EditLabel.vue
功能实现
- 实现编辑标签的功能
- 可以点击查看标签具体内容的页面,包括标签名
- 可以修改标签名
- 可以删除标签
修改FormItem.vue
- 从外部获取数据
inputValue
,此时会提示Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders...
v-model
改了外部数据
v-model="inputValue"
等价于:value="inputValue" @input="inputValue = $event.target.value"
(在input
标签内) 对此时的外部数据进行直接操作是不合法的
- 改为监听自定义事件
oninputValueChanged($event.target.value)
,发布修改的信息给父组件,通知父组件修改数据
- 用户在修改
inputValue
时,子组件不会直接对inputValue
进行变更,而是将inputValue
的值通过自定义方法传递出去,通知给父组件
oninputValueChanged(newValue: string) { this.$emit('update:InputValue', newValue); }
- 注意事件名统一:
- 子组件
this.$emit('update:inputValue', newValue);
- 父组件监听
@update:inputValue="updateTag"
FormItem.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
<template>
<div class="form-wrapper">
<label class="form-item">
<span class="name">{{ this.fieldName }}</span>
<input type="text"
@input="oninputValueChanged($event.target.value)"
:value="inputValue"
: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 FormItem extends Vue {
@Prop({default: ''}) inputValue!: string;
@Prop({required: true}) fieldName!: string;
@Prop({default: ''}) placeholder?: string;
@Watch('inputValue')
oninputValueChanged(newValue: string) {
this.$emit('update:InputValue', newValue);
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/style/global.scss";
.form-wrapper {
background: transparent;
.form-item {
align-items: center;
display: flex;
font-size: 14px;
padding-left: 16px;
.name {
padding-right: 16px;
}
input {
height: 40px;
flex-grow: 1;
background: transparent;
padding-right: 16px;
}
}
}
</style>
|
EditLabel.vue
- 取到标签的数据,赋值给内部数据
tag
- 先声明数据类型
inputValue?: {id: string, name: string} = undefined;
- 标签创建完成时的钩子
created(){...}
,从本地数据库中得到的数据赋值给tag
- 获取页面
url
中的id
,通过id
在所有标签中找到对应的tag
,将tag
赋值给inputvalue
- 得到
tag
后,在模板中展示:value="tag.name"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
<template>
<Layout>
<div class="headerBar">
<Icon class="left-icon" name="money_right" @click.native="goBack"/>
<span class="title">编辑标签</span>
</div>
<FormItem :inputValue="tag.name" class="form-item" field-name="标签名" placeholder="在这里输入标签名"/>
<div class="button-wrapper">
<Button>删除标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel.ts';
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;
created() {
const id = this.$route.params.id;
tagListModel.fetchData();
const tags = tagListModel.data;
// const tag = tags.filter(t => t.id === id)[0];
const tag = tags.find(t => t.id === id);
if (tag) {
this.tag = tag;
} else {
this.$router.replace('/404');
}
}
}
...
</script>
...
|
用户在EditLabel.vue
的输入框中修改标签名 updateTag()
- 监听
@update:inputValue
事件
@update:inputValue="update"
- 值由
FormItem.vue
中@input="oninputValueChanged($event.target.value)
的this.$emit('update:inputValue', newValue);
而传来,类型为字符串
- 传给方法
update(name: string) {...}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
<template>
<Layout>
<div class="headerBar">
<Icon class="left-icon"
name="money_right"
@click.native="goBack"/>
<span class="title">编辑标签</span>
</div>
<FormItem :inputValue="tag.name"
@update:inputValue="updateTag"
class="form-item"
field-name="标签名"
placeholder="在这里输入标签名"/>
<div class="button-wrapper">
<Button>删除标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel.ts';
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;
created() {
const id = this.$route.params.id;
tagListModel.fetchData();
const tags = tagListModel.data;
// const tag = tags.filter(t => t.id === id)[0];
const tag = tags.find(t => t.id === id);
if (tag) {
this.tag = tag;
} else {
this.$router.replace('/404');
}
}
updateTag(name: string) {
console.log(name);
}
}
...
</script>
|
- 查看
updateTag()
方法打印出的参数
- 重命名为
update()
方法
tagListModel.ts
添加方法
- 更新数据
updateData(id, name) {...}
- 删除数据
removeData(id: string) {...}
将修改的name
保存到本地数据tag.name
- 在
tagListModel.ts
中添加update(id: string, name: string) {...}
方法
- 在
tagListModel.ts
中添加removeData(id: string) {...}
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
const localStorageKeyName = 'tagList';
type Tag = {
id: string;
name: string;
}
type TagListModel = {
data: Tag[];
fetchData: () => Tag[];
create: (name: string) => 'success' | 'duplicated';
saveData: () => void;
updateData: (id: string, name: string) => 'success' | 'not found' | 'duplicated';
removeData: (id: string) => boolean;
}
// return this.data | 'success' | 'duplicated' | 'not found'
const tagListModel: TagListModel = {
data: [],
fetchData() {
this.data = JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]');
return this.data;
},
create(name: string) {
// this.data = [{id: '1', name: '1'}, {id: '2', name: '2'}]
const names = this.data.map(d => d.name);
if (names.indexOf(name) >= 0) { return 'duplicated'; }
this.data.push({id: name, name: name});
this.saveData();
return 'success';
},
saveData() {
localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));
},
updateData(id: string, name: string) {
const idList = this.data.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const nameList = this.data.map(item => item.name);
if (nameList.indexOf(name) >= 0) {
return 'duplicated';
} else {
const tag = this.data.filter(item => item.id === id)[0];
tag.name = name;
tag.id = name;
this.saveData();
return 'success';
}
} else {
return 'not found';
}
},
removeData(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.saveData();
return true;
}
};
export default tagListModel;
|
EditLabel.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
<template>
<Layout>
<div class="headerBar">
<Icon class="left-icon" name="money_right" @click.native="goBack"/>
<span class="title">编辑标签</span>
</div>
<FormItem :inputValue="tag.name"
@update:inputValue="update"
class="form-item" field-name="标签名" placeholder="在这里输入标签名"/>
<div class="button-wrapper">
<Button @click="remove">删除标签</Button>
</div>
</Layout>
</template>
<script lang="ts">
import Vue from 'vue';
import {Component} from 'vue-property-decorator';
import tagListModel from '@/models/tagListModel.ts';
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;
created() {
const id = this.$route.params.id;
window.tagList;
const tags = tagListModel.data;
// const tag = tags.filter(t => t.id === id)[0];
const tag = tags.find(t => t.id === id);
if (tag) {
this.tag = tag;
} else {
this.$router.replace('/404');
}
}
update(name: string) {
if (this.tag) {
tagListModel.update(this.tag.id, name);
}
}
remove() {
if (this.tag) {
tagListModel.remove(this.tag.id);
window.alert(`成功删除标签:${this.tag.name}`);
tagListModel.fetch();
this.$router.back();
}
}
goBack() {
this.$router.back();
}
}
</script>
...
|
bug: 在改name
时,对应修改id
为name
但可能会生成两个相同的id
id
生成器
id 的原则
- id 用来定位数据
- 一旦给了 id,就不要修改
- id 不能重复
- id 自增需要注意爆栈问题,JS不能显示超过17位数字
12345678912345678 + 1
,精度不够
- 当标签为空时,id 可清零重新计算
- 使用了闭包的原理
添加自定义库src/lib/idCreator.ts
1
2
3
4
5
6
7
8
9
10
|
let id: number = parseInt(window.localStorage.getItem('_idMax') || '0') || 0;
function createId() {
id++;
window.localStorage.setItem('_idMax', id.toString());
return id;
}
export default createId;
|
在创建标签时引入 tagListModel.ts
createId()
- 重构重命名了方法
fetch
uodate
remove
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
import createId from '@/lib/createId';
const localStorageKeyName = 'tagList';
type Tag = {
id: string;
name: string;
}
type TagListModel = {
data: Tag[];
fetch: () => Tag[];
create: (name: string) => 'success' | 'duplicated';
save: () => void;
update: (id: string, name: string) => 'success' | 'not found' | 'duplicated';
remove: (id: string) => boolean;
}
// return this.data | 'success' | 'duplicated' | 'not found'
const tagListModel: TagListModel = {
data: [],
save() {
localStorage.setItem(localStorageKeyName, JSON.stringify(this.data));
},
fetch() {
this.data = JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]');
return this.data;
},
create(name: string) {
// this.data = [{id: '1', name: '1'}, {id: '2', name: '2'}]
const names = this.data.map(d => d.name);
if (names.indexOf(name) >= 0) { return 'duplicated'; }
const id = createId().toString();
this.data.push({ id, name: name });
this.save();
return 'success';
},
update(id: string, name: string) {
const idList = this.data.map(item => item.id);
if (idList.indexOf(id) >= 0) {
const nameList = this.data.map(item => item.name);
if (nameList.indexOf(name) >= 0) {
return 'duplicated';
} else {
const tag = this.data.filter(item => item.id === id)[0];
tag.name = name;
tag.id = name;
this.save();
return 'success';
}
} else {
return 'not found';
}
},
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;
}
};
export default tagListModel;
|
纠错
FormItem.vue
对于输入的inputValue
不必使用 @Watch('inputValue')
,这一行可以删掉
input
的值一旦被用户变化,就会触发自定义事件 @update:inputValue
,所以就没必要再加一个 watch
了
- 加
watch
会触发两遍
Vue
的 template
里没必要写 this.
还有许多功能性的bug,需引入全局状态
- 标签组件和记账组件不共享标签数据
- 在记账组件中新增标签的数据不会同步到标签组件中
- 切换标签组件和记账组件时,新增标签的数据丢失
- 用户手动清空
FormItem.vue
时,返回报错