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

View File

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

View File

@@ -41,7 +41,7 @@
<div
v-if="previewImageMap[type]"
class="crop-tool flex flex-center"
@click="handleClickCrop(previewImageMap[type])"
@click="handleClickCrop(previewImageMap[type], type)"
>
<SvgIcon name="CCrop" color="#fff" size="12" />
</div>
@@ -128,7 +128,10 @@
class="sketch-element flex flex-center"
>
<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" />
</div>
</div>
@@ -222,6 +225,8 @@
isProduct
:info="false"
:autoCropWidth="90"
v-bind="$attrs"
:type="cropType"
/>
</template>
@@ -394,14 +399,21 @@ const handleSelectProdImg = (index: number) => {
}
}
const handleClickCrop = (data) => {
console.log(data)
const cropType = ref("")
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(
data,
(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 }
)
}