【项目-喵内记账-meoney-05】Statistic.vue 组件 列表统计页面 -标记问题
大纲链接 §
[toc]
知识概要
- 封装 Tabs,样式使用 deep 深度作用选择器,覆盖子组件内部元素样式
- 用 JS 配置组件默认
height - 用列表展示数据
- 添加
Statistic.vueSCSS ISO8601和dayjs- 数据排序
- 数据排序后分组
- 完成统计页面
createdAt of undefined的解决办法
封装 Tabs.vue
重构Types.vue 样式,使用 深度作用选择器 deep 语法
查看
Statistic.vue结构, 可复用Types.vue组件
- 注意样式加了
scoped属性,只影响当前组件的标签,不会深入组件标签继续影响内部结构- 组件
<Type class="x"/>d的样式只是加在表层的<div>上 - 即使写了
.x li {...;}的样式,不对内部<li>有任何影响
- 组件
- 可以在不污染其他组件样式的同时,使用
/deep/或::v-deep(SCSS)可以深入影响内部结构的样式 - 搜索
site: vuejs.org deep:Scoped CSS 深度作用选择器 deep语法
|
|
Statistic.vue中,在<Type/>上,对Types.vue组件传入绑定属性:type.sync="yyy"
|
|
深度作用选择器 选中的样式
&.selected {}
|
|
还有一个缺点,就是如果多层嵌套,无法通过类选择器精确/动态控制样式
最佳实践:精准控制组件内部结构的样式
使用 对象控制绑定的样式(表驱动) 和 动态前缀 来控制组件内部标签的样式
- Class 与 Style 绑定
- 从父组件传入一个 前缀字符串 到子组件中,由各不同子组件去拼接,就可实现精确/动态控制样式
- 在
Types.vue添加外部数据:属性前缀class-prefix- 注意写清外部数据类型:
@Prop(String) readonly classPrefix?: string; - 注意数据由于是外部传来的,无法指定初始化类型,强行指定类型
!:或者可选?:
- 注意写清外部数据类型:
- 用对象的形式赋值给绑定的类属性
:class="{selectorA: true, selectorB: false, selectorC: expressions, active: isActive}"- 当对象的属性值(一般是表达式)为
true时,属性(键)对应的类选择器生效 - 官方解释:
active这个class存在与否将取决于数据propertyisActive的truthiness - 可绑定多个选择器
- 可以与普通的
class attribute共存:<div class="static active" :class="{dynamicClss: true}"></div> - 动态地切换
class:当isActive变化时,class列表将相应地更新- 当
isActive: true,class列表将变为class="static active"
- 当
- 当对象的属性值(一般是表达式)为
- 动态属性前缀
<li :class="{[classPrefix + '-item']: classPrefix, selected: value==='-'}">- 如果
key里有变量,使用ES6对象动态属性{[xxx]: 'yyy'} - 使用(表达式)作为对象的属性名,即把表达式放在方括号内
- 还可以拼接字符串:
{[classPrefix + '-item']: classPrefix}
- 如果
- 表驱动:将所有类名以表的形式给出
Statistic.vue
|
|
Type.vue
|
|
- 可以用前缀精确地获取类名
zzz-item,控制内部的元素 - 可以直接用深度作用选择器作为最外部选择器,但会造成被子组件原来的样式干扰
- 传入不同的前缀实现样式互相隔绝,BEM风格
- 注意
CSS权重计算,子组件比父组件后加载
可以声明一个变量,传对象,将该对象作为
:class=""的绑定值
抽离Types.vue为Tabs.vue组件,可复用切换逻辑
可现实多项内容
Tabs切换
- 需要外部传参:对象数组包含显示文字和类型字符
+/-、前缀字符 - 内容为父组件传入
@Prop({required: true, type: Array}) dataSource!: { text: string; type: string }[]; - 声明类型
type DataSource = { text: string; type: string } dataSource改为@Prop({required: true, type: Array}) dataSource!: DataSource[];type类型字符串@Prop(String) readonly type!: string;classPrefix前缀字符串@Prop(String) readonly classPrefix?: string;
重构
Tabs.vue
|
|
重构
Statistic.vue
<Tabs class-prefix="interval" :data-source="typeList" :type.sync="type"/>- 样式前缀:
class-prefix="interval",子组件默认样式 - 传
dataSource为intervalList- 声明
intervalList = [{text: '按天', type: 'day'}, {text: '按周', type: 'week'}, {text: '按月', type: 'month'}]; - 传初始绑定的同步数据
:type.sync="interval"为interval = 'day';(按天)
- 声明
- 样式前缀:
- 用
Tabs.vue改造Types.vue:<Tabs class-prefix="type" :data-source="typeList" :type.sync="type"/>- 样式前缀:
class-prefix="type":::v-deep .type-item {...} - 声明数据源:
typeList = [{text: '支出', type: '-'}, {text: '收入', type: '+'}] - 传初始绑定的同步数据:
:type.sync="type"为type = '-';(支出)
- 样式前缀:
|
|
动态属性liClass作为样式Tabs.vue
|
|
- 遍历数据
dataSource,绑定:key="item.type",插值{{ item.text }}<li v-for="item in dataSource" :key="item.type">{{ item.text }}</li>
- 注册事件
@click="select(item)"<li v-for="item in dataSource" :key="item.type" @click="select(item)">{{ item.text }}</li>
class绑定::class="{[this.classPrefix +'-tabs-item']: this.classPrefix, selected: item.type === type}"太长- 封装样式对象返回变量的函数,接受
<template>中的数据传参item <li v-for="item in dataSource" :key="item.type" :class="liClass(item)" @click="select(item)">{{ item.text }}</li>- 缩写为
:class="liClass(item)" - 函数实现
liClass(item: DataSource) {return {[this.classPrefix +'-tabs-item']: this.classPrefix, selected: item.type === this.type};} - 不可使用箭头函数,避免
this指向
- 封装样式对象返回变量的函数,接受
- 发布事件
select(item: DataSource) {this.$emit('update:type', item.type);}- 注意类型声明
DataSource
Statistic.vue查看显示插值是否正确
|
|
模块化常量数据src/constants/intervalList.ts
- 使数据不可使用栈方法,
Object.freeze() - 禁止改变原对象,成为真正的常量
intervalList.ts
|
|
recordTypeList.ts
|
|
重构
Statistic.vue
|
|
- 重构
typeList为recordTypeList
重构Money.vue 使用Tabs.vue,并删除原 Types.vue
<Tabs :data-source="recordTypeList" :type.sync="record.type"/>
Money.vue
|
|
调整高度样式,更改优先级
- 注意
CSS权重计算- 子组件比父组件后加载
- 更多使用具体的
class选择器,精确控制样式 - 特别是使用深度作用选择器时,尽量 不使用标签选择器,里层的标签未知
- 解决样式被覆盖的方法:减少/增加 嵌套的选择器
- 减少嵌套:去除后代/子选择器
> li,改用class选择器增加权重
- 减少嵌套:去除后代/子选择器
Tabs.vue
|
|
Statistic.vue
|
|
用 TS 配置组件默认 height
- 使用外部数据传入高度属性值
- 注意高度属性值类型是字符串,不是数字
- 外部数据无需初始化,加上强制类型断言
!: string - 注意内联样式
style属性的权重
重构
Tabs.vue
|
|
重构
Statistic.vue
|
|
- 可以直接使用SCSS样式控制,也可以使用 TS 配置组件默认
height - 但不推荐,会造成逻辑与样式耦合
用列表展示数据
展示数据结构
结构类似于无根节点的树
trees
|
|
展示数据步骤
- 首先使用 钩子获取数据
beforeCreate() {this.$store.commit('fetchRecords');}
- 使用 计算属性操作数据,注意必须有返回值
return ...get recordList() {return ...}获取记录列表,注意返回值类型get result() {return ...}
- 使用 计数排序 桶排序
- 使用
hashTable存数据 - 大致结构 :
list: []+type/interval: string=recordTrees: {title: string, items: RecordItem[]}[] - 按天分组排序,在计算属性中写排序逻辑
get result() {}
- 使用
隐藏的bug
注意
recordList的类型,即使已经声明类型,recordList: RecordItem[],recordList类型为any
Vuex类型上的bug,在得到this.$store时,并不能返回正确的类型,导致得到的store返回的类型永远是anyVuex并不能像之前自己使用models里数据时声明数据类型tagListModel, 这是ts与vuex结合不好的地方,即使用API会丢失类型信息(vue3的新状态管理pinia修复了这一缺陷)- 注意
recordList[i].createdAt属性的类型 - 每次都 强制类型断言
as RootStateget recorList() {return (this.$store.state as RootState).recordList;}- 此时的返回值
recordList的类型显示正确为RecordItem[] - 属性
recordList[i].createdAt的类型显示为Date | undefined,但需要的是字符串,字符串才可以分割操作
Date类型不能被JSON.stringify序列化
- 当使用
JSON.parse()无法保证左右两边的类型是相同的state.recordList = JSON.parse(window.localStorage.getItem('recordList') ?? '[]') as RecordItem[];JSON.parse()返回的是any类型recordList强制定义类型为recordItem[],只在运行时才得到JSON不支持Date等内置对象,会转成string但不是所需要的
Date日期调用了toJSON()将其转换为了string字符串(同Date.toISOString()),因此会被当做字符串处
toISOString()特指ISO8601
- 类型声明
type RecordItem = {tags: string[]; tips: string; type: string; amount: number; createdAt?: Date;}中的createdAt?: Date;改为createdAt?: String; - 查看所有引用
createdAt,TS 编译报错 - 将所有改成
*.createdAt = new Date().toISOSting()
标记问题:这是
vue和TS配合不好的地方
- 在
src/store/index.ts中声明的store类型是RootState- 之前将
state的类型进行断言state: {tagList: [], recordList: [], currentTag: undefined} as RootState,
- 之前将
- 查看源码
vuex/types/vue.d.tsdeclare module "vue/type/vue" {interface Vue {$store: Store<any>}}写死类型是<any>
使用计算属性computed操作展示数据
TS声明空对象类型
- 分别声明空对象的键和值的类型
const hashTable: { [key: string]: RecordItem[] } = {};
使用
hashTable即键值对的数据结构来存数据
|
|
recordList[i].createdAt!.split('T');:- 判断拿到数据后,取出的值
createdAt一定存在,加上强制类型断言createdAt!.* ESLint报错就取消全局类型声明custom.d.ts中type RecordItem的createdAt?: string;,改为createdAt: string;
- 判断拿到数据后,取出的值
- 解构
const [date, time] = recordList[i].createdAt!.split('T');T的前面是日期,后面是时间ESLint报错time未使用就先删除time
- 类似计数排序:初始化
hashTable[date] = hashTable[date] || []; - 推入数据
hashTable[date].push(recordList[i]); - 尝试打印出
console.log(hashTable)或在视图中临时写上数据{{xxx}} - 查看每项的内容
发现一开始的hashTable声明类型结构错误
- 避免类型声明结构混乱,定义一个中间类型,先声明哈希表值的类型:
type HashTableValue = { title: string; items: RecordItem[] }; - 初始化声明哈希表改为
const hashTable: { [HashTableKey: string]: HashTableValue } = {};HashTableKey命名可为任意字符串,但从所需的结构上看,表示的是日期date- 按日期分组
- 遍历取到的计算属性
recordList:for (let i = 0; i < recordList.length; i++) {...}- 取出日期
const [date,] = recordList[i].createdAt.split('T'); - 将每次取出的日期
date作为hashTable的属性名hashTable[date] - 初始化
hashTable:hashTable[date] = hashTable[date] || {title: date, items: []};- 将
hashTable[date]对应的属性值赋值到hashTable的[date]属性上 - 保底值为
{title: date, items: []}
- 将
- 在每项的
items中推入遍历的数据(recordList[i]数组)hashTable[date].items.push(recordList[i]);
- 取出日期
- 返回值
hashTable
Statistics.vue
|
|
循环渲染数据
Statistics.vue
|
|
result返回的是hashTable,在 v-for 里使用对象,遍历hashTable的property<li v-for="(value, name) in result" :key="index">{{ value }}</li>- 必须写
:keyDOM diff算法让Vue正确识别不同的DOM,正确的复用- 给 Vue 一个提示,以便它能跟踪每个节点的身份,从而复用和重新排序现有元素,需要为每项提供一个唯一
key attribute - 这里可以使用唯一属性名作为
:key
- 给 Vue 一个提示,以便它能跟踪每个节点的身份,从而复用和重新排序现有元素,需要为每项提供一个唯一
- key值是必须唯一的,如果重复就会报错,
Dupliacated key detected NaN - 取到的
value也是一个对象(表示RecordItem) - 第二层循环
<li v-for="item in group.items" :key="item.id">{{ item.amount }} {{ item.createdAt }}</li>
暂时未实现按类型(支出/收入)和排序的逻辑
完善并添加Statistic.vue SCSS
- 注意样式加了
scope属性,只影响当前组件的标签,不会深入组件标签继续影响内部结构 - 使用深度作用选择器
/deep/(CSS)或::v-deep(SCSS) - 不用
min-height来撑开,改用line-height和padding来撑开高度
格式化显示
item.tags
custom.d.ts中改为type RecordItem = { tags: {id: string; name: string}, ...} }[];
显示
xx年xx月xx日,重构Statistic.vue
|
|
是否超出 存储最大值
localStorage的最大存储为5MB左右- 使用
IndexedDB
是否超出最大显示行数
- 引入
global.scss使用mixin封装的超出文字省略
|
|
ISO8601 和 dayjs
ISO8601- 得到
ISO8601const time = new Date().toISOString()const myTime = new Date(Date.parse(time))myTime.getHours()
- Why you shouldn’t use Moment.js…
Vue.js min才30k
使用
Day.js
- 安装
yarn add dayjs@1.8.20 - 引用包
import dayjs from 'dayjs'; - 查看
API:const api = dayjs();
正确显示
toISOString()修正时差,得到本地时间
toISOString()会得到零时区而非本地(东八区)的时间- How to ISO 8601 format a Date with Timezone Offset in JavaScript?
- 忽略时差
date.getTime() - (date.getTimezoneOffset() * 60000) - 增加状态属性
localTimeStamp
src/store/index.ts
|
|
src/custom.d.ts
|
|
Statistic.vue 改显示xx年xx月xx日为显示今天、明天、昨天、上周、上月、以前
|
|
const now = dayjs();- 判断相符
.isSame(now, 'day') - 得到昨天:
now.valueOf() - 86400*1000- 使用
Dayjs提供的API:dayjs(someday).isSame(now.subtract(1, 'day')) - 减一天
.subtract(1, 'day')
- 使用
- 格式化
thatDay.format('YYYY年M月D日');
模块化状态管理
- 分为
tagStore.ts和recordStore.ts - 在组件中执行状态变更的方法不变
this.$store.commit('typeXXX', payloadXXX); - 访问状态需要加一层模块名的属性
this.$store.state.tagStore.currentTag; - 原来的
this.$store.state.someState改为this.$store.state.yourModuleName.someState - 参考 vue状态管理之vuex(十六)
重构
custom.d.ts分开声明类型
|
|
重构
src/store/index.ts
|
|
tagStore.ts
|
|
recoredStore.ts
|
|
重构Money.vue、Statistic.vue和Labels.vue等相关组件
|
|
重构
Statistic.vue
|
|
重构
Labels.vue
|
|
重构
EditLabel.vue
|
|
重构
Tags.vue
|
|
- 参考文档
- Vue2.5+ Typescript 引入全面指南 - Vuex篇
Muation函数 不可为 async函数, 也不能 使用箭头函数来定义, 因为代码需要运行在重新绑定执行的上下文
数据排序(计数排序的变形)
首先了解数据
result的类型
- 目前
result是hashTable(对象) - 遍历对象时,遍历的顺序是否固定
- 遍历对象的
key,遍历的顺序是否固定 hashTable中的顺序是用户输入得到的,输入顺序不一定符合预期
- 遍历对象的
JS 中对象的
key是有顺序的
- JS 中对象的字段遍历顺序是没有保证的,例如
Object.keys函数产生的数组顺序没有保证 - 实际上在
ES2015之后标准规定了key的顺序(准确的说是规定了[[OwnPropertyKeys]]()这个内部方法返回的key的顺序)
结论:
keys数组分为三个部分:
- 可以作为数组索引的
key按照升序排列,例如1、2、3。 - 是字符串不是 symbol 的 key,按照创建顺序排列。
- symbol 类型的 key 也按照创建顺序排列。
- 参考链接 ECMAScript 9.1.11
|
|
必须转换为一个数组,才能排序
- 按时间排序,近的排在前面
- 显示的顺序为 今天 昨天 (本年内)具体日期 (去年以及之前)具体日期
|
|
clone.ts
|
|
- TS 声明类型:
type HashTableValue = { title: string; items: RecordItem[] };
- 查询到对应的
title,将recordList排序,依次推入数组 recordList是RecordItem的数组,进行排序.sort((a, b) => {})const n = recordList.sort((a, b) => {a.createdAt});a.createdAt的值为字符串,按ASCII顺序比较大小,不是预期的顺序,'a' - 'b' // NaN不能用字符串的减法- 使用
.sort()必须变为数字类型,用.valueOf():dayjs(a.createdAt).valueOf()
const newList = recordList.sort((a: RecordItem, b: RecordItem) => ( dayjs(a.createdAt).valueOf() - dayjs(b.createdAt).valueOf() ));- 需要逆向从近期到远期的
( dayjs(b.createdAt).valueOf() - dayjs(a.createdAt).valueOf() ) - 注意
.sort()会 改变原数组自身,保留原数组,使用 深克隆- 导入之前的工具函数
import clone from '@/lib/clone.ts';的clone() function clone(data: any) { return JSON.parse(JSON.stringify(data)) as RecordItem; }- 由于接受的参数类型是
any,而JSON.parse()返回值类型也是any - 使用泛型
function clone<T>(data: T): T {...}统一入参和返回值的类型- 在尖括号中声明类型
- 之后使用
.sort()TS 会自动推断类型
- 导入之前的工具函数
重构
Statistic.vue
|
|
- 先将局部数据
recordList排序,再push到数据中
数据排序后分组
- 判断数组长度
if(recordList.length === 0) {return []};,确保存在可操作数据 - 取出新的
newList的第一个数据const x = [{title: dayjs(recordList[0].createdAt).format('YYYY-MM-DD'), items: [recordList[0]]}];
- 从第二个数据的
.createdAt和第一个数据的title和开始循环比较- 新的数据和分组的
title是否一致- 一致放入当前组的
items - 不一致,作为新的一组的
title,放入新分组的items
- 一致放入当前组的
- 新的数据和分组的
|
|
统一显示时间戳比较 - 标记问题ok
const localDay = dayjs(current.createdAt.split('T')[0]);- 由于有时区的概念
dayjs(current.createdAt)算上时间部分的日期会延后一天 - 封装倒时差函数
clearJetLag(new Date(), '-')
src/store/modules/recordStore.ts
|
|
src/lib/clearJetLag.ts
|
|
重构命名
get groupedList() { ... return rusult;}
更改
template循环渲染的:key
- 数据由对象变为数组 方便排序
li v-for="(group, index) in groupedList" :key="index">
重构
Statistic.vue
|
|
完成统计页面
区分 支出和收入
在排序前加
filter
- 查找对应类型相匹配的
.filter(r=> r.type === this.type)
|
|
注意
.filter()可能会使返回的新数组长度变短为空数组,进行之后的操作的时候访问内部属性为空值undefined
- 判断经过筛选后的数组长度是否为 0 ,返回空数组,结束函数
if (newList.length === 0) {return [] as groupedType[];}
显示总额
- 给
result添加total属性- 初始化
{ title: ..., total: 0, items: ...} - 或者声明类型:
type groupedType = { title: string; total?: number; items: RecordItem[] }; total?: number;表示赋值时,total属性可以不存在const result: groupedType[] = {...}
- 初始化
- 需要统计的是每组
group各项items的amount属性 - 使用
.map遍历每组group(map是有返回值的forEach;forEach是没有返回值的map)- 使用
reduce对每组group的items进行归纳统计- 初始值
0 - 形参为 统计结果
sum项目item - 将
amount进行加减,统计到result的total属性 - 注意
amount类型
- 初始值
- 将统计结赋值给
group.total
- 使用
- 将
total显示到template:<h3 class="title">{{ showDay(group.title) }} <span> ¥{{ group.total }}</span></h3>
标记问题 记录bug:
amount显示为字符串拼接- 从输入得到的类型为字符串而非数字
- 在
Numpad.vue中传入的外部数据@Prop() readonly value!: number;没有声明类型 - 必须提前声明传入的类型
@Prop(Number) readonly value!: number; amount在<Numpad :value.sync="record.amount" @submit="saveRecord"/>处更新Numpad.vue中方法confirmNum() {this.$emit(...)}发布的第二个参数output类型是any,必须转为数字类型- 查看
localStorage中recordList的数据amount属性值都是字符串,应为数字
重构
Statistic.vue
|
|
Vue 和 TypeScript第二个结合不好的地方:this.$emit(event, ...args)
- TS 没有在
this.$emit()处警告...args的any类型
|
|
一些 bug
Money.vue、Tags.vue显示传值$event
<Tags @update:selectedTags="pickTags($event)"/>或不传值<Tags @update:selectedTags="pickTags"/>,使用方法隐式传值,默认传$eventpickTags(eventValue: Tag[]) {this.record.tags = eventValue;}
<Tags @update:selectedTags="record.tags = $event"/>内联语法,需要显示传值pickTags(selectedTags: Tag[]) {this.record.tags = selectedTags;}
- 参考
- v-on
$event- 用在 普通元素上时,只能 监听原生 DOM 事件
- 用在自定义元素组件上时,也可以监听子组件触发的自定义事件
- 监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个
$event property:v-on:click="handle('ok', $event)"
- 使用事件抛出一个值
- 子组件使用
$emit的第二个参数payload来提供这个值<button v-on:click="$emit('enlarge-text', 0.1)">Enlarge text</button> - 父级组件监听这个事件时,可通过
$event访问到被抛出的这个值<blog-post v-on:enlarge-text="postFontSize += $event"> - 如果这个事件处理函数是一个方法,这个值将会作为第一个参数传入这个方法
- 子组件使用
- 内联处理器中的方法
- 有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量
$event把它传入方法
- 有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量
- 事件处理方法 v-on 还可以接收一个需要调用的
Methods里方法名称- 原生 DOM 事件 的
event作为参数传给方法- 例如获取输入事件的输入值
@input="oninputValueChanged($event.target.value)" - 例如点击事件获取点击的目标值
@click="toggle(tag, $event.target.value)"其中参数tag由循环渲染列表的数据提供v-for="tag in tagList"
- 例如获取输入事件的输入值
- 原生 DOM 事件 的
- v-on
实现tooltip加个尖角标
formatter函数拼接样式- echarts修改tooltip默认样式(使用formatter函数拼接加工)
空白 无记录 显示提示文字 Statistic.vue
|
|
- 同样地
Money.vue缺 “添加至少一个标签” 逻辑 - 确认保存后 缺 重置备注 和标签 逻辑
<FormItem class="form-item" field-name="备注" placeholder="在这里输入备注" @update:inputValue="onUpdateTips" :value="record.tips"/>需要绑定:value="record.tips"才会起效
逻辑耦合
createTag方法中当store提交saveTags成功保存完标签后,window.alert('xxx')- 收集错误提示,
alert处理所有报错
createdAt of undefined 的解决办法
- 即使返回空值有时也需声明类型
if (newList.length === 0) {return [] as groupedType[];}
- 核心痛点:每次调用
this.$store.dispatch/this.$store.commit/this.$store.state/this.$store.getters都会伴随着类型丢失。
- 所有代码:https://github.com/FrankFang/morney-live-list
- 所有 commits:https://github.com/FrankFang/morney-live-list/commits/master
参考文档
- Vuex框架原理与源码分析-明裔
- Vue & TypeScript 初体验
http://www.quasarchs.com/- Vuex 基本入門 Day 8
- Vuex 模板使用了 vuex-class 简化 vuex
- Vuex业务模块划分项目实例
- 手摸手教你在vue-cli里面使用vuex,以及vuex简介
- Vuex 4.0中使用typescript, Vuex4.0中modules的ts使用,Vue3 + Vuex4.0 + TypeScript 使用详情
- Vuex 进阶使用之modules模块化划分、mapState、mapActions辅助函数的使用
- Vue&TypeScript初體驗使用Vuex(vuexmoduledecorators)
- Vuex进阶篇——Module模块化学习
- 更好的使用module vuex
- Vuex(module)
- Vuex 之 module 使用方法及场景
- Vuex 模块(Module)
- Vuex 模块化使用
- VueJS中学习使用Vuex详解
- 大型项目使用Vuex modules后,模块之间怎么访问action
- uni-app 使用vuex(vuex module)