uniapp实现裁剪图片-图片生成视频-视频精准定位到原图裁剪的位置(ai虚拟人、智能对话、图片生成视频相关,兼容微信小程序安卓和iOS端)

本文主要是介绍uniapp实现裁剪图片-图片生成视频-视频精准定位到原图裁剪的位置(ai虚拟人、智能对话、图片生成视频相关,兼容微信小程序安卓和iOS端),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原图:
请添加图片描述
选框示例图:
请添加图片描述
裁剪后的图片:
请添加图片描述
合成视频示例:

AI虚拟人展示视频

裁剪代码:

<template><view class="main_box"><view class="head_top" :style="{ height: headerHeight + 'px' }"><navheadbar :width='headerWidth' backTextColor='#fff' bgColor='transparent' bordColor='transparent'></navheadbar></view><view class="content_box"><view class="image_box"><image id='imgTemp' :src="bgImg" mode="widthFix" @load='loadImageHandle'></image><view class="avatar_picker" :style="{ position: 'absolute', left: `${offsetSl}%`, top: `${offsetSt}%` }" id="avatar_picker" @touchmove='touchMoveHandle' @touchstart="touchStartHandle" @touchend="touchEndHandle">请拖动选框选中头部</view><!-- <view class="avatar_picker" :style="{ position: 'absolute', left: `${offsetSl}%`, top: `${offsetSt}%` }" id="avatar_picker" @touchmove='touchMoveHandle' @touchstart="touchStartHandle" @touchend="touchEndHandle">{{ `x:${offsetSl}-y:${offsetSt}` }}</view> --></view><canvas v-if='ispost' class='canvs_box' id="myCanvas" canvas-id="myCanvas" :style="{ width: `${imagePickerData.width}px`, height: `${imagePickerData.height}px` }"></canvas></view><view class="config_btn" @click='clipAvatarHandle(bgImg)'>确认</view></view>
</template><script>import { getSameDataMixin } from '@/utils/sameDataMixin.js'import { initDataBaseMixin } from '@/utils/sameMethodsMixin.js'// 导入vuex中状态遍历方法:import { mapState } from 'vuex'// 导入校验规则:import { useGetBoxSizeHandle, imgToBase64, setDataLocal } from '@/utils/offlineTools.js'import { fetchCommitImageMission } from '@/api/index.js'import { uploadImg } from '@/utils/onlinetools.js'export default {data() {return {...getSameDataMixin(),// 网络背景图地址:bgImg: '',// 采集盒子距离窗口的百分比距离offsetSl: 0,offsetSt: 0,// 鼠标开始坐标百分比距离:startclientX: 0,startclientY: 0,// 采集盒子可移动百分比距离canMoveLeft: 0,canMoveTop: 0,// 鼠标结束距离百分比距离:endclientX: 0,endclientY: 0,// canvas:ctx: {},// canvas容器是否渲染::ispost: false,// 防抖锁:isBtned: false,// 背景图片的数据imageData: {},// 选中图片框的数据:imagePickerData: {}}},onLoad(e) {if (e.param) {let obj = JSON.parse(decodeURIComponent(e.param))this.bgImg = obj.bgImg}},created() {initDataBaseMixin(this)},computed: {...mapState(['globalUserInfo', 'globalMyCard'])},async mounted(){// 2选中图片框的数据:const picker = uni.createSelectorQuery().in(this).select("#avatar_picker")this.imagePickerData = await this.getAvatarPickerInfoHandle(picker)// 渲染canvas:this.ispost = trueconsole.log('选中框的数据:', this.imagePickerData) // bottom: 348  height: 250  left: 0 right: 250 top: 98 width: 250},methods: {// 图片数据:async loadImageHandle(){// 图片数据:let avatbox = uni.createSelectorQuery().in(this).select(".image_box")this.imageData = await this.getAvatarPickerInfoHandle(avatbox)console.log('图片数据:', this.imageData)// bottom: 812  height: 718.84375 left: 0 right: 375 top: 93.15625 width: 375},// 按下位置async touchStartHandle(e) {// 鼠标开始在图片上的坐标百分比:this.startclientX = e.changedTouches[0].clientX / this.imageData.width * 100this.startclientY = (e.changedTouches[0].clientY - this.imageData.top) / this.imageData.height * 100// 选框在图片上可移动距离相对图片百分百:let { left, top, width, height } = this.imagePickerData// 可移动的距离:this.canMoveLeft = (this.imageData.width - width - this.offsetSl) / this.imageData.width * 100this.canMoveTop = (this.imageData.height - height - this.offsetSt) / this.imageData.height * 100},// 移动了touchMoveHandle(e){// 鼠标移动的百分比距离:let x = e.changedTouches[0].clientX / this.imageData.width * 100 - this.startclientXlet y = (e.changedTouches[0].clientY - this.imageData.top) / this.imageData.height * 100 - this.startclientY// 移动选框:if (x > 0) {if (x <= this.canMoveLeft) {this.offsetSl = xthis.endclientX = x} else {this.offsetSl = this.canMoveLeftthis.endclientX = this.canMoveLeft}}if (x < 0) {if (0 <= (this.endclientX + x)) {this.offsetSl = this.endclientX + xthis.endclientX = this.endclientX + x} else {this.offsetSl = 0this.endclientX  = 0}}if (y > 0) {if (y <= this.canMoveTop) {this.offsetSt = ythis.endclientY = y} else {this.offsetSt = this.canMoveTopthis.endclientY = this.canMoveTop}}if (y < 0) {if (0 <= (this.endclientY + y)) {this.offsetSt = this.endclientY + ythis.endclientY = this.endclientY + y} else {this.offsetSt = 0this.endclientY  = 0}}},// 抬起位置touchEndHandle(e) {// 鼠标结束相对图片百分比距离:截取this.endclientX = this.offsetSlthis.endclientY = this.offsetSt},// 裁剪图片:clipAvatarHandle(avatarUrl){const _this = thisif (this.isBtned) return uni.showToast({ title: '请勿频繁操作!', icon: 'none', duration: 3000 })this.isBtned = trueif (!avatarUrl) return uni.showToast({ title: '图片地址无效!', icon: 'none', duration: 3000 })this.ispost = truelet avatarTemp = wx.env.USER_DATA_PATH + "/" + new Date().valueOf() + (Math.floor(Math.random() * 90000) + 10000) + avatarUrl.slice(avatarUrl.length - 5)// / 向用户发起授权请求uni.downloadFile({url: avatarUrl,filePath: avatarTemp,success: res => {console.log('下载成功', res)_this.getImgInfoHandle(avatarTemp)},fail: err => {console.log('err', err)uni.hideLoading()uni.showToast({ title: '保存失败!', icon: 'none' })}})},// 获取图片信息getImgInfoHandle(avatarTemp){const _this = this// 获取图片信息uni.getImageInfo({src: avatarTemp,success: avat => {_this.drawPosterHandle(avat)}})},// 绘制头像:drawPosterHandle(avat){const _this = this// 图片尺寸(需要绘制的图片尺寸):const imgW = this.imageData.widthconst imgH = this.imageData.heightconsole.log('imgW', imgW)console.log('imgH', imgH)// 裁剪框尺寸:const { width, height } = this.imagePickerDataconsole.log('width', width)console.log('height ', height )// 创建canvas上下文:this.ctx = uni.createCanvasContext("myCanvas", this)// 裁剪框:this.ctx.rect(0, 0, width, height)this.ctx.beginPath()// 绘制最外层容器:this.ctx.rect(0, 0, imgW, imgH)this.ctx.clip()// 计算canvas底图需要移动的实际像素:const x = imgW * this.offsetSl / 100const y = imgH * this.offsetSt / 100console.log('x', x)console.log('y', y)// 移动底图绘制到裁剪框this.ctx.drawImage(avat.path, -x, -y, imgW, imgH)this.ctx.save()// 下载海报:this.ctx.draw(true, () => {uni.canvasToTempFilePath({canvasId: "myCanvas",quality: 1,complete: async function (res) {_this.ispost = falselet list = [ res.tempFilePath ]console.log('裁剪图本地地址:', res.tempFilePath)uni.saveImageToPhotosAlbum({filePath: res.tempFilePath,success: () => {_this.ispost = falseuni.showToast({ title: "保存成功", icon: "none" })uni.hideLoading()console.log('进入9')},fail: err => {_this.ispost = true}})// 以下代码与上述:网络图片地址--裁剪选中---本地裁剪预览图地址---导入裁剪图到相册无关returnlet info = await uploadImg (list, '/appserv-card-xq/v1/cardTask/imageCheck', 'file' ,{ 'content-type': 'application/x-www-form-urlencoded' }, true).catch(err => {if (typeof err == 'object' && err !== null) {let errObj = JSON.parse(err.msg)uni.showToast({ title: errObj.msg || '形象复刻上传头像异常', icon: 'none', duration: 2000 })}})let uploadInfo = info[0]if (info && info[0]) {if (info[0].code == 0) {// 提交形象复刻训练_this.submitTranHandle(uploadInfo.data)}}}})})},// 提交形象训练任务:submitTranHandle(paths){if (!paths) returnlet obj = {fileUrl: paths}fetchCommitImageMission(obj).then(res => {if (res && res.code == 0) {let obj = {img: paths,currentStep: 3 , //复刻等待中trainTaskId: res.data,x: this.offsetSl,y: this.offsetSt,w: this.imageData.width,h: this.imageData.height,headImgUrl: paths,isSelect: 1,bottomImgUrl: this.bgImg}// 解锁:this.isBtned = false// 记录训练任务存本地:setDataLocal('lastTask', obj)uni.$emit('changeCurrentStep', obj)uni.navigateBack()} else {console.log('训练任务提交失败', res)}})},// 获取图片采集盒子信息:getAvatarPickerInfoHandle(e){return new Promise(resovle => {e.boundingClientRect(function(data) {resovle(data)}).exec()					})}}}
</script><style lang="scss" scoped>.main_box {@include mx_wh_bc_r(100%, 100%, $color: #333, $radius: false);@include mx_flex($direction: column);position: relative;.head_top {width: 100%;z-index: 99999;}.content_box {flex: 1;.image_box {position: relative;image {height: auto;width: 100%;}.avatar_picker {@include mx_lt($type: absolute, $left: 0, $top: 0);@include mx_wh_bc_r($width: 500rpx, $height: 500rpx, $color: false, $radius: false);background: rgba(255, 255, 255, .4);text-align: center;line-height: 500rpx;z-index: 99999;}}.canvs_box {position: absolute;}}.config_btn {@include mx_ct($type: absolute, $left: 50%, $top: 90%, $translateX: -50%, $translateY: 0);@include mx_wh_bc_r($width: 640rpx, $height: 100rpx, $color: #5A6AF6, $radius: 50rpx);@include mx_font($size: 42rpx, $color: #fff, $weight: false, $lineHeight: 100rpx, $fontFamily: false, $letterSpacing: false);text-align: center;}}
</style>

虚拟人展示页代码:只看视频盒子部分代码即可

<template><view class="main_box"><!-- 视频盒子: --><view class="video_box"><view class="image_box"><image :src="cardInfo.bottomImgUrl ? cardInfo.bottomImgUrl : ''" mode="widthFix"></image><video  v-if='(cardInfo.unMouthImage || cardInfo.mouthImage)' :style="{ zIndex: `${isPlay ? 999 : -999}`, position: 'absolute', left: `${cardInfo.x || 35}%`, top: `${cardInfo.y || 20}%` }" object-fit="cover" ref="video" :src="cardInfo.mouthImage" :controls='false' :autoplay='true' :enableProgressGesture='false' :muted='true' loop :showFullscreenBtn='false' :showCenterPlayBtn='false' :showMuteBtn='false'></video><video v-if='(cardInfo.unMouthImage || cardInfo.mouthImage)' :style="{position: 'absolute', left: `${cardInfo.x || 35}%`, top: `${cardInfo.y || 20}%` }" object-fit="cover" ref="video" :src="cardInfo.unMouthImage" :controls='false' :autoplay='true' :enableProgressGesture='false' :muted='true' loop :showFullscreenBtn='false' :showCenterPlayBtn='false' :showMuteBtn='false'></video></view></view><view class="head_top" :style="{ height: headerHeight + 'px' }"><view class="go_home" @click.stop="goPageHandle"><image src="https://img.js.design/assets/img/6548a53d1719b1e81764c8ef.png" mode=""></image><text>点我</text></view><view class="share_box" @click.stop="menuBtnClickHandle({ id: 1 })"><image src="../../static/images/shareIcon.svg" mode=""></image><text>分享</text></view></view><view class="content_box"><GoDetail btnTxt='查看详情' :isLook='true' :robotInfo='cardInfo'></GoDetail><view class="chat_box"><scroll-view scroll-y="true" :style="{height: boxHeight + 'px'}" :scroll-into-view="currentMsgId" @scrolltoupper='getMoreChatListHandle'><view :class='its.isMy ? "msg_box right_box" : "msg_box left_box"' v-for='(its, index) in chatList' :id='its.id'><view class="msg">{{ its.printText }}</view><view class="date"></view></view></scroll-view></view><view :class="keyboardHeight ? 'input_box top_key_bord_bg' : 'input_box'" :style="{ position: 'relative', top: `${keyboardHeight}px` }" ><image :src="isInput ? '../../static/images/icon-mike.png' : '../../static/images/icon_keyboard.png'" @click='isInput = !isInput'></image><input :adjust-position="false" v-if='isInput' type="text" v-model="inputValue" placeholder='说点什么...' /><view v-else class="voice_btn" @touchstart="touchStartHandle" @touchend="touchEndHandle">按住说话</view><view class="send_box" @click="sendMsgHandle"><image src="../../static/images/icon-send.png"></image><text>发送</text></view></view></view><!-- 或者头像和昵称模态框 --><modalbox v-if='contentBoxHeight' :isOpen='isOpenAvatorAndNickNameModal' :height='contentBoxHeight + headerHeight' top='0' left='0' translateY='0' translateX='0' bgColor='rgba(0, 0, 0, 0.6)' bordColor='rgba(0, 0, 0, 0.6)' v-slot:content><GetAvatarNickName @close='closeHandle' @save='saveHandle'></GetAvatarNickName></modalbox><!-- 分享: --><modalbox @close='menuBtnClickHandle({id: null})' v-if='contentBoxHeight' :isOpen='isOpenConfigShareModal' :height='contentBoxHeight + headerHeight + tabBarHeight' top='0' left='0' translateY='0' translateX='0' bgColor='rgba(0, 0, 0, 0.6)' bordColor='rgba(0, 0, 0, 0.6)' v-slot:content><view class="share_main_box"><button open-type="share" @click.stop='menuBtnClickHandle({ id: 4 })'><image src="../../static/images/wxFriend.png" mode=""></image><text>微信好友</text></button><view @click.stop='menuBtnClickHandle({ id: 5 })'><image src="../../static/images/cardPost.png" mode=""></image><text>名片海报</text></view></view></modalbox><view class="copy_voice" v-if='isTouch'><image src="../../static/images/speakGif.gif" mode=""></image><text>手指上划,取消发送</text></view></view>
</template><script>import { terminalId, wxAppid } from '@/config/globalConfig.js'// 引入公共基本数据(头部导航栏尺寸、底部tabBar栏尺寸、是否为ios系统):import { getSameDataMixin } from '@/utils/sameDataMixin.js'// 引入: initDataBaseMixin基本数据初始化方法(headerHeight, headerWidth, tabBarHeight, isIos等变量的初识)、refreshIsLightOrDarkByTime朝夕模式自动刷新import { initDataBaseMixin } from '@/utils/sameMethodsMixin.js'// 导入本地工具import { navigationToHandle, useGetBoxSizeHandle, loginByWeChatHandle, imgToBase64, setDataLocal } from '@/utils/offlineTools.js'// 导入vuex中状态遍历方法:import { mapState } from 'vuex'// 导入接口:import { loginByWechatIvCodeApi, fetchLogin, getValidChatCountApi, deductChatNumApi, updateUserInfoApi, fetchCardDetail, fetchRecorderVisitor, fetchAnalyzeOpenId, fetchContactList, queryAvatarAndBgimgApi, recordChatMsgApi, fetchSearchRecord           , fetchVideoStatusByFileSeq, getUserInfoApi } from '@/api/index.js'// 引入组件:import navheadbar from '../../components/navheadbar/navheadbar.vue'import { uploadImg2 } from '@/utils/onlinetools.js'// 导入websocketimport socketio from '@/socketio/index.js'import Vue from 'vue'export default {components: { navheadbar },onShareAppMessage(res) {return {title: `您好,我是${this.cardInfo.personName || '您的朋友'}, 我是第${this.cardInfo.id || 9999}个拥有数字分身的人类`,path: `/pages/aiChat/aiChat?scene=${this.cardInfo.cardId}`}},data() {return {// 基本数据:...getSameDataMixin(),// 中间有效区域的高度:boxHeight: 0,// 模态框高度:contentBoxHeight: 0,isOpenAvatorAndNickNameModal: false,// 分享弹框:isOpenConfigShareModal: false,// 消息列表:chatList: [],// 是否为输入类型:isInput: true,// 输入文本:inputValue: '',// 是否已经确认过:isLocked: true,// 卡片信息cardInfo: {},// 背景图:bgImg: '',// 是否播放:isPlay: false,// 队列消息:queueList: [],// 防抖:msgTimeId: 0,msgEndTimeId: 0,stopTimeId: 0,// 位置:touchStart: 0,touchEnd: 0,// 是否按下:isTouch: false,// 聊天消息id:currentMsgId: 0,// 缓存文本:bufferString: '',// 缓存文本定时器:bufferMsgTimeId: 0,// 语音播报锁:voiceLock: true,// 视频防抖:videoTimeId: 0,// 查询聊天记录请求参数:queryHistoryObj: {},// 页码pageNo: 1,// 键盘高度keyboardHeight: 0,// 当前是否为自己名片:currentIsMyCard: false}},watch: {'chatList': {handler(val){let tempObj = val[val.length - 1]if (!tempObj.isMy && !this.queryHistoryObj.isQueryHistory) {this.printTextHandle(val)} else {val[val.length - 1].printText = val[val.length - 1].content}}}, 'globalMyCard': {handler(val){if (this.currentIsMyCard) {this.cardInfo = val}}}},onShow(){this.$store.commit('updateGlobalCurrentCardMut', this.cardInfo)},computed: {...mapState(['globalScene', 'globalUserInfo', 'globalContacts', 'globalMyCard', 'globalCurrentCard'])},async mounted() {const info = uni.createSelectorQuery().select('.chat_box')this.boxHeight = await useGetBoxSizeHandle(info, 'height', [0, 0, 0, 0])let contentBox = uni.createSelectorQuery().select('.main_box')this.contentBoxHeight = await useGetBoxSizeHandle(contentBox, 'height', [0, 0, 0, 0])uni.onKeyboardHeightChange(res => {this.keyboardHeight = -res.height})},onLoad(e){if (e.param) {let obj = JSON.parse(decodeURIComponent(e.param))// 查聊天记录:if (obj.uid && obj.isQueryHistory) {this.queryHistoryObj = objthis.queryChatHistory(obj.uid)this.pageNo = 1} else {this.queryHistoryObj = {}}} else {this.queryHistoryObj = {}}// 是否分享获取列表页进入if (e.scene) {this.$store.commit('updateGlobalSceneMut', e.scene)} else {this.$store.commit('updateGlobalSceneMut', null)}// 语音模块:this.$WSI.addCallBack('cardChat', this.onMsg)// 初始化数据(登录、自动播放一系列)this.initData()},onHide(){this.$WSI.pause()},destroyed(){this.$WSI.removeCallBack('cardChat')this.$ws.removeCallBack('acknowledge')},created(){// 初始化基本数据:initDataBaseMixin(this)// 初始化网络状态:this.initNetWorkStatus()// 监听聊天次数用完:uni.$on('aiChatFinish', () => uni.showToast({ title: '您的次数已用完', icon: 'none', duration: 2000 }))// 监听切换到自己名片:uni.$on('changeMyCard', myCard => {this.cardInfo = myCardthis.$store.commit('updateGlobalCurrentCardMut', myCard)})},methods: {// 查询更多聊天记录:getMoreChatListHandle(){this.queryChatHistory()},// 查询聊天记录:queryChatHistory(uid){if (!this.queryHistoryObj.isQueryHistory) returnlet obj = {cardId: this.globalMyCard.cardId,uid: uid || this.globalCurrentCard.uid,pageNo: this.pageNo,pageSize: 10}fetchSearchRecord(obj).then(res => {if (res.code != 0) returnif (res.data.list.length != 0) {this.pageNo ++let ls = []res.data.list.forEach(item => {if (item.message && item.message.trim() != '') {ls.push({ id: ('current' + item.id), isMy: (item.type == 'request'), printText: item.message })}})this.chatList.unshift(...ls)}})},// 打字效果:printTextHandle(val){let arrList = []let tempObj = val[val.length - 1]arrList = tempObj.content.split('')let i = 0function pushMsg(){val[val.length - 1].printText += arrList[i]i++if (i < arrList.length) {setTimeout(() => {pushMsg()}, 30)}}pushMsg()},// 说话:async touchStartHandle(e){if (!this.globalUserInfo.token) return uni.showToast({ title: '请先登录', icon: 'none', duration: 2000 })// 查询聊天次数是否有效:let num = await this.isValidChatCountHandle()if (num <= 0) return uni.showToast({ title: '您的次数已用完!', icon: 'none', duration: 2000 })this.touchStart = e.changedTouches[0].clientYthis.touchEnd = e.changedTouches[0].clientYthis.isTouch = truethis.$WSI.pause()this.$WSI.touchStart()},// 停止说话:touchEndHandle(e){this.touchEnd = e.changedTouches[0].clientYthis.isTouch = falseif (this.touchStart - this.touchEnd > 10) {this.$WSI.close()this.$WSI.touchEnd()return}this.$WSI.touchEnd()},// 初始化:async initData(){const result = await loginByWeChatHandle().catch(err => {uni.showToast({ title: '登录失败', icon: 'none', duration: 3000, mask: true })})const obj = { wxCode: result.code, WxAppid: wxAppid, terminalId: terminalId }const idRes = await fetchAnalyzeOpenId(obj)console.log('idRes', idRes)if (idRes && idRes.code == 0) {const { openId, unionId } = idRes.datalet tempOpenId = { ...this.globalUserInfo, openId, unionId }this.$store.commit('updateGlobalUserInfoMut', tempOpenId)// 用户登录const myLogin = await loginByWechatIvCodeApi({ unionId: unionId })let tempLoginMy = {...myLogin.data,token2: myLogin.data.token}const loginRes = await fetchLogin({ keytp: 'openid', uname: openId, terminalId: terminalId })if (loginRes.code != 0) return uni.showToast({ title: '登录失败1', icon: 'none', duration: 3000, mask: true })let tempLoginInfo = { ...this.globalUserInfo, ...tempLoginMy, ...loginRes.data }this.$store.commit('updateGlobalUserInfoMut', tempLoginInfo)// 连接socket.iothis.connectIoHandle()// 添加wocket回调this.$ws.addCallBack('acknowledge', this.onMsgWebSocket)// 初始化名片信息:this.initCard()}},// 连接ws:connectIoHandle() {// 通过单例模式连接ws服务器:socketio.Instance.connect()// // 将websocket挂载到vue原型上:Vue.prototype.$ws = socketio.Instance// 添加wocket回调this.$ws.addCallBack('aiChatResp', this.onMsgWebSocket)},// 初始化名片(是否扫码或列表进入)initCard () {// 分享id(外部分享优先级最高,但仅限一次)let fetchShareId = ''// 是否记录访客let isRecorderVisitor = false// 扫描进入:if (this.globalScene) {// 分享名片进入-更改分享人名片id:fetchShareId = this.globalSceneisRecorderVisitor = true// 调用完分享后清除扫码绑定的idthis.$store.commit('updateGlobalSceneMut', null)} else if (this.globalContacts) {// 来自联系人页进入fetchShareId = this.globalContactsisRecorderVisitor = false}// 获取首页名片流程this.getCardInfo(fetchShareId, isRecorderVisitor)// 获取用户信息昵称头像:this.getUserInfoHandle()},// 查询名片详细信息:getCardInfo(share, isRecorder){// 有名片id时:if (share) {fetchCardDetail({ cardId: share }).then(async res => {if (res.code == 0 && res.data) {this.cardInfo = res.datathis.currentIsMyCard = falsethis.avatarAndBgimgHandle()this.$store.commit('updateGlobalCurrentCardMut', res.data)// 注册webso事件this.addSocketHandle()// 分享进入记录访客:if (isRecorder) {this.recorderVisitor(share)}// 有自我介绍视频id时获取视频:if (res.data?.sound) {await this.getIntroVideo(res.data).catch(() => {this.playAudio(res.data)})this.playAudio(res.data)}}})} else {// 没有id时查询的是自己的名片fetchCardDetail().then(res => {if (res && res.code == 0 && res.data) {this.cardInfo = res.dataconsole.log('自己的名片数据', res.data)this.currentIsMyCard = truethis.avatarAndBgimgHandle(true)// setDataLocal('currentCard', res.data)this.$store.commit('updateGlobalCurrentCardMut', res.data)// 注册webso事件this.addSocketHandle()// 修改store中自己名片信息:this.$store.commit('updateGlobalMyCardMut', res.data)if (res.data.sound) {this.getIntroVideo(res.data).then((updateTip) => {if (updateTip) {uni.showToast({ title: '形象复刻已升级,您可在<我的-形象复刻>重新生成', icon: 'none', duration: 4000 })}this.playAudio(res.data)}).catch((err) => {this.playAudio(res.data)})}} else {// 不存在我的卡片,获取老板boss卡片fetchContactList({ isDefault: 1 }).then((res) => {if (res && res.code == 0) {const bossCard = res.data.list[0]this.cardInfo = bossCardthis.currentIsMyCard = falsethis.avatarAndBgimgHandle()// if (!this.cardInfo.videoUrl) {// 	this.getCardInfo(this.cardInfo.cardId, false)// }// setDataLocal('CURRENT_CARD', bossCard)this.$store.commit('updateGlobalCurrentCardMut', bossCard)// 注册webso事件this.addSocketHandle()if (bossCard.sound) {// 获取自我介绍视频后播放音频this.getIntroVideo(bossCard).then(() => {this.playAudio(bossCard)}).catch((err) => {this.playAudio(bossCard)})}}})}})}},// 根据名片id查询名片照片信息:avatarAndBgimgHandle(isMy){let obj = {bmCardId: this.cardInfo.cardId,type: 0 // 0-形象照,1-证件照}queryAvatarAndBgimgApi(obj).then(res => {if (res.code == 200 && res.data) {console.log('执行了')let temp = {...this.cardInfo,x: res.data.x,w: res.data.w,h: res.data.h,y: res.data.y,myCardId: res.data.id,bottomImgUrl: res.data.bottomImgUrl}this.cardInfo = tempthis.$store.commit('updateGlobalCurrentCardMut', temp)console.log('更新当前头像定位数据:', temp)if (isMy) {console.log('更新自己头像定位数据:', temp)// 修改store中自己名片信息:this.$store.commit('updateGlobalMyCardMut', temp)}}})},// 获取用户信息:getUserInfoHandle(){getUserInfoApi().then(res => {if (res.code == 0) {let temp = {...this.globalUserInfo,...res.data}this.$store.commit('updateGlobalUserInfoMut', temp)// 是否有昵称或头像if ((!this.globalUserInfo.nickname) || (!this.globalUserInfo.imgUrl)) {this.isOpenAvatorAndNickNameModal = true}}})},// 获取自我介绍视频(resolve是否需要提示形象复刻升级)getIntroVideo(cardData) {const { videoUrl = '', robotId = '', mouthImage = '', unMouthImage = '' } = cardDatareturn new Promise((resolve, reject) => {if (mouthImage && unMouthImage) {// 存在已生成的形象,直接使用this.introVideo = mouthImagethis.staticVideo = unMouthImagethis.defaultVideo = ''resolve(false)return}// 废弃-------------------------------------------------------------------------------------------------------if (videoUrl || robotId) {// 未生成形象,从老形象接口获取fetchVideoStatusByFileSeq({ fileSeq: videoUrl || robotId }).then((res) => {if (res && res.code === 0 && res.data?.status === 1) {// 视频资源地址(在线形式)const resUrl = res.data?.minioData?.[0]?.namethis.introVideo = ''this.staticVideo = ''this.defaultVideo = resUrl// 仅当视频是老的版本,进行提示resolve(true)} else {this.introVideo = ''this.staticVideo = ''this.defaultVideo = ''reject(new Error('视频接口请求失败'))}})} else {this.introVideo = ''this.staticVideo = ''this.defaultVideo = ''resolve(false)}// 废弃-------------------------------------------------------------------------------------------------------})},// 自动播放自我介绍音频playAudio(cardInfo) {this.isPlay = truethis.$WSI.textToVoice(cardInfo.personalProfile, cardInfo.soundUrl, true, this.cardInfo.sound)},// 注册webSocket事件回调:addSocketHandle(){// 1.注册websock事件:let objTemp = {method: "authReq",robotId: this.globalCurrentCard.robotId,userIdentity: this.globalUserInfo.openId}this.$ws.send(objTemp)this.$nextTick(() => {let objTemp1 = {method: "heartbeat",userIdentity: this.globalUserInfo.openId}this.$ws.send(objTemp1)})},// 新增好友:recorderVisitor(shareId) {const payload = {cardId: shareId}fetchRecorderVisitor(payload)},// 初始化网络状态和监听网络状态变化:initNetWorkStatus(){let status = this.$NET.getNetWorkStatus()this.$store.commit('updateGlobalNetWorkStatusMut', status)// 监听网络状态发生变化:this.$NET.addCallBack('netWorkStatus', this.updateNetWork)},// 需改网络状态:updateNetWork(e){this.$store.commit('updateGlobalNetWorkStatusMut', e)if (!e) {uni.showLoading({ title: '网络连接已断开,正在尝试重连!', mask: false })} else {uni.hideLoading()}},// 发文本:async sendMsgHandle(){if (!this.inputValue) return// 查询聊天次数是否有效:let num = await this.isValidChatCountHandle()if (num <= 0) return uni.showToast({ title: '您的次数已用完!', icon: 'none', duration: 2000 })const obj = {type: 'voiceToText',content: this.inputValue,isMy: true}this.onMsg(obj)this.inputValue = ''},// 监听语音模块文字推送:onMsg(msg){if (msg == 'stopVideo') {// 停止播放后从队列中继续拿第一条文字转语音clearTimeout(this.msgTimeId)this.msgTimeId = setTimeout(() => {if (this.queueList[0] && this.isPlay) {this.$WSI.textToVoice(this.queueList[0], null, true, this.cardInfo.sound)this.isPlay = false} else {clearTimeout(this.stopTimeId)this.stopTimeId = setTimeout(() => {this.isPlay = false}, 300)}}, 1000)} else if (msg == 'playVideo') {// 视频开始播放时,删除队列中上一次第一条文字:this.msgEndTimeId = setTimeout(() => {if (this.queueList.length != 0 && (!this.isPlay)) {// 播放视频:this.isPlay = truethis.queueList.shift()}}, 800)} else if (msg?.type == 'voiceToText') {let objTemp = {cardId: this.globalCurrentCard.cardId,chatModel: 1,contents: [{type: '文本',data: {content: msg.content,},}],method: "aiChatReq",robotId: this.globalCurrentCard.robotId,terminalId: terminalId,userIdentity: this.globalUserInfo.openId}let rid = 'id' + Math.floor(Math.random()*(9-99999999)+99999999) + '' + Date.parse(new Date())this.currentMsgId = ridconst obj = {type: 'voiceToText',content: msg.content,isMy: true,id: rid}this.chatList.push(obj)this.$ws.send(objTemp)// 扣减聊天次数:let objNum = {userId: this.globalUserInfo.id}deductChatNumApi(objNum)// 记录聊天消息let redMsg = {message: msg.content,receiveUserId: this.globalCurrentCard.cardId,sendUserId: this.globalUserInfo.id,type: 0}recordChatMsgApi(redMsg)}},// 判断聊天次数余额是否有效:isValidChatCountHandle(){return new Promise(resolve => {let obj = {userId: this.globalUserInfo.id}getValidChatCountApi(obj).then(res => {if (res.code != 200) return uni.showToast({ title: res.msg || '查询聊天次数接口异常!', duration: 2000, icon: 'none' })resolve(res.data)})})},// 消息推送过来:onMsgWebSocket(data) {if (!(data.contents && data.contents.length != 0)) returnlet contentTemp = data.contents[0]if (contentTemp.type == '文本') {this.bufferString = this.bufferString + contentTemp.data.contentclearTimeout(this.bufferMsgTimeId)this.bufferMsgTimeId = setTimeout(() => {this.sendMsg(this.bufferString)this.bufferString = ''this.voiceLock = true}, 300)} else if (contentTemp.type == '缓冲文本') {this.sendMsg(contentTemp.data.content)}},sendMsg(msgContent) {if (msgContent == '') returnlet rid = 'current' + Math.floor(Math.random()*(9-99999999)+99999999) + '' + Date.parse(new Date())this.currentMsgId = rid// 队列中添加数据:this.queueList.push(msgContent)const obj = {type: 'voiceToText',content: msgContent,printText: '',isMy: false,id: rid}this.chatList.push(obj)// 当为播放时才可以执行文字转语音if (this.voiceLock && !this.isPlay) {this.$WSI.textToVoice(this.queueList[0], null, true, this.cardInfo.sound)this.voiceLock = false}// 记录聊天消息let redMsg = {message: msgContent,receiveUserId: this.globalUserInfo.id,sendUserId: this.globalCurrentCard.cardId,type: 0}recordChatMsgApi(redMsg)},// 关闭获取头像和昵称弹框closeHandle(){this.isOpenAvatorAndNickNameModal = false// 是否去到进入选择页面;this.isSelectPageHandle()},// 保存头像和昵称(提交信息)async saveHandle(e){let list = []// 提交信息:let obj = {nickname: e.nickname,id: this.globalUserInfo.id}if (!e.isReandomCreate) {// 上传头像list = await uploadImg2([e.avatarUrl], '/common/upload', 'file')obj.imgUrl = list[0]} else {obj.imgUrl = e.avatarUrl}updateUserInfoApi(obj).then(res => {if (res.code != 200) return uni.showToast({ title: res.msg || '设置头像和昵称接口异常!', duration: 2000, icon: 'none' })let temp = {...this.globalUserInfo,nickname: e.nickname,imgUrl: obj.imgUrl}this.$store.commit('updateGlobalUserInfoMut', temp)this.isOpenAvatorAndNickNameModal = false// 是否自动进入选择页面;// this.isSelectPageHandle()})},// 跳转页面:goPageHandle(){if (!this.globalUserInfo.id) return uni.showToast({ title: '登录异常!', duration: 2000, icon: 'none' })// 没有填写昵称和头像时:if (((!this.globalUserInfo.nickname) || (!this.globalUserInfo.imgUrl)) && this.isLocked) {this.isOpenAvatorAndNickNameModal = truethis.isLocked = falsereturn}// 是否去到进入选择页面;this.isSelectPageHandle()},// 是否到选择进入页面:isSelectPageHandle(){// 判断是否付过款:if (!this.globalUserInfo.vipConfigId) {navigationToHandle('pageA', 'homePage')} else {navigationToHandle('pages', 'index')					}},// 按钮处理:menuBtnClickHandle(e){if (e.id == 1) {// 分享弹框:this.isOpenConfigShareModal = true} else if (e.id == 5) {// 海报下载this.isOpenConfigShareModal = falsenavigationToHandle('pageA', 'sharePoster', this.cardInfo)} else {// 关闭分享弹框:this.isOpenConfigShareModal = false}}}}
</script>
<style lang='scss' scoped>.main_box {@include mx_wh_bc_r(100%, 100%, $color: false, $radius: false);@include mx_flex($direction: column);position: relative;.share_main_box {@include mx_flex($direction: row, $justify: false, $align: false);@include mx_lb($type: absolute, $left: false, $bottom: 100rpx);@include mx_wh_bc_r(100%, 550rpx, $color: #fff, $radius: 10rpx);view, button {@include mx_flex($direction: column, $justify: false, $align: center);@include mx_mp($boxSize: border-box, $padding: 78rpx 0 0, $margin: false);flex: 1;@include mx_wh_bc_r($width: false, $height: 100%, $color: #fff, $radius: false);&::after {border: none;background: transparent;}image{@include mx_wh_bc_r(120rpx, 120rpx, $color: false, $radius: 50%);}text{margin-top: 11rpx;heihgt: 33rpx;@include mx_font($size: 24rpx, $color: #333, $weight: 400, $lineHeight: 33rpx, $fontFamily: false);}}}.head_top {position: relative;@include mx_wh_bc_r($width: 100%, $height: false, $color: false, $radius: false);.go_home {@include mx_flex($direction: row, $justify: center, $align: center);@include mx_wh_bc_r($width: 112rpx, $height: 51rpx, $color: false, $radius: 26rpx);@include mx_lb($type: absolute, $left: 25rpx, $bottom: 20rpx);@include mx_clearbtn($bgcolor: transparent);z-index: 99999;image {@include mx_wh_bc_r($width: 40rpx, $height: 40rpx, $color: false, $radius: false);}text {@include mx_flex($direction: row, $justify: center, $align: center);@include mx_font($size: 18rpx, $color: #fff, $weight: false, $lineHeight: false, $fontFamily: false, $letterSpacing: false);}border: 1rpx solid #fff;}button:after {border: none;border-radius: none;}.share_box {@include mx_flex($direction: row, $justify: false, $align: center);@include mx_lb($type: absolute, $left: 400rpx, $bottom: 20rpx);@include mx_wh_bc_r($width: false, $height: 50rpx, $color: false, $radius: false);image {@include mx_wh_bc_r($width: 32rpx, $height: 32rpx, $color: false, $radius: false);}text {margin-left: 10rpx;@include mx_font($size: 22rpx, $color: #fff, $weight: false, $lineHeight: false, $fontFamily: false, $letterSpacing: false);}}}.content_box {flex: 1;@include mx_flex($direction: column, $justify: flex-end, $align: false);position: relative;overflow: hidden;.chat_box {margin: 0 auto;@include mx_wh_bc_r($width: 90%, $height: 500rpx, $color: false, $radius: false);.msg_box {@include mx_flex($direction: row, $justify: false, $align: center);width: 100%;margin: 10rpx 0;.msg {@include mx_mp($boxSize: border-box, $padding: 20rpx, $margin: 33rpx 0 0);@include mx_font($size: 26rpx, $color: #fff, $weight: 700, $lineHeight: false, $fontFamily: false);}}.left_box {flex-direction: row;.msg {@include mx_wh_bc_r($width: false, $height: false, $color: rgba(0, 186, 173, 0.3), $radius: 0 14rpx 14rpx 14rpx);}.date {padding-left: 32rpx;}}.right_box {flex-direction: row-reverse;.msg {@include mx_wh_bc_r($width: false, $height: false, $color: rgba(213, 119, 247, 0.3), $radius: 14rpx 0 14rpx 14rpx);}.date {padding-right: 32rpx;}}}.input_box {@include mx_flex($direction: row, $justify: false, $align: center);@include mx_wh_bc_r(100%, 150rpx, $color: flase, $radius: false);border-top: 1rpx solid transparent;z-index: 9999;image {margin: 0 20rpx;@include mx_wh_bc_r($width: 55rpx, $height: 55rpx, $color: false, $radius: 50%);}input, .voice_btn {flex: 1;@include mx_font($size: 28rpx, $color: #fff, $weight: false, $lineHeight: false, $fontFamily: false, $letterSpacing: false);@include mx_mp($boxSize: border-box, $padding:  0 30rpx, $margin: false);@include mx_wh_bc_r($width: false, $height: 66rpx, $color: #333, $radius: 36rpx);}.voice_btn {text-align: center;line-height: 58rpx;}.send_box {margin: 0 20rpx;@include mx_wh_bc_r($width: 120rpx, $height: 66rpx, $color: false, $radius: 36rpx);@include mx_flex($direction: row, $justify: false, $align: center);box-sizing: border-box;border: 1rpx solid #fff;image {margin: 0 10rpx;@include mx_wh_bc_r($width: 40rpx, $height: 40rpx, $color: false, $radius: 50%);}text {@include mx_font($size: 22rpx, $color: #fff, $weight: false, $lineHeight: false, $fontFamily: false, $letterSpacing: false);}}}.top_key_bord_bg {background: #D1D1DB;}}background-color: #222629;opacity: 0.9;.video_box {@include mx_ct($type: absolute, $left: 50%, $top: 50%, $translateX: -50%, $translateY: -50%);@include mx_wh_bc_r($width: 100%, $height: 100%, $color: false, $radius: false);z-index: -99;.image_box {@include mx_ct($type: relative, $left: 50%, $top: 50%, $translateX: -50%, $translateY: -50%);image {height: auto;width: 100%;}video {@include mx_lt($type: absolute, $left: 0, $top: 0);@include mx_wh_bc_r($width: 500rpx, $height: 500rpx, $color: false, $radius: false);@include mx_mp($boxSize: border-box, $padding: 0, $margin: 0);}}}.copy_voice {@include mx_wh_bc_r(262rpx, 262rpx, $color: false, $radius: false);@include mx_ct($type: absolute, $left: 50%, $top: 44%, $translateX: -50%, $translateY: -50%);@include mx_bic($url: '../../static/images/speakWrap.png', $color: false, $size: cover, $repeat: no-repeat);@include mx_mp($boxSize: border-box, $padding: 80rpx 0 0, $margin: false);text-align: center;z-index: 9999;image {@include mx_wh_bc_r(100%, 45%, $color: false, $radius: false);}text {@include mx_font($size: 22rpx, $color: #fff, $weight: 400, $lineHeight: 45rpx, $fontFamily: false);}}}
</style>

实现思路:

图片采集页面和聊天页面放置底图的容器尺寸都是:宽度100%,高度由图片撑开,视频和图片抓取尺寸定位一致,定位都相对于底图按百分比计算,图片采集实现步骤推导如下:
请添加图片描述

请添加图片描述

请添加图片描述
疑问解答:嘴巴会动、眼睛会动、安卓端裁剪的图片生成的视频在ios端底部有1像素留白(兼容性问题,后面可优化,上述代码开发阶段,有bug正常,再优化迭代中…)

提示:本文图片等素材来源于网络,若有侵权,请发邮件至邮箱:810665436@qq.com联系笔者删除。

笔者:苦海123

其它问题可通过以下方式联系本人咨询:

QQ:810665436

微信:ConstancyMan

这篇关于uniapp实现裁剪图片-图片生成视频-视频精准定位到原图裁剪的位置(ai虚拟人、智能对话、图片生成视频相关,兼容微信小程序安卓和iOS端)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/624518

相关文章

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

AI绘图怎么变现?想做点副业的小白必看!

在科技飞速发展的今天,AI绘图作为一种新兴技术,不仅改变了艺术创作的方式,也为创作者提供了多种变现途径。本文将详细探讨几种常见的AI绘图变现方式,帮助创作者更好地利用这一技术实现经济收益。 更多实操教程和AI绘画工具,可以扫描下方,免费获取 定制服务:个性化的创意商机 个性化定制 AI绘图技术能够根据用户需求生成个性化的头像、壁纸、插画等作品。例如,姓氏头像在电商平台上非常受欢迎,

W外链微信推广短连接怎么做?

制作微信推广链接的难点分析 一、内容创作难度 制作微信推广链接时,首先需要创作有吸引力的内容。这不仅要求内容本身有趣、有价值,还要能够激起人们的分享欲望。对于许多企业和个人来说,尤其是那些缺乏创意和写作能力的人来说,这是制作微信推广链接的一大难点。 二、精准定位难度 微信用户群体庞大,不同用户的需求和兴趣各异。因此,制作推广链接时需要精准定位目标受众,以便更有效地吸引他们点击并分享链接

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

从去中心化到智能化:Web3如何与AI共同塑造数字生态

在数字时代的演进中,Web3和人工智能(AI)正成为塑造未来互联网的两大核心力量。Web3的去中心化理念与AI的智能化技术,正相互交织,共同推动数字生态的变革。本文将探讨Web3与AI的融合如何改变数字世界,并展望这一新兴组合如何重塑我们的在线体验。 Web3的去中心化愿景 Web3代表了互联网的第三代发展,它基于去中心化的区块链技术,旨在创建一个开放、透明且用户主导的数字生态。不同于传统

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

AI一键生成 PPT

AI一键生成 PPT 操作步骤 作为一名打工人,是不是经常需要制作各种PPT来分享我的生活和想法。但是,你们知道,有时候灵感来了,时间却不够用了!😩直到我发现了Kimi AI——一个能够自动生成PPT的神奇助手!🌟 什么是Kimi? 一款月之暗面科技有限公司开发的AI办公工具,帮助用户快速生成高质量的演示文稿。 无论你是职场人士、学生还是教师,Kimi都能够为你的办公文

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G