Coder Social home page Coder Social logo

project-issue's People

Contributors

xuanweih avatar

Stargazers

 avatar  avatar

Watchers

 avatar

project-issue's Issues

微信小程序购物车滑动删除

商品信息(图片+ 价格+ 加购)是经常会复用到的模板 写在template里面
滑动删除主要是结合微信自带的
moveble-area+ movable-view
bindtouchstart="touchstart" bindtouchmove="touchmove" 通过touch事件来控制滑动

<!-- 商品组合 -->
<!-- goodsType: gift 赠品, common 普通商品带 + - 购物车,   售罄  isVip: true/false是否为心享会员
  'gray-mask': 送葱商品不满足条件时置灰显示-->
<template name="goods-detail">
  <movable-area class="mova_area {{goodsType === 'gift' ? 'min-mova' : ''}} {{item.saleType === 'C' ? 'cycle-buy' : ''}}">
    <movable-view class="mova_view {{!!giftShallotInfo && giftShallotInfo.isSatisfyGiftShallot === 'N' && item.goodsID == giftShallotInfo.shallotGoodsData.id ? 'gray-mask' : ''}}" direction="horizontal" inertia="true">
      <view class="soldout_mask" wx:if="{{goodsType === 'out'}}"></view>
      <view class="flexs s_b ontime_list {{item.isTouchMove &&  goodsType === 'common' ? 'touch-move-active' : ''}}" data-type="{{goodsType}}"    data-goodsID="{{item.goodsID}}"
       bindtouchstart="touchstart" bindtouchmove="touchmove">
        <view class="flexs s_b content" >
          <view class="flexs a_t goodsbutton {{goodsType === 'gift' ? 'typea' : '' }}{{goodsType === 'out' ? 'typec' : ''}}" catchtap="checkSingle"  data-goodsID="{{item.goodsID}}">
            <view wx:if="{{goodsType === 'out'}}" class="out-radio"></view>
            <radio-check wx:if="{{goodsType === 'common'}}" checked="{{item.isSelected === 'Y'}}" size="36rpx"></radio-check>
          </view>
          <template is="goods" data="{{goodsType: goodsType, picUrl, item,  isVip, isLogin}}"></template>
        </view>
        <view class="flexs a_t del" catchtap="del" data-goodsID="{{item.goodsID}}" data-list='preSale'>
            删除
        </view>
      </view>
    </movable-view>
  </movable-area>
</template>

购物车cart.wxml
循环调用模板即可

<!-- 商品列表 -->
 <view class="goods-list">
  <template is="goods-detail" wx:for="{{item.validGoodsList}}" wx:for-item="pItem" wx:for-index="pIndex" wx:key="pIndex" 
       data="{{goodsType: 'common', picUrl, item: pItem, isfromAcrivityPage: false, isLogin, giftShallotInfo, isVip}}"></template>
  </view>

cart.js

//手指触摸动作开始 记录起点X坐标
  touchstart(e) {
    const {
      goodsid,
      type
    } = e.currentTarget.dataset
    if (type !== 'common') return
    this._handleList.forEach(v => (v.isTouchMove = false))
    this.setData({
      startX: e.changedTouches[0].clientX,
      startY: e.changedTouches[0].clientY,
      cartList: this.data.cartList
    })
  },
  //滑动事件处理
  touchmove(e) {
    const {
      goodsid,
      type
    } = e.currentTarget.dataset
    if (type !== 'common') return
    let startX = this.data.startX, //开始X坐标
      startY = this.data.startY, //开始Y坐标
      touchMoveX = e.changedTouches[0].clientX, //滑动变化坐标
      touchMoveY = e.changedTouches[0].clientY, //滑动变化坐标
      item = this._handleList.find(v => v.goodsID === goodsid),
      //获取滑动角度
      angle = this.angle(
        { X: startX, Y: startY },
        { X: touchMoveX, Y: touchMoveY }
      );
    if (Math.abs(angle) > 30) return;
    item.isTouchMove = touchMoveX <= startX // true: 右滑; fasle: 左滑
    this.setData({ cartList: this.data.cartList })
  },
  /**
   * 计算滑动角度
   * @param {Object} start 起点坐标
   * @param {Object} end 终点坐标
   */
  angle: function (start, end) {
    var _X = end.X - start.X,
      _Y = end.Y - start.Y
    //返回角度 /Math.atan()返回数字的反正切值
    return 360 * Math.atan(_Y / _X) / (2 * Math.PI);
  },

封装一个文件下载

import $http from './http'
/**
 * 获取文件后缀名
 * @param {*} filename
 */
function getfilenameExtension (filename) {
  if (typeof filename === 'string') {
    const splits = filename.split('.')
    const splitLen = splits.length
    if (splitLen > 1 && splits[splitLen - 1]) {
      return '.' + splits[splitLen - 1]
    }
  }
  return ''
}
/**
 * 下载
 * @param {*} url 资源路径
 * @param {string} filename 文件名
 */
// 对于文件的资源服务首先存储到后端的 然后再下载文档的需求主要是通过 创建A标签 设置属性download 以及 href 为url实现
export const download = function (url, filename) {
  if (document) {
    let aElem = document.createElement('A')
    filename && aElem.setAttribute('download', filename)
    aElem.setAttribute('href', url)
    aElem.setAttribute('target', '_self')
    aElem.style.display = 'none'
    document.body.appendChild(aElem)
    aElem.click()
    setTimeout(() => {
      document.body.removeChild(aElem)
    })
  }
}
/**
 * 异步下载
 * @param {string} url 请求路径
 * @param {*} data 请求参数
 * @param {*} fileName 下载文件名(可选)不传此参数默认使用服务器返回的文件名
 * @param {*} defautFileName 默认文件名取不到文件名时使用
 */
// 对于后端直接返回文件流的情况, 我们需要自己封装一个处理函数
// 主要通过设置responseType blob 和 responseAll 为true
// 获取的返回结果后 用new FileReader对象的 onload方法 获取到e,target.result 作为url
// 然后再获取fileName 利用A标签 的download属性进行下载
export const ajaxDownload = function (url, data, fileName, defautFileName) {
  return $http.post(url, data, {
    responseType: 'blob',
    responseAll: true
  })
    .then(res => {
      const headers = res.headers
      const data = res.data
      const reader = new FileReader()
      reader.readAsDataURL(data)
      reader.onload = function (e) {
        let extension = getfilenameExtension(fileName)
        let resFileName = ''
        if (!fileName || !extension) {
          const cd = headers['content-disposition']
          const match = cd ? cd.match(/filename=[^;]+/) : null // 取文件名
          if (match) {
            resFileName = match[0].split('=')[1].replace(/"/g, '')
            if (decodeURI && typeof decodeURI === 'function') {
              resFileName = decodeURI(resFileName)
            }
          }
        }
        if (fileName && !extension) {
          fileName += getfilenameExtension(resFileName)
        }
        download(e.target.result, fileName || resFileName || defautFileName)
      }
      return res
    })
}

一些功能函数封装(如视频格式返回,文件类型判断等等)

工作中可能用到的一些功能处理函数:

获取文件名后缀
/**
 * 获取文件后缀名
 * @param {String} filename
 */
 export function getExt(filename) {
    if (typeof filename == 'string') {
        return filename
            .split('.')
            .pop()
            .toLowerCase()
    } else {
        throw new Error('filename must be a string type')
    }
}

使用 
getExt("file.txt") //->txt
复制内容到剪贴板
export function copyToBoard(value) {
    const element = document.createElement('textarea')
    document.body.appendChild(element)
    element.value = value
    element.select()
    if (document.execCommand('copy')) {
        document.execCommand('copy')
        document.body.removeChild(element)
        return true
    }
    document.body.removeChild(element)
    return false
}

休眠
/**
 * 休眠xxxms
 * @param {Number} milliseconds
 */
export function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
}

//使用方式
const fetchData=async()=>{
 await sleep(1000)
}
复制代码

图片相关
给定图片或者视频的格式 手动定义
export const IMAGE_FORMAT = [
  "bmp",
  "jpg",
  "jpeg",
  "png",
  "tif",
  "gif",
  "pcx",
  "tga",
  "exif",
  "fpx",
  "svg",
  "psd",
  "cdr",
  "pcd",
  "dxf",
  "ufo",
  "eps",
  "air",
  "aw",
  "WMF",
  "webp"
];
export const VIDEO_FORMAT = [
  "RM",
  "RMVB",
  "3GP",
  "AVI",
  "MPEG",
  "MPG",
  "MKV",
  "DAT",
  "ASF",
  "WMV",
  "FLV",
  "MOV",
  "MP4",
  "OGG",
  "OGM"
];
// 功能函数
/**
 * 获取后缀名称
 * @param {String} val 要处理的字符串
 */
export function getFileSuffix(val) {
  const index = val.lastIndexOf(".");
  // 这一步能截取出xls 或者 mp4 exe等等尾缀
  return val.substring(index, val.length);
}
// 根据尾缀判断出它是图片 还是视频
/**
 * 获取文件类型
 * @param {String} fileUrl 文件url
 */
export function getFileType(fileUrl = "") {
  let suffix = getFileSuffix(fileUrl);
  if (matchSuffix(IMAGE_FORMAT, suffix)) {
    return "image";
  } else if (matchSuffix(VIDEO_FORMAT, suffix)) {
    return "video";
  } else {
    return "other";
  }
}
/**
 * 下载xls文件
 * @param {String} fileUrl 文件url
 */
export function downloadXlsFile(url) {
  const link = document.createElement("a");
  link.href = url;
  // 提取链接里面的文件名
  const reg = /(?<=\/)([^/]+)$/g;
  const res = reg.exec(url);
  link.setAttribute("download", res ? res[0] : "file.xls");
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}


/**
 * 判断是否属于某类型内
 * @param {Array} types 类型集合
 * @param {String} suffix 后缀 eg:  .jpg
 */
export function matchSuffix(types, suffix) {
  return (
    types.findIndex(s => suffix.toLowerCase() === "." + s.toLowerCase()) !== -1
  );
}


文本转换
/**
 * 将js中的转义符转成html的元素
 *
 * @param {*} str
 * @param {string} [escapes="\\n"]
 * @param {string} [element="<br>"]
 * @returns
 */
export const jsEscapesToHtml = (str, escapes = "\\n", element = "<br>") => {
  const reg = new RegExp(`(${escapes})`, "g");
  return str.replace(reg, element);
};
/**
 * 文件名过长处理---用***省略中间文本
 * @param {*} str
 * @param {string} [sign="***"]
 * @returns string
 */
export const ellipsisStr = (str, sign = "***") => {
  return str.replace(/.{5}(.{3,}).{3,}\.+\w+$/g, (_match, $1) => {
    return str.replace($1, sign);
  });
};

生成随机字符串
/**
 * 生成随机id
 * @param {*} length
 * @param {*} chars
 */
export function uuid(length, chars) {
    chars =
        chars ||
        '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    length = length || 8
    var result = ''
    for (var i = length; i > 0; --i)
        result += chars[Math.floor(Math.random() * chars.length)]
    return result
}

对象转化为formData对象
/**
 * 对象转化为formdata
 * @param {Object} object
 */

 export function getFormData(object) {
    const formData = new FormData()
    Object.keys(object).forEach(key => {
        const value = object[key]
        if (Array.isArray(value)) {
            value.forEach((subValue, i) =>
                formData.append(key + `[${i}]`, subValue)
            )
        } else {
            formData.append(key, object[key])
        }
    })
    return formData
}

保留小数点以后n位
// 保留小数点以后几位,默认2位
export function cutNumber(number, no = 2) {
    if (typeof number != 'number') {
        number = Number(number)
    }
    return Number(number.toFixed(no))
}


数组去重
/**
 * 数组去重
 * @param {*} arr
 */
export function uniqueArray(arr) {
    if (!Array.isArray(arr)) {
        throw new Error('The first parameter must be an array')
    }
    if (arr.length == 1) {
        return arr
    }
    return [...new Set(arr)]
}

生成随机字符串
/**
 * 生成随机id
 * @param {*} length
 * @param {*} chars
 */
export function uuid(length, chars) {
    chars =
        chars ||
        '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    length = length || 8
    var result = ''
    for (var i = length; i > 0; --i)
        result += chars[Math.floor(Math.random() * chars.length)]
    return result
}

基于Vue.Draggable二次封装table

之前有需求做表格拖拽 使用sortablejs, 后来发现vue有一个Vue.Draggable插件也是基于sortablejs.用来做表格拖拽的.
于是尝试用vue.Draggable的表格和之前封装 simple-table 再次进行封装, 拖拽排序表格的组件:
实现如下:

使用vue-draggable的dragSelect tbody获取表格
通过 cansort 来控制 row是否接受控制 进入可拖拽状态
使用已经封装好的simple-table组件 做表格内容传递 headData

简单表格封装

<template>
  <td-draggable element="div" :list="$attrs.data" v-on="$listeners" v-model="newList" dragSelector="tbody" :options="{draggable: canSort?'.el-table__row':''}">
    <simple-table v-on="$listeners" v-bind="$attrs" :head-data="headData" :row-style="rowStyle">
      <template :slot="prop" slot-scope="scope" v-for="(value, prop) in $scopedSlots">
        <slot :name="prop" v-bind="scope"></slot>
      </template>
    </simple-table>
  </td-draggable>
</template>

<script>
import TdDraggable from '@/assets/packages/draggable/index'
import SimpleTable from './simple-table'
export default {
  name: 'simple-table-sort',
  components: {
    SimpleTable,
    TdDraggable
  },
  data () {
    return {
      newList: this.$attrs.data
    }
  },
  computed: {
    rowStyle () {
      if (this.canSort) return {'cursor': 'move'}
    }
  },
  props: {
    headData: Array,
    canSort: {
      type: Boolean,
      default: false
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus">
</style>

使用:

<simple-table-sort
          :head-data="tableHead"
          :show-index="true"
          :height="scope.height"
          :data="categorySort"
          :can-sort="canSort"
          @change="sortChange">
          <!-- 品类名称列 -->
          <template slot-scope="scope" slot="name">
            <div class="c-column">
              <span>{{ scope.row.categoryName }}</span>
            </div>
          </template>
          <!-- 商品数量列 -->
          <template slot-scope="scope" slot="count">
            <div class="c-column">
              <span>{{ scope.row.goodsCount }}</span>
            </div>
          </template>
          <!-- 操作列 -->
          <template slot-scope="scope" slot="operation">
            <div class="c-column">
              <el-button
                type="text"
                @click="relevantGoods(scope.row.id, scope.row.goodsCategoryID, scope.row.categoryName, scope.row.isSupportVip)">关联商品</el-button>
              <el-button
                type="text"
                @click="deleteCategory(scope.row.goodsCategoryID, scope.row.id)">删除</el-button>
            </div>
          </template>
        </simple-table-sort>
data() {
  return{
 tableHead: [
        { label: '已关联品类', prop: 'name', width: '60%', align: 'center' },
        { label: '已关联商品数量', prop: 'count', width: '20%', align: 'center' },
        { label: '操作', prop: 'operation', width: '20%', align: 'center' }
      ],
}
}

图片上传排序预览组件

实现一个 图片上传组件,支持上传时拖拽排序, 点击预览大图

思路大概是:

  1. 没有图片文件展示时, 给一个模板展示
  2. 有图片文件且没有超出时, 对应显示相关配置以及进度条显示 同时用focusing变量表示选中的图片
    注册点击预览事件handlePreview 点击弹出el-dialog 以及 el-carouse
    handlePreview (file) {
      if (this.isPreview) {
        this.dialogVisible = true
        setTimeout(() => {
          this.$refs.carouselBox.setActiveItem(this.fileList.indexOf(file))
        }, 100)
      }
      if ('on-preview' in this.$attrs) {
        this.$attrs['on-preview'](file)
      }
    },

点击更换图片
主要是通过 创造一个 type为file的input标签, input,onchange 把旧的文件放进新文件属性里面,再移除.
调用 handleStart(file) 再调用上传

接着就是基于el-upload完成图片上传的操作 注册一些相关的时间beforeUpload 如果用element的上传直接把路径写action里面,如果要自己自定义httprequest也可以另外写.
同时这个组件还支持上传多张时 拖拽排序 原理也是基于sortablejs 用sortable.create中的onend事件里, 通过

const currRow = this.fileList.splice(oldIndex, 1)[0]
this.fileList.splice(newIndex, 0, currRow)

props 暴露出去的属性大致分为, value 文件属性
type/ image 是否可以上传 canUpload 是否可以预览
一些文件限制 limit 对象 以及输出类型outputType
输出类型主要是用于看后端需要什么值:

      if (this.outputType === 'string') {
        this.$emit('input', files.length === 0 ? '' : files[0].picUrl ? files[0].picUrl : '')
      } else if (this.outputType === 'object') {
        this.$emit('input', files.length === 0 ? {} : files[0])
      } else if (this.outputType === 'array') {
        this.$emit('input', files)
      }

封装代码:

<template>
  <div class="img-upload-preview">
    <template v-if="fileList.length === 0 && type == 'image' && !canUpload">
      <div
        class="no-image"
        :class="{
          'big': size === 'big',
          'medium': size === 'medium',
          'small': size === 'small',
          'mini': size === 'mini',
        }"
      >
      </div>
    </template>
    <template v-if="fileList.length > 0 && type == 'image' && limit.maxNumber >= fileList.length" class="upload-type-image">
      <ul class="el-upload-list el-upload-list--picture-card">
        <li
          v-for="(file, index) in fileList"
          :key="`${file.url}-${index}`"
          class="el-upload-list__item single-image-box"
          :class="{
            'big': size === 'big',
            'medium': size === 'medium',
            'small': size === 'small',
            'mini': size === 'mini',
            'focusing' : focusing === index
          }"
          @mouseenter="focusing = index"
          @mouseleave="focusing = -1"
          @click="focusing = index"

        >
          <el-progress
            class="single-image-progress"
            v-if="file.status === 'uploading'"
            type="circle"
            :width="size === 'big' ? 120 : size === 'medium' ? 90 : size === 'small' ? 60 : 120"
            :stroke-width="size === 'big' ? 6 : size === 'medium' ? 4 : size === 'small' ? 3 : 6"
            :percentage="file.percentage">
          </el-progress>
          <span class="single-image-box__actions">
            <span v-if="isPreview" class="single-image-box__preview" @click="handlePreview(file)">
              <i class="el-icon-zoom-in"></i>
            </span>
            <span v-if="canUpload && size === 'mini'" class="single-image-box__change" @click="handleChangeImg(file)">
              <i class="el-icon-refresh"></i>
            </span>
            <span v-if="canUpload" class="single-image-box__change" @click="handleDelete(file)">
              <i class="el-icon-delete"></i>
            </span>
          </span>
          <span v-if="canUpload && size !== 'mini'" class="single-image-box__changes" @click="handleChangeImg(file)">
            点击更换
          </span>
          <img v-if="file.status === 'success'" :src="file.url">
        </li>
      </ul>
    </template>
    <div class="omp-upload-img" :class="{'no_upload': (isPreview && !canUpload) || fileList.length >= limit.maxNumber, 'is_preview': isPreview}">
      <input v-model="value" placeholder="请输入内容" @input="value = $event.target.value" v-show="false"/>
      <el-upload
        class="upload-box"
        :class="{
          'big': type === 'image' && size === 'big',
          'medium': type === 'image' && size === 'medium',
          'small': type === 'image' && size === 'small',
          'mini': type === 'image' && size === 'mini'
        }"
        ref="upload"
        v-bind="$attrs"
        v-on="$listeners"
        :list-type="listType"
        :file-list="fileList"
        :auto-upload="true"
        :action="uploadUrl"
        :data="data"
        :limit="limit.maxNumber"
        :show-file-list="type !== 'image'"
        :before-upload="beforeUpload"
        :on-success="handleSuccess"
        :on-exceed="handleExceed"
        :on-change="handleChange"
        :on-remove="handleRemove"
        :on-preview="handlePreview"
        :on-progress="handleProgress"
        :disabled="!canUpload"
      >
        <template v-if="['text', 'picture'].indexOf(listType) !== -1">
          <slot name="trigger" slot="trigger">
            <el-button slot="trigger" :size="size" type="primary" :disabled="!canUpload">上传<i class="el-icon-upload el-icon--right"></i></el-button>
          </slot>
          <slot name="tip" slot="tip">
            <div slot="tip" class="el-upload__tip">只能上传{{this.limit.format.join(',')}}文件</div>
          </slot>
          <slot></slot>
        </template>
        <template v-else>
          <slot>
            <div v-if="size === 'big' || size === 'medium'" style="display: flex; justify-content: center; align-items: center; flex-direction: column; height: 100%;">
              <i class="el-icon-upload"></i>
              <label v-if="placeholder !== ''" style="line-height: 20px; font-size: 12px;">{{placeholder}}</label>
            </div>
            <i v-else-if="size === 'small'" class="el-icon-upload" style="font-size: 24px;"></i>
            <i v-else class="el-icon-plus" style="font-size: 18px;"></i>
          </slot>
        </template>
      </el-upload>
      <el-dialog :visible.sync="dialogVisible" top="10vh" title="图片预览" append-to-body custom-class="omp-upload-preview">
        <el-carousel :autoplay="false" height="620px" class="preview-img" ref="carouselBox" :arrow="fileList.length > 1 ? 'hover' : 'never'" :indicator-position="fileList.length > 1 ? 'outside' : 'none'">
          <el-carousel-item v-for="(file, key) in fileList" :key="key" class="text-center">
            <img height="100%" :src="file.url" :alt="file.name">
          </el-carousel-item>
        </el-carousel>
      </el-dialog>
    </div>
  </div>
</template>

<script>
import helper from '@/utils/helper'
import Sortable from 'sortablejs'

let listTypeObj = {
  'text': 'text',
  'button': 'picture',
  'image': 'picture-card'
}
export default {
  name: 'imgUploadPreview',
  components: {
  },
  data () {
    return {
      focusing: -1,
      data: {
        fileName: '',
        fileSize: 0
      },
      listType: listTypeObj[this.type],
      input: '',
      fileList: [],
      dialogVisible: false,
      baseUrl: this.$api.common.imageUrl,
      uploadUrl: `${this.$api.common.imageUrl}upload`
    }
  },
  computed: {
  },
  props: {
    value: {
      default: () => {
        return []
      },
      type: [Array, String, Object]
    },
    type: {
      default: 'image',
      type: String
    },
    size: {
      default: 'medium',
      type: String
    },
    canUpload: {
      default: false,
      type: Boolean
    },
    isPreview: {
      default: true,
      type: Boolean
    },
    limit: {
      default: () => {
        return {
          width: undefined,
          height: undefined,
          format: ['jpeg', 'png', 'gif'],
          size: undefined,
          maxNumber: 1,
          isStrictSize: false,
          isSquare: false,
          overLimitMsg: '上传文件数量超过限制'
        }
      },
      type: Object
    },
    placeholder: {
      default: '',
      type: String
    },
    outputType: {
      default: 'string',
      type: String
    }
  },
  methods: {
    handleChange (file, fileList) {
      // 判断是否为替换文件
      if (file.raw.oldFile && file.status === 'ready') {
        const oldFile = file.raw.oldFile
        fileList.splice(fileList.indexOf(oldFile), 1, file)
        fileList.pop()
      }
      this.changeCommon(file, fileList)
      if ('on-change' in this.$attrs) {
        this.$attrs['on-change'](file, fileList)
      }
    },
    handleRemove (file, fileList) {
      this.changeCommon(file, fileList)
      if ('on-remove' in this.$attrs) {
        this.$attrs['on-remove'](file, fileList)
      }
    },
    changeCommon (file, fileList) {
      // 输出上传结果
      this.fileList = fileList
      this.dragTable()
      this.handleOutput()
    },
    // 文件上传成功status = 200 的回调
    handleSuccess (response, file, fileList) {
      if (response && response.errorCode === 0) {
        let resData = response.data
        let { groupName, remoteFileName } = resData
        let { width, height } = file.raw
        file.picUrl = `/${groupName}/${remoteFileName}?width=${width}&height=${height}`
        file.url = file.picUrl
        file.remoteFileName = resData.remoteFileName
        file.groupName = resData.groupName
      } else {
        this.$message.error(`上传失败!${response.errorMsg}`)
        // 判断是否为替换,如果是替换,还原旧文件
        if (file.raw.oldFile) {
          const oldFile = file.raw.oldFile
          fileList.splice(fileList.indexOf(file), 1, oldFile)
        } else {
          fileList.splice(fileList.indexOf(file), 1)
        }
      }
      if ('on-success' in this.$attrs) {
        this.$attrs['on-success'](response, file, fileList)
      }
    },
    // 上传前判断图片类型, 尺寸,宽高
    beforeUpload (file) {
      // debugger
      // 校验
      let fileSize = file.size / 1024
      if (this.limit.format) {
        const typeList = this.limit.format.map(item => {
          item = `image/${item}`
          return item
        })
        if (!typeList.includes(file.type)) {
          this.$message.error(`上传失败:图片格式只能为${this.limit.format.join(',')}!`)
          return false
        }
      }
      if (this.limit.size && fileSize > this.limit.size) {
        this.$message.error(`上传失败:图片大小需小于${this.limit.size}k,请重新上传!`)
        return false
      }
      const isCheck = new Promise((resolve, reject) => {
        let _URL = window.URL || window.webkitURL
        let img = new Image()
        img.onload = () => {
          file.width = img.width
          file.height = img.height
          let inValid, message
          if (this.limit.isStrictSize) {
            // 严格模式下存在单边严格,所以要分成三种情况分析
            // 只有宽度严格
            if (this.limit.width && !this.limit.height) {
              inValid = img.width !== this.limit.width
              message = `图片需严格遵守 ${this.limit.width} * 高度不限制`
            } else if (!this.limit.width && this.limit.height) {
              // 只有高度严格
              inValid = img.height !== this.limit.height
              message = `图片需严格遵守 宽度不限制 * ${this.limit.height}`
            } else {
              // 全严格
              inValid = img.width !== this.limit.width || img.height !== this.limit.height
              message = `图片需严格遵守 ${this.limit.width} * ${this.limit.height}`
            }
          } else {
            // 只有宽度
            if (this.limit.width && !this.limit.height) {
              inValid = img.width > this.limit.width
              message = `图片需遵守小于 ${this.limit.width} * 高度不限制`
            } else if (!this.limit.width && this.limit.height) {
              // 只有高度
              inValid = img.height > this.limit.height
              message = `图片需遵守小于 宽度不限制 * ${this.limit.height}`
            } else {
              inValid = img.width > this.limit.width || img.height > this.limit.height
              message = `图片需遵守小于 ${this.limit.width} * ${this.limit.height}`
            }
          }
          if (this.limit.isSquare) {
            inValid = img.width !== img.height
            message = `图片需遵守长宽1:1的正方形切图`
          }
          inValid ? reject(message) : resolve()
        }
        img.src = _URL.createObjectURL(file)
      }).then(() => {
        // 构造额外请求参数
        this.data = {
          fileName: file.name,
          fileSize: file.size
        }
        return file
      }, (message) => {
        this.$message.error(`上传失败:${message}`)
        return Promise.reject(new Error('something bad happened'))
      })
      return isCheck
    },
    handlePreview (file) {
      if (this.isPreview) {
        this.dialogVisible = true
        setTimeout(() => {
          this.$refs.carouselBox.setActiveItem(this.fileList.indexOf(file))
        }, 100)
      }
      if ('on-preview' in this.$attrs) {
        this.$attrs['on-preview'](file)
      }
    },
    handleExceed (files, fileList) {
      if ('on-exceed' in this.$attrs) {
        this.$attrs['on-exceed'](files, fileList)
      } else {
        this.$message.error(`上传失败:${this.limit.overLimitMsg}`)
      }
    },
    handleChangeImg (oldFile) {
      this.focusing = false
      // 创建一个承载器
      const newInput = document.createElement('input')
      newInput.type = 'file'
      newInput.onchange = (e) => {
        let file = e.target.files[0]
        // 将旧文件信息加入新文件属性里
        file.oldFile = oldFile
        // this.clearFiles(false)
        newInput.remove()
        setTimeout(() => {
          // 加入上传文件
          this.$refs.upload.handleStart(file)
          // 开始上传
          this.$refs.upload.submit()
        }, 100)
      }
      newInput.click()
    },
    handleProgress (event, file, fileList) {
      event.percent = 0
      if (file.percentage === undefined) {
        file.percentage = 0
      }
      return this.changeProgress(file)
    },
    changeProgress (file) {
      setTimeout(() => {
        file.percentage = file.percentage + 10
        if (file.percentage < 100) {
          this.changeProgress(file)
        } else {
          file.percentage = 100
        }
        return file
      }, (file.size / 1024) / 10)
    },
    handleOutput () {
      const fileList = helper.objDeepClone(this.fileList)
      const files = []
      // 如果是正在上传的图片, 不传值
      if (fileList.length !== 0 && fileList.filter(item => item.status === 'success').length !== fileList.length) return
      fileList.forEach(item => {
        files.push({
          groupName: item.groupName,
          picUrl: item.picUrl,
          remoteFileName: item.remoteFileName,
          size: item.size,
          width: item.raw ? item.raw.width : item.width,
          height: item.raw ? item.raw.height : item.height
        })
      })
      if (this.outputType === 'string') {
        this.$emit('input', files.length === 0 ? '' : files[0].picUrl ? files[0].picUrl : '')
      } else if (this.outputType === 'object') {
        this.$emit('input', files.length === 0 ? {} : files[0])
      } else if (this.outputType === 'array') {
        this.$emit('input', files)
      }
    },
    handleDelete (file) {
      this.fileList.splice(this.fileList.indexOf(file), 1)
      this.handleRemove(file, this.fileList)
    },
    clearFiles (boolean = true) {
      this.fileList = []
      this.$refs.upload.clearFiles()
      if (boolean) {
        this.handleOutput()
      }
    },
    abort (file) {
      this.$refs.upload.abort(file)
    },
    submit () {
      this.$refs.upload.submit()
    },
    // 拖拽
    dragTable () {
      if (this.$el) {
        const el = this.$el.querySelector('.el-upload-list.el-upload-list--picture-card')
        if (el !== null) {
          Sortable.create(el, {
            animation: 180,
            delay: 0,
            onEnd: ({ newIndex, oldIndex }) => {
              if (newIndex !== oldIndex) {
                const currRow = this.fileList.splice(oldIndex, 1)[0]
                this.fileList.splice(newIndex, 0, currRow)
                this.handleOutput()
              }
            }
          })
        }
      }
    }
  },
  created () {
    // this.baseUrl = this.$api.common.imageUrl
    // this.uploadUrl = this.baseUrl + 'upload'
  },
  mounted () {
    // 开启排序功能
    if (this.canUpload) {
      setTimeout(() => {
        this.dragTable()
      }, 500)
    }
  },
  watch: {
    value: {
      handler (newName, oldName) {
        this.fileList = []
        if (helper.isString(this.value) && this.value.length !== 0) {
          this.fileList = [{
            picUrl: this.value,
            url: this.baseUrl + this.value,
            status: 'success'
          }]
        } else if (helper.isObject(this.value) && JSON.stringify(this.value) !== '{}') {
          const object = helper.objDeepClone(this.value)
          object.url = this.baseUrl + this.value.picUrl
          object.status = 'success'
          this.fileList = [object]
        } else if (helper.isArray(this.value) && this.value.length !== 0) {
          const array = helper.objDeepClone(this.value)
          this.fileList = array.map(item => {
            item.url = this.baseUrl + item.picUrl
            item.status = 'success'
            return item
          })
        }
      },
      immediate: true,
      deep: true
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus">
.img-upload-preview
  position relative
  .no-image
    position relative
    background-color #fbfdff
    border 1px dashed #c0ccda
    border-radius 6px
    vertical-align top
    display inline-block
    &.big
      width 148px
      height 148px
      line-height 148px
    &.medium
      width 120px
      height 120px
      line-height 120px
    &.small
      width 86px
      height 86px
      line-height 86px
    &.mini
      width 53px
      height 53px
      line-height 53px
  .single-image-box
    position relative
    margin 5px !important
    background rgba(0, 0, 0, 0.3) !important
    border 1px dashed #c0ccda
    border-radius 6px
    -webkit-box-sizing border-box
    box-sizing border-box
    cursor pointer
    vertical-align top
    overflow hidden
    &.focusing
      .single-image-box__actions
        opacity 1
    &.big
      width 148px
      height 148px
      line-height 148px
    &.medium
      width 120px
      height 120px
      line-height 120px
    &.small
      width 86px
      height 86px
      line-height 86px
    &.mini
      width 53px
      height 53px
      line-height 53px
      .single-image-box__actions
        font-size 14px
    .single-image-progress
      display flex
      justify-content center
      align-items center
      position absolute
      top 50%
      left 50%
      transform translate(-50%, -50%)
    .single-image-box__actions
      position absolute
      width 100%
      height 100%
      left 0
      top 0
      cursor default
      text-align center
      color #fff
      opacity 0;
      font-size 20px
      background-color rgba(0, 0, 0, 0.5)
      transition opacity .3s
      z-index 10
    .single-image-box__changes
      font-size 12px
      height 30px
      line-height 30px
      display block
      position absolute
      bottom 0
      width 100%
      text-align center
      background rgba(0, 0, 0, 0.65)
      color #fff
      cursor pointer
      z-index 99
    img
      width 100%
      height 100%
      position relative
      object-fit contain
      object-position center
      vertical-align middle
      border-style none
      background rgba(0, 0, 0, 0.3)
      margin-top -4px
  .omp-upload-img
    display inline-block
    margin 5px
    .upload-box
      &.big
        .el-upload--picture-card
          width 148px
          height 148px
          line-height 148px
        .el-upload-list__item
          width 148px
          height 148px
      &.medium
        .el-upload--picture-card
          width 120px
          height 120px
          line-height 120px
        .el-upload-list__item
          width 120px
          height 120px
      &.small
        .el-upload--picture-card
          width 86px
          height 86px
          line-height 86px
        .el-upload-list__item
          width 86px
          height 86px
      &.mini
        .el-upload--picture-card
          width 53px
          height 53px
          line-height 53px
        .el-upload-list__item
          width 53px
          height 53px
          .el-upload-list__item-preview, .el-upload-list__item-delete
            font-size 16px
            margin 0 2px
          .el-icon-close-tip
            display none
    /* 屏蔽上传功能 */
    &.no_upload
      margin 0
      .el-upload--picture-card
        display none
    /* 屏蔽上传成功标签 */
    &.is_preview
      .el-upload-list__item
        &:hover
          .el-upload-list__item-status-label
            display none
        .el-upload-list__item-status-label
            display none
.omp-upload-preview
  .preview-img
    overflow hidden
    img
      width 100%
      height 560px
      position relative
      z-index 10
      object-fit contain
      object-position center
      vertical-align middle
      border-style none
      background rgba(0, 0, 0, 0.3)
</style>


组件使用

<img-upload-preview
    style="text-align: center;"
    type="image"
    size="medium"
    :canUpload="true"
    :isPreview="true"
    v-model="form.skuHeadPic"
    :limit="{
      width: undefined,
      height: undefined,
      size: undefined,
      maxNumber: 1,
      format: ['jpeg', 'png','gif'],
      isStrictSize: false,
      isSquare: false,
      overLimitMsg: '上传文件数量超过限制'
    }"
    placeholder="请上传商品头图"
    outputType="object"
    >
 </img-upload-preview>

前端如何实现水印生成

话不多说直接上代码:

/**
 * renderWaterMark
 * 生成水印
 * @param {Object}
 * */
export function renderWaterMark({
  container = document.body,
  width = "100px",
  height = "100px",
  textAlign = "center",
  textBaseline = "middle",
  font = "20px microsoft yahei",
  fillStyle = "rgba(184, 184, 184, 0.1)",
  content = "请勿外传",
  rotate = "30",
  zIndex = 9999
} = {}) {
  const canvas = document.createElement("canvas");
  canvas.setAttribute("width", width);
  canvas.setAttribute("height", height);
  const ctx = canvas.getContext("2d");

  ctx.textAlign = textAlign;
  ctx.textBaseline = textBaseline;
  ctx.font = font;
  ctx.fillStyle = fillStyle;
  ctx.rotate((Math.PI / 180) * rotate);
  ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);

  const base64Url = canvas.toDataURL();
  const renderWaterMarkDiv = document.createElement("div");
  renderWaterMarkDiv.id = "waterMark";
  renderWaterMarkDiv.setAttribute(
    "style",
    `
          position:absolute;
          top:0;
          left:0;
          width:100%;
          height:100%;
          z-index:${zIndex};
          pointer-events:none;
          background-repeat:repeat;
          background-image:url('${base64Url}')`
  );

  // container.style.position = 'relative';
  container.insertBefore(renderWaterMarkDiv, container.firstChild);
}


表格可拖拽排序

业务中碰到这样一个需求. 点击按钮拖拽排序后, el-table就进入了可拖拽的模式.

稍加百度了一下.发现了一个可以用的插件. sortable.js
下载插件 npm install sortable.js --save (下载的时候一定要这样去下载,不要去下载 npm install sortable--save )
因为sortable.js和sortable是不一样的哈
引入 import Sortable from 'sortablejs';
// 千万不要加点.js 否者就会报错create不是一个方法

  // 拖拽表格
    dragTable () {
      const tbody = document.querySelector('.el-table__body-wrapper tbody')
      const _this = this
      Sortable.create(tbody, {
        handle: '.allowDrag',
        onEnd ({newIndex, oldIndex}) {
          const dragTable = _this.tableData.splice(oldIndex, 1)[0]
          _this.tableData.splice(newIndex, 0, dragTable)
        }
      })
    },
  // mouted 时挂载
    mounted () {
    this.dragTable()
  },
这样表格就能拖拽了.  但刚开始的时候 这种操作和需求并不算匹配,因为页面挂载好了 就会自动变成可拖模式,
而需求希望点击排序才可以拖拽. 思考之后, 决定给表格column加个类名 allowDrag
        <el-table-column
            label="序号"
            type="index"
            head-align="center"
            align="center"
            width="120"
            :class-name="sorting? 'allowDrag':''"
        ></el-table-column>
通过点击按钮控制sorting字段来控制类.这样就可以达到效果了.

最后值得一提的element table务必指定row-key,row-key必须是唯一的,如ID,不然会出现排序不对的情况。
      <el-table
          v-loading="tableLoading"
          :data="tableData"
          :row-style="rowStyle"
          row-key="id"
          :height="scope.height"
      >

扩展: 如果要做列拖拽的话

    //列拖拽
    columnDrop() {
      const wrapperTr = document.querySelector('.el-table__header-wrapper tr')
      this.sortable = Sortable.create(wrapperTr, {
        animation: 180,
        delay: 0,
        onEnd: evt => {
          const oldItem = this.dropCol[evt.oldIndex]
          this.dropCol.splice(evt.oldIndex, 1)
          this.dropCol.splice(evt.newIndex, 0, oldItem)
        }
      })
    }

简单表格封装组件

如果是在做一些后台管理同学,经常会写表格
而大多情况下的表格比较简单, 就是普通的el-table + el-table-column 为了避免写太多的重复代码.可以稍加进行封装

<template>
  <el-table :data="data" v-on="$listeners" v-bind="$attrs" class="c-simple-table">
    <el-table-column
      v-if="selectionColumn"
      type="selection"
    >
    </el-table-column>
    <el-table-column
      v-if="showIndex"
      type="index"
      width="50"
      label="序号"
    >
    </el-table-column>
    <el-table-column v-bind="head.props" v-on="head.on" :key="index" v-for="(head, index) in headData"
      :label="head.label"
      :prop="head.prop"
      :min-width="head.width"
      :align="head.align || 'left'"
      :sortable="head.sortable"
    >
      <template slot-scope="scope">
        <template v-if="!$scopedSlots[head.prop]" class="cell-content">{{scope.row[head.prop] !== '' ? scope.row[head.prop] : '--'}}</template>
        <slot v-else :name="head.prop" v-bind="scope"></slot>
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  name: 'simple-table',
  data () {
    return {
    }
  },
  props: {
    data: Array,
    headData: Array,
    showIndex: {
      type: Boolean,
      default: false
    },
    selectionColumn: {
      type: Boolean,
      default: false
    }
  }
}
</script>

封装的思路也非常简单, 通过两个字段来控制是否需要selectiomColumn勾选框以及序号showIndex
对于表头数据传递一个数组headData来进行循环渲染.表格内容给一个插槽

这样可以通过配置headData数据
headData: [
{ label: '已关联品类', prop: 'name', width: '60%', align: 'center' },
{ label: '已关联商品数量', prop: 'count', width: '20%', align: 'center' },
{ label: '操作', prop: 'operation', width: '20%', align: 'center' }
],

封装一个按住Crtl显示气泡的组件

神奇的需求年年有, 当然是基于el-tooltip了 直接上代码:

留一个自定义插槽嵌入 html格式的提示内容
留一个默认插槽作为显示提示的载体
用一个input来获取焦点 注册事件判断是否按下了键盘ctrl
通过鼠标进入和离开时 改变判断字段的值. 需要鼠标移入且键盘被按下才触发
// 鼠标移向并按ctrl键时 气泡提示
<template>
  <el-tooltip v-bind="$attrs" :content="content" placement="top" :value="showTooltip" :manual="true" class="tooltip-wrap">
    <!-- 嵌入html格式的提示内容 -->
    <div slot="content" v-if="!content">
      <slot name="content"></slot>
    </div>
    <div @mouseenter="mouseoverHandle" @mouseleave="mouseleaveHandle">
      <slot></slot>
      <i class="icon el-icon-info" v-if="showIcon"></i>
      <!-- 不显示 为了使页面获得焦点 -->
      <input type="text" ref="tooltipInput" class="tooltip-input">
    </div>
  </el-tooltip>
</template>

<script>
export default {
  props: {
    content: String,
    showIcon: {
      type: Boolean,
      default: true
    }
  },
  data () {
    return {
      showTooltip: false,
      isMouseover: false
    }
  },
  methods: {
    mouseoverHandle () {
      this.isMouseover = true
      this.showTooltip = window.event.ctrlKey
    },
    mouseleaveHandle () {
      this.isMouseover = false
      this.showTooltip = false
    }
  },
  mounted () {
    // 使当前页面获得焦点,否则监听keydown事件不触发
    this.$refs['tooltipInput'].focus()
    window.addEventListener('keydown', (e) => {
      if (e.ctrlKey && this.isMouseover) {
        this.showTooltip = true
      } else {
        this.showTooltip = false
      }
    }, false)
    window.addEventListener('keyup', (e) => {
      this.showTooltip = false
    }, false)
  }
}
</script>

<style lang="stylus">
.tooltip-wrap
  display inline
  .tooltip-input
    position: absolute
    width 0
    height 0
    padding 0
    border 0
.tooltip-middle
  max-width 250px!important
</style>

组件使用:

<ctrl-tooltip content="操作后,可实时向erp系统同步一次全量的库存数据" :showIcon="false">
  <el-button @click="syncStock" :disabled="!stockDisabled">同步库存</el-button>
</ctrl-tooltip>
或者这样
  <div>
        领取限制:<el-checkbox v-model="moduleItem.activityCouponList[0].limitType" true-label="1" false-label="0">
        <ctrl-tooltip :showIcon="false">
             <template slot="content">
                  <div>不勾选,活动周期内,每用户只能领取一次</div>
                  <div>勾选后,活动周期内,每用户每天可领一次</div>
              </template>
              1人1天限领取1次
         </ctrl-tooltip>
        </el-checkbox>
</div>

组件循环引用导致的组件加载失败问题

记一次组件导入报错的问题

在开发需求的过程,需要在一个新建页面的保存时, 点击按钮跳转到对应的详情页.
但是在实习操作中发现点击这个按钮唤起组件时浏览器却报错了.

SelectTypeAndCatalogDialog.vue (这个页面是新建工单的页面
    <!-- 工单详情 -->
    <work-order-detail-dialog
      :visible.sync="workOrderDetail.visible"
      :id="workOrderDetail.id"
      @submit-success="handleClose"
    />
import WorkOrderDetailDialog from "@/components/WorkOrderDetailDialog";
调用这个工单详情页面的组件
等点击唤起组件时发现浏览器报错了

avatar
乍一看这个报错,我还以为是因为组件的驼峰没有写好
注意: vue在导入组件的时候, 组件名称一定要为驼峰形式, 组件在标签使用时用-隔开,很多时候这个报错都是因为命名造成的
不过很可惜 这里并不是因为命名, 在检查了好几篇之后用打印的方式尝试看看
在SelectTypeAndCatalogDialog.vue这个页面中打印 导入的WorkOrderDetailDialog发现确实为undefined
于是往上层组件开始寻找问题:

在allOrder.vue文件中(对于其他组件来说算是父级组件 全部工单列表)
      <!-- 添加工单 -->
      <select-type-and-catalog-dialog
        :visible.sync="addOrder.visible"
        @submit-success="initTableData"
      />
   
      <!-- 工单详情弹窗 -->
      <work-order-detail-dialog
        :id="workOrderDetail.id"
        :visible.sync="workOrderDetail.visible"
        @submit-success="fetchTableData"
      />
import WorkOrderDetailDialog from "@/components/WorkOrderDetailDialog";
import SelectTypeAndCatalogDialog from "@/components/SelectTypeAndCatalogDialog";
导入了这两个组件

发现在父组件中同时使用了这个两个组件
简单理解为 a(父) b(子) c(子) a调用了bc b调用了c, 发现WorkOrderDetailDialog其实在父组件和兄弟组件中都有调用
考虑可能是和引用的时候出错有关.仔细把两个组件都捋顺了一遍, 发现其实在WorkOrderDetailDialog组件中有调用一个panel组件
而在panel组件中又调用了SelectTypeAndCatalogDialog组件. 哦豁, 循环引用! 有问题!
整理整个组件调用的情况 得到如下图:
avatar
实际上, WorkOrderDetailDialog作为工单详情页还有递归调用的情况, 经过排查其实和递归组件问题无关,所以这里暂时就不做展开.

那么相互引用会存在什么问题呢?
这里就涉及到es6的import模块相互引用的问题:
这里引用阮一峰老师的例子:

//a.js
console.log("before import b")
import {b} from "./b"
console.log("b is " + b)
export let a = b+1;

//b.js
console.log("before import a")
import {a} from "./a"
console.log("a is " + a)
export let b = a+1;

输出:

before import a
a is undefined
before import b
b is NAN

这里有一个有趣的现象就是第一句输出并不是before import b,也就是虽然import语句在后面,但确会更早执行,当执行import b时,加载并运行b.js,从而第一句输出是before import a。
然后就是当运行b.js时,发现又需要import a.js,此时不会再去加载a.js了,而是认为整个a.js模块是{},所以a的值就是undefined了

那么同理 对于我所遇到的问题:
SelectTypeAndCatalogDialog中有调用WorkOrderDetailDialog
WorkOrderDetailDialog中有调用SelectTypeAndCatalogDialog

从项目结构来看, WorkOrderDetailDialog就相当于是a.js中的a了
所以在SelectTypeAndCatalogDialog中我们就取不到WorkOrderDetailDialog了

遇到了问题, 那应该怎么解决呢:
目前想到了三种解决方案并都测试有效:

  1. 简单粗暴, 注册这个详情页的弹窗WorkOrderDetailDialog为全局组件, 这样就可以调用到了不存在import的导入undefined问题
  2. 在SelectTypeAndCatalogDialog里面不去套WorkOrderDetailDialog组件了, 只要用$emit去触发父级组件的值来动态展示,
    这个方法的问题是如果有很多个页面都存在这种父级同时使用两个组件,要改很多地方,但是如果早期规划的时候考虑到不写这么多嵌套是可以考虑的.
  3. 不用import, 在局部组件components祖册的时候使用函数式WorkOrderDetailDialog: ()=> import(@/components/WorkOrderDetailDialog) 函数式的好处是,不像import那样在加载页面的时候就导入,而是在使用的时候才去加载这个组件,所以就没有了进入页面时的循环引入的问题.

好了这个问题的解决也让我加深了对循环引入的理解,受益良多 bye

input文件上传同名缓存问题

先抛问题:
在开发的时候,一个上传添加文件的组件中,如果上传一次文件报错了, 修改这个excel的内容再次上传,发现读的还是缓存里的老数据,新的文件并没有上传.所以导致一个文件上传错误的时候,修改excel内容后再次上传还是会报一样的错误. 或者上传成功的时候,如果修改文件内容但没有换名字再次上传其实还是用的之前的数据, 这存在很大的风险隐患.
定位问题是因为同名文件上传时导致的,

<template>
  <span style="margin-right:10px">
    <input
      class="input-file"
      type="file"
      @change="exportData"
      accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
    />
    <el-button
      type="primary"
      size="mini"
      icon="el-icon-upload"
      @click="btnClick"
      >浏览</el-button
    >
    <!-- 上传Excel表格数据 -->
  </span>
</template>

上传组件代码如上, 这里用的是原生的input调用change事件

    exportData(event) {
      this.file_name = event.target.files[0].name;
      if (!event.currentTarget.files.length) {
        return;
      }
      const that = this;
      // 拿取文件对象
      var f = event.currentTarget.files[0];
      this.file_name = event.target.files[0].name;
      // 用FileReader来读取
      var reader = new FileReader();
      // 重写FileReader上的readAsBinaryString方法
      FileReader.prototype.readAsBinaryString = function(f) {
        var binary = "";
        var wb; // 读取完成的数据
        var outdata; // 你需要的数据
        var reader = new FileReader();
        reader.onload = function() {
          // 读取成Uint8Array,再转换为Unicode编码(Unicode占两个字节)
          var bytes = new Uint8Array(reader.result);
          var length = bytes.byteLength;
          for (var i = 0; i < length; i++) {
            binary += String.fromCharCode(bytes[i]);
          }
          // 接下来就是xlsx了,具体可看api
          try {
            wb = XLSX.read(binary, {
              type: "binary",
            });
            outdata = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
            // 自定义方法向父组件传递数据
            that.$emit("getResult", {
              outdata,
              file_name: that.file_name,
            });
          } catch (error) {
            that.$message.warning("exel 解析失败");
          }
        };
        reader.readAsArrayBuffer(f);
      };
      reader.readAsBinaryString(f);
    },
  },

上传成功后利用 XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); 转换为输出数据返给父级

点击浏览

 btnClick() {
      // 点击事件
      const fileDom = document.querySelector(".input-file");
      fileDom.value = '';
      fileDom.click();
    },
在这里触发点击input来上传
fileDom 里面如果上传过了数据其实是有会有一个属性 files里面存放文件
同时fileDom.value暴露的是一个文件路径 c:/xxxxxxx/xxxx
所以 当文件名没有变化的时候,这个路径是没有变化的, 所以fileDom.value也没有变化 这样就不会触发
onchange事件所以导致用的还是之前的fileList.
这里处理起来其实很简单只要加上fileDom.value = ''; 让input触发change事件就可以了

问题虽然很简单,但是定位的过程和分析产生的原因还是有些受益.

keep-alive 前进刷新后退缓存

keep-alive

实际工作中,常常会遇到这样的需求, 在列表页进入详情页或者编辑页的时候, 再返回列表页,如果没有做缓存操作的话,可能会导致表单查询的条件被清空, 需要重新选择筛选条件. 所以这个时候需要缓存, 列表页.如何实现呢:

route/ index.js
举例
可以假设一个缓存对象, 对于这个缓存对象来说 key代表的是列表路由的名称
value 对应的 是详情页或者编辑页的路由名数组
同时 写一个方法用于翻转这个对象

let cacheObj = {
  'goodsSku': ['skuEdit', 'relevantCity', 'tagManage', 'skuAdd', 'cityRelevantCategory', 'cityRelevantStore',
    'cityRelevantGoods'],
  'bgxxGoods': ['bgxxEdit', 'relevantCity', 'bgxxAdd', 'cityRelevantCategory', 'cityRelevantStore',
    'cityRelevantGoods', 'mixGoodsEdit', 'relevantBgxxGoods']\
}
let reverseObj = (obj) => {
  let newObj = {}
  for (let [key, value] of Object.entries(obj)) {
    value.forEach((item) => {
      if (newObj[item]) {
        newObj[item].push(key)
      } else {
        newObj[item] = [key]
      }
    })
  }
  return newObj
}
let reverseCacheObj = reverseObj(cacheObj)
// 翻转结果如下例子: 
{ skuEdit: [ 'goodsSku' ],
  relevantCity: [ 'goodsSku' ],
  tagManage: [ 'goodsSku' ],
  skuAdd: [ 'goodsSku' ],
  cityRelevantCategory: [ 'goodsSku' ],
  cityRelevantStore: [ 'goodsSku' ],
  cityRelevantGoods: [ 'goodsSku' ] }

let router = new Router({
  routes: Routes
})

router.beforeEach((to, from, next) => {
  // 开始进入页面默认缓存
  if (Object.keys(cacheObj).indexOf(to.name) !== -1) {
    store.commit('addCachedViews', to.name)
  }
  // 离开判断,不是去其所属页面则去除缓存
  if (Object.keys(cacheObj).indexOf(from.name) !== -1) {
    if (cacheObj[from.name].indexOf(to.name) !== -1) {
      store.commit('addCachedViews', from.name)
    } else {
      store.commit('delCachedViews', from.name)
    }
  }
  // 反向判断,其所属页面进入其所属页面其他页面或者其页面以外页面则清楚缓存
  if (Object.keys(reverseCacheObj).indexOf(from.name) !== -1) {
    let cacheName = reverseCacheObj[from.name]
    if (cacheName.indexOf(to.name) === -1) {
      cacheName.forEach((name) => {
        if (cacheObj[name].indexOf(to.name) === -1) {
          store.commit('delCachedViews', name)
        }
      })
    }
  }
  next()
})

export default router
---------------------------
layout.vue
    <div class="c-app-content" >
      <keep-alive :include="cachedViews">
        <router-view></router-view>
      </keep-alive>
    </div>

  watch: {
    $route (val) {
      // 一直需要缓存的组件
      if (val.meta.alwaysAlive) {
        this.$store.commit('addCachedViews', val.name)
      }
      this.getBreadcrumb()
    }
  },
  computed: {
    cachedViews () {
      return this.$store.state.cachedViews
    }
  },
  ----------------------------
  store/index.js
  
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    cachedViews: []
  },
  mutations: {
    addCachedViews (state, view) {
      if (state.cachedViews.indexOf(view) !== -1) return
      state.cachedViews.push(view)
    },
    delCachedViews (state, view) {
      for (const i of state.cachedViews) {
        if (i === view) {
          const index = state.cachedViews.indexOf(i)
          state.cachedViews.splice(index, 1)
        }
      }
    }
  }
})

git rebase实际使用

一个版本的需求开发中 commit 提交次数过多
这样会显得提交记录非常的冗余 所以可以采用git rebase进行变基操作
具体是怎么使用呢

可以先使用 git log查看记录 或者指定对应数量的log  git log -10
找到log 到对应的commitid (你想要更替的记录再之前的一条的commitid

拿到commitid之后使用
git rebase -i  (commitid
进入编辑页面------ 编辑之前commit 提供的语句:
------
上面未被注释的部分列出的是我们本次rebase操作包含的所有提交,下面注释部分是git为我们提供的命令说明。
每一个commit id 前面的pick表示指令类型,git 为我们提供了以下几个命令:
  pick:保留该commit(缩写:p)
  reword:保留该commit,但我需要修改该commit的注释(缩写:r)
  edit:保留该commit, 但我要停下来修改该提交(不仅仅修改注释)(缩写:e)
  squash:将该commit和前一个commit合并(缩写:s)
  fixup:将该commit和前一个commit合并,但我不要保留该提交的注释信息(缩写:f)
  exec:执行shell命令(缩写:x)
  drop:我要丢弃该commit(缩写:d)
------
修改之后输入:wq退出编辑

最后git push --force 一定要强行推才能清掉之前commit记录 不然就推不上去
如果使用了git pull 之前的变基操作就白费了

微信小程序图片懒加载

最简单的实现方式应该是通过微信自带的api createIntersectionObserver
通过判断intersectionRatio 是否大于0 大于代表图片进入了可视区然后通过 imgshow字段决定图片是否展示

<view wx:for="{{ImgData}}">
	<img class="img-{{index}}" wx:if={{item.ingShow}}></img>
</view>
ImgData.forEach((item,index)=>{
    this.createIntersectionObserver().relativeToViewport.observe(`.img-${index}`,res=>{
        if (res.intersectionRatio > 0){
            this.setData({
                item.imgShow:true
            })
        }
    })
})

这种方法虽然快捷 但是也是有弊端的.弊端通过循环判断条件,当条件符合的时候就setData一次这样反而适得其反,导致性能变差
更可能需要加上节流来优化 但效果也并不是最佳

或者也可以用小程序获取到用户手机系统的高度,通过判断节点的top小于手机系统的高度用来确定页面的内容是否在显示的区域里,然后实现懒加载效果。

  <image class = "img {{item.showImg? 'active':''}}" wx:for = '{{img}}' mode = 'widthFix' src="{{item.showImg ? item.src:item.lazy }}"  wx:key='index' iid= 'img'></image>
 onLoad: function (options) {
    let that = this;
    wx.getSystemInfo({
      success: res => {
        that.data.height = res.screenHeight
      }
    })
  },
  onReady: function () {
    let imglist = this.data.img
    let that = this;
    wx.createSelectorQuery().selectAll('#img').boundingClientRect(img => {
      img.map((item, index) => {//通过节点选择器获取到id img标签的内容并且绑定了事件
        if (item.top <= that .data.height) {//通过判断判断节点的高度是否小于视图区域动态添加showImg属性
          imglist[index].showImg = true
        } else {
          imglist[index].showImg = false
        }
        if (item.bottom == 0) {
        ///因为我的页面效果是三张图片并排显示,为了避免最后一排图片没有效果所以这里判断当是最后一组图片的时候修改其属性
          imglist[length - 1].showImg = true
          imglist[length - 2].showImg = true
          imglist[length - 3].showImg = true
        }
        that.setData({
          img: imglist
        })
      })
    }).exec() 
  },
  onPageScroll(e){
    this.onReady()
  },

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.