【项目-喵内记账-meoney-02】Money.vue 组件
大纲链接 §
[toc]
Figama UI
整体思路 ⇧
代码超过 150行 就需要考虑模块化分割
- HTML按结构逻辑:整体从上到下,局部从外到里依次写出
- 将HTML按结构或功能分为若干个组件
- 将SCSS分为
reset
、全局、变量、局部 - TypeScript依据单一功能原则,模块化
目前文件结构
|
|
App.vue
中只展示router-view
- 所有完整展示页面放在
src/views
路径下 - 所有功能的组件放在
src/components
路径下 - 全局样式和初始化样式放入
src/assets/style
路径下 - 从
router
读取不同router-view
,默认展示页面Money.vue
,在Nav.vue
组件中展示路由选项 Money
大致结构为div.layout-wrapper
中包含- 内容展示页
div.content
- 导航标签页
Nav
- 其他展示页面结构相同
- 内容展示页
- 分别提取出
Layout.vue
、Nav.vue
组件Layout.vue
:div.wrapper
(div.content
+Nav
)- 不同页面中的
div.content
用匿名插槽<slot/>
div.content
(Money
|Labels
|Statistics
)
- 不同页面中的
Nav.vue
:<nav></nav>
(若干个<router-link/>
)
router-view
>Layout
>div.content_
(Money
|Labels
|Statistics
) +Nav
HTML初步结构 ⇧
- 先整体 后细化–类似递归
Money.vue
|
|
CSS 思路 ⇧
reset.scss
- 全局
- 变量
- 局部
mixins.scss
CSS reset
⇧
- 重置样式设置的组件中不可用
scope
,不仅仅影响当前组件,需要影响整个页面 - 去除选中状态的
:focus {outline: none;};
reset.scss
|
|
全局样式(字体、行高) ⇧
字体
fonts.css
搜索"fonts.css" 中文
- 黑体-横竖等宽,宋体-横细竖粗
Fonts.css
– 跨平台中文字体解决方案- 使用黑体字系列的最佳实践
- GitHub fonts.css
- 腾讯云 字体 | Fonts
global.scss
|
|
App.vue
|
|
- 柔和的黑色
#333
- 按照组件来隔离样式,如果将样式写在
body
上,团队间注意可能会互相影响的样式 - 选择将总体样式写在
#app
上 font-size: 16px;
明确设置总体字体大小,局部可覆盖演示
变量 ⇧
- 所有变量放到
global.scss
- 字体以
$font-
开头 - 颜色以
$color-
开头
global.scss
|
|
global.scss
中除了SCSS变量、函数和Mixin
以外不要放任何其他样式global.scss
是被各个模块多次引用的,每引用一次,就多复制一次- 变量会最终被编译器删掉,文件也不存在
局部样式 ⇧
- 预先写好CSS结构
Money.vue
|
|
分为四个部分结构
div.tags
部分结构label.notes
部分结构ul.types
部分结构div.numpad
部分结构
div.tags
部分结构 ⇧
|
|
border-radius: 50%;
默认是指宽度的50%
,而需要的效果是高度的50%
- 设置高度
height: 24px;
- 设置
border-radius: (24px/2);
,注意写法(24/2)px
是错误的 - 提取变量
$h: 24px;
,使用height: $h; border-radius: ($h/2);
- 使用
padding: 0 16px;
而不是宽度开控制盒子整体尺寸 - 使用
margin-right: 4px;
,不使用margin-left
,左边空隙在外面容器上加样式 - 覆盖掉全局样式
font-size: 14px;
- 字体默认不继承父元素,不继承
body
上设置的字体,必须在reset.scss
中重置button, input { font: inherit;}
- 字体未设置垂直居中,调大高度可以发现目前并不居中
- 用
flex
布局 - 确保只有一行文字时,可用行高
line-height
和元素高height
一样
- 用
- “新增按钮”样式
button {background: transparent;border: none;border-bottom: 1px solid;color: #999;}
- 注意细节,下划线长于文字 添加
padding: 0 3px;
- 注意细节,下划线长于文字 添加
label.notes
部分结构 ⇧
- 左右结构
- 将
label
标签的display
属性从inline
改为block
,否则不显示背景色 display: flex;
和垂直居中align-items: center;
|
|
ul.types
部分结构 ⇧
- 左右结构
- 容器不用确定的宽度
> li {width: 50%;}
|
|
- 用
height: 64px; display: flex; justify-content: center; align-items: center;
代替line-height: 64px;
- 注意添加
&.selected {border-bottom: 4px solid;}
时会多一点高度,而处于未选中状态时,底部边框高度消失,切换时造成文字抖动 - 所以不能用会影响文字所占空间的
border
- 附加的元素可用伪元素
&.selected::after {}
- 附加的元素可用伪元素
div.numpad
部分结构 ⇧
- 输出结果栏用等宽字体
font-family: Consolas, monospace
|
|
- 数字键盘用
float
布局时注意清除浮动float: left;
- 提取清除浮动的共用样式
placeholder
到global.scss
%clearFix { &::after { content: ''; display: block; clear: both; } }
- 在父元素上引用
@extend %clearFix;
- 相当于将所有拥有此
placeholder
的选择器名都复制过去,并用逗号分隔,替换掉原先%clearFix
的位置
- 提取外阴影公共样式
%outShadow {box-shadow: inset 0 -5px 5px -5px fade_out(black, 0.6), inset 0 5px 5px -5px fade_out(black, 0.5);}
- float布局 V.S. 网格结构
grid
布局
之后会改为
grid
布局,用数据循环生成键盘结构
提取变量到global.scss
⇧
|
|
为了布局
Money
内部模块,改进Layout.vue
- 需求:根据不同的展示页面,
div.layout-wrapper
>div.content
使用不同的样式 - 如果在
Money.vue
中Layout
标签上加样式属性class="xxx"
- 因为使用了
Layout.vue
组件,实际会加到Layout.vue
的根标签div.layout-wrapper
上- 但预期应该是样式加在更里面一层的
div.content
上,从而控制Money.vue
内部四个模块的布局样式(.tags
、.notes
、.types
、.numpad
),而Nav
组件不受影响 - 结构:
Layout
>div.layout-wrapper
>div.content_
(Money
|Labels
|Statistics
)+Nav
- 但预期应该是样式加在更里面一层的
- 方法: 使用外部数据 动态绑定到内部具体某个标签样式属性上
- 在
Layout.vue
组件中引入外部数据props: ['classPrefix']
- 绑定动态样式,标签中可以同时出现
class
和绑定的:class
属性,会自动合并 - 由
Layout.vue
外部Money
组件传来的数据<Layout class-prefix="layout">...</div>
得到props: ['classPrefix']
(Vue
会自动处理标签和js中的大小写,自动识别对应的驼峰和连字符写法,HTML标签中的属性名不可有大写的字符)
- 在
- 但
Money.vue
组件的<style scoped>
是由有范围的,加在原来样式标签里会因为作用域而无效,有scoped
属性的样式,只能在当前组件中有效,在Layout.vue
组件中无效 - 再添加一个没有
scoped
属性的样式标签:Vue
的单文件组件中可以有多个样式标签
重构Money.vue
,其中<style>
与<style scoped>
并存 ⇧
|
|
- 文档中必须写清楚
- 参考
ant-design
UI库的做法,通过前缀,传不同外部数据,来区分props: ['classPrefix']
重构Layout.vue
⇧
|
|
- 注意
:class="classPrefix && `${classPrefix}-wrapper`"
,双引号里的是JS表达式 - 用反引号语法作为插值,写入外部数据
${classPrefix}
为变量 Layout.vue
从Money.vue
中标签<Layout class-prefix="layout">
接受数据class-prefix
- 动态绑定为
\`${classPrefix}-wrapper\`
进行拼接 - 这样秩序按照前缀就可拼接应用到更多其他样式中
- 组件外接受
classPrefix
变量,说明组件内有供外部控制的CSS class
,但不是一个个传,只需传一个前缀,对应活干多个元素上的class
vue deep
防止样式互相覆盖
Money.vue
组件模块化思路 ⇧
vue组件输入补全提示,必须先输入
<
, 而直接打组件名不会出不全提示
分割提取Money
中的组件 ⇧
可使用
webStorm
自动提取,自动补全引用路径,但会丢失样式,以及提取的目录只能是当前目录,需要手动移动
- 保证每个模块代码不超过 150行,通用封装组件,避免屎山
- 避免同名可建目录,
webStorm
自动重构引入路径 - 分割提取
Money.vue
为四个子组件:Tags.vue
、Notes.vue
、Types.vue
、Numpad.vue
- 注意组件的引入路径和样式的依赖
Money.vue
|
|
使用TypeScript
与装饰器写Types
组件 ⇧
- 用来分辨支出还是收入,从而展示不同UI
用Types
组件的 JS 对比 TS 写法 ⇧
Types.vue
JS 写法
|
|
Money.vue
未用到装饰器的TS
写法
|
|
- Vue 处理绑定值为
undefined
自动去除 三目运算符返回的false
值,变为空值:<li :class=" type === '-' ? 'selected' : '' ">支出</li>
- 点击时,触发函数并传参
selectType('-')
- 注意绑定的样式
'selected'
是字符串类型<li :class="type === '-' && 'selected'" @click="selectType('-')">支出</li>
Types
组件得到外部数据"Hi"
Types.vue
TS
写法 第一个 TS Vue
组件 ⇧
vue/cli
初始化时已使用TS
配置tsconfig.json
|
|
安装 装饰器
vue-property-decorator
库使用ts装饰器写法
|
|
src/components/Money/Types.vue
|
|
TypeScript
只支持在 Vue 的<script>
标签里,<template>
标签里只支持 JS 表达式TypeScript
使用前先导入引用import Vue from 'vue';
也可从import {Vue,} from 'vue-property-decorator';
中导入TypeScript
语句末尾加分号TS Vue
组件不用构造选项,构造选项没有类型TS Vue
组件使用 类组件写法:export default class Types extends Vue {}
TypeScript
引入装饰器来自动处理export default class
里的 成员变量 和 方法声明- 解析为
initial data
method
computed
prop
lifecycle hook
等数据 import {Component} from 'vue-property-decorator'; @Component...
TypeScript
赋值语句自动变为实例的内部数据data
- 参数
type
默认是any
类型的,但 TS配置 不允许any
类型的 - 必须声明类型:
selectType(type: string) {...}
- 解析为
- 使用装饰器
vue-property-decorator
库 - 代替 Vue Class Component 官方的库
vue-class-component
vue-property-decorator
使用指南- vue 官方的装饰器库
vue-class-component
的装饰器@Component
- CRM旧版文档
使用 装饰器[vue-property-decorator
] @Prop
目的 ⇧
目的
- 单一原则
- 关注点分离
- 装饰器使代码逻辑 高内聚
- Vue 使代码逻辑低耦合
- 解决了使用选项
options
写法时,同一逻辑分散在各个选项属性中
实践
- 参照文档添加
@Prop(Number) readonly propA: number | undefined;
- 指定类型为
Number
只读 - 属性名为
propA
- 默认取值必须是
number | undefined
- 此时传来的外部数据必须是数字类型,否则会报错
| undefined
为编译时的检查:this.xxx.yyy
直接出现红下划线,意味着在运行代码前接报错- 即 TS 可以提前告知代码错误
- (需添加判断
if(this.xxx === undefined){console.log('没有xxx')}else{console.log(this.xxx.yyy)}
消除红线代码警告) - 还会继续检查是否存在
yyy
,改成this.xxx.toString()
才可通过检查
- 指定类型为
Alt + Enter
添加引入import {Component, Prop} from 'vue-property-decorator';
- 装饰器可以极大程度简化Vue组件中各种状态的声明
Money.vue
|
|
Types.vue
|
|
@Prop(Number) xxx: number | undefined;
Number
是指运行时的类型number | undefined
是指编译时的类型- TS 增加了类型声明
使用TypeScript
⇧
- 命令行中查看TS最新版本
npm info typescript version
package.json
中可以改版本,之后yarn install
自动卸载重装- 手动重启
webStorm
TypeScript
的好处
- 类型提示:更智能的提示
- 编译时报错:还没运行代码就知道自己写错了 无法编译成 JS
- 类型检查:无法
.
点出错误的属性
写 Vue 组件的三种方式(
*.vue
单文件组件)
1.用 JS 对象 export default { data, props, methods, created, ...}
2.用 TS 类 <script lang="ts">
默认使用class XXX
*推荐
|
|
3.用 JS 类 <script lang="js">
|
|
TypeScript
的本质
JS + Type
TypeScript
通过编译器 编译为JavaScript
,再给浏览器- 编译时 V.S. 运行时
- 编译时 如果有编译错误 终端 Error 导致编译失败 浏览器不自动刷新 不更新 JS
- 运行时 如果错误 浏览器控制台 Error
- 使用编译器
TSCompiler
检查,删掉类型就是 可以在浏览器运行的JS - 为了开发的流畅,就算TS编译报错,也能跳过错误,继续编译JS不停止,除非设置
tsconfig.json
添加{"compilerOptions": { "noEmitOnError": ..., ...} ...}
抽出封装NumPad.vue
组件 ⇧
功能需求 ⇧
- 点击
0~9
的数字,在div.output
显示相应的数字(其实是字符串) - 点击
.
,添加小数点 - 点击
清空
,div.output
显示0
- 点击
OK
,确认标签名,添加相应的标签 (后续功能实现)
注意点 ⇧
- 先输入
0
,再输入.
,0.
两个字符都会显示在div.output
中 - 通常用字符串显示完整数字
0.130
- 一开始未设置默认高度,
div.output
塌陷 - 必须给默认值,高度撑起文档流,使用
'0'
(空格或$nbsp;
都不行) - 如果设置
min-height
,添加计算样式的高度,可能会造成页面的闪烁 - 从
Numpad
中输入的不是数字,是字符串,包含小数点.
的字符 Vue.js
的模板template
中不使用TS语法,无法做类型检查,只支持JS- 方法中不传参,绑定到事件上,
Vue.js
会自动传参,这个参数就是和此事件相关的所有信息的事件对象- 通常取名
event
或e
- 方法
inputContent(event) { event.target.textContent }
event.target.textContent
可以获取到当前元素里的内容,即写入的文本节点的字符串- TS 对 参数
event
进行类型检查,声明对象所属的类inputContent(event: Event) {...}
- 没有所谓的点击事件类,只有鼠标事件类、键盘事件类和UI事件,用户事件等 MDN 鼠标事件
click
属于MouseEvent
类,是DOM
内置的类型,更具体地声明参数类型inputContent(event: MouseEvent) {...}
- 声明方法参数的类型
inputContent(event: MouseEvent) {console.log(event.target.textContent);}
event.target
错误提示可能为空值null
,需要添加判断语句event.target.textContent
也可能为空值,比如图片元素就没有文字内容- 使用 TS 断言 可以强制指定为按捏元素的类型
(event.target as HTMLButtonElement)
来代替判断语句 Vue2.0
和 TS 的结合得不太好
- 通常取名
实现 ⇧
- 需要一个响应式
data
保存输出字符串output: string = ''
- TS 中变量有默认值,就不用声明变量的类型,可简化为
output = ''
- TS 中变量有默认值,就不用声明变量的类型,可简化为
- 插值
{{output}}
- 点击事件
<button @click="output += 1">1</button>
- 抽象出方法
inputContent(content: string) {}
- 改写事件方法
<button @click="inputContent("1")">1</button>
,注意类型是字符串 - 重复太多 改写事件为使用获取目标元素的文本内容
inputContent(event: MouseEvent) {const button = (event.target as HTMLButtonElement);}}
- 不传参
<button @click="inputContent">1</button>
- 点击事件方法有默认参数
event
时执行不需加括号
功能限制与出现的Bug
⇧
- 点击两次
'0'
,重复出现'0'
- 小数点前,当前存在的值为以
'0'
开头,点击多次'0'
,不重复出现'0'
- 分别输入
'01'
自动消'0'
,变为'1'
- 小数点后,可以多次出现
'0'
- 小数点前,当前存在的值为以
- 点击两次小数点
'.'
,重复出现'.'
- 正确的为点击一次
'.'
,出现'.'
,再次点击'.'
,不出现'.'
- 正确的为点击一次
- 默认
'0'
存在,直接输入'.'
,显示''0.
- 显示的数字长短限制
使用条件判断解决Bug
|
|
- 预想值必为字符串
const input = button.textContent as string;
,排除空值的情况- 语法简化为
const input = button.textContent!
,但通不过TSlint
检查,所以不常用
- 语法简化为
- 合并逻辑 输入
0
| 输入其他数字1~9
- 以任何数字(0~9 一位)开头输入都替换显示的数字
this.output = input;
删除逻辑 ⇧
|
|
清空逻辑 ⇧
|
|
确认添加标签逻辑 ⇧
|
|
出现的Bug2
⇧
- 移动端输入300ms延迟,不顺畅
<button @click="inputContent">1</button>
改为<button @touchstart="inputContent">1</button>
- 每个按钮重复绑定事件
- 限制小数到 人民币 分的单位
Numpad.vue
|
|
抽出封装Notes.vue
组件 ⇧
notes
模块 -v-model
- 备注功能,输入框,提交输入信息
|
|
- 绑定
input
标签的:value
值 - 监听
input
标签的@change
事件 - 给
@change
事件 原生HTML自带标签拿到 事件目标值@change="value = $event.target.value"
@change
只有光标移入才会触发,改为@input
事件更合适- 提取为自定义事件
@input="onInput"
写自定义方法onInput(event: KeyboardEvent) {...}
v-model
简写: ⇧
- 绑定数据
:value="xxx"
- 原生输入事件
@input="xxx = $event.target.value"
获取事件对象的目标值 - 以上两句可以直接简写为
v-model="xxx"
,无需写method
xxx
一般命名为value
Notes.vue
|
|
v-model
就是:value
+@change=
语法糖,模拟双向绑定
抽出封装Tags.vue
组件 ⇧
|
|
- TS 声明 字符串数组
tags: string[] = []
成员只能是字符串的数组 - 标签是外部数据传来的
@Prop(Array) tags: string[] = [];
- 必须加括号
@Prop() tags: string[];
- 必须加括号
- 由于是外部数据,可不赋默认值
@Prop(Array) tags: string[] | undefined;
重构外部父组件 Money.vue
⇧
|
|
- 之后
tags
可以从localStorage
读取
重写Tags.vue
组件的 template
, 指令v-for
遍历<li>
⇧
<li v-for="tag in dataSource" :key="tag">{{ tag }}</li>
|
|
添加选中的逻辑和选中样式 Tags.vue
⇧
- 点击选中的逻辑
<li v-for="tag in dataSource" :key="tag" @click="select(tag)">{{ tag }}</li>
- 点击选中的方法
select(tag: string) {this.selectedTags.push(tag);}
- 绑定选中时的样式
- 使用对象的形式:
:class="{selected: true | false}"
true
表示起效,false
表示无效 填写JS表达式来判断- 选中逻辑
:class="{selected: selectedTags.indexOf(tag)>=0}"
选中的标签数据是否包含当前标签名,字符串
- 使用对象的形式:
- 给选中的标签加样式
&.selected {...}
如果点击选中,则添加样式 selecte
选中的逻辑改为切换的逻辑toggle
,通过点击切换,是否添加 选中的样式
|
|
新增标签的逻辑 ⇧
- 点击 新增标签 按钮 绑定点击事件
<button @click="createTag">新增标签</button>
- 创建标签的方法
createTag(){...}
- 创建标签后 不能直接操作外部数据
- 通过发布一个自定义事件来通知外部改变数据
- TS 在声明外部数据时 添加
readonly
属性
|
|
- 通知父组件更改数据
this.$emit('update:dataSource', [...this.dataSource, name]);
<Tags :data-source="tags" @update:dataSource="tags = $event"/>
.sync
语法糖简写<Tags :data-source.sync="tags"/>
.sync
修饰符 Money.vue
⇧
- 子组件触发
this.$emit('undate:dataXxx', [...args])
- 在父组件中对应标签绑定的属性上添加
.sync
修饰符 就得到子组件传来的数据[...args]
|
|
TS 的 prop
类型错误 ⇧
- 留了一个坑,不写
@Props(这里的类型)
,只写冒号后面的类型,@Prop() xxx!: boolean;
- 这种偷懒的写法会在很后面造成一个 bug。应该写成
@Prop(Boolean) xxx!: boolean;
- 原因:
- 左边的
Boolean
是跟Vue
说xxx
的类型是Boolean
(运行时类型) - 如果不写左边的
Boolean
,那么Vue
就不知道xxx
的类型是什么了,默认就把xxx
当作字符串了 - 因此,当你给
xxx
传值时,Vue
会将其自动转换成字符串。而与我们的预期「xxx
是boolean
」不相符,这就是bug
。
- 左边的
收集四个组件的 value
⇧
收集最新选中的标签 ⇧
- 触发
toggle(){...}
时,发布数据给父组件Money
的<Tags/>
标签this.$emit('update:selectedTags', this.selectedTags);
Tags.vue
|
|
修改
Money.vue
|
|
分别收集标签的备注信息、类型信息、数额信息(点击ok
时) ⇧
Money.vue
|
|
- 备注信息
notesValues
- 类型信息
type
- 数额信息
amount
Notes.vue
为数据inputValue
添加监视属性watch
使用 TS @Watch
装饰器 ⇧
|
|
@Watch('inputValue')
参数中写需要监听的响应式变量- 之后紧接一个函数表达式,参数为
newValue
表示新值oldValue: string
没用到,可删
Types.vue
在selectType()
触发时 发布this.$emit('update:value', value)
⇧
- 为避免数据没有变化时重复触发
selectType()
,使用 TS@Watch
装饰器,惰性监听
|
|
this.$emit('update:value', value);
- 注意不带
'@update:value'
- 勿写成
this.$emit('@update:value', value);
- 注意不带
Numpad.vue
在confirmContent()
触发时 this.$emit('update:value', this.output)
⇧
|
|
Money.vue
监听四个组件的变化,并收集传值放到Record
类的数据record
中 ⇧
- TS 声明数据类型
|
|
- 再声明数据(复杂类型)并赋初始值
record: Record = {tags: [], notes: '', type: '-', amount: 0}
- 触发对应方法时改变数据
|
|
父组件统一传默认值给子组件 ⇧
- 子组件的默认数据靠父组件传值而来,而在子组件自己内部赋的默认值是不可靠的。(有可能变更需求时忘记改)
Type.vue
组件中的数据type
应该是由父组件传入- 无需声明 data
type = '-'; // '-' 表示支出, '+'表示收入
- 无需监视
@Watch('type')
- 无需声明 data
<Types :value="record.type" @update:value="onUpdateType"/>
- 简化
<Types :value.sync="record.type"/>
- 简化
- 其它组件也做同样操作
Numpad.vue
|
|
Types.vue
|
|
使用.sync
修饰符 绑定四个组件的 value
⇧
Money.vue
|
|
使用window.localStorage
持久化存储四个组件的 value
⇧
- 需要将收集的
record
保存到本地
Numpad.vue
添加按下 ok 发布提交数据事件的逻辑
|
|
写入localStorage.setItem
修改Money.vue
⇧
- 监听提交事件
<Numpad :value.sync="record.amount" @submit="saveRecord"/>
- 保存记录方法
saveRecord(){ this.recordList.push(this.record);}
- 注意保存数据的操作逻辑统一放到监听数据变化的
@Watch
上 - 避免多处出现保存数据的操作
- 好处是只要发现监听的数据变化了,自动触发
@Watch
的方法
- 注意保存数据的操作逻辑统一放到监听数据变化的
- 用
@Watch('recordList')
监听recordList
变化onRecordeChange() {localStorage.setItem('recordList', JSON.stringify(this.recordList))}
|
|
数据引用的Bug ⇧
- 生成多条数据后,查看
Application
里保存的localStorage
中保存的数据都一样 - 保存记录
saveRecord(){ this.recordList.push(this.record);}
推入数组的是一个内存的地址引用 - 需要深拷贝数据
JSON.parse(JSON.stringify())
|
|
读取localStorage
⇧
- 默认的
recordList
从window.localStorage.getItem('recordList')
中读取 - 注意
recordList
的类型是Record[]
复杂数组 window.localStorage.getItem('recordList')
读取的数据是以字符串形式保存的- 反序列化
JSON.parse()
- TS 提示
window.localStorage.getItem('recordList')
有可能取到的值是null
window.localStorage.getItem('xxx')
获取一个没有setItem
的key
就会得到null
- 解决方法是赋一个空的字符串数组
recordList: Record[] = JSON.parse(window.localStorage.getItem('recordList') ?? '[]');
recordList: Record[] = JSON.parse(window.localStorage.getItem('recordList') || '[]');
保存数据时再加一个时间戳createdAt: new Date(2020,0,0)
⇧
- TS 声明类型时除了写基本数据类型外,还可以写内置的对象(类/构造函数)
type Record = { tags: string[]; notes: string; type: string; amount: number; createdAt: Date | undefined; }
type Record = { tags: string[]; notes: string; type: string; amount: number; createdAt?: Date; }
Money.vue
|
|
之前的
localStorage
中未保存时间戳,新加的数据带有时间戳
*数据迁移是什么(数据库版本) ⇧
- 在
localStorage
中添加数据库版本 window.localStorage.setItem('version', '0.0.1');
- 数据迁移:数据库升级 补充或消除原先的数据
- 数据迁移复用,一次只做一个版本的迁移;跨版本只需一次一次地升级
- 预先规划好数据
|
|
MVC
封装Model
封装数据(库/本地存储)有关的操作 ⇧
- 获取数据
fetchData() {...}
- 保存数据
saveData() {...}
- 导出
export default model
- 引用
import model from '@/model.js'
到Money.vue
创建model.js
⇧
|
|
Money.vue
引入model.js
|
|
- TS 中不能用
import
直接引入 JS 的模块 比如import model from '@/model.js';
- 使用
require('xxx').default
语法引入model.js
默认导出的export default model
:const model = require('@/model.js').model;
- 或者在
model.js
中export {model}
- 在
Money.vue
中require('xxx').model
- 在
- 或用解构
const {model} = require('@/model.js').model
- 在
Money.vue
中require('xxx')
- 在
- 缺点是导入的JS模块是没有任何类型信息的
Money.vue
|
|
改为model.ts
⇧
- 全局声明类型时避免类型冲突
Record
–>RecordItem
- 新建TS全局声明文件
custom.d.ts
,文件名任意,只需注意后缀*.d.ts
custom.d.ts
|
|
model.js
文件名改写为model.ts
- 可识别 TS 文件的默认导出
import {model} from '@/model.ts';
- 声明形参的类型
saveData(data: RecordItem[]) {...}
- 断言返回值的类型
return JSON.parse(window.localStorage.getItem(localStorageKeyName) ?? '[]') as RecordItem[];
- 封装深拷贝
clone
方法
mdoel.ts
|
|
- 删除
Money.vue
中重复的类型声明recordList: RecordItem[] = model.fetchData();
model.fetchData()
已经在model.ts
中声明返回值的类型,无需重复声明recordList: RecordItem[]
- 在声明时提前写好类型断言,调用时就可以不写, TS 自动推测
mdoel.ts
|
|
- 导入
import {model} from '@/model.ts'; // export {model}
- 或改成
import recordListModel from '@/mdoel.ts'; // export default model
重构Money.vue
⇧
|
|
一些bug修正与代码重构 ⇧
成功 按条件 给不同的事件类型(click | touchstart) 动态绑定 一个 按条件选出的事件 ⇧
@[clientEvent]="handleButtonFn($event, item.bundleEvent)"
- 显示地传参事件对象
$event
:name="item.id"
'delete' | 'clear' | 'ok'
|
|
内置事件类型名注意全小写
mousemove
touchstart
- 通过浏览器开发工具>事件侦听器查看具体的元素上绑定的事件名是否写错
实现仿win10计算器和日历的探照灯效果 ⇧
getParent()
找父节点showSearchlight()
显示探照灯background: radial-gradient(circle at var(--x-pos) var(--y-pos), #aaa, transparent 100px);
|
|
重构searchlight逻辑 不进行DOM操作
- 绑定节点的
style
属性 - 设置变量
var(--x)
,记录鼠标位置 - 根据鼠标位置设置
background: radial-gradient();
的样式
|
|
使用.sync
⇧
<FormItem ... @update:inputValue="onUpdateTips" :inputValue="record.tips"/>
改为:<FormItem ... :inputValue.sync="record.tips"/>
- 原来方法
Mehods
中的onUpdateTips(value: string) {this.record.tips = value;}
可以删除
成功提交一个记录后,初始化:
- 标签
tags
初始化:取消选中标签this.selectedTags = [];
- 备注
tips
初始化:清空备注栏this.record.tips = '';
- 数字盘输出初始化:清空数字输出
this.output = '0';
Tab
种类无需初始化
标签
tags
初始化 逻辑
- 点击OK 成功提交一个记录
Numpad.vue -> click Button OK -> confirmNum() -> this.$emit
('update:deselectTags', true) -> Money.vue
Money.vue -> <Numpad @update:deselectTags="deselectTags" /> -> deselectTags(){emptyTags = []}
|
|
单一功能原则 ⇧
- 添加方法
submit()
其中包含单一化逻辑: checkoutRecord()
alertInform()
saveRecord()
reset()
统一 成功提交一个记录后,初始化的逻辑
Numpad.vue
控制 按下OK 按钮- 通知父组件
Money.vue
:const number = parseFloat(this.output);
this.$emit('update:amount', number);
传值给父组件this.$emit('submit');
通知父组件Money.vue
执行自定义事件@submit="submit"
Money.vue
开始检查逻辑checkoutRecord()
- 初始
let checkoutResult = true;
- 未选泽标签逻辑
if (!this.record.tags || this.record.tags.length === 0)
:checkoutResult = false;
- 已选好标签:
return checkoutResult;
- 将
checkoutResult
值传回给Numpad.vue
- 初始
Numpad.vue
开始在下次更新后,执行重置逻辑this.$nextTick(() => {this.reset();});
reset()
判断if (this.isReset)
true
:this.output = '0'; this.$emit('update:deselectTags', true);
重置标签false
:this.$emit('update:deselectTags', false);
不清零,不重置标签
Money.vue
开始重置逻辑
- 数字盘输出初始化:清空数字输出
this.output = '0';
- 通知父组件
Money.vue
控制- 是否成功提交一个记录
checkoutRecord()
- 保存记录
saveRecord()
- 重置各子组件
reset()
- 重置
Tags.vue
:this.selectedTags = [];
- 重置
FormItem.vue
:this.record.tips = '';
- 重置
Numpad.vue
:`` - 重置
Tabs.vue
:``
- 重置
- 是否成功提交一个记录
Tags.vue
控制 标签tags初始化:取消选中标签this.selectedTags = [];
FormItem.vue
控制 备注tips初始化:清空备注栏this.record.tips = '';
Tabs.vue
控制 Tab种类无需初始化
使用事件代理实现监听 子组件事件 改变样式 ⇧
- 绑定样式
:class="['basic-btn', {'current': buttonIndex === curIndex}]"
- 设置生效时的样式
.current { background: red; }
- 绑定事件方法
@click="selectBtn(buttonIndex)"
- 定义方法发布通知给父组件
this.$emit('selectBtn', buttonIndex)
在Vue中改用事件代理
- 父组件 通过 事件冒泡机制 代理子组件的点击事件事件
- 需要传入 事件对象
$event
- 在方法参数中 接收事件对象 参数
e: Event
- 解构 事件对象目标(事件原对象)
target = e.target
- 需要传入 事件对象
- 去除子组件中的 点击事件监听及对应方法
- 子组件中 加上 HTML5 的自定义属性
:data-index="buttonIndex"
- 在父组件中获取
dataset
属性:target.dataset.index;
- 注意取到的属性值为 字符串类型,在这里需要转换为 数字类型
parseInt(target.dataset.index, 10)
- 在父组件中获取
- 在标签中设置的 属性
data-xxx-xxx
只可以包含 字母,数字 和下面的字符:dash (-)
,dot (.)
,colon (:)
,underscore (_)
- 此外不应包含
ASCII
码大写字母 形成的驼峰式命名 - 比如
<numpad-button :data-bundle-event="item.bundleEvent">
- HTML5自定义属性前缀
data-xxx
- 解释:HTML规定可以为元素添加非标准的属性,但要添加前缀
data-
- 目的:为元素提供与 渲染无关 的信息,或者语义信息。
- 属性可以任意添加和命名,只要以
data-
开头 - 访问:添加自定义属性之后,可以通过元素的
dataset
属性来访问自定义的值。
- 解释:HTML规定可以为元素添加非标准的属性,但要添加前缀
使用 js 操作
dataset
注意:
- 在添加或读取属性的时候需要去掉前缀
data-
- 如果属性名称中还包含连字符(
-
),需要转成 驼峰命名 方式 - 但如果在CSS中使用选择器,需要使用 连字符格式
修改事件代理 辨别子组件内部元素是否为 目标元素
e.target
|
|
事件代理小结
- 子组件利用 HTML5的自定义属性
dataset
可以传递数据给父组件 - 特别是当子组件是通过
v-for
循环渲染形成的,且需要事件代理
Numpad.vue
|
|
小结 ⇧
- 代码仓库
.sync
怎么用TS
以及装饰器怎么用window.localStorage
怎么用- 数据迁移是什么
- 存储的标签是写死在数据中的
tags = ['衣', '食', '住', '行', '理财'];
怎么用Model
来存储所有Model
相关数据 - 仓库
- 所有 commits(倒序)
保存选中状态可用
<keep-alive></keep-alive>