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:
2026-03-21 22:13:12 +08:00
parent a27736a832
commit b024a2bcb5
25 changed files with 2628 additions and 1656 deletions

View 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,
}
}