feat: 首页改造成ChatGPT风格对话界面,调整各页面padding布局

This commit is contained in:
2026-03-10 10:49:24 +08:00
parent 8839ec244a
commit 40f99f0c49
12 changed files with 295 additions and 19 deletions

View File

@@ -29,7 +29,7 @@ export const MarkdownTabPage: React.FC<MarkdownTabPageProps> = ({
return (
<div
className="max-w-4xl mx-auto w-full"
className="w-full px-10 py-4"
style={{ zoom: zoom / 100 }}
>
<div className="h-full w-full">

View File

@@ -1,9 +1,5 @@
import { ChatLayout } from './components/ChatLayout'
export const HomePage = () => {
return (
<div className="flex items-center justify-center h-full">
<h1 className="text-3xl font-light text-gray-600 dark:text-gray-300">
</h1>
</div>
)
return <ChatLayout />
}

View File

@@ -0,0 +1,120 @@
import { Send } from 'lucide-react'
import { useChatStore, type ChatMessage } from '@/stores/chatStore'
import { useState, useRef, useEffect } from 'react'
const MessageItem = ({ message }: { message: ChatMessage }) => {
const isUser = message.role === 'user'
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`}>
<div
className={`flex gap-3 max-w-[80%] ${
isUser ? 'flex-row-reverse' : 'flex-row'
}`}
>
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
isUser ? 'bg-gray-600' : 'bg-green-600'
}`}>
{isUser ? (
<span className="text-white text-sm">👤</span>
) : (
<span className="text-white text-sm">🤖</span>
)}
</div>
<div
className={`px-4 py-2.5 rounded-lg text-sm leading-relaxed whitespace-pre-wrap ${
isUser
? 'bg-[#343541] text-white'
: 'bg-transparent text-gray-100'
}`}
>
{message.content}
</div>
</div>
</div>
)
}
export const ChatContent = () => {
const { currentChatId, chats, addMessage, updateChatTitle } = useChatStore()
const [input, setInput] = useState('')
const messagesEndRef = useRef<HTMLDivElement>(null)
const currentChat = chats.find((c) => c.id === currentChatId)
const messages = currentChat?.messages ?? []
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages.length])
const handleSend = () => {
if (!input.trim() || !currentChatId) return
const userMessage: ChatMessage = {
id: Math.random().toString(36).substring(2, 15),
role: 'user',
content: input.trim(),
timestamp: Date.now(),
}
addMessage(currentChatId, userMessage)
if (messages.length === 0) {
const title = input.trim().slice(0, 20) + (input.trim().length > 20 ? '...' : '')
updateChatTitle(currentChatId, title)
}
setInput('')
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
return (
<div className="flex-1 flex flex-col h-full bg-[#343541]">
<div className="flex-1 overflow-y-auto px-4 py-4">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full">
<h1 className="text-2xl font-light text-gray-400">
</h1>
</div>
) : (
<div className="max-w-3xl mx-auto pt-4">
{messages.map((msg) => (
<MessageItem key={msg.id} message={msg} />
))}
<div ref={messagesEndRef} />
</div>
)}
</div>
<div className="px-4 pb-4">
<div className="max-w-3xl mx-auto flex gap-2 items-end">
<div className="flex-1">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="发送消息..."
rows={1}
className="w-full bg-[#40414f] text-white rounded-lg px-4 py-3 resize-none focus:outline-none focus:ring-2 focus:ring-green-500 placeholder-gray-400 text-sm"
style={{ minHeight: '48px', maxHeight: '200px' }}
/>
</div>
<button
onClick={handleSend}
disabled={!input.trim()}
className="px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors shrink-0"
>
<Send size={18} className="text-white" />
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import { ChatSidebar } from './ChatSidebar'
import { ChatContent } from './ChatContent'
export const ChatLayout = () => {
return (
<div className="flex h-full w-full overflow-hidden">
<ChatSidebar />
<ChatContent />
</div>
)
}

View File

@@ -0,0 +1,53 @@
import { Plus, Trash2 } from 'lucide-react'
import { useChatStore } from '@/stores/chatStore'
export const ChatSidebar = () => {
const { chats, currentChatId, createChat, deleteChat, selectChat } = useChatStore()
const handleNewChat = () => {
createChat()
}
return (
<div className="w-[260px] h-full flex flex-col bg-[#262626] dark:bg-[#262626]">
<button
onClick={handleNewChat}
className="flex items-center gap-2 px-3 py-2.5 mx-2 mt-2 rounded-md border border-gray-600 hover:bg-gray-700 transition-colors text-sm"
>
<Plus size={16} />
<span></span>
</button>
<div className="flex-1 overflow-y-auto mt-3 px-2">
{chats.map((chat) => (
<div
key={chat.id}
onClick={() => selectChat(chat.id)}
className={`group flex items-center justify-between px-3 py-2 rounded-md cursor-pointer text-sm mb-1 transition-colors ${
currentChatId === chat.id
? 'bg-[#343541] text-white'
: 'text-gray-300 hover:bg-[#343541]/50'
}`}
>
<span className="truncate flex-1">{chat.title}</span>
<button
onClick={(e) => {
e.stopPropagation()
deleteChat(chat.id)
}}
className="opacity-0 group-hover:opacity-100 hover:text-red-400 transition-opacity p-1"
>
<Trash2 size={14} />
</button>
</div>
))}
{chats.length === 0 && (
<div className="text-gray-500 text-sm text-center py-8">
</div>
)}
</div>
</div>
)
}

View File

@@ -189,7 +189,7 @@ export const PyDemosPage: React.FC = () => {
className="h-full w-full overflow-y-auto"
onContextMenu={handleBackgroundContextMenu}
>
<div className="max-w-4xl mx-auto w-full p-6 min-h-full">
<div className="max-w-4xl mx-auto w-full px-14 py-12 pb-40 min-h-full">
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-8 flex items-center gap-2">
<Play className="w-8 h-8" />
Python Demo

View File

@@ -482,7 +482,7 @@ export const RemotePage: React.FC = () => {
}
return (
<div className="h-full flex flex-col">
<div className="w-full px-4 py-8 pb-16 h-full flex flex-col">
<div className="pl-12 pt-6 pb-4 shrink-0">
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 flex items-center gap-2 whitespace-nowrap">
<Monitor className="w-8 h-8 shrink-0" />
@@ -490,11 +490,10 @@ export const RemotePage: React.FC = () => {
</h1>
</div>
<div className="flex-1 flex px-0 -ml-4">
<div className="absolute left-60 top-[88px] bottom-16 w-px bg-gray-300/40 dark:bg-gray-500/50" />
<div className="flex-1 flex">
<div
ref={listContainerRef}
className="w-56 shrink-0 pr-4 overflow-hidden select-none"
className="w-48 shrink-0 overflow-hidden select-none border-r border-gray-200 dark:border-gray-700 pr-3"
>
<button
onClick={handleAddDevice}

View File

@@ -75,7 +75,7 @@ export const SearchPage: React.FC = () => {
}
return (
<div className="max-w-4xl mx-auto w-full p-6">
<div className="max-w-4xl mx-auto w-full px-14 py-12 pb-40">
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-8 flex items-center gap-2">
<Search className="w-8 h-8" />

View File

@@ -673,7 +673,7 @@ export const TimeTrackingPage: React.FC = () => {
}
return (
<div className="max-w-4xl mx-auto w-full p-6">
<div className="max-w-4xl mx-auto w-full px-14 py-12 pb-40">
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-8 flex items-center gap-2">
<Clock className="w-8 h-8" />

View File

@@ -216,7 +216,7 @@ export const TodoPage: React.FC = () => {
])].sort()
return (
<div className="max-w-4xl mx-auto w-full p-6">
<div className="max-w-4xl mx-auto w-full px-14 py-12 pb-40">
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-8 flex items-center gap-2">
<ListTodo className="w-8 h-8" />
TODO
@@ -260,7 +260,7 @@ export const TodoPage: React.FC = () => {
const canEdit = isEditable(date)
return (
<div key={date} className="space-y-2">
<div key={date} className="pt-4 space-y-2">
<div className="flex items-center gap-2">
<h3 className={`text-lg font-semibold ${isTodayDate
? 'text-gray-900 dark:text-gray-100'
@@ -330,7 +330,7 @@ export const TodoPage: React.FC = () => {
) : (
<span
onDoubleClick={() => canEdit && !item.completed && handleStartEdit(date, index, item.content)}
className={`flex-1 ${canEdit ? 'cursor-pointer' : 'cursor-default'
className={`flex-1 px-2 ${canEdit ? 'cursor-pointer' : 'cursor-default'}
} ${item.completed
? 'text-gray-400 dark:text-gray-500 line-through'
: 'text-gray-700 dark:text-gray-200'

View File

@@ -208,7 +208,6 @@ export const NoteBrowser = () => {
<TabContentCache
openFiles={openFiles}
activeFile={selectedFile}
containerClassName="p-8 pb-40"
onTocUpdated={handleTocUpdated}
/>
</div>

98
src/stores/chatStore.ts Normal file
View File

@@ -0,0 +1,98 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: number
}
export interface Chat {
id: string
title: string
messages: ChatMessage[]
createdAt: number
updatedAt: number
}
interface ChatState {
chats: Chat[]
currentChatId: string | null
createChat: () => string
deleteChat: (id: string) => void
selectChat: (id: string) => void
updateChatTitle: (id: string, title: string) => void
addMessage: (chatId: string, message: ChatMessage) => void
getCurrentChat: () => Chat | null
}
const generateId = () => Math.random().toString(36).substring(2, 15)
export const useChatStore = create<ChatState>()(
persist(
(set, get) => ({
chats: [],
currentChatId: null,
createChat: () => {
const id = generateId()
const now = Date.now()
const newChat: Chat = {
id,
title: '新对话',
messages: [],
createdAt: now,
updatedAt: now,
}
set((state) => ({
chats: [newChat, ...state.chats],
currentChatId: id,
}))
return id
},
deleteChat: (id) => {
set((state) => {
const newChats = state.chats.filter((c) => c.id !== id)
const newCurrentId = state.currentChatId === id
? (newChats[0]?.id ?? null)
: state.currentChatId
return { chats: newChats, currentChatId: newCurrentId }
})
},
selectChat: (id) => {
set({ currentChatId: id })
},
updateChatTitle: (id, title) => {
set((state) => ({
chats: state.chats.map((c) =>
c.id === id ? { ...c, title, updatedAt: Date.now() } : c
),
}))
},
addMessage: (chatId, message) => {
set((state) => ({
chats: state.chats.map((c) =>
c.id === chatId
? { ...c, messages: [...c.messages, message], updatedAt: Date.now() }
: c
),
}))
},
getCurrentChat: () => {
const state = get()
return state.chats.find((c) => c.id === state.currentChatId) ?? null
},
}),
{
name: 'chat-storage',
}
)
)