本文主要是介绍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端)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!