feat: ImgCropper 图片剪裁支持设置裁剪框宽高比 (#973)

 Closes: #920
This commit is contained in:
不如摸鱼去 2025-03-25 23:41:32 +08:00 committed by GitHub
parent b85237643c
commit 5a3f85df6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 311 additions and 30 deletions

View File

@ -70,6 +70,7 @@ function handleCancel(event) {
| confirm-button-text | 确认按钮文案 | string | - | 完成 | - |
| quality | 生成的图片质量 [wx.canvasToTempFilePath属性介绍](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html#%E5%8F%82%E6%95%B0) | number | 0/1 | 1 | - |
| file-type | 目标文件的类型,[wx.canvasToTempFilePath属性介绍](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html#%E5%8F%82%E6%95%B0) | string | - | png | - |
| aspect-ratio | 裁剪框宽高比,格式为 width:height | string | - | 1:1 | $LOWEST_VERSION$ |
## Events
@ -94,3 +95,79 @@ function handleCancel(event) {
| 类名 | 说明 | 最低版本 |
|-----|------|--------|
| custom-class | 根节点样式 | - |
## 基本用法
```html
<!-- 设置3:2的裁剪框 -->
<wd-img-cropper
v-model="show"
:img-src="src"
aspect-ratio="3:2"
@confirm="handleConfirm"
@cancel="handleCancel"
>
</wd-img-cropper>
```
## 裁剪后上传
结合 `useUpload` 可以实现裁剪完成后自动上传图片的功能。
```html
<wd-img-cropper
v-model="show"
:img-src="src"
@confirm="handleConfirmUpload"
@cancel="handleCancel"
>
</wd-img-cropper>
```
```typescript
import { ref } from 'vue'
import { useUpload, useToast } from '@/uni_modules/wot-design-uni'
import { type UploadFileItem } from '@/uni_modules/wot-design-uni/components/wd-upload/types'
const { startUpload, UPLOAD_STATUS } = useUpload()
const { show: showToast } = useToast()
const show = ref(false)
const src = ref('')
const imgSrc = ref('')
async function handleConfirmUpload(event) {
const { tempFilePath } = event
// 构建上传文件对象
const file: UploadFileItem = {
url: tempFilePath,
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: new Date().getTime()
}
try {
// 开始上传
await startUpload(file, {
action: 'https://your-upload-url',
onSuccess() {
imgSrc.value = tempFilePath
showToast({
msg: '上传成功'
})
},
onError() {
showToast({
msg: '上传失败'
})
},
onProgress(res) {
console.log('上传进度:', res.progress)
}
})
} catch (error) {
console.error('上传失败:', error)
}
}
```

View File

@ -1,7 +1,7 @@
<!--
* @Author: weisheng
* @Date: 2023-09-20 11:10:41
* @LastEditTime: 2024-06-06 21:45:41
* @LastEditTime: 2025-03-25 22:59:47
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/pages/imgCropper/Index.vue
@ -29,15 +29,79 @@
<view style="font-size: 14px">点击上传头像</view>
</view>
</demo-block>
<demo-block title="自定义裁剪比例" style="text-align: center">
<view class="profile-grid">
<view v-for="(ratio, index) in ['1:1', '3:2', '16:9']" :key="index" class="profile-item">
<wd-img-cropper
v-model="showCustom[index]"
:img-src="srcCustom[index]"
:aspect-ratio="ratio"
@confirm="handleCustomConfirm(index, $event)"
@cancel="handleCustomCancel"
></wd-img-cropper>
<view v-if="!imgSrcCustom[index]" class="img" @click="uploadCustom(index)">
<wd-icon name="fill-camera" custom-class="img-icon"></wd-icon>
</view>
<wd-img
v-if="imgSrcCustom[index]"
:width="ratio === '1:1' ? '200px' : '300px'"
:height="getHeight(ratio)"
:src="imgSrcCustom[index]"
mode="aspectFit"
custom-class="profile-img"
@click="uploadCustom(index)"
/>
<view style="font-size: 14px">{{ ratio }} 比例裁剪</view>
</view>
</view>
</demo-block>
<demo-block title="裁剪后上传" style="text-align: center">
<wd-img-cropper v-model="showUpload" :img-src="srcUpload" @confirm="handleConfirmUpload" @cancel="handleCancel"></wd-img-cropper>
<view class="profile">
<view v-if="!imgSrcUpload" class="img" @click="uploadWithCrop">
<wd-icon name="fill-camera" custom-class="img-icon"></wd-icon>
</view>
<wd-img
v-if="imgSrcUpload"
round
width="200px"
height="200px"
:src="imgSrcUpload"
mode="aspectFit"
custom-class="profile-img"
@click="uploadWithCrop"
/>
<view style="font-size: 14px">点击上传裁剪后的头像</view>
</view>
</demo-block>
</page-wraper>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useUpload, useToast } from '@/uni_modules/wot-design-uni'
import { type UploadFileItem } from '@/uni_modules/wot-design-uni/components/wd-upload/types'
const { startUpload, UPLOAD_STATUS } = useUpload()
const { show: showToast } = useToast()
const src = ref<string>('')
const imgSrc = ref<string>('')
const show = ref<boolean>(false)
//
const showCustom = ref<boolean[]>([false, false, false])
const srcCustom = ref<string[]>(['', '', ''])
const imgSrcCustom = ref<string[]>(['', '', ''])
//
const showUpload = ref<boolean>(false)
const srcUpload = ref<string>('')
const imgSrcUpload = ref<string>('')
function upload() {
uni.chooseImage({
count: 1,
@ -48,20 +112,96 @@ function upload() {
}
})
}
function uploadCustom(index: number) {
uni.chooseImage({
count: 1,
success: (res) => {
const tempFilePaths = res.tempFilePaths[0]
srcCustom.value[index] = tempFilePaths
showCustom.value[index] = true
}
})
}
function uploadWithCrop() {
uni.chooseImage({
count: 1,
success: (res) => {
srcUpload.value = res.tempFilePaths[0]
showUpload.value = true
}
})
}
function handleConfirm(event: any) {
const { tempFilePath } = event
imgSrc.value = tempFilePath
}
function handleCustomConfirm(index: number, event: any) {
const { tempFilePath } = event
imgSrcCustom.value[index] = tempFilePath
}
async function handleConfirmUpload(event: any) {
const { tempFilePath } = event
//
const file: UploadFileItem = {
url: tempFilePath,
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: new Date().getTime()
}
try {
//
await startUpload(file, {
action: 'https://mockapi.eolink.com/zhTuw2P8c29bc981a741931bdd86eb04dc1e8fd64865cb5/upload', //
onSuccess() {
imgSrcUpload.value = tempFilePath
showToast({
msg: '上传成功'
})
},
onError() {
showToast({
msg: '上传失败'
})
},
onProgress(res) {
console.log('上传进度:', res.progress)
}
})
} catch (error) {
console.error('上传失败:', error)
}
}
function imgLoaderror(res: any) {
console.log('加载失败', res)
}
function imgLoaded(res: any) {
console.log('加载成功', res)
}
function handleCancel(event: any) {
console.log('取消', event)
}
function handleCustomCancel(event: any) {
console.log('取消', event)
}
function getHeight(ratio: string): string {
const [w, h] = ratio.split(':').map(Number)
if (ratio === '1:1') return '200px'
return `${(300 * h) / w}px`
}
</script>
<style lang="scss" scoped>
.wot-theme-dark {
:deep(.profile-img) {
@ -98,4 +238,37 @@ function handleCancel(event: any) {
top: 50%;
transform: translate(-50%, -50%);
}
.profile-grid {
display: flex;
flex-direction: column;
gap: 40px;
align-items: center;
padding: 20px;
}
.profile-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.profile-item .img {
width: 300px;
height: 169px; // 16:9
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.04);
position: relative;
}
.profile-item:first-child .img {
width: 200px;
height: 200px;
border-radius: 50%;
}
.profile-item:nth-child(2) .img {
height: 200px;
}
</style>

View File

@ -1,7 +1,7 @@
/*
* @Author: weisheng
* @Date: 2024-06-03 23:43:43
* @LastEditTime: 2024-06-06 21:40:53
* @LastEditTime: 2025-03-25 17:28:13
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-img-cropper/types.ts
@ -57,7 +57,11 @@ export const imgCropperProps = {
/**
*
*/
maxScale: makeNumberProp(3)
maxScale: makeNumberProp(3),
/**
* width:height
*/
aspectRatio: makeStringProp('1:1')
}
export type ImgCropperProps = ExtractPropTypes<typeof imgCropperProps>

View File

@ -7,11 +7,11 @@
<view class="wd-img-cropper__cut">
<!-- 上方阴影块 -->
<view :class="`wd-img-cropper__cut--top ${IS_TOUCH_END ? '' : 'is-hightlight'}`" :style="`height: ${cutTop}px;`"></view>
<view class="wd-img-cropper__cut--middle">
<view class="wd-img-cropper__cut--middle" :style="`height: ${cutHeight}px;`">
<!-- 左侧阴影块 -->
<view
:class="`wd-img-cropper__cut--left ${IS_TOUCH_END ? '' : 'is-hightlight'}`"
:style="`width: ${cutLeft}px; height: ${cutWidth}px;`"
:style="`width: ${cutLeft}px; height: ${cutHeight}px;`"
></view>
<!-- 裁剪框 -->
<view class="wd-img-cropper__cut--body" :style="`width: ${cutWidth}px; height: ${cutHeight}px;`">
@ -163,14 +163,21 @@ watch(
INIT_IMGWIDTH = props.imgWidth
INIT_IMGHEIGHT = props.imgHeight
info.value = uni.getSystemInfoSync()
const tempCutSize = info.value.windowWidth - offset.value * 2
cutWidth.value = tempCutSize
cutHeight.value = tempCutSize
cutTop.value = (info.value.windowHeight * TOP_PERCENT - tempCutSize) / 2
// aspectRatio
const [widthRatio, heightRatio] = props.aspectRatio.split(':').map(Number)
const tempCutWidth = info.value.windowWidth - offset.value * 2
const tempCutHeight = (tempCutWidth * heightRatio) / widthRatio
cutWidth.value = tempCutWidth
cutHeight.value = tempCutHeight
cutTop.value = (info.value.windowHeight * TOP_PERCENT - tempCutHeight) / 2
cutLeft.value = offset.value
canvasScale.value = props.exportScale
canvasHeight.value = tempCutSize
canvasWidth.value = tempCutSize
canvasHeight.value = tempCutHeight
canvasWidth.value = tempCutWidth
//
initImageSize()
// canvas
@ -268,7 +275,23 @@ function setRoate(angle: number) {
if (!angle || props.disabledRotate) return
revertIsAnimation(true)
imgAngle.value = angle
//
//
let tempPicWidth = picWidth.value
let tempPicHeight = picHeight.value
//
if ((angle / 90) % 2) {
tempPicWidth = picHeight.value
tempPicHeight = picWidth.value
}
//
const widthRatio = cutWidth.value / tempPicWidth
const heightRatio = cutHeight.value / tempPicHeight
imgScale.value = Math.max(widthRatio, heightRatio)
//
detectImgPosIsEdge()
}
@ -306,39 +329,43 @@ function loadImg() {
}
/**
* 设置图片尺寸使其有一边小于裁剪框尺寸
* 1图片宽或高 小于裁剪框自动放大至一边与高平齐
* 2图片宽或高 大于裁剪框自动缩小至一边与高平齐
* 设置图片尺寸使其短边完全显示并填满裁剪框
*/
function computeImgSize() {
let tempPicWidth: number = picWidth.value
let tempPicHeight: number = picHeight.value
if (!INIT_IMGHEIGHT && !INIT_IMGWIDTH) {
//
tempPicWidth = imgInfo.value!.width
tempPicHeight = imgInfo.value!.height
/**
* a = imgWidth; b = imgHeight; x = cutWidth; y = cutHeight
* 共有三种宽高比1a/b > x/y 2a/b < x/y 3a/b = x/y
* 1已知 b = y => a = a/b*y
* 2已知 a = x => b = b/a*x
* 3可用上方任意公式
*/
if (picWidth.value / picHeight.value > cutWidth.value / cutHeight.value) {
//
const imgRatio = imgInfo.value!.width / imgInfo.value!.height
const cropRatio = cutWidth.value / cutHeight.value
if (imgRatio > cropRatio) {
//
tempPicHeight = cutHeight.value
tempPicWidth = (imgInfo.value!.width / imgInfo.value!.height) * cutHeight.value
tempPicWidth = tempPicHeight * imgRatio
} else {
//
tempPicWidth = cutWidth.value
tempPicHeight = (imgInfo.value!.height / imgInfo.value!.width) * cutWidth.value
tempPicHeight = tempPicWidth / imgRatio
}
} else if (INIT_IMGHEIGHT && !INIT_IMGWIDTH) {
tempPicWidth = (imgInfo.value!.width / imgInfo.value!.height) * Number(INIT_IMGHEIGHT)
tempPicHeight = Number(INIT_IMGHEIGHT)
tempPicWidth = (imgInfo.value!.width / imgInfo.value!.height) * tempPicHeight
} else if ((!INIT_IMGHEIGHT && INIT_IMGWIDTH) || (INIT_IMGHEIGHT && INIT_IMGWIDTH)) {
tempPicHeight = (imgInfo.value!.height / imgInfo.value!.width) * Number(INIT_IMGWIDTH)
tempPicWidth = Number(INIT_IMGWIDTH)
tempPicHeight = (imgInfo.value!.height / imgInfo.value!.width) * tempPicWidth
}
//
const widthRatio = cutWidth.value / tempPicWidth
const heightRatio = cutHeight.value / tempPicHeight
const scale = Math.max(widthRatio, heightRatio)
picWidth.value = tempPicWidth
picHeight.value = tempPicHeight
//
imgScale.value = scale
}
/**