『Vue2 后台管理系统』

本文章知识来源于 后台集成方案 vue-element-admin


二、功能

8、DndList 可拖拽列表

安装 vuedraggable 插件:

npm i vuedraggable

src/components/DndList/index.vue

<template>
  <div class="dndList">
    <div :style="{width:width1}" class="dndList-list">
      <h3>{{ list1Title }}</h3>
      <draggable :set-data="setData" :list="list1" group="article" class="dragArea">
        <div v-for="element in list1" :key="element.id" class="list-complete-item">
          <div class="list-complete-item-handle">
            {{ element.id }}[{{ element.author }}] {{ element.title }}
          </div>
          <div style="position:absolute;right:0px;">
            <span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
              <i style="color:#ff4949" class="el-icon-delete" />
            </span>
          </div>
        </div>
      </draggable>
    </div>
    <div :style="{width:width2}" class="dndList-list">
      <h3>{{ list2Title }}</h3>
      <draggable :list="list2" group="article" class="dragArea">
        <div v-for="element in list2" :key="element.id" class="list-complete-item">
          <div class="list-complete-item-handle2" @click="pushEle(element)">
            {{ element.id }} [{{ element.author }}] {{ element.title }}
          </div>
        </div>
      </draggable>
    </div>
  </div>
</template>

<script>
import draggable from 'vuedraggable'

export default {
  name: 'DndList',
  components: { draggable },
  props: {
    list1: {
      type: Array,
      default() {
        return []
      }
    },
    list2: {
      type: Array,
      default() {
        return []
      }
    },
    list1Title: {
      type: String,
      default: 'list1'
    },
    list2Title: {
      type: String,
      default: 'list2'
    },
    width1: {
      type: String,
      default: '48%'
    },
    width2: {
      type: String,
      default: '48%'
    }
  },
  methods: {
    isNotInList1(v) {
      return this.list1.every(k => v.id !== k.id)
    },
    isNotInList2(v) {
      return this.list2.every(k => v.id !== k.id)
    },
    deleteEle(ele) {
      for (const item of this.list1) {
        if (item.id === ele.id) {
          const index = this.list1.indexOf(item)
          this.list1.splice(index, 1)
          break
        }
      }
      if (this.isNotInList2(ele)) {
        this.list2.unshift(ele)
      }
    },
    pushEle(ele) {
      for (const item of this.list2) {
        if (item.id === ele.id) {
          const index = this.list2.indexOf(item)
          this.list2.splice(index, 1)
          break
        }
      }
      if (this.isNotInList1(ele)) {
        this.list1.push(ele)
      }
    },
    setData(dataTransfer) {
      // to avoid Firefox bug
      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
      dataTransfer.setData('Text', '')
    }
  }
}
</script>

<style lang="scss" scoped>
.dndList {
  background: #fff;
  padding-bottom: 40px;
  &:after {
    content: "";
    display: table;
    clear: both;
  }
  .dndList-list {
    float: left;
    padding-bottom: 30px;
    &:first-of-type {
      margin-right: 2%;
    }
    .dragArea {
      margin-top: 15px;
      min-height: 50px;
      padding-bottom: 30px;
    }
  }
}

.list-complete-item {
  cursor: pointer;
  position: relative;
  font-size: 14px;
  padding: 5px 12px;
  margin-top: 4px;
  border: 1px solid #bfcbd9;
  transition: all 1s;
}

.list-complete-item-handle {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-right: 50px;
}

.list-complete-item-handle2 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-right: 20px;
}

.list-complete-item.sortable-chosen {
  background: #4AB7BD;
}

.list-complete-item.sortable-ghost {
  background: #30B08F;
}

.list-complete-enter,
.list-complete-leave-active {
  opacity: 0;
}
</style>

具体使用:

<template>
  <dnd-list
    :list1="list1"
    :list2="list2"
    list1-title="选中数据"
    list2-title="全部数据"
  />
</template>

<script>
import DndList from '@/components/DndList'
import { fetchList } from '@/api/article'

export default {
  name: 'DndListDemo',
  components: { DndList },
  data() {
    return {
      list1: [],
      list2: []
    }
  },
  created() {
    this.getData()
  },
  methods: {
    getData() {
      this.listLoading = true
      fetchList().then(response => {
        this.list1 = response.data.items.splice(0, 5)
        this.list2 = response.data.items
      })
    }
  }
}
</script>

9、DragSelect 拖放排序下拉框

安装 sortablejs 依赖:

npm i sortablejs

封装 DragSelect 组件:

<template>
  <el-select
    ref="dragSelect"
    v-model="selectVal"
    v-bind="$attrs"
    class="drag-select"
    multiple
    v-on="$listeners"
  >
    <slot />
  </el-select>
</template>

<script>
import Sortable from 'sortablejs'

export default {
  name: 'DragSelect',
  props: {
    value: {
      type: Array,
      required: true
    }
  },
  computed: {
    selectVal: {
      get() {
        return [...this.value]
      },
      set(val) {
        this.$emit('input', [...val])
      }
    }
  },
  mounted() {
    this.setSort()
  },
  methods: {
    setSort() {
      const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
      this.sortable = Sortable.create(el, {
        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
        setData: function(dataTransfer) {
          dataTransfer.setData('Text', '')
          // to avoid Firefox bug
          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
        },
        onEnd: evt => {
          const targetRow = this.value.splice(evt.oldIndex, 1)[0]
          this.value.splice(evt.newIndex, 0, targetRow)
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.drag-select {
  ::v-deep {
    .sortable-ghost {
      opacity: .8;
      color: #fff !important;
      background: #42b983 !important;
    }

    .el-tag {
      cursor: pointer;
    }
  }
}
</style>

具体使用:

<template>
  <div class="components-container">
    <el-drag-select v-model="value" style="width:500px;" multiple placeholder="请选择">
      <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
    </el-drag-select>
  </div>
</template>

<script>
import ElDragSelect from '@/components/DragSelect'

export default {
  components: { ElDragSelect },
  data() {
    return {
      value: ['1', '2'], // 默认选中
      options: [ // 全部数据
        { value: '1', label: 'Apple' },
        { value: '2', label: 'Banana' },
        { value: '3', label: 'Orange' },
        { value: '4', label: 'Pear' },
        { value: '5', label: 'Strawberry' }
      ]
    }
  }
}
</script>

10、Dropzone 文件上传

安装 sortablejs 依赖:

npm i dropzone

封装 Dropzone 组件:

<template>
  <div :id="id" :ref="id" :action="url" class="dropzone">
    <input type="file" name="file">
  </div>
</template>

<script>
import Dropzone from 'dropzone'
import 'dropzone/dist/dropzone.css'
// import { getToken } from 'api/qiniu';

Dropzone.autoDiscover = false

export default {
  props: {
    id: {
      type: String,
      required: true
    },
    url: {
      type: String,
      required: true
    },
    clickable: {
      type: Boolean,
      default: true
    },
    defaultMsg: {
      type: String,
      default: '上传图片'
    },
    acceptedFiles: {
      type: String,
      default: ''
    },
    thumbnailHeight: {
      type: Number,
      default: 200
    },
    thumbnailWidth: {
      type: Number,
      default: 200
    },
    showRemoveLink: {
      type: Boolean,
      default: true
    },
    maxFilesize: {
      type: Number,
      default: 2
    },
    maxFiles: {
      type: Number,
      default: 3
    },
    autoProcessQueue: {
      type: Boolean,
      default: true
    },
    useCustomDropzoneOptions: {
      type: Boolean,
      default: false
    },
    defaultImg: {
      default: '',
      type: [String, Array]
    },
    couldPaste: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      dropzone: '',
      initOnce: true
    }
  },
  watch: {
    defaultImg(val) {
      if (val.length === 0) {
        this.initOnce = false
        return
      }
      if (!this.initOnce) return
      this.initImages(val)
      this.initOnce = false
    }
  },
  mounted() {
    const element = document.getElementById(this.id)
    const vm = this
    this.dropzone = new Dropzone(element, {
      clickable: this.clickable,
      thumbnailWidth: this.thumbnailWidth,
      thumbnailHeight: this.thumbnailHeight,
      maxFiles: this.maxFiles,
      maxFilesize: this.maxFilesize,
      dictRemoveFile: 'Remove',
      addRemoveLinks: this.showRemoveLink,
      acceptedFiles: this.acceptedFiles,
      autoProcessQueue: this.autoProcessQueue,
      dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
      dictMaxFilesExceeded: '只能一个图',
      previewTemplate: '<div class="dz-preview dz-file-preview">  <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div>  <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>  <div class="dz-error-message"><span data-dz-errormessage></span></div>  <div class="dz-success-mark"> <i class="material-icons">done</i> </div>  <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
      init() {
        const val = vm.defaultImg
        if (!val) return
        if (Array.isArray(val)) {
          if (val.length === 0) return
          val.map((v, i) => {
            const mockFile = { name: 'name' + i, size: 12345, url: v }
            this.options.addedfile.call(this, mockFile)
            this.options.thumbnail.call(this, mockFile, v)
            mockFile.previewElement.classList.add('dz-success')
            mockFile.previewElement.classList.add('dz-complete')
            vm.initOnce = false
            return true
          })
        } else {
          const mockFile = { name: 'name', size: 12345, url: val }
          this.options.addedfile.call(this, mockFile)
          this.options.thumbnail.call(this, mockFile, val)
          mockFile.previewElement.classList.add('dz-success')
          mockFile.previewElement.classList.add('dz-complete')
          vm.initOnce = false
        }
      },
      accept: (file, done) => {
        /* 七牛*/
        // const token = this.$store.getters.token;
        // getToken(token).then(response => {
        //   file.token = response.data.qiniu_token;
        //   file.key = response.data.qiniu_key;
        //   file.url = response.data.qiniu_url;
        //   done();
        // })
        done()
      },
      sending: (file, xhr, formData) => {
        // formData.append('token', file.token);
        // formData.append('key', file.key);
        vm.initOnce = false
      }
    })

    if (this.couldPaste) {
      document.addEventListener('paste', this.pasteImg)
    }

    this.dropzone.on('success', file => {
      vm.$emit('dropzone-success', file, vm.dropzone.element)
    })
    this.dropzone.on('addedfile', file => {
      vm.$emit('dropzone-fileAdded', file)
    })
    this.dropzone.on('removedfile', file => {
      vm.$emit('dropzone-removedFile', file)
    })
    this.dropzone.on('error', (file, error, xhr) => {
      vm.$emit('dropzone-error', file, error, xhr)
    })
    this.dropzone.on('successmultiple', (file, error, xhr) => {
      vm.$emit('dropzone-successmultiple', file, error, xhr)
    })
  },
  destroyed() {
    document.removeEventListener('paste', this.pasteImg)
    this.dropzone.destroy()
  },
  methods: {
    removeAllFiles() {
      this.dropzone.removeAllFiles(true)
    },
    processQueue() {
      this.dropzone.processQueue()
    },
    pasteImg(event) {
      const items = (event.clipboardData || event.originalEvent.clipboardData).items
      if (items[0].kind === 'file') {
        this.dropzone.addFile(items[0].getAsFile())
      }
    },
    initImages(val) {
      if (!val) return
      if (Array.isArray(val)) {
        val.map((v, i) => {
          const mockFile = { name: 'name' + i, size: 12345, url: v }
          this.dropzone.options.addedfile.call(this.dropzone, mockFile)
          this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v)
          mockFile.previewElement.classList.add('dz-success')
          mockFile.previewElement.classList.add('dz-complete')
          return true
        })
      } else {
        const mockFile = { name: 'name', size: 12345, url: val }
        this.dropzone.options.addedfile.call(this.dropzone, mockFile)
        this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val)
        mockFile.previewElement.classList.add('dz-success')
        mockFile.previewElement.classList.add('dz-complete')
      }
    }

  }
}
</script>

<style scoped>
    .dropzone {
        border: 2px solid #E5E5E5;
        font-family: 'Roboto', sans-serif;
        color: #777;
        transition: background-color .2s linear;
        padding: 5px;
    }

    .dropzone:hover {
        background-color: #F6F6F6;
    }

    i {
        color: #CCC;
    }

    .dropzone .dz-image img {
        width: 100%;
        height: 100%;
    }

    .dropzone input[name='file'] {
        display: none;
    }

    .dropzone .dz-preview .dz-image {
        border-radius: 0px;
    }

    .dropzone .dz-preview:hover .dz-image img {
        transform: none;
        filter: none;
        width: 100%;
        height: 100%;
    }

    .dropzone .dz-preview .dz-details {
        bottom: 0px;
        top: 0px;
        color: white;
        background-color: rgba(33, 150, 243, 0.8);
        transition: opacity .2s linear;
        text-align: left;
    }

    .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
        background-color: transparent;
    }

    .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
        border: none;
    }

    .dropzone .dz-preview .dz-details .dz-filename:hover span {
        background-color: transparent;
        border: none;
    }

    .dropzone .dz-preview .dz-remove {
        position: absolute;
        z-index: 30;
        color: white;
        margin-left: 15px;
        padding: 10px;
        top: inherit;
        bottom: 15px;
        border: 2px white solid;
        text-decoration: none;
        text-transform: uppercase;
        font-size: 0.8rem;
        font-weight: 800;
        letter-spacing: 1.1px;
        opacity: 0;
    }

    .dropzone .dz-preview:hover .dz-remove {
        opacity: 1;
    }

    .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
        margin-left: -40px;
        margin-top: -50px;
    }

    .dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
        color: white;
        font-size: 5rem;
    }
</style>

具体使用:

<template>
  <dropzone
    id="myVueDropzone"
    url="https://httpbin.org/post"
    @dropzone-removedFile="dropzoneR"
    @dropzone-success="dropzoneS"
  />
</template>

<script>
import Dropzone from '@/components/Dropzone'

export default {
  components: { Dropzone },
  methods: {
    dropzoneS(file) {
      console.log(file)
      this.$message({ message: '上传成功', type: 'success' })
    },
    dropzoneR(file) {
      console.log(file)
      this.$message({ message: '删除成功', type: 'success' })
    }
  }
}
</script>

11、Screenfull 全屏

安装 screenfull 依赖:

npm i screenfull@4.2.0

封装 Screenfull 全局组件:

<template>
  <div>
    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
  </div>
</template>

<script>
import screenfull from 'screenfull'

export default {
  name: 'Screenfull',
  data() {
    return {
      isFullscreen: false
    }
  },
  mounted() {
    this.init()
  },
  beforeDestroy() {
    this.destroy()
  },
  methods: {
    click() {
      if (!screenfull.enabled) {
        this.$message({
          message: 'you browser can not work',
          type: 'warning'
        })
        return false
      }
      screenfull.toggle()
    },
    change() {
      this.isFullscreen = screenfull.isFullscreen
    },
    init() {
      if (screenfull.enabled) {
        screenfull.on('change', this.change)
      }
    },
    destroy() {
      if (screenfull.enabled) {
        screenfull.off('change', this.change)
      }
    }
  }
}
</script>

<style scoped>
.screenfull-svg {
  display: inline-block;
  cursor: pointer;
  fill: #5a5e66;;
  width: 20px;
  height: 20px;
  vertical-align: 10px;
}
</style>

src/layout/components/Navbar.vue 中 使用:

<template>
  <div class="navbar">
    ....
    <div class="right-menu">
      <!-- 在此位置添加 全屏按钮 -->
      <screenfull id="screenfull" class="right-menu-item hover-effect" />

      <el-dropdown class="avatar-container" trigger="click">
        ....
      </el-dropdown>
    </div>
  </div>
</template>

<script>
import Screenfull from '@/components/Screenfull'

export default {
  components: {
    Screenfull
  },
  ....
}
</script>

12、Sticky 粘性布局

src/components/Sticky/index.vue

<template>
  <div :style="{height:height+'px',zIndex:zIndex}">
    <div
      :class="className"
      :style="{top:(isSticky ? stickyTop +'px' : ''),zIndex:zIndex,position:position,width:width,height:height+'px'}"
    >
      <slot>
        <div>sticky</div>
      </slot>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Sticky',
  props: {
    stickyTop: {
      type: Number,
      default: 0
    },
    zIndex: {
      type: Number,
      default: 1
    },
    className: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      active: false,
      position: '',
      width: undefined,
      height: undefined,
      isSticky: false
    }
  },
  mounted() {
    this.height = this.$el.getBoundingClientRect().height
    window.addEventListener('scroll', this.handleScroll)
    window.addEventListener('resize', this.handleResize)
  },
  activated() {
    this.handleScroll()
  },
  destroyed() {
    window.removeEventListener('scroll', this.handleScroll)
    window.removeEventListener('resize', this.handleResize)
  },
  methods: {
    sticky() {
      if (this.active) {
        return
      }
      this.position = 'fixed'
      this.active = true
      this.width = this.width + 'px'
      this.isSticky = true
    },
    handleReset() {
      if (!this.active) {
        return
      }
      this.reset()
    },
    reset() {
      this.position = ''
      this.width = 'auto'
      this.active = false
      this.isSticky = false
    },
    handleScroll() {
      const width = this.$el.getBoundingClientRect().width
      this.width = width || 'auto'
      const offsetTop = this.$el.getBoundingClientRect().top
      if (offsetTop < this.stickyTop) {
        this.sticky()
        return
      }
      this.handleReset()
    },
    handleResize() {
      if (this.isSticky) {
        this.width = this.$el.getBoundingClientRect().width + 'px'
      }
    }
  }
}
</script>

具体使用:

<template>
  <div class="app-container">
    <sticky :z-index="10" class-name="sub-navbar">
      <div class="test">123</div>
    </sticky>
  </div>
</template>

<script>
import Sticky from '@/components/Sticky'

export default {
  components: { Sticky },
}
</script>

<style lang="scss" scoped>
.test{
  height: 100px;
  background-color: pink;
}
</style>

13、ThemePicker 主题切换

封装 ThemePicker 组件:

<template>
  <el-color-picker
    v-model="theme"
    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
    class="theme-picker"
    popper-class="theme-picker-dropdown"
  />
</template>

<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color

export default {
  data() {
    return {
      chalk: '', // content of theme-chalk css
      theme: ''
    }
  },
  computed: {
    defaultTheme() {
      return this.$store.state.settings.theme
    }
  },
  watch: {
    defaultTheme: {
      handler: function(val, oldVal) {
        this.theme = val
      },
      immediate: true
    },
    async theme(val) {
      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
      if (typeof val !== 'string') return
      const themeCluster = this.getThemeCluster(val.replace('#', ''))
      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
      console.log(themeCluster, originalCluster)

      const $message = this.$message({
        message: '  Compiling the theme',
        customClass: 'theme-message',
        type: 'success',
        duration: 0,
        iconClass: 'el-icon-loading'
      })

      const getHandler = (variable, id) => {
        return () => {
          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)

          let styleTag = document.getElementById(id)
          if (!styleTag) {
            styleTag = document.createElement('style')
            styleTag.setAttribute('id', id)
            document.head.appendChild(styleTag)
          }
          styleTag.innerText = newStyle
        }
      }

      if (!this.chalk) {
        const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
        await this.getCSSString(url, 'chalk')
      }

      const chalkHandler = getHandler('chalk', 'chalk-style')

      chalkHandler()

      const styles = [].slice.call(document.querySelectorAll('style'))
        .filter(style => {
          const text = style.innerText
          return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
        })
      styles.forEach(style => {
        const { innerText } = style
        if (typeof innerText !== 'string') return
        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
      })

      this.$emit('change', val)

      $message.close()
    }
  },

  methods: {
    updateStyle(style, oldCluster, newCluster) {
      let newStyle = style
      oldCluster.forEach((color, index) => {
        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
      })
      return newStyle
    },

    getCSSString(url, variable) {
      return new Promise(resolve => {
        const xhr = new XMLHttpRequest()
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4 && xhr.status === 200) {
            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
            resolve()
          }
        }
        xhr.open('GET', url)
        xhr.send()
      })
    },

    getThemeCluster(theme) {
      const tintColor = (color, tint) => {
        let red = parseInt(color.slice(0, 2), 16)
        let green = parseInt(color.slice(2, 4), 16)
        let blue = parseInt(color.slice(4, 6), 16)

        if (tint === 0) { // when primary color is in its rgb space
          return [red, green, blue].join(',')
        } else {
          red += Math.round(tint * (255 - red))
          green += Math.round(tint * (255 - green))
          blue += Math.round(tint * (255 - blue))

          red = red.toString(16)
          green = green.toString(16)
          blue = blue.toString(16)

          return `#${red}${green}${blue}`
        }
      }

      const shadeColor = (color, shade) => {
        let red = parseInt(color.slice(0, 2), 16)
        let green = parseInt(color.slice(2, 4), 16)
        let blue = parseInt(color.slice(4, 6), 16)

        red = Math.round((1 - shade) * red)
        green = Math.round((1 - shade) * green)
        blue = Math.round((1 - shade) * blue)

        red = red.toString(16)
        green = green.toString(16)
        blue = blue.toString(16)

        return `#${red}${green}${blue}`
      }

      const clusters = [theme]
      for (let i = 0; i <= 9; i++) {
        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
      }
      clusters.push(shadeColor(theme, 0.1))
      return clusters
    }
  }
}
</script>

<style>
.theme-message,
.theme-picker-dropdown {
  z-index: 99999 !important;
}

.theme-picker .el-color-picker__trigger {
  height: 26px !important;
  width: 26px !important;
  padding: 2px;
}

.theme-picker-dropdown .el-color-dropdown__link-btn {
  display: none;
}
</style>

新建 src/styles/element-variables.scss

/**
* I think element-ui's default theme color is too light for long-term use.
* So I modified the default color and you can modify it to your liking.
**/

/* theme color */
$--color-primary: #1890ff;
$--color-success: #13ce66;
$--color-warning: #ffba00;
$--color-danger: #ff4949;
// $--color-info: #1E1E1E;

$--button-font-weight: 400;

// $--color-text-regular: #1f2d3d;

$--border-color-light: #dfe4ed;
$--border-color-lighter: #e6ebf5;

$--table-border: 1px solid #dfe6ec;

/* icon font path, required */
$--font-path: "~element-ui/lib/theme-chalk/fonts";

@import "~element-ui/packages/theme-chalk/src/index";

// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
  theme: $--color-primary;
}

修改 src/store/modules/settings.js

import variables from '@/styles/element-variables.scss'
....

const state = {
  theme: variables.theme,
  ....
}

最后在 src/layout/components/Navbar.vue 中引入使用:

<theme-picker style="height: 26px;margin: -12px 8px 0 0;" @change="themeChange" />

<script>
import ThemePicker from '@/components/ThemePicker'

export default {
  components: {
    ....
    ThemePicker
  },
  methods: {
    ....
    themeChange(val) {
      this.$store.dispatch('settings/changeSetting', {
        key: 'theme',
        value: val
      })
    }
  }
}
</script>

14、countTo 数字滚动效果

安装 vue-count-to 依赖:

npm i vue-count-to

具体使用:

<template>
  <div class="app-container">
    <count-to
      ref="countTo"
      :start-val="startVal"
      :end-val="endVal"
      :duration="duration"
      :decimals="decimals"
      :separator="separator"
      :prefix="prefix"
      :suffix="suffix"
      :autoplay="true"
    />

    <br>
    <br>

    <el-button @click="startCountTo">开始计时效果</el-button>
    <el-button @click="pauseCountTo">暂停计时效果</el-button>
    <el-button @click="resetCountTo">重置计时效果</el-button>
  </div>
</template>

<script>
import countTo from 'vue-count-to'

export default {
  components: { countTo },
  data() {
    return {
      startVal: 0, // 开始计算的值
      endVal: 2023, // 达到的值
      duration: 4000, // 持续时间
      decimals: 2, // 支持小数位数
      separator: ',', // 分隔符
      prefix: '¥ ', // 前缀
      suffix: ' 元' // 后缀
    }
  },
  methods: {
    startCountTo() {
      this.$refs.countTo.start()
    },
    pauseCountTo() {
      this.$refs.countTo.pause()
    },
    resetCountTo() {
      this.$refs.countTo.reset()
    }
  }
}
</script>

15、PDF 打印(PC端)

<template>
  <div
    v-loading.fullscreen.lock="fullscreenLoading"
    class="main-article"
    element-loading-text="正在努力生成PDF"
  >
    ....
  </div>
</template>

<script>
export default {
  data() {
    return {
      fullscreenLoading: true
    }
  },
  mounted() {
    this.fetchData()
  },
  methods: {
    fetchData() {
      get('./content.js').then(data => {
        ...
        document.title = title // 设置PDF的标题
        
        setTimeout(() => {
          this.fullscreenLoading = false
          this.$nextTick(() => {
            window.print()
          })
        }, 3000)
      })
    }
  }
}
</script>

三、自定义指令

1、权限指令 permission

src
 └── directive
        ├── permission
                ├── permission.js
                ├── index.js

permission/permission.js

import store from '@/store'

function checkPermission(el, binding) {
  const { value } = binding
  const roles = store.getters && store.getters.roles

  if (value && value instanceof Array) {
    if (value.length > 0) {
      const permissionRoles = value

      const hasPermission = roles.some(role => {
        return permissionRoles.includes(role)
      })

      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    }
  } else {
    throw new Error(`need roles! Like v-permission="['admin','editor']"`)
  }
}

export default {
  inserted(el, binding) {
    checkPermission(el, binding)
  },
  update(el, binding) {
    checkPermission(el, binding)
  }
}

permission/index.js

import permission from './permission'

const install = function(Vue) {
  Vue.directive('permission', permission)
}

if (window.Vue) {
  window['permission'] = permission
  Vue.use(install); // eslint-disable-line
}

permission.install = install
export default permission

main.js 注册全局指令:

import permission from '@/directive/permission/index.js' // 权限判断指令

Vue.directive('permission', permission) 

在 组件 内使用:

<div v-permission="['admin','editor']">有权限的人才看得到</div>

❗注意:必须先保证 store 存储的权限角色字段 为 roles 数组,并且在 src/store/getters.js 设置了快捷访问变量。

const getDefaultState = () => {
  return {
    ....
    roles: []
  }
}

3、拖拽弹框 drag dialog

目录如下:

src
 └── directive
        ├── el-drag-dialog
                ├── drag.js
                ├── index.js

el-drag-dialog/drag.js

export default {
  bind(el, binding, vnode) {
    const dialogHeaderEl = el.querySelector('.el-dialog__header')
    const dragDom = el.querySelector('.el-dialog')
    dialogHeaderEl.style.cssText += ';cursor:move;'
    dragDom.style.cssText += ';top:0px;'

    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
    const getStyle = (function() {
      if (window.document.currentStyle) {
        return (dom, attr) => dom.currentStyle[attr]
      } else {
        return (dom, attr) => getComputedStyle(dom, false)[attr]
      }
    })()

    dialogHeaderEl.onmousedown = (e) => {
      // 鼠标按下,计算当前元素距离可视区的距离
      const disX = e.clientX - dialogHeaderEl.offsetLeft
      const disY = e.clientY - dialogHeaderEl.offsetTop

      const dragDomWidth = dragDom.offsetWidth
      const dragDomHeight = dragDom.offsetHeight

      const screenWidth = document.body.clientWidth
      const screenHeight = document.body.clientHeight

      const minDragDomLeft = dragDom.offsetLeft
      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth

      const minDragDomTop = dragDom.offsetTop
      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight

      // 获取到的值带px 正则匹配替换
      let styL = getStyle(dragDom, 'left')
      let styT = getStyle(dragDom, 'top')

      if (styL.includes('%')) {
        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
      } else {
        styL = +styL.replace(/\px/g, '')
        styT = +styT.replace(/\px/g, '')
      }

      document.onmousemove = function(e) {
        // 通过事件委托,计算移动的距离
        let left = e.clientX - disX
        let top = e.clientY - disY

        // 边界处理
        if (-(left) > minDragDomLeft) {
          left = -minDragDomLeft
        } else if (left > maxDragDomLeft) {
          left = maxDragDomLeft
        }

        if (-(top) > minDragDomTop) {
          top = -minDragDomTop
        } else if (top > maxDragDomTop) {
          top = maxDragDomTop
        }

        // 移动当前元素
        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`

        // emit onDrag event
        vnode.child.$emit('dragDialog')
      }

      document.onmouseup = function(e) {
        document.onmousemove = null
        document.onmouseup = null
      }
    }
  }
}

el-drag-dialog/index.js

import drag from './drag'

const install = function(Vue) {
  Vue.directive('el-drag-dialog', drag)
}

if (window.Vue) {
  window['el-drag-dialog'] = drag
  Vue.use(install); // eslint-disable-line
}

drag.install = install
export default drag

在 组件 内使用:

<template>
  <div class="components-container">
    <el-button type="primary" @click="show = true">打开弹框</el-button>

    <el-dialog v-el-drag-dialog :visible.sync="show" title="Shipping address" @dragDialog="handleDrag">
      <el-select ref="select" v-model="value" placeholder="请选择">
        <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
      </el-select>
      <el-table :data="gridData">
        <el-table-column property="date" label="Date" width="150" />
        <el-table-column property="name" label="Name" width="200" />
        <el-table-column property="address" label="Address" />
      </el-table>
    </el-dialog>
  </div>
</template>

<script>
import elDragDialog from '@/directive/el-drag-dialog'

export default {
  name: 'DragDialogDemo',
  directives: { elDragDialog },
  data() {
    return {
      show: false,
      options: [
        { value: '选项1', label: '黄金糕' }
      ],
      value: '',
      gridData: [{
        date: '2023-12-12',
        name: 'csheng',
        address: '福建省厦门市集美区'
      }]
    }
  },
  methods: {
    handleDrag() {
      this.$refs.select.blur()
    }
  }
}
</script>

4、水波纹涟漪效果 waves

常用于在 点击按钮 上面。

目录如下:

src
 └── directive
        ├── waves
                ├── waves.js
                ├── waves.css
                ├── index.js

waves/waves.js

import './waves.css'

const context = '@@wavesContext'

function handleClick(el, binding) {
  function handle(e) {
    const customOpts = Object.assign({}, binding.value)
    const opts = Object.assign({
      ele: el, // 波纹作用元素
      type: 'hit', // hit 点击位置扩散 center中心点扩展
      color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
    },
    customOpts
    )
    const target = opts.ele
    if (target) {
      target.style.position = 'relative'
      target.style.overflow = 'hidden'
      const rect = target.getBoundingClientRect()
      let ripple = target.querySelector('.waves-ripple')
      if (!ripple) {
        ripple = document.createElement('span')
        ripple.className = 'waves-ripple'
        ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
        target.appendChild(ripple)
      } else {
        ripple.className = 'waves-ripple'
      }
      switch (opts.type) {
        case 'center':
          ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
          ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
          break
        default:
          ripple.style.top =
            (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
              document.body.scrollTop) + 'px'
          ripple.style.left =
            (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
              document.body.scrollLeft) + 'px'
      }
      ripple.style.backgroundColor = opts.color
      ripple.className = 'waves-ripple z-active'
      return false
    }
  }

  if (!el[context]) {
    el[context] = {
      removeHandle: handle
    }
  } else {
    el[context].removeHandle = handle
  }

  return handle
}

export default {
  bind(el, binding) {
    el.addEventListener('click', handleClick(el, binding), false)
  },
  update(el, binding) {
    el.removeEventListener('click', el[context].removeHandle, false)
    el.addEventListener('click', handleClick(el, binding), false)
  },
  unbind(el) {
    el.removeEventListener('click', el[context].removeHandle, false)
    el[context] = null
    delete el[context]
  }
}

waves/waves.css

.waves-ripple {
    position: absolute;
    border-radius: 100%;
    background-color: rgba(0, 0, 0, 0.15);
    background-clip: padding-box;
    pointer-events: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-transform: scale(0);
    -ms-transform: scale(0);
    transform: scale(0);
    opacity: 1;
}

.waves-ripple.z-active {
    opacity: 0;
    -webkit-transform: scale(2);
    -ms-transform: scale(2);
    transform: scale(2);
    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
    transition: opacity 1.2s ease-out, transform 0.6s ease-out;
    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
}

waves/index.js

import waves from './waves'

const install = function(Vue) {
  Vue.directive('waves', waves)
}

if (window.Vue) {
  window.waves = waves
  Vue.use(install); // eslint-disable-line
}

waves.install = install
export default waves

在 组件 内使用:

<template>
  <el-button v-waves type="primary" icon="el-icon-search" @click="handleFilter">搜索</el-button>
</template>

<script>
import waves from '@/directive/waves'

export default {
  directives: { waves }
}
</script>

5、图片加载失败

Vue.directive('imagerror', {
  inserted(dom, { value = '图片地址(默认图片)' }) {
    dom.onerror = () => {
      dom.src = value
    }
  }
})

6、按钮权限

全局注册

Vue.directive('permission', {
  inserted(dom, { value }) {
    const { points } = store.state.user.userInfo?.roles || []
    if (!points.includes(value)) {
      dom.remove()
    }
  }
})

具体使用

<el-button v-permission="import-employees">导入员工</el-button>

7、按钮防抖

全局注册

Vue.directive('debounce', {
  inserted(el, binding) {
    el.addEventListener('click', e => {
      el.classList.add('is-disabled')
      el.disabled = true
      setTimeout(() => {
        el.disabled = false
        el.classList.remove('is-disabled')
      }, 2000)
    })
  }
})

具体使用

<el-button v-debounce @click="doSomething">点我</el-button>

四、方法

1、10000 => 10,000

/**
 * 数字格式化
 * 比如 10000 => "10,000"
 * @param {number} num
 */
export function toThousandFilter(num) {
  return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
}

2、10000 => 10k

/**
 * 数字格式化
 * 比如 10000 => 10k
 * @param {number} num
 * @param {number} digits
 */
export function numberFormatter(num, digits) {
  const si = [
    { value: 1E18, symbol: 'E' },
    { value: 1E15, symbol: 'P' },
    { value: 1E12, symbol: 'T' },
    { value: 1E9, symbol: 'G' },
    { value: 1E6, symbol: 'M' },
    { value: 1E3, symbol: 'k' }
  ]
  for (let i = 0; i < si.length; i++) {
    if (num >= si[i].value) {
      return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol
    }
  }
  return num.toString()
}

3、首字符转为大写

/**
 * 首字符转为大写
 * @param {String} string
 */
export function uppercaseFirst(string) {
  return string.charAt(0).toUpperCase() + string.slice(1)
}

4、获取时间戳

/**
 * 获取时间戳
 * @param {string} type
 * @returns {Date}
 */
export function getTime(type) {
  if (type === 'start') {
    return new Date().getTime() - 3600 * 1000 * 24 * 90
  } else {
    return new Date(new Date().toDateString())
  }
}

使用例子:

// 示例1: 获取当前时间前推90天的日期和时间
const startDate = getTime('start')
console.log(startDate) // 1686294102251,90天前的日期和时间

// 示例2: 获取当前日期的Date对象,时间部分为午夜
const currentDate = getTime()
console.log(currentDate)  // Thu Sep 07 2023 00:00:00 GMT+0800 (中国标准时间)
// 当前日期的Date对象,时间部分为 00:00:00.000

5、将时间戳转换成相对时间描述

/**
 * 将时间戳转换成相对时间描述
 * @param {number} time 时间戳
 */
export function timeAgo(time) {
  const between = Date.now() / 1000 - Number(time)
  if (between < 3600) {
    return ~~(between / 60) + ' 分钟'
  } else if (between < 86400) {
    return ~~(between / 3600) + ' 小时'
  } else {
    return ~~(between / 86400) + ' 天'
  }
}

使用例子:

import { timeAgo } from '@/utils/index.js'

export default {
  created() {
    const timestamp = 1631000000 // 假设时间戳为2021年9月8日的某个时间点
    console.log(timeAgo(timestamp))// 729 days
  },
}

6、将日期对象转换为字符串

/**
 * 将日期对象转换为字符串格式
 * @param {(Object|string|number)} time
 * @param {string} cFormat
 * @returns {string | null}
 */
export function parseTime(time, cFormat) {
  if (arguments.length === 0 || !time) {
    return null
  }
  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
  let date
  if (typeof time === 'object') {
    date = time
  } else {
    if ((typeof time === 'string')) {
      if ((/^[0-9]+$/.test(time))) {
        // support "1548221490638"
        time = parseInt(time)
      } else {
        // support safari
        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
        time = time.replace(new RegExp(/-/gm), '/')
      }
    }

    if ((typeof time === 'number') && (time.toString().length === 10)) {
      time = time * 1000
    }
    date = new Date(time)
  }
  const formatObj = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  }
  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
    const value = formatObj[key]
    // Note: getDay() returns 0 on Sunday
    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
    return value.toString().padStart(2, '0')
  })
  return time_str
}

使用例子:

const result1 = parseTime(1631000000) // 2021-09-07 15:33:20
const result2 = parseTime(1631000000, '{y}/{m}/{d} {h}:{i}') // 2021/09/07 15:33

7、将时间戳转换成相对时间描述或字符串

/**
 * 将时间戳转换成相对时间描述或字符串
 * @param {number} time 时间戳
 * @param {string} option(可选)
 * @returns {string}
 */
export function formatTime(time, option) {
  if (('' + time).length === 10) {
    time = parseInt(time) * 1000
  } else {
    time = +time
  }
  const d = new Date(time)
  const now = Date.now()

  const diff = (now - d) / 1000

  if (diff < 30) {
    return '刚刚'
  } else if (diff < 3600) {
    // less 1 hour
    return Math.ceil(diff / 60) + '分钟前'
  } else if (diff < 3600 * 24) {
    return Math.ceil(diff / 3600) + '小时前'
  } else if (diff < 3600 * 24 * 2) {
    return '1天前'
  }
  if (option) {
    return parseTime(time, option)
  } else {
    return (
      d.getMonth() +
      1 +
      '月' +
      d.getDate() +
      '日' +
      d.getHours() +
      '时' +
      d.getMinutes() +
      '分'
    )
  }
}

使用例子:

const result1 = formatTime(1694051380508) // 刚刚
const result2 = formatTime(1631000000) // 9月7日15时33分
const result3 = formatTime(1631000000, '{y}-{m}-{d} {h}:{i}:{s}') // 2021-09-07 15:33:20

8、将查询参数的URL解析为JS对象

方法 ① :使用正则表达式来匹配和解析查询参数

/**
 * 将查询参数的URL解析为JS对象
 * @param {string} url
 * @returns {Object}
 */
export function getQueryObject(url) {
  url = url == null ? window.location.href : url
  const search = url.substring(url.lastIndexOf('?') + 1)
  const obj = {}
  const reg = /([^?&=]+)=([^?&=]*)/g
  search.replace(reg, (rs, $1, $2) => {
    const name = decodeURIComponent($1)
    let val = decodeURIComponent($2)
    val = String(val)
    obj[name] = val
    return rs
  })
  return obj
}

方法 ② :使用字符串分割和遍历的方法来解析

/**
 * 将查询参数的URL解析为JS对象
 * @param {string} url
 * @returns {Object}
 */
export function param2Obj(url) {
  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
  if (!search) {
    return {}
  }
  const obj = {}
  const searchArr = search.split('&')
  searchArr.forEach(v => {
    const index = v.indexOf('=')
    if (index !== -1) {
      const name = v.substring(0, index)
      const val = v.substring(index + 1, v.length)
      obj[name] = val
    }
  })
  return obj
}

2个方法的使用例子:

const url = 'https://example.com/page?name=John&age=30'
const result1 = getQueryObject(url) // {"name": "John", "age": "30"}
const result2 = param2Obj(url) // {"name": "John", "age": "30"}

9、深拷贝

/**
 * 深拷贝
 * If you want to use a perfect deep copy, use lodash's _.cloneDeep
 * @param {Object} source
 * @returns {Object}
 */
export function deepClone(source) {
  if (!source && typeof source !== 'object') {
    throw new Error('error arguments', 'deepClone')
  }
  const targetObj = source.constructor === Array ? [] : {}
  Object.keys(source).forEach(keys => {
    if (source[keys] && typeof source[keys] === 'object') {
      targetObj[keys] = deepClone(source[keys])
    } else {
      targetObj[keys] = source[keys]
    }
  })
  return targetObj
}

使用例子:

// 示例1:深拷贝一个对象
const obj = {
  name: 'John',
  age: 30,
  address: { city: 'New York', zip: '10001' }
}
const clonedObj = deepClone(obj)
console.log(clonedObj)
console.log(clonedObj === obj) // false

// 示例2:深拷贝一个数组
const ary = [1, 2, [3, 4]]
const clonedArray = deepClone(ary)
console.log(clonedArray) // 输出深拷贝后的数组
console.log(clonedArray === ary) // 输出 false,说明它们是不同的数组

10、计算UTF-8字符串的字节长度

/**
 * 计算UTF-8字符串的字节长度
 * @param {string} input value
 * @returns {number} output value
 */
export function byteLength(str) {
  let s = str.length
  for (var i = str.length - 1; i >= 0; i--) {
    const code = str.charCodeAt(i)
    if (code > 0x7f && code <= 0x7ff) s++
    else if (code > 0x7ff && code <= 0xffff) s += 2
    if (code >= 0xDC00 && code <= 0xDFFF) i--
  }
  return s
}

使用例子:

const myString = 'csheng'
const lengthInBytes = byteLength(myString) // 6

11、检查/添加/删除/切换 类

检查/添加/删除 类:

/**
 * 检查一个元素是否有一个类
 * @param {HTMLElement} elm
 * @param {string} cls
 * @returns {boolean}
 */
export function hasClass(ele, cls) {
  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
}

/**
 * 向元素添加类
 * @param {HTMLElement} elm
 * @param {string} cls
 */
export function addClass(ele, cls) {
  if (!hasClass(ele, cls)) ele.className += ' ' + cls
}

/**
 * 从元素中移除类
 * @param {HTMLElement} elm
 * @param {string} cls
 */
export function removeClass(ele, cls) {
  if (hasClass(ele, cls)) {
    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
    ele.className = ele.className.replace(reg, ' ')
  }
}
<template>
  <div id="myElement" class="box">这是一个示例元素</div>
</template>

<script>
import { hasClass, addClass, removeClass } from '@/utils'

export default {
  mounted() {
    const element = document.getElementById('myElement')

    // 检查元素是否包含类名 'box'
    const hasBoxClass = hasClass(element, 'box') // 返回 true
    console.log('hasBoxClass', hasBoxClass)

    // 向元素添加类名 'highlight'
    addClass(element, 'highlight')

    // 检查元素是否包含类名 'highlight'
    const hasHighlightClass = hasClass(element, 'highlight') // 返回 true
    console.log('hasHighlightClass', hasHighlightClass)

    // 从元素中移除类名 'box'
    removeClass(element, 'box')

    // 再次检查元素是否包含类名 'box'
    const hasBoxClassAfterRemoval = hasClass(element, 'box') // 返回 false
    console.log('hasBoxClassAfterRemoval', hasBoxClassAfterRemoval)
  }
}
</script>

切换 类:

/**
 * 切换 类
 * @param {HTMLElement} element
 * @param {string} className
 */
export function toggleClass(element, className) {
  if (!element || !className) {
    return
  }
  let classString = element.className
  const nameIndex = classString.indexOf(className)
  if (nameIndex === -1) {
    classString += '' + className
  } else {
    classString =
      classString.substr(0, nameIndex) +
      classString.substr(nameIndex + className.length)
  }
  element.className = classString
}

<template>
  <div class="app-container">
    <button @click="toggleClass">切换 类</button>
    <br><br>
    <div ref="myElement" class="box">这是一个盒子</div>
  </div>
</template>

<script>
import { toggleClass } from '@/utils'

export default {
  methods: {
    toggleClass() {
      const element = this.$refs.myElement
      toggleClass(element, ' active')
    }
  }
}
</script>

<style lang="scss" scoped>
.box {
  width: 100px;
  height: 100px;
  border: 1px solid #ccc;
}
.active{
  background-color: red;
}
</style>

12、数组去重

/**
 * 去除一个数组中的重复元素
 * @param {Array} arr
 * @returns {Array}
 */
export function uniqueArr(arr) {
  return Array.from(new Set(arr))
}

13、生成唯一的字符串

通常可以用作标识符或者用于确保某些操作的唯一性

/**
 * 生成一个唯一的字符串
 * @returns {string}
 */
export function createUniqueString() {
  const timestamp = +new Date() + ''
  const randomNum = parseInt((1 + Math.random()) * 65536) + ''
  return (+(randomNum + timestamp)).toString(32)
}

14、防抖函数

/**
 * 防抖函数
 * @param {Function} func
 * @param {number} wait
 * @param {boolean} immediate
 * @return {*}
 */
export function debounce(func, wait, immediate) {
  let timeout, args, context, timestamp, result

  const later = function() {
    // 据上一次触发时间间隔
    const last = +new Date() - timestamp

    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last)
    } else {
      timeout = null
      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
      if (!immediate) {
        result = func.apply(context, args)
        if (!timeout) context = args = null
      }
    }
  }

  return function(...args) {
    context = this
    timestamp = +new Date()
    const callNow = immediate && !timeout
    // 如果延时不存在,重新设定延时
    if (!timeout) timeout = setTimeout(later, wait)
    if (callNow) {
      result = func.apply(context, args)
      context = args = null
    }

    return result
  }
}

使用例子:

<template>
  <div class="app-container">
    <input
      v-model="keyWord"
      type="text"
      placeholder="请输入关键词"
      @input="handleInput"
    >
  </div>
</template>

<script>
import { debounce } from '@/utils'

export default {
  data() {
    return {
      keyWord: '',
      debounceSearch: null
    }
  },
  created() {
    // 写在这里的目的:保证每次输入时不会创建新的实例,从而导致多次请求
    this.debounceSearch = this.createDebounceSearch()
  },
  methods: {
    // 获取搜索内容
    search(query) {
      // 发起异步请求获取数据
      console.log('获取新数据')
    },
    createDebounceSearch() {
      return debounce((query) => {
        this.search(query)
      }, 2000)
    },
    handleInput() {
      this.debounceSearch(this.keyWord)
    }
  }
}
</script>

15、合并2个对象

/**
 * 将两个对象进行合并,并且第二个对象的属性具有优先权
 * @param {Object} target
 * @param {(Object|Array)} source
 * @returns {Object}
 */
export function objectMerge(target, source) {
  if (typeof target !== 'object') {
    target = {}
  }
  if (Array.isArray(source)) {
    return source.slice()
  }
  Object.keys(source).forEach(property => {
    const sourceProperty = source[property]
    if (typeof sourceProperty === 'object') {
      target[property] = objectMerge(target[property], sourceProperty)
    } else {
      target[property] = sourceProperty
    }
  })
  return target
}

具体使用:

const targetObject = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA'
  }
}

const sourceObject = {
  age: 35,
  address: {
    city: 'Los Angeles'
  },
  hobbies: ['reading', 'swimming']
}

const result = objectMerge(targetObject, sourceObject)

最终输出结果:

{
    "name": "John",
    "age": 35,
    "address": {
        "city": "Los Angeles",
        "country": "USA"
    },
    "hobbies": [
        "reading",
        "swimming"
    ]
}

16、将JSON对象转换成URL查询字符串

/**
 * 移除数组中的假值
 * @param {Array} actual
 * @returns {Array}
 */
export function cleanArray(actual) {
  const newArray = []
  for (let i = 0; i < actual.length; i++) {
    if (actual[i]) {
      newArray.push(actual[i])
    }
  }
  return newArray
}

/**
 * 将该对象的键值对转化为 URL 查询字符串格式
 * @param {Object} json
 * @returns {Array}
 */
export function param(json) {
  if (!json) return ''
  return cleanArray(
    Object.keys(json).map(key => {
      if (json[key] === undefined) return ''
      return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
    })
  ).join('&')
}

具体使用:

const data = {
  name: 'csheng',
  age: 18
}
const queryString = param(data)
console.log(queryString) // "name=csheng&age=18"

17、将HTML转换为纯文本字符串

/**
 * 将HTML转换为纯文本字符串
 * @param {string} val
 * @returns {string}
 */
export function html2Text(val) {
  const div = document.createElement('div')
  div.innerHTML = val
  return div.textContent || div.innerText
}

具体使用:

const htmlString = '<p>Hello <strong>world</strong>!</p>'
const text = html2Text(htmlString)
console.log(text) // 输出: "Hello world!"

18、列表 转换 树形结构

/**
 * 列表型数据转化树形
 * @param {Array} list
 * @param {string} rootValue
 */
export function transformData(arr, root) {
  const newArr = []
  arr.forEach(item => {
    if (item.pid === root) {
      const children = transformData(arr, item.id)
      if (children.length) item.children = children
      newArr.push(item)
    }
  })
  return newArr
}

具体使用:

const dataList = [
  { id: '1', name: '部门A', pid: '' },
  { id: '2', name: '部门B', pid: '' },
  { id: '3', name: '员工1', pid: '1' },
  { id: '4', name: '员工2', pid: '1' },
  { id: '5', name: '员工3', pid: '2' },
  { id: '6', name: '员工4', pid: '2' },
];
const treeData = transListToTreeData(dataList, '');
console.log(treeData);

19、根据 ID 查找父节点/集合

/**
 * 根据给定的 ID 查找该节点的所有父节点
 * @param {Array} list
 * @param {string} id
 */
export function getParentsById(list, id) {
  for (const i in list) {
    // 1、查询第一层有没有匹配的value值
    if (list[i].value === id) {
      return [list[i].value]; // 单个元素的数组
    }
    // 2、深层查询
    if (list[i].children) {
      // 递归继续 “深挖”
      const node = getParentsById(list[i].children, id);
      // 如果挖到了,就把所在的项的value叠加在数组最前面
      if (node !== undefined) {
        node.unshift(list[i].value);
        return node;
      }
    }
  }
}

具体使用:

const data = [
  {
    value: 1,
    children: [
      {
        value: 2,
        children: [{ value: 3 }, { value: 4 }],
      },
      {
        value: 5,
        children: [{ value: 6 }],
      },
    ],
  },
  {
    value: 7,
    children: [{ value: 8 }],
  },
];
const result = getParentsById(data, 4)
console.log(result); // [1, 2, 4]

20、将 http 替换成访问地址的网路协议

/**
 * 传入的 URL 字符串中的 http: 替换为当前页面的协议
 * 确保网页上加载的资源(例如图片、样式表、脚本等)使用与当前页面相同的协议,以避免浏览器的安全性限制
 * @param {string} url 访问地址
 * @returns {string}
 */
export function httpsReplace(url) {
  return url.replace("http:", window.location.protocol);
}

五、校验函数

通常存放在 src/utils/validate.js 里面

1、链接指向外部网页

/**
 * 链接是否指向外部网页
 * @param {string} path
 * @returns {Boolean}
 */
export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}

2、用户名

/**
 * 用户名
 * @param {string} str
 * @returns {Boolean}
 */
export function validUsername(str) {
  const valid_map = ['admin', 'editor']
  return valid_map.indexOf(str.trim()) >= 0
}

3、URL

/**
 * URL
 * @param {string} url
 * @returns {Boolean}
 */
export function validURL(url) {
  const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
  return reg.test(url)
}

4、字母

/**
 * 小写字母
 * @param {string} str
 * @returns {Boolean}
 */
export function validLowerCase(str) {
  const reg = /^[a-z]+$/
  return reg.test(str)
}

/**
 * 大写字母
 * @param {string} str
 * @returns {Boolean}
 */
export function validUpperCase(str) {
  const reg = /^[A-Z]+$/
  return reg.test(str)
}

/**
 * 字母
 * @param {string} str
 * @returns {Boolean}
 */
export function validAlphabets(str) {
  const reg = /^[A-Za-z]+$/
  return reg.test(str)
}

5、邮箱

/**
 * 邮箱
 * @param {string} email
 * @returns {Boolean}
 */
export function validEmail(email) {
  const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  return reg.test(email)
}

6、字符串

/**
 * 字符串
 * @param {string} str
 * @returns {Boolean}
 */
export function isString(str) {
  if (typeof str === 'string' || str instanceof String) {
    return true
  }
  return false
}

7、数组

/**
 * 数组
 * @param {Array} arg
 * @returns {Boolean}
 */
export function isArray(arg) {
  if (typeof Array.isArray === 'undefined') {
    return Object.prototype.toString.call(arg) === '[object Array]'
  }
  return Array.isArray(arg)
}

六、工作遇到的方法

1、导入导出

接口 记得 设置 blob 数据类型❗

export function getExportTemplate() {
  return request({
    url: '/sys/user/import/template',
    responseType: 'blob' // 二进制文件流
  })
}

导入使用,用到 fileSaver:

import fileSaver from 'file-saver'
import { getExportTemplate } from '@/api/employee'

// 新增 员工模板
async getTemplate() {
  const result = await getExportTemplate()
  fileSaver.saveAs(result, '员工信息模板.xlsx')
},

七、element-UI 篇

1、el-switch 回填注意

<!-- 没有冒号传递的是 string 类型,要求 v-model 也对应传递 string 类型 -->
<el-tooltip>
  <el-switch
    v-model="value"
    active-value="100"
    inactive-value="0"
  />
</el-tooltip>

<!-- 有冒号传递的是 number 类型,要求 v-model 也对应传递 number 类型 -->
<el-tooltip>
  <el-switch
    v-model="value"
    :active-value="100"
    :inactive-value="0"
  />
</el-tooltip>

2、el-tree 获取和回填选中项id

<el-tree
  ref="tree"
  :data="data"
  show-checkbox
  default-expand-all
  node-key="id"
  highlight-current
  :props="defaultProps"
/>
// 通过 key 获取选中的 id 集合
getCheckedKeys() {
   console.log(this.$refs.tree.getCheckedKeys())
}
// 通过 key 回填选中项
setCheckedKeys() {
   this.$refs.tree.setCheckedKeys([3])
}

3、@click事件修饰符

.native.prevent 的作用:给组件绑定原生事件和阻止事件冒泡。

<el-button 
	type="text" 
	size="small" 
	@click.native.prevent="handleOrderSupplement(scope.row)"
>点击</el-button>

4、datetimeRangePikcer 选项

options.shortcuts = [
  {
    text: '今天',
    onClick (picker) {
      const end = new Date()
      const start = new Date()
      picker.$emit('pick', [start, end])
    }
  },
  {
    text: '昨天',
    onClick (picker) {
      const end = new Date()
      const start = new Date()
      start.setDate(end.getDate() - 1)
      picker.$emit('pick', [start, end])
    }
  },
  {
    text: '最近7天',
    onClick (picker) {
      const end = new Date()
      const start = new Date()
      start.setDate(end.getDate() - 6) // 6天前
      picker.$emit('pick', [start, end])
    }
  },
  {
    text: '过去7天',
    onClick (picker) {
      const end = new Date()
      const start = new Date()
      end.setDate(end.getDate() - 1) // 昨天
      start.setDate(end.getDate() - 6) // 7天前
      picker.$emit('pick', [start, end])
    }
  },
  {
    text: '最近30天',
    onClick (picker) {
      const end = new Date()
      const start = new Date()
      start.setDate(end.getDate() - 29) // 29天前
      picker.$emit('pick', [start, end])
    }
  },
  {
    text: '本月',
    onClick (picker) {
      const end = new Date()
      const start = new Date(end.getFullYear(), end.getMonth(), 1) // 当月第一天
      picker.$emit('pick', [start, end])
    }
  },
  {
    text: '上月',
    onClick (picker) {
      const end = new Date()
      end.setDate(0) // 上月最后一天
      const start = new Date(end.getFullYear(), end.getMonth(), 1) // 上月第一天
      picker.$emit('pick', [start, end])
    }
  }
]

八、知识储备

1、$route.fullPath / $route.path

  • $route.fullPath :完整的解析URL,包括查询和哈希;
  • $route.path :当前路径的路径,始终解析为绝对路径,例如 “/ foo / bar”。

2、对象{}转为&字符串

const obj = {a: 1, b: 2};

// 方法一:使用 URLSearchParams 对象
const queryString = new URLSearchParams(obj).toString()

// 方法二:使用 qs 第三方库(qs.stringify)
import qs from 'qs' // 导入方式 1
const qs = require('qs'); // 导入方式 2
const obj = {a: 1, b: 2};
const queryString = qs.stringify(obj);

// 额外补充:qs.parse 将 & 字符串转换为对象 {}
const paramsString = 'a=1&b=2';
const obj = qs.parse(paramsString); // {a: 1, b: 2}

{a: 1, b: 2} 处理为 ?a=1&b=2&

let str = "?";
for (const [key, value] of Object.entries(obj)) {
  str += `${key}=${value}&`;
}

3、FormData格式

4、path.resolve 和 path.join

  • path.join([path1][, path2][, …])方法只是将每个路径片段进行拼接,并规范化生成一个路径。
  • path.resolve([path1][, path2][, …])方法会把一个路径或路径片段的序列解析为一个绝对路径。

5、环境变量的获取

// .env.development
# just a flag
ENV = 'development'

# base api
VUE_APP_BASE_API = '/dev-api'
console.log(process.env.NODE_ENV) // 'development'
console.log(process.env.VUE_APP_BASE_API) // '/dev-api'

6、拖拽事件

h5 新增的 api 拖拽事件:

const div = document.querySelector("div");

// 拖拽中 持续触发
div.addEventListener("dragover", function (e) {
  e.preventDefault(); 
});
// 松开手
div.addEventListener("drop", function (e) {
  e.preventDefault(); 
  console.log(e.dataTransfer.files[0]); // 获取拖拽的文件
}); 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Vue2后台管理系统是一个基于Vue.js框架的前端项目,用于构建和管理后台管理界面。该项目可以通过安装Node.js并在终端输入"npm install"和"npm run serve"来启动。登录账户为admin,密码为admin。 在项目中,可以使用Vue Router插件来实现路由功能。通过导入VueVue Router,并配置路由表,可以定义不同路径对应的组件。例如,可以使用"/login"路径来渲染Login组件,使用"/home"路径来渲染Home组件。除此之外,还可以设置重定向、子路由等功能。 该项目是一个前端入门级的后台管理系统模板,主要用于熟悉Vue框架和插件的使用。如果在项目中遇到问题或有好的解决方案,可以在评论区提出并与其他开发者交流。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [vue2后台管理系统](https://blog.csdn.net/fanlangke/article/details/126566029)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Vue2 公司后台管理系统(仅前端)](https://blog.csdn.net/weixin_52615959/article/details/125628852)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Heisenberg504

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值