298 lines
7.0 KiB
Vue
298 lines
7.0 KiB
Vue
<template>
|
|
<div class="message-item" :class="messageClass">
|
|
<div class="message-content">
|
|
<!-- 用户消息 -->
|
|
<div v-if="message.type === 'user'" class="user-message">
|
|
<div class="message-bubble user-bubble">
|
|
{{ message.content }}
|
|
</div>
|
|
<div class="message-time">
|
|
{{ formatTime(message.timestamp) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI消息 -->
|
|
<div v-else-if="message.type === 'ai'" class="ai-message">
|
|
<div class="ai-avatar">
|
|
<div class="avatar-icon">🤖</div>
|
|
</div>
|
|
<div class="ai-content">
|
|
<div class="message-bubble ai-bubble">
|
|
<div v-if="isTyping" class="typing-text">
|
|
<div class="markdown" v-html="rendered"></div>
|
|
<span class="cursor">|</span>
|
|
</div>
|
|
<div v-else class="markdown" v-html="rendered"></div>
|
|
</div>
|
|
<div class="message-time">
|
|
{{ formatTime(message.timestamp) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 思考过程 -->
|
|
<div v-else-if="message.type === 'thinking'" class="thinking-message">
|
|
<div class="ai-avatar">
|
|
<div class="avatar-icon thinking">💭</div>
|
|
</div>
|
|
<div class="thinking-content">
|
|
<div class="thinking-bubble">
|
|
<div class="thinking-dots">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
<div class="thinking-text">{{ message.content }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 组件消息(订单卡片等) -->
|
|
<div v-else-if="message.type === 'component'" class="component-message">
|
|
<div class="ai-avatar">
|
|
<div class="avatar-icon">🤖</div>
|
|
</div>
|
|
<div class="component-content">
|
|
<div class="component-wrapper">
|
|
<OrderCard
|
|
v-if="message.component?.type === 'order_card'"
|
|
:order-data="message.component.data"
|
|
/>
|
|
<SimpleOrderCard
|
|
v-else-if="(message as any).component?.order"
|
|
:order="(message as any).component.order"
|
|
/>
|
|
</div>
|
|
<div class="message-time">
|
|
{{ formatTime(message.timestamp) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch, onMounted } from 'vue';
|
|
import { marked } from 'marked';
|
|
import type { Message } from '@/types';
|
|
import OrderCard from './OrderCard.vue';
|
|
import SimpleOrderCard from './SimpleOrderCard.vue';
|
|
|
|
interface Props {
|
|
message: Message;
|
|
isTyping?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
isTyping: false
|
|
});
|
|
|
|
// 显示内容(用于打字机效果)
|
|
const displayContent = ref('');
|
|
const rendered = ref('');
|
|
|
|
const sanitizeMd = (s: string) => (s || '').replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
// 消息样式类
|
|
const messageClass = computed(() => ({
|
|
'message-user': props.message.type === 'user',
|
|
'message-ai': props.message.type === 'ai',
|
|
'message-thinking': props.message.type === 'thinking',
|
|
'message-component': props.message.type === 'component'
|
|
}));
|
|
|
|
// 格式化时间
|
|
const formatTime = (timestamp: number): string => {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diff = now.getTime() - date.getTime();
|
|
|
|
// 如果是今天
|
|
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
|
return date.toLocaleTimeString('zh-CN', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// 如果是昨天
|
|
const yesterday = new Date(now);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
if (date.getDate() === yesterday.getDate()) {
|
|
return '昨天 ' + date.toLocaleTimeString('zh-CN', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// 其他日期
|
|
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
// 打字机效果
|
|
watch(() => props.message.content, (newContent) => {
|
|
displayContent.value = newContent;
|
|
rendered.value = marked.parse(sanitizeMd(newContent || ''));
|
|
}, { immediate: true });
|
|
|
|
onMounted(() => {
|
|
displayContent.value = props.message.content;
|
|
rendered.value = marked.parse(sanitizeMd(displayContent.value || ''));
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.message-item {
|
|
@apply mb-4 px-4;
|
|
}
|
|
|
|
.message-content {
|
|
@apply max-w-full;
|
|
}
|
|
|
|
/* 用户消息样式 */
|
|
.user-message {
|
|
@apply flex flex-col items-end;
|
|
}
|
|
|
|
.user-bubble {
|
|
@apply bg-blue-500 text-white px-4 py-2 rounded-2xl rounded-br-md max-w-xs md:max-w-md break-words;
|
|
}
|
|
|
|
/* AI消息样式 */
|
|
.ai-message {
|
|
@apply flex items-start space-x-3;
|
|
}
|
|
|
|
.ai-avatar {
|
|
@apply flex-shrink-0 w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center;
|
|
}
|
|
|
|
.avatar-icon {
|
|
@apply text-lg;
|
|
}
|
|
|
|
.avatar-icon.thinking {
|
|
@apply animate-pulse;
|
|
}
|
|
|
|
.ai-content {
|
|
@apply flex-1 min-w-0;
|
|
}
|
|
|
|
.ai-bubble {
|
|
@apply bg-gray-100 text-gray-800 px-4 py-2 rounded-2xl rounded-bl-md max-w-xs md:max-w-md break-words;
|
|
}
|
|
|
|
.markdown :is(h1,h2,h3,h4,h5,h6) { @apply font-semibold text-gray-900 }
|
|
.markdown p { @apply text-gray-800 leading-6 }
|
|
.markdown code { @apply bg-gray-200 rounded px-1 py-0.5 text-sm }
|
|
.markdown pre { @apply bg-gray-200 rounded p-3 overflow-auto text-sm }
|
|
.markdown ul { @apply list-disc pl-5 }
|
|
.markdown ol { @apply list-decimal pl-5 }
|
|
.dark .markdown :is(h1,h2,h3,h4,h5,h6) { @apply text-gray-100 }
|
|
.dark .markdown p { @apply text-gray-100 }
|
|
.dark .markdown code, .dark .markdown pre { @apply bg-gray-700 text-gray-100 }
|
|
|
|
/* 思考过程样式 */
|
|
.thinking-message {
|
|
@apply flex items-start space-x-3 opacity-70;
|
|
}
|
|
|
|
.thinking-content {
|
|
@apply flex-1 min-w-0;
|
|
}
|
|
|
|
.thinking-bubble {
|
|
@apply bg-gray-50 border border-gray-200 px-4 py-2 rounded-2xl rounded-bl-md max-w-xs md:max-w-md;
|
|
}
|
|
|
|
.thinking-dots {
|
|
@apply flex space-x-1 mb-2;
|
|
}
|
|
|
|
.thinking-dots span {
|
|
@apply w-2 h-2 bg-gray-400 rounded-full animate-bounce;
|
|
}
|
|
|
|
.thinking-dots span:nth-child(2) {
|
|
animation-delay: 0.1s;
|
|
}
|
|
|
|
.thinking-dots span:nth-child(3) {
|
|
animation-delay: 0.2s;
|
|
}
|
|
|
|
.thinking-text {
|
|
@apply text-sm text-gray-600 italic;
|
|
}
|
|
|
|
/* 组件消息样式 */
|
|
.component-message {
|
|
@apply flex items-start space-x-3;
|
|
}
|
|
|
|
.component-content {
|
|
@apply flex-1 min-w-0;
|
|
}
|
|
|
|
.component-wrapper {
|
|
@apply max-w-sm;
|
|
}
|
|
|
|
/* 消息气泡通用样式 */
|
|
.message-bubble {
|
|
@apply shadow-sm;
|
|
}
|
|
|
|
/* 消息时间样式 */
|
|
.message-time {
|
|
@apply text-xs text-gray-500 mt-1;
|
|
}
|
|
|
|
.user-message .message-time {
|
|
@apply text-right;
|
|
}
|
|
|
|
/* 打字机效果 */
|
|
.typing-text {
|
|
@apply inline;
|
|
}
|
|
|
|
.cursor {
|
|
@apply animate-pulse;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 640px) {
|
|
.message-bubble {
|
|
@apply max-w-xs;
|
|
}
|
|
}
|
|
|
|
/* 暗色主题支持 */
|
|
.dark .ai-bubble {
|
|
@apply bg-gray-700 text-gray-100;
|
|
}
|
|
|
|
.dark .thinking-bubble {
|
|
@apply bg-gray-800 border-gray-600;
|
|
}
|
|
|
|
.dark .thinking-text {
|
|
@apply text-gray-400;
|
|
}
|
|
|
|
.dark .ai-avatar {
|
|
@apply bg-gray-700;
|
|
}
|
|
|
|
.dark .message-time {
|
|
@apply text-gray-400;
|
|
}
|
|
</style>
|