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

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

View File

@ -196,6 +196,7 @@
@clickMenu="(menuType) => onContextMenu(menuType, item)"
:isShowCopy="isShowCopy(item)"
:isShowWithdraw="isRevoke(talkParams.uid, item) || isLeader"
:isShowConvertText="isShowConvertText(item)"
>
<component
class="component-content"
@ -218,6 +219,11 @@
</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
class="talk-tools have_read_num"
@ -226,7 +232,11 @@
>
<span v-if="talkParams.type === 1">未读</span>
<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>
</div>
@ -714,7 +724,10 @@ import {
ServeTalkRecords,
ServeReadConditionList,
ServeMessageReadDetail,
uploadImg,
ServeConvertText,
} from '@/api/chat'
import { uniqueId } from '@/utils'
import copy07 from '@/static/image/chatList/copy07@2x.png'
import multipleChoices from '@/static/image/chatList/multipleChoices@2x.png'
import cite from '@/static/image/chatList/cite@2x.png'
@ -857,7 +870,13 @@ watch(
readNumElement.textContent = readNum > 0 ? '已读' : '未读'
} else {
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(() => {
return virtualList.value.filter((item) => item.isCheck)
})
@ -1379,7 +1408,9 @@ const onContextMenu = (menuType, item) => {
case 'actionDelete':
actionDelete(item)
break
case 'convertText':
convertText(item)
break
default:
break
}
@ -2559,12 +2590,20 @@ const getMessageReadDetail = (isUnread) => {
if (code == 200) {
if (Number(isUnread) === 0) {
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 + ''
} else if (Number(isUnread) === 1) {
state.value.msgReadDetailTabs[0].title = '未读 ' + data.count + ''
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) {
state.value.msgReadOrNotDetail = data.data
@ -2591,6 +2630,74 @@ const loadMoreReadDetails = () => {
state.value.readNumPage += 1
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>
<style scoped lang="less">
.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 {
text-align: right;
color: #7a58de;