|
@@ -36,7 +36,7 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="chat">
|
|
|
- <!-- 欢���消息 -->
|
|
|
+ <!-- 欢迎消息 -->
|
|
|
<div v-if="messages.length === 0" class="welcome-section">
|
|
|
<div class="welcome-header">
|
|
|
<img
|
|
@@ -150,37 +150,31 @@
|
|
|
</template>
|
|
|
</div>
|
|
|
</template>
|
|
|
- <div>
|
|
|
- {{
|
|
|
- message.role === "assistant"
|
|
|
- ? message.displayContent
|
|
|
- : message.content
|
|
|
- }}
|
|
|
- </div>
|
|
|
+ <div
|
|
|
+ class="message-text"
|
|
|
+ v-if="message.role === 'assistant'"
|
|
|
+ v-html="renderMarkdown(message.displayContent)"
|
|
|
+ ></div>
|
|
|
+ <div v-else>{{ message.content }}</div>
|
|
|
|
|
|
<!-- 音频控制按钮 -->
|
|
|
<div
|
|
|
v-if="message.role === 'assistant' && message.audioData"
|
|
|
class="audio-controls"
|
|
|
>
|
|
|
- <button
|
|
|
- class="audio-btn"
|
|
|
- @click="handleAudioClick(message)"
|
|
|
- >
|
|
|
+ <button class="audio-btn" @click="handleAudioClick(message)">
|
|
|
<!-- 只在当前消息正在播放时显示波形动画 -->
|
|
|
- <div v-if="currentPlayingId === message.id" class="wave-animation">
|
|
|
+ <div
|
|
|
+ v-if="currentPlayingId === message.id"
|
|
|
+ class="wave-animation"
|
|
|
+ >
|
|
|
<span class="wave-bar"></span>
|
|
|
<span class="wave-bar"></span>
|
|
|
<span class="wave-bar"></span>
|
|
|
<span class="wave-bar"></span>
|
|
|
</div>
|
|
|
<!-- 其他情况显示播放图标 -->
|
|
|
- <img
|
|
|
- v-else
|
|
|
- :src="playIcon"
|
|
|
- alt="播放"
|
|
|
- class="audio-icon"
|
|
|
- />
|
|
|
+ <img v-else :src="playIcon" alt="播放" class="audio-icon" />
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -374,6 +368,14 @@ import pauseIcon from "../assets/svg/pause.svg";
|
|
|
import playIcon from "../assets/svg/play.svg";
|
|
|
import axios from "axios";
|
|
|
import { useStore } from "vuex";
|
|
|
+import MarkdownIt from "markdown-it";
|
|
|
+
|
|
|
+// 初始化 markdown-it
|
|
|
+const md = new MarkdownIt({
|
|
|
+ html: true,
|
|
|
+ linkify: true,
|
|
|
+ typographer: true,
|
|
|
+});
|
|
|
|
|
|
// 响应式状态
|
|
|
const headerOpen = ref(false);
|
|
@@ -732,12 +734,12 @@ const playAudioMessage = async (audioBase64, messageId) => {
|
|
|
currentPlayingId.value = messageId;
|
|
|
|
|
|
// 添加事件监听
|
|
|
- audio.addEventListener('ended', () => {
|
|
|
+ audio.addEventListener("ended", () => {
|
|
|
stopAudio();
|
|
|
});
|
|
|
|
|
|
- audio.addEventListener('error', (e) => {
|
|
|
- console.error('音频播放错误:', e);
|
|
|
+ audio.addEventListener("error", (e) => {
|
|
|
+ console.error("音频播放错误:", e);
|
|
|
stopAudio();
|
|
|
});
|
|
|
|
|
@@ -745,7 +747,7 @@ const playAudioMessage = async (audioBase64, messageId) => {
|
|
|
await audio.play();
|
|
|
isPlaying.value = true;
|
|
|
} catch (error) {
|
|
|
- console.error('音频播放失败:', error);
|
|
|
+ console.error("音频播放失败:", error);
|
|
|
stopAudio();
|
|
|
}
|
|
|
};
|
|
@@ -764,6 +766,12 @@ const stopAudio = () => {
|
|
|
onUnmounted(() => {
|
|
|
stopAudio();
|
|
|
});
|
|
|
+
|
|
|
+// 添加 renderMarkdown 方法
|
|
|
+const renderMarkdown = (content) => {
|
|
|
+ if (!content) return "";
|
|
|
+ return md.render(content);
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
@@ -1727,4 +1735,107 @@ onUnmounted(() => {
|
|
|
width: 20px;
|
|
|
height: 20px;
|
|
|
}
|
|
|
+.message-text {
|
|
|
+ margin: 0;
|
|
|
+ white-space: normal; /* 确保文本会换行 */
|
|
|
+ word-wrap: break-word; /* 长单词或 URL 会被断开以适应容器宽度 */
|
|
|
+ overflow: auto;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 20px;
|
|
|
+}
|
|
|
+.message-text ::v-deep p {
|
|
|
+ font-size: 12px !important;
|
|
|
+ margin: 5px 0 10px;
|
|
|
+ padding: 0;
|
|
|
+ font-size: inherit;
|
|
|
+ line-height: 22px;
|
|
|
+ font-weight: inherit;
|
|
|
+}
|
|
|
+.message-text ::v-deep ol {
|
|
|
+ padding-bottom: 10px;
|
|
|
+}
|
|
|
+.message-text ::v-deep .source-section h3 {
|
|
|
+ margin: 20px 0 3px;
|
|
|
+ color: #333;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+.message-text ::v-deep ol li {
|
|
|
+ padding-top: 3px;
|
|
|
+}
|
|
|
+.message-text ::v-deep ol li a {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #1296db;
|
|
|
+}
|
|
|
+</style>
|
|
|
+<style lang="less" scoped>
|
|
|
+::v-deep .message-content {
|
|
|
+ /* 确保 scoped 样式能够渗透到动态生成的内容 */
|
|
|
+ h1,
|
|
|
+ h2,
|
|
|
+ h3,
|
|
|
+ h4,
|
|
|
+ h5,
|
|
|
+ h6 {
|
|
|
+ margin-top: 16px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ code {
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding: 2px 4px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-family: monospace;
|
|
|
+ }
|
|
|
+
|
|
|
+ pre {
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow-x: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ ul,
|
|
|
+ ol {
|
|
|
+ padding-left: 24px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ blockquote {
|
|
|
+ border-left: 4px solid #ddd;
|
|
|
+ padding-left: 16px;
|
|
|
+ margin: 8px 0;
|
|
|
+ color: rgba(0, 0, 0, 0.65);
|
|
|
+ }
|
|
|
+
|
|
|
+ a {
|
|
|
+ color: #1677ff;
|
|
|
+ text-decoration: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ a:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+ }
|
|
|
+
|
|
|
+ table {
|
|
|
+ border-collapse: collapse;
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ th,
|
|
|
+ td {
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ padding: 8px;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background: #f5f5f5;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|