feat: 图片裁剪组件

This commit is contained in:
2026-04-21 13:47:23 +08:00
parent 8a9b217314
commit 429c7db195
3 changed files with 169 additions and 110 deletions

View File

@@ -1,6 +1,7 @@
<template> <template>
<a-modal <a-modal
class="image-clip-dialog generalModel" class="image-clip-dialog generalModel"
:class="{ 'is-product': data.isProduct }"
v-model:visible="show" v-model:visible="show"
:footer="null" :footer="null"
width="70%" width="70%"
@@ -25,10 +26,19 @@
ref="imageClipRef" ref="imageClipRef"
v-bind="$attrs" v-bind="$attrs"
:ratio="data.ratio" :ratio="data.ratio"
:fixedBox="type !== 'apparel'"
:url="data.url" :url="data.url"
@change="(v) => (data.preview_url = v)" @change="(v) => (data.preview_url = v)"
/> />
<div class="preview" v-if="data.isPreview"> <div
class="preview"
:class="{
'is-product': data.isProduct,
'is-apprael': type === 'apprael',
'is-cover': type === 'cover'
}"
v-if="data.isPreview"
>
<div class="title"> <div class="title">
<span class="icon"><svg-icon name="seller-preview" size="24" /></span> <span class="icon"><svg-icon name="seller-preview" size="24" /></span>
<span class="label">Crop Preview</span> <span class="label">Crop Preview</span>
@@ -44,124 +54,156 @@
</template> </template>
<script setup> <script setup>
import { ref } from "vue" import { ref } from "vue"
import ImageClip from "./image-clip.vue" import ImageClip from "./image-clip.vue"
const data = reactive({
url: "", defineProps({
title: "Crop Image", type: {
preview_url: "", type: String,
ratio: [1, 1], default: () => false
isPreview: true, }
callback: null })
})
const show = ref(false) const data = reactive({
const open = (url, callback, options) => { url: "",
if (!url || !callback) return title: "Crop Image",
data.url = url preview_url: "",
data.callback = callback ratio: [1, 1],
data.ratio = [1, 1] isPreview: true,
data.isPreview = true callback: null,
data.preview_url = "" isProduct: false // 是否商品编辑
data.title = "Crop Image" })
if (options) { const show = ref(false)
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview const open = (url, callback, options) => {
if (options.hasOwnProperty("ratio")) data.ratio = options.ratio if (!url || !callback) return
if (options.hasOwnProperty("title")) data.title = options.title data.url = url
} data.callback = callback
show.value = true data.ratio = options.ratio || [1, 1]
} data.isPreview = true
const onCancel = () => { data.preview_url = ""
show.value = false data.title = options.title || "Crop Image"
} if (options) {
const imageClipRef = ref(null) if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
const onSubmit = () => { data.isProduct = options.isProduct
imageClipRef.value.getCropBlob().then((blob) => { }
if (data.callback) data.callback(blobToFile(blob, "image.png")) show.value = true
onCancel() }
}) const onCancel = () => {
} show.value = false
// 将blob转换为file对象 }
const blobToFile = (blob, fileName) => { const imageClipRef = ref(null)
return new File([blob], fileName, { type: blob.type }) const onSubmit = () => {
} imageClipRef.value.getCropBlob().then((blob) => {
defineExpose({ if (data.callback) data.callback(blobToFile(blob, "image.png"))
open onCancel()
}) })
}
// 将blob转换为file对象
const blobToFile = (blob, fileName) => {
return new File([blob], fileName, { type: blob.type })
}
defineExpose({
open
})
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.image-clip-dialog-box { .image-clip-dialog-box {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
.submit {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: #262626;
color: #fff;
cursor: pointer;
}
> .header {
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
.submit { margin-bottom: 5rem;
width: 4rem; > .title {
height: 4rem; font-family: pingfang_heavy;
border-radius: 50%; font-size: 2.4rem;
background: #262626; color: #595959;
color: #fff;
cursor: pointer;
} }
> .header { > .right {
display: flex;
justify-content: space-between;
margin-bottom: 5rem;
> .title {
font-family: pingfang_heavy;
font-size: 2.4rem;
color: #595959;
}
> .right {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
> button {
width: 10rem;
height: 4.8rem;
border-radius: 4rem;
border: none;
background: #e4e5eb;
font-family: pingfang_heavy;
font-size: 1.6rem;
color: #000;
}
}
}
> .content {
flex: 1;
overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
> .image-clip { gap: 2rem;
flex: 1; > button {
width: 10rem;
height: 4.8rem;
border-radius: 4rem;
border: none;
background: #e4e5eb;
font-family: pingfang_heavy;
font-size: 1.6rem;
color: #000;
} }
> .preview { }
margin-left: 6rem; }
width: 28rem; > .content {
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
> .image-clip {
flex: 1;
&.is-product {
width: initial;
flex: none;
}
}
> .preview {
margin-left: 6rem;
width: 28rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2.4rem;
> .title {
display: flex; display: flex;
flex-direction: column;
justify-content: center;
align-items: center; align-items: center;
gap: 2.4rem; justify-content: center;
> .title { gap: 1.2rem;
display: flex; > .label {
align-items: center; font-family: pingfang_heavy;
justify-content: center; font-size: 1.6rem;
gap: 1.2rem;
> .label {
font-family: pingfang_heavy;
font-size: 1.6rem;
}
} }
> img { }
width: 100%; > img {
height: auto; width: 100%;
margin-bottom: 3rem; height: auto;
margin-bottom: 3rem;
}
&.is-product {
margin-left: 0;
height: 100%;
justify-content: flex-start;
img {
width: 20.8rem;
height: 36.7rem;
margin-bottom: 0;
box-shadow: 4px 4px 16px 0px #0000000f;
border: 1px solid #ededed;
}
&.is-cover{
img{
// width: 29.7rem;
// height: 37.5rem;
}
} }
} }
} }
} }
}
</style> </style>

View File

@@ -10,6 +10,7 @@
fixed fixed
movable movable
centerBox centerBox
:fixedBox="fixedBox"
@realTime="onChange" @realTime="onChange"
v-bind="$attrs" v-bind="$attrs"
outputType="png" outputType="png"
@@ -50,6 +51,10 @@ const props = defineProps({
isProduct: { isProduct: {
type: Boolean, type: Boolean,
default: false default: false
},
fixedBox: {
type: Boolean,
default:true
} }
}) })
const attrs = useAttrs() const attrs = useAttrs()

View File

@@ -41,7 +41,7 @@
<div <div
v-if="previewImageMap[type]" v-if="previewImageMap[type]"
class="crop-tool flex flex-center" class="crop-tool flex flex-center"
@click="handleClickCrop(previewImageMap[type])" @click="handleClickCrop(previewImageMap[type], type)"
> >
<SvgIcon name="CCrop" color="#fff" size="12" /> <SvgIcon name="CCrop" color="#fff" size="12" />
</div> </div>
@@ -128,7 +128,10 @@
class="sketch-element flex flex-center" class="sketch-element flex flex-center"
> >
<img class="img-src" :src="item.url" alt="" /> <img class="img-src" :src="item.url" alt="" />
<div class="crop-tool flex flex-center"> <div
class="crop-tool flex flex-center"
@click="handleClickCrop(item.url, 'apparel')"
>
<SvgIcon name="CCrop" color="#fff" size="12" /> <SvgIcon name="CCrop" color="#fff" size="12" />
</div> </div>
</div> </div>
@@ -222,6 +225,8 @@
isProduct isProduct
:info="false" :info="false"
:autoCropWidth="90" :autoCropWidth="90"
v-bind="$attrs"
:type="cropType"
/> />
</template> </template>
@@ -394,14 +399,21 @@ const handleSelectProdImg = (index: number) => {
} }
} }
const handleClickCrop = (data) => { const cropType = ref("")
console.log(data) const handleClickCrop = (data, type) => {
console.log(data, type)
const titleList = {
sketch: "Crop Sketch",
mainProductImage: "Crop Main Product Image",
cover: "Crop Cover"
}
cropType.value = type
imageClipDialogRef.value.open( imageClipDialogRef.value.open(
data, data,
(file) => { (file) => {
selectList.value[currentIndex.value].sketch = URL.createObjectURL(file) selectList.value[currentIndex.value].sketch = URL.createObjectURL(file)
}, },
{ ratio: [9, 16], isPreview: true, title: "Crop Brand Banner" } { ratio: [9, 16], isPreview: true, title: titleList[type], isProduct: true }
) )
} }