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:
24
web/client/src/App.tsx
Normal file
24
web/client/src/App.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
web/client/src/components/accounts/AccountList.tsx
Normal file
3
web/client/src/components/accounts/AccountList.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function AccountList() {
|
||||||
|
return <div className="p-6 text-zinc-500">账户管理</div>;
|
||||||
|
}
|
||||||
3
web/client/src/components/assets/AssetGallery.tsx
Normal file
3
web/client/src/components/assets/AssetGallery.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function AssetGallery() {
|
||||||
|
return <div className="p-6 text-zinc-500">资产画廊</div>;
|
||||||
|
}
|
||||||
3
web/client/src/components/chat/ChatView.tsx
Normal file
3
web/client/src/components/chat/ChatView.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function ChatView() {
|
||||||
|
return <div className="flex-1 flex items-center justify-center text-zinc-500">选择或开始新对话</div>;
|
||||||
|
}
|
||||||
3
web/client/src/components/config/ConfigForm.tsx
Normal file
3
web/client/src/components/config/ConfigForm.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function ConfigForm() {
|
||||||
|
return <div className="p-6 text-zinc-500">设置</div>;
|
||||||
|
}
|
||||||
17
web/client/src/components/layout/AppLayout.tsx
Normal file
17
web/client/src/components/layout/AppLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
web/client/src/components/layout/MiddlePanel.tsx
Normal file
38
web/client/src/components/layout/MiddlePanel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
web/client/src/components/layout/Sidebar.tsx
Normal file
35
web/client/src/components/layout/Sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
web/client/src/store/index.ts
Normal file
24
web/client/src/store/index.ts
Normal 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 }),
|
||||||
|
}));
|
||||||
53
web/client/src/types/index.ts
Normal file
53
web/client/src/types/index.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user