el-menu el-collapse 左右滚动联动,左右展开项联动

需求:

  1. 左侧menu每次只保持一个子菜单的展开,右侧每次只能展开一个面板(手风琴效果)
  2. 滚动左侧的 menu 菜单,右侧的折叠面板(collapse)会跟随滚动到对应区域
  3. 滚动右侧的折叠面板,左侧的menu 菜单会跟随滚动到对应区域
  4. 展开左侧的menu 菜单,右侧的折叠面板对应的面板展开,滚动到顶部
  5. 点击左侧的 menu 子项item,右侧折叠面板中的二级菜单滚动到顶部
  6. 点击展开右侧的折叠面板,左侧对应的菜单项展开

遇到的问题:

  1. 左侧menu 的数据获取是一个接口,右侧菜单的子项是根据左侧的一级菜单id获取,每次展开左侧右侧需要去异步获取数据填充到面板中,展开的时候数据没有填充完滚动条还未撑起来滚动位置会出错
  2. 左右侧滚动联动的时候,左侧滚动去滚动右侧,右侧滚动去设置左侧滚动,当左右两侧都设置了对方跟随自己滚动的时候,滚动时会出现卡顿,他们会互相循环调用

解决问题:

  1. 动态计算前面有多少个兄弟节点,每个节点是固定高度,设置计算后的值
  2. 设置一个拦截标识,左侧滚动的时候禁止右侧去触发左侧的滚动;右侧滚动的时候禁止左侧去触发右侧的滚动

效果:

代码实现:

menu 组件:

<template>
  <div class="menuList">
    <div class="search-box">
      <el-autocomplete
        class="search-input"
        v-model="keyword"
        size="small"
        :clearable="true"
        placeholder="请输入关键字"
        :trigger-on-focus="false"
        :fetch-suggestions="querySeachSync"
        @select="handleSelect" />
      <div @click="seachData" class="bth-search el-icon-search"></div>
    </div>
    <el-menu
      id="label-mid-el-menu"
      :default-active="curMenuId[1]"
      @select="handleSelectMenu"
      @open="handleOpenMenu"
      :unique-opened="true">
      <nav-item v-for="nav in navList"
        :key="nav.classificationId"
        :ref="'menu_' + nav.classificationId"
        :nav="nav"
        @clickNav="hanleClickNavItem" />
    </el-menu>
  </div>
</template>

<script>
import NavItem from './NavItem.vue'
import { keywordLabels } from '@/api/modules/labelMiddleware.js'
export default {
  props: {
    navList: {
      type: Array,
      default: () => []
    },
    curMenuId: {
      type: Array
    }
  },
  watch: {
    curMenuId(arr) {
      // 折叠面板展开时展开对应的menu
      this.scrollToTarget(arr[0])
    }
  },
  components: {
    NavItem
  },
  data() {
    return {
      keyword: '',
      isScroll: false,
      // 定时器
      scrollTimer: null
    }
  },
  methods: {
    // 展开右侧折叠面板的时候当前menu滚动到顶部
    scrollToTarget(classificationId) {
      // 设置当前为左侧滚动标识,不触发右侧滚动
      window.rightScroll = true
      window.clearTimeout(this.scrollTimer)
      this.scrollTimer = setTimeout(_ => {
        // 放开标识
        window.rightScroll = false
      }, 800)

      // 找到目标元素
      const id = classificationId
      let el = this.$refs['menu_' + id][0]
      el = el.$el ? el.$el : el

      // 通过计算前面有多少元素来手动计算scrollTop高度
      const scrollBox = document.querySelector('#label-mid-el-menu')
      const children = Array.from(el.parentNode.children)
      const prevSiblings = []
      for (const child of children) {
        if (child === el) {
          break
        }
        prevSiblings.push(child)
      }
      const top = prevSiblings.length * 44
      this.$nextTick(_ => {
        scrollBox.scrollTo({
          top: top,
          behavior: 'smooth'
        })
      })
    },
    // 模糊搜索
    async seachData() {
      const { data } = await keywordLabels({
        keyword: this.keyword
      })
      return data
    },
    // 搜索出来选择
    handleSelect(label) {
      this.$emit('setCurIdArea', label.firstClassificationId)
    },
    // 键入关键字搜索
    async querySeachSync(keyworld, callback) {
      const data = await this.seachData()
      const res = data.map(item => {
        return {
          ...item,
          value: item.labelName
        }
      })
      callback(res)
    },
    // 设置菜单默认选中项为第一个
    setDefaultActive(list) {
      while (true) {
        if (Array.isArray(list)) {
          list = list[0]
          continue
        }
        if (list.childList && list.childList.length > 0) {
          list = list.childList
          continue
        }
        break
      }
      this.defaultActive = list.classificationId
    },
    // 点击item
    hanleClickNavItem(key) {
    },
    // 展开某个菜单列表
    handleOpenMenu(key, keyPath) {
      // console.log(key, keyPath, 'openMenu');
      this.$emit('setCurIdArea', keyPath, 'menu')
    },
    // 选择菜单列表菜单
    handleSelectMenu(key, keyPath) {
      // console.log(typeof key, keyPath, 'itemMenu')
      if (keyPath.length === 1) {
        // 菜单没有子项,触发menu
        this.$emit('setCurIdArea', keyPath, 'menu')
      } else {
        this.$emit('setCurIdArea', keyPath, 'item')
      }
    },
    // 右侧滚动的时候左侧滚动到目标位置
    menuScroll() {
      // 右侧如果正在滚动不触发左侧滚动
      if (window.rightScroll === true) {
        return
      }
      // 设置左侧正在滚动的标识,此时不触发右侧滚动
      window.leftScroll = true
      window.clearTimeout(this.scrollTimer)
      this.scrollTimer = setTimeout(_ => {
        // 左侧滚动结束,放开标识,此时才能触发右侧的滚动
        window.leftScroll = false
      }, 600)
      // 左侧滚动的menu
      const menuEl = document.querySelector('#label-mid-el-menu')
      // menu的滚动高度
      const scrollMenu = menuEl.scrollHeight - menuEl.offsetHeight
      // 右侧的折叠面板
      const labelContent = document.querySelector('.label-middleware-list-wrapper .content')
      // 折叠面板的滚动高度
      const labelScroll = labelContent.scrollHeight - labelContent.offsetHeight
      // 计算滚动比率
      const ratio = scrollMenu / labelScroll
      labelContent.scrollTo({
        top: menuEl.scrollTop / ratio
      })
    }
  },
  mounted() {
    this.setDefaultActive(this.navList)
    const menuEl = document.querySelector('#label-mid-el-menu')
    menuEl.addEventListener('scroll', this.menuScroll)
  },
  beforeDestroy() {
    const menuEl = document.querySelector('#label-mid-el-menu')
    menuEl.removeEventListener('scroll', this.menuScroll)
  }
}
</script>

<style lang="scss" scoped>
.menuList {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding-top: 16px;
  border-right: solid 1px #e6e6e6;

  .search-box {
    display: flex;
    align-items: center;
    width: 208px;
    height: 32px;
    background: #FFFFFF;
    border-radius: 2px;
    margin-left: 16px;
    margin-bottom: 16px;

    .search-input {
      flex: 1;
      margin-right: -2px;
      /deep/ .el-input__inner {
        height: 32px;
        line-height: 32px;
        border-right: none;
      }
    }
    .bth-search {
      flex: 0 0 auto;
      width: 32px;
      height: 32px;
      background: #E02D39;
      color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
      z-index: 2;
      cursor: pointer;
    }
  }
  .el-menu {
    flex: 1;
    overflow: auto;
    border-right: none;

    /deep/ {
      .is-active:not(.el-submenu) {
        color: #e02d39;
        position: relative;
        background: #F9F9FA;
        font-weight: bold;
        &::after {
          content: "";
          position: absolute;
          left: 0;
          top: 0;
          width: 2px;
          height: 100%;
          background: #e02d39;
        }
      }
    }
  }
}
</style>

menu 里面的 item 组件:

<template>
  <el-submenu
    :key="nav.classificationId"
    v-if="nav.childList && nav.childList.length>0"
    :index="String(nav.classificationId)">
    <template slot="title">
      <span>{{nav.classificationName}}</span>
    </template>
    <nav-item
      v-for="item in nav.childList"
      :key="item.classificationId"
      :nav="item"
      v-on="$listeners" />
  </el-submenu>
  <el-menu-item
    v-else
    :key="nav.classificationId"
    :index="String(nav.classificationId)"
    @click="$emit('clickNav', nav.classificationId)">
    <span slot="title">{{nav.classificationName}}</span>
  </el-menu-item>
</template>

<script>
export default {
  name: 'NavItem',
  props: {
    nav: {
      type: Object,
      default: () => {}
    }
  }
}
</script>

<style lang="scss" scoped>
  .el-submenu {
    /deep/ {
      .el-menu-item,
      .el-submenu__title  {
        height: 44px;
        line-height: 44px;
      }
      // .el-submenu__title {
      //   padding-right: 32px !important;
      // }
      // .el-submenu .el-menu-item {
      //   padding: 0 32px;
      // }
      .is-active:not(.el-submenu) {
        color: #e02d39;
        position: relative;
        background: #F9F9FA;
        font-weight: bold;
        &::after {
          content: "";
          position: absolute;
          left: 0;
          top: 0;
          width: 2px;
          height: 100%;
          background: #e02d39;
        }
      }
    }
  }
  /*菜单关闭*/
  .el-submenu /deep/ .el-submenu__title .el-submenu__icon-arrow{
    -webkit-transform: rotateZ(-90deg);
    -ms-transform: rotate(-90deg);
    transform: rotateZ(-90deg);
  }
  /*菜单展开*/
  .el-submenu.is-opened /deep/ .el-submenu__title .el-submenu__icon-arrow{
    -webkit-transform: rotateZ(0deg);
    -ms-transform: rotate(0deg);
    transform: rotateZ(0deg);
  }
</style>

右侧的 collapse LabelList 组件:

<template>
  <div class="label-middleware-list-wrapper"
    v-loading="loading"
    element-loading-text="加载中请稍后..."
    element-loading-background="rgba(255, 255, 255, .8)">
    <div class="content">
      <el-collapse
        accordion
        v-model="activeItem"
        @change="handleChangeCollapse">
        <el-collapse-item
          v-for="nav in labelList"
          :ref="'collapse_'+nav.classificationId"
          :key="nav.classificationId"
          :title="nav.classificationName"
          :name="nav.classificationId"
          >
          <div class="sub-box"
            v-for="subs in nav.children"
            :key="subs.classificationId"
            :ref="'collapse_sub_'+subs.classificationId">
            <div class="sub-title">{{subs.classificationName}}</div>
            <popover-label
              :labelList="subs.labList"
              :selectedLabels="selectedLabels"
              :popoverVisible="popoverVisible"
              @addLabels="addLabels"
              @closePopover="closePopover"
              @showPopover="showPopover" />
          </div>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>

<script>
import { queryLabels } from '@/api/modules/labelMiddleware.js'
import PopoverLabel from './PopoverLabel.vue'

export default {
  components: {
    PopoverLabel
  },
  props: {
    navList: {
      type: Array,
      default: () => []
    },
    selectedLabels: {
      type: Array,
      default: () => []
    },
    curIdArea: {
      type: Array,
      defaut: () => []
    },
    curAreaType: {
      type: String
    }
  },
  data() {
    return {
      loading: false,
      activeItem: 'org',
      labelList: [],
      childLabels: [],
      popoverVisible: {},
      // 定时器
      scrollTimer: null
    }
  },
  watch: {
    // 展开当前区域
    async curIdArea(keyPath) {
      const classificationId = keyPath[0]
      this.activeItem = isNaN(classificationId) ? classificationId : Number(classificationId)
      // 如果左侧是展开的菜单
      if (this.curAreaType === 'menu') {
        await this.openCollapse(classificationId)
        this.scrollToTarget()
      }
      // 如果左侧是点击的item
      if (this.curAreaType === 'item') {
        const curItemId = keyPath[keyPath.length - 1]
        this.scrollToItem(curItemId)
      }
    }
  },
  methods: {
    // 滚动到目标位置, 点击navItem时
    scrollToItem(navId) {
      // 设置当前为左侧滚动标识,不触发右侧滚动
      window.leftScroll = true
      window.clearTimeout(this.scrollTimer)
      this.scrollTimer = setTimeout(_ => {
        // 放开标识
        window.leftScroll = false
      }, 800)
      let el = this.$refs['collapse_sub_' + navId][0]
      el = el.$el ? el.$el : el
      const scrollBox = document.querySelector('.label-middleware-list-wrapper .content')
      this.$nextTick(_ => {
        scrollBox.scrollTo({
          top: el.offsetTop - 8,
          behavior: 'smooth'
        })
      })
    },
    // 滚动到目标位置, 展开menu时
    scrollToTarget(classificationId) {
      // 设置当前为右侧滚动标识,不触发左侧滚动
      window.leftScroll = true
      window.clearTimeout(this.scrollTimer)
      this.scrollTimer = setTimeout(_ => {
        // 放开标识
        window.leftScroll = false
      }, 800)

      // 找到目标元素
      const id = classificationId ?? this.curIdArea
      let el = this.$refs['collapse_' + id][0]
      el = el.$el ? el.$el : el
      while (el) {
        if (!el.classList.contains('el-collapse-item')) {
          el = el.parentNode
        } else {
          break
        }
      }

      // 通过计算前面有多少元素来手动计算scrollTop高度
      const scrollBox = document.querySelector('.label-middleware-list-wrapper .content')
      const children = Array.from(el.parentNode.children)
      const prevSiblings = []
      for (const child of children) {
        if (child === el) {
          break
        }
        prevSiblings.push(child)
      }
      const top = prevSiblings.length * 48
      this.$nextTick(_ => {
        scrollBox.scrollTo({
          top: top,
          behavior: 'smooth'
        })
      })
    },
    // 添加标签addLabels
    addLabels(label) {
      this.$emit('addLabels', label)
    },
    // 关闭popover
    closePopover() {
      for (let item in this.popoverVisible) {
        this.popoverVisible[item] = false
      }
    },
    // 展示特定的popover
    showPopover(item) {
      this.popoverVisible[item] = true
    },
    // 获取label列表
    async getLabels() {
      try {
        this.loading = true
        const { data } = await queryLabels({
          classificationId: Array.isArray(this.activeItem) ? this.activeItem[this.activeItem.length - 1] : this.activeItem
        })
        this.childLabels = data
        this.loading = false
        const target = this.labelList.find(item => item.classificationId === this.activeItem)
        // 设置数据结构
        data.forEach(item => {
          if (item.labList) {
            item.labList.forEach(label => {
              // 最终值
              label.tagAttributes = []
              label.curRanges = []
              // 临时修改的值,点击确定后才赋值给最终的值
              label.tempTagAttributes = []
              label.tempCurRanges = []
            })
          }
        })
        if (target) {
          this.$set(target, 'children', data)
        }
      } catch (err) {
        console.log(err)
      }
    },
    // 设置机构标签
    async setOrgLabels(id) {
      const target = this.labelList.find(() => id === this.activeItem)
        const data = [{
          classificationId: 'org',
          classificationName: "组织机构",
          labList: [
            {
              labelId: "org_label",
              labelName: "中台机构",
              labelPropType: "org_label",
              labelPropList: [],
              labelRuleDescription: "",
              selected: false,
              tagAttributes: [],
              curRanges: [],
              value: "",
              orgLabel: ''
            }
          ],
          selected: false
        }]
        if (target) {
          this.$set(target, 'children', data)
        }
    },
    // 折叠面板展开设置数据
    async openCollapse(id) {
      if (id) {
        // 初始化手动添加的组织机构
        if (id === 'org') {
          await this.setOrgLabels('org')
        } else {
          // 获取当前详细labels
          await this.getLabels()
        }
      }
    },
    // 展开关闭collapse
    async handleChangeCollapse(id) {
      if (id) {
        await this.openCollapse(id)
        // 当前展开项的第0或者第一个子元素,由于左侧没有本身子元素所以设为第一个
        let classificationId = (this.childLabels[1]?.classificationId ?? this.childLabels[0]?.classificationId) || ''
        // 组织机构是手动添加的数据单独处理
        classificationId = id === 'org' ? 'org_label' : classificationId
        this.$emit('changeCollapse', [String(id), String(classificationId) || String(id)])
        // 先将右侧滚动到目标位置
        this.scrollToTarget(id)
        // 左侧滚动到目标位置
        this.labelListScroll()
      }
    },
    // 右侧滚动的时候左侧滚动到目标位置
    labelListScroll() {
      // 左侧如果正在滚动不触发右侧滚动
      if (window.leftScroll === true) {
        return
      }
      // 设置右侧正在滚动的标识,此时不触发左侧滚动
      window.rightScroll = true
      window.clearTimeout(this.scrollTimer)
      this.scrollTimer = setTimeout(_ => {
        // 右侧滚动结束,放开标识,此时才能触发左侧的滚动
        window.rightScroll = false
      }, 600)
      // 左侧滚动的menu
      const menuEl = document.querySelector('#label-mid-el-menu')
      // menu的滚动高度
      const scrollMenu = menuEl.scrollHeight - menuEl.offsetHeight
      // 右侧的折叠面板
      const labelContent = document.querySelector('.label-middleware-list-wrapper .content')
      // 折叠面板的滚动高度
      const labelScroll = labelContent.scrollHeight - labelContent.offsetHeight
      // 计算滚动比率
      const ratio = scrollMenu / labelScroll
      menuEl.scrollTo({
        top: labelContent.scrollTop * ratio
      })
    }
  },
  mounted() {
    const labelContent = document.querySelector('.label-middleware-list-wrapper .content')
    labelContent.addEventListener('scroll', this.labelListScroll)
  },
  beforeDestroy() {
    const labelContent = document.querySelector('.label-middleware-list-wrapper .content')
    labelContent.removeEventListener('scroll', this.labelListScroll)
  },
  created() {
    this.labelList = this.navList
    this.openCollapse(this.activeItem)
  }
}
</script>

<style lang="scss" scoped>
.label-middleware-list-wrapper {
  height: 100%;
  .content {
    height: 100%;
    overflow: auto;
  }
  .el-collapse {
    position: relative;
  }
  .el-collapse-item /deep/ .el-collapse-item__content {
    border-top: 1px solid #EBEEF5;
    padding-bottom: 0;
  }
  /deep/ .el-collapse-item__header{
    font-size: 14px;
    font-family: PingFangSC-Regular, PingFang SC;
    font-weight: 500;
    color: #262626;
  }
  .sub-box {
    padding: 8px 16px 0 12px;
    background: #FAFAFA;

    .sub-title {
      height: 18px;
      font-size: 13px;
      font-family: PingFangSC-Regular, PingFang SC;
      font-weight: 400;
      color: #292B33;
      line-height: 18px;
      margin-bottom: 9px;
    }
    .label-box {
      display: flex;

      /deep/ .label-item {
        white-space: nowrap;
        display: flex;
        align-items: center;
        height: 28px;
        padding: 0 8px;
        background: #F1F1F1;
        border-radius: 3px;
        font-size: 12px;
        font-family: PingFangSC-Regular, PingFang SC;
        font-weight: 400;
        color: #595959;
        margin-right: 8px;
        margin-bottom: 8px;
        cursor: pointer;
      }
    }
  }

}
</style>

 menu 和 右侧的 LbelList 组件的父组件:

<template>
  <el-dialog
    class="dialog"
    title="标签组合筛选"
    width="80%"
    :visible="visible"
    :close-on-click-modal="false"
    @close="handleClose"
    >
    <div class="body">
      <div class="line"></div>
      <div class="content">
        <div class="left-nav">
          <nav-list
            :navList="navList"
            :curMenuId.sync="curMenuId"
            @setCurIdArea="setCurIdArea" />
        </div>
        <div class="label-collapse">
          <label-list
            :navList="navList"
            :selectedLabels="selectedLabels"
            :curIdArea="curIdArea"
            :curAreaType="curAreaType"
            @addLabels="addLabels"
            @changeCollapse="changeCollapse" />
        </div>
      </div>
      <div class="selected-label">
        <div class="title">已选标签:</div>
        <div class="selected-box labels-box">
          <div class="label-item" v-for="(label, index) in selectedLabels" :key="label.labelId">
            <div class="text">
              <div>{{label.labelName}}:</div>
              <div class="value-item"
                v-for="item in renderLabelInfo(label)"
                :key="item.showValue">{{item.showValue}}</div>
            </div>
            <div @click="deleteLabel(label, index)" class="delete-label el-icon-close"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="footer" slot="footer">
      <div class="tip">共筛选出<span class="number">{{total}}</span>个</div>
      <div class="buttons">
        <button class="btn-reset" @click="handleClickClearAll">清空重选</button>
        <button class="btn-confirm" @click="handleClickSubmit">确定</button>
      </div>
    </div>
  </el-dialog>
</template>

<script>
import NavList from './NavList.vue'
import LabelList from './LabelList.vue'
import { getClassificationTree } from '@/api/modules/labelMiddleware.js'
export default {
  components: {
    NavList,
    LabelList
  },
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    selectedLabels: {
      type: Array,
      default: () => []
    },
    total: {
      type: Number
    },
    renderLabelInfo: {
      type: Function,
      default: () => () => {}
    }
  },
  data() {
    return {
      navList: [],
      search: '',
      curIdArea: [],
      // 左侧选中的type,展开还是点击item
      curAreaType: '',
      curMenuId: ['org', 'org_label']
    }
  },
  methods: {
    // 添加label
    addLabels(label) {
      this.$emit('addLabels', label)
    },
    // 删除label
    deleteLabel(label, index) {
      this.$emit('deleteLabel', label, index)
    },
    // 左侧菜单当前展开项
    setCurIdArea(keyPath, type) {
      this.curIdArea = keyPath
      this.curAreaType = type
    },
    // 获取navList,左侧的menu菜单数据,右侧的面板一级数据
    async getNavList() {
      const { data } = await getClassificationTree()
      this.navList = data || []
      this.navList.unshift({
        classificationId: 'org',
        classificationName: "组织机构",
        childList: [
          {
            childList: null,
            classificationId: 'org_label',
            classificationName: "组织机构"
          }
        ]
      })
    },
    // 点击清空重选
    handleClickClearAll() {
      this.$emit('resetSelectLabels')
    },
    // 点击确定
    handleClickSubmit() {
      this.$emit('getTableData')
      this.handleClose()
    },
    // 关闭dialog
    handleClose() {
      this.$emit('closeDialog')
    },
    // 修改了折叠面板,设置当前展开想的父级和当前id
    changeCollapse(idArr) {
      this.curMenuId = idArr
    }
  },
  mounted() {
    this.getNavList()
  }
}
</script>

<style lang="scss" scoped>
.dialog {
  /deep/ .el-dialog{
    margin-top: 8vh !important;
  }
  /deep/ .el-dialog__body {
    padding: 0;
  }
  .left-nav {
    height: 100%;
    width: 240px;
  }
  .content {
    display: flex;
    border: 1px solid rgba(0, 0, 0, 0.1);
    border-right: none;
    border-left: none;
    margin-bottom: 8px;
    height: 556px;
    padding: 0;
    padding-right: 16px;

    .label-collapse {
      flex: 1;
      overflow: auto;
      /deep/ {
        .el-collapse-item__header {
          padding-left: 20px;
        }
      }
    }

  }
  .selected-label {
    margin-bottom: 18px;
    padding: 0 16px;
    .title {
      height: 20px;
      font-size: 14px;
      font-family: PingFangSC-Medium, PingFang SC;
      font-weight: 500;
      color: #262626;
      line-height: 20px;
      margin-bottom: 12px;
    }
    .labels-box {
      display: flex;
      min-height: 50px;
      max-height: 76px;
      overflow: auto;
      flex: 1;
      flex-wrap: wrap;

      .label-item {
        display: flex;
        align-items: center;
        height: 28px;
        background: rgba(224, 45, 57, 0.06);
        border-radius: 3px;
        padding: 4px 11px 4px 8px;
        margin-right: 8px;
        margin-bottom: 8px;
        white-space: nowrap;

        .text {
          display: flex;
          align-items: center;
          white-space: nowrap;
          font-size: 14px;
          font-family: PingFangSC-Regular, PingFang SC;
          font-weight: 400;
          color: #595959;
          margin-right: 24px;
          .value-item:not(:last-child){
            margin-right: 8px;
          }
        }
        .delete-label {
          color: #8C8C8C;
          cursor: pointer;
          font-size: 16px;
        }
      }
    }
  }

  .footer {
    display: flex;
    justify-content: space-between;
    align-items: center;

    .tip {
      font-size: 12px;
      font-family: PingFangSC-Regular, PingFang SC;
      font-weight: 400;
      color: #595959;
      line-height: 17px;

      .number {
        font-size: 12px;
        font-family: PingFangSC-Regular, PingFang SC;
        font-weight: 400;
        color: #E02D39;
        line-height: 17px;
        padding: 0 4px;
      }
    }
    .buttons {
      display: flex;
      align-items: center;
      .btn-reset {
        width: 72px;
        height: 28px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 3px;
        border: 1px solid #E02D39;
        font-size: 12px;
        font-family: PingFangSC-Medium, PingFang SC;
        font-weight: 500;
        color: #E02D39;
        margin-right: 8px;
        cursor: pointer;
      }
      .btn-confirm {
        height: 28px;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 60px;
        background: #E02D39;
        border-radius: 3px;
        font-size: 12px;
        font-family: PingFangSC-Medium, PingFang SC;
        font-weight: 500;
        color: #FFFFFF;
        cursor: pointer;
      }
    }
  }
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值