vue3.0 搭建项目总结

1.环境配置

项目中的不同开发环境有很多依赖配置,所以可以根据环境设置不同的配置,以免在不同环境经常修改文件

1 在根目录下创建 `.env.[环境]` 文件,可以在不同环境设置一些配置变量,如图

《vue3.0 搭建项目总结》

《vue3.0 搭建项目总结》 .env.dev 文件

2.eslint 配置

在package.json 文件里面有一个eslintConfig对象,可设置rules: 如图
    

《vue3.0 搭建项目总结》

3.配置svg

  • 在vue.config.js 里面需在module.exports对象里面设置

    chainWebpack: config => {
        config.module.rules.delete('svg') // 重点:删除默认配置中处理svg,//const svgRule = config.module.rule('svg') //svgRule.uses.clear()
        config.module
          .rule('svg-sprite-loader')
          .test(/\.svg$/)
          .use('svg-sprite-loader')
          .loader('svg-sprite-loader')
          .options({
            symbolId: 'icon-[name]'
          })
      }
  • svg component

    <template>
      <svg :class="svgClass" aria-hidden="true">
        <use :xlink:href="iconName" />
      </svg>
    </template>
    
    <script>
    export default {
      name: 'SvgIcon',
      props: {
        iconClass: {
          type: String,
          required: true
        },
        className: {
          type: String,
          default: ''
        }
      },
      computed: {
        iconName() {
          return `#icon-${this.iconClass}`
        },
        svgClass() {
          if (this.className) {
            return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    }
  }
}
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

```
  • 使用svg组件
import SvgIcon from '@/components/SvgIcon.vue'
// 设置全局组件svgIcon
Vue.component('svg-icon', SvgIcon)
const req = require.context('./assets/svg', true, /\.svg$/) // 查询文件加下面的svg文件
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req) // 全局导入svg文件

4.通用组件

级联(多选且可以选择全部)组件

  • 安装插件 multi-cascader-base-ele
  • 使用
    import multiCascader from ‘multi-cascader-base-ele’
    Vue.use(multiCascader)

— 支持选择全部

<template>
  <div>
    <MultiTestCascader v-model="selectedOptions" class="multi-cascader" :props="customProps" :options="options" multiple filterable select-children :show-all-levels="false" clearable only-out-put-leaf-node @change="cascaderChange" />
  </div>
</template>
<script>
export default {
  props: {
  // 传入级联列表数据
    options: {
      type: Array,
      default: () => []
    },
    // 传入选择数据
    list: {
      type: Array,
      default: () => []
    },
    // 自定义相关字段
    customProps: {
      type: Object,
      default: () => {
        return {
          label: 'label',
          value: 'value',
          children: 'children'
        }
      }
    },
    // 显示全部类型 1 全部二级/全部三级 2 全部二级分类/全部三级分类 3 全省/全市
    type: {
      type: String,
      default: () => '1'
    }
  },
  data() {
    return {
      selectedOptions: this.list,
      listStatus: true
    }
  },
  created() {

  },
  watch: {
    options(newValue, oldValue) {
      this.setListDisabled(newValue)
      this.addAllLabel(newValue)
    },
    list(newValue) {
      if (this.listStatus) {
        this.cascaderChange(newValue)
        this.listStatus = false
      }
    }
  },
  mounted() {
    this.setListDisabled(this.options)
    this.addAllLabel(this.options)
  },
  methods: {
    addAllLabel(list) {
      list.forEach(val => {
        if (val[this.customProps.children] && val[this.customProps.children].length > 0 && val[this.customProps.children][0][this.customProps.label] !== (this.type === '1' ? '全部一级' : (this.type === '2' ? '全部一级分类' : (this.type === '3' ? '全省' : '')))) {
          if (val[this.customProps.children].length > 1) {
            val[this.customProps.children].unshift({
              [this.customProps.label]: this.type === '1' ? '全部二级' : (this.type === '2' ? '全部二级分类' : (this.type === '3' ? '全省' : '')),
              [this.customProps.value]: val[this.customProps.value],
              [this.customProps.children]: null
            })
          }
          val[this.customProps.children].forEach(v => {
            if (v[this.customProps.children] && v[this.customProps.children].length > 1 && v[this.customProps.children][0][this.customProps.label] !== (this.type === '1' ? '全部二级' : (this.type === '2' ? '全部二级分类' : (this.type === '3' ? '全省' : '')))) {
              if (v[this.customProps.children].length > 1) {
                v[this.customProps.children].unshift({
                  [this.customProps.label]: this.type === '1' ? '全部三级' : (this.type === '2' ? '全部三级分类' : (this.type === '3' ? '全市' : '')),
                  [this.customProps.value]: v[this.customProps.value],
                  [this.customProps.children]: null
                })
              }
            }
          })
        }
      })
    },
    setListDisabled(list) {
      const label = this.customProps.label
      const value = this.customProps.value
      const children = this.customProps.children
      list.forEach(val => {
        val.disabled = false
        if (val[children]) this.setListDisabled(val[children])
      })
    },
    cascaderChange(itemList) {
      if (!itemList || itemList.length === 0) {
        this.selectedOptions = []
      }
      this.setListDisabled(this.options)
      const label = this.customProps.label
      const value = this.customProps.value
      const children = this.customProps.children
      this.options.forEach((v, l) => {
        this.selectedOptions.forEach(val => {
          if (val[0] === '-1') {
            if (v[value] !== '-1') v.disabled = true
            else v.disabled = false
            if (v[children] && v[children].length > 0) {
              v[children].forEach(c => { c.disabled = true })
            }
          } else {
            if (v[value] === '-1') v.disabled = true
            else v.disabled = false
            if (v[children] && v[children].length > 0) {
              v[children].forEach(c => { c.disabled = false })
            }
          }
          if (val.length === 2 && v[value] === val[0] && v[children]) {
            v[children].forEach((item, num) => {
              item.disabled = false
              if (val[0] === val[1] && item[value] === val[1]) {
                item.disabled = false
              } else {
                if (val[0] === val[1] && num !== 0) {
                  item.disabled = true
                  if (item[children]) {
                    item[children].forEach(i => {
                      i.disabled = true
                    })
                  }
                }
                if (val[0] !== val[1] && num === 0 && v[children].length > 1) item.disabled = true
              }
              // this.options[l][children][0].disabled = true
            })
          }
          if (val.length === 3 && v[value] === val[0] && v[children]) {
            v[children].forEach((item, j) => {
              // let status = false
              if (item[children] && val[1] === item[value]) {
                item.disabled = false
                item[children].forEach((i, index) => {
                  i.disabled = false
                  if (i[value] === val[2]) status = true
                  if (i[value] === val[2] && val[1] === val[2]) {
                    i.disabled = false
                  } else {
                    if (val[1] !== val[2] && index === 0 && v[children].length > 1) i.disabled = true
                    if (val[1] === val[2] && index !== 0) i.disabled = true
                  }
                })
                // this.options[0].disabled = true
                this.options[l][children][0].disabled = true
                // return status
              }
            })
          }
        })
      })
      this.selectedOptions = this.selectedOptions.map(val => {
        if (val.length === 2 && val[0] === val[1]) return [val[0]]
        if (val.length === 1 && val[0] === '-1') return [val[0]]
        if (val.length === 3 && val[1] === val[2]) return [val[0], val[1]]
        return val
      })
      const item = this.selectedOptions[this.selectedOptions.length - 1]
      const length = this.selectedOptions.length
      let status = -1
      this.selectedOptions.some((val, index) => {
        if ((length - 1) === index) return true
        if (item.length === val.length) {
          if (item.join(',') === val.join(',')) {
            status = 1
            return true
          }
        }
        if (item.length > val.length) {
          if (item.join(',').includes(val.join(','))) {
            status = 2
            return true
          }
        }
        if (val.length > item.length) {
          if (val.join(',').includes(item.join(','))) {
            status = 3
            return true
          }
        }
      })
      if (status !== -1) {
        this.selectedOptions.splice(this.selectedOptions.length - 1, 1)
      }
      this.$emit('update:list', this.selectedOptions)
    }
  }
}
</script>

上传(支持图片/视频/裁剪图片/拖拽)

  • 安装插件
    vuedraggable axios vue-cropper
  • 代码
<!--  -->
<template>
  <div class="image-draggable">
    <draggable v-model="draggableList" @end="onEnd">
      <!-- <transition-group> -->
      <div v-for="(item, index) in draggableList" :key="index" class="image-list">
        <template v-if="item.isImg">
          <img :src="item.displayUrl" alt="" srcset="" style="width: 148px; height: 148px;">
          <div class="icon">
            <span @click="viewImage(item.displayUrl)">
              <svg-icon icon-class="view" class="icon-size" style="margin-right: 10px;"></svg-icon>
            </span>
            <span @click="remove(index)">
              <svg-icon icon-class="delete" class="icon-size"></svg-icon>
            </span>
          </div>
        </template>
        <template v-if="!item.isImg">
          <video :src="item.displayUrl" :ref="item.id" :id="item.id" :poster="item.coverUrl" style="width: 148px; height: 148px;">
          </video>
          <div class="icon">
            <span v-if="item.isPlay" @click="play(item)" class="video-icon">
              <svg-icon icon-class="play" class="icon-size"></svg-icon>
            </span>
            <span v-if="!item.isPlay" @click="pause(item)" class="video-icon">
              <svg-icon icon-class="pause" class="icon-size"></svg-icon>
            </span>
            <span @click="fullPlay(item)" class="video-icon">
              <svg-icon icon-class="full" class="icon-size"></svg-icon>
            </span>
            <span @click="remove(index)">
              <svg-icon icon-class="delete" class="icon-size"></svg-icon>
            </span>
          </div>
        </template>
      </div>

      <!-- </transition-group> -->
    </draggable>
    <el-upload :id="uploadId" :disabled="isDiabled" :action="uploadUrl" class="image-upload" :headers="headers" :accept="accept" list-type="picture-card" :show-file-list="false" :on-preview="handlePictureCardPreview" :on-progress="handleProgress" :on-change="fileChange" :auto-upload="!isCropper" :on-remove="handleRemove" :on-success="imageSuccess" :before-upload="fileBeforeUpload">
      <i class="el-icon-plus"></i>
      <el-progress :percentage="percentage" v-if="isUpload && isLoading" :show-text="false"></el-progress>
    </el-upload>
    <el-dialog :visible.sync="dialogVisible">
      <img width="100%" :src="dialogImageUrl" alt="">
    </el-dialog>
    <el-dialog :visible.sync="modifyCropper">
      <div :style="{height: (autoCropHeight + 100) + 'px'}">
        <vueCropper ref="cropper" :img="imgSrc" :outputSize="option.size" :outputType="option.outputType" :info="true" :full="option.full" :canMove="option.canMove" :canMoveBox="option.canMoveBox" :original="option.original" :autoCrop="option.autoCrop" :autoCropHeight="autoCropHeight" :autoCropWidth="autoCropWidth" :fixedBox="option.fixedBox" @realTime="realTime" @imgLoad="imgLoad"></vueCropper>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="modifyCropper = false">取 消</el-button>
        <el-button type="primary" @click="uploadCropperImage">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
// 拖拽
import draggable from 'vuedraggable'
// 裁剪
import { VueCropper } from 'vue-cropper'
// 上传地址
import { upload } from '@/api'
import { getToken } from '@/util/auth'
import axios from 'axios'

export default {
  name: '',
  data() {
    return {
      headers: {
        Authorization: getToken()
      },
      uploadUrl: upload,
      displayUrl: '',
      dialogImageUrl: '',
      dialogVisible: false,
      percentage: 0,
      accept: '',
      draggableList: [],
      isUpload: false,
      modifyCropper: false,
      isDiabled: false,
      cropperImage: {
      },
      uploadId: 'id' + Date.now(),
      imgSrc: '',
      option: {
        size: 0.5,
        full: true, // 输出原图比例截图 props名full
        outputType: 'png',
        canMove: true,
        original: true,
        canMoveBox: false,
        autoCrop: true,
        fixedBox: true
      }
    }
  },
  props: {
    // 已存在的文件
    fileList: {
      type: Array,
      default() {
        return [
        ]
      }
    },
    // 返回类型 Array 数组 Object 对象
    returnType: {
      type: String,
      default: 'Array'
    },
    // 自定义对象
    customObject: {
      type: Object,
      default: () => { }
    },
    // 上传的最大个数
    maxNum: {
      type: Number,
      required: true,
      default: 1
    },
    // 单位MB
    maxSize: {
      type: Number,
      default: 15
    },
    autoCropWidth: {
      type: Number,
      default: 180
    },
    autoCropHeight: {
      type: Number,
      default: 180
    },
    // 上传类型 All 图片/视频 image 图片 video视频
    acceptType: {
      type: String,
      default: 'All'
    },
    // 是否裁剪
    isCropper: {
      type: Boolean,
      default: false
    },
    // 是否显示加载条
    isLoading: {
      type: Boolean,
      default: true
    },

    outputSize: {
      type: Number,
      default: 1
    },
    outputType: {
      type: String,
      default: 'jpeg'
    }
  },
  components: {
    draggable,
    VueCropper
  },
  watch: {
    draggableList(newValue, oldValue) {
      this.getElement(this.draggableList.length)
    },
    fileList(newValue, oldValue) {
      this.draggableList = newValue
      this.initImage()
    }
  },

  computed: {},

  mounted() {
    if (this.acceptType === 'All') {
      this.accept = 'image/png, image/jpeg, image/gif, image/jpg, .mp4,.qlv,.qsv,.ogg,.flv,.avi,.wmv,.rmvb'
    }
    if (this.acceptType === 'image') {
      this.accept = 'image/png, image/jpeg, image/gif, image/jpg'
    }
    if (this.acceptType === 'video') {
      this.accept = '.mp4,.qlv,.qsv,.ogg,.flv,.avi,.wmv,.rmvb'
    }
    this.initImage()
  },
  methods: {
    // 获取五位数的随机数
    getRandom() {
      return (((Math.random() + Math.random()) * 10000) + '').substr(0, 5).replace('.', 0)
    },
    initImage() {
      const _this = this
      // console.log('file', this.fileList)
      if (this.fileList.length > 0) {
        this.draggableList = this.fileList.map(val => {
          let displayUrl = ''
          let coverUrl = ''
          let isImg = true
          const files = (val.url ? val.url : val).split(',')
          if (files.length === 3) {
            displayUrl = files[1]
            coverUrl = files[2]
            isImg = false
          } else if (files.length === 1) {
            displayUrl = (val.url ? val.url : val)
            isImg = true
          }
          const fileObj = Object.assign({}, {
            coverUrl: coverUrl,
            displayUrl: displayUrl,
            isImg: isImg,
            isPlay: true,
            name: Date.now(),
            url: (val.url ? val.url : val),
            id: val.id || Date.now() + _this.getRandom()
          })
          return fileObj
        }).filter(val => { return val.url })
      }
    },
    handleRemove(file, fileList) {
      this.getElement(fileList.length)
    },
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url
      this.dialogVisible = true
    },
    handleProgress(event, file, fileList) {
      this.percentage = +file.percentage
    },
    fileBeforeUpload(file, event) {
      if (this.acceptType === 'image' && !file.type.includes('image/')) {
        this.$warning('请上传图片')
        return false
      }
      if (this.acceptType === 'video' && !file.type.includes('video/')) {
        this.$warning('请上传视频')
        return false
      }
      this.isUpload = true
      if (file.type.includes('image/') && (file.size > this.maxSize * 1024 * 1024)) {
        this.$warning(`请上传小于${this.maxSize}M的图片`)
        this.percentage = 0
        this.isLoading = false
        return false
      }
      if (file.type.includes('video/')) this.isDiabled = true
      if (this.isCropper) {
        return false
      }
    },
    fileChange(file, fileList) {
      if (file.percentage === 0 && this.isCropper) {
        if (file.raw.type.includes('video/')) {
          this.$warning('请上传图片')
          return
        }
        this.imgSrc = file.url
        this.modifyCropper = true
        this.cropperImage = {
          coverUrl: '',
          isImg: true,
          isPlay: true,
          name: file.name
        }
      }
    },
    // 实时预览函数
    realTime(data) {
      this.previews = data
    },
    imgLoad(data) {
    },
    // 裁剪后上传图片
    uploadCropperImage() {
      const _this = this
      this.$refs.cropper.getCropBlob((data) => {
        const config = {
          headers: {
            'Authorization': _this.headers.Authorization,
            'Content-Type': 'multipart/form-data'
          }
        }
        const formdata = new FormData()
        formdata.append('file', data)
        // this.uploadUrl 上传
        axios.post(this.uploadUrl, formdata, config).then(response => {
          _this.cropperImage = Object.assign({}, _this.cropperImage, {
            displayUrl: response.data.data,
            url: response.data.data,
            id: Date.now()
          })
          _this.draggableList.push(_this.cropperImage)
          _this.$emit('getImageList', _this.draggableList.map(val => {
            if (this.returnType === 'Array') {
              return val.url
            }
            if (this.returnType === 'Object') {
              return {
                url: val.url,
                uploadStatus: true
              }
            }
          }), _this.customObject)
          _this.modifyCropper = false
        }).catch(error => {
          console.log('err', error)
        })
      })
    },
    imageSuccess(response, file, fileList) {
      const _this = this
      try {
        this.getElement(fileList.length)
        let displayUrl = ''
        let coverUrl = ''
        let isImg = true
        const url = file.response.data || file.url
        this.isUpload = false
        const files = url.split(',')
        if (files.length === 3) {
          displayUrl = files[1]
          coverUrl = files[2]
          isImg = false
        } else if (files.length === 1) {
          displayUrl = url
          isImg = true
        }
        const id = Date.now()
        _this.draggableList.push({
          name: file.name,
          url: url,
          coverUrl: coverUrl,
          displayUrl: displayUrl,
          isImg: isImg,
          isPlay: true,
          id: id
        })
        if (isImg) {
          _this.percentage = 0
          _this.$emit('getImageList', _this.draggableList.map(val => {
            if (this.returnType === 'Array') {
              return val.url
            }
            if (this.returnType === 'Object') {
              return {
                url: val.url,
                uploadStatus: true
              }
            }
          }), _this.customObject)
          return
        }
        _this.$emit('getImageList', _this.draggableList.map(val => {
          if (this.returnType === 'Array') {
            return val.url
          }
          if (this.returnType === 'Object') {
            return {
              url: val.url,
              uploadStatus: false
            }
          }
        }), _this.customObject)
        setTimeout(() => {
          const keys = Object.keys(_this.$refs)
          const video = _this.$refs[`${keys[keys.length - 1]}`][0]
          const removeId = keys[keys.length - 1]
          const interval = setInterval(() => {
            if (video.readyState === 4) {
              const duration = video.duration
              this.isDiabled = false
              if (duration < 3 || duration > 60) {
                _this.$message.success('请上传大于三秒小于六十秒的视频')
                _this.percentage = 0
                // _this.remove(_this.draggableList.length - 1)
                _this.draggableList = _this.draggableList.filter(val => {
                  return (val.id + '') !== (removeId + '')
                })
                _this.$emit('getImageList', _this.draggableList.map(val => {
                  if (this.returnType === 'Array') {
                    return val.url
                  }
                  if (this.returnType === 'Object') {
                    return {
                      url: val.url,
                      uploadStatus: true
                    }
                  }
                }), _this.customObject)
                _this.getElement(_this.draggableList.length)
              }
              _this.percentage = 0
              _this.$emit('getImageList', _this.draggableList.map(val => {
                if (this.returnType === 'Array') {
                  return val.url
                }
                if (this.returnType === 'Object') {
                  return {
                    url: val.url,
                    uploadStatus: true
                  }
                }
              }), _this.customObject)
              clearInterval(interval)
            }
            video.src = displayUrl
            video.poster = coverUrl
          }, 1000)
        }, 1000)
      } catch (error) {
        console.log('error', error)
      }
    },
    play(item) {
      const video = document.getElementById(item.id)
      video.play()
      item.isPlay = !item.isPlay
    },
    pause(item) {
      const video = document.getElementById(item.id)
      video.pause()
      item.isPlay = !item.isPlay
    },
    // 全屏播放
    fullPlay(item) {
      const video = document.getElementById(item.id)
      // w3c
      typeof video.requestFullScreen === 'function' && video.requestFullScreen()
      // webkit(谷歌)
      typeof video.webkitRequestFullScreen === 'function' && video.webkitRequestFullScreen()
      // 火狐
      typeof video.mozRequestFullScreen === 'function' && video.mozRequestFullScreen()
      // IE
      typeof video.msExitFullscreen === 'function' && video.msExitFullscreen()
    },
    viewImage(url) {
      this.dialogImageUrl = url
      this.dialogVisible = true
    },
    remove(index) {
      this.draggableList.splice(index, 1)
      this.$emit('getImageList', this.draggableList.map(val => {
        if (this.returnType === 'Array') {
          return val.url
        }
        if (this.returnType === 'Object') {
          return {
            url: val.url,
            uploadStatus: true
          }
        }
      }), this.customObject)
      this.getElement(this.draggableList.length)
    },
    onEnd(event) {
      this.$emit('getImageList', this.draggableList.map(val => {
        if (this.returnType === 'Array') {
          return val.url
        }
        if (this.returnType === 'Object') {
          return {
            url: val.url,
            uploadStatus: true
          }
        }
      }), this.customObject)
    },
    isImg(obj) {
      const item = obj.url
      if (item === '' || item === null || typeof item === 'undefined') {
        return false
      }
      const index = item.lastIndexOf('.')
      var ext = item.substr(index + 1)
      if (ext.includes('!')) ext = ext.split('!')[0]
      ext = ext.toLowerCase()
      var tps = ['jpg', 'jpeg', 'png']
      let ok = false
      for (let i = 0; i < tps.length; i++) {
        if (tps[i] === ext) {
          ok = true
          break
        }
      }
      return ok
    },
    getElement(length) {
      const _this = this
      if (length >= _this.maxNum) {
        document.querySelectorAll(`#${_this.uploadId} .el-upload--picture-card`).forEach(val => {
          if (val.firstElementChild.className === 'el-icon-plus') {
            val.style.display = 'none'
            return true
          }
        })
      } else {
        document.querySelectorAll(`#${_this.uploadId} .el-upload--picture-card`).forEach(val => {
          if (val.firstElementChild.className === 'el-icon-plus') {
            val.style.display = 'inline-block'
            return true
          }
        })
      }
    }
  }
}

</script>
<style lang='scss' scoped>
.image-draggable {
    display: flex;
    flex-wrap: wrap;
    .image-list {
        position: relative;

        display: inline-block;
        overflow: hidden;

        width: 148px;
        height: 148px;
        margin-right: 10px;

        cursor: pointer;
        &:hover {
            .icon {
                height: 20%;

                transition: all .5s;
                .video-icon {
                    display: inline-block;

                    margin-right: 10px;
                }
            }
        }
        .icon {
            position: absolute;
            bottom: 0;

            display: flex;
            justify-content: center;

            width: 100%;
            height: 0;

            background-color: rgba(215, 215, 215, 1);
            .icon-size {
                width: 2em;
                height: 2em;
            }
            .video-icon {
                display: none;
            }
        }
    }
}
</style>
<style lang="scss">
.image-draggable {
    .el-progress {
        top: -50%;
    }
}
</style>

注册全局事件

  • 创建eventBus.js

《vue3.0 搭建项目总结》

  • 使用

    import eventBus from ‘./plugins/eventBus’
    Vue.use(eventBus)

处理缓存

  • 借用mounted, activated 事件处理数据
    在某一次打开页面的时候进行数据初始化存储, 放置在vuex中,或者全局变量中,当需要初始化进行一个初始化,采取mixins引入

《vue3.0 搭建项目总结》

《vue3.0 搭建项目总结》

5.路由配置

  • 我的项目中使用的是element-UI的导航菜单,支持路由,但是不支持前进后退, 所以使用如下方式处理

     // 监控hash改变且处理,使浏览器支持前进后退
     window.addEventListener('hashchange', function(path) {
         router.replace(path.newURL.split('#')[1])
     })
  • 项目中路由处理逻辑判断
    判断缓存, 是否新打开,添加重定向,获取所有打开路由, 获取所有展示路由菜单

6.API处理

  • 该项目采用axios调取api接口

《vue3.0 搭建项目总结》

《vue3.0 搭建项目总结》

    原文作者:shaowu
    原文地址: https://segmentfault.com/a/1190000019231926
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞