351 lines
7.0 KiB
Vue
351 lines
7.0 KiB
Vue
<!-- 图片裁剪组件 github: https://github.com/acccccccb/vue-img-cutter/tree/master -->
|
|
<template>
|
|
<div class="cutter-container">
|
|
<div class="cutter-component">
|
|
<div class="title">{{ title }}</div>
|
|
<ImgCutter
|
|
ref="imgCutterModal"
|
|
@cutDown="cutDownImg"
|
|
@onPrintImg="cutterPrintImg"
|
|
@onImageLoadComplete="handleImageLoadComplete"
|
|
@onImageLoadError="handleImageLoadError"
|
|
@onClearAll="handleClearAll"
|
|
v-bind="cutterProps"
|
|
class="img-cutter"
|
|
>
|
|
<template #choose>
|
|
<ElButton type="primary" plain v-ripple>选择图片</ElButton>
|
|
</template>
|
|
<template #cancel>
|
|
<ElButton type="danger" plain v-ripple>清除</ElButton>
|
|
</template>
|
|
<template #confirm>
|
|
<!-- <ElButton type="primary" style="margin-left: 10px">确定</ElButton> -->
|
|
<div></div>
|
|
</template>
|
|
</ImgCutter>
|
|
</div>
|
|
|
|
<div v-if="showPreview" class="preview-container">
|
|
<div class="title">{{ previewTitle }}</div>
|
|
<div
|
|
class="preview-box"
|
|
:style="{
|
|
width: `${cutterProps.cutWidth}px`,
|
|
height: `${cutterProps.cutHeight}px`
|
|
}"
|
|
>
|
|
<img class="preview-img" :src="temImgPath" alt="预览图" v-if="temImgPath" />
|
|
</div>
|
|
<ElButton class="download-btn" @click="downloadImg" :disabled="!temImgPath" v-ripple
|
|
>下载图片</ElButton
|
|
>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import ImgCutter from 'vue-img-cutter'
|
|
|
|
defineOptions({ name: 'ArtCutterImg' })
|
|
|
|
interface CutterProps {
|
|
// 基础配置
|
|
/** 是否模态框 */
|
|
isModal?: boolean
|
|
/** 是否显示工具栏 */
|
|
tool?: boolean
|
|
/** 工具栏背景色 */
|
|
toolBgc?: string
|
|
/** 标题 */
|
|
title?: string
|
|
/** 预览标题 */
|
|
previewTitle?: string
|
|
/** 是否显示预览 */
|
|
showPreview?: boolean
|
|
|
|
// 尺寸相关
|
|
/** 容器宽度 */
|
|
boxWidth?: number
|
|
/** 容器高度 */
|
|
boxHeight?: number
|
|
/** 裁剪宽度 */
|
|
cutWidth?: number
|
|
/** 裁剪高度 */
|
|
cutHeight?: number
|
|
/** 是否允许大小调整 */
|
|
sizeChange?: boolean
|
|
|
|
// 移动和缩放
|
|
/** 是否允许移动 */
|
|
moveAble?: boolean
|
|
/** 是否允许图片移动 */
|
|
imgMove?: boolean
|
|
/** 是否允许缩放 */
|
|
scaleAble?: boolean
|
|
|
|
// 图片相关
|
|
/** 是否显示原始图片 */
|
|
originalGraph?: boolean
|
|
/** 是否允许跨域 */
|
|
crossOrigin?: boolean
|
|
/** 文件类型 */
|
|
fileType?: 'png' | 'jpeg' | 'webp'
|
|
/** 质量 */
|
|
quality?: number
|
|
|
|
// 水印
|
|
/** 水印文本 */
|
|
watermarkText?: string
|
|
/** 水印字体大小 */
|
|
watermarkFontSize?: number
|
|
/** 水印颜色 */
|
|
watermarkColor?: string
|
|
|
|
// 其他功能
|
|
/** 是否保存裁剪位置 */
|
|
saveCutPosition?: boolean
|
|
/** 是否预览模式 */
|
|
previewMode?: boolean
|
|
|
|
// 输入图片
|
|
imgUrl?: string
|
|
}
|
|
|
|
interface CutterResult {
|
|
fileName: string
|
|
file: File
|
|
blob: Blob
|
|
dataURL: string
|
|
}
|
|
|
|
const props = withDefaults(defineProps<CutterProps>(), {
|
|
// 基础配置默认值
|
|
isModal: false,
|
|
tool: true,
|
|
toolBgc: '#fff',
|
|
title: '',
|
|
previewTitle: '',
|
|
showPreview: true,
|
|
|
|
// 尺寸相关默认值
|
|
boxWidth: 700,
|
|
boxHeight: 458,
|
|
cutWidth: 470,
|
|
cutHeight: 270,
|
|
sizeChange: true,
|
|
|
|
// 移动和缩放默认值
|
|
moveAble: true,
|
|
imgMove: true,
|
|
scaleAble: true,
|
|
|
|
// 图片相关默认值
|
|
originalGraph: true,
|
|
crossOrigin: true,
|
|
fileType: 'png',
|
|
quality: 0.9,
|
|
|
|
// 水印默认值
|
|
watermarkText: '',
|
|
watermarkFontSize: 20,
|
|
watermarkColor: '#ffffff',
|
|
|
|
// 其他功能默认值
|
|
saveCutPosition: true,
|
|
previewMode: true
|
|
})
|
|
|
|
const emit = defineEmits(['update:imgUrl', 'error', 'imageLoadComplete', 'imageLoadError'])
|
|
|
|
const temImgPath = ref('')
|
|
const imgCutterModal = ref()
|
|
|
|
// 计算属性:整合所有ImgCutter的props
|
|
const cutterProps = computed(() => ({
|
|
...props,
|
|
WatermarkText: props.watermarkText,
|
|
WatermarkFontSize: props.watermarkFontSize,
|
|
WatermarkColor: props.watermarkColor
|
|
}))
|
|
|
|
// 图片预加载
|
|
function preloadImage(url: string): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image()
|
|
img.crossOrigin = 'anonymous'
|
|
img.onload = () => resolve()
|
|
img.onerror = reject
|
|
img.src = url
|
|
})
|
|
}
|
|
|
|
// 初始化裁剪器
|
|
async function initImgCutter() {
|
|
if (props.imgUrl) {
|
|
try {
|
|
await preloadImage(props.imgUrl)
|
|
imgCutterModal.value?.handleOpen({
|
|
name: '封面图片',
|
|
src: props.imgUrl
|
|
})
|
|
} catch (error) {
|
|
emit('error', error)
|
|
console.error('图片加载失败:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 生命周期钩子
|
|
onMounted(() => {
|
|
if (props.imgUrl) {
|
|
temImgPath.value = props.imgUrl
|
|
initImgCutter()
|
|
}
|
|
})
|
|
|
|
// 监听图片URL变化
|
|
watch(
|
|
() => props.imgUrl,
|
|
(newVal) => {
|
|
if (newVal) {
|
|
temImgPath.value = newVal
|
|
initImgCutter()
|
|
}
|
|
}
|
|
)
|
|
|
|
// 实时预览
|
|
function cutterPrintImg(result: { dataURL: string }) {
|
|
temImgPath.value = result.dataURL
|
|
}
|
|
|
|
// 裁剪完成
|
|
function cutDownImg(result: CutterResult) {
|
|
emit('update:imgUrl', result.dataURL)
|
|
}
|
|
|
|
// 图片加载完成
|
|
function handleImageLoadComplete(result: any) {
|
|
emit('imageLoadComplete', result)
|
|
}
|
|
|
|
// 图片加载失败
|
|
function handleImageLoadError(error: any) {
|
|
emit('error', error)
|
|
emit('imageLoadError', error)
|
|
}
|
|
|
|
// 清除所有
|
|
function handleClearAll() {
|
|
temImgPath.value = ''
|
|
}
|
|
|
|
// 下载图片
|
|
function downloadImg() {
|
|
console.log('下载图片')
|
|
const a = document.createElement('a')
|
|
a.href = temImgPath.value
|
|
a.download = 'image.png'
|
|
a.click()
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.cutter-container {
|
|
display: flex;
|
|
flex-flow: row wrap;
|
|
|
|
.title {
|
|
padding-bottom: 10px;
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.cutter-component {
|
|
margin-right: 30px;
|
|
}
|
|
|
|
.preview-container {
|
|
.preview-box {
|
|
background-color: var(--art-active-color) !important;
|
|
|
|
.preview-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
}
|
|
|
|
.download-btn {
|
|
display: block;
|
|
margin: 20px auto;
|
|
}
|
|
}
|
|
|
|
:deep(.toolBoxControl) {
|
|
z-index: 100;
|
|
}
|
|
|
|
:deep(.dockMain) {
|
|
right: 0;
|
|
bottom: -40px;
|
|
left: 0;
|
|
z-index: 10;
|
|
padding: 0;
|
|
background-color: transparent !important;
|
|
opacity: 1;
|
|
}
|
|
|
|
:deep(.copyright) {
|
|
display: none !important;
|
|
}
|
|
|
|
:deep(.i-dialog-footer) {
|
|
margin-top: 60px !important;
|
|
}
|
|
|
|
:deep(.dockBtn) {
|
|
height: 26px;
|
|
padding: 0 10px;
|
|
font-size: 12px;
|
|
line-height: 26px;
|
|
color: var(--el-color-primary) !important;
|
|
background-color: var(--el-color-primary-light-9) !important;
|
|
border: 1px solid var(--el-color-primary-light-4) !important;
|
|
}
|
|
|
|
:deep(.dockBtnScrollBar) {
|
|
margin: 0 10px 0 6px;
|
|
background-color: var(--el-color-primary-light-1);
|
|
}
|
|
|
|
:deep(.scrollBarControl) {
|
|
border-color: var(--el-color-primary);
|
|
}
|
|
|
|
:deep(.closeIcon) {
|
|
line-height: 15px !important;
|
|
}
|
|
}
|
|
|
|
.dark {
|
|
.cutter-container {
|
|
:deep(.toolBox) {
|
|
border: transparent;
|
|
}
|
|
|
|
:deep(.dialogMain) {
|
|
background-color: transparent !important;
|
|
}
|
|
|
|
:deep(.i-dialog-footer) {
|
|
.btn {
|
|
background-color: var(--el-color-primary) !important;
|
|
border: transparent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|