580 lines
15 KiB
Vue
580 lines
15 KiB
Vue
<template>
|
|
<view class="select-page">
|
|
<view class="select-header" :style="{ paddingTop: statusBarPadding }">
|
|
<u-icon name="arrow-left" size="32" color="#1f2048" @click="goBack"></u-icon>
|
|
<text class="header-title">我是玄衡</text>
|
|
</view>
|
|
|
|
<view class="intro-card">
|
|
<image class="intro-avatar" src="/static/logo.jpg"
|
|
mode="aspectFill"></image>
|
|
<view class="flex flex-col justify-start items-start">
|
|
<text class="intro-name">我是玄衡</text>
|
|
<text class="intro-desc">知五行 破内卷 助你关键决策</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="phrase-list">
|
|
<view class="phrase" :style="{color: selectedPhrase === item ? '#1b2a75' : '#9a9fc3'}"
|
|
v-for="(item, index) in phrases" :key="index" @click="selectPhrase(item)">{{
|
|
item
|
|
}}
|
|
</view>
|
|
</view>
|
|
|
|
<!-- <view class="mentor-choose">-->
|
|
<!-- <view class="mentor-item" :class="{active: currentMentor === mentor.roleName}" v-for="mentor in mentors"-->
|
|
<!-- :key="mentor.id" @click="currentMentor = mentor.roleName">-->
|
|
<!-- <image class="mentor-img" :src="mentor.avatar" mode="aspectFit"></image>-->
|
|
<!-- <text class="mentor-text">{{ mentor.roleName }}</text>-->
|
|
<!-- </view>-->
|
|
<!-- </view>-->
|
|
|
|
<!-- <button class="go-btn" @click="goChat">去聊天</button>-->
|
|
<Login ref="Login" @done="getMessageList"/>
|
|
<view class="chat-input" :style="{bottom: `${inputBottom}px`}">
|
|
<view class="m-40 flex justify-between items-center w-100p">
|
|
<view class="ml-15">
|
|
<u-icon :name="voiceMode ? '/static/icon/键盘.png' : '/static/icon/话筒.png'" size="60rpx"
|
|
@click="toggleVoiceMode"/>
|
|
</view>
|
|
<view class="input-wrap" v-if="!voiceMode">
|
|
<textarea
|
|
:adjust-position="false"
|
|
class="textarea"
|
|
v-model="content"
|
|
placeholder="迷茫困惑的话和我聊聊吧"
|
|
maxlength="-1"
|
|
placeholder-style="text-align:center;"
|
|
auto-height
|
|
fixed
|
|
:show-confirm-bar="false"
|
|
/>
|
|
</view>
|
|
<view class="record-wrap" v-else>
|
|
<view
|
|
class="record-btn"
|
|
@longpress="startRecord"
|
|
@touchend="stopRecord"
|
|
@touchcancel="stopRecord"
|
|
>
|
|
{{ recording ? '录音中...' : recordBtnText }}
|
|
</view>
|
|
</view>
|
|
<button class="send-btn" @click="goChat">发送</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import {addChatListReq, chatListReq, listChatRoleReq, makeTransReq, voiceGetResReq} from "@/api/chat";
|
|
import Login from "@/components/Login.vue";
|
|
import {getUserInfo} from "@/util/user";
|
|
import {dictDataReq} from "@/api/common";
|
|
import {MAIN_API} from "@/config";
|
|
import {coinAddReq} from "@/api/coin";
|
|
|
|
export default {
|
|
components: {Login},
|
|
data() {
|
|
return {
|
|
statusBarPadding: '40rpx',
|
|
phrases: ['你好', '今天天气怎么样', '聊聊山海经', '聊聊论语'],
|
|
mentors: [],
|
|
currentMentor: '',
|
|
selectedPhrase: '你好',
|
|
messageList: [],
|
|
content: '',
|
|
inputBottom: 0,
|
|
toChat: false,
|
|
voiceMode: false,
|
|
recording: false,
|
|
recordBtnText: '按住说话',
|
|
recorder: null,
|
|
voicePollingTimer: null,
|
|
maxVoicePolling: 15,
|
|
voicePlayingIndex: null,
|
|
audioCtx: null,
|
|
voiceType: 'default',
|
|
}
|
|
},
|
|
onLoad() {
|
|
// this.inputBottom = uni.getSystemInfoSync().safeAreaInsets.bottom
|
|
const info = uni.getSystemInfoSync()
|
|
this.statusBarPadding = `${(info.statusBarHeight || 20) + 10}px`
|
|
this.getWelcomeWords()
|
|
// if (!getUserInfo().token) this.$refs.Login.open()
|
|
// else this.getMessageList()
|
|
if (getUserInfo().token) this.getMessageList()
|
|
uni.removeStorageSync('forceNoJump')
|
|
uni.onKeyboardHeightChange(res => {
|
|
setTimeout(() => {
|
|
console.log('onKeyboardHeightChange', res.height)
|
|
this.inputBottom = res.height
|
|
}, 100)
|
|
})
|
|
},
|
|
methods: {
|
|
onKeyboardChange(e) {
|
|
this.inputBottom = e.detail.height
|
|
},
|
|
async getWelcomeWords() {
|
|
const {data} = await dictDataReq({
|
|
dictId: 167
|
|
})
|
|
this.phrases = data.map(item => item.dictDataCode)
|
|
this.selectedPhrase = this.phrases[0]
|
|
this.content = this.phrases[0]
|
|
},
|
|
goBack() {
|
|
uni.setStorageSync('forceNoJump', 1)
|
|
uni.switchTab({url: '/pages/index/index'})
|
|
},
|
|
async getRoleList() {
|
|
const {data} = await listChatRoleReq()
|
|
this.mentors = data
|
|
this.currentMentor = data[0].roleName
|
|
},
|
|
toggleVoiceMode() {
|
|
uni.authorize({
|
|
scope: 'scope.record',
|
|
success() {
|
|
console.log('授权成功')
|
|
},
|
|
fail() {
|
|
console.log('授权失败')
|
|
},
|
|
complete(e) {
|
|
console.log('授权完成', e)
|
|
}
|
|
})
|
|
if (this.voiceMode) {
|
|
if (this.recorder && this.recording) {
|
|
this.recorder.stop()
|
|
}
|
|
this.recording = false
|
|
this.voiceMode = false
|
|
this.recordBtnText = '按住说话'
|
|
return
|
|
}
|
|
this.voiceMode = true
|
|
this.recordBtnText = '按住说话'
|
|
typeof uni.hideKeyboard === 'function' && uni.hideKeyboard()
|
|
this.initRecorder()
|
|
},
|
|
async goChat() {
|
|
if (!getUserInfo().token) return this.$refs.Login.open()
|
|
const mentor = this.mentors.find(item => item.name === this.currentMentor)
|
|
let url = `/pages/user/setting?word=${this.content}`
|
|
if (mentor) {
|
|
url += `&roleName=${mentor.name}&roleDesc=${mentor.desc}&avatar=${mentor.avatar}`
|
|
const {data} = await addChatListReq({
|
|
roleName: mentor.name,
|
|
roleDesc: mentor.desc,
|
|
roleId: mentor.id
|
|
})
|
|
url += `&chatId=${data.id}`
|
|
}
|
|
uni.navigateTo({url})
|
|
},
|
|
async getMessageList() {
|
|
const res = await chatListReq()
|
|
this.messageList = res.data
|
|
if (this.messageList.length > 0)
|
|
await this.getRoleList()
|
|
},
|
|
selectPhrase(item) {
|
|
this.selectedPhrase = item
|
|
this.content = item
|
|
},
|
|
initRecorder() {
|
|
if (this.recorder) return
|
|
this.recorder = uni.getRecorderManager()
|
|
this.recorder.onStart(() => {
|
|
this.recording = true
|
|
this.recordBtnText = '录音中...'
|
|
})
|
|
this.recorder.onStop((res) => {
|
|
this.recording = false
|
|
const path = res && res.tempFilePath
|
|
if (path) this.processVoiceFile(path)
|
|
else this.recordBtnText = '按住说话'
|
|
})
|
|
this.recorder.onError(() => {
|
|
this.recording = false
|
|
this.recordBtnText = '按住说话'
|
|
uni.showToast({title: '录音失败', icon: 'none'})
|
|
})
|
|
},
|
|
startRecord() {
|
|
if (!this.voiceMode) return
|
|
this.initRecorder()
|
|
if (this.recording) return
|
|
this.recording = true
|
|
this.recordBtnText = '录音中...'
|
|
try {
|
|
this.recorder.start({
|
|
duration: 60000,
|
|
format: 'mp3',
|
|
sampleRate: 16000,
|
|
numberOfChannels: 1,
|
|
encodeBitRate: 48000
|
|
})
|
|
} catch (e) {
|
|
this.recording = false
|
|
this.recordBtnText = '按住说话'
|
|
uni.showToast({title: '无法开始录音', icon: 'none'})
|
|
}
|
|
},
|
|
stopRecord() {
|
|
if (!this.voiceMode || !this.recording || !this.recorder) return
|
|
this.recording = false
|
|
this.recorder.stop()
|
|
},
|
|
async processVoiceFile(filePath) {
|
|
this.recordBtnText = '处理中...'
|
|
uni.showLoading({title: '识别中...'})
|
|
try {
|
|
const {url} = await this.uploadVoiceFile(filePath)
|
|
if (!url) throw new Error('no file')
|
|
// const file = 'https://guoxue.gxwebsoft.com/api/file/20251202/97e5e082becb44e88ece0e9cbd050dca.mp3'
|
|
// const {data: taskId} = await makeTransReq({file})
|
|
const {data: taskId} = await makeTransReq({file: url})
|
|
if (taskId) {
|
|
this.startVoicePolling(taskId)
|
|
} else {
|
|
throw new Error('no task')
|
|
}
|
|
} catch (err) {
|
|
uni.hideLoading()
|
|
uni.showToast({title: '语音识别失败', icon: 'none'})
|
|
this.recordBtnText = '按住说话'
|
|
}
|
|
},
|
|
uploadVoiceFile(filePath) {
|
|
const {token} = getUserInfo()
|
|
return new Promise((resolve, reject) => {
|
|
uni.uploadFile({
|
|
url: `${MAIN_API}/api/file/upload`,
|
|
name: 'file',
|
|
filePath,
|
|
header: {
|
|
Authorization: token,
|
|
TenantId: 10543
|
|
},
|
|
success: (res) => {
|
|
try {
|
|
const result = JSON.parse(res.data)
|
|
const url = result?.data?.url || result?.data
|
|
resolve({url})
|
|
} catch (e) {
|
|
reject(e)
|
|
}
|
|
},
|
|
fail: reject
|
|
})
|
|
})
|
|
},
|
|
startVoicePolling(taskId) {
|
|
this.clearVoicePolling()
|
|
let attempt = 0
|
|
this.voicePollingTimer = setInterval(async () => {
|
|
attempt += 1
|
|
try {
|
|
const {data} = await voiceGetResReq(taskId)
|
|
const result = typeof data === 'string' ? data : (data?.text || '')
|
|
if (result && result !== '__NULL__') {
|
|
this.clearVoicePolling()
|
|
uni.hideLoading()
|
|
this.recordBtnText = '按住说话'
|
|
const wordResult = JSON.parse(data)
|
|
// console.log('wordResult', wordResult)
|
|
if (wordResult.Sentences && wordResult.Sentences.length)
|
|
// this.send(wordResult.Sentences[0].Text)
|
|
this.content = wordResult.Sentences[0].Text
|
|
else uni.showToast({title: '未识别到内容', icon: 'none'})
|
|
} else if (result === '__NULL__') {
|
|
this.clearVoicePolling()
|
|
uni.hideLoading()
|
|
this.recordBtnText = '按住说话'
|
|
uni.showToast({title: '未识别到内容', icon: 'none'})
|
|
} else if (attempt >= this.maxVoicePolling) {
|
|
this.clearVoicePolling()
|
|
uni.hideLoading()
|
|
this.recordBtnText = '按住说话'
|
|
uni.showToast({title: '识别超时', icon: 'none'})
|
|
}
|
|
} catch (err) {
|
|
this.clearVoicePolling()
|
|
uni.hideLoading()
|
|
this.recordBtnText = '按住说话'
|
|
uni.showToast({title: '识别失败', icon: 'none'})
|
|
}
|
|
}, 2000)
|
|
},
|
|
clearVoicePolling() {
|
|
if (this.voicePollingTimer) {
|
|
clearInterval(this.voicePollingTimer)
|
|
this.voicePollingTimer = null
|
|
}
|
|
},
|
|
},
|
|
onUnload() {
|
|
this.clearVoicePolling()
|
|
this.stopAudio()
|
|
this.recordBtnText = '按住说话'
|
|
uni.hideLoading()
|
|
uni.offKeyboardHeightChange()
|
|
},
|
|
onHide() {
|
|
this.clearVoicePolling()
|
|
this.stopAudio()
|
|
this.recordBtnText = '按住说话'
|
|
uni.hideLoading()
|
|
uni.offKeyboardHeightChange()
|
|
},
|
|
onShareTimeline() {
|
|
if (getUserInfo().token) {
|
|
coinAddReq({
|
|
type: '+',
|
|
desc: '分享获得',
|
|
num: 1
|
|
})
|
|
}
|
|
return {
|
|
title: '简幸',
|
|
path: '/pages/index/index'
|
|
}
|
|
},
|
|
onShareAppMessage() {
|
|
if (getUserInfo().token) {
|
|
coinAddReq({
|
|
type: '+',
|
|
desc: '分享获得',
|
|
num: 1
|
|
})
|
|
}
|
|
return {
|
|
title: '简幸',
|
|
path: '/pages/index/index'
|
|
}
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.select-page {
|
|
min-height: 100vh;
|
|
//background: linear-gradient(180deg, #f5f7ff 0%, #ffffff 100%);
|
|
padding: 40rpx 30rpx 80rpx;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.select-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.header-title {
|
|
font-size: 32rpx;
|
|
color: #14164e;
|
|
}
|
|
|
|
.pill {
|
|
background: rgba(255, 255, 255, 0.7);
|
|
padding: 10rpx 28rpx;
|
|
border-radius: 30rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.intro-card {
|
|
margin-top: 200rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
background: #fff;
|
|
border-radius: 30rpx;
|
|
padding: 20rpx;
|
|
//box-shadow: 0 10rpx 20rpx rgba(123, 137, 255, 0.15);
|
|
}
|
|
|
|
.intro-avatar {
|
|
width: 100rpx;
|
|
height: 100rpx;
|
|
border-radius: 20rpx;
|
|
margin-right: 20rpx;
|
|
}
|
|
|
|
.intro-name {
|
|
font-size: 32rpx;
|
|
color: #14164e;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.intro-desc {
|
|
font-size: 26rpx;
|
|
color: #9a9fc3;
|
|
margin-top: 10rpx;
|
|
}
|
|
|
|
.phrase-list {
|
|
margin-top: 30rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.phrase {
|
|
background: rgba(160, 175, 255, 0.2);
|
|
border-radius: 40rpx;
|
|
padding: 20rpx 30rpx;
|
|
color: #26306d;
|
|
font-size: 28rpx;
|
|
display: inline-flex;
|
|
width: auto;
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.mentor-choose {
|
|
margin-top: 40rpx;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.mentor-item {
|
|
width: 48%;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border-radius: 30rpx;
|
|
text-align: center;
|
|
padding: 20rpx;
|
|
box-shadow: 0 12rpx 20rpx rgba(120, 134, 255, 0.15);
|
|
}
|
|
|
|
.mentor-item.active {
|
|
border: 2rpx solid #f0c730;
|
|
}
|
|
|
|
.mentor-img {
|
|
width: 160rpx;
|
|
height: 160rpx;
|
|
}
|
|
|
|
.mentor-text {
|
|
margin-top: 12rpx;
|
|
font-size: 28rpx;
|
|
color: #424577;
|
|
}
|
|
|
|
.go-btn {
|
|
margin-top: 60rpx;
|
|
height: 90rpx;
|
|
line-height: 90rpx;
|
|
border-radius: 45rpx;
|
|
background: #1b2a75;
|
|
color: #fff;
|
|
font-size: 30rpx;
|
|
}
|
|
|
|
|
|
.chat-input {
|
|
width: 100%;
|
|
padding: 40rpx 0;
|
|
//padding: 20rpx 30rpx 40rpx;
|
|
align-items: center;
|
|
gap: 20rpx;
|
|
background: #eee;
|
|
display: flex;
|
|
justify-content: center;
|
|
position: fixed;
|
|
left: 0;
|
|
bottom: 0;
|
|
}
|
|
|
|
.chat-input .input-wrap {
|
|
flex: 1;
|
|
border-radius: 10rpx;
|
|
background: #fff;
|
|
color: #000;
|
|
padding: 15rpx 30rpx;
|
|
margin: 0 10rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
//height: 100rpx;
|
|
min-height: 40rpx;
|
|
}
|
|
|
|
.chat-input .textarea {
|
|
width: 100%;
|
|
//height: 100%;
|
|
background: none !important;
|
|
color: #000 !important;
|
|
text-align: left;
|
|
line-height: 40rpx;
|
|
padding: 0;
|
|
min-height: 35rpx;
|
|
}
|
|
|
|
.send-btn {
|
|
width: 120rpx;
|
|
height: 70rpx;
|
|
border-radius: 20rpx;
|
|
//background: linear-gradient(120deg, #fbc889, #f37a5b);
|
|
background-color: white;
|
|
border: 1px solid white;
|
|
color: #000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 15rpx 0 0;
|
|
font-size: 25rpx;
|
|
}
|
|
|
|
.send-btn:after {
|
|
border: none;
|
|
}
|
|
|
|
|
|
.record-wrap {
|
|
flex: 1;
|
|
border-radius: 10rpx;
|
|
//background: linear-gradient(120deg, #fbc889, #f37a5b);
|
|
background: #fff;
|
|
color: #000;
|
|
padding: 15rpx 30rpx;
|
|
margin: 0 10rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 40rpx;
|
|
}
|
|
|
|
.record-btn {
|
|
width: 100%;
|
|
text-align: center;
|
|
color: #000;
|
|
}
|
|
|
|
.voice-toggle {
|
|
width: 120rpx;
|
|
height: 100rpx;
|
|
border-radius: 999rpx;
|
|
background: #dfe1f2;
|
|
color: #1f2654;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 10rpx;
|
|
border: none;
|
|
}
|
|
|
|
.voice-toggle.active {
|
|
background: #21284f;
|
|
color: #fff;
|
|
}
|
|
|
|
</style>
|