op
This commit is contained in:
@@ -1,217 +1,760 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="basic_container">
|
<div class="cropper-demo">
|
||||||
<div class="tool_wrap">
|
<div class="container">
|
||||||
<button :class="{ active: isCropperMove }" @click="handleMove">
|
<header>
|
||||||
移动
|
<h1>CropperJS 组件演示</h1>
|
||||||
</button>
|
<p class="description">
|
||||||
<button @click="handleRotate">旋转</button>
|
此演示展示了CropperJS各个组件的功能和使用方法。您可以通过下方控制面板调整裁剪区域、查看预览效果,并了解每个组件的作用。
|
||||||
<button :class="{ active: isCropperSelection }" @click="handleCropper">
|
</p>
|
||||||
{{ isCropperSelection ? "重置选区" : "裁剪" }}
|
</header>
|
||||||
</button>
|
|
||||||
</div>
|
<div class="demo-area">
|
||||||
<div class="dialog_wrap">
|
<div class="cropper-container">
|
||||||
<div class="image_wrap" ref="imageWrap">
|
<h3>裁剪画布</h3>
|
||||||
<cropper-canvas ref="croppercanvas" background>
|
<div class="canvas-wrapper">
|
||||||
<cropper-image
|
<canvas
|
||||||
:src="fileObj.fileShow"
|
ref="canvas"
|
||||||
alt="Picture"
|
class="cropper-canvas"
|
||||||
ref="cropperimage"
|
@mousedown="onCanvasMouseDown"
|
||||||
rotatable
|
@mousemove="onCanvasMouseMove"
|
||||||
scalable
|
@mouseup="onCanvasMouseUp"
|
||||||
skewable
|
@mouseleave="onCanvasMouseLeave"
|
||||||
translatable
|
></canvas>
|
||||||
></cropper-image>
|
</div>
|
||||||
<cropper-shade hidden ref="cropperShade"></cropper-shade>
|
|
||||||
<cropper-handle :action="currentType" plain></cropper-handle>
|
<div class="viewer-container">
|
||||||
<cropper-selection
|
<div class="viewer-label">实时预览</div>
|
||||||
id="cropperSelection"
|
<canvas ref="previewCanvas" class="preview-viewer"></canvas>
|
||||||
ref="cropperselection"
|
</div>
|
||||||
movable
|
|
||||||
resizable
|
<div class="status-bar">
|
||||||
hidden
|
<div>选区位置: <span>{{ positionInfo }}</span></div>
|
||||||
outlined
|
<div>选区尺寸: <span>{{ sizeInfo }}</span></div>
|
||||||
@change="onCropperSelectionChange"
|
<div>宽高比: <span>{{ ratioInfo }}</span></div>
|
||||||
>
|
</div>
|
||||||
<cropper-crosshair centered />
|
</div>
|
||||||
<cropper-handle
|
|
||||||
action="move"
|
<div class="controls">
|
||||||
theme-color="rgba(255, 255, 255, 0.35)"
|
<h3>控制面板</h3>
|
||||||
/>
|
|
||||||
<cropper-handle action="n-resize" />
|
<div class="control-group">
|
||||||
<cropper-handle action="e-resize" />
|
<h4>选区操作</h4>
|
||||||
<cropper-handle action="s-resize" />
|
<div class="btn-group">
|
||||||
<cropper-handle action="w-resize" />
|
<button
|
||||||
<cropper-handle action="ne-resize" />
|
:class="{ active: interactionMode === 'move' }"
|
||||||
<cropper-handle action="nw-resize" />
|
@click="setInteractionMode('move')"
|
||||||
<cropper-handle action="se-resize" />
|
>
|
||||||
<cropper-handle action="sw-resize" />
|
移动选区
|
||||||
</cropper-selection>
|
</button>
|
||||||
</cropper-canvas>
|
<button
|
||||||
|
:class="{ active: interactionMode === 'resize' }"
|
||||||
|
@click="setInteractionMode('resize')"
|
||||||
|
>
|
||||||
|
调整大小
|
||||||
|
</button>
|
||||||
|
<button @click="rotateSelection">旋转</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h4>宽高比设置</h4>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
v-for="ratio in aspectRatios"
|
||||||
|
:key="ratio.value"
|
||||||
|
@click="setAspectRatio(ratio.value)"
|
||||||
|
>
|
||||||
|
{{ ratio.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h4>遮罩设置</h4>
|
||||||
|
<div class="slider-container">
|
||||||
|
<label for="opacitySlider">遮罩透明度: <span>{{ shade.opacity }}</span></label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="opacitySlider"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
v-model="shade.opacity"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
v-for="color in shadeColors"
|
||||||
|
:key="color.value"
|
||||||
|
@click="setShadeColor(color.value)"
|
||||||
|
>
|
||||||
|
{{ color.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h4>十字准星</h4>
|
||||||
|
<div class="crosshair-controls">
|
||||||
|
<button @click="showCrosshair">显示准星</button>
|
||||||
|
<button @click="hideCrosshair">隐藏准星</button>
|
||||||
|
<button @click="moveCrosshair">移动准星</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h4>其他操作</h4>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button @click="resetSelection">重置选区</button>
|
||||||
|
<button @click="cropImage">裁剪图片</button>
|
||||||
|
<button @click="downloadResult">下载结果</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info_wrap">
|
|
||||||
<div class="cropper_preview">
|
<div class="component-info">
|
||||||
<cropper-viewer
|
<h3>组件说明</h3>
|
||||||
selection="#cropperSelection"
|
<div class="info-grid">
|
||||||
style="width: 200px"
|
<div class="info-card" v-for="component in components" :key="component.name">
|
||||||
></cropper-viewer>
|
<h4>{{ component.name }}</h4>
|
||||||
|
<p>{{ component.description }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn_wrap">
|
|
||||||
<input type="file" ref="input_form" @change="handleUploadSuccess" />
|
|
||||||
<button type="primary" @click="handleConfirm">确 认</button>
|
|
||||||
</div>
|
|
||||||
点击确认后,看控制台,有信息
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import "cropperjs";
|
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { computed, ref } from "vue";
|
|
||||||
|
|
||||||
const fileObj = ref({});
|
// Refs
|
||||||
|
const canvas = ref(null)
|
||||||
|
const previewCanvas = ref(null)
|
||||||
|
|
||||||
const croppercanvas = ref();
|
// 响应式状态
|
||||||
const cropperimage = ref();
|
const selection = reactive({
|
||||||
const cropperselection = ref();
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 300,
|
||||||
|
height: 200,
|
||||||
|
aspectRatio: 0,
|
||||||
|
isMoving: false,
|
||||||
|
isResizing: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
const shade = reactive({
|
||||||
* 选区逻辑
|
opacity: 0.5,
|
||||||
*/
|
color: '#000000'
|
||||||
// 是否正在开始选区
|
})
|
||||||
const isCropperSelection = ref(false);
|
|
||||||
const isCropperMove = ref(true);
|
|
||||||
|
|
||||||
// 判断当前是移动还是选区
|
const crosshair = reactive({
|
||||||
const currentType = computed(() => (isCropperMove.value ? "move" : "select"));
|
visible: false,
|
||||||
|
x: 200,
|
||||||
|
y: 150
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
const interactionMode = ref('move')
|
||||||
* 按钮方法
|
const rotation = ref(0)
|
||||||
*/
|
|
||||||
// 旋转
|
|
||||||
function handleRotate() {
|
|
||||||
cropperimage.value.$rotate("90deg");
|
|
||||||
cropperimage.value.$center("contain");
|
|
||||||
}
|
|
||||||
// 裁剪
|
|
||||||
function handleCropper() {
|
|
||||||
isCropperMove.value = false;
|
|
||||||
if (isCropperMove.value) {
|
|
||||||
cropperselection.value.$clear();
|
|
||||||
} else {
|
|
||||||
const cropperCanvas = croppercanvas.value;
|
|
||||||
const cropperCanvasRect = cropperCanvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
const cropperImage = cropperimage.value;
|
// 计算属性
|
||||||
const cropperImageRect = cropperImage.getBoundingClientRect();
|
const positionInfo = computed(() => {
|
||||||
const maxSelection = {
|
return `x: ${selection.x}, y: ${selection.y}`
|
||||||
x: cropperImageRect.left - cropperCanvasRect.left,
|
})
|
||||||
y: cropperImageRect.top - cropperCanvasRect.top,
|
|
||||||
width: cropperImageRect.width,
|
const sizeInfo = computed(() => {
|
||||||
height: cropperImageRect.height,
|
return `${selection.width} × ${selection.height}`
|
||||||
};
|
})
|
||||||
cropperselection.value.$change(
|
|
||||||
maxSelection.x,
|
const ratioInfo = computed(() => {
|
||||||
maxSelection.y,
|
const ratio = (selection.width / selection.height).toFixed(2)
|
||||||
maxSelection.width,
|
return selection.aspectRatio ?
|
||||||
maxSelection.height
|
`${selection.aspectRatio}:1` :
|
||||||
);
|
`${ratio}:1 (自由)`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 常量数据
|
||||||
|
const aspectRatios = [
|
||||||
|
{ value: 0, label: '自由' },
|
||||||
|
{ value: 1, label: '1:1 (方形)' },
|
||||||
|
{ value: 1.777, label: '16:9 (宽屏)' },
|
||||||
|
{ value: 0.75, label: '3:4 (竖屏)' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const shadeColors = [
|
||||||
|
{ value: '#000000', label: '黑色遮罩' },
|
||||||
|
{ value: '#3498db', label: '蓝色遮罩' },
|
||||||
|
{ value: '#e74c3c', label: '红色遮罩' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const components = [
|
||||||
|
{
|
||||||
|
name: 'CropperImage',
|
||||||
|
description: '承载原始图片的基础组件,负责加载和显示图片,支持跨域处理和图片大小限制。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CropperShade',
|
||||||
|
description: '创建遮罩层,突出显示裁剪区域,可调整透明度和颜色。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CropperHandle',
|
||||||
|
description: '裁剪框的控制手柄,用于调整选区大小和旋转,支持八个方向的手柄。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CropperCrosshair',
|
||||||
|
description: '十字准星,用于精确定位,可显示/隐藏和移动到指定位置。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CropperSelection',
|
||||||
|
description: '管理裁剪选区,包括位置、大小和约束条件,支持宽高比锁定。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CropperCanvas',
|
||||||
|
description: '画布容器,承载所有裁剪组件,提供绘制上下文和事件处理。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CropperViewer',
|
||||||
|
description: '实时预览裁剪结果的视图,同步显示裁剪区域的内容。'
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 图片引用
|
||||||
|
const img = new Image()
|
||||||
|
img.src = 'https://images.unsplash.com/photo-1506744038136-46273834b3fb'
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const setCanvasSize = () => {
|
||||||
|
if (!canvas.value) return
|
||||||
|
|
||||||
|
const container = canvas.value.parentElement
|
||||||
|
canvas.value.width = container.clientWidth
|
||||||
|
canvas.value.height = container.clientHeight
|
||||||
|
drawCanvas()
|
||||||
}
|
}
|
||||||
// 移动
|
|
||||||
function handleMove() {
|
const drawCanvas = () => {
|
||||||
if (!isCropperMove.value) {
|
if (!canvas.value || !previewCanvas.value) return
|
||||||
isCropperMove.value = true;
|
|
||||||
// 如果想要点击移动,清除选区,可以打开下面的代码注释
|
const ctx = canvas.value.getContext('2d')
|
||||||
// cropperselection.value.$clear();
|
const previewCtx = previewCanvas.value.getContext('2d')
|
||||||
|
|
||||||
|
// 清空画布
|
||||||
|
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
|
||||||
|
|
||||||
|
// 绘制背景
|
||||||
|
ctx.fillStyle = '#ecf0f1'
|
||||||
|
ctx.fillRect(0, 0, canvas.value.width, canvas.value.height)
|
||||||
|
|
||||||
|
// 绘制图片
|
||||||
|
if (img.complete) {
|
||||||
|
const scale = Math.min(
|
||||||
|
canvas.value.width / img.width,
|
||||||
|
canvas.value.height / img.height
|
||||||
|
)
|
||||||
|
const x = (canvas.value.width - img.width * scale) / 2
|
||||||
|
const y = (canvas.value.height - img.height * scale) / 2
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
if (rotation.value !== 0) {
|
||||||
|
ctx.translate(canvas.value.width / 2, canvas.value.height / 2)
|
||||||
|
ctx.rotate(rotation.value * Math.PI / 180)
|
||||||
|
ctx.translate(-canvas.value.width / 2, -canvas.value.height / 2)
|
||||||
|
}
|
||||||
|
ctx.drawImage(img, x, y, img.width * scale, img.height * scale)
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制遮罩
|
||||||
|
ctx.fillStyle = shade.color
|
||||||
|
ctx.globalAlpha = shade.opacity
|
||||||
|
ctx.fillRect(0, 0, canvas.value.width, canvas.value.height)
|
||||||
|
|
||||||
|
// 清除选区区域的遮罩
|
||||||
|
ctx.globalCompositeOperation = 'destination-out'
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x,
|
||||||
|
selection.y,
|
||||||
|
selection.width,
|
||||||
|
selection.height
|
||||||
|
)
|
||||||
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
// 绘制选区边框
|
||||||
|
ctx.strokeStyle = '#3498db'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.strokeRect(
|
||||||
|
selection.x,
|
||||||
|
selection.y,
|
||||||
|
selection.width,
|
||||||
|
selection.height
|
||||||
|
)
|
||||||
|
|
||||||
|
// 绘制控制手柄
|
||||||
|
const handleSize = 8
|
||||||
|
ctx.fillStyle = '#3498db'
|
||||||
|
|
||||||
|
// 四角手柄
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x - handleSize/2,
|
||||||
|
selection.y - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x + selection.width - handleSize/2,
|
||||||
|
selection.y - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x - handleSize/2,
|
||||||
|
selection.y + selection.height - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x + selection.width - handleSize/2,
|
||||||
|
selection.y + selection.height - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// 四边手柄
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x + selection.width/2 - handleSize/2,
|
||||||
|
selection.y - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x + selection.width/2 - handleSize/2,
|
||||||
|
selection.y + selection.height - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x - handleSize/2,
|
||||||
|
selection.y + selection.height/2 - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
ctx.fillRect(
|
||||||
|
selection.x + selection.width - handleSize/2,
|
||||||
|
selection.y + selection.height/2 - handleSize/2,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// 绘制十字准星
|
||||||
|
if (crosshair.visible) {
|
||||||
|
ctx.strokeStyle = '#e74c3c'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.setLineDash([5, 5])
|
||||||
|
|
||||||
|
// 横线
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(0, crosshair.y)
|
||||||
|
ctx.lineTo(canvas.value.width, crosshair.y)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 竖线
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(crosshair.x, 0)
|
||||||
|
ctx.lineTo(crosshair.x, canvas.value.height)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.setLineDash([])
|
||||||
|
|
||||||
|
// 中心点
|
||||||
|
ctx.fillStyle = '#e74c3c'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(crosshair.x, crosshair.y, 4, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新预览
|
||||||
|
updatePreview()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePreview = () => {
|
||||||
|
if (!previewCanvas.value || !canvas.value) return
|
||||||
|
|
||||||
|
const previewCtx = previewCanvas.value.getContext('2d')
|
||||||
|
previewCtx.clearRect(0, 0, previewCanvas.value.width, previewCanvas.value.height)
|
||||||
|
|
||||||
|
if (img.complete) {
|
||||||
|
const scale = Math.min(
|
||||||
|
canvas.value.width / img.width,
|
||||||
|
canvas.value.height / img.height
|
||||||
|
)
|
||||||
|
const imgX = (canvas.value.width - img.width * scale) / 2
|
||||||
|
const imgY = (canvas.value.height - img.height * scale) / 2
|
||||||
|
|
||||||
|
// 计算源图像中的对应区域
|
||||||
|
const srcX = (selection.x - imgX) / scale
|
||||||
|
const srcY = (selection.y - imgY) / scale
|
||||||
|
const srcWidth = selection.width / scale
|
||||||
|
const srcHeight = selection.height / scale
|
||||||
|
|
||||||
|
// 绘制到预览画布
|
||||||
|
previewCtx.drawImage(
|
||||||
|
img,
|
||||||
|
srcX, srcY, srcWidth, srcHeight,
|
||||||
|
0, 0, previewCanvas.value.width, previewCanvas.value.height
|
||||||
|
)
|
||||||
|
|
||||||
|
// 绘制预览边框
|
||||||
|
previewCtx.strokeStyle = '#3498db'
|
||||||
|
previewCtx.lineWidth = 2
|
||||||
|
previewCtx.strokeRect(0, 0, previewCanvas.value.width, previewCanvas.value.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const setAspectRatio = (ratio) => {
|
||||||
* 监听选择区变化
|
selection.aspectRatio = ratio
|
||||||
* @param event
|
if (ratio > 0) {
|
||||||
*/
|
selection.height = selection.width / ratio
|
||||||
function onCropperSelectionChange(event) {
|
}
|
||||||
if (event.detail.width && event.detail.height) {
|
drawCanvas()
|
||||||
isCropperSelection.value = true;
|
}
|
||||||
} else {
|
|
||||||
isCropperSelection.value = false;
|
const setShadeColor = (color) => {
|
||||||
|
shade.color = color
|
||||||
|
drawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInteractionMode = (mode) => {
|
||||||
|
interactionMode.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCrosshair = () => {
|
||||||
|
crosshair.visible = true
|
||||||
|
drawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideCrosshair = () => {
|
||||||
|
crosshair.visible = false
|
||||||
|
drawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveCrosshair = () => {
|
||||||
|
crosshair.x = Math.random() * canvas.value.width
|
||||||
|
crosshair.y = Math.random() * canvas.value.height
|
||||||
|
drawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSelection = () => {
|
||||||
|
selection.x = 100
|
||||||
|
selection.y = 100
|
||||||
|
selection.width = 300
|
||||||
|
selection.height = 200
|
||||||
|
selection.aspectRatio = 0
|
||||||
|
rotation.value = 0
|
||||||
|
drawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotateSelection = () => {
|
||||||
|
rotation.value = (rotation.value + 90) % 360
|
||||||
|
drawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cropImage = () => {
|
||||||
|
alert('裁剪功能已触发!在实际应用中,这里会执行裁剪操作。')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadResult = () => {
|
||||||
|
alert('下载功能已触发!在实际应用中,这里会下载裁剪后的图片。')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画布事件处理
|
||||||
|
const onCanvasMouseDown = (e) => {
|
||||||
|
if (!canvas.value) return
|
||||||
|
|
||||||
|
const rect = canvas.value.getBoundingClientRect()
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const y = e.clientY - rect.top
|
||||||
|
|
||||||
|
// 检查是否点击在选区内
|
||||||
|
if (x >= selection.x && x <= selection.x + selection.width &&
|
||||||
|
y >= selection.y && y <= selection.y + selection.height) {
|
||||||
|
selection.isMoving = true
|
||||||
|
selection.startX = x - selection.x
|
||||||
|
selection.startY = y - selection.y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const onCanvasMouseMove = (e) => {
|
||||||
* 确认裁剪
|
if (selection.isMoving && canvas.value) {
|
||||||
*/
|
const rect = canvas.value.getBoundingClientRect()
|
||||||
const emit = defineEmits(["success"]);
|
const x = e.clientX - rect.left
|
||||||
async function handleConfirm() {
|
const y = e.clientY - rect.top
|
||||||
if (isCropperSelection.value) {
|
|
||||||
const res = await cropperselection.value.$toCanvas();
|
selection.x = x - selection.startX
|
||||||
|
selection.y = y - selection.startY
|
||||||
const dataImage = res.toDataURL("image/png");
|
|
||||||
const file = dataURLtoFile(dataImage, fileObj.value.name);
|
// 限制选区不超出画布
|
||||||
emit("success", {
|
selection.x = Math.max(
|
||||||
...fileObj.value,
|
0,
|
||||||
file: file,
|
Math.min(canvas.value.width - selection.width, selection.x)
|
||||||
fileShow: dataImage,
|
)
|
||||||
});
|
selection.y = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(canvas.value.height - selection.height, selection.y)
|
||||||
|
)
|
||||||
|
|
||||||
|
drawCanvas()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 将data:image转成新的file
|
|
||||||
function dataURLtoFile(dataurl, filename) {
|
const onCanvasMouseUp = () => {
|
||||||
var arr = dataurl.split(","),
|
selection.isMoving = false
|
||||||
mime = arr[0].match(/:(.*?);/)[1],
|
|
||||||
bstr = atob(arr[1]),
|
|
||||||
n = bstr.length,
|
|
||||||
u8arr = new Uint8Array(n);
|
|
||||||
while (n--) {
|
|
||||||
u8arr[n] = bstr.charCodeAt(n);
|
|
||||||
}
|
|
||||||
const blob = new Blob([u8arr], { type: mime });
|
|
||||||
const file = new File([blob], filename, { type: mime });
|
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const onCanvasMouseLeave = () => {
|
||||||
* 文件上传
|
selection.isMoving = false
|
||||||
*/
|
|
||||||
const input_form = ref();
|
|
||||||
function handleUploadSuccess() {
|
|
||||||
const files = input_form.value.files;
|
|
||||||
|
|
||||||
if (files.length) {
|
|
||||||
fileObj.value = {
|
|
||||||
name: files[0].name,
|
|
||||||
file: files[0],
|
|
||||||
fileShow: URL.createObjectURL(files[0]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
setCanvasSize()
|
||||||
|
window.addEventListener('resize', setCanvasSize)
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
drawCanvas()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', setCanvasSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听器
|
||||||
|
watch([shade, selection], () => {
|
||||||
|
drawCanvas()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(() => shade.opacity, () => {
|
||||||
|
drawCanvas()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dialog_wrap {
|
.cropper-demo {
|
||||||
display: flex;
|
min-height: 100vh;
|
||||||
.image_wrap {
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
width: 300px;
|
padding: 20px;
|
||||||
height: 300px;
|
color: #333;
|
||||||
flex-shrink: 0;
|
}
|
||||||
|
|
||||||
cropper-canvas {
|
.container {
|
||||||
width: 100%;
|
max-width: 1200px;
|
||||||
height: 100%;
|
margin: 0 auto;
|
||||||
}
|
|
||||||
}
|
|
||||||
.info_wrap {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #7f8c8d;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-area {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 500px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
flex: 0 0 300px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
border: 2px dashed #bdc3c7;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-viewer {
|
||||||
|
width: 200px;
|
||||||
|
height: 150px;
|
||||||
|
border: 2px solid #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
& + button {
|
padding: 10px 15px;
|
||||||
margin-left: 20px;
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-info {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h4 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crosshair-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crosshair-controls button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.demo-area {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-container, .controls {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button.active {
|
</style>
|
||||||
background-color: #c6dff8;
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Reference in New Issue
Block a user