Browse Source

头像上传前裁剪

htc 1 day ago
parent
commit
7cd2b30e39

+ 41 - 43
pagesRole/addRole.vue

@@ -5,17 +5,15 @@
 			<div class="title adfac">角色形象<span>*</span></div>
 			<div class="upload">
 				<div class="sc">
-					<u-upload width="188rpx" height="188rpx"
-						:fileList="fileList"
-					    @afterRead="afterRead"
-					    @delete="deletePic"
-					    :maxCount="1"
-					>
-						<div class="imgs">
+					<div class="imgs" @tap="selectImage">
+						<template v-if="!resultUrl">
 							<image class="img1" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/06/03/0e628447-e818-4f5e-88b4-a2af61b00bbd.png"></image>
 							<image class="img2" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/06/03/63556f0a-87ac-4d0e-b034-1b3cc65f4d4d.png"></image>
-						</div>
-					</u-upload>
+						</template>
+						<template v-else>
+							<image class="img1" :src="resultUrl"></image>
+						</template>
+					</div>
 				</div>
 				<div class="text">上传角色形象</div>
 			</div>
@@ -41,7 +39,17 @@
 				<image src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/06/03/ebf6fcea-a7ba-4ba5-8f20-c70b16c7dc3f.png"></image>
 			</div>
 		</div>
-		<div class="zt_btn" @tap="comfirmSure">{{agentId?'编辑角色':'创建角色'}}</div>
+		<div class="zt_btn" @tap="comfirmSure">{{agentId?'编辑角色':'创建角色'}}</div>
+		<tt-cropper
+			mode="free"
+			:imageUrl="imageUrl"
+			:width="500"
+			:height="500"
+			:radius="90"
+			:delay="300"
+			@cancel="onCancel"
+			@confirm="onConfirm"
+		></tt-cropper>
 	</view>
 </template>
 
@@ -77,7 +85,9 @@
 					"language": "",
 					"deviceId": "",
 					"chatHistoryConf": ""
-				}
+				},
+				imageUrl:'',
+				resultUrl:''
 			}
 		},
 		onLoad(option) {
@@ -105,36 +115,21 @@
 			}
 		},
 		methods:{
-			// 删除图片
-			deletePic(event) {
-				this.fileList.splice(event.index, 1);
-			},
-			// 新增图片
-			async afterRead(event) {
-				// 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
-				let lists = [].concat(event.file);
-				let fileListLen = this.fileList.length;
-				lists.map((item) => {
-				  this.fileList.push({
-					...item,
-					status: "uploading",
-					message: "上传中",
-				  });
+			selectImage(){
+				uni.chooseImage({
+					count: 1,
+					sizeType: ['compressed'],
+					success: (res) => {
+						this.imageUrl = res.tempFilePaths[0];
+					}
 				});
-				for (let i = 0; i < lists.length; i++) {
-				  const result = await this.uploadFilePromise(lists[i].url);
-				  let item = this.fileList[fileListLen];
-				  this.fileList.splice(
-					fileListLen,
-					1,
-					Object.assign(item, {
-					  status: "success",
-					  message: "",
-					  url: result,
-					})
-				  );
-				  fileListLen++;
-				}
+			},
+			onCancel() {
+			  this.imageUrl = "";
+			},
+			async onConfirm(res) {
+				this.resultUrl = await this.uploadFilePromise(res.tempFilePath);
+				this.imageUrl = "";
 			},
 			uploadFilePromise(url) {
 				return new Promise((resolve, reject) => {
@@ -188,7 +183,7 @@
 				})
 			},
 			comfirmSure(){
-				if(this.fileList.length===0) return this.$showToast('请上传角色头像')
+				if(!this.resultUrl) return this.$showToast('请上传角色头像')
 				if(!this.agentDto.agentName) return this.$showToast('请输入角色昵称')
 				if(this.agentDto.voiceText==='请选择音色') return this.$showToast('请选择音色')
 				
@@ -205,7 +200,7 @@
 				dto.langCode = 'zh';
 				dto.language = '中文';
 				dto.vllmModelId = 'VLLM_ChatGLMVLLM';
-				if(this.fileList.length) dto.avatar = this.fileList[0].url;
+				dto.avatar = this.resultUrl;
 				this.$api.post(this.agentId?`/agent/update/${this.agentId}`:'/agent',dto).then(res=>{
 					if(res.data.code!==0) return this.$showToast(res.data.msg)
 					this.$showToast(this.agentId?'编辑成功':'创建成功');
@@ -218,7 +213,7 @@
 				this.$api.get(`/agent/${this.agentId}`).then(res=>{
 					if(res.data.code!==0) return this.$showToast(res.data.msg)
 					this.agentDto = {...this.agentDto,...res.data.data};
-					if(this.agentDto.avatar) this.fileList.push({url:this.agentDto.avatar});
+					if(this.agentDto.avatar) this.resultUrl = this.agentDto.avatar;
 					if(this.agentDto.systemPrompt) this.showta = true;
 					this.getModelVoiceList();
 				})
@@ -248,6 +243,9 @@
 		bottom: -16rpx !important;
 		right: -16rpx !important;
 	}
+	::v-deep .t-cropper{
+		left: 0;
+	}
 	
 	.ph{
 		font-family: PingFangSC, PingFang SC;

+ 16 - 0
uni_modules/tt-cropper/changelog.md

@@ -0,0 +1,16 @@
+## 1.0.8(2024-09-09)
+矩形旋转居中修改,矩形框最大缩放
+## 1.0.7(2024-08-12)
+优化了vue2版本在微信场景下,:style 不支持 comImageStyle('image-wrap') 语法问题。
+## 1.0.6(2024-08-08)
+优化选择大文件图片后,无法在Image中直接显示图片,提供针对大图片压缩思路
+## 1.0.5(2024-06-16)
+优化了H5场景下部分问题,更新了文档
+## 1.0.3(2024-06-16)
+本次未更新内容,更新了文档
+## 1.0.2(2024-06-16)
+更新了文档
+## 1.0.1(2024-06-16)
+优化确定按钮,快速重复点击出现多个裁剪图片
+## 1.0.0(2024-04-15)
+vue2/vue3图片裁剪工具

+ 202 - 0
uni_modules/tt-cropper/components/tt-cropper/tt-cropper.scss

@@ -0,0 +1,202 @@
+$clipper-edge-border-width: 6rpx !default;
+
+.t-cropper {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  bottom: 0;
+  z-index: 1000;
+  overflow: hidden;
+  .canvas {
+    position: absolute;
+    top: 5000px;
+    left: 5000px;
+  }
+  // 裁剪区域
+  .t-preview-container {
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    bottom: 0;
+    z-index: 1000;
+    opacity: 0;
+    overflow: hidden;
+
+    .preview-body {
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: 0;
+      bottom: 0;
+      background: #000;
+      overflow: hidden;
+      .mask-model {
+        position: absolute;
+        left: 0;
+        right: 0;
+        top: 0;
+        bottom: 0;
+        background: #000;
+        opacity: 0.4;
+        pointer-events: none;
+      }
+      .image-wrap {
+        position: absolute;
+        .image {
+          position: absolute;
+        }
+      }
+      // 裁剪框盒子
+      .frame-box {
+        position: absolute;
+        left: 100px;
+        top: 100px;
+        width: 200px;
+        height: 200px;
+        // 矩形图片
+        .rect {
+          position: absolute;
+          left: -2px;
+          top: -2px;
+          width: 100%;
+          height: 100%;
+          border: 2rpx solid white;
+          overflow: hidden;
+          box-sizing: content-box;
+          .image-rect {
+            position: absolute;
+            .rect-img {
+              position: absolute;
+            }
+          }
+        }
+        //裁剪框线条
+        .line-one {
+          position: absolute;
+          width: 100%;
+          border-top: 1px dashed #ccc;
+          left: 0;
+          top: 33.3%;
+          box-sizing: content-box;
+        }
+        .line-two {
+          position: absolute;
+          width: 100%;
+          border-top: 1px dashed #ccc;
+          left: 0;
+          top: 66.7%;
+          box-sizing: content-box;
+        }
+        .line-three {
+          position: absolute;
+          height: 100%;
+          border-right: 1px dashed #ccc;
+          top: 0;
+          left: 33.3%;
+          box-sizing: content-box;
+        }
+        .line-four {
+          position: absolute;
+          height: 100%;
+          border-right: 1px dashed #ccc;
+          top: 0;
+          left: 66.7%;
+          box-sizing: content-box;
+        }
+        .frame-left-top {
+          position: absolute;
+          width: 20px;
+          height: 20px;
+          left: -8rpx;
+          top: -8rpx;
+          border-left: 4rpx solid #fff;
+          border-top: 4rpx solid #fff;
+          box-sizing: content-box;
+        }
+        .frame-left-bottom {
+          position: absolute;
+          width: 20px;
+          height: 20px;
+          left: -8rpx;
+          bottom: -4rpx;
+          border-left: 4rpx solid #fff;
+          border-bottom: 4rpx solid #fff;
+          box-sizing: content-box;
+        }
+        .frame-right-top {
+          position: absolute;
+          width: 20px;
+          height: 20px;
+          right: -4rpx;
+          top: -8rpx;
+          border-right: 4rpx solid #fff;
+          border-top: 4rpx solid #fff;
+          box-sizing: content-box;
+        }
+        .frame-right-bottom {
+          position: absolute;
+          width: 20px;
+          height: 20px;
+          right: -4rpx;
+          bottom: -4rpx;
+          border-right: 4rpx solid #fff;
+          border-bottom: 4rpx solid #fff;
+          box-sizing: content-box;
+        }
+      }
+    }
+
+    // 底部工具栏
+    .toolbar {
+      position: absolute;
+      width: calc(100% - 64rpx);
+      height: 100rpx;
+      left: 0;
+      bottom: 10rpx;
+      text-align: center;
+      display: flex;
+      justify-content: space-between;
+      padding: 0 32rpx;
+      align-items: center;
+      // IOS 底部安全距离
+      padding-bottom: constant(safe-area-inset-bottom);
+      padding-bottom: env(safe-area-inset-bottom);
+      .btn-cancel {
+        width: 112rpx;
+        font-size: 28rpx;
+        color: #d5dfe5;
+        font-weight: bold;
+      }
+      .btn-rotate {
+        width: 112rpx;
+        font-size: 28rpx;
+        color: #d5dfe5;
+        font-weight: bold;
+        image {
+          width: 60rpx;
+          height: 60rpx;
+        }
+      }
+      .btn-confirm {
+        font-size: 28rpx;
+        color: #ffffff;
+        font-weight: bold;
+        width: 112rpx;
+        height: 60rpx;
+        line-height: 60rpx;
+        background: #07c160;
+        border-radius: 6rpx;
+        text-align: center;
+      }
+    }
+
+    .transit {
+      transition: width 0.3s, height 0.3s, left 0.3s, top 0.3s, transform 0.3s;
+    }
+  }
+  .showPage {
+    opacity: 1 !important;
+  }
+}

File diff suppressed because it is too large
+ 1107 - 0
uni_modules/tt-cropper/components/tt-cropper/tt-cropper.vue


+ 84 - 0
uni_modules/tt-cropper/package.json

@@ -0,0 +1,84 @@
+{
+  "id": "tt-cropper",
+  "displayName": "tt-cropper图片裁剪插件",
+  "version": "1.0.8",
+  "description": "vue2/vue3图片裁剪插件,头像裁剪、支持自定义尺寸、等比例缩放、拖动、图片翻转、剪切圆形/圆角图片,高性能裁剪图片插件",
+  "keywords": [
+    "图片裁剪",
+    ",头像裁剪,缩放,旋转,拖动"
+],
+  "repository": "",
+  "engines": {
+    "HBuilderX": "^3.1.0"
+  },
+  "dcloudext": {
+    "type": "component-vue",
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": ""
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "Vue": {
+          "vue2": "y",
+          "vue3": "y"
+        },
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "u",
+          "app-uvue": "u"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "u",
+          "IE": "u",
+          "Edge": "u",
+          "Firefox": "u",
+          "Safari": "u"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "u",
+          "字节跳动": "u",
+          "QQ": "u",
+          "钉钉": "u",
+          "快手": "u",
+          "飞书": "u",
+          "京东": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        }
+      }
+    }
+  }
+}

+ 184 - 0
uni_modules/tt-cropper/readme.md

@@ -0,0 +1,184 @@
+# t-cropper
+
+> **t-cropper  一款高性能移动端图片裁剪工具**
+
+## 平台兼容
+
+| App   |   H5   |   微信小程序 |   支付宝小程序 |
+| :---: | :---:  | :----------: | :-----------: |
+|  √    |     √  |      √       |      √        |
+
+### 属性说明
+
+|属性         |类型     |默认     |备注      |
+| :--------: | :-----: | :----:  | :----:  |
+| mode       |String   | "ratio"  | 裁剪模式|
+| imageUrl   |String   |   " "    | 需要裁剪的图片路径|
+| width      |Number   | 200     | 图片裁剪后的宽度,固定大小时有效|
+| height     |Number   | 200     | 图片裁剪后的高度,固定大小时有效|
+| maxWidth   |Number   | 1024    | 图片裁剪后的最大宽度 |
+| maxHeight  |Number   | 1024    | 图片裁剪后的最大高度 |
+| scaleRatio |Number   | 0.7    | 裁剪比列缩放,建议不超过0.95 |
+| minRatio  |Number   | 1    | 最小缩放 |
+| maxRatio  |Number   | 3    | 最大缩放 |
+| radius  |Number   | 0    | 裁剪图片圆角半径,单位px |
+| delay   |Number   | 250    | 确定按钮快速重复点击时间 |
+| isRotateBtn  |Boolean   | true    | 是否显示旋转按钮 |
+| isCutSize  |Boolean   | true    | 是否导出高清裁剪原图(h5) |
+
+### mode有效值
+
+| 模式     |值       |说明   |
+| :-----: | :-----: | :----: |
+| 固定模式 |fixed    | 裁剪出指定大小的图片,一般用于头像上传    |
+| 等比缩放 |ratio    | 限定宽高比,裁剪大小不固定  |
+| 自由模式 |free     | 不限定宽高比,裁剪大小不固定  |
+
+### 事件说明
+
+|事件名称     |说明     |返回     |
+| :--------: | :-----: | :----:  |
+| confirm        |点击确定按钮    |   object    |
+| cancel      |点击取消按钮  | -  |
+
+### 示例
+
+```html
+<template>
+  <view>
+    <tt-cropper
+      mode="ratio"
+      :imageUrl="model.imageUrl"
+      :width="500"
+      :height="500"
+      :radius="90"
+      :delay="300"
+      @cancel="onCancel"
+      @confirm="onConfirm"
+    ></tt-cropper>
+    <view class="preview">
+      <image
+        v-for="(item, index) in model.resultUrl"
+        :key="item.id"
+        class="images"
+        @click="prviewImgae(index, item.url)"
+        :src="item.url"
+      />
+    </view>
+    <button class="button" type="primary" @click="selectFile">选择图片</button>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      model: {
+        imageUrl: "",
+        resultUrl: [],
+      },
+    };
+  },
+  methods: {
+    // 使用uni.compressImage压缩图片
+    compressImage() {
+      uni.chooseImage({
+        count: 1,
+        sizeType: ['original'],
+        success: (res) => {
+          uni.showLoading({
+            title: "处理中...",
+            mask: true,
+          });
+          // 使用uni.compressImage压缩图片
+          uni.compressImage({
+            src: res.tempFilePaths[0],
+            quality: 80, // 压缩质量
+            success: (compressRes) => {
+              this.model.imageUrl = compressRes.tempFilePath;
+            },
+            fail: (err) => {
+              console.error("图片压缩失败:", err);
+            },
+            complete: () => {
+              uni.hideLoading(); // 关闭loading
+            },
+          });
+        },
+      });
+    },
+    // 使用默认压缩方式
+    defaultCompressImage() {
+      uni.chooseImage({
+        count: 1,
+        sizeType: ['compressed'],
+        success: (res) => {
+          this.model.imageUrl = res.tempFilePaths[0];
+        },
+      });
+    },
+    /**
+     *** 特别声明:在使用uni.chooseImage选择的大图片文件无法直接在Image组件中显示,通常涉及到以下可能的问题和限制。
+     *** 图片大小和尺寸限制:移动设备和浏览器对于能够加载和处理的图片大小有限制,如果选择的图片文件尺寸过大,可能无法正常加载和显示。
+     *** 性能问题:大图片文件可能会导致页面加载缓慢或者卡顿,尤其是在移动设备上。
+     *** 内存问题:加载大图片可能会消耗大量的内存资源,特别是在移动设备上,可能导致内存不足或者页面崩溃的问题。
+     *** 解决参考方案如下:
+     *** 防止选择大文件图片后无法在Image中直接临时路径显示图片,导致无法在裁剪插件中显示,
+     *** 根据项目需要对大尺寸图片进行压缩、对图片质量要求高的,需要提前上传至oss进行采用网络图片进行裁剪。
+     */
+    selectFile() {
+      // 推荐使用其他压缩方式:这里只是简单对大文件图片压缩-仅供思路参考,切勿使用该方式
+      // 示例一:uni.compressImage压缩图片
+      // this.compressImage();
+
+      // 示例二:使用自带压缩图
+      this.defaultCompressImage();
+    },
+
+    // 关闭
+    onCancel() {
+      this.model.imageUrl = "";
+    },
+
+    // 确定裁剪
+    onConfirm(res) {
+      const params = {
+        id: new Date().getTime(),
+        url: res.tempFilePath,
+      };
+      this.model.resultUrl.push(params);
+      this.model.imageUrl = "";
+    },
+
+    // 预览图片
+    prviewImgae(index, url) {
+      uni.previewImage({
+        current: index, // 当前资源下标
+        urls: [url],
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.preview {
+  padding: 32rpx;
+
+  .images {
+    margin: 10rpx;
+    width: 200rpx;
+    height: 200rpx;
+  }
+}
+.button {
+  margin: 0 20rpx;
+}
+</style>
+
+
+```
+
+### 注意
+
+1.uni-app版本不断更新,插件有时无法适应新版本,感谢大家及时提交bug,但希望大家手下留情,不要轻易给差评!

+ 15 - 0
uni_modules/tt-cropper/static/svg/rotate.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="30px" height="30px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
+	.st1{fill:#FFFFFF;}
+</style>
+<g>
+	<path class="st0" d="M17.1,24.2h-12c-0.2,0-0.3-0.2-0.3-0.3v-9.3c0-0.2,0.2-0.3,0.3-0.3h12c0.2,0,0.3,0.2,0.3,0.3v9.3
+		C17.5,24.1,17.3,24.2,17.1,24.2z"/>
+	<path class="st0" d="M16.6,5.4c4.8,0,8.7,3.9,8.7,8.7"/>
+	<polyline class="st0" points="19.3,10.1 14.9,5.6 19.3,1.2 	"/>
+</g>
+</svg>