refactor(frontend): move views into app and pages structure
Reorganize the frontend around app-level routing and page modules so the runtime and feature screens share a clearer navigation and composition layout for future work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
210
frontend/src/pages/chat/composables/useChatView.ts
Normal file
210
frontend/src/pages/chat/composables/useChatView.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { conversationApi, type Message } from '@/api/conversation'
|
||||
import { documentApi } from '@/api/document'
|
||||
|
||||
export interface SelectedFile {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
size: number
|
||||
}
|
||||
|
||||
interface MessageWithAttachments extends Message {
|
||||
attachments?: SelectedFile[]
|
||||
}
|
||||
|
||||
export function useChatView() {
|
||||
const store = useConversationStore()
|
||||
const inputMessage = ref('')
|
||||
const isSending = ref(false)
|
||||
const chatContainer = ref<HTMLElement>()
|
||||
const inputRef = ref<HTMLTextAreaElement>()
|
||||
const isTyping = ref(false)
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
const showEmojiPicker = ref(false)
|
||||
const selectedFiles = ref<SelectedFile[]>([])
|
||||
|
||||
async function sendMessage() {
|
||||
if (!inputMessage.value.trim() || isSending.value) return
|
||||
|
||||
isSending.value = true
|
||||
isTyping.value = true
|
||||
const text = inputMessage.value.trim()
|
||||
const attachments = [...selectedFiles.value]
|
||||
inputMessage.value = ''
|
||||
|
||||
store.addMessage({
|
||||
id: `temp-${Date.now()}`,
|
||||
role: 'user',
|
||||
content: text,
|
||||
created_at: new Date().toISOString(),
|
||||
attachments,
|
||||
} as MessageWithAttachments)
|
||||
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
|
||||
try {
|
||||
const response = await conversationApi.chat(text, store.currentConversationId || undefined, attachments.map((file) => file.id))
|
||||
selectedFiles.value = []
|
||||
isTyping.value = false
|
||||
store.addMessage({
|
||||
id: response.data.message_id,
|
||||
role: 'assistant',
|
||||
content: response.data.content,
|
||||
model: response.data.agent_name,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
if (!store.currentConversationId) {
|
||||
store.setCurrentConversation(response.data.conversation_id)
|
||||
await loadConversations()
|
||||
}
|
||||
} catch (error) {
|
||||
isTyping.value = false
|
||||
console.error('发送失败:', error)
|
||||
store.addMessage({
|
||||
id: `err-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: '抱歉,连接失败。请检查服务状态。',
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
isSending.value = false
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
async function loadConversations() {
|
||||
try {
|
||||
const response = await conversationApi.list()
|
||||
store.setConversations(response.data)
|
||||
} catch (error) {
|
||||
console.error('加载对话列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function selectConversation(id: string) {
|
||||
store.setCurrentConversation(id)
|
||||
store.setMessages([])
|
||||
try {
|
||||
const response = await conversationApi.getMessages(id)
|
||||
store.setMessages(response.data)
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function newConversation() {
|
||||
store.setCurrentConversation('')
|
||||
store.setMessages([])
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
|
||||
async function deleteConversation(id: string, event: Event) {
|
||||
event.stopPropagation()
|
||||
try {
|
||||
await conversationApi.delete(id)
|
||||
store.removeConversation(id)
|
||||
if (store.currentConversationId === id) {
|
||||
store.setCurrentConversation('')
|
||||
store.setMessages([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (chatContainer.value) {
|
||||
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function formatConvDate(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
|
||||
if (diff < 86400000) return '今天'
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
function autoResize(event: Event) {
|
||||
const element = event.target as HTMLTextAreaElement
|
||||
element.style.height = 'auto'
|
||||
element.style.height = `${Math.min(element.scrollHeight, 120)}px`
|
||||
}
|
||||
|
||||
async function handleFileSelect(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (!input.files?.length) return
|
||||
|
||||
for (const file of input.files) {
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
alert(`文件 ${file.name} 超过10MB限制`)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const response = await documentApi.upload(file)
|
||||
selectedFiles.value.push({
|
||||
id: response.data.id,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
alert(`文件 ${file.name} 上传失败`)
|
||||
}
|
||||
}
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function insertEmoji(emoji: string) {
|
||||
inputMessage.value += emoji
|
||||
showEmojiPicker.value = false
|
||||
}
|
||||
|
||||
function openFilePicker() {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConversations()
|
||||
inputRef.value?.focus()
|
||||
})
|
||||
|
||||
return {
|
||||
store,
|
||||
inputMessage,
|
||||
isSending,
|
||||
chatContainer,
|
||||
inputRef,
|
||||
isTyping,
|
||||
fileInputRef,
|
||||
showEmojiPicker,
|
||||
selectedFiles,
|
||||
sendMessage,
|
||||
selectConversation,
|
||||
newConversation,
|
||||
deleteConversation,
|
||||
formatTime,
|
||||
formatConvDate,
|
||||
autoResize,
|
||||
handleFileSelect,
|
||||
insertEmoji,
|
||||
openFilePicker,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user