【项目-喵内记账-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-designUI库的做法,通过前缀,传不同外部数据,来区分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.vueJS 写法
|
|
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 datamethodcomputedproplifecycle 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 + TypeTypeScript通过编译器 编译为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'
|
|
内置事件类型名注意全小写
mousemovetouchstart
- 通过浏览器开发工具>事件侦听器查看具体的元素上绑定的事件名是否写错
实现仿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.vueMoney.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>