feat(vioce): 添加语音输入功能并优化相关设置
- 新增 VoiceInputSettings 组件用于语音输入设置 - 在 SettingsPanel 中集成语音输入设置选项 - 修改 ChatInterface 以支持语音输入功能 - 优化 WebSocketClient 发送数据逻辑 - 更新相关组件以适应新的语音输入功能
This commit is contained in:
parent
15514759e2
commit
d56ee78dc3
|
@ -0,0 +1,77 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
### 技术栈
|
||||||
|
- Javascript
|
||||||
|
- Vue3
|
||||||
|
- vite
|
||||||
|
- axios
|
||||||
|
- element
|
||||||
|
- pinia
|
||||||
|
- mockjs
|
||||||
|
- sass
|
||||||
|
|
||||||
|
### 文件目录
|
||||||
|
|
||||||
|
- src/
|
||||||
|
- assets/ # 静态资源文件
|
||||||
|
- components/ # 公共组件
|
||||||
|
- views/ # 页面视图组件
|
||||||
|
- router/ # 路由配置
|
||||||
|
- store/ # Pinia状态管理
|
||||||
|
- api/ # API接口封装
|
||||||
|
- utils/ # 工具函数
|
||||||
|
- styles/ # 全局样式
|
||||||
|
- mock/ # Mock数据
|
||||||
|
- App.vue # 根组件
|
||||||
|
- main.js # 入口文件
|
||||||
|
|
||||||
|
### 开发规范
|
||||||
|
|
||||||
|
1. 命名规范
|
||||||
|
- 组件名:大驼峰命名法,如 `UserProfile.vue`
|
||||||
|
- 变量名:小驼峰命名法,如 `userInfo`
|
||||||
|
- 常量名:全大写,下划线分隔,如 `API_BASE_URL`
|
||||||
|
- 文件夹名:小写,中划线分隔,如 `user-center`
|
||||||
|
|
||||||
|
2. 代码风格
|
||||||
|
- 使用 ESLint + Prettier 进行代码格式化
|
||||||
|
- 组件内使用 `<script setup>` 语法
|
||||||
|
- 使用 TypeScript 进行类型检查
|
||||||
|
- 遵循 Vue3 组合式 API 规范
|
||||||
|
|
||||||
|
3. 组件开发
|
||||||
|
- 组件必须包含 name 属性
|
||||||
|
- Props 必须定义类型和默认值
|
||||||
|
- 使用 defineEmits 定义事件
|
||||||
|
- 复杂组件需要添加注释说明
|
||||||
|
|
||||||
|
4. 状态管理
|
||||||
|
- 使用 Pinia 进行状态管理
|
||||||
|
- Store 按功能模块划分
|
||||||
|
- 避免在组件中直接修改 store 状态
|
||||||
|
|
||||||
|
5. 路由规范
|
||||||
|
- 路由配置统一在 router 目录下管理
|
||||||
|
- 使用路由懒加载
|
||||||
|
- 路由命名遵循小驼峰命名法
|
||||||
|
|
||||||
|
6. API 规范
|
||||||
|
- API 请求统一在 api 目录下管理
|
||||||
|
- 使用 axios 进行请求封装
|
||||||
|
- 统一错误处理
|
||||||
|
- 请求参数和响应数据需要定义类型
|
||||||
|
|
||||||
|
7. 样式规范
|
||||||
|
- 使用 SCSS 预处理器
|
||||||
|
- 遵循 BEM 命名规范
|
||||||
|
- 全局样式统一在 styles 目录下管理
|
||||||
|
- 组件样式使用 scoped
|
||||||
|
|
||||||
|
8. 提交规范
|
||||||
|
- 遵循 Conventional Commits 规范
|
||||||
|
- 提交信息必须清晰描述改动内容
|
||||||
|
|
||||||
|
- 禁止提交大文件
|
|
@ -5,6 +5,8 @@
|
||||||
<ThemeSettings />
|
<ThemeSettings />
|
||||||
<el-divider />
|
<el-divider />
|
||||||
<InterfaceSettings />
|
<InterfaceSettings />
|
||||||
|
<el-divider />
|
||||||
|
<VoiceInputSettings />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -13,6 +15,7 @@ import { ElDivider } from 'element-plus';
|
||||||
import DisplayModeSettings from './DisplayModeSettings.vue';
|
import DisplayModeSettings from './DisplayModeSettings.vue';
|
||||||
import ThemeSettings from './ThemeSettings.vue';
|
import ThemeSettings from './ThemeSettings.vue';
|
||||||
import InterfaceSettings from './InterfaceSettings.vue';
|
import InterfaceSettings from './InterfaceSettings.vue';
|
||||||
|
import VoiceInputSettings from './VoiceInputSettings.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="voice-input-settings">
|
||||||
|
<h4>语音输入设置</h4>
|
||||||
|
<div class="settings-item">
|
||||||
|
<span class="settings-label">语音转文字后直接发送</span>
|
||||||
|
<el-switch
|
||||||
|
v-model="voiceInputSettings.sendDirectly"
|
||||||
|
@change="updateSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useSettingsStore } from '@/store/settings'
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
const voiceInputSettings = computed({
|
||||||
|
get: () => settingsStore.voiceInputSettings,
|
||||||
|
set: (value) => settingsStore.updateVoiceInputSettings(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateSettings = () => {
|
||||||
|
settingsStore.updateVoiceInputSettings(voiceInputSettings.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.voice-input-settings {
|
||||||
|
.settings-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -31,7 +31,7 @@ const chatModes = {
|
||||||
**车牌号**: 闽C12345
|
**车牌号**: 闽C12345
|
||||||
**车辆信息**:拥有一辆行驶4年的大众途观L,购买价格约25万,主要用于商务出行及家庭使用。
|
**车辆信息**:拥有一辆行驶4年的大众途观L,购买价格约25万,主要用于商务出行及家庭使用。
|
||||||
**投保信息**:在其他保司购买交强险及商业险(三者险、车损险),还有1个月到期。有过一次轻微追尾事故,已通过保司处理。王先生对成本控制非常敏感,但也能意识到保险的重要性。他希望得到保障全面、价格合理的产品和优质的服务。`,
|
**投保信息**:在其他保司购买交强险及商业险(三者险、车损险),还有1个月到期。有过一次轻微追尾事故,已通过保司处理。王先生对成本控制非常敏感,但也能意识到保险的重要性。他希望得到保障全面、价格合理的产品和优质的服务。`,
|
||||||
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价中的各类异议开展对练,着重训练处理“报价中异议”的能力。按照“保全保足”原则给出险种推荐方案。',
|
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价中的各类异议开展对练,着重训练处理"报价中异议"的能力。按照"保全保足"原则给出险种推荐方案。',
|
||||||
},
|
},
|
||||||
post_quote_objection: {
|
post_quote_objection: {
|
||||||
name: '报价后异议',
|
name: '报价后异议',
|
||||||
|
@ -45,7 +45,7 @@ const chatModes = {
|
||||||
**车牌号**: 粤B56789
|
**车牌号**: 粤B56789
|
||||||
**车辆信息**:拥有一辆行驶4年多的本田思域,1.5T,CVT燃动版,购买价格约15万,用于日常通勤及周末短途出行。
|
**车辆信息**:拥有一辆行驶4年多的本田思域,1.5T,CVT燃动版,购买价格约15万,用于日常通勤及周末短途出行。
|
||||||
**投保信息**:在其他保司购买交强险及商业险(三者险、车损险、不计免赔险、车上人员责任险),还有2个月到期。无事故记录,仅有两次违章停车记录。张女士注重性价比和服务质量,希望保险的保障全面、价格合理、服务贴心。`,
|
**投保信息**:在其他保司购买交强险及商业险(三者险、车损险、不计免赔险、车上人员责任险),还有2个月到期。无事故记录,仅有两次违章停车记录。张女士注重性价比和服务质量,希望保险的保障全面、价格合理、服务贴心。`,
|
||||||
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理“报价后异议”的能力。按照“保全保足”原则给出险种推荐方案。',
|
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理"报价后异议"的能力。按照"保全保足"原则给出险种推荐方案。',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 返回示例
|
// 返回示例
|
||||||
|
|
|
@ -29,12 +29,20 @@ export const useSettingsStore = defineStore('settings', {
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
// 动画效果开关
|
// 动画效果开关
|
||||||
animationsEnabled: true,
|
animationsEnabled: true,
|
||||||
|
// 语音输入设置
|
||||||
|
voiceInputSettings: {
|
||||||
|
sendDirectly: true, // 语音转文字后直接发送
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
persist: {
|
persist: {
|
||||||
key: 'settings',
|
enabled: true,
|
||||||
// Persist displayMode and theme
|
strategies: [
|
||||||
paths: ['sidebarCollapsed', 'displayMode', 'theme', 'animationsEnabled']
|
{
|
||||||
|
key: 'settings',
|
||||||
|
storage: localStorage,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -109,7 +117,15 @@ export const useSettingsStore = defineStore('settings', {
|
||||||
applyDisplayMode(this.displayMode);
|
applyDisplayMode(this.displayMode);
|
||||||
this.toggleAnimations(this.animationsEnabled); // Fallback
|
this.toggleAnimations(this.animationsEnabled); // Fallback
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// 更新语音输入设置
|
||||||
|
updateVoiceInputSettings(settings) {
|
||||||
|
this.voiceInputSettings = {
|
||||||
|
...this.voiceInputSettings,
|
||||||
|
...settings
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -67,10 +67,27 @@ class WebSocketClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof data === 'object') {
|
// 如果是ArrayBuffer或TypedArray,直接发送二进制
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
this.socket.send(data);
|
||||||
|
} else if (
|
||||||
|
data instanceof Int8Array ||
|
||||||
|
data instanceof Uint8Array ||
|
||||||
|
data instanceof Int16Array ||
|
||||||
|
data instanceof Uint16Array ||
|
||||||
|
data instanceof Int32Array ||
|
||||||
|
data instanceof Uint32Array ||
|
||||||
|
data instanceof Float32Array ||
|
||||||
|
data instanceof Float64Array
|
||||||
|
) {
|
||||||
|
this.socket.send(data.buffer);
|
||||||
|
} else if (typeof data === 'object') {
|
||||||
|
// 仅普通对象才转JSON
|
||||||
data = JSON.stringify(data);
|
data = JSON.stringify(data);
|
||||||
|
this.socket.send(data);
|
||||||
|
} else {
|
||||||
|
this.socket.send(data);
|
||||||
}
|
}
|
||||||
this.socket.send(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('发送数据失败:', error);
|
console.error('发送数据失败:', error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,9 +58,11 @@
|
||||||
<!-- <div v-if="message.createdAt" class="message-timestamp">
|
<!-- <div v-if="message.createdAt" class="message-timestamp">
|
||||||
{{ formatTimestamp(message.createdAtTimestamp) }}
|
{{ formatTimestamp(message.createdAtTimestamp) }}
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="message-text" v-html="message.kehu"></div>
|
<div class="message-text">
|
||||||
<div v-if="message.respondingType === 'kehu'" class="typing-indicator">
|
<span v-html="message.kehu"></span>
|
||||||
<span></span><span></span><span></span>
|
<span v-if="message.respondingType === 'kehu'" class="typing-indicator">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,7 +92,8 @@
|
||||||
<VoiceInput
|
<VoiceInput
|
||||||
v-else
|
v-else
|
||||||
:is-disabled="isInputDisabled"
|
:is-disabled="isInputDisabled"
|
||||||
@sendText="handleSendMessage"
|
@sendText="handleVoiceSendText"
|
||||||
|
@fillInput="handleVoiceFillInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 总结弹窗 -->
|
<!-- 总结弹窗 -->
|
||||||
|
@ -110,13 +113,12 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick, watch, computed, defineOptions } from 'vue'
|
import { ref, onMounted, onUnmounted, nextTick, watch, computed, defineOptions } from 'vue'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import * as echarts from 'echarts'
|
|
||||||
import { useChatStore } from '@/store/chat'
|
import { useChatStore } from '@/store/chat'
|
||||||
import ChatHeader from '@/views/chat/components/ChatHeader.vue'
|
import ChatHeader from '@/views/chat/components/ChatHeader.vue'
|
||||||
import ChatInput from '@/views/chat/components/ChatInput.vue'
|
import ChatInput from '@/views/chat/components/ChatInput.vue'
|
||||||
import VoiceInput from '@/views/chat/components/VoiceInput.vue'
|
import VoiceInput from '@/views/chat/components/VoiceInput.vue'
|
||||||
import { ElTag, ElButton, ElInput, ElAvatar, ElIcon } from 'element-plus'
|
import { ElTag, ElButton, ElAvatar, ElIcon } from 'element-plus'
|
||||||
import { Plus, CaretBottom, CaretRight, Microphone, ChatDotSquare } from '@element-plus/icons-vue'
|
import { CaretBottom, CaretRight, Microphone, ChatDotSquare } from '@element-plus/icons-vue'
|
||||||
import userAvatarUrl from '@/assets/user.png';
|
import userAvatarUrl from '@/assets/user.png';
|
||||||
import customAvatarUrl from '@/assets/custom.png';
|
import customAvatarUrl from '@/assets/custom.png';
|
||||||
|
|
||||||
|
@ -125,25 +127,24 @@ defineOptions({
|
||||||
})
|
})
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
const messageInput = ref('')
|
|
||||||
const messageInputRef = ref(null)
|
const messageInputRef = ref(null)
|
||||||
const chatInputRef = ref(null)
|
const chatInputRef = ref(null)
|
||||||
const chatContainer = ref(null)
|
const chatContainer = ref(null)
|
||||||
const scoreChart = ref(null)
|
|
||||||
const maxRows = 6
|
const maxRows = 6
|
||||||
const isUserScrolling = ref(false)
|
const isUserScrolling = ref(false)
|
||||||
const shouldAutoScroll = ref(true)
|
const shouldAutoScroll = ref(true)
|
||||||
const lastScrollTop = ref(0)
|
const lastScrollTop = ref(0)
|
||||||
const lastScrollHeight = ref(0)
|
const lastScrollHeight = ref(0)
|
||||||
const isSummaryCollapsed = ref(false)
|
const isSummaryCollapsed = ref(false)
|
||||||
const showBackgroundDialog = ref(false)
|
|
||||||
const showBackground = ref(false)
|
const showBackground = ref(false)
|
||||||
const isVoiceInputMode = ref(false)
|
const isVoiceInputMode = ref(false)
|
||||||
|
|
||||||
// 使用计算属性获取当前会话的消息
|
// 计算属性:获取当前会话消息
|
||||||
const currentMessages = computed(() => {
|
const currentMessages = computed(() => {
|
||||||
return chatStore.currentMessages
|
return chatStore.currentMessages
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 计算属性:判断发送按钮是否禁用
|
||||||
const sendButtonsDisabled = computed(() => {
|
const sendButtonsDisabled = computed(() => {
|
||||||
if (!chatStore.currentConversation) return false
|
if (!chatStore.currentConversation) return false
|
||||||
const currentConversation = chatStore.currentConversation
|
const currentConversation = chatStore.currentConversation
|
||||||
|
@ -151,10 +152,10 @@ const sendButtonsDisabled = computed(() => {
|
||||||
return ['finished', 'typing'].includes(currentConversation.conversationStatus)
|
return ['finished', 'typing'].includes(currentConversation.conversationStatus)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取最后一条包含dafen字段的消息
|
// 计算属性:获取最后一条包含评分字段的消息
|
||||||
const lastMessageWithDafen = computed(() => {
|
const lastMessageWithDafen = computed(() => {
|
||||||
if (!chatStore.currentMessages.length) return null
|
if (!chatStore.currentMessages.length) return null
|
||||||
// 从后往前查找第一条包含dafen字段的消息
|
// 从后往前查找第一条包含评分字段的消息
|
||||||
for (let i = chatStore.currentMessages.length - 1; i >= 0; i--) {
|
for (let i = chatStore.currentMessages.length - 1; i >= 0; i--) {
|
||||||
if (chatStore.currentMessages[i].dafen) {
|
if (chatStore.currentMessages[i].dafen) {
|
||||||
return chatStore.currentMessages[i]
|
return chatStore.currentMessages[i]
|
||||||
|
@ -179,15 +180,15 @@ const isNearBottom = (threshold = 100) => {
|
||||||
return container.scrollHeight - container.scrollTop - container.clientHeight < threshold
|
return container.scrollHeight - container.scrollTop - container.clientHeight < threshold
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听滚动事件
|
// 处理滚动事件
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const container = chatContainer.value
|
const container = chatContainer.value
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
// 检测是否是用户主动滚动
|
// 检测用户是否主动滚动
|
||||||
if (Math.abs(container.scrollTop - lastScrollTop.value) > 10) {
|
if (Math.abs(container.scrollTop - lastScrollTop.value) > 10) {
|
||||||
isUserScrolling.value = true
|
isUserScrolling.value = true
|
||||||
// 根据是否接近底部来决定是否恢复自动滚动
|
// 根据是否接近底部决定是否恢复自动滚动
|
||||||
shouldAutoScroll.value = isNearBottom(50)
|
shouldAutoScroll.value = isNearBottom(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,16 +196,16 @@ const handleScroll = () => {
|
||||||
lastScrollHeight.value = container.scrollHeight
|
lastScrollHeight.value = container.scrollHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到底部的方法
|
// 滚动到底部
|
||||||
const scrollToBottom = (force = false) => {
|
const scrollToBottom = (force = false) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const container = chatContainer.value
|
const container = chatContainer.value
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
// 在以下情况下滚动到底部:
|
// 在以下情况滚动到底部:
|
||||||
// 1. 强制滚动
|
// 1. 强制滚动
|
||||||
// 2. 允许自动滚动且没有用户滚动操作
|
// 2. 允许自动滚动且没有用户滚动操作
|
||||||
// 3. 用户已经接近底部
|
// 3. 用户已接近底部
|
||||||
if (force || (shouldAutoScroll.value && !isUserScrolling.value) || isNearBottom(50)) {
|
if (force || (shouldAutoScroll.value && !isUserScrolling.value) || isNearBottom(50)) {
|
||||||
container.scrollTop = container.scrollHeight
|
container.scrollTop = container.scrollHeight
|
||||||
lastScrollTop.value = container.scrollTop
|
lastScrollTop.value = container.scrollTop
|
||||||
|
@ -221,21 +222,21 @@ watch(() => chatStore.messageCache, (newCache, oldCache) => {
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// 监听当前会话的消息
|
// 监听当前会话消息
|
||||||
watch(() => currentMessages.value, (newMessages, oldMessages) => {
|
watch(() => currentMessages.value, (newMessages, oldMessages) => {
|
||||||
if (!oldMessages || newMessages.length > oldMessages.length) {
|
if (!oldMessages || newMessages.length > oldMessages.length) {
|
||||||
scrollToBottom(shouldAutoScroll.value)
|
scrollToBottom(shouldAutoScroll.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查最后一条消息的respondingType
|
// 检查最后一条消息的类型
|
||||||
const lastMessage = newMessages[newMessages.length - 1]
|
const lastMessage = newMessages[newMessages.length - 1]
|
||||||
if (lastMessage && ['kehu', 'pingfen', 'zongjie'].includes(lastMessage.respondingType)) {
|
if (lastMessage && ['kehu', 'pingfen', 'zongjie'].includes(lastMessage.respondingType)) {
|
||||||
scrollToBottom(true) // 特定类型消息强制滚动
|
scrollToBottom(true) // 特定类型消息强制滚动
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// 监听消息的respondingType变化
|
// 监听消息类型变化
|
||||||
watch(() => currentMessages.value?.map(msg => msg.respondingType), (newTypes, oldTypes) => {
|
watch(() => currentMessages.value?.map(msg => msg.respondingType), (newTypes, oldTypes) => {
|
||||||
if (!newTypes || !oldTypes) return
|
if (!newTypes || !oldTypes) return
|
||||||
const lastType = newTypes[newTypes.length - 1]
|
const lastType = newTypes[newTypes.length - 1]
|
||||||
|
@ -244,7 +245,7 @@ watch(() => currentMessages.value?.map(msg => msg.respondingType), (newTypes, ol
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// Unified message sending function
|
// 统一消息发送函数
|
||||||
const handleSendMessage = (message) => {
|
const handleSendMessage = (message) => {
|
||||||
if (!message || !message.trim() || isInputDisabled.value) return;
|
if (!message || !message.trim() || isInputDisabled.value) return;
|
||||||
console.log("ChatInterface sending message:", message);
|
console.log("ChatInterface sending message:", message);
|
||||||
|
@ -252,7 +253,7 @@ const handleSendMessage = (message) => {
|
||||||
shouldAutoScroll.value = true; // 发送消息时恢复自动滚动
|
shouldAutoScroll.value = true; // 发送消息时恢复自动滚动
|
||||||
scrollToBottom(true);
|
scrollToBottom(true);
|
||||||
|
|
||||||
// If coming from voice input, switch back to text mode and focus
|
// 从语音输入切换回文本模式并聚焦
|
||||||
if (isVoiceInputMode.value) {
|
if (isVoiceInputMode.value) {
|
||||||
isVoiceInputMode.value = false;
|
isVoiceInputMode.value = false;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
@ -261,12 +262,40 @@ const handleSendMessage = (message) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理语音直接发送
|
||||||
|
function handleVoiceSendText(text) {
|
||||||
|
if (!text || !text.trim() || isInputDisabled.value) return;
|
||||||
|
chatStore.sendMessage(text);
|
||||||
|
shouldAutoScroll.value = true;
|
||||||
|
scrollToBottom(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理语音转文字填入输入框
|
||||||
|
function handleVoiceFillInput(text) {
|
||||||
|
if (!text || !text.trim()) return;
|
||||||
|
isVoiceInputMode.value = false; // 切换到文本输入
|
||||||
|
nextTick(() => {
|
||||||
|
if (chatInputRef.value) {
|
||||||
|
// 确保输入框存在并设置值
|
||||||
|
chatInputRef.value.setValue?.(text) || (chatInputRef.value.value = text);
|
||||||
|
// 聚焦输入框
|
||||||
|
chatInputRef.value.focus?.();
|
||||||
|
// 触发输入事件以确保值更新
|
||||||
|
const input = chatInputRef.value.$el?.querySelector('textarea');
|
||||||
|
if (input) {
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化聊天内容
|
// 格式化聊天内容
|
||||||
const formatMarkdown = (text, tagType) => {
|
const formatMarkdown = (text, tagType) => {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
return marked.parse(text)
|
return marked.parse(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调整文本区域高度
|
||||||
const adjustTextareaHeight = () => {
|
const adjustTextareaHeight = () => {
|
||||||
const textarea = messageInputRef.value
|
const textarea = messageInputRef.value
|
||||||
textarea.style.height = 'auto'
|
textarea.style.height = 'auto'
|
||||||
|
@ -274,10 +303,12 @@ const adjustTextareaHeight = () => {
|
||||||
textarea.style.height = newHeight + 'px'
|
textarea.style.height = newHeight + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换总结面板
|
||||||
const toggleSummary = () => {
|
const toggleSummary = () => {
|
||||||
isSummaryCollapsed.value = !isSummaryCollapsed.value
|
isSummaryCollapsed.value = !isSummaryCollapsed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化时间戳
|
||||||
const formatTimestamp = (timestamp) => {
|
const formatTimestamp = (timestamp) => {
|
||||||
console.log('%c timestamp:', 'color: #2196F3; font-weight: bold', timestamp)
|
console.log('%c timestamp:', 'color: #2196F3; font-weight: bold', timestamp)
|
||||||
if (!timestamp) return ''
|
if (!timestamp) return ''
|
||||||
|
@ -293,18 +324,22 @@ const formatTimestamp = (timestamp) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算属性:判断输入框是否禁用
|
||||||
const isInputDisabled = computed(() => {
|
const isInputDisabled = computed(() => {
|
||||||
return chatStore.currentConversation?.conversationStatus === 'finished'
|
// 会话结束 或 正在回复/生成时禁用输入
|
||||||
|
const status = chatStore.currentConversation?.conversationStatus
|
||||||
|
return status !== 'active'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 切换背景显示
|
||||||
const toggleBackground = () => {
|
const toggleBackground = () => {
|
||||||
showBackground.value = !showBackground.value
|
showBackground.value = !showBackground.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to switch input mode
|
// 切换输入模式
|
||||||
const switchInputMode = () => {
|
const switchInputMode = () => {
|
||||||
isVoiceInputMode.value = !isVoiceInputMode.value;
|
isVoiceInputMode.value = !isVoiceInputMode.value;
|
||||||
// Focus the text input when switching back
|
// 切换回文本模式时聚焦输入框
|
||||||
if (!isVoiceInputMode.value) {
|
if (!isVoiceInputMode.value) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
chatInputRef.value?.focus();
|
chatInputRef.value?.focus();
|
||||||
|
@ -570,6 +605,7 @@ html.dark .background-section .background-content {
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
min-height: 24px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -850,6 +886,7 @@ html.dark .background-section .background-content {
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -876,4 +913,20 @@ html.dark .background-section .background-content {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 1px;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
||||||
|
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: scale(0.8); }
|
||||||
|
40% { transform: scale(1.2); }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -9,12 +9,12 @@
|
||||||
@keyup.enter.exact.prevent="handleSend"
|
@keyup.enter.exact.prevent="handleSend"
|
||||||
resize="none"
|
resize="none"
|
||||||
ref="messageInputRef"
|
ref="messageInputRef"
|
||||||
:disabled="isInputDisabled"
|
:disabled="isDisabled"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
class="send-btn"
|
class="send-btn"
|
||||||
@click="handleSend"
|
@click="handleSend"
|
||||||
:disabled="isInputDisabled || !messageInput.trim()"
|
:disabled="isDisabled || !messageInput.trim()"
|
||||||
type="primary"
|
type="primary"
|
||||||
>
|
>
|
||||||
发送
|
发送
|
||||||
|
@ -27,7 +27,7 @@ import { ref } from 'vue'
|
||||||
import { ElInput, ElButton } from 'element-plus'
|
import { ElInput, ElButton } from 'element-plus'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isInputDisabled: {
|
isDisabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
|
@ -40,15 +40,25 @@ const messageInputRef = ref(null)
|
||||||
const maxRows = 6
|
const maxRows = 6
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (!messageInput.value.trim() || props.isInputDisabled) return
|
if (!messageInput.value.trim() || props.isDisabled) return
|
||||||
emit('send', messageInput.value)
|
emit('send', messageInput.value)
|
||||||
messageInput.value = ''
|
messageInput.value = ''
|
||||||
messageInputRef.value.style.height = 'auto'
|
// messageInputRef.value.style.height = 'auto'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose focus method
|
// Expose methods
|
||||||
defineExpose({
|
defineExpose({
|
||||||
focus: () => messageInputRef.value?.focus()
|
focus: () => messageInputRef.value?.focus(),
|
||||||
|
setValue: (value) => {
|
||||||
|
if (messageInputRef.value) {
|
||||||
|
messageInput.value = value;
|
||||||
|
// 触发输入事件以确保值更新
|
||||||
|
const input = messageInputRef.value.$el?.querySelector('textarea');
|
||||||
|
if (input) {
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const adjustTextareaHeight = () => {
|
const adjustTextareaHeight = () => {
|
||||||
|
|
|
@ -37,10 +37,10 @@
|
||||||
<span class="modal-btn-label record-label">{{ isRecording ? '完成' : ' ' }}</span>
|
<span class="modal-btn-label record-label">{{ isRecording ? '完成' : ' ' }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
class="modal-btn confirm-btn"
|
class="modal-btn confirm-btn"
|
||||||
:type="isRecording ? 'danger' : 'primary'"
|
type="primary"
|
||||||
circle
|
circle
|
||||||
@click="confirmVoiceInput"
|
@click="confirmVoiceInput"
|
||||||
:icon="isRecording ? ChatDotSquare : Microphone"
|
:icon="isRecording ? Check : Microphone"
|
||||||
>
|
>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,12 +68,13 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElButton, ElMessage, ElIcon } from 'element-plus'
|
import { ElButton, ElMessage, ElIcon } from 'element-plus'
|
||||||
import { Microphone, ChatDotSquare, Close } from '@element-plus/icons-vue'
|
import { Microphone, Check, Close } from '@element-plus/icons-vue'
|
||||||
import Recorder from 'recorder-core'
|
import Recorder from 'recorder-core'
|
||||||
import 'recorder-core/src/engine/pcm'
|
import 'recorder-core/src/engine/pcm'
|
||||||
import 'recorder-core/src/engine/wav'
|
import 'recorder-core/src/engine/wav'
|
||||||
import WebSocketClient from '@/utils/websocket'
|
import WebSocketClient from '@/utils/websocket'
|
||||||
import AudioWaveform from '@/components/AudioWaveform.vue'
|
import AudioWaveform from '@/components/AudioWaveform.vue'
|
||||||
|
import { useSettingsStore } from '@/store/settings'
|
||||||
|
|
||||||
// Props definition
|
// Props definition
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -84,7 +85,10 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
// Emits definition
|
// Emits definition
|
||||||
const emit = defineEmits(['sendText', 'sendVoice'])
|
const emit = defineEmits(['sendText', 'fillInput'])
|
||||||
|
|
||||||
|
// 获取设置
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
// Refs related to voice input
|
// Refs related to voice input
|
||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
|
@ -97,10 +101,12 @@ let recorder = null
|
||||||
let websocketClient = null
|
let websocketClient = null
|
||||||
let timer = null
|
let timer = null
|
||||||
let audioChunks = []
|
let audioChunks = []
|
||||||
|
let sampleBuf = new Int16Array(0)
|
||||||
|
const chunk_size = 960 // 16kHz下60ms一帧
|
||||||
|
|
||||||
// WebSocket configuration
|
// WebSocket configuration
|
||||||
const wsConfig = {
|
const wsConfig = {
|
||||||
url: 'wss://160.202.224.52:10096/',
|
url: 'wss://fire.lexcubia.com:31003/ws',
|
||||||
initConfig: {
|
initConfig: {
|
||||||
mode: '2pass',
|
mode: '2pass',
|
||||||
wav_format: 'pcm',
|
wav_format: 'pcm',
|
||||||
|
@ -147,34 +153,42 @@ const openVoiceModal = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeVoiceModal = () => {
|
const closeVoiceModal = () => {
|
||||||
showVoiceModal.value = false
|
// 1. 关闭WebSocket连接
|
||||||
|
if (websocketClient) {
|
||||||
|
websocketClient.disconnect()
|
||||||
|
websocketClient = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 停止录音并释放资源
|
||||||
if (isRecording.value) {
|
if (isRecording.value) {
|
||||||
stopRecording()
|
stopRecording()
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
}
|
}
|
||||||
clearInterval(timer)
|
|
||||||
|
|
||||||
// 释放麦克风资源
|
|
||||||
if (recorder) {
|
if (recorder) {
|
||||||
recorder.close(() => {
|
recorder.close(() => {
|
||||||
// 确保所有音频轨道都被停止
|
|
||||||
if (recorder.stream) {
|
if (recorder.stream) {
|
||||||
recorder.stream.getTracks().forEach(track => track.stop())
|
recorder.stream.getTracks().forEach(track => track.stop())
|
||||||
}
|
}
|
||||||
recorder = null
|
recorder = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭WebSocket连接
|
// 3. 清理定时器
|
||||||
if (websocketClient) {
|
if (timer) {
|
||||||
websocketClient.disconnect()
|
clearInterval(timer)
|
||||||
websocketClient = null
|
timer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理音频数据
|
// 4. 清理数据
|
||||||
audioChunks = []
|
audioChunks = []
|
||||||
realTimeText.value = ''
|
realTimeText.value = ''
|
||||||
remainingTime.value = 30
|
remainingTime.value = 30
|
||||||
|
sampleBuf = new Int16Array(0)
|
||||||
|
pcmData.value = []
|
||||||
|
|
||||||
|
// 5. 最后关闭弹窗
|
||||||
|
showVoiceModal.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelVoiceInputFromModal = () => {
|
const cancelVoiceInputFromModal = () => {
|
||||||
|
@ -185,35 +199,32 @@ const confirmVoiceInput = async () => {
|
||||||
if (isRecording.value) {
|
if (isRecording.value) {
|
||||||
stopRecording()
|
stopRecording()
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
clearInterval(timer)
|
|
||||||
|
|
||||||
if (audioChunks.length === 0) {
|
if (realTimeText.value) {
|
||||||
ElMessage.warning('录音时间太短')
|
if (settingsStore.voiceInputSettings.sendDirectly) {
|
||||||
closeVoiceModal()
|
emit('sendText', realTimeText.value)
|
||||||
return
|
} else {
|
||||||
|
emit('fillInput', realTimeText.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('没有识别到文字')
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' })
|
|
||||||
emit('sendVoice', audioBlob)
|
|
||||||
closeVoiceModal()
|
|
||||||
}
|
}
|
||||||
|
closeVoiceModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendAsText = () => {
|
const sendAsText = () => {
|
||||||
if (isRecording.value) {
|
if (isRecording.value) {
|
||||||
stopRecording()
|
stopRecording()
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
clearInterval(timer)
|
|
||||||
|
|
||||||
if (realTimeText.value) {
|
if (realTimeText.value) {
|
||||||
emit('sendText', realTimeText.value)
|
emit('sendText', realTimeText.value)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning('没有识别到文字')
|
ElMessage.warning('没有识别到文字')
|
||||||
}
|
}
|
||||||
closeVoiceModal()
|
|
||||||
} else {
|
|
||||||
closeVoiceModal()
|
|
||||||
}
|
}
|
||||||
|
closeVoiceModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startRecording = (stream) => {
|
const startRecording = (stream) => {
|
||||||
|
@ -223,17 +234,26 @@ const startRecording = (stream) => {
|
||||||
sampleRate: 16000, // 采样率
|
sampleRate: 16000, // 采样率
|
||||||
bitRate: 16, // 比特率
|
bitRate: 16, // 比特率
|
||||||
onProcess: (buffers, powerLevel, duration, sampleRate) => {
|
onProcess: (buffers, powerLevel, duration, sampleRate) => {
|
||||||
const pcm = buffers[buffers.length - 1]
|
const pcm48k = buffers[buffers.length - 1]
|
||||||
if (pcm && pcm.length > 0) {
|
if (pcm48k && pcm48k.length > 0) {
|
||||||
pcmData.value = Array.from(pcm)
|
// 1. 重采样为16kHz
|
||||||
}
|
const array_48k = [pcm48k]
|
||||||
// WebSocket 发送逻辑保留
|
const data_16k = Recorder.SampleData(array_48k, sampleRate, 16000).data
|
||||||
if (websocketClient && websocketClient.socket.readyState === WebSocket.OPEN) {
|
// 2. 拼接到sampleBuf
|
||||||
const pcmDataSend = buffers[buffers.length - 1]
|
const merged = new Int16Array(sampleBuf.length + data_16k.length)
|
||||||
websocketClient.send({
|
merged.set(sampleBuf)
|
||||||
is_speaking: true,
|
merged.set(data_16k, sampleBuf.length)
|
||||||
audio_data: pcmDataSend
|
sampleBuf = merged
|
||||||
})
|
// 3. 分片发送
|
||||||
|
while(sampleBuf.length >= chunk_size) {
|
||||||
|
const sendBuf = sampleBuf.slice(0, chunk_size)
|
||||||
|
sampleBuf = sampleBuf.slice(chunk_size)
|
||||||
|
if (websocketClient && websocketClient.socket.readyState === WebSocket.OPEN) {
|
||||||
|
websocketClient.send(sendBuf.buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 4. 波形显示
|
||||||
|
pcmData.value = Array.from(data_16k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -249,7 +269,10 @@ const startRecording = (stream) => {
|
||||||
const stopRecording = () => {
|
const stopRecording = () => {
|
||||||
if (recorder) {
|
if (recorder) {
|
||||||
recorder.stop((blob, duration) => {
|
recorder.stop((blob, duration) => {
|
||||||
// Convert PCM to WAV before saving
|
if (duration < 500) { // 小于0.5秒
|
||||||
|
ElMessage.warning('录音时间太短')
|
||||||
|
return
|
||||||
|
}
|
||||||
Recorder.pcm2wav({
|
Recorder.pcm2wav({
|
||||||
blob: blob,
|
blob: blob,
|
||||||
sampleRate: 16000,
|
sampleRate: 16000,
|
||||||
|
@ -494,11 +517,6 @@ onUnmounted(() => {
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dark mode styles for VoiceInput elements
|
|
||||||
html.dark .voice-bubble {
|
|
||||||
// Adjust bubble color for dark mode if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .modal-btn-label {
|
html.dark .modal-btn-label {
|
||||||
color: #707070;
|
color: #707070;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="settings-panel">
|
||||||
|
<!-- 其他设置项 -->
|
||||||
|
|
||||||
|
<!-- 语音输入设置 -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="section-title">语音输入设置</h3>
|
||||||
|
<div class="settings-item">
|
||||||
|
<span class="settings-label">语音转文字后直接发送</span>
|
||||||
|
<el-switch
|
||||||
|
v-model="voiceInputSettings.sendDirectly"
|
||||||
|
@change="updateVoiceInputSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 其他设置项 -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useSettingsStore } from '@/store/settings'
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
// 语音输入设置
|
||||||
|
const voiceInputSettings = computed({
|
||||||
|
get: () => settingsStore.voiceInputSettings,
|
||||||
|
set: (value) => settingsStore.updateVoiceInputSettings(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ... 其他代码 ...
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.settings-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
93
yarn.lock
93
yarn.lock
|
@ -1342,11 +1342,34 @@ browserslist@^4.23.1, browserslist@^4.24.0, browserslist@^4.24.4:
|
||||||
node-releases "^2.0.19"
|
node-releases "^2.0.19"
|
||||||
update-browserslist-db "^1.1.1"
|
update-browserslist-db "^1.1.1"
|
||||||
|
|
||||||
|
buffer-alloc-unsafe@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
||||||
|
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
|
||||||
|
|
||||||
|
buffer-alloc@^1.1.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.npmmirror.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
|
||||||
|
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
|
||||||
|
dependencies:
|
||||||
|
buffer-alloc-unsafe "^1.1.0"
|
||||||
|
buffer-fill "^1.0.0"
|
||||||
|
|
||||||
buffer-builder@^0.2.0:
|
buffer-builder@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.npmmirror.com/buffer-builder/-/buffer-builder-0.2.0.tgz#3322cd307d8296dab1f604618593b261a3fade8f"
|
resolved "https://registry.npmmirror.com/buffer-builder/-/buffer-builder-0.2.0.tgz#3322cd307d8296dab1f604618593b261a3fade8f"
|
||||||
integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==
|
integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==
|
||||||
|
|
||||||
|
buffer-fill@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
||||||
|
integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==
|
||||||
|
|
||||||
|
buffer-from@^1.0.0:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||||
|
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||||
|
|
||||||
bundle-name@^4.1.0:
|
bundle-name@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
|
resolved "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
|
||||||
|
@ -1448,6 +1471,11 @@ copy-anything@^3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-what "^4.1.8"
|
is-what "^4.1.8"
|
||||||
|
|
||||||
|
core-util-is@~1.0.0:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
|
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||||
|
|
||||||
cross-spawn@^7.0.3:
|
cross-spawn@^7.0.3:
|
||||||
version "7.0.6"
|
version "7.0.6"
|
||||||
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||||
|
@ -1503,6 +1531,13 @@ de-indent@^1.0.2:
|
||||||
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||||
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
||||||
|
|
||||||
|
debug@2, debug@^2.2.0:
|
||||||
|
version "2.6.9"
|
||||||
|
resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||||
|
dependencies:
|
||||||
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0:
|
debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
resolved "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||||
|
@ -1930,6 +1965,11 @@ immutable@^5.0.2:
|
||||||
resolved "https://registry.npmmirror.com/immutable/-/immutable-5.1.1.tgz#d4cb552686f34b076b3dcf23c4384c04424d8354"
|
resolved "https://registry.npmmirror.com/immutable/-/immutable-5.1.1.tgz#d4cb552686f34b076b3dcf23c4384c04424d8354"
|
||||||
integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==
|
integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==
|
||||||
|
|
||||||
|
inherits@~2.0.1:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
is-core-module@^2.16.0:
|
is-core-module@^2.16.0:
|
||||||
version "2.16.1"
|
version "2.16.1"
|
||||||
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
|
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
|
||||||
|
@ -1993,6 +2033,11 @@ is-wsl@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-inside-container "^1.0.0"
|
is-inside-container "^1.0.0"
|
||||||
|
|
||||||
|
isarray@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||||
|
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
|
||||||
|
|
||||||
isexe@^2.0.0:
|
isexe@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||||
|
@ -2159,6 +2204,11 @@ mrmime@^2.0.0:
|
||||||
resolved "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc"
|
resolved "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc"
|
||||||
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
|
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||||
|
|
||||||
ms@^2.1.3:
|
ms@^2.1.3:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
|
@ -2273,6 +2323,11 @@ pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3:
|
||||||
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
||||||
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
||||||
|
|
||||||
|
pcm@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/pcm/-/pcm-1.0.3.tgz#8e9508ab321f93ca61cd4ab44cc846530a185f24"
|
||||||
|
integrity sha512-N/qULxlCd++KuA4gAi9+wG9HpPmSOHowYj6KumrGsaoDVrcswut6K/GomqoFcbVXSnAnAf2SevVn1ehTQgFdHA==
|
||||||
|
|
||||||
perfect-debounce@^1.0.0:
|
perfect-debounce@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
resolved "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
||||||
|
@ -2658,11 +2713,26 @@ read-cache@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pify "^2.3.0"
|
pify "^2.3.0"
|
||||||
|
|
||||||
|
readable-stream@^1.1.14:
|
||||||
|
version "1.1.14"
|
||||||
|
resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||||
|
integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==
|
||||||
|
dependencies:
|
||||||
|
core-util-is "~1.0.0"
|
||||||
|
inherits "~2.0.1"
|
||||||
|
isarray "0.0.1"
|
||||||
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readdirp@^4.0.1:
|
readdirp@^4.0.1:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
||||||
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
|
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
|
||||||
|
|
||||||
|
recorder-core@^1.3.25011100:
|
||||||
|
version "1.3.25011100"
|
||||||
|
resolved "https://registry.npmmirror.com/recorder-core/-/recorder-core-1.3.25011100.tgz#128ee11f2d24547b82605a5383274226b3a20a65"
|
||||||
|
integrity sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA==
|
||||||
|
|
||||||
resolve@^1.1.7:
|
resolve@^1.1.7:
|
||||||
version "1.22.10"
|
version "1.22.10"
|
||||||
resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
|
resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
|
||||||
|
@ -2937,6 +3007,18 @@ std-env@^3.8.1:
|
||||||
resolved "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
|
resolved "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
|
||||||
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
||||||
|
|
||||||
|
stream-parser@^0.3.1:
|
||||||
|
version "0.3.1"
|
||||||
|
resolved "https://registry.npmmirror.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
|
||||||
|
integrity sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==
|
||||||
|
dependencies:
|
||||||
|
debug "2"
|
||||||
|
|
||||||
|
string_decoder@~0.10.x:
|
||||||
|
version "0.10.31"
|
||||||
|
resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||||
|
integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
|
||||||
|
|
||||||
strip-final-newline@^4.0.0:
|
strip-final-newline@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
|
resolved "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
|
||||||
|
@ -3224,6 +3306,17 @@ vue@^3.5.13:
|
||||||
"@vue/server-renderer" "3.5.13"
|
"@vue/server-renderer" "3.5.13"
|
||||||
"@vue/shared" "3.5.13"
|
"@vue/shared" "3.5.13"
|
||||||
|
|
||||||
|
wav@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/wav/-/wav-1.0.2.tgz#bdbf3fa0d9b4519e9dfd2f603299ead0a2f22060"
|
||||||
|
integrity sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==
|
||||||
|
dependencies:
|
||||||
|
buffer-alloc "^1.1.0"
|
||||||
|
buffer-from "^1.0.0"
|
||||||
|
debug "^2.2.0"
|
||||||
|
readable-stream "^1.1.14"
|
||||||
|
stream-parser "^0.3.1"
|
||||||
|
|
||||||
webpack-virtual-modules@^0.6.2:
|
webpack-virtual-modules@^0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
||||||
|
|
Loading…
Reference in New Issue