封装一个树形结构的穿梭框

302 阅读7分钟
  • 全选功能
  • 左边搜索滚动定位到具体城市

image.png 截屏2023-06-28 14.13.48.png

export function cloneDeep(data) {
  return JSON.parse(JSON.stringify(data))
}

ChooseCity.vue子组件

<template>
  <div>
    <a-modal
      :width="800"
      :title="title"
      :visible="show"
      :confirm-loading="confirmLoading"
      :loading="loading"
      @ok="handleOk"
      @cancel="handleCancel"
    >
      <div class="transfer-box">
        <a-transfer
          :data-source="dataSourceTransfer"
          :target-keys="filterTargetKeys"
          :render="(item) => item.title"
          :show-select-all="true"
          @change="handleTransferChange"
          :list-style="{
            width: '300px',
            height: '360px'
          }"
          show-search
          :titles="['可选城市', '已选城市']"
          @search="onSearch"
        >
          <template
            slot="children"
            slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect, itemSelectAll } }"
          >
            <a-tree
              v-if="direction === 'left'"
              :key="searchValue"
              blockNode
              checkable
              :defaultExpandAll="false"
              :checkedKeys="[...selectedKeys, ...targetKeys]"
              :treeData="cityOptions"
              @expand="onExpand"
              :expanded-keys.sync="expandedKeys"
              :auto-expand-parent="autoExpandParent"
              @check="
                (_, props) => {
                  handleTreeChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll)
                }
              "
              @select="
                (_, props) => {
                  handleTreeChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll)
                }
              "
            >
              <template slot="title" slot-scope="{ title }">
                <span v-if="title.indexOf(searchValue) > -1">
                  {{ title.substr(0, title.indexOf(searchValue)) }}
                  <span :id="title.indexOf(searchValue) > -1 ? 'target-elem' : ''" style="color: #f50">{{
                    searchValue
                  }}</span>
                  {{ title.substr(title.indexOf(searchValue) + searchValue.length) }}
                </span>
                <span v-else>{{ title }}</span>
              </template>
            </a-tree>
          </template>
        </a-transfer>
      </div>

      <!-- 底部 -->
      <template slot="footer">
        <a-button key="cancel" class="normal-btn" @click="handleCancel">取消</a-button>
        <a-button key="submit" class="common-btn" :loading="confirmLoading" @click="handleOk">提交</a-button>
      </template>
    </a-modal>
  </div>
</template>

<script>
import { cloneDeep } from '@/views/financialBillSystem/billingManagement/utils/mapping'

function isChecked(selectedKeys, eventKey) {
  return selectedKeys.indexOf(eventKey) !== -1
}

function handleTreeData(data, targetKeys = []) {
  data.forEach((item) => {
    item['disabled'] = targetKeys.includes(item.key)
    if (item.children) {
      handleTreeData(item.children, targetKeys)
    }
  })
  return data
}

export default {
  name: 'ChooseCity',
  props: {
    sureCityList: {
      required: true,
      type: Array
    },
    noFilterKeys: {
      required: true,
      type: Array
    },
    cityList: {
      required: true,
      type: Array
    }
  },
  data() {
    return {
      title: '选择城市',
      show: true,
      loading: false,
      confirmLoading: false,
      targetKeys: [],
      dataSource: [],
      cityOptions: [],
      arr: [],
      expandedKeys: [],
      searchValue: '',
      autoExpandParent: true,
      dataList: [],
      parentKeys: [], //父节点
      filterTargetKeys: [], //过滤掉父节点后
      dataSourceTransfer: [] //所有子节点原数据
    }
  },

  created() {
    console.log('sureCityList', this.sureCityList, this.cityList)
    this.$nextTick(() => {
      this.initCityTreeData()
    })
  },
  methods: {
    //左边搜索滚动到指定位置
    scrollPosition() {
      this.$nextTick(() => {
        const elem = document.querySelector('#target-elem') // 获取目标元素
        console.log('elem', elem)
        if (!elem) return
        const scrollableElem = document.querySelector('.ant-transfer-list-body-customize-wrapper ') // 获取目标元素的父元素(可滚动容器)
        console.log(scrollableElem)
        const scrollY = scrollableElem.scrollTop // 获取当前垂直方向上滚动的距离
        const scrollHeight = scrollableElem.scrollHeight // 获取可滚动容器的实际高度
        const clientHeight = scrollableElem.clientHeight // 获取可滚动容器的可视区域高度

        const targetY = elem.offsetTop - scrollableElem.offsetTop // 获取目标元素相对于可滚动容器的上边距
        const scrollToY = Math.min(targetY, scrollHeight - clientHeight) // 计算滚动条滚动目标位置

        scrollableElem.scrollTo({
          top: scrollToY,
          behavior: 'smooth'
        })
      })
    },
    //源数据-转化为一层-父加子
    flatten(list = []) {
      list.forEach((item) => {
        this.dataSource.push(item)
        this.flatten(item.children)
      })
    },
    generateData(_level, _preKey, _tns) {
      const preKey = _preKey || '0'
      const tns = _tns || this.cityOptions

      const children = []
      for (let i = 0; i < x; i++) {
        const key = `${preKey}-${i}`
        tns.push({ title: key, key, scopedSlots: { title: 'title' } })
        if (i < y) {
          children.push(key)
        }
      }
      if (_level < 0) {
        return tns
      }
      const level = _level - 1
      children.forEach((key, index) => {
        tns[index].children = []
        return this.generateData(level, key, tns[index].children)
      })
    },
    generateList(data) {
      for (let i = 0; i < data.length; i++) {
        const node = data[i]
        const key = node.key
        const title = node.title
        this.dataList.push({ key, title })
        if (node.children) {
          this.generateList(node.children)
        }
      }
    },
    getParentKey(key, tree) {
      let parentKey
      for (let i = 0; i < tree.length; i++) {
        const node = tree[i]
        if (node.children) {
          if (node.children.some((item) => item.key === key)) {
            parentKey = node.key
          } else if (this.getParentKey(key, node.children)) {
            parentKey = this.getParentKey(key, node.children)
          }
        }
      }
      return parentKey
    },
    //搜索展开当前父节点
    onExpand(expandedKeys) {
      console.log('expandedKeys', expandedKeys)
      this.expandedKeys = expandedKeys
      this.autoExpandParent = false
    },
    handleSearch(e) {
      setTimeout(() => {
        const value = e

        const expandedKeys = this.dataList
          .map((item) => {
            if (item.title.indexOf(value) > -1) {
              return this.getParentKey(item.key, this.cityOptions)
            }
            return null
          })
          .filter((item, i, self) => item && self.indexOf(item) === i)
        console.log(expandedKeys)
        Object.assign(this, {
          expandedKeys: value ? expandedKeys : [],
          searchValue: value
        })

        this.scrollPosition()
        console.log(this.autoExpandParent)
      }, 200)
    },

    onSearch(dir, val) {
      console.log(dir, val)
      if (dir === 'left') {
        this.handleSearch(val)
      }
    },
    // 查找详情的父节点
    checkDetailParentKey() {
      let parKey = [] // 查找详情的父节点
      if (this.sureCityList && this.sureCityList.length > 0) {
        this.filterTargetKeys = this.sureCityList.map((item) => {
          return item.key
        })
        parKey = this.searchParents(this.dataSource, this.filterTargetKeys, false)

        console.log('初始化', this.targetKeys, this.filterTargetKeys, this.noFilterKeys, parKey)
      }
      return parKey
    },
    //获取子节点全部勾选的父节点
    getAllSelectParentKey(parentList) {
      //找出重复出现的次数,如果小于子节点的length则去除这个元素
      //如果等于子节点的length则只保留一个这个元素

      //获得元素以及出现了几次
      let getRepeatObj = this.getShowAcount(parentList)
      console.log(getRepeatObj)
      let newObj = {} //只保留等于子节点长度的
      for (let k in getRepeatObj) {
        this.cityList.forEach((item) => {
          if (item.key == k) {
            item.children.length > getRepeatObj[k] ? (newObj[k] = 0) : (newObj[k] = getRepeatObj[k])
          }
        })
      }
      console.log('找到父节点的子节全被选择的父节点', newObj)
      let allSelectParKey = []
      let noAllSelectparKey = []
      for (let k in newObj) {
        if (newObj[k] !== 0) {
          allSelectParKey.push(k)
        } else {
          noAllSelectparKey.push(k)
        }
      }
      return [allSelectParKey, noAllSelectparKey]
    },
    //初始化树节点
    initCityTreeData() {
      this.cityOptions = cloneDeep(this.cityList)
      this.dataSourceTransfer = this.getAllChild(cloneDeep(this.cityList)) //所有子节点原数据-控制左边的总数量的
      this.generateList(this.cityOptions) //将树结构处理成需要的字段
      this.flatten(this.cityList) //所有数据-父加子,将树结构转化为一级

      this.parentKeys = this.cityOptions.map((item) => {
        return item.key
      }) //获取所有父节点的key
      let parKey = this.checkDetailParentKey() // 查找详情的父节点
      console.log(parKey)
      let parKeys = this.getAllSelectParentKey(parKey)[0] //获取子节点全部勾选的父节点
      console.log('parKeys', parKeys)
      this.targetKeys = [...this.filterTargetKeys, ...parKeys]
      console.log('最后的key', this.targetKeys)
      this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys) //选择后让勾选禁用
    },

    //判断数组里元素出现的次数
    getShowAcount(names) {
      var countedNames = names.reduce((obj, name) => {
        if (name in obj) {
          obj[name]++
        } else {
          obj[name] = 1
        }
        return obj
      }, {})
      //reduce的第二个参数就是obj的初始值
      return countedNames
    },
    // 树形结构数据过滤
    filterTree(tree = [], targetKeys = [], validate = () => {}) {
      console.log('tree', tree, targetKeys)
      if (!tree.length) {
        return []
      }
      const result = []
      for (let item of tree) {
        if (item.children && item.children.length) {
          let node = {
            ...item,
            children: [],
            disabled: targetKeys.includes(item.key) // 禁用属性
          }
          // 子级处理
          for (let o of item.children) {
            if (!validate.apply(null, [o, targetKeys])) continue
            node.children.push({ ...o, disabled: targetKeys.includes(o.key) })
          }
          if (node.children.length) {
            result.push(node)
          }
        }
      }
      return result
    },
    formatOptions(data, children) {
      const arr = children || []
      data.forEach((e) => {
        const item = {
          key: e.id,
          title: e.name,
          scopedSlots: { title: 'title' }
          // disabled: e.children ? true : false
        }
        if (e.children && e.children.length) {
          item.children = []
          this.formatOptions(e.children, item.children)
        }
        arr.push(item)
      })
      return arr
    },
    onChange(targetKeys) {
      console.log('Target Keys:', targetKeys)
      // this.targetKeys = targetKeys
      // this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys)
    },
    handleTransferChange(targetKeys, direction, moveKeys) {
      console.log('Target Keys:', targetKeys, direction, moveKeys)

      // 已选城市移除
      if (direction === 'left') {
        //全选
        if (this.isSelectAllCity(moveKeys, this.getAllChild(this.cityOptions))) {
          this.targetKeys = []
          this.filterTargetKeys = []
        } else {
          let parKey = this.searchParents(this.dataSource, moveKeys) //查找移除的父节点
          this.targetKeys = this.targetKeys.filter((item) => ![...moveKeys, ...parKey].includes(item))
          console.log('parKey', parKey, this.targetKeys)
          this.filterTargetKeys = this.filterTargetKeys.filter((item) => !moveKeys.includes(item))
        }
        this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys)
      } else {
        //全选
        if (this.isSelectAllCity(targetKeys, this.getAllChild(this.cityOptions))) {
          console.log('全选')
          this.targetKeys = [...targetKeys, ...this.parentKeys]
          this.filterTargetKeys = targetKeys
        } else {
          console.log('非全选')
          let parKey = this.searchParents(this.dataSource, targetKeys) //查找父节点
          this.targetKeys = [...targetKeys, ...parKey]
          this.filterTargetKeys = targetKeys
        }

        console.log(' this.targetKeys', this.targetKeys, ',this.filterTargetKeys', this.filterTargetKeys)
        this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys)
      }
    },
    handleTreeChecked(keys, e, checkedKeys, itemSelect, itemSelectAll) {
      // console.log('check', keys, e, checkedKeys, itemSelect, itemSelectAll)
      // this.targetKeys = keys
      const {
        eventKey,
        checked,
        dataRef: { children }
      } = e.node
      if (this.parentKeys && this.parentKeys.includes(eventKey)) {
        // 父节点选中:将所有子节点也选中
        // let childKeys = children ? children.map((item) => item.key) : []
        // if (childKeys.length) itemSelectAll(childKeys, !checked)
        //当子节点已有被禁用勾选的,再次点击全选只勾选没被禁用的
        let abledChildKeys = []
        children.forEach((item) => {
          if (!item.disabled) {
            abledChildKeys.push(item.key)
          }
        })
        // console.log('abledChildKeys', abledChildKeys)
        let childKeys = abledChildKeys.length ? abledChildKeys : []
        if (childKeys.length) itemSelectAll(childKeys, !checked)
      } else {
        itemSelect(eventKey, isChecked(keys, eventKey))
      }
      // itemSelect(eventKey, !isChecked(checkedKeys, eventKey)) // 子节点选中
    },
    //关闭编辑
    closeEditModal(nodeData) {
      this.isShowEditModal = nodeData
    },
    handleBillNameEdit(type) {
      console.log('编辑')
      this.type = type
      this.isShowEditModal = true
    },
    //查找所有父节点
    searchParents(list, moveKeys, flag = true) {
      let arr = []
      list.forEach((item) => {
        if (moveKeys.includes(item.key)) {
          if (item.key != '0') {
            arr.push(item.parentId)
          }
        }
      })
      if (flag) {
        arr = [...new Set(arr)]
      }
      return arr
    },
    // 关闭弹窗
    handleCancel() {
      this.$emit('closeCityModal', false)
    },
    handleOk() {
      console.log(this.filterTargetKeys)
      if (this.filterTargetKeys.length === 0) {
        return this.$message.info('至少选择一个城市')
      }

      //穿梭框右边过滤出父节点的数据
      let sureCityList = this.dataList.filter((item) => {
        return this.filterTargetKeys.includes(item.key)
      })

      let flag = this.isSelectAllCity(sureCityList, this.getAllChild(this.cityOptions))
      this.$emit('handleChangeCity', sureCityList, this.dataSource, flag)
      this.$emit('closeCityModal', false)
    },
    //获取所有子节点
    getAllChild(list) {
      let arr = []
      list.forEach((item) => {
        if (item.children.length) {
          item.children.forEach((child) => {
            arr.push(child)
          })
        }
      })
      return arr
    },
    //判断是否选了全部城市
    isSelectAllCity(selectCity, childAllCity) {
      return selectCity.length === childAllCity.length
    }
  }
}
</script>

<style lang="less" scoped>
.transfer-box {
  padding-bottom: 35px;
  display: flex;
  justify-content: center;
}
::v-deep .ant-transfer-list-body.ant-transfer-list-body-with-search {
  overflow-y: hidden;
  padding-top: 50px;
  width: 100%;
}
::v-deep .ant-transfer-list-body-customize-wrapper {
  overflow: auto;
  height: 100%;
}
::v-deep .ant-transfer-list-content {
  padding-bottom: 15px;
  height: 100%;
}
::v-deep .ant-transfer-list-body.ant-transfer-list-body-with-search .ant-transfer-list-body-search-wrapper {
  position: absolute;
  z-index: 99;
  width: 100%;
  background-color: #fff;
}
</style>


父组件

<template>
  <div ref="ruleConfigDetail" class="financial-bill">
    <bread-crumb></bread-crumb>
    <a-card title="" :bordered="false" class="card-box" :loading="pageLoading">
      <a-form-model
        class="collapse-search-form"
        ref="ruleForm"
        :model="form"
        :rules="rules"
        :label-col="formItemLayout.labelCol"
        :wrapper-col="formItemLayout.wrapperCol"
      >

        <a-card title="业务规则" :bordered="false">
          <!-- 城市 -->
          <a-row :gutter="60">
            <a-col :lg="10" :md="12" :sm="24">
              <a-form-model-item label="城市" prop="sureCityList">
                <a-button class="common-btn" @click="openAddCityModal" :disabled="!isEdit">
                  <a-icon type="plus" />添加城市
                </a-button>
              </a-form-model-item>
            </a-col>
          </a-row>
          <div class="city-tags" v-if="form.sureCityList.length > 0">
            <template v-for="(tag, index) in form.sureCityList">
              <a-tooltip :key="tag.key" :title="tag.title">
                <a-tag :key="tag.key" :closable="isEdit" @close="deleteTag(index)">
                  {{ tag.title }}
                </a-tag>
              </a-tooltip>
            </template>
          </div>
        </a-card>
       
      </a-form-model>
      <!-- 底部 -->
      <div class="footer-btn" v-if="isEdit">
        <a-button class="normal-btn" @click="handleCancel">取消</a-button>
        <a-button class="common-btn" @click="handleSubmit">提交</a-button>
      </div>
    </a-card>

   
    <!-- 选择城市 -->
    <ChooseCity
      ref="chooseCity"
      v-if="isShowCityModal"
      :cityList="cityList"
      @closeCityModal="closeCityModal"
      @handleChangeCity="handleChangeCity"
      :sureCityList="form.sureCityList"
    />
  </div>
</template>

<script>
import BreadCrumb from '@/components/tools/Breadcrumb'
import ChooseBillRuleModel from './components/ChooseBillRuleModel'
import ChooseCity from './components/ChooseCity.vue'
import { getRuleConfigDetail, addBillRuleConfig, editBillRuleConfig } from '@/api/financialBillSystem/billingManagement'
import { cloneDeep } from '@/views/financialBillSystem/billingManagement/utils/mapping'

export default {
  name: 'BillingRuleConfigDetail',
  components: {
    BreadCrumb,
    ChooseCity
  },
  data() {
    return {
      form: {
        sureCityList: []
      },
      formItemLayout: {
        labelCol: { span: 8 },
        wrapperCol: { span: 16 }
      },
      isShowCityModal: false,
      rules: {
        sureCityList: [{ required: true, message: '城市不能为空', trigger: ['change', 'blur'] }]
      },
      dataSource: [], //源数据
      cityList: [],
      pageLoading: false,
      wholeCountry: [{ cityId: '-1', cityName: '全国' }], //全国
      isSelectAllFag: false
    }
  },
  created() {
    this.cityList = this.formatTreeOptions(this.$store.getters.cityList)
    if (this.id) {
      this.$nextTick(async () => {
        await this.getDetailData()
      })
    }
  },
  methods: {
    //取消
    handleCancel() {
      this.$router.go(-1)
    },
    //提交
    handleSubmit() {
      this.$refs.ruleForm.validate((valid) => {
        console.log(valid)
        if (!valid) return false
        this.$confirm({
          title: '是否确认生效规则?',
          // content: 'Some descriptions',
          getContainer: () => this.$refs.ruleConfigDetail,
          okText: '确认',
          // okType: 'danger',
          cancelText: '取消',
          onOk: async () => {
            console.log('OK')

            //校验成功后

            let params = {
              ...this.form,
              invoiceRuleId: this.chooseBill.id,
              cityList: this.getSelectCity()
            }
            console.log('提交', params)
            if (this.id) {
              const { success, message, info } = await editBillRuleConfig(params)

              if (success) {
                this.$message.success(info ? info : message)
                this.$router.back()
              } else {
                this.$message.error(message)
              }
            } else {
              const { success, message, info } = await addBillRuleConfig(params)
              if (success) {
                this.$message.success(info ? info : message)
                this.$router.back()
              } else {
                this.$message.error(message)
              }
            }
          },
          onCancel() {
            console.log('Cancel')
          }
        })
      })
    },
    //判断是否全选-处理成后端需要的数据
    getSelectCity() {
      let cityList = []
      //全选
      if (this.isSelectAllFag) {
        cityList = this.wholeCountry
      } else {
        this.form.sureCityList.forEach((item) => {
          cityList.push({ cityId: item.key, cityName: item.title })
        })
      }
      return cityList
    },
    //处理成下拉框的格式
    formaterSelectList(data) {
      let arr = []
      data.forEach((item) => {
        arr.push({
          value: item.dictCode,
          label: item.dictValue
        })
      })
      return arr
    },

    //处理穿梭框树形结构
    formatTreeOptions(data, children, parentId = '0') {
      // console.log(data);
      const arr = children || []
      data.forEach((e) => {
        // console.log(e)
        const item = {
          key: e.cityId.toString(),
          title: e.allName,
          scopedSlots: { title: 'title' },
          parentId
          // disabled: e.children ? true : false
        }

        if (e.cityCommons && e.cityCommons.length) {
          item.children = []
          this.formatTreeOptions(e.cityCommons, item.children, e.cityId.toString())
        }
        arr.push(item)
      })
      return arr
    },
    //获取详情
    async getDetailData() {
      this.pageLoading = true
      let params = {
        id: this.id
      }

      const { success, message, info } = await getRuleConfigDetail(params)
      if (success) {

        // info.cityList = this.wholeCountry
        let detailCityKeys = cloneDeep(this.getDetailChildKeys(info.cityList))
        console.log('详情城市', detailCityKeys)
        this.$set(this.form, 'sureCityList', detailCityKeys)
        this.pageLoading=false
      } else {
        this.$message.error(message)
      }
    },
    //判断是否有全国-获取详情的城市key
    getDetailChildKeys(detailCityList) {
      let arr = []
      //全选
      if (detailCityList[0].cityId == -1) {
        this.$store.getters.cityList.forEach((item) => {
          if (item.cityCommons.length) {
            item.cityCommons.forEach((child) => {
              arr.push({ title: child.allName, key: child.cityId.toString() })
            })
          }
        })
      } else {
        //非全选
        detailCityList.forEach((item) => {
          arr.push({ title: item.cityName, key: item.cityId.toString() })
        })
      }

      return arr
    },
    //获取所有子节点
    getAllChild(list) {
      let arr = []
      list.forEach((item) => {
        if (item.children.length) {
          item.children.forEach((child) => {
            arr.push(item.key)
          })
        }
      })
      return arr
    },
   
    //删除城市标签
    deleteTag(index) {
      this.form.sureCityList.splice(index, 1)
      console.log(index, this.form.sureCityList)
      this.$refs['ruleForm'].validateField('sureCityList') //解决校验不消失的问题
    },
    //确认已选择的城市
    handleChangeCity(nodeData, dataSource, flag) {
      console.log('是否全选', flag)
      this.isSelectAllFag = flag
      this.$set(this.form, 'sureCityList', nodeData)
      this.$refs['ruleForm'].validateField('sureCityList') //解决校验不消失的问题
      this.$forceUpdate()
      this.dataSource = dataSource
    },
    //关闭选择城市弹窗
    closeCityModal(nodeData) {
      this.isShowCityModal = false
    },
    //打开选择城市弹窗
    openAddCityModal() {
      this.isShowCityModal = true
    },
   
  }
}
</script>

<style lang="less" scoped>
::v-deep .ant-card .ant-card-body {
  padding: 16px 0 0;
}
.collapse-search-form {
  padding: 15px 0 16px;
  .ant-calendar-range-picker-separator {
    color: #333;
    vertical-align: baseline;
  }
  .button-group {
    display: inline-block;
    .ant-btn {
      margin: 0 12px;
      &.ant-btn-link .anticon {
        margin-left: 2px;
        font-size: 12px;
      }
    }
  }
  .line-box {
    margin-top: 20px;
  }
  .tax-table {
    padding: 15px 80px 20px;
  }
  .bill-rule-btn {
    margin-left: 80px;
    margin-bottom: 20px;
  }
  .city-tags {
    border: 1px solid #ccc;
    border-radius: 5px;
    white-space: wrap;
    margin: 0 230px 30px 175px;
    min-height: 50px;
    max-height: 200px;
    padding: 10px;
    overflow-y: scroll;
  }
  .ant-tag {
    margin-bottom: 10px;
  }
}
.footer-btn {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-bottom: 40px;
  .ant-btn {
    width: 86px;
  }
  .common-btn {
    margin-left: 100px;
  }
}
::v-deep .ant-modal-confirm .ant-modal-confirm-btns button + button {
  background-image: linear-gradient(90deg, #ff921b 20%, #ff7000 85%);
  border-width: 0px;
  outline: none;
}
.normal-btn {
  &:hover,
  &:focus,
  &:link,
  &:active,
  &:visited {
    border-color: #ff6633;
    color: #ff6633;
  }
}
::v-deep .invoiceType .ant-form-item-control.has-error .ant-form-explain {
  display: var(--primary);
}
</style>