新增语音输入和语音转文字接口对接,并调整部分样式

This commit is contained in:
wangyifeng 2025-04-22 19:06:02 +08:00
parent eb1523516c
commit dd42b26d1f
3 changed files with 251 additions and 108 deletions

View File

@ -1,9 +1,6 @@
import request from '@/service/index.js' import request from '@/service/index.js'
import qs from 'qs' import qs from 'qs'
import { import { useTalkStore, useDialogueStore } from '@/store'
useTalkStore,
useDialogueStore
} from '@/store'
// 获取聊天列表服务接口 // 获取聊天列表服务接口
export const ServeGetTalkList = (data) => { export const ServeGetTalkList = (data) => {
@ -44,7 +41,11 @@ export const ServeTopTalkList = (data) => {
// 清除聊天消息未读数服务接口 // 清除聊天消息未读数服务接口
export const ServeClearTalkUnreadNum = (data, unReadNum) => { export const ServeClearTalkUnreadNum = (data, unReadNum) => {
console.log('=======chatApp==UnreadNum', unReadNum) console.log('=======chatApp==UnreadNum', unReadNum)
if(!useTalkStore().items[useTalkStore().findTalkIndex(useDialogueStore().index_name)]?.is_disturb){ if (
!useTalkStore().items[
useTalkStore().findTalkIndex(useDialogueStore().index_name)
]?.is_disturb
) {
if (typeof plus !== 'undefined') { if (typeof plus !== 'undefined') {
let OAWebView = plus.webview.all() let OAWebView = plus.webview.all()
OAWebView.forEach((webview) => { OAWebView.forEach((webview) => {
@ -229,4 +230,13 @@ export const ServeMessageReadDetail = (data) => {
method: 'POST', method: 'POST',
data, data,
}) })
} }
// 语音转文字
export const ServeConvertText = (data) => {
return request({
url: '/api/v1/talk/message/voice-to-text',
method: 'POST',
data,
})
}

View File

@ -25,7 +25,15 @@
class="flex flex-col items-center justify-center" class="flex flex-col items-center justify-center"
> >
<tm-image :width="40" :height="40" :src="copy07"></tm-image> <tm-image :width="40" :height="40" :src="copy07"></tm-image>
<div>复制</div> <div class="mt-1">复制</div>
</div>
<div
v-if="props.isShowConvertText"
@click="() => itemClick('convertText')"
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="copy07"></tm-image>
<div class="mt-1">转文字</div>
</div> </div>
<div <div
@click="() => itemClick('multipleChoose')" @click="() => itemClick('multipleChoose')"
@ -36,7 +44,7 @@
:height="40" :height="40"
:src="multipleChoices" :src="multipleChoices"
></tm-image> ></tm-image>
<div>多选</div> <div class="mt-1">多选</div>
</div> </div>
<div <div
v-if="props.isShowCite" v-if="props.isShowCite"
@ -44,7 +52,7 @@
class="flex flex-col items-center justify-center" class="flex flex-col items-center justify-center"
> >
<tm-image :width="40" :height="40" :src="cite"></tm-image> <tm-image :width="40" :height="40" :src="cite"></tm-image>
<div>引用</div> <div class="mt-1">引用</div>
</div> </div>
<div <div
v-if="props.isShowWithdraw" v-if="props.isShowWithdraw"
@ -52,14 +60,14 @@
class="flex flex-col items-center justify-center" class="flex flex-col items-center justify-center"
> >
<tm-image :width="40" :height="40" :src="withdraw"></tm-image> <tm-image :width="40" :height="40" :src="withdraw"></tm-image>
<div>撤回</div> <div class="mt-1">撤回</div>
</div> </div>
<div <div
@click="() => itemClick('actionDelete')" @click="() => itemClick('actionDelete')"
class="flex flex-col items-center justify-center" class="flex flex-col items-center justify-center"
> >
<tm-image :width="40" :height="40" :src="delete07"></tm-image> <tm-image :width="40" :height="40" :src="delete07"></tm-image>
<div>删除</div> <div class="mt-1">删除</div>
</div> </div>
</div> </div>
<div :style="data.iconStyle" class="icon"></div> <div :style="data.iconStyle" class="icon"></div>
@ -78,8 +86,8 @@
// //
// uniapp & vue // uniapp & vue
import { onLoad, onReady } from "@dcloudio/uni-app"; import { onLoad, onReady } from '@dcloudio/uni-app'
import { defineEmits, defineProps } from "vue"; import { defineEmits, defineProps } from 'vue'
import { import {
reactive, reactive,
ref, ref,
@ -87,18 +95,18 @@ import {
computed, computed,
nextTick, nextTick,
getCurrentInstance, getCurrentInstance,
onMounted, onMounted,
onBeforeUnmount onBeforeUnmount,
} from "vue"; } from 'vue'
import copy07 from "@/static/image/chatList/copy07@2x.png"; import copy07 from '@/static/image/chatList/copy07@2x.png'
import multipleChoices from "@/static/image/chatList/multipleChoices@2x.png"; import multipleChoices from '@/static/image/chatList/multipleChoices@2x.png'
import cite from "@/static/image/chatList/cite@2x.png"; import cite from '@/static/image/chatList/cite@2x.png'
import withdraw from "@/static/image/chatList/withdraw@2x.png"; import withdraw from '@/static/image/chatList/withdraw@2x.png'
import delete07 from "@/static/image/chatList/delete@2x.png"; import delete07 from '@/static/image/chatList/delete@2x.png'
// pinia // pinia
const systemInfo = uni.getSystemInfoSync(); const systemInfo = uni.getSystemInfoSync()
const bubbleRef = ref(null); const bubbleRef = ref(null)
const props = defineProps({ const props = defineProps({
isShowCopy: { isShowCopy: {
@ -113,109 +121,114 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}); isShowConvertText: {
//
type: Boolean,
default: false,
},
})
const emits = defineEmits(["clickMenu"]); const emits = defineEmits(['clickMenu'])
/** /**
* @name 生成UUID * @name 生成UUID
*/ */
const uuid = () => { const uuid = () => {
const reg = /[xy]/g; const reg = /[xy]/g
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
.replace(reg, function (c) { .replace(reg, function (c) {
var r = (Math.random() * 16) | 0, var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8; v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16); return v.toString(16)
}) })
.replace(/-/g, ""); .replace(/-/g, '')
}; }
const popoverBoxId = `ID${uuid()}`; const popoverBoxId = `ID${uuid()}`
const popoverContentId = `ID${uuid()}`; const popoverContentId = `ID${uuid()}`
const instance = getCurrentInstance(); const instance = getCurrentInstance()
const data = reactive({ const data = reactive({
popoverShow: false, popoverShow: false,
defaultStyle: {}, defaultStyle: {},
showStyle: { showStyle: {
left: 0, left: 0,
right: "", right: '',
transform: "", transform: '',
}, },
iconStyle: { iconStyle: {
left: "", left: '',
right: "", right: '',
transform: "", transform: '',
}, },
}); })
/** /**
* @name 获取DOM * @name 获取DOM
*/ */
const getDom = (dom) => { const getDom = (dom) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(instance); const query = uni.createSelectorQuery().in(instance)
let select = query.select(dom); let select = query.select(dom)
const boundingClientRect = select.boundingClientRect((data) => { const boundingClientRect = select.boundingClientRect((data) => {
resolve(data); resolve(data)
}); })
boundingClientRect.exec(); boundingClientRect.exec()
}); })
}; }
const itemClick = (item) => { const itemClick = (item) => {
emits("clickMenu", item); emits('clickMenu', item)
}; }
// 5 // 5
let pressDownTime = 0; let pressDownTime = 0
let time = null; let time = null
const onTouchstart = () => { const onTouchstart = () => {
time && clearTimeout(time); time && clearTimeout(time)
time = setTimeout(open, 500); time = setTimeout(open, 500)
}; }
const onTouchend = () => { const onTouchend = () => {
time && clearTimeout(time); time && clearTimeout(time)
}; }
const open = async () => { const open = async () => {
let popoverContent = await getDom(`#${popoverContentId}`); let popoverContent = await getDom(`#${popoverContentId}`)
let popoverBox = await getDom(`#${popoverBoxId}`); let popoverBox = await getDom(`#${popoverBoxId}`)
// //
let originX = popoverBox.width / 2; let originX = popoverBox.width / 2
// //
let isTop = popoverBox.top - 50 > popoverContent.height; let isTop = popoverBox.top - 50 > popoverContent.height
// //
data.defaultStyle = { data.defaultStyle = {
top: isTop ? "60rpx" : "auto", top: isTop ? '60rpx' : 'auto',
bottom: !isTop ? "-20rpx" : "auto", bottom: !isTop ? '-20rpx' : 'auto',
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(.8)`, transform: `translateY(${isTop ? '-100%' : '100%'}) scale(.8)`,
}; }
// //
if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) { if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) {
data.defaultStyle.right = 0; data.defaultStyle.right = 0
// //
data.defaultStyle["transform-origin"] = `${ data.defaultStyle['transform-origin'] = `${
popoverContent.width - originX popoverContent.width - originX
}px ${isTop ? "100%" : "0%"}`; }px ${isTop ? '100%' : '0%'}`
} else { } else {
data.defaultStyle.left = 0; data.defaultStyle.left = 0
// //
data.defaultStyle["transform-origin"] = `${originX}px ${ data.defaultStyle['transform-origin'] = `${originX}px ${
isTop ? "100%" : "0%" isTop ? '100%' : '0%'
}`; }`
} }
data.showStyle = { ...data.defaultStyle }; data.showStyle = { ...data.defaultStyle }
// icon // icon
let iconDefsultStyle = { let iconDefsultStyle = {
transform: `translate(0%, ${isTop ? "20%" : "-20%"})`, transform: `translate(0%, ${isTop ? '20%' : '-20%'})`,
"border-top-color": isTop ? "#333333" : "", 'border-top-color': isTop ? '#333333' : '',
"border-bottom-color": !isTop ? "#333333" : "", 'border-bottom-color': !isTop ? '#333333' : '',
top: !isTop ? "-20rpx" : "auto", top: !isTop ? '-20rpx' : 'auto',
bottom: isTop ? "-20rpx" : "auto", bottom: isTop ? '-20rpx' : 'auto',
}; }
setTimeout(() => { setTimeout(() => {
if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) { if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) {
@ -224,55 +237,55 @@ const open = async () => {
...data.defaultStyle, ...data.defaultStyle,
// //
opacity: 1, opacity: 1,
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(1)`, transform: `translateY(${isTop ? '-100%' : '100%'}) scale(1)`,
"pointer-events": "auto", 'pointer-events': 'auto',
}; }
data.iconStyle = { data.iconStyle = {
right: `${originX}px`, right: `${originX}px`,
left: "auto", left: 'auto',
...iconDefsultStyle, ...iconDefsultStyle,
}; }
} else { } else {
data.showStyle = { data.showStyle = {
// //
...data.defaultStyle, ...data.defaultStyle,
// //
opacity: 1, opacity: 1,
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(1)`, transform: `translateY(${isTop ? '-100%' : '100%'}) scale(1)`,
"pointer-events": "auto", 'pointer-events': 'auto',
}; }
data.iconStyle = { data.iconStyle = {
left: `${originX}px`, left: `${originX}px`,
right: "auto", right: 'auto',
...iconDefsultStyle, ...iconDefsultStyle,
}; }
} }
if (!data.popoverShow) data.popoverShow = true; if (!data.popoverShow) data.popoverShow = true
}, 200); }, 200)
}; }
const close = (time) => { const close = (time) => {
setTimeout(() => { setTimeout(() => {
data.popoverShow = false; data.popoverShow = false
data.showStyle = data.defaultStyle; data.showStyle = data.defaultStyle
}, time || 0); }, time || 0)
}; }
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (data.popoverShow = false) { if ((data.popoverShow = false)) {
return false return false
} }
if (bubbleRef.value && !bubbleRef.value.contains(event.target)) { if (bubbleRef.value && !bubbleRef.value.contains(event.target)) {
close(); close()
} }
}; }
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside)
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside); document.removeEventListener('click', handleClickOutside)
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -196,6 +196,7 @@
@clickMenu="(menuType) => onContextMenu(menuType, item)" @clickMenu="(menuType) => onContextMenu(menuType, item)"
:isShowCopy="isShowCopy(item)" :isShowCopy="isShowCopy(item)"
:isShowWithdraw="isRevoke(talkParams.uid, item) || isLeader" :isShowWithdraw="isRevoke(talkParams.uid, item) || isLeader"
:isShowConvertText="isShowConvertText(item)"
> >
<component <component
class="component-content" class="component-content"
@ -218,6 +219,11 @@
</div> --> </div> -->
</div> </div>
<div style="display: flex; justify-content: flex-end;">
<div class="talk-tools voice-content" v-if="item.voiceContent">
<span>{{ item.voiceContent }}</span>
</div>
</div>
<div <div
class="talk-tools have_read_num" class="talk-tools have_read_num"
@ -226,7 +232,11 @@
> >
<span v-if="talkParams.type === 1">未读</span> <span v-if="talkParams.type === 1">未读</span>
<span v-if="talkParams.type === 2"> <span v-if="talkParams.type === 2">
已读 (0/{{ (Number(talkParams.num)-1) > 0 ? Number(talkParams.num)-1 : 0 }}) 已读 (0/{{
Number(talkParams.num) - 1 > 0
? Number(talkParams.num) - 1
: 0
}})
</span> </span>
</div> </div>
@ -714,7 +724,10 @@ import {
ServeTalkRecords, ServeTalkRecords,
ServeReadConditionList, ServeReadConditionList,
ServeMessageReadDetail, ServeMessageReadDetail,
uploadImg,
ServeConvertText,
} from '@/api/chat' } from '@/api/chat'
import { uniqueId } from '@/utils'
import copy07 from '@/static/image/chatList/copy07@2x.png' import copy07 from '@/static/image/chatList/copy07@2x.png'
import multipleChoices from '@/static/image/chatList/multipleChoices@2x.png' import multipleChoices from '@/static/image/chatList/multipleChoices@2x.png'
import cite from '@/static/image/chatList/cite@2x.png' import cite from '@/static/image/chatList/cite@2x.png'
@ -857,7 +870,13 @@ watch(
readNumElement.textContent = readNum > 0 ? '已读' : '未读' readNumElement.textContent = readNum > 0 ? '已读' : '未读'
} else { } else {
readNumElement.textContent = readNumElement.textContent =
'已读 (' + readNum + '/' + (Number(talkParams.num) - 1 > 0 ? Number(talkParams.num) - 1 : 0) + ')' '已读 (' +
readNum +
'/' +
(Number(talkParams.num) - 1 > 0
? Number(talkParams.num) - 1
: 0) +
')'
} }
} }
} }
@ -1159,6 +1178,16 @@ const isShowCopy = (item) => {
} }
} }
//
const isShowConvertText = (item) => {
switch (item.msg_type) {
case 4:
return true
default:
return false
}
}
const selectedMessage = computed(() => { const selectedMessage = computed(() => {
return virtualList.value.filter((item) => item.isCheck) return virtualList.value.filter((item) => item.isCheck)
}) })
@ -1379,7 +1408,9 @@ const onContextMenu = (menuType, item) => {
case 'actionDelete': case 'actionDelete':
actionDelete(item) actionDelete(item)
break break
case 'convertText':
convertText(item)
break
default: default:
break break
} }
@ -2559,12 +2590,20 @@ const getMessageReadDetail = (isUnread) => {
if (code == 200) { if (code == 200) {
if (Number(isUnread) === 0) { if (Number(isUnread) === 0) {
state.value.msgReadDetailTabs[0].title = state.value.msgReadDetailTabs[0].title =
'未读 ' + (Number(talkParams.num) - 1 - data.count > 0 ? Number(talkParams.num) - 1 - data.count : 0) + '' '未读 ' +
(Number(talkParams.num) - 1 - data.count > 0
? Number(talkParams.num) - 1 - data.count
: 0) +
''
state.value.msgReadDetailTabs[1].title = '已读 ' + data.count + '' state.value.msgReadDetailTabs[1].title = '已读 ' + data.count + ''
} else if (Number(isUnread) === 1) { } else if (Number(isUnread) === 1) {
state.value.msgReadDetailTabs[0].title = '未读 ' + data.count + '' state.value.msgReadDetailTabs[0].title = '未读 ' + data.count + ''
state.value.msgReadDetailTabs[1].title = state.value.msgReadDetailTabs[1].title =
'已读 ' + (Number(talkParams.num) - 1 - data.count > 0 ? Number(talkParams.num) - 1 - data.count : 0) + '' '已读 ' +
(Number(talkParams.num) - 1 - data.count > 0
? Number(talkParams.num) - 1 - data.count
: 0) +
''
} }
if (state.value.readNumPage === 1) { if (state.value.readNumPage === 1) {
state.value.msgReadOrNotDetail = data.data state.value.msgReadOrNotDetail = data.data
@ -2591,6 +2630,74 @@ const loadMoreReadDetails = () => {
state.value.readNumPage += 1 state.value.readNumPage += 1
getMessageReadDetail(state.value.currentIsUnread) getMessageReadDetail(state.value.currentIsUnread)
} }
const onProgressFn = (progress, id) => {
// console.log((progress.loaded / progress.total) * 100, 'progress')
}
//
const endRecord = (file, url, duration) => {
console.log(file, url, duration)
const form = new FormData()
form.append('file', file)
form.append('source', 'fonchain-chat')
form.append('type', 'file')
let randomId = uniqueId()
const resp = uploadImg(form, (e) => onProgressFn(e, randomId))
// console.log(resp)
resp.then(({ code, data }) => {
// console.log(code, data)
if (code === 0) {
const mediaUrl = data.ori_url
sendMediaMessage(mediaUrl, duration, file.size)
} else {
}
})
resp.catch(() => {})
}
//
const sendMediaMessage = (mediaUrl, duration, size) => {
// console.log(mediaUrl, 'mediaUrl')
let message = {
type: 'voice', //
content: '',
quote_id: '',
receiver: {
receiver_id: talkParams.receiver_id, //idid
talk_type: talkParams.type, //12
},
url: mediaUrl,
duration: duration,
size: size,
}
console.log(message, 'message')
onSendMessage(
message,
(result) => {
console.log(result, 'result')
},
true,
)
}
//
const convertText = (msgItem) => {
const resp = ServeConvertText({
voiceUrl: msgItem.extra.url,
msgId: msgItem.msg_id,
})
// console.log(resp, 'resp')
resp.then(({ code, data }) => {
// console.log(code, data, 'data')
if (code === 200) {
console.log(data.convText, 'convText')
msgItem.voiceContent = data.convText
}
})
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.dialog-page { .dialog-page {
@ -2780,6 +2887,19 @@ const loadMoreReadDetails = () => {
} }
} }
.voice-content {
text-align: right;
color: #7a58de;
font-size: 22rpx;
font-weight: 400;
line-height: 34rpx;
margin: 5rpx 0 0;
background-color: #f5f5f5;
padding: 10rpx;
border-radius: 10rpx;
width: fit-content;
}
.have_read_num { .have_read_num {
text-align: right; text-align: right;
color: #7a58de; color: #7a58de;
@ -2899,7 +3019,7 @@ const loadMoreReadDetails = () => {
:deep(.round-3) { :deep(.round-3) {
max-height: 320rpx; max-height: 320rpx;
margin: 0 0 0 16rpx!important; margin: 0 0 0 16rpx !important;
overflow-y: scroll; overflow-y: scroll;
} }
} }