Skip to content

Commit 2bbdfaa

Browse files
authored
feat(图片): 添加图片裁剪功能 (#543)
* feat(图片): 添加图片裁剪功能 * fix: 图片裁剪兼容本地图片不存在src的情况 * chore: 裁剪细节调整
1 parent 526f45c commit 2bbdfaa

File tree

7 files changed

+397
-3
lines changed

7 files changed

+397
-3
lines changed

.eslintrc-auto-import.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@
7171
"watchPostEffect": true,
7272
"watchSyncEffect": true
7373
}
74-
}
74+
}

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@
1111
"src/language"
1212
],
1313
"i18n-ally.sourceLanguage": "zh",
14-
"i18n-ally.keystyle": "nested" // 需要Prettier的配置文件
14+
"i18n-ally.keystyle": "nested",
15+
"[vue]": {
16+
"editor.defaultFormatter": "esbenp.prettier-vscode"
17+
} // 需要Prettier的配置文件
1518
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"view-ui-plus": "^1.3.7",
3333
"vite-svg-loader": "^5.1.0",
3434
"vue": "^3.2.25",
35+
"vue-cropper": "^1.1.4",
3536
"vue-i18n": "9.0.0",
3637
"vue-masonry": "^0.16.0",
3738
"vue-router": "^4.0.16",

src/components/cropperDialog.vue

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
<template>
2+
<Modal
3+
v-model="visible"
4+
@on-ok="onOk"
5+
@on-cancel="onCancel"
6+
title="图片裁剪"
7+
width="80%"
8+
style="height: 80%"
9+
>
10+
<div class="main">
11+
<Spin size="large" fix :show="loading">图片加载中...</Spin>
12+
<div class="options">
13+
<label>裁剪比例</label>
14+
<div class="flex mt-2 ratio-item-wrapper">
15+
<div class="ratio-item" :class="{ active: !fixed }" @click="changeRatio()">自由</div>
16+
<div
17+
class="ratio-item"
18+
:class="{ active: fixed && fixedRatio[0] === 1 && fixedRatio[1] === 1 }"
19+
@click="changeRatio([1, 1])"
20+
>
21+
<div class="ratio-item-content" style="height: 70px; width: 70px">1:1</div>
22+
</div>
23+
<div
24+
class="ratio-item"
25+
:class="{ active: fixed && fixedRatio[0] === 2 && fixedRatio[1] === 3 }"
26+
@click="changeRatio([2, 3])"
27+
>
28+
<div class="ratio-item-content" style="height: 70px; width: calc(70px * 2 / 3)">
29+
2:3
30+
</div>
31+
</div>
32+
<div
33+
class="ratio-item"
34+
:class="{ active: fixed && fixedRatio[0] === 3 && fixedRatio[1] === 2 }"
35+
@click="changeRatio([3, 2])"
36+
>
37+
<div class="ratio-item-content" style="height: calc(70px * 2 / 3); width: 70px">
38+
3:2
39+
</div>
40+
</div>
41+
<div
42+
class="ratio-item"
43+
:class="{ active: fixed && fixedRatio[0] === 4 && fixedRatio[1] === 3 }"
44+
@click="changeRatio([4, 3])"
45+
>
46+
<div class="ratio-item-content" style="height: calc(70px * 3 / 4); width: 70px">
47+
4:3
48+
</div>
49+
</div>
50+
<div
51+
class="ratio-item"
52+
:class="{ active: fixed && fixedRatio[0] === 3 && fixedRatio[1] === 4 }"
53+
@click="changeRatio([3, 4])"
54+
>
55+
<div class="ratio-item-content" style="width: calc(70px * 3 / 4); height: 70px">
56+
3:4
57+
</div>
58+
</div>
59+
<div
60+
class="ratio-item"
61+
:class="{ active: fixed && fixedRatio[0] === 16 && fixedRatio[1] === 9 }"
62+
@click="changeRatio([16, 9])"
63+
>
64+
<div class="ratio-item-content" style="height: calc(70px * 9 / 16); width: 70px">
65+
16:9
66+
</div>
67+
</div>
68+
<div
69+
class="ratio-item"
70+
:class="{ active: fixed && fixedRatio[0] === 9 && fixedRatio[1] === 16 }"
71+
@click="changeRatio([9, 16])"
72+
>
73+
<div class="ratio-item-content" style="width: calc(70px * 9 / 16); height: 70px">
74+
9:16
75+
</div>
76+
</div>
77+
</div>
78+
<label>当前尺寸</label>
79+
<div class="flex mt-2">
80+
<Input
81+
v-model="cropperWidth"
82+
type="number"
83+
@change="changeCropperSize('width', Number($event.target.value))"
84+
/>
85+
<span class="mx-2">X</span>
86+
<Input
87+
v-model="cropperHeight"
88+
type="number"
89+
@change="changeCropperSize('height', Number($event.target.value))"
90+
/>
91+
</div>
92+
</div>
93+
<div class="cropper-wrapper">
94+
<VueCropper
95+
ref="cropperRef"
96+
:img="img"
97+
:outputSize="1"
98+
outputType="png"
99+
autoCrop
100+
:fixed="fixed"
101+
:fixedNumber="fixedRatio"
102+
centerBox
103+
:fixedBox="false"
104+
full
105+
@realTime="onPreview"
106+
/>
107+
</div>
108+
<div class="preview-wrapper">
109+
<div
110+
style="height: 100px; width: 100px; border: 1px solid rgba(59, 130, 246, 0.5)"
111+
:style="previewStyle"
112+
>
113+
<div :style="previews.div" v-if="previews">
114+
<img :src="previews.url" :style="{ ...previews.img, maxWidth: previews.img.width }" />
115+
</div>
116+
</div>
117+
118+
<div class="title">预览</div>
119+
</div>
120+
</div>
121+
</Modal>
122+
</template>
123+
124+
<script setup lang="ts">
125+
import { ref } from 'vue';
126+
import 'vue-cropper/dist/index.css';
127+
import { VueCropper } from 'vue-cropper';
128+
// import { blobToFile } from '../../views/creation/createShortVideo/components/framesContent/utils'
129+
130+
const visible = ref(false);
131+
132+
const cropperRef = ref();
133+
134+
const img = ref();
135+
136+
const previewStyle = ref({
137+
width: '100px',
138+
height: '100px',
139+
overflow: 'hidden',
140+
margin: '0',
141+
});
142+
143+
const loading = ref(false);
144+
145+
const previews = ref();
146+
147+
const cropperWidth = ref(0);
148+
const cropperHeight = ref(0);
149+
150+
function changeCropperSize(type: 'width' | 'height', value: number) {
151+
if (fixed.value) {
152+
if (type === 'width') {
153+
cropperHeight.value = (value * fixedRatio.value[1]) / fixedRatio.value[0];
154+
} else {
155+
cropperWidth.value = (value * fixedRatio.value[0]) / fixedRatio.value[1];
156+
}
157+
}
158+
nextTick(() => {
159+
cropperRef.value.goAutoCrop(Number(cropperWidth.value), Number(cropperHeight.value));
160+
});
161+
}
162+
163+
function onPreview(_previews) {
164+
if (_previews.w === 0 && _previews.h === 0) {
165+
loading.value = true;
166+
} else {
167+
loading.value = false;
168+
}
169+
console.log('🚀 ~ onPreview ~ _previews:', _previews);
170+
cropperWidth.value = _previews.w;
171+
cropperHeight.value = _previews.h;
172+
previewStyle.value = {
173+
width: _previews.w + 'px',
174+
height: _previews.h + 'px',
175+
overflow: 'hidden',
176+
margin: '0',
177+
zoom: 100 / _previews.w,
178+
};
179+
180+
previews.value = _previews;
181+
}
182+
183+
function onCancel() {
184+
visible.value = false;
185+
}
186+
const fixedRatio = ref([1, 1]);
187+
const fixed = ref(false);
188+
function changeRatio(ratio?: [number, number]) {
189+
if (ratio) {
190+
fixed.value = true;
191+
fixedRatio.value = ratio;
192+
// 宽度不变,按照比例修改高度
193+
// cropperHeight.value = (cropperWidth.value * ratio[1]) / ratio[0];
194+
} else {
195+
fixed.value = false;
196+
}
197+
nextTick(() => {
198+
cropperRef.value.goAutoCrop(9999, 9999);
199+
});
200+
}
201+
202+
let _callback = null;
203+
function onOk() {
204+
cropperRef.value.getCropData((data) => {
205+
_callback && _callback(data);
206+
visible.value = false;
207+
});
208+
}
209+
210+
defineExpose({
211+
open(data, callback) {
212+
img.value = data.img;
213+
_callback = callback;
214+
visible.value = true;
215+
},
216+
});
217+
</script>
218+
219+
<style scoped lang="less">
220+
.main {
221+
display: flex;
222+
align-items: stretch;
223+
min-height: 600px;
224+
position: relative;
225+
}
226+
227+
.options {
228+
width: 256px;
229+
.mt-2 {
230+
margin-top: 8px;
231+
}
232+
.flex {
233+
display: flex;
234+
align-items: center;
235+
}
236+
.mx-2 {
237+
margin: 0 8px;
238+
}
239+
}
240+
241+
.cropper-wrapper {
242+
flex: 1;
243+
min-height: 100%;
244+
padding: 0 16px;
245+
}
246+
247+
.title {
248+
text-align: center;
249+
font-size: 14px;
250+
color: #0a0a15;
251+
margin-top: 12px;
252+
}
253+
254+
.ratio-item-wrapper {
255+
display: flex;
256+
flex-wrap: wrap;
257+
gap: 8px;
258+
margin-bottom: 12px;
259+
}
260+
261+
.ratio-item {
262+
width: 80px;
263+
height: 80px;
264+
// border: 1px solid #99999f;
265+
background-color: #fcfcfc;
266+
border-radius: 4px;
267+
text-align: center;
268+
line-height: 30px;
269+
display: flex;
270+
align-items: center;
271+
justify-content: center;
272+
cursor: pointer;
273+
274+
&.active {
275+
border: 1px solid #2d8cf0;
276+
}
277+
}
278+
279+
.ratio-item-content {
280+
background-color: #f0f0f0;
281+
display: flex;
282+
align-items: center;
283+
justify-content: center;
284+
border-radius: 4px;
285+
}
286+
287+
.preview-wrapper {
288+
width: 100px;
289+
}
290+
</style>

0 commit comments

Comments
 (0)