|
@@ -30,24 +30,24 @@
|
|
|
<!-- 内容区域 -->
|
|
<!-- 内容区域 -->
|
|
|
<div class="content-box">
|
|
<div class="content-box">
|
|
|
<!-- 人物形象 -->
|
|
<!-- 人物形象 -->
|
|
|
- <div
|
|
|
|
|
- v-if="currentDialogue && !isInputCardVisible"
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="currentDialogue && (currentDialogue.type === 'digital' || currentDialogue.type === 'quest')"
|
|
|
:key="`character-${currentDialogueIndex}`"
|
|
:key="`character-${currentDialogueIndex}`"
|
|
|
- class="character"
|
|
|
|
|
- :class="{
|
|
|
|
|
- 'left': getCharacterSide(currentDialogue.roleName) === 'left',
|
|
|
|
|
|
|
+ class="character"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'left': getCharacterSide(currentDialogue.roleName) === 'left',
|
|
|
'right': getCharacterSide(currentDialogue.roleName) === 'right'
|
|
'right': getCharacterSide(currentDialogue.roleName) === 'right'
|
|
|
}"
|
|
}"
|
|
|
:style="{ backgroundImage: `url(${getCharacterImage(currentDialogue.roleName)})` }"
|
|
:style="{ backgroundImage: `url(${getCharacterImage(currentDialogue.roleName)})` }"
|
|
|
></div>
|
|
></div>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<!-- 对话卡片 -->
|
|
<!-- 对话卡片 -->
|
|
|
- <div
|
|
|
|
|
- v-if="currentDialogue && activeInputMode !== 'keyboard' && !isInputCardVisible"
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="currentDialogue && (currentDialogue.type === 'digital' || currentDialogue.type === 'quest')"
|
|
|
:key="`dialogue-${currentDialogueIndex}`"
|
|
:key="`dialogue-${currentDialogueIndex}`"
|
|
|
- class="dialogue-card"
|
|
|
|
|
- :class="{
|
|
|
|
|
- 'left': getCharacterSide(currentDialogue.roleName) === 'left',
|
|
|
|
|
|
|
+ class="dialogue-card"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'left': getCharacterSide(currentDialogue.roleName) === 'left',
|
|
|
'right': getCharacterSide(currentDialogue.roleName) === 'right'
|
|
'right': getCharacterSide(currentDialogue.roleName) === 'right'
|
|
|
}"
|
|
}"
|
|
|
>
|
|
>
|
|
@@ -56,68 +56,38 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="dialogue-content" v-html="parseMarkdown(currentDialogue.content)"></div>
|
|
<div class="dialogue-content" v-html="parseMarkdown(currentDialogue.content)"></div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<!-- 用户输入卡片 -->
|
|
<!-- 用户输入卡片 -->
|
|
|
- <div
|
|
|
|
|
- v-if="isInputCardVisible && (activeInputMode === 'keyboard' || activeInputMode === 'voice')"
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="currentDialogue.type == 'user'"
|
|
|
class="dialogue-card user-input-card"
|
|
class="dialogue-card user-input-card"
|
|
|
>
|
|
>
|
|
|
<div class="dialogue-header">
|
|
<div class="dialogue-header">
|
|
|
<span class="role-name">我</span>
|
|
<span class="role-name">我</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="dialogue-content">
|
|
<div class="dialogue-content">
|
|
|
- <!-- 键盘输入内容 -->
|
|
|
|
|
- <template v-if="activeInputMode === 'keyboard'">
|
|
|
|
|
- <textarea
|
|
|
|
|
- v-model="userInput"
|
|
|
|
|
- class="user-input-textarea"
|
|
|
|
|
- placeholder="请输入内容..."
|
|
|
|
|
- @keyup.enter.exact="submitUserInput"
|
|
|
|
|
- ></textarea>
|
|
|
|
|
- <div class="input-actions">
|
|
|
|
|
- <button class="cancel-btn" @click="cancelUserInput">取消</button>
|
|
|
|
|
- <button class="submit-btn" @click="submitUserInput">发送</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 语音输入内容 -->
|
|
|
|
|
- <template v-else-if="activeInputMode === 'voice'">
|
|
|
|
|
- <div class="voice-input-content" v-if="voiceInput">
|
|
|
|
|
- {{ voiceInput }}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="voice-input-placeholder" v-else>
|
|
|
|
|
- 点击麦克风开始语音输入...
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="input-actions">
|
|
|
|
|
- <button class="cancel-btn" @click="cancelUserInput">取消</button>
|
|
|
|
|
- <button class="submit-btn" @click="submitVoiceInput">发送</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ v-model="userInput"
|
|
|
|
|
+ class="user-input-textarea"
|
|
|
|
|
+ placeholder="请输入内容..."
|
|
|
|
|
+ @keyup.enter.exact="submitUserInput"
|
|
|
|
|
+ ></textarea>
|
|
|
|
|
+ <div class="input-actions">
|
|
|
|
|
+ <button class="cancel-btn" @click="cancelUserInput">清空</button>
|
|
|
|
|
+ <button class="submit-btn" @click="submitUserInput">发送</button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<!-- 输入按钮区域 -->
|
|
<!-- 输入按钮区域 -->
|
|
|
- <div class="input-buttons-container">
|
|
|
|
|
|
|
+ <div class="input-buttons-container" v-if="currentDialogue.type == 'user'">
|
|
|
<!-- 语音输入按钮 -->
|
|
<!-- 语音输入按钮 -->
|
|
|
- <div
|
|
|
|
|
- class="voice-input-outer"
|
|
|
|
|
- :class="{ 'active': activeInputMode === 'voice', 'inactive': activeInputMode === 'keyboard' }"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div class="voice-input-outer" :class="{ 'recording': isVoiceRecording }">
|
|
|
<VoiceInput
|
|
<VoiceInput
|
|
|
@voiceRecognized="handleVoiceRecognized"
|
|
@voiceRecognized="handleVoiceRecognized"
|
|
|
@recordingStatusChanged="handleRecordingStatusChanged"
|
|
@recordingStatusChanged="handleRecordingStatusChanged"
|
|
|
- @click="toggleVoiceInput"
|
|
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
- <!-- 键盘输入按钮 -->
|
|
|
|
|
- <div
|
|
|
|
|
- class="keyboard-input-outer"
|
|
|
|
|
- :class="{ 'active': activeInputMode === 'keyboard', 'inactive': activeInputMode === 'voice' }"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="keyboard-btn" @click="toggleKeyboardInput">
|
|
|
|
|
- <el-icon class="keyboard-icon"><Grid /></el-icon>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -160,14 +130,12 @@ const currentSectionIndex = ref(0)
|
|
|
const currentDialogueIndex = ref(0)
|
|
const currentDialogueIndex = ref(0)
|
|
|
// 是否正在播放
|
|
// 是否正在播放
|
|
|
const isPlaying = ref(false)
|
|
const isPlaying = ref(false)
|
|
|
-// 当前激活的输入模式:'voice' 或 'keyboard'
|
|
|
|
|
-const activeInputMode = ref('voice')
|
|
|
|
|
// 输入卡片是否可见
|
|
// 输入卡片是否可见
|
|
|
const isInputCardVisible = ref(false)
|
|
const isInputCardVisible = ref(false)
|
|
|
// 用户输入内容
|
|
// 用户输入内容
|
|
|
const userInput = ref('')
|
|
const userInput = ref('')
|
|
|
-// 语音输入内容
|
|
|
|
|
-const voiceInput = ref('')
|
|
|
|
|
|
|
+// 语音录音状态
|
|
|
|
|
+const isVoiceRecording = ref(false)
|
|
|
|
|
|
|
|
// 音频对象
|
|
// 音频对象
|
|
|
// 背景音频
|
|
// 背景音频
|
|
@@ -203,38 +171,36 @@ const md = new MarkdownIt({
|
|
|
// 方法
|
|
// 方法
|
|
|
const handleVoiceRecognized = (text) => {
|
|
const handleVoiceRecognized = (text) => {
|
|
|
console.log('语音识别结果:', text)
|
|
console.log('语音识别结果:', text)
|
|
|
- voiceInput.value = text
|
|
|
|
|
|
|
+ // 获取输入框元素
|
|
|
|
|
+ const textarea = document.querySelector('.user-input-textarea')
|
|
|
|
|
+ if (textarea) {
|
|
|
|
|
+ // 获取光标位置
|
|
|
|
|
+ const startPos = textarea.selectionStart
|
|
|
|
|
+ const endPos = textarea.selectionEnd
|
|
|
|
|
+ // 在光标位置插入文本
|
|
|
|
|
+ userInput.value = userInput.value.substring(0, startPos) + text + userInput.value.substring(endPos)
|
|
|
|
|
+ // 重新设置光标位置到插入文本的末尾
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ textarea.selectionStart = textarea.selectionEnd = startPos + text.length
|
|
|
|
|
+ }, 0)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果没有找到输入框,直接替换整个内容
|
|
|
|
|
+ userInput.value = text
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理录音状态变化
|
|
// 处理录音状态变化
|
|
|
const handleRecordingStatusChanged = (isRecording) => {
|
|
const handleRecordingStatusChanged = (isRecording) => {
|
|
|
console.log('录音状态:', isRecording)
|
|
console.log('录音状态:', isRecording)
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 切换键盘输入
|
|
|
|
|
-const toggleKeyboardInput = () => {
|
|
|
|
|
- console.log('切换键盘输入')
|
|
|
|
|
- activeInputMode.value = 'keyboard'
|
|
|
|
|
- isInputCardVisible.value = true
|
|
|
|
|
|
|
+ isVoiceRecording.value = isRecording
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 提交用户输入
|
|
// 提交用户输入
|
|
|
-const submitUserInput = () => {
|
|
|
|
|
|
|
+const submitUserInput = async () => {
|
|
|
if (userInput.value.trim()) {
|
|
if (userInput.value.trim()) {
|
|
|
console.log('用户输入:', userInput.value)
|
|
console.log('用户输入:', userInput.value)
|
|
|
- // 提交后隐藏输入卡片
|
|
|
|
|
- isInputCardVisible.value = false
|
|
|
|
|
- userInput.value = ''
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 提交语音输入
|
|
|
|
|
-const submitVoiceInput = async () => {
|
|
|
|
|
- if (voiceInput.value.trim()) {
|
|
|
|
|
- console.log('语音输入:', voiceInput.value)
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
await createAiChart();
|
|
await createAiChart();
|
|
|
-
|
|
|
|
|
await doSendMessage();
|
|
await doSendMessage();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -243,14 +209,6 @@ const submitVoiceInput = async () => {
|
|
|
const cancelUserInput = () => {
|
|
const cancelUserInput = () => {
|
|
|
isInputCardVisible.value = false
|
|
isInputCardVisible.value = false
|
|
|
userInput.value = ''
|
|
userInput.value = ''
|
|
|
- voiceInput.value = ''
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 切换语音输入
|
|
|
|
|
-const toggleVoiceInput = () => {
|
|
|
|
|
- console.log('切换语音输入')
|
|
|
|
|
- activeInputMode.value = 'voice'
|
|
|
|
|
- isInputCardVisible.value = true
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 解析 Markdown 内容
|
|
// 解析 Markdown 内容
|
|
@@ -475,7 +433,7 @@ const createAiChart = async () => {
|
|
|
/** 真正执行【发送】消息操作 */
|
|
/** 真正执行【发送】消息操作 */
|
|
|
const doSendMessage = async () => {
|
|
const doSendMessage = async () => {
|
|
|
// 校验
|
|
// 校验
|
|
|
- if (voiceInput.value.length < 1) {
|
|
|
|
|
|
|
+ if (userInput.value.length < 1) {
|
|
|
console.error('发送失败,原因:内容为空!')
|
|
console.error('发送失败,原因:内容为空!')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -485,15 +443,16 @@ const doSendMessage = async () => {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 清空输入框
|
|
|
|
|
- voiceInput.value = ''
|
|
|
|
|
-
|
|
|
|
|
// 执行发送
|
|
// 执行发送
|
|
|
await doSendMessageStream({
|
|
await doSendMessageStream({
|
|
|
conversationId: activeConversationId.value,
|
|
conversationId: activeConversationId.value,
|
|
|
- content: voiceInput.value,
|
|
|
|
|
|
|
+ content: userInput.value,
|
|
|
contentAnswer: null,
|
|
contentAnswer: null,
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ // 清空输入框
|
|
|
|
|
+ userInput.value = ''
|
|
|
|
|
+ isInputCardVisible.value = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 真正执行【发送】消息操作 */
|
|
/** 真正执行【发送】消息操作 */
|
|
@@ -733,7 +692,7 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: flex-end;
|
|
align-items: flex-end;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
- padding-bottom: rpx(20);
|
|
|
|
|
|
|
+ padding-bottom: rpx(10);
|
|
|
z-index: 10;
|
|
z-index: 10;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -888,7 +847,7 @@ onUnmounted(() => {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
left: 50%;
|
|
left: 50%;
|
|
|
transform: translateX(-50%);
|
|
transform: translateX(-50%);
|
|
|
- bottom: rpx(50);
|
|
|
|
|
|
|
+ bottom: rpx(70);
|
|
|
right: auto;
|
|
right: auto;
|
|
|
animation: dialogueEnterCenter 0.6s ease forwards;
|
|
animation: dialogueEnterCenter 0.6s ease forwards;
|
|
|
}
|
|
}
|
|
@@ -1060,61 +1019,50 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.voice-input-outer {
|
|
.voice-input-outer {
|
|
|
- width: rpx(30);
|
|
|
|
|
- height: rpx(30);
|
|
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: rpx(50);
|
|
|
|
|
+ height: rpx(50);
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
background: transparent;
|
|
background: transparent;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
|
|
|
- &.active {
|
|
|
|
|
- width: rpx(40);
|
|
|
|
|
- height: rpx(40);
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translateX(-50%);
|
|
|
|
|
- z-index: 11;
|
|
|
|
|
-
|
|
|
|
|
- :deep(.voice-input-container) {
|
|
|
|
|
- .speech-btn {
|
|
|
|
|
- width: rpx(36);
|
|
|
|
|
- height: rpx(36);
|
|
|
|
|
- border: rpx(2) solid rgba(0, 100, 192);
|
|
|
|
|
- box-shadow: 0 rpx(3) rpx(8) rgba(0, 0, 0, 0.3);
|
|
|
|
|
-
|
|
|
|
|
- .el-icon {
|
|
|
|
|
- font-size: rpx(18);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 正常状态只显示一圈
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 85%;
|
|
|
|
|
+ height: 85%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: rpx(2) solid rgba(80, 190, 240, 0.6);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- &.inactive {
|
|
|
|
|
- width: rpx(24);
|
|
|
|
|
- height: rpx(24);
|
|
|
|
|
- left: calc(50% - rpx(40));
|
|
|
|
|
- transform: translateX(-50%);
|
|
|
|
|
- z-index: 10;
|
|
|
|
|
|
|
+ // 录音时显示波纹效果
|
|
|
|
|
+ &.recording {
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ animation: pulse 2s infinite;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- :deep(.voice-input-container) {
|
|
|
|
|
- .speech-btn {
|
|
|
|
|
- width: rpx(22);
|
|
|
|
|
- height: rpx(22);
|
|
|
|
|
- border: rpx(1) solid rgba(128, 128, 128, 0.5);
|
|
|
|
|
- box-shadow: none;
|
|
|
|
|
- background: linear-gradient(135deg, #E0E0E0, #C0C0C0);
|
|
|
|
|
-
|
|
|
|
|
- .el-icon {
|
|
|
|
|
- font-size: rpx(12);
|
|
|
|
|
- color: #808080;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ &::after {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 85%;
|
|
|
|
|
+ height: 85%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: rpx(2) solid rgba(80, 190, 240, 0.4);
|
|
|
|
|
+ animation: pulse 2s infinite 0.5s;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
:deep(.voice-input-container) {
|
|
:deep(.voice-input-container) {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 10;
|
|
|
.speech-btn {
|
|
.speech-btn {
|
|
|
- width: rpx(30);
|
|
|
|
|
- height: rpx(30);
|
|
|
|
|
|
|
+ width: rpx(40);
|
|
|
|
|
+ height: rpx(40);
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
- border: rpx(1) solid rgba(0, 100, 192);
|
|
|
|
|
|
|
+ border: rpx(2) solid rgba(0, 100, 192);
|
|
|
background: linear-gradient(135deg, #A0DCF0, #50BEF0);
|
|
background: linear-gradient(135deg, #A0DCF0, #50BEF0);
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -1123,19 +1071,40 @@ onUnmounted(() => {
|
|
|
gap: 0;
|
|
gap: 0;
|
|
|
transition: all 0.3s ease;
|
|
transition: all 0.3s ease;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
|
|
|
&:hover {
|
|
&:hover {
|
|
|
- transform: scale(1.1);
|
|
|
|
|
- box-shadow: 0 rpx(2) rpx(8) rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ box-shadow: 0 rpx(4) rpx(12) rgba(0, 0, 0, 0.3);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
&:active {
|
|
&:active {
|
|
|
transform: scale(0.95);
|
|
transform: scale(0.95);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.5);
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ transition: width 0.6s, height 0.6s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:active::before {
|
|
|
|
|
+ width: rpx(80);
|
|
|
|
|
+ height: rpx(80);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.el-icon {
|
|
.el-icon {
|
|
|
- font-size: rpx(15);
|
|
|
|
|
|
|
+ font-size: rpx(20);
|
|
|
color: #0064BE;
|
|
color: #0064BE;
|
|
|
|
|
+ z-index: 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.countdown-text {
|
|
.countdown-text {
|
|
@@ -1149,6 +1118,17 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+@keyframes pulse {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ transform: scale(1);
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: scale(1.15);
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.keyboard-input-outer {
|
|
.keyboard-input-outer {
|
|
|
width: rpx(30);
|
|
width: rpx(30);
|
|
|
height: rpx(30);
|
|
height: rpx(30);
|