mirror of
https://gitee.com/wot-design-uni/wot-design-uni.git
synced 2025-12-06 17:18:40 +08:00
parent
b85237643c
commit
5a3f85df6f
@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
* 共有三种宽高比:1、a/b > x/y 2、a/b < x/y 3、a/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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user