1,裁剪头像
- 点击头像进行选择图片。
<u-cell-item title="头像" @tap="hanAvarEdit">
<img :src="avatarUrl" class="avatar" />
</u-cell-item>
跳转至crop页面
hanAvarEdit() {
uni.chooseImage({
count: 1,
success: (res) => {
const path = res.tempFilePaths[0]
uni.navigateTo({
url: `/pages/profile/crop?src=${encodeURIComponent(path)}`
})
}
})
},
- 拿到图片后进行裁剪或重新选择图片(crop页面)。
<template>
<view class="container">
<uni-cropper
:src="src"
:crop-width="700"
:crop-height="1000"
@update:src="updateSrc"
@upload-success="handleUploadSuccess"
/>
</view>
</template>
<script>
import uniCropper from '@/components/uni-cropper/uni-cropper.vue'
export default {
components: { uniCropper },
data() {
return { src: '' }
},
onLoad(options) {
if (options.src) {
this.src = decodeURIComponent(options.src)
} else {
uni.showToast({ title: '图片路径丢失', icon: 'none' })
uni.navigateBack()
}
},
methods: {
updateSrc(newSrc) {
this.src = newSrc
},
handleUploadSuccess(res) {
uni.showToast({ title: '上传成功', icon: 'none' })
uni.navigateBack()
}
}
}
</script>
<style scoped>
.container {
padding: 20rpx;
}
</style>
- 组件文件,uni-cropper.vue
<template>
<view class="uni-cropper-container">
<canvas canvas-id="cropperCanvas" id="cropperCanvas" class="cropper-canvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"></canvas>
<view class="crop-frame" :style="{
width: cropWidth + 'rpx',
height: cropHeight + 'rpx',
top: cropTop + 'rpx'
}"></view>
<view class="button-group">
<u-button type="primary" class="cropper-btn" @click="getCrop">裁剪</u-button>
<u-button class="cropper-btn" @click="reSelectImage">重新选择</u-button>
</view>
</view>
</template>
<script>
import global from '../../utils/global.js'
export default {
name: 'uni-cropper',
props: {
src: String,
cropWidth: {
type: Number,
default: 300
},
cropHeight: {
type: Number,
default: 300
}
},
data() {
return {
localSrc: '',
imgInfo: null,
canvasWidth: 0,
canvasHeight: 0,
position: {
x: 0,
y: 0
},
scale: 1,
touches: [],
initialDistance: 0,
initialScale: 1,
cropTop: 0
}
},
watch: {
src(newVal) {
if (newVal) {
this.localSrc = newVal
this.loadImageInfo(newVal)
}
}
},
mounted() {
uni.createSelectorQuery()
.in(this)
.select('.uni-cropper-container')
.boundingClientRect(rect => {
this.canvasWidth = rect.width
this.canvasHeight = rect.height
this.cropTop = (this.canvasHeight - uni.upx2px(this.cropHeight)) / 2
if (this.src) {
this.localSrc = this.src
this.loadImageInfo(this.src)
}
})
.exec()
},
methods: {
loadImageInfo(path) {
uni.getImageInfo({
src: path,
success: res => {
this.imgInfo = res
this.initCanvas()
},
fail: () => {
uni.showToast({
title: '图片加载失败',
icon: 'none'
})
}
})
},
initCanvas() {
const imgW = this.imgInfo.width
const imgH = this.imgInfo.height
const cropW = uni.upx2px(this.cropWidth)
const cropH = uni.upx2px(this.cropHeight)
const scaleW = cropW / imgW
const scaleH = cropH / imgH
this.scale = Math.min(scaleW, scaleH)
const drawW = imgW * this.scale
const drawH = imgH * this.scale
this.position.x = (this.canvasWidth - drawW) / 2
this.position.y = (this.canvasHeight - drawH) / 2
this.drawImage()
},
drawImage() {
if (!this.imgInfo) return
const ctx = uni.createCanvasContext('cropperCanvas', this)
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
const imgW = this.imgInfo.width * this.scale
const imgH = this.imgInfo.height * this.scale
const cutLeft = (this.canvasWidth - uni.upx2px(this.cropWidth)) / 2
const cutTopPx = this.cropTop
ctx.save()
ctx.beginPath()
ctx.rect(cutLeft, cutTopPx, uni.upx2px(this.cropWidth), uni.upx2px(this.cropHeight))
ctx.clip()
ctx.drawImage(this.localSrc, this.position.x, this.position.y, imgW, imgH)
ctx.restore()
ctx.draw()
},
getDistance(p1, p2) {
const dx = p2.x - p1.x
const dy = p2.y - p1.y
return Math.sqrt(dx * dx + dy * dy)
},
onTouchStart(e) {
this.touches = e.touches
if (e.touches.length === 2) {
this.initialDistance = this.getDistance(e.touches[0], e.touches[1])
this.initialScale = this.scale
}
},
onTouchMove(e) {
if (e.touches.length === 1 && this.touches.length === 1) {
const dx = e.touches[0].x - this.touches[0].x
const dy = e.touches[0].y - this.touches[0].y
this.position.x += dx
this.position.y += dy
this.touches = e.touches
this.drawImage()
} else if (e.touches.length === 2) {
const newDistance = this.getDistance(e.touches[0], e.touches[1])
const scaleChange = newDistance / this.initialDistance
this.scale = Math.max(0.5, Math.min(5, this.initialScale * scaleChange))
this.drawImage()
}
},
onTouchEnd() {
this.touches = []
},
getCrop() {
uni.canvasToTempFilePath({
canvasId: 'cropperCanvas',
width: this.canvasWidth,
height: this.canvasHeight,
success: (res) => {
console.log('裁剪成功', res.tempFilePath)
uni.uploadFile({
url: global.uploadUrl,
filePath: res.tempFilePath,
name: 'file',
success: uploadRes => {
const response = JSON.parse(uploadRes.data)
if (response.status === '200') {
const param = {
mobile: uni.getStorageSync("compInfo").mobile,
profilePictureFileName: response.body.fileName,
profilePictureFileSize: response.body.fileSize,
profilePictureFileToken: response.body.fileToken
}
this.$global.request('/crm/api/edc/wechat/personalCenter/updateUserInfo', 'post', param)
.then(resDa => {
if (resDa.data.status === '200') {
uni.navigateBack()
}
}).catch(err => console.log(err))
}
}
})
},
fail: (err) => {
console.error('裁剪失败', err)
uni.showToast({
title: '裁剪失败',
icon: 'none'
})
}
}, this)
},
reSelectImage() {
uni.chooseImage({
count: 1,
success: res => {
this.$emit('update:src', res.tempFilePaths[0])
}
})
}
}
}
</script>
<style scoped>
.uni-cropper-container {
position: relative;
width: 100%;
height: 400px;
/* 这里可改为动态高度或父容器撑开 */
}
.cropper-canvas {
width: 100%;
height: 100%;
background: #000;
}
.crop-frame {
position: absolute;
left: 50%;
transform: translateX(-50%);
border: 2px dashed #fff;
pointer-events: none;
width: auto;
/* 通过动态绑定宽高 */
height: auto;
}
.button-group {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 40rpx;
}
.cropper-btn {
width: 100%;
margin-bottom: 20rpx;
background-color: #007aff;
color: #fff;
text-align: center;
border-radius: 10rpx;
border: none;
}
</style>
2,微信签名
思路:使用canvas先画出一个画布,然后再画布上签名,形成一个图片,最后把图片通过接口传向后端。
pages/personal/signing.vue
<template>
<view class="servi-agreem">
<Signature @confirm="onConfirm" @clear="onClear" />
</view>
</template>
<script>
import Signature from "@/components/Signature/index.vue";
import global from '../../utils/global.js'
export default {
data() {
return {
detailDTO:{
bizCode:'',
signFileToken:'',
},
}
},
onLoad: function(option) {
// console.log(option);
this.detailDTO.bizCode = option.bizCode;
},
components: {
Signature
},
methods: {
onConfirm(filePath) {
console.log("签名图片路径:", filePath);
// 可以调用上传、保存等操作
wx.uploadFile({
url: global.uploadUrl,
filePath: filePath,
name: 'file', // 后端接收文件字段名
success: (res) => {
if(res.statusCode == '200'){
console.log('上传成功', res);
let resData = JSON.parse(res.data)
this.detailDTO.signFileToken = resData.body.fileToken;
this.$global.request('/crm/api/edc/wechat/personalCenter/signAgreement', 'post',this.detailDTO).then(
(resp) => {
// console.log(resp.data.body)
uni.switchTab({
url: '/pages/personal/personal'
});
}).catch((err) => {
});
}
},
fail: (err) => {
console.error('上传失败', err);
}
});
},
onClear() {
console.log("签名已清除");
},
}
}
</script>
<style scoped lang="scss">
.servi-agreem {
background: #f1f1f1;
width: 750rpx;
height: 100%;
min-height: 100vh;
}
</style>
组件页,@/components/Signature/index.vue
<template>
<view class="signature-wrapper">
<canvas canvas-id="signatureCanvas" id="signatureCanvas" class="signature-canvas" disable-scroll="true" @touchstart="startDraw" @touchmove="moveDraw" @touchend="endDraw" />
<view class="btn-group">
<button @tap="clearCanvas">清除</button>
<button @tap="exportSignature">保存</button>
</view>
</view>
</template>
<script>
export default {
name: "Signature",
emits: ["confirm", "clear"],
data() {
return {
ctx: null,
isDrawing: false,
canvasId: "signatureCanvas",
canvasWidth: 0,
canvasHeight: 0
};
},
mounted() {
this.initCanvas();
},
methods: {
initCanvas() {
this.$nextTick(() => {
uni.createSelectorQuery()
.in(this)
.select("#signatureCanvas")
.boundingClientRect(rect => {
this.canvasWidth = rect.width;
this.canvasHeight = rect.height;
this.ctx = uni.createCanvasContext(this.canvasId, this);
this.ctx.setStrokeStyle("#000");
this.ctx.setLineWidth(8);
this.ctx.setLineCap("round");
this.ctx.setLineJoin("round");
})
.exec();
});
},
startDraw(e) {
const { x, y } = e.touches[0];
this.ctx.beginPath();
this.ctx.moveTo(x, y);
this.isDrawing = true;
},
moveDraw(e) {
if (!this.isDrawing) return;
const { x, y } = e.touches[0];
this.ctx.lineTo(x, y);
this.ctx.stroke();
},
endDraw() {
this.ctx.draw(true); // 在 touchend 时绘制整段线
this.isDrawing = false;
},
clearCanvas() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.draw();
this.$emit("clear");
},
exportSignature() {
uni.canvasToTempFilePath(
{
canvasId: this.canvasId,
success: res => {
this.$emit("confirm", res.tempFilePath);
},
fail: err => {
uni.showToast({ title: "保存失败", icon: "none" });
}
},
this
);
}
}
};
</script>
<style scoped>
.signature-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.signature-canvas {
width: 100%;
height: 300px;
border: 1px solid #ccc;
background-color: #fff;
}
.btn-group {
margin-top: 20rpx;
display: flex;
gap: 30rpx;
}
</style>
3,页面下拉刷新
- 配置页面开启下拉刷新
// pages/your-page/your-page.json
{
"enablePullDownRefresh": true
}
- 页面逻辑实现
// 监听页面下拉刷新
onPullDownRefresh() {
Promise.all([
this.autoUpdate(),
this.init(),
this.initData(),
]).then(() => {
uni.stopPullDownRefresh();
});
},
created() {
}