vue-chat-kit 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,229 +1,534 @@
1
1
  <template>
2
2
  <el-dialog
3
- v-model="dialogVisible"
4
- title="裁剪头像"
5
- width="500px"
3
+ v-model="visible"
4
+ title="调整头像"
5
+ :width="dialogWidth"
6
6
  :close-on-click-modal="false"
7
+ :append-to-body="appendToBody"
7
8
  @closed="handleClosed"
8
9
  >
9
10
  <div class="avatar-crop-container">
10
- <div class="crop-area" ref="cropAreaRef">
11
- <img
12
- :src="imageSrc"
13
- ref="imageRef"
14
- class="crop-image"
15
- @load="onImageLoad"
16
- @mousedown="startDrag"
17
- @touchstart="startDrag"
18
- />
19
- <div class="crop-overlay">
20
- <div class="crop-box">
21
- <div class="crop-border" ref="cropBoxRef"></div>
11
+ <div v-if="imageSrc" class="crop-wrapper">
12
+ <div
13
+ ref="cropAreaRef"
14
+ class="crop-area"
15
+ :style="{ height: cropAreaHeight }"
16
+ >
17
+ <img
18
+ :src="imageSrc"
19
+ ref="imageRef"
20
+ @load="onImageLoad"
21
+ :style="imageStyle"
22
+ />
23
+ <div
24
+ class="crop-box"
25
+ :style="cropBoxStyle"
26
+ @mousedown="startCropDrag"
27
+ @touchstart="startCropDrag"
28
+ >
29
+ <div v-for="handle in cropHandles" :key="handle" class="crop-handle" :class="handle"
30
+ @mousedown.stop="startResize(handle, $event)"
31
+ @touchstart.stop="startResize(handle, $event)"
32
+ ></div>
33
+ </div>
34
+ </div>
35
+ <div class="preview-wrapper">
36
+ <div class="preview-label">预览</div>
37
+ <div class="preview-box" :style="{ width: previewSize + 'px', height: previewSize + 'px' }">
38
+ <canvas
39
+ ref="previewCanvasRef"
40
+ :width="previewSize"
41
+ :height="previewSize"
42
+ class="preview-canvas"
43
+ ></canvas>
22
44
  </div>
23
45
  </div>
24
- <div v-if="isDragging" class="crop-mask"></div>
25
- </div>
26
- <div class="zoom-controls">
27
- <el-slider
28
- v-model="scale"
29
- :min="0.5"
30
- :max="3"
31
- :step="0.1"
32
- :show-tooltip="false"
33
- />
34
46
  </div>
47
+ <el-empty v-else description="请上传图片" />
35
48
  </div>
36
49
  <template #footer>
37
- <el-button @click="dialogVisible = false">取消</el-button>
38
- <el-button type="primary" @click="handleConfirm">确定</el-button>
50
+ <div class="dialog-footer">
51
+ <el-button @click="handleCancel">取消</el-button>
52
+ <el-button type="primary" :loading="loading" :disabled="!imageSrc" @click="handleConfirm">
53
+ 确认
54
+ </el-button>
55
+ </div>
39
56
  </template>
40
57
  </el-dialog>
41
58
  </template>
42
59
 
43
60
  <script setup>
44
- import { ref, computed, watch, nextTick } from 'vue'
61
+ import { ref, computed, nextTick, watch } from "vue";
62
+ import { ElMessage } from "element-plus";
45
63
 
46
64
  const props = defineProps({
47
- modelValue: { type: Boolean, default: false },
48
- src: { type: String, default: '' }
49
- })
65
+ modelValue: Boolean,
66
+ src: { type: String, default: "" },
67
+ dialogWidth: { type: String, default: "520px" },
68
+ cropAreaHeight: { type: String, default: "288px" },
69
+ previewSize: { type: Number, default: 96 },
70
+ outputSize: { type: Number, default: 400 },
71
+ outputType: { type: String, default: "image/jpeg" },
72
+ outputQuality: { type: Number, default: 0.9 },
73
+ appendToBody: { type: Boolean, default: true },
74
+ aspectRatio: { type: Number, default: 1 },
75
+ minSize: { type: Number, default: 50 },
76
+ });
50
77
 
51
- const emit = defineEmits(['update:modelValue', 'confirm'])
78
+ const emit = defineEmits(["update:modelValue", "confirm", "cancel", "closed"]);
52
79
 
53
- const dialogVisible = computed({
80
+ const visible = computed({
54
81
  get: () => props.modelValue,
55
- set: (val) => emit('update:modelValue', val)
56
- })
57
-
58
- const imageSrc = ref('')
59
- const imageRef = ref(null)
60
- const cropAreaRef = ref(null)
61
- const cropBoxRef = ref(null)
62
- const scale = ref(1)
63
- const position = ref({ x: 0, y: 0 })
64
- const isDragging = ref(false)
65
- const startPos = ref({ x: 0, y: 0 })
66
- const imageSize = ref({ width: 0, height: 0 })
67
-
68
- watch(() => props.src, (val) => {
69
- if (val) {
70
- imageSrc.value = val
71
- scale.value = 1
72
- position.value = { x: 0, y: 0 }
73
- }
74
- })
82
+ set: (val) => emit("update:modelValue", val),
83
+ });
84
+
85
+ const imageSrc = computed({
86
+ get: () => props.src,
87
+ set: (val) => {},
88
+ });
89
+
90
+ const cropHandles = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
91
+ const loading = ref(false);
92
+ const cropAreaRef = ref(null);
93
+ const imageRef = ref(null);
94
+ const previewCanvasRef = ref(null);
95
+
96
+ const imageData = ref({
97
+ naturalWidth: 0,
98
+ naturalHeight: 0,
99
+ displayWidth: 0,
100
+ displayHeight: 0,
101
+ offsetX: 0,
102
+ offsetY: 0,
103
+ });
104
+
105
+ const cropBox = ref({
106
+ x: 0,
107
+ y: 0,
108
+ size: 150,
109
+ minSize: props.minSize,
110
+ });
111
+
112
+ const imageStyle = computed(() => ({
113
+ width: `${imageData.value.displayWidth}px`,
114
+ height: `${imageData.value.displayHeight}px`,
115
+ maxWidth: "none",
116
+ maxHeight: "none",
117
+ display: "block",
118
+ }));
119
+
120
+ const cropBoxStyle = computed(() => ({
121
+ left: `${cropBox.value.x}px`,
122
+ top: `${cropBox.value.y}px`,
123
+ width: `${cropBox.value.size}px`,
124
+ height: `${cropBox.value.size / props.aspectRatio}px`,
125
+ }));
126
+
127
+ let dragState = null;
128
+ let resizeState = null;
75
129
 
76
130
  const onImageLoad = () => {
77
131
  nextTick(() => {
78
- if (imageRef.value && cropAreaRef.value) {
79
- const img = imageRef.value
80
- const area = cropAreaRef.value
81
- const minSide = Math.min(area.clientWidth, area.clientHeight)
82
-
83
- if (img.naturalWidth > img.naturalHeight) {
84
- imageSize.value.height = minSide
85
- imageSize.value.width = (img.naturalWidth / img.naturalHeight) * minSide
86
- } else {
87
- imageSize.value.width = minSide
88
- imageSize.value.height = (img.naturalHeight / img.naturalWidth) * minSide
89
- }
90
-
91
- position.value = {
92
- x: (minSide - imageSize.value.width) / 2,
93
- y: (minSide - imageSize.value.height) / 2
94
- }
132
+ const img = imageRef.value;
133
+ const cropArea = cropAreaRef.value;
134
+ if (!img || !cropArea) return;
135
+
136
+ const containerWidth = cropArea.clientWidth;
137
+ const containerHeight = cropArea.clientHeight;
138
+ const imgRatio = img.naturalWidth / img.naturalHeight;
139
+ const containerRatio = containerWidth / containerHeight;
140
+
141
+ let displayWidth, displayHeight, offsetX, offsetY;
142
+
143
+ if (imgRatio > containerRatio) {
144
+ displayWidth = containerWidth;
145
+ displayHeight = containerWidth / imgRatio;
146
+ offsetX = 0;
147
+ offsetY = (containerHeight - displayHeight) / 2;
148
+ } else {
149
+ displayHeight = containerHeight;
150
+ displayWidth = containerHeight * imgRatio;
151
+ offsetX = (containerWidth - displayWidth) / 2;
152
+ offsetY = 0;
95
153
  }
96
- })
97
- }
98
-
99
- const startDrag = (e) => {
100
- isDragging.value = true
101
- const clientX = e.touches ? e.touches[0].clientX : e.clientX
102
- const clientY = e.touches ? e.touches[0].clientY : e.clientY
103
- startPos.value = { x: clientX - position.value.x, y: clientY - position.value.y }
104
-
105
- document.addEventListener('mousemove', onDrag)
106
- document.addEventListener('mouseup', stopDrag)
107
- document.addEventListener('touchmove', onDrag)
108
- document.addEventListener('touchend', stopDrag)
109
- }
110
-
111
- const onDrag = (e) => {
112
- if (!isDragging.value) return
113
- const clientX = e.touches ? e.touches[0].clientX : e.clientX
114
- const clientY = e.touches ? e.touches[0].clientY : e.clientY
115
- position.value = {
116
- x: clientX - startPos.value.x,
117
- y: clientY - startPos.value.y
118
- }
119
- }
120
154
 
121
- const stopDrag = () => {
122
- isDragging.value = false
123
- document.removeEventListener('mousemove', onDrag)
124
- document.removeEventListener('mouseup', stopDrag)
125
- document.removeEventListener('touchmove', onDrag)
126
- document.removeEventListener('touchend', stopDrag)
127
- }
128
-
129
- const handleConfirm = () => {
130
- const canvas = document.createElement('canvas')
131
- const ctx = canvas.getContext('2d')
132
- const size = 200
133
- canvas.width = size
134
- canvas.height = size
135
-
136
- const img = imageRef.value
137
- const cropBox = cropBoxRef.value
138
- const area = cropAreaRef.value
139
-
140
- if (img && cropBox && area) {
141
- const boxRect = cropBox.getBoundingClientRect()
142
- const areaRect = area.getBoundingClientRect()
143
-
144
- const cropX = (boxRect.left - areaRect.left - position.value.x) / scale.value
145
- const cropY = (boxRect.top - areaRect.top - position.value.y) / scale.value
146
- const cropSize = boxRect.width / scale.value
147
-
148
- ctx.drawImage(
149
- img,
150
- cropX * (img.naturalWidth / imageSize.value.width),
151
- cropY * (img.naturalHeight / imageSize.value.height),
152
- cropSize * (img.naturalWidth / imageSize.value.width),
153
- cropSize * (img.naturalHeight / imageSize.value.height),
154
- 0,
155
- 0,
156
- size,
157
- size
158
- )
159
-
160
- canvas.toBlob((blob) => {
161
- emit('confirm', { file: blob, url: canvas.toDataURL('image/png') })
162
- dialogVisible.value = false
163
- }, 'image/png')
155
+ imageData.value = {
156
+ naturalWidth: img.naturalWidth,
157
+ naturalHeight: img.naturalHeight,
158
+ displayWidth,
159
+ displayHeight,
160
+ offsetX,
161
+ offsetY,
162
+ };
163
+
164
+ const initialSize = Math.min(displayWidth, displayHeight) * 0.6;
165
+ cropBox.value.size = initialSize;
166
+ cropBox.value.x = offsetX + (displayWidth - initialSize) / 2;
167
+ cropBox.value.y = offsetY + (displayHeight - initialSize / props.aspectRatio) / 2;
168
+
169
+ updatePreview();
170
+ });
171
+ };
172
+
173
+ const updatePreview = () => {
174
+ const canvas = previewCanvasRef.value;
175
+ const img = imageRef.value;
176
+ if (!canvas || !img) return;
177
+
178
+ const ctx = canvas.getContext("2d");
179
+ const { naturalWidth, naturalHeight, displayWidth, displayHeight, offsetX, offsetY } = imageData.value;
180
+ const scaleX = naturalWidth / displayWidth;
181
+ const scaleY = naturalHeight / displayHeight;
182
+
183
+ const sx = (cropBox.value.x - offsetX) * scaleX;
184
+ const sy = (cropBox.value.y - offsetY) * scaleY;
185
+ const sw = cropBox.value.size * scaleX;
186
+ const sh = (cropBox.value.size / props.aspectRatio) * scaleY;
187
+
188
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
189
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
190
+ };
191
+
192
+ const startCropDrag = (e) => {
193
+ e.preventDefault();
194
+ const startX = e.clientX || e.touches[0].clientX;
195
+ const startY = e.clientY || e.touches[0].clientY;
196
+ dragState = {
197
+ startX,
198
+ startY,
199
+ boxX: cropBox.value.x,
200
+ boxY: cropBox.value.y,
201
+ };
202
+
203
+ const onMove = (e) => {
204
+ const clientX = e.clientX || e.touches[0].clientX;
205
+ const clientY = e.clientY || e.touches[0].clientY;
206
+ const dx = clientX - dragState.startX;
207
+ const dy = clientY - dragState.startY;
208
+
209
+ const { offsetX, offsetY, displayWidth, displayHeight } = imageData.value;
210
+ const cropHeight = cropBox.value.size / props.aspectRatio;
211
+ const newX = Math.max(offsetX, Math.min(offsetX + displayWidth - cropBox.value.size, dragState.boxX + dx));
212
+ const newY = Math.max(offsetY, Math.min(offsetY + displayHeight - cropHeight, dragState.boxY + dy));
213
+
214
+ cropBox.value.x = newX;
215
+ cropBox.value.y = newY;
216
+ updatePreview();
217
+ };
218
+
219
+ const onEnd = () => {
220
+ dragState = null;
221
+ document.removeEventListener("mousemove", onMove);
222
+ document.removeEventListener("mouseup", onEnd);
223
+ document.removeEventListener("touchmove", onMove);
224
+ document.removeEventListener("touchend", onEnd);
225
+ };
226
+
227
+ document.addEventListener("mousemove", onMove);
228
+ document.addEventListener("mouseup", onEnd);
229
+ document.addEventListener("touchmove", onMove, { passive: false });
230
+ document.addEventListener("touchend", onEnd);
231
+ };
232
+
233
+ const startResize = (handle, e) => {
234
+ e.preventDefault();
235
+ const startX = e.clientX || e.touches[0].clientX;
236
+ const startY = e.clientY || e.touches[0].clientY;
237
+ resizeState = {
238
+ handle,
239
+ startX,
240
+ startY,
241
+ boxX: cropBox.value.x,
242
+ boxY: cropBox.value.y,
243
+ boxSize: cropBox.value.size,
244
+ };
245
+
246
+ const onMove = (e) => {
247
+ const clientX = e.clientX || e.touches[0].clientX;
248
+ const clientY = e.clientY || e.touches[0].clientY;
249
+ const dx = clientX - resizeState.startX;
250
+ const dy = clientY - resizeState.startY;
251
+
252
+ const { offsetX, offsetY, displayWidth, displayHeight } = imageData.value;
253
+ let newSize = resizeState.boxSize;
254
+ let newX = resizeState.boxX;
255
+ let newY = resizeState.boxY;
256
+ const cropHeight = resizeState.boxSize / props.aspectRatio;
257
+
258
+ switch (handle) {
259
+ case "se":
260
+ newSize = Math.max(cropBox.value.minSize, Math.min(displayWidth - (resizeState.boxX - offsetX), displayHeight - (resizeState.boxY - offsetY), resizeState.boxSize + Math.max(dx, dy)));
261
+ break;
262
+ case "nw":
263
+ newSize = Math.max(cropBox.value.minSize, Math.min(resizeState.boxX - offsetX + resizeState.boxSize, resizeState.boxY - offsetY + cropHeight, resizeState.boxSize - Math.max(dx, dy)));
264
+ newX = resizeState.boxX + (resizeState.boxSize - newSize);
265
+ newY = resizeState.boxY + (cropHeight - newSize / props.aspectRatio);
266
+ break;
267
+ case "ne":
268
+ newSize = Math.max(cropBox.value.minSize, Math.min(offsetX + displayWidth - resizeState.boxX, resizeState.boxY - offsetY + cropHeight, resizeState.boxSize + Math.max(dx, -dy)));
269
+ newY = resizeState.boxY + (cropHeight - newSize / props.aspectRatio);
270
+ break;
271
+ case "sw":
272
+ newSize = Math.max(cropBox.value.minSize, Math.min(resizeState.boxX - offsetX + resizeState.boxSize, offsetY + displayHeight - resizeState.boxY, resizeState.boxSize + Math.max(-dx, dy)));
273
+ newX = resizeState.boxX + (resizeState.boxSize - newSize);
274
+ break;
275
+ case "n":
276
+ newSize = Math.max(cropBox.value.minSize, Math.min(resizeState.boxY - offsetY + cropHeight, resizeState.boxSize - dy));
277
+ newY = resizeState.boxY + (cropHeight - newSize / props.aspectRatio);
278
+ break;
279
+ case "s":
280
+ newSize = Math.max(cropBox.value.minSize, Math.min(offsetY + displayHeight - resizeState.boxY, resizeState.boxSize + dy));
281
+ break;
282
+ case "w":
283
+ newSize = Math.max(cropBox.value.minSize, Math.min(resizeState.boxX - offsetX + resizeState.boxSize, resizeState.boxSize - dx));
284
+ newX = resizeState.boxX + (resizeState.boxSize - newSize);
285
+ break;
286
+ case "e":
287
+ newSize = Math.max(cropBox.value.minSize, Math.min(offsetX + displayWidth - resizeState.boxX, resizeState.boxSize + dx));
288
+ break;
289
+ }
290
+
291
+ cropBox.value.size = newSize;
292
+ cropBox.value.x = newX;
293
+ cropBox.value.y = newY;
294
+ updatePreview();
295
+ };
296
+
297
+ const onEnd = () => {
298
+ resizeState = null;
299
+ document.removeEventListener("mousemove", onMove);
300
+ document.removeEventListener("mouseup", onEnd);
301
+ document.removeEventListener("touchmove", onMove);
302
+ document.removeEventListener("touchend", onEnd);
303
+ };
304
+
305
+ document.addEventListener("mousemove", onMove);
306
+ document.addEventListener("mouseup", onEnd);
307
+ document.addEventListener("touchmove", onMove, { passive: false });
308
+ document.addEventListener("touchend", onEnd);
309
+ };
310
+
311
+ const getCroppedImage = () => {
312
+ const canvas = document.createElement("canvas");
313
+ const img = imageRef.value;
314
+ if (!img) return null;
315
+
316
+ const { naturalWidth, naturalHeight, displayWidth, displayHeight, offsetX, offsetY } = imageData.value;
317
+ const scaleX = naturalWidth / displayWidth;
318
+ const scaleY = naturalHeight / displayHeight;
319
+
320
+ const sx = (cropBox.value.x - offsetX) * scaleX;
321
+ const sy = (cropBox.value.y - offsetY) * scaleY;
322
+ const sw = cropBox.value.size * scaleX;
323
+ const sh = (cropBox.value.size / props.aspectRatio) * scaleY;
324
+
325
+ canvas.width = props.outputSize;
326
+ canvas.height = props.outputSize / props.aspectRatio;
327
+
328
+ const ctx = canvas.getContext("2d");
329
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
330
+
331
+ return new Promise((resolve) => {
332
+ canvas.toBlob(
333
+ (blob) => {
334
+ resolve(new File([blob], "avatar.jpg", { type: props.outputType }));
335
+ },
336
+ props.outputType,
337
+ props.outputQuality
338
+ );
339
+ });
340
+ };
341
+
342
+ const getCroppedImageDataUrl = () => {
343
+ const canvas = document.createElement("canvas");
344
+ const img = imageRef.value;
345
+ if (!img) return null;
346
+
347
+ const { naturalWidth, naturalHeight, displayWidth, displayHeight, offsetX, offsetY } = imageData.value;
348
+ const scaleX = naturalWidth / displayWidth;
349
+ const scaleY = naturalHeight / displayHeight;
350
+
351
+ const sx = (cropBox.value.x - offsetX) * scaleX;
352
+ const sy = (cropBox.value.y - offsetY) * scaleY;
353
+ const sw = cropBox.value.size * scaleX;
354
+ const sh = (cropBox.value.size / props.aspectRatio) * scaleY;
355
+
356
+ canvas.width = props.outputSize;
357
+ canvas.height = props.outputSize / props.aspectRatio;
358
+
359
+ const ctx = canvas.getContext("2d");
360
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
361
+
362
+ return canvas.toDataURL(props.outputType, props.outputQuality);
363
+ };
364
+
365
+ const handleConfirm = async () => {
366
+ if (!imageSrc.value) return;
367
+
368
+ loading.value = true;
369
+ try {
370
+ const file = await getCroppedImage();
371
+ const dataUrl = getCroppedImageDataUrl();
372
+ if (file) {
373
+ emit("confirm", { file, dataUrl });
374
+ }
375
+ } catch (error) {
376
+ console.error(error);
377
+ ElMessage.error("生成图片失败");
378
+ } finally {
379
+ loading.value = false;
164
380
  }
165
- }
381
+ };
382
+
383
+ const handleCancel = () => {
384
+ emit("cancel");
385
+ visible.value = false;
386
+ };
166
387
 
167
388
  const handleClosed = () => {
168
- imageSrc.value = ''
169
- scale.value = 1
170
- position.value = { x: 0, y: 0 }
171
- }
389
+ reset();
390
+ emit("closed");
391
+ };
392
+
393
+ const reset = () => {
394
+ cropBox.value = { x: 0, y: 0, size: 150, minSize: props.minSize };
395
+ };
396
+
397
+ watch(
398
+ () => props.src,
399
+ (newSrc) => {
400
+ if (newSrc) {
401
+ nextTick(() => {
402
+ if (imageRef.value && imageRef.value.complete) {
403
+ onImageLoad();
404
+ }
405
+ });
406
+ }
407
+ }
408
+ );
409
+
410
+ defineExpose({
411
+ getCroppedImage,
412
+ getCroppedImageDataUrl,
413
+ reset,
414
+ });
172
415
  </script>
173
416
 
174
417
  <style scoped>
175
418
  .avatar-crop-container {
419
+ min-height: 200px;
420
+ }
421
+
422
+ .crop-wrapper {
176
423
  display: flex;
177
424
  flex-direction: column;
178
- gap: 20px;
425
+ gap: 16px;
179
426
  }
180
427
 
181
428
  .crop-area {
182
429
  position: relative;
183
- width: 100%;
184
- height: 300px;
185
430
  overflow: hidden;
186
- background: #f5f5f5;
187
- border-radius: 8px;
431
+ background: #f5f7fa;
432
+ border-radius: 12px;
433
+ display: flex;
434
+ align-items: center;
435
+ justify-content: center;
436
+ border: 1px solid #e4e7ed;
188
437
  }
189
438
 
190
- .crop-image {
439
+ .crop-box {
191
440
  position: absolute;
441
+ border: 2px solid #409eff;
442
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
192
443
  cursor: move;
193
- user-select: none;
194
- max-width: none;
444
+ box-sizing: border-box;
195
445
  }
196
446
 
197
- .crop-overlay {
447
+ .crop-handle {
198
448
  position: absolute;
199
- inset: 0;
200
- pointer-events: none;
449
+ width: 14px;
450
+ height: 14px;
451
+ background: #409eff;
452
+ border: 3px solid #fff;
453
+ border-radius: 50%;
454
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
201
455
  }
202
456
 
203
- .crop-box {
204
- position: absolute;
457
+ .crop-handle.nw {
458
+ top: -7px;
459
+ left: -7px;
460
+ cursor: nw-resize;
461
+ }
462
+ .crop-handle.n {
463
+ top: -7px;
464
+ left: 50%;
465
+ transform: translateX(-50%);
466
+ cursor: n-resize;
467
+ }
468
+ .crop-handle.ne {
469
+ top: -7px;
470
+ right: -7px;
471
+ cursor: ne-resize;
472
+ }
473
+ .crop-handle.e {
474
+ right: -7px;
205
475
  top: 50%;
476
+ transform: translateY(-50%);
477
+ cursor: e-resize;
478
+ }
479
+ .crop-handle.se {
480
+ bottom: -7px;
481
+ right: -7px;
482
+ cursor: se-resize;
483
+ }
484
+ .crop-handle.s {
485
+ bottom: -7px;
206
486
  left: 50%;
207
- transform: translate(-50%, -50%);
208
- width: 200px;
209
- height: 200px;
487
+ transform: translateX(-50%);
488
+ cursor: s-resize;
489
+ }
490
+ .crop-handle.sw {
491
+ bottom: -7px;
492
+ left: -7px;
493
+ cursor: sw-resize;
494
+ }
495
+ .crop-handle.w {
496
+ left: -7px;
497
+ top: 50%;
498
+ transform: translateY(-50%);
499
+ cursor: w-resize;
210
500
  }
211
501
 
212
- .crop-border {
213
- width: 100%;
214
- height: 100%;
215
- border: 2px solid #fff;
216
- box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
217
- border-radius: 8px;
502
+ .preview-wrapper {
503
+ display: flex;
504
+ align-items: center;
505
+ justify-content: center;
506
+ gap: 16px;
218
507
  }
219
508
 
220
- .crop-mask {
221
- position: absolute;
222
- inset: 0;
223
- cursor: move;
509
+ .preview-label {
510
+ font-size: 14px;
511
+ color: #606266;
512
+ font-weight: 500;
513
+ }
514
+
515
+ .preview-box {
516
+ border-radius: 50%;
517
+ overflow: hidden;
518
+ border: 3px solid #e4e7ed;
519
+ background: #fff;
520
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
224
521
  }
225
522
 
226
- .zoom-controls {
227
- padding: 0 20px;
523
+ .preview-canvas {
524
+ display: block;
525
+ width: 100%;
526
+ height: 100%;
527
+ }
528
+
529
+ .dialog-footer {
530
+ display: flex;
531
+ justify-content: center;
532
+ gap: 12px;
228
533
  }
229
534
  </style>