Files
urbanLifeline/urbanLifelineWeb/packages/platform/src/views/public/Chat/AIChatView.vue
2025-12-20 17:12:42 +08:00

427 lines
15 KiB
Vue

<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 {
ChevronDown as ArrowDown,
Paperclip,
Star,
Image as Picture,
MoreHorizontal as MoreFilled,
Camera as CameraFilled,
Mic as Microphone,
Send as Promotion,
Plus,
PanelLeftClose as Fold,
PanelLeftOpen as Expand,
MessageCircle as ChatDotRound,
Trash2 as Delete
} from 'lucide-vue-next'
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>