vue 图片裁剪组件

简介

图片裁剪组件同上传图片组件一样,用的较多,这里主要用的是 vue-cropper 这个包,功能支持裁剪图片文件类型检验,图片大小检验,图片分辨率校验以及图片比列校验等功能。

主要依赖说明 (先安装,步骤略)

1
{
2
  "element-ui": "2.11.1",
3
  "vue": "^2.6.10",
4
  "vue-router": "^3.0.1",
5
  "vue-cropper": "^0.4.7"
6
}

正文

1.组件

src/components/Cropper.vue

1
<template>
2
  <div class="custom-upload">
3
    <el-dialog
4
      title="图片裁剪"
5
      :visible.sync="showCropper"
6
      top="6vh"
7
      width="50%"
8
      height="600"
9
      class="cropper-dialog"
10
      center
11
      append-to-body
12
    >
13
      <vue-cropper
14
        v-if="showCropper"
15
        id="corpper"
16
        ref="cropper"
17
        :class="{'corpper-warp':showCropper}"
18
        v-bind="cropper"
19
      />
20
      <div v-if="showCropper" class="cropper-button">
21
        <el-button
22
          class="cancel-btn"
23
          size="small"
24
          @click.native="showCropper=false"
25
          >取消</el-button
26
        >
27
        <el-button
28
          size="small"
29
          type="primary"
30
          :loading="loading"
31
          @click="uploadCover"
32
          >完成</el-button
33
        >
34
      </div>
35
    </el-dialog>
36
    <input
37
      :id="id"
38
      type="file"
39
      style="display: none"
40
      name="single"
41
      accept="image/*"
42
      @change="onChange($event)"
43
    />
44
45
    <el-button
46
      size="small"
47
      type="primary"
48
      :loading="loading"
49
      @click="handleOpenFile()"
50
    >
51
      <i class="fa fa-upload" />
52
      {{ buttonName }}
53
    </el-button>
54
    <div v-if="tips" class="tips clear-margin-top">{{ tips }}</div>
55
  </div>
56
</template>
57
58
<script>
59
  // 上传文件组件
60
  import { VueCropper } from "vue-cropper";
61
62
  // 定义的接口根据自己项目更换
63
  import { uploadImage } from "@/api/upload";
64
65
  import { isImageFile, isMaxFileSize, readFile } from "@/utils/upload"; // 见下文
66
  import { Message } from "element-ui";
67
68
  export default {
69
    components: {
70
      VueCropper
71
    },
72
    props: {
73
      // 最大上传文件的大小
74
      maxFileSize: {
75
        type: Number,
76
        default: 2 // (MB)
77
      },
78
      // 按钮文字
79
      buttonName: {
80
        type: String,
81
        default: "添加图片"
82
      },
83
      // 提示内容
84
      tips: {
85
        type: String
86
      },
87
      // 图片裁剪比列
88
      fixedNumber: {
89
        type: Array,
90
        default: function() {
91
          return [];
92
        }
93
      },
94
      // 图片文件分辨率的宽度
95
      width: {
96
        type: Number,
97
        default: 460
98
      },
99
      // 图片文件分辨率的高度
100
      height: {
101
        type: Number,
102
        default: 300
103
      }
104
    },
105
    data() {
106
      return {
107
        id: "cropper-input-" + +new Date(),
108
        loading: false,
109
        showCropper: false,
110
        cropper: {
111
          img: "",
112
          info: true,
113
          size: 0.9,
114
          outputType: "png",
115
          canScale: true,
116
          autoCrop: true,
117
          full: true,
118
          // 只有自动截图开启 宽度高度才生效
119
          autoCropWidth: this.width,
120
          autoCropHeight: this.height,
121
          fixedBox: false,
122
          // 开启宽度和高度比例
123
          fixed: true,
124
          fixedNumber: this.fixedNumber,
125
          original: false,
126
          canMoveBox: true,
127
          canMove: true
128
        }
129
      };
130
    },
131
    methods: {
132
      // 打开文件
133
      handleOpenFile() {
134
        const input = document.getElementById(this.id);
135
        // 解决同一个文件不能监听的问题
136
        input.addEventListener(
137
          "click",
138
          function() {
139
            this.value = "";
140
          },
141
          false
142
        );
143
        // 点击input
144
        input.click();
145
      },
146
147
      // 裁剪input 监听
148
      async onChange(e) {
149
        const file = e.target.files[0];
150
        if (!file) {
151
          return Message.error("选择图片失败");
152
        }
153
        // 验证文件类型
154
        if (!isImageFile(file)) {
155
          return;
156
        }
157
        try {
158
          // 读取文件
159
          const src = await readFile(file);
160
          this.showCropper = true;
161
          this.cropper.img = src;
162
        } catch (error) {
163
          console.log(error);
164
        }
165
      },
166
167
      // 封面上传功能
168
      uploadCover() {
169
        this.$refs.cropper.getCropBlob(async imgRes => {
170
          try {
171
            // 文件大小限制
172
            if (!isMaxFileSize(imgRes, this.maxFileSize)) {
173
              return;
174
            }
175
            this.loading = true;
176
            const url = await uploadImage(imgRes);
177
            this.$emit("subUploadSucceed", url);
178
            Message.success("上传成功");
179
            this.loading = false;
180
            this.showCropper = false;
181
          } catch (error) {
182
            this.loading = false;
183
            this.showCropper = false;
184
            Message.error(error.data.message);
185
          }
186
        });
187
      }
188
    }
189
  };
190
</script>
191
192
<style lang="scss">
193
  #corpper {
194
    width: 90%;
195
    height: 400px;
196
    margin: 0 auto;
197
    background-image: none;
198
    background: #fff;
199
    z-index: 1002;
200
  }
201
  .cropper-dialog {
202
    height: 800px;
203
    text-align: center;
204
    .el-dialog__header {
205
      padding-top: 15px;
206
    }
207
    .el-dialog--center .el-dialog__body {
208
      padding-top: 0;
209
      padding-bottom: 15px;
210
    }
211
    .el-dialog {
212
      text-align: center;
213
    }
214
  }
215
  .cropper-button {
216
    z-index: 1003;
217
    text-align: center;
218
    margin-top: 20px;
219
    .el-button {
220
      font-size: 16px;
221
      cursor: pointer;
222
      text-align: center;
223
    }
224
    .cancel-btn {
225
      color: #373737;
226
    }
227
    .el-button:last-child {
228
      margin-left: 100px;
229
    }
230
  }
231
  .cropper-modal {
232
    background-color: rgba(0, 0, 0, 0.5) !important;
233
  }
234
  .custom-upload {
235
    .tips {
236
      margin-top: 10px;
237
      color: red;
238
      font-size: 12px;
239
    }
240
    .clear-margin-top {
241
      margin-top: 0;
242
    }
243
  }
244
</style>

2.使用

1
<template>
2
  <div v-if="url">
3
    <img :src="url" height="160" />
4
  </div>
5
  <div>
6
    <App-cropper
7
      :width="300"
8
      :height="300"
9
      :fixed-number="[1,1]"
10
      @subUploadSucceed="getShopImages"
11
    />
12
  </div>
13
</template>
14
15
<script>
16
  import AppCropper from "@/components/Cropper";
17
  export default {
18
    name: "GoodsForm",
19
    components: {
20
      AppCropper
21
    },
22
    data() {
23
      return {
24
        url: ""
25
      };
26
    },
27
    methods: {
28
      // 海报上传成功
29
      handleUploadSucceed(url) {
30
        this.url = url;
31
      }
32
    }
33
  };
34
</script>

3.补充 src/utils/upload.js 文件

1
import { Message } from "element-ui";
2
3
/**
4
 *
5
 * @param {file} file 源文件
6
 * @desc 限制为图片文件
7
 * @retutn 是图片文件返回true否则返回false
8
 */
9
export const isImageFile = (file, fileTypes) => {
10
  const types = fileTypes || [
11
    "image/png",
12
    "image/gif",
13
    "image/jpeg",
14
    "image/jpg",
15
    "image/bmp",
16
    "image/x-icon"
17
  ];
18
  const isImage = types.includes(file.type);
19
  if (!isImage) {
20
    Message.error("上传文件非图片格式!");
21
    return false;
22
  }
23
24
  return true;
25
};
26
27
/**
28
 *
29
 * @param {file} file 源文件
30
 * @param {number} fileMaxSize  图片限制大小单位(MB)
31
 * @desc 限制为文件上传大小
32
 * @retutn 在限制内返回true否则返回false
33
 */
34
export const isMaxFileSize = (file, fileMaxSize = 2) => {
35
  const isMaxSize = file.size / 1024 / 1024 < fileMaxSize;
36
  if (!isMaxSize) {
37
    Message.error("上传头像图片大小不能超过 " + fileMaxSize + "MB!");
38
    return false;
39
  }
40
  return true;
41
};
42
43
/**
44
 *
45
 * @param {file} file 源文件
46
 * @desc 读取图片文件为base64文件格式
47
 * @retutn 返回base64文件
48
 */
49
export const readFile = file => {
50
  return new Promise((resolve, reject) => {
51
    const reader = new FileReader();
52
    reader.onload = e => {
53
      const data = e.target.result;
54
      resolve(data);
55
    };
56
    reader.onerror = () => {
57
      const err = new Error("读取图片失败");
58
      reject(err.message);
59
    };
60
61
    reader.readAsDataURL(file);
62
  });
63
};
64
65
/**
66
 *
67
 * @param {string} src  图片地址
68
 * @desc 加载真实图片
69
 * @return 读取成功返回图片真实宽高对象 ag: {width:100,height:100}
70
 */
71
export const loadImage = src => {
72
  return new Promise((resolve, reject) => {
73
    const image = new Image();
74
    image.src = src;
75
    image.onload = () => {
76
      const data = {
77
        width: image.width,
78
        height: image.height
79
      };
80
      resolve(data);
81
    };
82
    image.onerror = () => {
83
      const err = new Error("加载图片失败");
84
      reject(err);
85
    };
86
  });
87
};
88
89
/**
90
 *
91
 * @param {file} file 源文件
92
 * @param {object} props   文件分辨率的宽和高   ag: props={width:100, height :100}
93
 * @desc  判断图片文件的分辨率是否在限定范围之内
94
 * @throw  分辨率不在限定范围之内则抛出异常
95
 *
96
 */
97
export const isAppropriateResolution = async (file, props) => {
98
  try {
99
    const { width, height } = props;
100
    const base64 = await readFile(file);
101
    const image = await loadImage(base64);
102
    if (image.width !== width || image.height !== height) {
103
      throw new Error("上传图片的分辨率必须为" + width + "*" + height);
104
    }
105
  } catch (error) {
106
    throw error;
107
  }
108
};
109
110
/**
111
 *
112
 * @param {file} file 源文件
113
 * @param {array} ratio   限制的文件比例 ag:  ratio= [1,1]
114
 * @desc 判断图片文件的比列是否在限定范围
115
 * @throw  比例不在限定范围之内则抛出异常
116
 */
117
export const isAppRatio = async (file, ratio) => {
118
  try {
119
    const [w, h] = ratio;
120
    if (h === 0 || w === 0) {
121
      const err = "上传图片的比例不能出现0";
122
      Message.error(err);
123
      throw new Error(err);
124
    }
125
    const base64 = await readFile(file);
126
    const image = await loadImage(base64);
127
    if (image.width / image.height !== w / h) {
128
      throw new Error("上传图片的宽高比例必须为 " + w + " : " + h);
129
    }
130
  } catch (error) {
131
    throw error;
132
  }
133
};