mirror of
http://112.124.100.131/ebiz-ai/ebiz-base-ai.git
synced 2025-12-09 19:06:50 +08:00
feat(AI): 添加语音识别功能
- 新增 audioToText API 用于语音识别 - 实现录音和停止录音功能 - 添加语音识别结果处理逻辑 -优化聊天输入框,支持语音输入模式切换 - 调整聊天界面布局,增加录音相关UI
This commit is contained in:
@@ -36,3 +36,14 @@ export function chat(data) {
|
|||||||
|
|
||||||
return getUrl('/hasl/chat')
|
return getUrl('/hasl/chat')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function audioToText(data) {
|
||||||
|
//聊天获取产品百宝箱
|
||||||
|
// 聊天框输入内容以“百宝箱”结尾时,调用这个接口
|
||||||
|
return request({
|
||||||
|
url: getUrl('/chat/audio-to-text'),
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,9 @@
|
|||||||
<messageComponent :messagesList="messages" :is-deep="isDeep" :is-search="isSearching" :think-ok='isThink' @setProductName='setProductName'></messageComponent>
|
<messageComponent :messagesList="messages" :is-deep="isDeep" :is-search="isSearching" :think-ok='isThink' @setProductName='setProductName'></messageComponent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 滚动到顶部按钮 -->
|
||||||
|
<div class="button-container" style="background: #fff">
|
||||||
|
<van-icon name="upgrade" size="2em" @click="scrollToTop" />
|
||||||
<!-- 按钮点击滚动到顶-->
|
|
||||||
<div class="button-container" style='background: #fff'>
|
|
||||||
<van-icon name="upgrade" size='2em' @click='scrollToTop' />
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
@@ -30,8 +27,48 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
<footer class="chat-footer">
|
<footer class="chat-footer">
|
||||||
<input type="text" v-model="newMessage" placeholder="请简短描述您的问题" @keyup.enter="sendMessage" />
|
<!-- 输入框 or 按住说话提示 -->
|
||||||
<button @click="sendMessage" :disabled="messageStatus === 'send'" :class="{ disabled: messageStatus === 'send' }">发送</button>
|
<div class="input-wrapper">
|
||||||
|
<input
|
||||||
|
v-if="!isVoiceMode"
|
||||||
|
type="text"
|
||||||
|
v-model="newMessage"
|
||||||
|
placeholder="请简短描述您的问题"
|
||||||
|
@keyup.enter="sendMessage"
|
||||||
|
/>
|
||||||
|
<div v-else class="voice-hint-container"
|
||||||
|
@mousedown="startRecording"
|
||||||
|
@selectstart="() => false"
|
||||||
|
@mouseup="stopRecording"
|
||||||
|
@mouseleave="stopRecording"
|
||||||
|
@touchend="stopRecording"
|
||||||
|
@touchstart="startRecording">
|
||||||
|
<div class="waveform" :class="{ active: isRecording }" >
|
||||||
|
<span class="bar"></span>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<span class="bar"></span>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="hint-text" v-if='!isRecording'>按住说话</div>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 语音按钮:按住说话 -->
|
||||||
|
<button
|
||||||
|
@click='isVoiceMode = !isVoiceMode'
|
||||||
|
class="mic-button ml10 mr10"
|
||||||
|
>
|
||||||
|
<svg-icon v-if='!isVoiceMode' icon-class="voice" class-name="chat-icon" style='width: 20px;height: 20px'></svg-icon>
|
||||||
|
<span v-else style='font-size: 18px;color:#707070' class='ml5 mr5'>⌨</span>
|
||||||
|
</button>
|
||||||
|
<!-- 发送按钮 -->
|
||||||
|
<button
|
||||||
|
@click="sendMessage"
|
||||||
|
:disabled="messageStatus === 'send'"
|
||||||
|
:class="{ disabled: messageStatus === 'send' }"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,7 +78,7 @@ import { Icon } from 'vant'
|
|||||||
import messageComponent from './components/message.vue'
|
import messageComponent from './components/message.vue'
|
||||||
import SvgIcon from '@/components/svg-icon/index.vue'
|
import SvgIcon from '@/components/svg-icon/index.vue'
|
||||||
import HotProducts from '@/views/AI/components/HotProducts.vue'
|
import HotProducts from '@/views/AI/components/HotProducts.vue'
|
||||||
import { chat, chatProduct } from '@/api/generatedApi'
|
import { chat, chatProduct,audioToText } from '@/api/generatedApi'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -50,7 +87,6 @@ export default {
|
|||||||
messageComponent,
|
messageComponent,
|
||||||
HotProducts,
|
HotProducts,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
productName:'',
|
productName:'',
|
||||||
@@ -67,9 +103,15 @@ export default {
|
|||||||
isDeep: false,
|
isDeep: false,
|
||||||
autoScrollEnabled: true,
|
autoScrollEnabled: true,
|
||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
|
// 录音相关状态
|
||||||
|
isRecording: false,
|
||||||
|
mediaRecorder: null,
|
||||||
|
audioChunks: [],
|
||||||
|
isRecognizing: false,
|
||||||
|
// 语音模式开关
|
||||||
|
isVoiceMode: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
deepInternet() {
|
deepInternet() {
|
||||||
this.isDeep = !this.isDeep
|
this.isDeep = !this.isDeep
|
||||||
@@ -82,38 +124,29 @@ export default {
|
|||||||
this.conversationId = ''
|
this.conversationId = ''
|
||||||
this.productName = ''
|
this.productName = ''
|
||||||
},
|
},
|
||||||
|
|
||||||
hasTreasureBox() {
|
hasTreasureBox() {
|
||||||
chatProduct({ query: this.newMessage }).then((res) => {
|
chatProduct({ query: this.newMessage }).then((res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
this.messageStatus = 'stop'
|
this.messageStatus = 'stop'
|
||||||
this.messages.push({ type: 'box', text: this.newMessage,detail:res.content })
|
this.messages.push({ type: 'box', text: this.newMessage, detail: res.content })
|
||||||
this.newMessage = ''
|
this.newMessage = ''
|
||||||
}
|
}
|
||||||
}).catch(()=>{
|
}).catch(() => {
|
||||||
this.messageStatus = 'stop'
|
this.messageStatus = 'stop'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
sendMessage() {
|
sendMessage() {
|
||||||
if(this.messageStatus === 'send'){
|
if (this.messageStatus === 'send') return
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.newMessage.trim() === '') return
|
if (this.newMessage.trim() === '') return
|
||||||
|
|
||||||
this.messages.push({ type: 'user', text: this.newMessage })
|
this.messages.push({ type: 'user', text: this.newMessage })
|
||||||
this.messageStatus = 'send'
|
this.messageStatus = 'send'
|
||||||
|
|
||||||
if (this.newMessage.includes('工具箱')) {
|
if (this.newMessage.includes('工具箱')) {
|
||||||
this.hasTreasureBox()
|
this.hasTreasureBox()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.autoScrollEnabled = true
|
this.autoScrollEnabled = true
|
||||||
this.axiosGetAiChat()
|
this.axiosGetAiChat()
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
throttle(fn, delay = 50) {
|
throttle(fn, delay = 50) {
|
||||||
let lastCall = 0
|
let lastCall = 0
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
@@ -124,15 +157,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
scrollToTop() {
|
||||||
scrollToTop(){
|
|
||||||
const messageArea = this.$refs.messageArea
|
const messageArea = this.$refs.messageArea
|
||||||
messageArea.scrollTop = 0
|
if (messageArea) {
|
||||||
|
messageArea.scrollTop = 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
scrollToBottom() {
|
||||||
scrollToBottom: function () {
|
|
||||||
if (!this.autoScrollEnabled) return
|
if (!this.autoScrollEnabled) return
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const messageArea = this.$refs.messageArea
|
const messageArea = this.$refs.messageArea
|
||||||
if (messageArea) {
|
if (messageArea) {
|
||||||
@@ -140,22 +172,81 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setProductName(e){
|
setProductName(e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
},
|
},
|
||||||
handleScroll() {
|
handleScroll() {
|
||||||
const messageArea = this.$refs.messageArea
|
const messageArea = this.$refs.messageArea
|
||||||
if (!messageArea) return
|
if (!messageArea) return
|
||||||
|
|
||||||
const threshold = 10
|
const threshold = 10
|
||||||
const isAtBottom = messageArea.scrollHeight - messageArea.clientHeight <= messageArea.scrollTop + threshold
|
const isAtBottom =
|
||||||
|
messageArea.scrollHeight - messageArea.clientHeight <=
|
||||||
|
messageArea.scrollTop + threshold
|
||||||
this.autoScrollEnabled = isAtBottom
|
this.autoScrollEnabled = isAtBottom
|
||||||
this.scrollPosition = messageArea.scrollTop
|
this.scrollPosition = messageArea.scrollTop
|
||||||
},
|
},
|
||||||
|
async startRecording() {
|
||||||
|
if (this.isRecognizing) return
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
|
this.mediaRecorder = new MediaRecorder(stream)
|
||||||
|
this.audioChunks = []
|
||||||
|
this.mediaRecorder.ondataavailable = event => {
|
||||||
|
if (event.data.size > 0) {
|
||||||
|
this.audioChunks.push(event.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mediaRecorder.onstop = () => {
|
||||||
|
this.handleStopRecording()
|
||||||
|
}
|
||||||
|
this.mediaRecorder.start()
|
||||||
|
this.isRecording = true
|
||||||
|
} catch (err) {
|
||||||
|
alert('无法访问麦克风,请检查权限')
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopRecording() {
|
||||||
|
if (this.mediaRecorder && this.isRecording) {
|
||||||
|
this.mediaRecorder.stop()
|
||||||
|
this.isRecording = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleStopRecording() {
|
||||||
|
this.isRecognizing = true
|
||||||
|
const blob = new Blob(this.audioChunks, { type: 'audio/webm' })
|
||||||
|
try {
|
||||||
|
const text = await this.callVoiceRecognitionAPI(blob)
|
||||||
|
console.log(text)
|
||||||
|
if (text) {
|
||||||
|
this.newMessage = text
|
||||||
|
this.messageStatus = 'stop'
|
||||||
|
this.sendMessage()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('语音识别失败:', error)
|
||||||
|
this.newMessage = ''
|
||||||
|
} finally {
|
||||||
|
this.isRecognizing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
callVoiceRecognitionAPI(blob) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', blob);
|
||||||
|
formData.append('appType', 'haslBigHelper');
|
||||||
|
formData.append('user', 'chenyuda');
|
||||||
|
audioToText(formData).then(res => {
|
||||||
|
if(res){
|
||||||
|
resolve(res.content)
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
axiosGetAiChat() {
|
axiosGetAiChat() {
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
|
||||||
this.currentMessage = JSON.parse(
|
this.currentMessage = JSON.parse(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'bot',
|
type: 'bot',
|
||||||
@@ -168,55 +259,46 @@ export default {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.messages.push(this.currentMessage)
|
this.messages.push(this.currentMessage)
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
query: this.newMessage,
|
query: this.newMessage,
|
||||||
isDeep: this.isDeep ? 1 : 0,
|
isDeep: this.isDeep ? 1 : 0,
|
||||||
isOnline: this.isSearching ? 1 : 0,
|
isOnline: this.isSearching ? 1 : 0,
|
||||||
user: 'chenyuda',
|
user: 'chenyuda',
|
||||||
conversationId: this.conversationId,
|
conversationId: this.conversationId,
|
||||||
productName:this.productName,
|
productName: this.productName,
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(chat(), {
|
fetch(chat(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
body: JSON.stringify(params),
|
body: JSON.stringify(params),
|
||||||
})
|
})
|
||||||
.then(async (res) =>{
|
.then(async (res) => {
|
||||||
this.newMessage = ''
|
this.newMessage = ''
|
||||||
await this.processStreamResponse(res)
|
await this.processStreamResponse(res)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.messageStatus = 'stop'
|
this.messageStatus = 'stop'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async processStreamResponse(response) {
|
async processStreamResponse(response) {
|
||||||
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`)
|
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`)
|
||||||
|
|
||||||
if (!response.body) {
|
if (!response.body) {
|
||||||
console.error('响应体不存在:', response)
|
console.error('响应体不存在:', response)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
if (done) break
|
if (done) break
|
||||||
|
|
||||||
buffer += new TextDecoder().decode(value)
|
buffer += new TextDecoder().decode(value)
|
||||||
const lines = buffer.split('\n')
|
const lines = buffer.split('\n')
|
||||||
|
|
||||||
lines.slice(0, -1).forEach((line) => {
|
lines.slice(0, -1).forEach((line) => {
|
||||||
const parsed = this.parseStreamLine(line)
|
const parsed = this.parseStreamLine(line)
|
||||||
if (parsed) this.updateMessageContent(parsed)
|
if (parsed) this.updateMessageContent(parsed)
|
||||||
})
|
})
|
||||||
|
|
||||||
buffer = lines[lines.length - 1] || ''
|
buffer = lines[lines.length - 1] || ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取流数据时发生错误:', error)
|
console.error('读取流数据时发生错误:', error)
|
||||||
@@ -224,17 +306,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseStreamLine(line) {
|
parseStreamLine(line) {
|
||||||
try {
|
try {
|
||||||
const cleanLine = line.replace(/^data:\s*/, '')
|
const cleanLine = line.replace(/^data:\s*/, '')
|
||||||
if (!cleanLine) return null
|
if (!cleanLine) return null
|
||||||
const data = JSON.parse(cleanLine)
|
const data = JSON.parse(cleanLine)
|
||||||
if(data.answer){
|
if (data.answer) {
|
||||||
this.answerMap+=data.answer
|
this.answerMap += data.answer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.updateConversationState(data)
|
this.updateConversationState(data)
|
||||||
return data
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -243,48 +322,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateConversationState(data) {
|
updateConversationState(data) {
|
||||||
this.conversationId = data.conversation_id || conversationId.value
|
this.conversationId = data.conversation_id || this.conversationId
|
||||||
if (data.answer && data.answer.indexOf('<think>') !== -1) {
|
if (data.answer && data.answer.indexOf('withTo') !== -1) {
|
||||||
this.isThink = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.answer && data.answer.indexOf('</think>') !== -1) {
|
|
||||||
this.isThink = false
|
this.isThink = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateMessageContent({ answer, event }) {
|
updateMessageContent({ answer, event }) {
|
||||||
if(event === 'message_end'){
|
if (event === 'message_end') {
|
||||||
this.messageStatus = 'stop'
|
this.messageStatus = 'stop'
|
||||||
}
|
}
|
||||||
if (!this.currentMessage || !answer) return;
|
if (!this.currentMessage || !answer) return
|
||||||
|
const mode = this.isThink ? 'think' : 'text'
|
||||||
const mode = this.isThink ? 'think' : 'text';
|
|
||||||
|
|
||||||
this.currentMessage[mode] += answer
|
this.currentMessage[mode] += answer
|
||||||
return
|
},
|
||||||
// 清除之前的动画任务
|
|
||||||
if (this.timer) {
|
|
||||||
cancelAnimationFrame(this.timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderChar = () => {
|
|
||||||
|
|
||||||
|
|
||||||
if (this.answerIndex < this.answerMap.length) {
|
|
||||||
this.currentMessage[mode] += this.answerMap[this.answerIndex++];
|
|
||||||
// this.$nextTick(() => {
|
|
||||||
this.timer = requestAnimationFrame(renderChar);
|
|
||||||
// });
|
|
||||||
} else {
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.timer = requestAnimationFrame(renderChar);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
messages: {
|
messages: {
|
||||||
handler() {
|
handler() {
|
||||||
@@ -305,6 +356,10 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
.chat-main {
|
.chat-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -312,12 +367,14 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
position: relative;
|
position: relative;
|
||||||
.button-container{
|
|
||||||
|
.button-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 120px;
|
bottom: 120px;
|
||||||
right:10px;
|
right: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-content {
|
.chat-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.message-area {
|
.message-area {
|
||||||
@@ -358,13 +415,88 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
input {
|
.input-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px;
|
|
||||||
border: none;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-hint-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
&:active {
|
||||||
|
background-color: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveform {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
//height: 30px;
|
||||||
|
//width: 60px;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
&.active .bar {
|
||||||
|
animation: wave-animation 1s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
width: 4px;
|
||||||
|
background-color: $primary-color;
|
||||||
|
margin: 0 1px;
|
||||||
|
|
||||||
|
&:nth-child(1) { height: 10px; animation-delay: 0s; }
|
||||||
|
&:nth-child(2) { height: 16px; animation-delay: 0.1s; }
|
||||||
|
&:nth-child(3) { height: 12px; animation-delay: 0.2s; }
|
||||||
|
&:nth-child(4) { height: 18px; animation-delay: 0.3s; }
|
||||||
|
&:nth-child(5) { height: 14px; animation-delay: 0.4s; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
margin-top: 4px;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mic-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: $primary-color;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: #e6454a; /* 按下变红 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -376,11 +508,20 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
@keyframes wave-animation {
|
||||||
pointer-events: none;
|
0%, 100% {
|
||||||
opacity: 0.5;
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scaleY(1.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user