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 | }; |