mock数据,AI对话,全部应用
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
.chat-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 左侧侧边栏样式 - ChatGPT Style
|
||||
.chat-sidebar {
|
||||
width: 260px;
|
||||
background: #f7f7f8;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
&.collapsed {
|
||||
width: 60px;
|
||||
|
||||
.new-chat-btn {
|
||||
padding: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.new-chat-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #7c3aed;
|
||||
color: #7c3aed;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
color: #7c3aed;
|
||||
}
|
||||
}
|
||||
|
||||
.conversations-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.list-section {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-title {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: #374151;
|
||||
|
||||
&:hover {
|
||||
background: #e5e7eb;
|
||||
|
||||
.conv-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.conv-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conv-actions {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.action-icon {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
color: #6b7280;
|
||||
|
||||
&:hover {
|
||||
background: #d1d5db;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主聊天区域
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
.agent-dropdown {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.agent-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-agent-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 40px 80px;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
&.user {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
align-items: flex-end;
|
||||
|
||||
.message-text {
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
flex-shrink: 0;
|
||||
|
||||
.ai-avatar-small {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
object-fit: contain;
|
||||
background: #f3f4f6;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-width: 70%;
|
||||
|
||||
.message-text {
|
||||
padding: 12px 16px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 12px;
|
||||
color: #1f2937;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 16px;
|
||||
|
||||
span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #9ca3af;
|
||||
border-radius: 50%;
|
||||
animation: typing 1.4s infinite ease-in-out both;
|
||||
|
||||
&:nth-child(1) { animation-delay: -0.32s; }
|
||||
&:nth-child(2) { animation-delay: -0.16s; }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input-area {
|
||||
padding: 20px 80px 30px;
|
||||
background: #fff;
|
||||
|
||||
.input-wrapper {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
min-height: 24px;
|
||||
max-height: 120px;
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
|
||||
.action-buttons, .send-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: #7c3aed;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #5b21b6;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div class="chat-view">
|
||||
<!-- Left Sidebar - ChatGPT Style -->
|
||||
<aside class="chat-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="sidebar-header">
|
||||
<button class="collapse-toggle" @click="sidebarCollapsed = !sidebarCollapsed">
|
||||
<el-icon><Fold v-if="!sidebarCollapsed" /><Expand v-else /></el-icon>
|
||||
<span v-if="!sidebarCollapsed">{{ sidebarCollapsed ? '展开' : '收起' }}</span>
|
||||
</button>
|
||||
<button class="new-chat-btn" @click="handleNewChat">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span v-if="!sidebarCollapsed">新建对话</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!sidebarCollapsed" class="conversations-list">
|
||||
<div class="list-section">
|
||||
<div class="section-title">今天</div>
|
||||
<div
|
||||
v-for="conv in todayConversations"
|
||||
:key="conv.id"
|
||||
class="conversation-item"
|
||||
:class="{ active: currentConversationId === conv.id }"
|
||||
@click="selectConversation(conv)"
|
||||
>
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span class="conv-title">{{ conv.title }}</span>
|
||||
<div class="conv-actions">
|
||||
<el-icon class="action-icon" @click.stop="deleteConversation(conv.id)"><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-section" v-if="olderConversations.length > 0">
|
||||
<div class="section-title">历史记录</div>
|
||||
<div
|
||||
v-for="conv in olderConversations"
|
||||
:key="conv.id"
|
||||
class="conversation-item"
|
||||
:class="{ active: currentConversationId === conv.id }"
|
||||
@click="selectConversation(conv)"
|
||||
>
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span class="conv-title">{{ conv.title }}</span>
|
||||
<div class="conv-actions">
|
||||
<el-icon class="action-icon" @click.stop="deleteConversation(conv.id)"><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Chat Area -->
|
||||
<div class="chat-main">
|
||||
<!-- Header -->
|
||||
<header class="chat-header">
|
||||
<el-dropdown trigger="click" @command="handleAgentChange" class="agent-dropdown">
|
||||
<div class="header-title">
|
||||
<span class="agent-icon">{{ currentAgent.icon }}</span>
|
||||
<span>{{ currentAgent.name }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
:command="agent.id"
|
||||
:class="{ 'is-active': agent.id === currentAgent.id }"
|
||||
>
|
||||
<span class="dropdown-agent-icon">{{ agent.icon }}</span>
|
||||
<span>{{ agent.name }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</header>
|
||||
|
||||
<!-- Chat Content -->
|
||||
<div class="chat-content" ref="chatContentRef">
|
||||
<!-- Welcome Message -->
|
||||
<div v-if="messages.length === 0" class="welcome-section">
|
||||
<ChatDefault
|
||||
:agent="currentAgent"
|
||||
@suggestion-click="handleSuggestionClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div v-else class="messages-container">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message"
|
||||
:class="message.role"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
<img v-if="message.role === 'assistant'" src="/logo.jpg" alt="AI" class="ai-avatar-small" />
|
||||
<div v-else class="user-avatar-small">👤</div>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-text">{{ message.content }}</div>
|
||||
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div v-if="isLoading" class="message assistant">
|
||||
<div class="message-avatar">
|
||||
<img src="/logo.jpg" alt="AI" class="ai-avatar-small" />
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
placeholder="请输入内容..."
|
||||
@keydown.enter.prevent="handleSend"
|
||||
rows="1"
|
||||
ref="textareaRef"
|
||||
></textarea>
|
||||
<div class="input-actions">
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn" title="附件">
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
</button>
|
||||
<button class="action-btn" title="表情">
|
||||
<el-icon><Star /></el-icon>
|
||||
</button>
|
||||
<button class="action-btn" title="图片">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</button>
|
||||
<button class="action-btn" title="更多">
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</button>
|
||||
<button class="action-btn" title="截图">
|
||||
<el-icon><CameraFilled /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="send-actions">
|
||||
<button class="action-btn" title="语音">
|
||||
<el-icon><Microphone /></el-icon>
|
||||
</button>
|
||||
<button class="send-btn" @click="handleSend" :disabled="!inputText.trim()">
|
||||
<el-icon><Promotion /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, nextTick, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
ArrowDown,
|
||||
Paperclip,
|
||||
Star,
|
||||
Picture,
|
||||
MoreFilled,
|
||||
CameraFilled,
|
||||
Microphone,
|
||||
Promotion,
|
||||
Plus,
|
||||
Fold,
|
||||
Expand,
|
||||
ChatDotRound,
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
import ChatDefault from './components/ChatDefault/ChatDefault.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// Mock 数据 - 智能体列表
|
||||
const agents = ref([
|
||||
{
|
||||
id: 'default',
|
||||
name: '城市生命线助手',
|
||||
icon: '🏙️',
|
||||
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
description: '我是城市生命线智能助手,专注于城市基础设施安全管理'
|
||||
},
|
||||
{
|
||||
id: 'emergency',
|
||||
name: '应急处理助手',
|
||||
icon: '🚨',
|
||||
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
description: '专注于应急事件处理和预案制定'
|
||||
},
|
||||
{
|
||||
id: 'analysis',
|
||||
name: '数据分析助手',
|
||||
icon: '📊',
|
||||
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
description: '提供数据分析和可视化服务'
|
||||
},
|
||||
{
|
||||
id: 'safety',
|
||||
name: '安全检查助手',
|
||||
icon: '🔍',
|
||||
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
||||
description: '协助进行安全隐患排查和整改'
|
||||
}
|
||||
])
|
||||
|
||||
const currentAgent = ref(agents.value[0])
|
||||
|
||||
// 状态管理
|
||||
const sidebarCollapsed = ref(false)
|
||||
const currentConversationId = ref<number | null>(null)
|
||||
const inputText = ref('')
|
||||
const isLoading = ref(false)
|
||||
const chatContentRef = ref<HTMLElement | null>(null)
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// Mock 数据 - 对话历史
|
||||
interface Conversation {
|
||||
id: number
|
||||
title: string
|
||||
date: Date
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
content: string
|
||||
role: 'user' | 'assistant'
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
const conversations = ref<Conversation[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '城市生命线关键设施咨询',
|
||||
date: new Date(),
|
||||
messages: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '消防安全隐患处理方案',
|
||||
date: new Date(),
|
||||
messages: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '排水系统优化建议',
|
||||
date: new Date(Date.now() - 86400000),
|
||||
messages: []
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '应急预案讨论',
|
||||
date: new Date(Date.now() - 172800000),
|
||||
messages: []
|
||||
}
|
||||
])
|
||||
|
||||
const messages = ref<Message[]>([])
|
||||
|
||||
// 今天的对话
|
||||
const todayConversations = computed(() => {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
return conversations.value.filter(conv => new Date(conv.date) >= today)
|
||||
})
|
||||
|
||||
// 历史对话
|
||||
const olderConversations = computed(() => {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
return conversations.value.filter(conv => new Date(conv.date) < today)
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp: string) => {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
if (chatContentRef.value) {
|
||||
chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 新建对话
|
||||
const handleNewChat = () => {
|
||||
const newConv: Conversation = {
|
||||
id: Date.now(),
|
||||
title: '新对话',
|
||||
date: new Date(),
|
||||
messages: []
|
||||
}
|
||||
conversations.value.unshift(newConv)
|
||||
currentConversationId.value = newConv.id
|
||||
messages.value = []
|
||||
}
|
||||
|
||||
// 选择对话
|
||||
const selectConversation = (conv: Conversation) => {
|
||||
currentConversationId.value = conv.id
|
||||
messages.value = conv.messages || []
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
// 删除对话
|
||||
const deleteConversation = (id: number) => {
|
||||
const index = conversations.value.findIndex(c => c.id === id)
|
||||
if (index > -1) {
|
||||
conversations.value.splice(index, 1)
|
||||
if (currentConversationId.value === id) {
|
||||
currentConversationId.value = null
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换智能体
|
||||
const handleAgentChange = (agentId: string) => {
|
||||
const agent = agents.value.find(a => a.id === agentId)
|
||||
if (agent) {
|
||||
currentAgent.value = agent
|
||||
// 切换智能体时清空对话
|
||||
messages.value = []
|
||||
currentConversationId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理建议点击
|
||||
const handleSuggestionClick = async (suggestion: string) => {
|
||||
inputText.value = suggestion
|
||||
await handleSend()
|
||||
}
|
||||
|
||||
// 发送消息 - Mock 实现
|
||||
const handleSend = async () => {
|
||||
const text = inputText.value.trim()
|
||||
if (!text || isLoading.value) return
|
||||
|
||||
// 添加用户消息
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
content: text,
|
||||
role: 'user',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
messages.value.push(userMessage)
|
||||
inputText.value = ''
|
||||
|
||||
await scrollToBottom()
|
||||
|
||||
// 显示加载状态
|
||||
isLoading.value = true
|
||||
|
||||
// Mock API 响应
|
||||
setTimeout(async () => {
|
||||
const mockResponses = [
|
||||
'根据城市生命线安全管理的相关规定,我为您分析如下...',
|
||||
'这是一个很好的问题。让我详细为您解答...',
|
||||
'基于您的需求,我建议采取以下措施...',
|
||||
'从专业角度来看,这个问题需要综合考虑多个因素...'
|
||||
]
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: Date.now().toString() + '_ai',
|
||||
content: mockResponses[Math.floor(Math.random() * mockResponses.length)],
|
||||
role: 'assistant',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
messages.value.push(assistantMessage)
|
||||
|
||||
// 保存到当前对话
|
||||
if (currentConversationId.value) {
|
||||
const conv = conversations.value.find(c => c.id === currentConversationId.value)
|
||||
if (conv) {
|
||||
conv.messages = messages.value
|
||||
// 更新对话标题(取第一条用户消息)
|
||||
if (conv.title === '新对话' && messages.value.length > 0) {
|
||||
const firstUserMsg = messages.value.find(m => m.role === 'user')
|
||||
if (firstUserMsg) {
|
||||
conv.title = firstUserMsg.content.slice(0, 20) + (firstUserMsg.content.length > 20 ? '...' : '')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
await scrollToBottom()
|
||||
}, 1000 + Math.random() * 1000) // 随机延迟 1-2 秒
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 从 URL 参数读取 agentId
|
||||
const agentId = route.query.agentId as string
|
||||
if (agentId) {
|
||||
const agent = agents.value.find(a => a.id === agentId)
|
||||
if (agent) {
|
||||
currentAgent.value = agent
|
||||
console.log('[AI Chat] 已切换到智能体:', agent.name)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('AI Chat View mounted')
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url('./AIChatView.scss');
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
# AI 聊天界面组件
|
||||
|
||||
## 功能概览
|
||||
|
||||
完整的 AI 聊天界面实现,包含侧边栏对话历史、智能体切换、消息交互等功能。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
Chat/
|
||||
├── AIChatView.vue # 主聊天界面组件
|
||||
├── AIChatView.scss # 主界面样式
|
||||
├── components/
|
||||
│ ├── ChatDefault/ # 欢迎界面组件
|
||||
│ │ ├── ChatDefault.vue
|
||||
│ │ └── ChatDefault.scss
|
||||
│ └── ChatHistory/ # 历史记录组件(预留)
|
||||
│ ├── ChatHistory.vue
|
||||
│ └── ChatHistory.scss
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 侧边栏功能
|
||||
- ✅ 对话历史列表(今天/历史记录分组)
|
||||
- ✅ 新建对话
|
||||
- ✅ 删除对话
|
||||
- ✅ 侧边栏收起/展开
|
||||
|
||||
### 2. 智能体系统
|
||||
- ✅ 多智能体支持(城市生命线、应急处理、数据分析、安全检查)
|
||||
- ✅ 智能体切换(下拉选择)
|
||||
- ✅ 每个智能体独立的欢迎信息和建议
|
||||
|
||||
### 3. 消息交互
|
||||
- ✅ 消息发送/接收
|
||||
- ✅ 用户/AI 消息区分显示
|
||||
- ✅ 打字中动画效果
|
||||
- ✅ 消息时间显示
|
||||
- ✅ 自动滚动到底部
|
||||
|
||||
### 4. 输入功能
|
||||
- ✅ 多行文本输入
|
||||
- ✅ Enter 发送(Shift+Enter 换行)
|
||||
- ✅ 附件、表情、图片等功能按钮(UI 已实现)
|
||||
- ✅ 发送按钮禁用状态
|
||||
|
||||
## Mock 数据
|
||||
|
||||
### 智能体列表
|
||||
```typescript
|
||||
[
|
||||
{
|
||||
id: 'default',
|
||||
name: '城市生命线助手',
|
||||
icon: '🏙️',
|
||||
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
description: '我是城市生命线智能助手,专注于城市基础设施安全管理'
|
||||
},
|
||||
// ... 其他智能体
|
||||
]
|
||||
```
|
||||
|
||||
### Mock API 响应
|
||||
发送消息后,系统会在 1-2 秒后返回随机的 Mock 响应:
|
||||
- "根据城市生命线安全管理的相关规定,我为您分析如下..."
|
||||
- "这是一个很好的问题。让我详细为您解答..."
|
||||
- "基于您的需求,我建议采取以下措施..."
|
||||
- "从专业角度来看,这个问题需要综合考虑多个因素..."
|
||||
|
||||
## 样式特点
|
||||
|
||||
### 设计风格
|
||||
- ChatGPT 风格的侧边栏布局
|
||||
- 现代简约的配色方案
|
||||
- 流畅的动画效果
|
||||
- 响应式设计
|
||||
|
||||
### 主题色
|
||||
- 主色调:紫色 `#7c3aed`
|
||||
- 背景色:灰白 `#f7f7f8`
|
||||
- 边框色:浅灰 `#e5e7eb`
|
||||
- 文字色:深灰 `#374151`
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 基本使用
|
||||
```vue
|
||||
<template>
|
||||
<AIChatView />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AIChatView from '@/views/public/Chat/AIChatView.vue'
|
||||
</script>
|
||||
```
|
||||
|
||||
### 集成到路由
|
||||
```typescript
|
||||
{
|
||||
path: '/chat',
|
||||
name: 'Chat',
|
||||
component: () => import('@/views/public/Chat/AIChatView.vue')
|
||||
}
|
||||
```
|
||||
|
||||
## 后续接入真实 API
|
||||
|
||||
### 修改发送消息函数
|
||||
在 `AIChatView.vue` 中找到 `handleSend` 函数,将 Mock 实现替换为真实 API 调用:
|
||||
|
||||
```typescript
|
||||
// 替换这部分 Mock 代码
|
||||
setTimeout(async () => {
|
||||
const mockResponses = [...]
|
||||
// ...
|
||||
}, 1000)
|
||||
|
||||
// 改为真实 API 调用
|
||||
try {
|
||||
const response = await chatAPI.sendMessage({
|
||||
message: text,
|
||||
agentId: currentAgent.value.id,
|
||||
conversationId: currentConversationId.value
|
||||
})
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: response.id,
|
||||
content: response.content,
|
||||
role: 'assistant',
|
||||
timestamp: response.timestamp
|
||||
}
|
||||
messages.value.push(assistantMessage)
|
||||
} catch (error) {
|
||||
console.error('发送失败:', error)
|
||||
}
|
||||
```
|
||||
|
||||
### 加载历史对话
|
||||
在 `onMounted` 中添加历史对话加载:
|
||||
|
||||
```typescript
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const history = await chatAPI.getConversations()
|
||||
conversations.value = history
|
||||
} catch (error) {
|
||||
console.error('加载历史失败:', error)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- Vue 3
|
||||
- Element Plus
|
||||
- TypeScript
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保已安装 Element Plus 并正确配置图标
|
||||
2. `/logo.jpg` 需要存在于 public 目录
|
||||
3. 消息滚动使用了 `scrollTop`,需要在有高度的容器中使用
|
||||
4. 缩进使用 4 个空格(符合用户规则)
|
||||
|
||||
## 待优化功能
|
||||
|
||||
- [ ] 消息流式输出
|
||||
- [ ] 代码块语法高亮
|
||||
- [ ] Markdown 渲染
|
||||
- [ ] 消息重新生成
|
||||
- [ ] 消息编辑
|
||||
- [ ] 导出对话记录
|
||||
- [ ] 附件上传功能实现
|
||||
- [ ] 语音输入功能实现
|
||||
@@ -0,0 +1,82 @@
|
||||
.chat-default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.avatar-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.suggestion-cards {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 800px;
|
||||
|
||||
.suggestion-card {
|
||||
width: 220px;
|
||||
padding: 20px;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="chat-default">
|
||||
<!-- AI 头像 -->
|
||||
<div class="ai-avatar">
|
||||
<div class="avatar-icon" :style="{ background: agent.color }">
|
||||
{{ agent.icon }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述文字 -->
|
||||
<p class="welcome-text">
|
||||
{{ agent.description }}
|
||||
</p>
|
||||
|
||||
<!-- 欢迎标题 -->
|
||||
<h2 class="welcome-title">{{ welcomeTitle }}</h2>
|
||||
|
||||
<!-- 建议卡片 -->
|
||||
<div class="suggestion-cards">
|
||||
<div
|
||||
v-for="(suggestion, index) in currentSuggestions"
|
||||
:key="index"
|
||||
class="suggestion-card"
|
||||
@click="handleSuggestionClick(suggestion)"
|
||||
>
|
||||
<div class="card-icon" :style="{ background: agent.color }">
|
||||
<component :is="cardIcons[index % cardIcons.length]" />
|
||||
</div>
|
||||
<p class="card-text">{{ suggestion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { OfficeBuilding, Warning, Cloudy } from '@element-plus/icons-vue'
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
color: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
agent: Agent
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
suggestionClick: [suggestion: string]
|
||||
}>()
|
||||
|
||||
// 各智能体的建议内容
|
||||
const agentSuggestions: Record<string, string[]> = {
|
||||
default: [
|
||||
'城市生命线关键设施有哪些?',
|
||||
'消防安全隐患常见问题以及处理措施有哪些?',
|
||||
'如何平衡排水能力和生态环境保护?'
|
||||
],
|
||||
emergency: [
|
||||
'应急预案应该包含哪些关键内容?',
|
||||
'突发事件的处理流程是什么?',
|
||||
'如何建立高效的应急响应机制?'
|
||||
],
|
||||
analysis: [
|
||||
'如何分析城市设施运行数据?',
|
||||
'帮我生成一份数据分析报告',
|
||||
'如何预测设施故障风险?'
|
||||
],
|
||||
safety: [
|
||||
'安全检查的标准流程是什么?',
|
||||
'常见安全隐患有哪些类型?',
|
||||
'如何制定隐患整改计划?'
|
||||
]
|
||||
}
|
||||
|
||||
// 各智能体的欢迎标题
|
||||
const agentWelcomeTitles: Record<string, string> = {
|
||||
default: '今天需要我帮你做点什么吗?',
|
||||
emergency: '需要我帮你处理什么应急事件?',
|
||||
analysis: '需要我帮你分析什么数据?',
|
||||
safety: '需要我帮你检查什么安全隐患?'
|
||||
}
|
||||
|
||||
// 当前智能体的建议
|
||||
const currentSuggestions = computed(() => {
|
||||
return agentSuggestions[props.agent.id] || agentSuggestions.default
|
||||
})
|
||||
|
||||
// 当前欢迎标题
|
||||
const welcomeTitle = computed(() => {
|
||||
return agentWelcomeTitles[props.agent.id] || agentWelcomeTitles.default
|
||||
})
|
||||
|
||||
// 卡片图标
|
||||
const cardIcons = [OfficeBuilding, Warning, Cloudy]
|
||||
|
||||
// 处理建议点击
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
emit('suggestionClick', suggestion)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url('./ChatDefault.scss');
|
||||
</style>
|
||||
Reference in New Issue
Block a user