web/src/views/ChatAgentView.vue
2025-06-04 10:31:48 +08:00

2256 lines
53 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import { http } from '../services/api'
import { marked } from 'marked'
// 配置marked选项
onMounted(() => {
marked.setOptions({
breaks: true,
gfm: true, // GitHub风格Markdown支持表格等扩展语法
})
// 初始化对话列表
fetchConversations()
})
// 消息类型定义
interface Message {
id: string
type: 'user' | 'assistant'
content: string
timestamp: Date
isStreaming?: boolean
}
// 对话类型定义
interface Conversation {
id: string
name: string
}
// 历史消息类型定义
interface HistoryMessage {
id: string
conversation_id: string
query: string
answer: string
created_at: number
}
// 状态管理
const messageInput = ref('')
const messages = ref<Message[]>([])
const isLoading = ref(false)
const chatContainer = ref<HTMLElement | null>(null)
// 对话相关状态
const conversations = ref<Conversation[]>([])
const selectedConversationId = ref<string>('')
const isLoadingConversations = ref(false)
const isLoadingHistory = ref(false)
const isDropdownOpen = ref(false)
// 自定义弹框状态
const showDeleteDialog = ref(false)
const showRenameDialog = ref(false)
const conversationToDelete = ref<Conversation | null>(null)
const conversationToRename = ref<Conversation | null>(null)
const newConversationName = ref('')
const renameInputRef = ref<HTMLInputElement | null>(null)
// 流式输出控制
const currentTaskId = ref<string>('')
const streamingAbortController = ref<AbortController | null>(null)
// 根据环境选择API基础URL
const apiBaseUrl =
import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'
// 获取对话列表
const fetchConversations = async () => {
try {
isLoadingConversations.value = true
const response = await http.get(`${apiBaseUrl}/analysis/conversations`)
if (response.ok) {
conversations.value = await response.json()
// 默认始终使用新对话,不自动加载历史会话
}
} catch (error) {
console.error('获取对话列表失败:', error)
} finally {
isLoadingConversations.value = false
}
}
// 获取对话历史消息
const fetchConversationMessages = async (conversationId: string) => {
try {
isLoadingHistory.value = true
const response = await http.get(
`${apiBaseUrl}/analysis/conversation_messages/${conversationId}`,
)
if (response.ok) {
const historyMessages: HistoryMessage[] = await response.json()
// 将历史消息转换为Message格式
const convertedMessages: Message[] = []
historyMessages.forEach((msg) => {
// 添加用户消息
convertedMessages.push({
id: `${msg.id}_query`,
type: 'user',
content: msg.query,
timestamp: new Date(msg.created_at * 1000),
})
// 添加助手回复
convertedMessages.push({
id: `${msg.id}_answer`,
type: 'assistant',
content: msg.answer,
timestamp: new Date(msg.created_at * 1000),
})
})
messages.value = convertedMessages
await scrollToBottom()
}
} catch (error) {
console.error('获取对话历史失败:', error)
} finally {
isLoadingHistory.value = false
}
}
// 处理对话选择变化
const selectConversationFromDropdown = async (conversationId: string) => {
selectedConversationId.value = conversationId
await fetchConversationMessages(conversationId)
closeConversationDropdown()
}
// 选择新对话
const selectNewConversation = () => {
selectedConversationId.value = ''
messages.value = []
closeConversationDropdown()
}
// 关闭下拉会话列表
const closeConversationDropdown = () => {
isDropdownOpen.value = false
}
// 获取当前对话名称
const getCurrentConversationName = () => {
if (!selectedConversationId.value) {
return '新对话'
}
const conversation = conversations.value.find((c) => c.id === selectedConversationId.value)
return conversation ? conversation.name : '未命名对话'
}
// 滚动到底部
const scrollToBottom = async () => {
await nextTick()
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
}
}
// 生成唯一ID
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
// 处理回车键发送消息
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendMessage()
}
}
// 发送消息
const sendMessage = async () => {
const message = messageInput.value.trim()
if (!message || isLoading.value) return
// 添加用户消息
const userMessage: Message = {
id: generateId(),
type: 'user',
content: message,
timestamp: new Date(),
}
messages.value.push(userMessage)
messageInput.value = ''
// 创建助手消息占位符
const assistantMessage: Message = {
id: generateId(),
type: 'assistant',
content: '',
timestamp: new Date(),
isStreaming: true,
}
messages.value.push(assistantMessage)
isLoading.value = true
await scrollToBottom()
// 创建AbortController用于取消请求
streamingAbortController.value = new AbortController()
try {
const response = await http.post(`${apiBaseUrl}/analysis/chat-messages`, {
message: message,
...(selectedConversationId.value && { conversation_id: selectedConversationId.value }),
})
console.log('响应状态:', response.status, response.ok) // 调试日志
console.log('响应头:', response.headers) // 调试日志
if (!response.ok) {
// 处理错误响应
try {
const errorData = await response.json()
assistantMessage.content = `错误: ${errorData.detail || '请求失败'}`
} catch {
assistantMessage.content = `错误: 请求失败,状态码: ${response.status}`
}
assistantMessage.isStreaming = false
return
}
const reader = response.body?.getReader()
if (!reader) {
throw new Error('无法获取响应流')
}
console.log('开始读取流式数据...') // 调试日志
const decoder = new TextDecoder()
let buffer = ''
while (true) {
// 检查是否被中止
if (streamingAbortController.value?.signal.aborted) {
break
}
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) continue
try {
const jsonStr = line.slice(6)
console.log('接收到数据:', jsonStr) // 调试日志
const data = JSON.parse(jsonStr)
console.log('解析后的数据:', data) // 调试日志
switch (data.event) {
case 'workflow_started':
// 提取task_id
if (data.task_id) {
currentTaskId.value = data.task_id
console.log('获取到task_id:', data.task_id)
}
break
case 'node_started':
break
case 'node_finished':
break
case 'node_failed':
break
case 'text_chunk':
if (data.data && data.data.text) {
console.log('添加文本块:', data.data.text) // 调试日志
assistantMessage.content += data.data.text
// 强制触发响应式更新
messages.value = [...messages.value]
await nextTick()
await scrollToBottom()
}
break
case 'message':
// 处理新的消息格式
if (data.answer) {
console.log('添加消息内容:', data.answer) // 调试日志
assistantMessage.content += data.answer
// 强制触发响应式更新
messages.value = [...messages.value]
await nextTick()
await scrollToBottom()
}
break
case 'workflow_finished':
if (data.data && data.data.outputs && data.data.outputs.text) {
if (assistantMessage.content !== data.data.outputs.text) {
assistantMessage.content = data.data.outputs.text
await scrollToBottom()
}
}
assistantMessage.isStreaming = false
// 清理task_id
currentTaskId.value = ''
break
case 'error':
const errorMessage = data.error || '未知错误'
assistantMessage.content = `错误: ${errorMessage}`
assistantMessage.isStreaming = false
// 清理task_id
currentTaskId.value = ''
await scrollToBottom()
break
default:
console.log('未处理的事件类型:', data.event, data) // 调试日志
break
}
} catch (error) {
console.error('解析响应数据出错:', error, '原始数据:', line)
assistantMessage.content = '错误: 解析响应数据时出错,请稍后重试'
assistantMessage.isStreaming = false
currentTaskId.value = ''
}
}
}
// 确保消息状态正确设置
if (assistantMessage.isStreaming) {
assistantMessage.isStreaming = false
}
} catch (error) {
console.error('发送消息失败:', error)
assistantMessage.content = '错误: 发送消息失败,请稍后重试'
assistantMessage.isStreaming = false
} finally {
isLoading.value = false
currentTaskId.value = ''
streamingAbortController.value = null
await scrollToBottom()
}
}
// 复制消息内容
const copyMessage = async (content: string) => {
try {
await navigator.clipboard.writeText(content)
} catch (error) {
console.error('复制失败:', error)
}
}
// 解析markdown内容
const parseMarkdown = (content: string) => {
if (!content || content.startsWith('错误:')) {
return content
}
try {
// 对于可能不完整的内容,先进行一些预处理
const processedContent = content
// 如果内容以不完整的代码块开始暂时不处理markdown
if (
processedContent.includes('```') &&
(processedContent.match(/```/g) || []).length % 2 !== 0
) {
// 代码块未闭合,显示原始文本但保留换行
return processedContent.replace(/\n/g, '<br>')
}
// 处理markdown内容
let html = marked(processedContent) as string
// 将表格包装在div中以提供更好的滚动支持
html = html.replace(/<table>/g, '<div class="table-container"><table>')
html = html.replace(/<\/table>/g, '</table></div>')
return html
} catch (error) {
// 如果markdown解析失败比如内容不完整返回原始文本
console.warn('Markdown解析失败返回原始文本:', error)
return content.replace(/\n/g, '<br>')
}
}
// 格式化时间
const formatTime = (date: Date) => {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
})
}
// 发送示例消息
const sendExampleMessage = async (message: string) => {
messageInput.value = message
await sendMessage()
}
// 切换下拉会话列表
const toggleConversationDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value
}
// 重命名会话
const startRenaming = (conversation: Conversation) => {
conversationToRename.value = conversation
newConversationName.value = conversation.name
showRenameDialog.value = true
// 下一个tick后聚焦输入框
nextTick(() => {
renameInputRef.value?.focus()
renameInputRef.value?.select()
})
}
// 取消重命名
const cancelRename = () => {
showRenameDialog.value = false
conversationToRename.value = null
newConversationName.value = ''
}
// 确认重命名
const confirmRename = () => {
if (!conversationToRename.value || !newConversationName.value.trim()) return
const trimmedName = newConversationName.value.trim()
if (trimmedName === conversationToRename.value.name) {
cancelRename()
return
}
renameConversation(conversationToRename.value.id, trimmedName)
cancelRename()
}
// 重命名会话API调用
const renameConversation = async (conversationId: string, newName: string) => {
try {
const response = await http.post(
`${apiBaseUrl}/analysis/conversations/${conversationId}/name`,
{
name: newName,
},
)
if (response.ok) {
// 更新本地会话列表
const conversationIndex = conversations.value.findIndex((c) => c.id === conversationId)
if (conversationIndex !== -1) {
conversations.value[conversationIndex].name = newName
}
} else {
console.error('重命名会话失败')
alert('重命名失败,请稍后重试')
}
} catch (error) {
console.error('重命名会话失败:', error)
alert('重命名失败,请稍后重试')
}
}
// 删除会话
const deleteConversation = async (conversationId: string) => {
const conversation = conversations.value.find((c) => c.id === conversationId)
if (!conversation) return
conversationToDelete.value = conversation
showDeleteDialog.value = true
}
// 取消删除
const cancelDelete = () => {
showDeleteDialog.value = false
conversationToDelete.value = null
}
// 确认删除
const confirmDelete = async () => {
if (!conversationToDelete.value) return
const conversationId = conversationToDelete.value.id
try {
// 使用createAuthenticatedFetch发送DELETE请求
const { createAuthenticatedFetch } = await import('../services/api')
const authenticatedFetch = createAuthenticatedFetch()
const response = await authenticatedFetch(
`${apiBaseUrl}/analysis/conversations/${conversationId}`,
{
method: 'DELETE',
},
)
if (response.ok) {
// 从本地会话列表中移除
conversations.value = conversations.value.filter((c) => c.id !== conversationId)
// 如果删除的是当前选中的会话,切换到新对话或第一个会话
if (selectedConversationId.value === conversationId) {
if (conversations.value.length > 0) {
selectedConversationId.value = conversations.value[0].id
await fetchConversationMessages(conversations.value[0].id)
} else {
selectedConversationId.value = ''
messages.value = []
}
}
} else {
console.error('删除会话失败')
alert('删除失败,请稍后重试')
}
} catch (error) {
console.error('删除会话失败:', error)
alert('删除失败,请稍后重试')
}
cancelDelete()
}
// 停止流式输出
const stopStreaming = async () => {
if (!currentTaskId.value) return
try {
const response = await http.post(`${apiBaseUrl}/analysis/stop_streaming`, {
task_id: currentTaskId.value,
})
if (response.ok) {
console.log('停止流式输出成功')
} else {
console.error('停止流式输出失败')
}
} catch (error) {
console.error('停止流式输出失败:', error)
}
// 无论是否成功,都清理状态
currentTaskId.value = ''
if (streamingAbortController.value) {
streamingAbortController.value.abort()
streamingAbortController.value = null
}
// 停止当前流式消息
const lastMessage = messages.value[messages.value.length - 1]
if (lastMessage && lastMessage.isStreaming) {
lastMessage.isStreaming = false
}
isLoading.value = false
}
</script>
<template>
<div class="chat-agent-view">
<div class="chat-container">
<!-- 会话标题和选择器 -->
<div class="conversation-header">
<div class="conversation-title-container" @click="toggleConversationDropdown">
<h2 class="conversation-title">
{{ getCurrentConversationName() }}
</h2>
<svg
class="dropdown-icon"
:class="{ active: isDropdownOpen }"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
<!-- 下拉会话列表 -->
<transition name="dropdown" appear>
<div v-if="isDropdownOpen" class="conversation-dropdown">
<div class="dropdown-content">
<!-- 新对话选项 -->
<button
class="dropdown-item new-conversation"
@click="selectNewConversation"
:disabled="isLoadingHistory"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
<span>新对话</span>
</button>
<!-- 加载状态 -->
<div v-if="isLoadingConversations" class="dropdown-loading">
<span>加载中...</span>
</div>
<!-- 会话列表 -->
<template v-else>
<div
v-for="conversation in conversations"
:key="conversation.id"
class="dropdown-item"
:class="{ active: selectedConversationId === conversation.id }"
>
<div
class="conversation-info"
@click="selectConversationFromDropdown(conversation.id)"
>
<span class="conversation-name">{{ conversation.name }}</span>
</div>
<div class="conversation-actions">
<button
class="action-btn rename-btn"
@click.stop="startRenaming(conversation)"
title="重命名"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button
class="action-btn delete-btn"
@click.stop="deleteConversation(conversation.id)"
title="删除"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,6 5,6 21,6"></polyline>
<path
d="m19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2"
></path>
</svg>
</button>
</div>
</div>
</template>
<!-- 历史加载状态 -->
<div v-if="isLoadingHistory" class="dropdown-loading">
<span>加载对话历史中...</span>
</div>
</div>
</div>
</transition>
</div>
<!-- 对话区域 -->
<div class="chat-messages" ref="chatContainer">
<!-- 欢迎消息 -->
<div v-if="messages.length === 0" class="welcome-message">
<div class="welcome-content">
<div class="welcome-icon">
<svg viewBox="0 0 24 24" fill="currentColor" width="48" height="48">
<path
d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 7.5,13M16.5,13A2.5,2.5 0 0,0 14,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13Z"
/>
</svg>
</div>
<h3>AI交易智能体</h3>
<p>为您提供市场分析、交易建议和投资策略</p>
<!-- 积分消耗提示 -->
<div class="points-notice">
<div class="points-notice-icon">💰</div>
<div class="points-notice-text">
<strong>积分消耗提示:</strong>每次分析消耗
<span class="points-amount">20 积分</span>
</div>
</div>
<div class="example-questions">
<p class="example-title">快速点击分析:</p>
<div class="example-grid">
<button
class="example-question"
@click="sendExampleMessage('分析BTC行情')"
:disabled="isLoading"
>
<span class="example-icon">₿</span>
<span>分析BTC行情</span>
</button>
<button
class="example-question"
@click="sendExampleMessage('分析ETH行情')"
:disabled="isLoading"
>
<span class="example-icon">Ξ</span>
<span>分析ETH行情</span>
</button>
<button
class="example-question"
@click="sendExampleMessage('分析贵州茅台股票')"
:disabled="isLoading"
>
<span class="example-icon">📈</span>
<span>分析贵州茅台股票</span>
</button>
<button
class="example-question"
@click="sendExampleMessage('分析中国平安股票')"
:disabled="isLoading"
>
<span class="example-icon">🏦</span>
<span>分析中国平安股票</span>
</button>
<button
class="example-question"
@click="sendExampleMessage('分析苹果公司(AAPL)股票')"
:disabled="isLoading"
>
<span class="example-icon">🍎</span>
<span>分析苹果公司股票</span>
</button>
<button
class="example-question"
@click="sendExampleMessage('分析黄金价格走势')"
:disabled="isLoading"
>
<span class="example-icon">🪙</span>
<span>分析黄金价格走势</span>
</button>
</div>
</div>
</div>
</div>
<!-- 消息列表 -->
<div v-for="message in messages" :key="message.id" class="message-wrapper">
<div
class="message"
:class="{
'user-message': message.type === 'user',
'assistant-message': message.type === 'assistant',
}"
>
<div class="message-avatar">
<div v-if="message.type === 'user'" class="user-avatar">用</div>
<div v-else class="assistant-avatar">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
<path
d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 7.5,13M16.5,13A2.5,2.5 0 0,0 14,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13Z"
/>
</svg>
</div>
</div>
<div class="message-content">
<div class="message-header">
<span class="message-sender">{{
message.type === 'user' ? '您' : 'AI 交易智能体'
}}</span>
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
</div>
<div class="message-body">
<div
v-if="message.type === 'user' || message.content.startsWith('错误:')"
class="message-text"
:class="{ 'error-message': message.content.startsWith('错误:') }"
>
{{ message.content }}
</div>
<div
v-else
class="message-text markdown-content"
v-html="parseMarkdown(message.content)"
></div>
<div v-if="message.isStreaming" class="streaming-indicator">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
<div class="message-actions">
<button
v-if="!message.isStreaming"
class="action-btn copy-btn"
@click="copyMessage(message.content)"
:disabled="!message.content"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input">
<div class="input-container">
<textarea
v-model="messageInput"
class="message-input"
placeholder="输入您的问题例如分析BTC、分析苹果股票、分析黄金价格"
@keydown="handleKeydown"
:disabled="isLoading"
rows="1"
></textarea>
<button
v-if="!isLoading"
class="send-button"
@click="sendMessage"
:disabled="!messageInput.trim()"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22,2 15,22 11,13 2,9"></polygon>
</svg>
</button>
<button v-else class="stop-button" @click="stopStreaming" title="停止生成">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none">
<rect x="6" y="6" width="12" height="12" rx="2" ry="2"></rect>
</svg>
</button>
</div>
</div>
</div>
<!-- 删除确认弹框 -->
<div v-if="showDeleteDialog" class="dialog-overlay" @click.self="cancelDelete">
<div class="dialog-content">
<div class="dialog-header">
<h3>删除会话</h3>
<button class="dialog-close" @click="cancelDelete">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="dialog-body">
<p>确定要删除会话"{{ conversationToDelete?.name }}"吗?</p>
<p class="warning-text">删除后将无法恢复。</p>
</div>
<div class="dialog-footer">
<button class="btn btn-secondary" @click="cancelDelete">取消</button>
<button class="btn btn-danger" @click="confirmDelete">删除</button>
</div>
</div>
</div>
<!-- 重命名弹框 -->
<div v-if="showRenameDialog" class="dialog-overlay" @click.self="cancelRename">
<div class="dialog-content">
<div class="dialog-header">
<h3>重命名会话</h3>
<button class="dialog-close" @click="cancelRename">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="dialog-body">
<label class="input-label">新的会话名称</label>
<input
v-model="newConversationName"
type="text"
class="rename-input"
placeholder="请输入会话名称"
@keydown.enter="confirmRename"
@keydown.esc="cancelRename"
ref="renameInputRef"
/>
</div>
<div class="dialog-footer">
<button class="btn btn-secondary" @click="cancelRename">取消</button>
<button
class="btn btn-primary"
@click="confirmRename"
:disabled="
!newConversationName.trim() ||
newConversationName.trim() === conversationToRename?.name
"
>
确定
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.chat-agent-view {
height: 100vh;
height: 100dvh; /* 动态视口高度,适配移动端 */
background-color: var(--color-bg-primary);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.chat-container {
max-width: 800px;
margin: 0 auto;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.conversation-header {
padding: 0.6rem 0.75rem;
height: 3rem;
background-color: var(--color-bg-primary);
position: relative;
}
.conversation-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 12px;
background: linear-gradient(to bottom, var(--color-bg-primary), transparent);
pointer-events: none;
z-index: 1;
}
.conversation-title-container {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
cursor: pointer;
position: relative;
z-index: 2;
}
.conversation-title {
margin: 0;
font-size: 0.85rem;
font-weight: 500;
color: var(--color-text-primary);
text-align: center;
}
.dropdown-icon {
width: 14px;
height: 14px;
color: var(--color-text-secondary);
transition: transform 0.3s ease;
}
.dropdown-icon.active {
transform: rotate(180deg);
}
.conversation-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-width: 500px;
margin: 0 auto;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--border-radius) var(--border-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 120;
}
.dropdown-content {
padding: 1rem;
}
.dropdown-item {
width: 100%;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
margin-bottom: 0.75rem;
border: 1px solid transparent;
border-radius: var(--border-radius);
background-color: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.dropdown-item:hover:not(:disabled) {
border-color: var(--color-accent);
background-color: rgba(51, 85, 255, 0.05);
color: var(--color-accent);
}
.dropdown-item:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.dropdown-item svg {
width: 16px;
height: 16px;
}
.conversation-info {
flex: 1;
cursor: pointer;
}
.conversation-name {
font-size: 0.9rem;
font-weight: 500;
color: var(--color-text-primary);
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dropdown-item.active .conversation-name {
color: var(--color-accent);
}
.conversation-actions {
display: flex;
gap: 0.25rem;
opacity: 0;
transition: opacity 0.2s ease;
}
.dropdown-item:hover .conversation-actions {
opacity: 1;
}
.conversation-actions .action-btn {
padding: 0.25rem;
border: none;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.conversation-actions .action-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
color: var(--color-text-primary);
}
.conversation-actions .action-btn svg {
width: 14px;
height: 14px;
}
.conversation-actions .delete-btn:hover {
background-color: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.dropdown-loading {
text-align: center;
padding: 1rem;
color: var(--color-text-secondary);
font-size: 0.85rem;
}
/* 下拉框动画 */
.dropdown-enter-active {
transition: all 0.3s ease-out;
}
.dropdown-leave-active {
transition: all 0.2s ease-in;
}
.dropdown-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.dropdown-enter-to {
opacity: 1;
transform: translateY(0);
}
.dropdown-leave-from {
opacity: 1;
transform: translateY(0);
}
.dropdown-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 2rem;
display: flex;
flex-direction: column;
padding-bottom: 1rem; /* 为输入框留出空间 */
}
.welcome-message {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
text-align: center;
min-height: 0;
overflow-y: auto;
}
.welcome-content {
max-width: 500px;
padding: 2rem 1rem;
width: 100%;
}
.welcome-icon {
font-size: 3rem;
margin-bottom: 1.5rem;
color: var(--color-accent);
}
.welcome-content h3 {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
}
.welcome-content p {
color: var(--color-text-secondary);
margin-bottom: 1.5rem;
line-height: 1.6;
}
/* 移动端积分提示样式 */
.points-notice {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
background-color: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 12px;
margin-bottom: 2rem;
max-width: 100%;
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.1);
}
.points-notice-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.points-notice-text {
font-size: 0.9rem;
color: var(--color-text-primary);
line-height: 1.4;
}
.points-notice-text strong {
font-weight: 600;
color: var(--color-text-primary);
}
.points-amount {
color: #f59e0b;
font-weight: 600;
font-size: 1em;
}
.example-questions {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.example-title {
font-size: 0.9rem;
color: var(--color-text-secondary);
margin-bottom: 0.5rem;
}
.example-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
width: 100%;
max-width: 600px;
}
.example-question {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 1.5rem;
border: 1px solid var(--color-border);
border-radius: 12px;
background: var(--color-bg-secondary);
color: var(--color-text-primary);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
text-align: left;
min-height: 60px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.example-question:hover:not(:disabled) {
border-color: var(--color-accent);
background: var(--color-bg-primary);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.example-question:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.example-icon {
font-size: 1.1rem;
flex-shrink: 0;
width: 24px;
text-align: center;
}
.message-wrapper {
width: 100%;
display: flex;
margin-bottom: 1.5rem;
}
.message {
display: flex;
gap: 1rem;
max-width: 80%;
width: fit-content;
}
.user-message {
flex-direction: row-reverse;
margin-left: auto;
}
.assistant-message {
margin-right: auto;
}
.message-avatar {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-avatar {
background: linear-gradient(135deg, var(--color-accent), #667eea);
color: white;
font-weight: 600;
font-size: 1.1rem;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.3);
}
.assistant-avatar {
background: linear-gradient(135deg, var(--color-bg-secondary), #f8fafc);
color: var(--color-text-primary);
border: 2px solid var(--color-border);
font-weight: 600;
font-size: 1.1rem;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.message-content {
flex: 1;
min-width: 80px;
max-width: none;
width: fit-content;
}
.user-message .message-content {
text-align: right;
}
.message-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.user-message .message-header {
justify-content: flex-end;
}
.message-sender {
font-size: 0.85rem;
font-weight: 500;
color: var(--color-text-primary);
}
.message-time {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.message-body {
background-color: var(--color-bg-secondary);
border-radius: 18px;
padding: 1rem 1.25rem;
position: relative;
width: fit-content;
min-width: 100px;
max-width: 600px;
word-wrap: break-word;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.user-message .message-body {
background: linear-gradient(135deg, var(--color-accent), #667eea);
color: white;
border: none;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
}
.message-text {
line-height: 1.6;
word-break: break-word;
}
.user-message .message-text {
color: white;
}
.error-message {
color: #ff3333 !important;
background-color: rgba(255, 51, 51, 0.1) !important;
border: 1px solid rgba(255, 51, 51, 0.2);
}
.streaming-indicator {
margin-top: 0.5rem;
}
.typing-dots {
display: flex;
gap: 0.25rem;
align-items: center;
}
.typing-dots span {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--color-text-secondary);
animation: typing 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing {
0%,
80%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1);
}
}
.message-actions {
display: flex;
justify-content: flex-end;
margin-top: 0.5rem;
gap: 0.5rem;
}
.user-message .message-actions {
justify-content: flex-start;
}
.action-btn {
padding: 0.25rem;
border: none;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
opacity: 0.6;
}
.action-btn:hover:not(:disabled) {
opacity: 1;
background-color: rgba(0, 0, 0, 0.1);
}
.action-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.action-btn svg {
width: 14px;
height: 14px;
}
.user-message .action-btn {
color: rgba(255, 255, 255, 0.8);
}
.user-message .action-btn:hover:not(:disabled) {
color: white;
background-color: rgba(255, 255, 255, 0.2);
}
.chat-input {
padding: 1rem;
background-color: var(--color-bg-primary);
border-top: none;
flex-shrink: 0;
position: sticky;
bottom: 0;
z-index: 10;
}
.input-container {
display: flex;
gap: 1rem;
align-items: center;
max-width: 100%;
background-color: var(--color-bg-primary);
border-radius: var(--border-radius);
padding: 0.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border: 1px solid var(--color-border);
}
.message-input {
flex: 1;
/* height: 44px; */
max-height: 120px;
padding: 0 1rem;
border: none;
border-radius: var(--border-radius);
background-color: transparent;
color: var(--color-text-primary);
font-size: 0.95rem;
line-height: 1.5;
resize: none;
outline: none;
transition: all 0.2s ease;
}
.message-input:focus {
outline: none;
}
.input-container:focus-within {
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
border-color: var(--color-accent);
}
.message-input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.send-button {
width: 44px;
height: 44px;
border: none;
border-radius: var(--border-radius);
background-color: var(--color-accent);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.send-button:hover:not(:disabled) {
background-color: var(--color-accent-hover);
}
.send-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.send-button svg {
width: 20px;
height: 20px;
}
.stop-button {
width: 44px;
height: 44px;
border: none;
border-radius: var(--border-radius);
background-color: rgba(239, 68, 68, 0.1);
color: #ef4444;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.stop-button:hover:not(:disabled) {
background-color: rgba(239, 68, 68, 0.2);
color: #dc2626;
}
.stop-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.stop-button svg {
width: 20px;
height: 20px;
}
.button-loader {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* 响应式设计 */
@media (max-width: 768px) {
.chat-agent-view {
height: 100vh;
height: 100dvh;
min-height: -webkit-fill-available;
}
.chat-messages {
padding: 1rem;
padding-bottom: 100px; /* 为固定输入框留出空间 */
}
.welcome-message {
height: auto;
min-height: calc(100vh - 160px); /* 减去header和输入框的高度 */
align-items: flex-start;
padding-top: 2rem;
}
.welcome-content {
padding: 0 1rem 2rem 1rem;
max-width: 100%;
}
.welcome-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.welcome-content h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
}
.welcome-content p {
margin-bottom: 1.5rem;
font-size: 0.9rem;
}
/* 移动端积分提示样式 */
.points-notice {
padding: 0.75rem 1rem;
margin-bottom: 1.5rem;
gap: 0.5rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.points-notice-icon {
font-size: 1.25rem;
}
.points-notice-text {
font-size: 0.85rem;
}
.example-questions {
gap: 1rem;
}
.example-title {
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
.example-grid {
grid-template-columns: 1fr;
max-width: 100%;
gap: 0.75rem;
padding: 0;
}
.example-question {
padding: 0.75rem 1rem;
font-size: 0.85rem;
min-height: 48px;
margin: 0 0.5rem;
text-align: center;
justify-content: center;
}
.example-icon {
font-size: 1.1rem;
}
.chat-input {
padding: 0.75rem 1rem;
background-color: var(--color-bg-primary);
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
border-top: 1px solid var(--color-border);
}
.input-container {
gap: 0.75rem;
padding: 0.375rem;
border-radius: 24px;
align-items: center;
display: flex;
flex-direction: row;
margin: 0 auto;
max-width: 800px;
}
.message-input {
height: 36px !important;
padding: 0 0.75rem;
font-size: 0.9rem;
border-radius: 20px;
flex: 1;
min-width: 0;
box-sizing: border-box;
resize: none;
overflow: hidden;
line-height: 36px;
}
.send-button {
width: 36px;
height: 36px;
border-radius: 18px;
flex-shrink: 0;
}
.send-button svg {
width: 16px;
height: 16px;
}
.stop-button {
width: 36px;
height: 36px;
border-radius: 18px;
flex-shrink: 0;
}
.stop-button svg {
width: 16px;
height: 16px;
}
.message {
gap: 0.75rem;
max-width: 90%;
}
.message-avatar {
width: 40px;
height: 40px;
font-size: 1.1rem;
}
.user-avatar {
font-size: 1rem;
}
.assistant-avatar {
font-size: 1rem;
}
.message-content {
min-width: 80px;
}
.message-body {
max-width: 400px;
min-width: 80px;
}
.title {
font-size: 1.25rem;
}
.description {
font-size: 0.85rem;
}
.welcome-icon {
font-size: 2.5rem;
}
.welcome-content h3 {
font-size: 1.25rem;
}
}
@media (max-width: 480px) {
.chat-agent-view {
height: 100vh;
height: 100dvh;
min-height: -webkit-fill-available;
}
.chat-messages {
padding: 1rem 0.75rem 100px 0.75rem; /* 为固定输入框留出空间 */
}
.welcome-message {
height: auto;
min-height: calc(100vh - 180px); /* 减去header和输入框的高度 */
align-items: flex-start;
padding-top: 1rem;
}
.welcome-content {
padding: 0 0.5rem 2rem 0.5rem;
max-width: 100%;
}
.welcome-icon {
font-size: 2rem;
margin-bottom: 1rem;
}
.welcome-content h3 {
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.welcome-content p {
margin-bottom: 1.25rem;
font-size: 0.85rem;
line-height: 1.5;
}
/* 小屏幕积分提示样式 */
.points-notice {
padding: 0.6rem 0.75rem;
margin-bottom: 1.25rem;
gap: 0.4rem;
margin-left: 0;
margin-right: 0;
}
.points-notice-icon {
font-size: 1.1rem;
}
.points-notice-text {
font-size: 0.8rem;
}
.example-questions {
gap: 0.75rem;
}
.example-title {
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.example-grid {
grid-template-columns: 1fr;
max-width: 100%;
gap: 0.5rem;
padding: 0;
}
.example-question {
padding: 0.6rem 0.75rem;
font-size: 0.8rem;
min-height: 44px;
margin: 0;
text-align: center;
justify-content: center;
border-radius: 8px;
}
.example-icon {
font-size: 1rem;
}
.conversation-header {
padding: 0.6rem 0.75rem;
height: 3rem;
}
.conversation-header::after {
height: 12px;
}
.conversation-title {
font-size: 0.85rem;
}
.dropdown-icon {
width: 14px;
height: 14px;
}
/* 移动端让会话操作按钮始终可见 */
.conversation-actions {
opacity: 1;
}
.conversation-actions .action-btn {
padding: 0.5rem;
}
.conversation-actions .action-btn svg {
width: 16px;
height: 16px;
}
}
</style>
<style>
/* Markdown 样式 - 继承自 UniversalAnalysisView */
.chat-agent-view .markdown-content {
white-space: normal !important;
}
.chat-agent-view .markdown-content h1,
.chat-agent-view .markdown-content h2,
.chat-agent-view .markdown-content h3,
.chat-agent-view .markdown-content h4,
.chat-agent-view .markdown-content h5,
.chat-agent-view .markdown-content h6 {
margin-top: 1rem;
margin-bottom: 0.75rem;
font-weight: 600;
line-height: 1.25;
color: var(--color-text-primary);
font-size: 1.1rem;
}
.chat-agent-view .markdown-content h1 {
border-bottom: 1px solid var(--color-border);
padding-bottom: 0.3em;
}
.chat-agent-view .markdown-content h2 {
border-bottom: 1px solid var(--color-border);
padding-bottom: 0.3em;
}
.chat-agent-view .markdown-content p {
margin-top: 0;
margin-bottom: 1rem;
line-height: 1.6;
}
.chat-agent-view .markdown-content ul,
.chat-agent-view .markdown-content ol {
margin-top: 0;
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.chat-agent-view .markdown-content li {
margin-bottom: 0.25rem;
line-height: 1.5;
}
.chat-agent-view .markdown-content li p {
margin-bottom: 0.5rem;
}
.chat-agent-view .markdown-content blockquote {
padding: 0.5rem 1rem;
color: var(--color-text-secondary);
border-left: 0.25rem solid var(--color-border);
margin: 0 0 1rem;
}
.chat-agent-view .markdown-content pre {
margin-top: 0;
margin-bottom: 1rem;
padding: 0.75rem;
overflow: auto;
font-size: 0.85rem;
line-height: 1.45;
background-color: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
}
.chat-agent-view .markdown-content code {
font-family:
SFMono-Regular,
Consolas,
Liberation Mono,
Menlo,
monospace;
font-size: 0.85rem;
padding: 0.2em 0.4em;
margin: 0;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
.chat-agent-view .markdown-content pre code {
padding: 0;
background-color: transparent;
}
.chat-agent-view .markdown-content .table-container {
width: 100%;
overflow-x: auto;
margin-bottom: 1rem;
}
.chat-agent-view .markdown-content table {
width: 100%;
margin-top: 0;
margin-bottom: 0;
border-spacing: 0;
border-collapse: collapse;
border: 1px solid var(--color-border);
font-size: 0.9rem;
}
.chat-agent-view .markdown-content table th,
.chat-agent-view .markdown-content table td {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
text-align: left;
vertical-align: top;
word-break: break-word;
}
.chat-agent-view .markdown-content table th {
font-weight: 600;
background-color: var(--color-bg-secondary);
}
.chat-agent-view .markdown-content table tr:nth-child(2n) {
background-color: rgba(0, 0, 0, 0.02);
}
.chat-agent-view .markdown-content a {
color: var(--color-accent);
text-decoration: none;
}
.chat-agent-view .markdown-content a:hover {
text-decoration: underline;
}
.chat-agent-view .markdown-content img {
max-width: 100%;
height: auto;
display: block;
margin: 0.5rem auto;
}
.chat-agent-view .markdown-content hr {
height: 0.25em;
padding: 0;
margin: 1rem 0;
background-color: var(--color-border);
border: 0;
}
/* 用户消息中的markdown样式调整 */
.user-message .markdown-content h1,
.user-message .markdown-content h2,
.user-message .markdown-content h3,
.user-message .markdown-content h4,
.user-message .markdown-content h5,
.user-message .markdown-content h6 {
color: white;
}
.user-message .markdown-content a {
color: rgba(255, 255, 255, 0.9);
}
.user-message .markdown-content code {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.user-message .markdown-content pre {
background-color: rgba(255, 255, 255, 0.1);
}
.user-message .markdown-content blockquote {
border-left-color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.9);
}
/* 自定义弹框样式 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.dialog-content {
background-color: var(--color-bg-primary);
border-radius: 12px;
min-width: 400px;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
border: 1px solid var(--color-border);
overflow: hidden;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
}
.dialog-header h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text-primary);
}
.dialog-close {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background-color: transparent;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.dialog-close:hover {
background-color: rgba(0, 0, 0, 0.1);
color: var(--color-text-primary);
}
.dialog-close svg {
width: 16px;
height: 16px;
}
.dialog-body {
padding: 1.5rem;
}
.dialog-body p {
margin: 0 0 0.5rem 0;
color: var(--color-text-primary);
line-height: 1.5;
}
.warning-text {
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.input-label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
font-weight: 500;
color: var(--color-text-primary);
}
.rename-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
font-size: 0.95rem;
transition: all 0.2s ease;
box-sizing: border-box;
}
.rename-input:focus {
outline: none;
border-color: var(--color-accent);
background-color: var(--color-bg-primary);
}
.dialog-footer {
display: flex;
gap: 0.75rem;
padding: 1.5rem;
border-top: 1px solid var(--color-border);
background-color: var(--color-bg-secondary);
justify-content: flex-end;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
min-width: 80px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background-color: var(--color-accent);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--color-accent-hover);
}
.btn-secondary {
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
border-color: var(--color-text-secondary);
}
.btn-danger {
background-color: #ef4444;
color: white;
}
.btn-danger:hover:not(:disabled) {
background-color: #dc2626;
}
/* 移动端弹框适配 */
@media (max-width: 480px) {
.dialog-content {
min-width: auto;
width: 95%;
margin: 1rem;
}
.dialog-header,
.dialog-body,
.dialog-footer {
padding: 1rem;
}
.dialog-footer {
flex-direction: column-reverse;
}
.btn {
width: 100%;
}
}
</style>