修改:优化cartoon的图片裁剪比例为3:4

This commit is contained in:
Linzm committed 2026-11-03 16:05:31 +08:00
1 parent 505c585772
commit 482f6a57ce
1 file changed
+275 -66
+275 -66
View File
@@ -177,7 +177,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="photo-upload-body"> <div class="photo-upload-body" @click="triggerPick">
<div v-if="!picture" class="photo-upload-box"> <div v-if="!picture" class="photo-upload-box">
<div class="photo-upload-area"> <div class="photo-upload-area">
<div class="photo-upload-plus"> <div class="photo-upload-plus">
@@ -195,10 +195,60 @@
</div> </div>
</div> </div>
<img class="photo-upload-img" v-if="picture" :src="picture" alt="" srcset=""> <img class="photo-upload-img" v-if="picture" :src="picture" alt="" srcset="">
<div class="photo-upload-box-1"> <input
<h5-cropper :option="option" @getbase64Data="getbase64Data" @imgorigoinf="imgorigoinf"></h5-cropper> ref="fileInput"
type="file"
accept="image/*"
class="photo-upload-input"
@change="onFileChange"
@click.stop
>
</div>
<Teleport to="body">
<div v-if="cropping" class="ct-cropper">
<div class="ct-cropper__stage">
<VueCropper
ref="cropperRef"
:img="cropImg"
:auto-crop="true"
:auto-crop-width="9999"
:auto-crop-height="9999"
:fixed="true"
:fixed-number="config.upload.crop.ratio"
:fixed-box="false"
:center-box="false"
:can-move="true"
:can-move-box="true"
:can-scale="true"
:info="true"
:info-true="true"
:full="false"
:high="true"
:enlarge="config.upload.crop.enlarge"
:output-type="config.upload.crop.outputType"
:output-size="1"
:fill-color="config.upload.crop.fillColor"
mode="contain"
/>
</div>
<div class="ct-cropper__bar">
<button class="ct-cropper__btn" type="button" @click="onCropCancel">{{ toValueWithout("取消") }}</button>
<div class="ct-cropper__zoom">
<button class="ct-cropper__zoom-btn" type="button" aria-label="缩小" @click="onCropZoom(-1)">-</button>
<button class="ct-cropper__zoom-btn" type="button" aria-label="放大" @click="onCropZoom(1)">+</button>
</div>
<button
class="ct-cropper__btn ct-cropper__btn--ok"
type="button"
:disabled="cropConfirming"
@click="onCropConfirm"
>
{{ toValueWithout("确定") }}
</button>
</div> </div>
</div> </div>
</Teleport>
<div :style="`height: 120px;`"></div> <div :style="`height: 120px;`"></div>
<div class="design-action-bar"> <div class="design-action-bar">
<div class="design-left"> <div class="design-left">
@@ -214,9 +264,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import H5Cropper from 'vue-cropper-h5' import { VueCropper } from 'vue-cropper'
import "vue-cropper-h5/dist/style.css" import 'vue-cropper/dist/index.css'
import { showLoadingToast, showToast, closeToast, showFailToast, showSuccessToast } from 'vant'; import { showLoadingToast, showToast, closeToast, showFailToast, showSuccessToast } from 'vant';
import type { UploaderFileListItem } from 'vant' import type { UploaderFileListItem } from 'vant'
import * as badgeApi from '@/api/badge' import * as badgeApi from '@/api/badge'
@@ -237,25 +288,11 @@ const config = cartoonConfig
const referPicture = ref<UploaderFileListItem[]>([]) const referPicture = ref<UploaderFileListItem[]>([])
const router = useRouter() const router = useRouter()
// 初始化裁剪器配置 const fileInput = ref<HTMLInputElement | null>(null)
const option = ref({ const cropperRef = ref<any>(null)
...config.upload.cropOptions const cropping = ref(false)
}) const cropImg = ref('')
const cropConfirming = ref(false)
// 判断是否是APP
const openApp = () => {
const ua = navigator.userAgent;
const isIOS = /iPhone|iPad|iPod/i.test(ua);
const isAndroid = /Android/i.test(ua);
if (isIOS) {
option.value.ceilbutton = true
} else if (isAndroid) {
option.value.ceilbutton = false
} else {
option.value.ceilbutton = true
}
}
const imgurl = ref('') const imgurl = ref('')
@@ -437,31 +474,57 @@ const picture = ref(null)
const imageWidth = ref(0); const imageWidth = ref(0);
const imageHeight = ref(0); const imageHeight = ref(0);
function getbase64Data(data: string) { let lockedScrollY = 0
picture.value = data;
const img = new Image(); function preventTouchScroll(e: TouchEvent) {
img.src = data; const host = document.querySelector('.ct-cropper')
img.onload = () => { if (host && e.target instanceof Node && host.contains(e.target)) return
imageWidth.value = img.naturalWidth; e.preventDefault()
imageHeight.value = img.naturalHeight; }
if (imageWidth.value < config.upload.minWidth || imageHeight.value < config.upload.minHeight) {
showToast(toValueWithout(`请上传尺寸大于${config.upload.minWidth}*${config.upload.minHeight}像素的照片`)) function setBodyScrollLocked(locked: boolean) {
imgurl.value = null const html = document.documentElement
picture.value = null const body = document.body
return false; if (locked) {
} lockedScrollY = window.scrollY || window.pageYOffset || 0
const blob = base64ToBlob(data); html.style.overflow = 'hidden'
if (blob.size > config.upload.maxSize) { body.style.overflow = 'hidden'
showToast(toValueWithout(`照片大小不能超过${config.upload.maxSize / 1024 / 1024}M`)) body.style.position = 'fixed'
imgurl.value = null body.style.top = `-${lockedScrollY}px`
picture.value = null body.style.left = '0'
return false; body.style.right = '0'
} body.style.width = '100%'
imgurl.value = blob; document.addEventListener('touchmove', preventTouchScroll, { passive: false })
} else {
html.style.overflow = ''
body.style.overflow = ''
body.style.position = ''
body.style.top = ''
body.style.left = ''
body.style.right = ''
body.style.width = ''
document.removeEventListener('touchmove', preventTouchScroll)
window.scrollTo(0, lockedScrollY)
} }
} }
function openCropping(dataUrl: string) {
cropImg.value = dataUrl
cropping.value = true
setBodyScrollLocked(true)
}
function closeCropping() {
cropping.value = false
cropImg.value = ''
cropConfirming.value = false
setBodyScrollLocked(false)
}
function triggerPick() {
fileInput.value?.click()
}
function base64ToBlob(base64: string) { function base64ToBlob(base64: string) {
const arr = base64.split(','); const arr = base64.split(',');
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg'; const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg';
@@ -476,29 +539,98 @@ function base64ToBlob(base64: string) {
}); });
} }
function imgorigoinf(data: File) { function applyCroppedData(data: string) {
const img = new Image(); picture.value = data
const objectUrl = URL.createObjectURL(data); const img = new Image()
img.src = objectUrl; img.src = data
img.onload = () => { img.onload = () => {
imageWidth.value = img.naturalWidth; imageWidth.value = img.naturalWidth
imageHeight.value = img.naturalHeight; imageHeight.value = img.naturalHeight
if (imageWidth.value < config.upload.minWidth || imageHeight.value < config.upload.minHeight) { if (imageWidth.value < config.upload.minWidth || imageHeight.value < config.upload.minHeight) {
showToast(toValueWithout(`请上传尺寸大于${config.upload.minWidth}*${config.upload.minHeight}像素的照片`)) showToast(toValueWithout(`请上传尺寸大于${config.upload.minWidth}*${config.upload.minHeight}像素的照片`))
setTimeout(() => {
const btn = document.querySelector('.btn')
if (btn) {
(btn as HTMLElement).click()
}
}, 1500);
imgurl.value = null imgurl.value = null
picture.value = null picture.value = null
return return
} }
const blob = base64ToBlob(data)
if (blob.size > config.upload.maxSize) {
showToast(toValueWithout(`照片大小不能超过${config.upload.maxSize / 1024 / 1024}M`))
imgurl.value = null
picture.value = null
return
}
imgurl.value = blob
}
img.onerror = () => {
showToast(toValueWithout('裁剪图片加载失败'))
imgurl.value = null
picture.value = null
}
}
function onCropCancel() {
closeCropping()
}
/** 裁剪页缩放:正数放大、负数缩小;也支持双指捏合 */
function onCropZoom(step: number) {
const cropper = cropperRef.value as any
cropper?.changeScale?.(step)
}
function onCropConfirm() {
if (cropConfirming.value) return
const cropper = cropperRef.value as any
if (!cropper?.getCropData) {
showToast(toValueWithout('裁剪组件未就绪'))
return
}
cropConfirming.value = true
cropper.getCropData((data: string) => {
closeCropping()
if (!data) {
showToast(toValueWithout('裁剪失败'))
return
}
applyCroppedData(data)
})
}
function onFileChange(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) return
if (file.size > config.upload.maxSize) {
showToast(toValueWithout(`照片大小不能超过${config.upload.maxSize / 1024 / 1024}M`))
return
}
const objectUrl = URL.createObjectURL(file)
const img = new Image()
img.src = objectUrl
img.onload = () => {
imageWidth.value = img.naturalWidth
imageHeight.value = img.naturalHeight
URL.revokeObjectURL(objectUrl) URL.revokeObjectURL(objectUrl)
if (imageWidth.value < config.upload.minWidth || imageHeight.value < config.upload.minHeight) {
showToast(toValueWithout(`请上传尺寸大于${config.upload.minWidth}*${config.upload.minHeight}像素的照片`))
return
}
const reader = new FileReader()
reader.onload = (ev) => {
const dataUrl = String(ev.target?.result || '')
if (!dataUrl) {
showToast(toValueWithout('图片读取失败'))
return
}
nextTick(() => openCropping(dataUrl))
}
reader.onerror = () => showToast(toValueWithout('图片读取失败'))
reader.readAsDataURL(file)
} }
img.onerror = () => { img.onerror = () => {
URL.revokeObjectURL(objectUrl) URL.revokeObjectURL(objectUrl)
showToast(toValueWithout('图片加载失败'))
} }
} }
@@ -697,10 +829,13 @@ const createLog = () => {
} }
onMounted(() => { onMounted(() => {
openApp()
getOrderStat() getOrderStat()
getSizeList() getSizeList()
}) })
onUnmounted(() => {
if (cropping.value) closeCropping()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -843,6 +978,7 @@ onMounted(() => {
height: 80vw; height: 80vw;
margin: 16px auto 0 auto; margin: 16px auto 0 auto;
text-align: center; text-align: center;
cursor: pointer;
} }
.photo-upload-box { .photo-upload-box {
margin: 16px auto 0 auto; margin: 16px auto 0 auto;
@@ -856,21 +992,94 @@ onMounted(() => {
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
} }
.photo-upload-box-1 { .photo-upload-input {
width: 80vw;
height: 70vw;
border-radius: 12px;
background: rgba(0, 0, 0, 0);
position: absolute; position: absolute;
bottom: 0; inset: 0;
left: 0; width: 100%;
right: 0; height: 100%;
opacity: 0;
pointer-events: none;
} }
.photo-upload-img { .photo-upload-img {
max-width: 80vw; max-width: 80vw;
max-height: 80vw; max-height: 80vw;
border-radius: 12px; border-radius: 12px;
} }
/* 全屏裁剪层:3:4 + 可缩放 */
.ct-cropper {
position: fixed;
inset: 0;
z-index: 2147483000;
display: flex;
flex-direction: column;
background: #000;
overscroll-behavior: none;
}
.ct-cropper__stage {
flex: 1;
min-height: 0;
width: 100%;
position: relative;
}
.ct-cropper__stage :deep(.vue-cropper) {
width: 100% !important;
height: 100% !important;
background: #111 !important;
touch-action: none;
}
.ct-cropper__stage :deep(.cropper-view-box) {
outline: 2px solid #fff !important;
}
.ct-cropper__stage :deep(.crop-point) {
background-color: #50cf54 !important;
opacity: 0.95;
}
.ct-cropper__bar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
box-sizing: border-box;
background: #111;
}
.ct-cropper__zoom {
display: flex;
align-items: center;
gap: 10px;
}
.ct-cropper__zoom-btn {
width: 40px;
height: 40px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
color: #fff;
font-size: 22px;
line-height: 1;
padding: 0;
}
.ct-cropper__btn {
height: 40px;
line-height: 40px;
padding: 0 20px;
border: none;
border-radius: 20px;
background: transparent;
color: #fff;
font-size: 15px;
}
.ct-cropper__btn--ok {
background: #50cf54;
color: #fff;
font-weight: 600;
}
.ct-cropper__btn:disabled {
opacity: 0.6;
}
.photo-upload-header { .photo-upload-header {
width: 100%; width: 100%;
display: flex; display: flex;