feat: 首页改造成ChatGPT风格对话界面,调整各页面padding布局
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
}
|
||||
|
||||
120
src/modules/home/components/ChatContent.tsx
Normal file
120
src/modules/home/components/ChatContent.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
11
src/modules/home/components/ChatLayout.tsx
Normal file
11
src/modules/home/components/ChatLayout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
53
src/modules/home/components/ChatSidebar.tsx
Normal file
53
src/modules/home/components/ChatSidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
搜索
|
||||
|
||||
@@ -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" />
|
||||
时间统计
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
98
src/stores/chatStore.ts
Normal 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',
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user