分享重点:
- 组件级状态管理(vuex)的使用
当某一个模块较大的时候可以不使用 vuex 的全局状态管理,创建基于当前模块的vuex状态管理模式,管理当前模块的状态。
- vuex plugin 使用
vuex 提供 plugin 选项,方便多组件间状态管理。
- 表单数据处理
使用 vuex plugin 来管理多组件表单状态,让你使用更方便。
使用vuex plugin 可以实现表单数据的组件级状态管理。
开始使用
1,在当前组件根目录注册store,新建store.js 文件,如下:
(新建的 store 文件夹即是当前模块功能的状态管理)
当前文件为当前模块的状态管理文件,管理当前模块的状态。
import Vue from 'vue'
import { SET, EDITED, LOAD, MERGE, SELECT_CARRIER_SUG } from './constants' // 常量
import adaptor from './adaptor' // 当前store数据源
import basicInfoPlugin from './BasicInfo/plugin' // 子组件页面plugin
import consignPlugin from './Consign/plugin'
const empty = () => 1
// 在 index.vue 中充当 poundModule
export default {
namespaced: true,
state: () => ({}),
mutations: {
// 处理数据,将数据更改为响应式
[LOAD]: (state, payload) => Object.entries(adaptor(payload)).forEach(([k, v]) => Vue.set(state, k, v)),
[SET]: (state, payload) => Vue.set(state, payload.key, payload.val),
[EDITED]: (state, payload) => { payload.val ? state.edited[payload.key] = true : delete state.edited[payload.key] },
[MERGE]: (state, payload) => Object.entries(payload).forEach(([k, v]) => Vue.set(state, k, v)),
[SELECT_CARRIER_SUG]: empty
},
actions: {},
getters: {},
plugins: [basicInfoPlugin, consignPlugin ] // 注册plugin
}
2,父组件(当前模块根组件)初始化store,并处理所有数据
// index.vue
import Vuex from 'vuex'
import mixin from './mixin'
import BasicInfo from './BasicInfo' // 子组件
import Consign from './Consign' // 子组件
import poundModule from './store'
export default {
components: {
BasicInfo,
Consign,
},
mixins: [mixin],
provide() {
// 将store及数据注册为依赖注入,方便在子组件使用
return { poundStore: this.poundStore, poundState: this.poundStore.state }
},
beforeCreate() {
// 创建store
this.poundStore = new Vuex.Store(poundModule)
},
async created() {
await this.init()
},
methods: {
init() {
// 初始化 store 数据,该处传入的载荷(即参数)会传入LOAD 方法的函数中,共同构造当前数据源
// store 数据来源于 LOAD 时传入的数据源:adaptor方法,和此处传递的data
this.poundStore.commit(LOAD, { data: this.data, usedFor: usedFor, source })
},
// 表单校验方法
validate() {
// 数组为子组件ref属性数组
['basicInfo','consign'].forEach(key => {
// 获取子组件ref对象
const formRef = this.$refs[key] && this.$refs[key].$refs && this.$refs[key].$refs['form']
// 表单校验
formRef && formRef.validate((valid, fields) => {
if (!valid) {}
})
})
}
}
}
mixin
// mixin.js
import { EDITED, SET, MERGE } from './constants'
export default {
data() {
return {}
},
methods: {
set(key, val) {
this.poundStore.commit(SET, { key, val })
},
merge(key, val) {
this.poundStore.commit(MERGE, { [key]: val })
},
edit(editKey, val = true) {
const key = this.poundStore.state.keyMap[editKey]
if (this.poundStore.state.edited[key] ^ val) this.poundStore.commit(EDITED, { key, val })
},
validateScrollView(ref) {
let dom = ref
if (Object.prototype.toString.call(dom) !== '[object Object]') {
dom = dom[0]
}
dom.$el.scrollIntoView({
block: 'center',
behavior: 'smooth'
})
}
}
}
到此,当前模块 store 挂载完成,状态数据生成,可以使用。
当前是为了说明 store 数据的来源和处理
扫描二维码关注公众号,回复: 14811737 查看本文章store 的所有数据来源于两部分:
第一:index.vue 中传入的 data
第二:store 初始化 LOAD 时传入的本地生成的用于渲染页面的数据,
这份数据对页面数据的默认值,显示,禁用等都做了处理。
这两份数据在初始化LOAD的时候做了处理,进行了双向绑定
// adaptor.js 文件 import { camelCase, requiredOfSetting, defaultOf, showOf, editOf } from './utils' function adaptor(payload) { const data = payload.data const usedFor = payload.usedFor const isPublishOrder = payload.isPublishOrder const source = payload.source const base = { ...data } // 赋默认值 const val = (key, dft = '') => key in base ? base[key] : defaultOf(key) !== null ? defaultOf(key) : dft const showOfSetting = showOf({ ...data, usedFor, source }) const editByUsedFor = editOf(usedFor) const editOfSetting = key => usedFor === 'detail' ? false : editByUsedFor(key) const textMap = {} const fullNameMap = {} const validateMap = {} const keyMap = {} const getMap = {} const camelKey = {} const field = (key, text, validate, get, dft) => { const newKey = camelCase(key) textMap[newKey] = text fullNameMap[newKey] = Array.isArray(text) ? text[0] : text keyMap[newKey] = camelKey[newKey] = key keyMap[key] = newKey validate && (validateMap[newKey] = validate) get && (getMap[newKey] = get) return { [newKey]: val(key, dft), [`${newKey}Editable`]: editOfSetting(key), [`${newKey}Required`]: requiredOfSetting(key), [`${newKey}Show`]: showOfSetting(key) } } // return的数据为store的初始化数据 return { usedFor: usedFor, isPublishOrder: isPublishOrder, source: source, ...field('createdUserName', '创建人'), startStationId: val('startStationId'), // ... 省略其他值 } } export default adaptor
// utils.js文件(adaptor.js 文件中用到) import store from '@/store' import { camelCase } from '@/util' import { regexpMap, validateTips } from '@/constants' import { PriceMethodEnum, ChiefDriverDeductMethodEnum, SettlementObjectEnum, PaymentTypeEnum } from '@/constants' import { defaultRequiredField, selectRequiredField } from './constants' export { camelCase } export const expandField = (fieldArr, suffixArr) => fieldArr.reduce((pre, field) => ([...pre, ...suffixArr.map(suffix => `${field}${suffix}`)]), []) export const mapPoundState = (fieldArr, isVModel) => fieldArr.reduce((pre, field) => { function get() { return this.poundStore.state[field] } isVModel ? pre[field] = { get, set(val) { this.set(field, val) this.edit(field) } } : pre[field] = get return pre }, {}) export const requiredOfSetting = key => defaultRequiredField.includes(key) export const defaultOf = key => { const defaultMap = { isPublish: 0, consignerName: (store.getters.userInfo && store.getters.userInfo.currentCompanyName) || '', priceMethod: PriceMethodEnum.PerWeight, settlementObject: SettlementObjectEnum.ForDriver, unitWeight: 0, transportPaymentType: PaymentTypeEnum.Wallet, chiefDriverPriceUnit: PriceMethodEnum.PerTrunk, chiefDriverDeductMethod: ChiefDriverDeductMethodEnum.OutsideBuckle, authUserMap: {} } return defaultMap.hasOwnProperty(key) ? defaultMap[key] : null } export const showOf = data => key => { const setting = { lossCalculationRule: Boolean(data['isDeductLoss'] && +data['isDeductLoss'] === 1), permitDamageQty: Boolean(data['lossCalculationRule'] && +data['lossCalculationRule'] === 2), driverFreightEraseNum: Boolean(data['driverFreightErase'] && +data['driverFreightErase'] === 6), payableFreightEraseNum: Boolean(data['payableFreightErase'] && +data['payableFreightErase'] === 6), goodsCode: data.usedFor === 'detail', transportTimeQty: data.source === 'carrier', transportDistanceQty: data.source === 'carrier' || data.usedFor === 'detail', effectiveTime: false } return setting.hasOwnProperty(key) ? setting[key] : true } export const editOf = usedFor => key => { let editMap = {} if (usedFor === 'create' || usedFor === 'copy') { editMap = { settlementObject: false } } else if (usedFor === 'edit') { editMap = { goodsTag: false, goodsType: false, carrierId: false, carrierCode: false, carrierName: false } } return editMap.hasOwnProperty(key) ? editMap[key] : true } export const validate = (val, type) => { const valid = !val || new RegExp(regexpMap[type]).test(val) const reason = validateTips[regexpMap[type]] return { valid, reason } } const requiredTipPrefix = key => selectRequiredField.includes(key) ? '请选择' : '请输入' export const lessThanValidator = (lessThanNumber, tip, isFloat = true) => (rule, value, callback) => { if (!new RegExp(regexpMap[isFloat ? 'FLOAT_P' : 'NUMBER']).test(+value)) { return callback(new Error(tip)) } if (+value > lessThanNumber) { return callback(new Error(tip)) } callback() } export const validator = (rule, value, callback, state) => { const { field } = rule const { fullNameMap, validateMap } = state let val = state[field] if (typeof val === 'string') val = val.trim() const isEmpty = val === '' || val === null || val === undefined if (state[`${field}Required`] && isEmpty) { return callback(new Error(`${requiredTipPrefix(field)}${fullNameMap[field]}`)) } if (validateMap[field]) { const regArr = Array.isArray(validateMap[field]) ? validateMap[field] : [validateMap[field]] for (const reg of regArr) { const result = validate(val || '', reg) if (!result.valid) { return callback(new Error(`${result.reason}`)) } } } callback() } export const createRules = (fields, state, extRules = {}) => { const { fullNameMap } = state return fields.reduce((pre, field) => { let ruleArr = [ { required: state[`${field}Required`], message: `${requiredTipPrefix(field)}${fullNameMap[field]}`, trigger: 'blur' }, { validator: (rule, value, callback) => validator(rule, value, callback, state), trigger: 'blur' } ] if (extRules && extRules[field]) { ruleArr = [...ruleArr, ...extRules[field]] } pre[field] = ruleArr return pre }, {}) } export function toFixeds(val, pre) { const num = parseFloat(val) if (isNaN(num)) return 0 const p = 10 ** pre const value = num * p let res = (Math.round(value) / p).toString() let dotPos = res.indexOf('.') if (dotPos < 0) { dotPos = res.length res += '.' } while (res.length <= (dotPos + pre)) { res += '0' } return res }
3,vuex plugin 使用 及 文件说明
每个子组件都可以新建plugin.js 文件,用来存放当前组件的数据处理方法,以及使用vuex 的plugin API,如下:
// basicInfo/plugin.js
import { SET, LOAD, MERGE } from '../constants' // 常量
import { GoodsTagEnum, SettlementObjectEnum } from '@/constants' // 枚举数据
// 当 store.js 初始化后调用
export default store => {
const state = store.state
const set = (key, val, src) => store.commit(SET, { key, val, src })
// eslint-disable-next-line no-unused-vars
const merge = (key, val) => store.commit(MERGE, { [key]: val })
const onChange = (key, val, src, payload) => {
switch (key) {
case 'goodsType': {
set('goodsQtyUnit', state.goodsQtyUnit === 0 ? 1 : state.goodsQtyUnit)
break
}
// 省略部分
default:
break
}
}
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用,mutation 的格式为 { type, payload }
// store 中每一次载荷的提交都会触发当前方法执行
// 页面级值的修改都会触发当前事件
switch (mutation.type) {
case SET: onChange(mutation.payload.key, mutation.payload.val, mutation.payload.src, mutation.payload)
break
case LOAD: {
set('vehicleGroupDtoListShow', state.isPublish === 0)
return
}
default:
break
}
})
}
详细使用请看:vuex plugin
4,组件中使用当前页面级 store 数据
在每一个子组件中,表单直接绑定的都是当前store的状态数据,最便捷的绑定方法就是使用计算属性,为每一个属性绑定具有set,get方法的计算属性,从而在表单值改变后同步到 store 中,原理请看官方文档介绍:表单处理
子组件使用:
html
<el-form
ref="form"
:class="b('container')"
:model="poundState"
:rules="rules"
:inline="true"
label-width="140px"
>
<el-row>
<el-col
v-if="goodsNameShow"
:span="12"
>
<el-form-item
label="货源名称: "
prop="goodsName"
>
<el-input
v-model="goodsName"
placeholder="请输入货源名称"
:disabled="!goodsNameEditable"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="业务单号: "
prop="externalGoodsCode"
>
<el-input
v-model="externalGoodsCode"
maxlength="20"
:placeholder="externalGoodsCodeEditable ? '请输入业务单号' : ''"
:disabled="!externalGoodsCodeEditable"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
js
import { mapPoundState, expandField, createRules } from '../utils' // 上面有展示
import { basicField } from '../constants' // 包含表单绑定属性数据的数组
import mixin from '../mixin'
export default {
components: {
HddCollapse
},
// 注入状态数据
inject: ['poundStore', 'poundState'],
mixins: [mixin, publicDict()],
computed: {
// 将页面绑定的数据处理为计算属性的方法
// basicField形如:['goodsName','externalGoodsCode']
...mapPoundState(basicField, true),
}
}