vue表单级别状态管理(vuex plugin 插件使用分享)

分享重点:

  • 组件级状态管理(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),
  }
}

猜你喜欢

转载自blog.csdn.net/lwx931449660/article/details/121271791
今日推荐