feat(web): add three-column layout shell with placeholder views

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 02:24:43 +08:00
parent 0c8283d6e9
commit 58ceb0af00
10 changed files with 203 additions and 0 deletions

24
web/client/src/App.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { AppLayout } from '@/components/layout/AppLayout';
import { ChatView } from '@/components/chat/ChatView';
import { AccountList } from '@/components/accounts/AccountList';
import { AssetGallery } from '@/components/assets/AssetGallery';
import { ConfigForm } from '@/components/config/ConfigForm';
import { useAppStore } from '@/store';
function MainContent() {
const view = useAppStore((s) => s.activeView);
switch (view) {
case 'chat': return <ChatView />;
case 'accounts': return <AccountList />;
case 'assets': return <AssetGallery />;
case 'config': return <ConfigForm />;
}
}
export default function App() {
return (
<AppLayout>
<MainContent />
</AppLayout>
);
}

View File

@@ -0,0 +1,3 @@
export function AccountList() {
return <div className="p-6 text-zinc-500"></div>;
}

View File

@@ -0,0 +1,3 @@
export function AssetGallery() {
return <div className="p-6 text-zinc-500"></div>;
}

View File

@@ -0,0 +1,3 @@
export function ChatView() {
return <div className="flex-1 flex items-center justify-center text-zinc-500"></div>;
}

View File

@@ -0,0 +1,3 @@
export function ConfigForm() {
return <div className="p-6 text-zinc-500"></div>;
}

View File

@@ -0,0 +1,17 @@
import { Sidebar } from './Sidebar';
import { MiddlePanel } from './MiddlePanel';
import { useAppStore } from '@/store';
export function AppLayout({ children }: { children: React.ReactNode }) {
const activeView = useAppStore((s) => s.activeView);
return (
<div className="h-screen flex bg-zinc-950 text-zinc-50 overflow-hidden">
<Sidebar />
{activeView === 'chat' && <MiddlePanel />}
<main className="flex-1 flex flex-col min-w-0">
{children}
</main>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useAppStore } from '@/store';
export function MiddlePanel() {
const { conversations, activeConversationId, setActiveConversationId } = useAppStore();
return (
<aside className="w-60 flex flex-col border-r border-zinc-800">
<div className="p-3 flex items-center justify-between">
<Input
placeholder="搜索对话..."
className="h-8 text-xs bg-zinc-900 border-zinc-800"
/>
<Button size="icon" variant="ghost" className="h-8 w-8 ml-1">
<Plus size={16} />
</Button>
</div>
<ScrollArea className="flex-1 px-2">
{conversations.map((conv) => (
<button
key={conv.id}
onClick={() => setActiveConversationId(conv.id)}
className={`w-full text-left px-3 py-2 rounded-md text-sm truncate mb-0.5 transition-colors
${conv.id === activeConversationId ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-200'}`}
>
{conv.title}
</button>
))}
{conversations.length === 0 && (
<p className="text-xs text-zinc-600 text-center mt-8"></p>
)}
</ScrollArea>
</aside>
);
}

View File

@@ -0,0 +1,35 @@
import { MessageCircle, FolderOpen, Image, Settings } from 'lucide-react';
import { useAppStore } from '@/store';
import type { NavView } from '@/types';
import { cn } from '@/lib/utils';
const navItems: { id: NavView; icon: typeof MessageCircle; label: string }[] = [
{ id: 'chat', icon: MessageCircle, label: '对话' },
{ id: 'accounts', icon: FolderOpen, label: '账户' },
{ id: 'assets', icon: Image, label: '资产' },
{ id: 'config', icon: Settings, label: '设置' },
];
export function Sidebar() {
const { activeView, setActiveView } = useAppStore();
return (
<aside className="w-14 flex flex-col items-center py-4 gap-2 border-r border-zinc-800">
{navItems.map(({ id, icon: Icon, label }) => (
<button
key={id}
onClick={() => setActiveView(id)}
className={cn(
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
activeView === id
? 'bg-zinc-800 text-white'
: 'text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/50'
)}
title={label}
>
<Icon size={20} />
</button>
))}
</aside>
);
}

View File

@@ -0,0 +1,24 @@
import { create } from 'zustand';
import type { NavView, Conversation } from '@/types';
interface AppState {
activeView: NavView;
setActiveView: (view: NavView) => void;
selectedAccountId: string | null;
setSelectedAccountId: (id: string | null) => void;
activeConversationId: string | null;
setActiveConversationId: (id: string | null) => void;
conversations: Conversation[];
setConversations: (list: Conversation[]) => void;
}
export const useAppStore = create<AppState>((set) => ({
activeView: 'chat',
setActiveView: (view) => set({ activeView: view }),
selectedAccountId: null,
setSelectedAccountId: (id) => set({ selectedAccountId: id }),
activeConversationId: null,
setActiveConversationId: (id) => set({ activeConversationId: id }),
conversations: [],
setConversations: (list) => set({ conversations: list }),
}));

View File

@@ -0,0 +1,53 @@
export type NavView = 'chat' | 'accounts' | 'assets' | 'config';
export interface Account {
id: string;
name: string;
description: string;
defaultFormat: string;
imageModel: string;
videoModel: string;
batchSize: number;
ttsVoice: string;
ttsInstruction: string;
storyboardPrompt: string;
imageStylePrompt: string;
videoStylePrompt: string;
references: { file: string; url?: string }[];
capcut: Record<string, unknown>;
}
export interface Conversation {
id: string;
title: string;
account_id: string | null;
created_at: string;
updated_at: string;
}
export interface Message {
id: string;
conversation_id: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
tool_calls?: unknown;
created_at: string;
}
export interface Asset {
id: string;
account_id: string | null;
manifest_path: string | null;
type: 'image' | 'video';
file_path: string;
url: string | null;
shot_index: number | null;
created_at: string;
}
export interface ConfigItem {
id: string;
key: string;
value: Record<string, unknown>;
updated_at: string;
}