Files
guoxue-mp/pages/chat/select.vue
T
2026-03-25 15:59:24 +08:00

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>