fix: redesign project workbench layout
This commit is contained in:
@@ -1524,6 +1524,27 @@
|
||||
"message": "auto-save 2026-05-20 12:49 (~3)",
|
||||
"hash": "a190800",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T14:07:08+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-20 14:07 (-1)",
|
||||
"hash": "09664a2",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T14:12:33+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-20 14:12 (+1, ~3)",
|
||||
"hash": "6bd8873",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T14:45:05+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-20 14:45 (~4)",
|
||||
"hash": "527ccfa",
|
||||
"files_changed": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -342,6 +342,207 @@ input, textarea {
|
||||
background: linear-gradient(90deg, rgba(0,0,0,0.16), rgba(42,42,42,0.08));
|
||||
}
|
||||
|
||||
.project-shell {
|
||||
background:
|
||||
radial-gradient(circle at 22% 8%, rgba(230, 245, 120, 0.12), transparent 34%),
|
||||
radial-gradient(circle at 86% 18%, rgba(73, 199, 182, 0.09), transparent 30%),
|
||||
linear-gradient(135deg, rgba(255,255,255,0.025), rgba(255,255,255,0));
|
||||
}
|
||||
|
||||
.project-dock {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.10);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(36, 36, 36, 0.76), rgba(12, 12, 12, 0.64)),
|
||||
rgba(24, 24, 24, 0.60);
|
||||
box-shadow:
|
||||
inset -1px 0 0 rgba(255,255,255,0.04),
|
||||
18px 0 70px -48px rgba(0,0,0,0.96);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.project-dock--closed {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.project-dock-list,
|
||||
.project-brief-panel,
|
||||
.project-production-panel {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
||||
}
|
||||
|
||||
.project-dock-list::-webkit-scrollbar,
|
||||
.project-brief-panel::-webkit-scrollbar,
|
||||
.project-production-panel::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.project-dock-list::-webkit-scrollbar-thumb,
|
||||
.project-brief-panel::-webkit-scrollbar-thumb,
|
||||
.project-production-panel::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.project-stage {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-stage-inner {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 18px 22px 20px;
|
||||
}
|
||||
|
||||
.project-topbar {
|
||||
min-height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border: 1px solid rgba(255,255,255,0.09);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255,255,255,0.065), rgba(255,255,255,0.026)),
|
||||
rgba(24,24,24,0.46);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.07);
|
||||
backdrop-filter: blur(18px);
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.project-empty-canvas {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.project-board {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.project-board-head {
|
||||
min-height: 102px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(300px, 560px);
|
||||
gap: 18px;
|
||||
align-items: stretch;
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255,255,255,0.07), rgba(255,255,255,0.026)),
|
||||
rgba(28,28,28,0.50);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.08),
|
||||
0 24px 90px -54px rgba(0, 0, 0, 0.92);
|
||||
backdrop-filter: blur(18px);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.project-reference-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-reference-tile {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.92);
|
||||
min-height: 70px;
|
||||
aspect-ratio: 1 / 1;
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.32);
|
||||
}
|
||||
|
||||
.project-reference-tile span {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 6px;
|
||||
background: rgba(0,0,0,0.54);
|
||||
padding: 2px 5px;
|
||||
color: rgba(255,255,255,0.76);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.project-board-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(250px, 310px) minmax(0, 1fr) 88px;
|
||||
gap: 14px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.project-brief-panel {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(255,255,255,0.07), rgba(255,255,255,0.025)),
|
||||
rgba(24,24,24,0.52);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.07);
|
||||
backdrop-filter: blur(18px);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.project-primary-preview {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.92);
|
||||
aspect-ratio: 4 / 3;
|
||||
min-height: 190px;
|
||||
box-shadow:
|
||||
0 20px 60px -38px rgba(230,245,120,0.7),
|
||||
inset 0 0 0 1px rgba(255,255,255,0.36);
|
||||
}
|
||||
|
||||
.project-stat {
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.045);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
|
||||
padding: 10px;
|
||||
color: rgba(255,255,255,0.82);
|
||||
}
|
||||
|
||||
.project-stat--accent {
|
||||
background: rgba(230,245,120,0.13);
|
||||
color: #e6f578;
|
||||
box-shadow: inset 0 0 0 1px rgba(230,245,120,0.22);
|
||||
}
|
||||
|
||||
.project-spec-card {
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.22);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.project-production-panel {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-production-panel > div {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dashboard-workbench {
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 8px;
|
||||
@@ -439,9 +640,43 @@ input, textarea {
|
||||
grid-template-columns: minmax(0, 1fr) 84px;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.project-board-grid {
|
||||
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr) 96px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.project-shell {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.project-stage {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.project-stage-inner {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.project-board-head,
|
||||
.project-board-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-reference-strip {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.project-brief-panel,
|
||||
.project-production-panel {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.session-workspace {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
|
||||
176
src/app/page.tsx
176
src/app/page.tsx
@@ -6,6 +6,7 @@ import Sidebar from '@/components/Sidebar';
|
||||
import PackPanel from '@/components/PackPanel';
|
||||
import ProjectGalleryDrawer from '@/components/ProjectGalleryDrawer';
|
||||
import { OasisCanvas } from '@/components/login/OasisCanvas';
|
||||
import { PACK_ORDER, PACK_TEMPLATES } from '@/lib/templates';
|
||||
import type {
|
||||
GenImage,
|
||||
GenSession,
|
||||
@@ -22,6 +23,124 @@ import type {
|
||||
} from '@/lib/types';
|
||||
import type { VIDEO_TEMPLATES } from '@/lib/templates';
|
||||
|
||||
function modeLabel(mode?: GenSession['inputMode']) {
|
||||
if (mode === 'remix') return '二创项目';
|
||||
if (mode === 'replicate') return '复刻项目';
|
||||
if (mode === 'extend') return '补全项目';
|
||||
return '想法项目';
|
||||
}
|
||||
|
||||
function projectTitle(session: GenSession) {
|
||||
return session.characterSpec?.name || session.prompt || '未命名项目';
|
||||
}
|
||||
|
||||
function imageSourcesForSession(session: GenSession) {
|
||||
const uploaded = session.uploadedImages?.map(image => ({
|
||||
url: image.url,
|
||||
label: image.role === 'subject' ? '主体图' : image.role === 'reference' ? '参考图' : image.accessoryName || image.role,
|
||||
})) ?? [];
|
||||
if (uploaded.length) return uploaded;
|
||||
return session.refImages.map((url, index) => ({ url, label: `参考 ${index + 1}` }));
|
||||
}
|
||||
|
||||
function packSlotTotal() {
|
||||
return PACK_ORDER.reduce((sum, kind) => sum + PACK_TEMPLATES[kind].length, 0);
|
||||
}
|
||||
|
||||
function ProjectStat({ label, value, tone }: { label: string; value: string | number; tone?: 'accent' | 'soft' }) {
|
||||
return (
|
||||
<div className={`project-stat ${tone === 'accent' ? 'project-stat--accent' : ''}`}>
|
||||
<div className="text-[10px] text-white/40">{label}</div>
|
||||
<div className="mt-1 text-[18px] font-semibold tracking-tight">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReferenceStrip({ session }: { session: GenSession }) {
|
||||
const refs = imageSourcesForSession(session);
|
||||
if (!refs.length) return null;
|
||||
|
||||
return (
|
||||
<div className="project-reference-strip">
|
||||
{refs.slice(0, 6).map((ref, index) => (
|
||||
<div key={`${ref.url}-${index}`} className="project-reference-tile">
|
||||
<img src={ref.url} alt={ref.label} className="h-full w-full object-contain" />
|
||||
<span>{ref.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectBrief({ session }: { session: GenSession }) {
|
||||
const selectedImages = session.images.filter(image => image.status === 'selected');
|
||||
const primaryImage = selectedImages[0] ?? session.images[0] ?? null;
|
||||
const generatedAssets = (session.packs ?? []).reduce((sum, pack) => sum + pack.assets.length, 0);
|
||||
const totalSlots = packSlotTotal();
|
||||
|
||||
return (
|
||||
<section className="project-brief-panel">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<span className="section-eyebrow">Project Core</span>
|
||||
<h2 className="mt-2 text-[20px] font-semibold leading-tight text-white">{projectTitle(session)}</h2>
|
||||
<p className="mt-2 line-clamp-3 text-[12px] leading-relaxed text-white/50">
|
||||
{session.characterSpec?.oneLiner || session.prompt || '当前项目暂无描述'}
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-[#e6f578]/15 px-3 py-1 text-[10px] font-semibold text-[#e6f578] ring-1 ring-[#e6f578]/25">
|
||||
{modeLabel(session.inputMode)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{primaryImage && (
|
||||
<div className="project-primary-preview mt-5">
|
||||
<img src={primaryImage.url} alt="当前主方案" className="h-full w-full object-contain" />
|
||||
<div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-3 bg-gradient-to-t from-black/72 to-transparent p-3">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-white/70">Primary</span>
|
||||
<span className="text-[10px] text-white/50">{primaryImage.status === 'selected' ? '已选中' : '待筛选'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-5 grid grid-cols-3 gap-2">
|
||||
<ProjectStat label="图库" value={session.images.length} />
|
||||
<ProjectStat label="选中" value={selectedImages.length} tone="accent" />
|
||||
<ProjectStat label="产出" value={`${generatedAssets}/${totalSlots}`} />
|
||||
</div>
|
||||
|
||||
{session.characterSpec ? (
|
||||
<div className="project-spec-card mt-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-[11px] font-semibold text-white/80">角色设定</span>
|
||||
<span className="text-[10px] text-[#e6f578]/70">已锁定</span>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2 text-[11px]">
|
||||
{[
|
||||
['形态', session.characterSpec.speciesShape],
|
||||
['比例', session.characterSpec.bodyRatio],
|
||||
['配色', session.characterSpec.colorPalette.join('、')],
|
||||
['材料', session.characterSpec.materials.join('、')],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="grid grid-cols-[42px_minmax(0,1fr)] gap-2">
|
||||
<span className="text-white/35">{label}</span>
|
||||
<span className="truncate text-white/75">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="project-spec-card mt-5">
|
||||
<div className="text-[11px] font-semibold text-white/80">角色设定</div>
|
||||
<p className="mt-2 text-[11px] leading-relaxed text-white/45">
|
||||
从右侧图库选中主方案后,在生产矩阵里锁定角色设定。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [sessions, setSessions] = useState<GenSession[]>([]);
|
||||
const [current, setCurrent] = useState<GenSession | null>(null);
|
||||
@@ -282,7 +401,7 @@ export default function Home() {
|
||||
<OasisCanvas />
|
||||
<div className="app-oasis-shade" />
|
||||
<div className="app-grass-floor" />
|
||||
<div className="relative z-10 flex h-screen text-white">
|
||||
<div className="project-shell relative z-10 flex h-screen text-white">
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
onToggle={() => setSidebarOpen(v => !v)}
|
||||
@@ -291,26 +410,15 @@ export default function Home() {
|
||||
onPick={id => setCurrent(sessions.find(s => s.id === id) ?? null)}
|
||||
onNew={() => setCurrent(null)}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto max-w-[1680px] px-5 py-6 sm:px-8 xl:px-10">
|
||||
<header className="flex items-center justify-between mb-8 gap-4">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-9 h-9 rounded-[8px] bg-gradient-to-br from-[#e6f578] to-[#d6b36a] flex items-center justify-center shrink-0 shadow-glow-violet">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#081006" strokeWidth="2.5">
|
||||
<path d="M12 2l2.5 6.5L21 11l-6.5 2.5L12 20l-2.5-6.5L3 11l6.5-2.5z" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<main className="project-stage min-w-0 flex-1">
|
||||
<div className="project-stage-inner">
|
||||
<header className="project-topbar">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-base font-semibold tracking-tight leading-tight text-[#f8f7ef]">
|
||||
AI Toy Patent
|
||||
<span className="ml-2 text-white/48 font-normal text-sm">把一句想法变成专利 / 生产 / 宣发素材包</span>
|
||||
</h1>
|
||||
<p className="text-[11px] text-[#e6f578]/55 mt-0.5">批量出意向 · 快筛 · 锁定角色 · 一键四包 · Seedance 视频</p>
|
||||
<span className="section-eyebrow">AI Toy Patent</span>
|
||||
<h1 className="mt-1 text-[18px] font-semibold tracking-tight text-[#f8f7ef]">项目生产工作台</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{current && (
|
||||
<>
|
||||
<a
|
||||
href={`/api/audit/${encodeURIComponent(current.id)}`}
|
||||
target="_blank"
|
||||
@@ -319,7 +427,6 @@ export default function Home() {
|
||||
>
|
||||
记录
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
@@ -328,14 +435,14 @@ export default function Home() {
|
||||
退出
|
||||
</button>
|
||||
<span className={provider === 'gpt' ? 'chip chip-live' : provider === '?' ? 'chip chip-neutral' : 'chip chip-mock'}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${provider === 'gpt' ? 'bg-emerald-400' : provider === '?' ? 'bg-white/40' : 'bg-amber-400'}`} />
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${provider === 'gpt' ? 'bg-emerald-400' : provider === '?' ? 'bg-white/40' : 'bg-amber-400'}`} />
|
||||
{provider === 'gpt' ? 'GPT · gpt-image-2' : provider === 'mock' ? 'Mock · 占位图' : provider === '?' ? '待连接' : provider}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="space-y-8">
|
||||
{!current && (
|
||||
<section className="project-empty-canvas">
|
||||
<PromptPanel
|
||||
session={null}
|
||||
onGenerate={handleGenerate}
|
||||
@@ -343,21 +450,25 @@ export default function Home() {
|
||||
loading={loading}
|
||||
uploadLoading={uploadLoading}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{current && (
|
||||
<section className="dashboard-workbench session-workspace flex flex-col gap-5 p-5">
|
||||
<div className="flex shrink-0 items-end justify-between gap-4">
|
||||
<div>
|
||||
<span className="section-eyebrow">Project Workspace</span>
|
||||
<h2 className="mt-2 text-lg font-semibold text-white">项目工作台</h2>
|
||||
<p className="text-xs text-white/40 mt-1">
|
||||
{new Date(current.createdAt).toLocaleString('zh-CN')}
|
||||
<section className="project-board">
|
||||
<div className="project-board-head">
|
||||
<div className="min-w-0">
|
||||
<span className="section-eyebrow">Current Project</span>
|
||||
<h2 className="mt-2 line-clamp-1 text-[24px] font-semibold leading-tight text-white">{projectTitle(current)}</h2>
|
||||
<p className="mt-1 text-[11px] text-white/40">
|
||||
{new Date(current.createdAt).toLocaleString('zh-CN')} · {current.id}
|
||||
</p>
|
||||
</div>
|
||||
<code className="max-w-[220px] truncate text-[11px] text-white/30 font-mono">{current.id}</code>
|
||||
<ReferenceStrip session={current} />
|
||||
</div>
|
||||
<div className="session-split min-h-0 flex-1">
|
||||
<aside className="session-pack-pane min-h-0 overflow-hidden">
|
||||
|
||||
<div className="project-board-grid">
|
||||
<ProjectBrief session={current} />
|
||||
<section className="project-production-panel">
|
||||
<PackPanel
|
||||
session={current}
|
||||
loadingKind={loadingKind}
|
||||
@@ -370,13 +481,12 @@ export default function Home() {
|
||||
onRegenerateAsset={handleRegenerateAsset}
|
||||
onGenerateVideo={handleGenerateVideo}
|
||||
/>
|
||||
</aside>
|
||||
</section>
|
||||
<ProjectGalleryDrawer session={current} onAction={handleAction} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,66 +9,65 @@ function modeLabel(mode?: GenSession['inputMode']) {
|
||||
return '想法';
|
||||
}
|
||||
|
||||
function imageSourcesForSession(session: GenSession) {
|
||||
const uploaded = session.uploadedImages?.map(image => ({
|
||||
url: image.url,
|
||||
label: image.role === 'subject' ? '主体图' : image.role === 'reference' ? '参考图' : image.accessoryName || image.role,
|
||||
})) ?? [];
|
||||
if (uploaded.length) return uploaded;
|
||||
return session.refImages.map((url, index) => ({ url, label: `参考 ${index + 1}` }));
|
||||
function projectTitle(session: GenSession) {
|
||||
return session.characterSpec?.name || session.prompt || '未命名项目';
|
||||
}
|
||||
|
||||
function ActiveSessionDetail({ session }: { session: GenSession }) {
|
||||
const refs = imageSourcesForSession(session);
|
||||
function ProjectThumbs({ session }: { session: GenSession }) {
|
||||
const refs = session.uploadedImages?.map(image => image.url) ?? session.refImages;
|
||||
const thumbs = refs.length ? refs : session.images.map(image => image.url);
|
||||
|
||||
return (
|
||||
<div className="flex -space-x-2">
|
||||
{thumbs.slice(0, 3).map((url, index) => (
|
||||
<span key={`${url}-${index}`} className="grid h-8 w-8 overflow-hidden rounded-[8px] bg-white ring-1 ring-white/15">
|
||||
<img src={url} alt="" className="h-full w-full object-contain" />
|
||||
</span>
|
||||
))}
|
||||
{thumbs.length === 0 && (
|
||||
<span className="grid h-8 w-8 place-items-center rounded-[8px] bg-white/[0.06] text-[10px] text-white/30 ring-1 ring-white/10">
|
||||
-
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectCard({ session, active, onPick }: { session: GenSession; active: boolean; onPick: () => void }) {
|
||||
const selectedCount = session.images.filter(image => image.status === 'selected').length;
|
||||
const packCount = session.packs?.length ?? 0;
|
||||
|
||||
return (
|
||||
<div className="rounded-[8px] border border-white/10 bg-white/[0.055] p-3 shadow-[0_18px_52px_-38px_rgba(230,245,120,0.68)] backdrop-blur-xl">
|
||||
<button
|
||||
onClick={onPick}
|
||||
className={`project-card-button w-full cursor-pointer rounded-[8px] p-3 text-left transition-all ${
|
||||
active
|
||||
? 'bg-[#e6f578]/15 text-white ring-1 ring-[#e6f578]/45 shadow-[0_18px_48px_-34px_rgba(230,245,120,0.85)]'
|
||||
: 'bg-white/[0.035] text-white/75 ring-1 ring-white/[0.07] hover:bg-white/[0.06] hover:text-white hover:ring-white/20'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<ProjectThumbs session={session} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-[9px] uppercase tracking-[0.16em] text-[#b9c22a]/80">Current session</div>
|
||||
<span className="rounded-full border border-white/10 bg-black/28 px-2 py-0.5 text-[9px] text-white/48">
|
||||
<span className={`rounded-full px-2 py-0.5 text-[9px] ${active ? 'bg-[#e6f578] text-[#081006]' : 'bg-black/25 text-white/45'}`}>
|
||||
{modeLabel(session.inputMode)}
|
||||
</span>
|
||||
<span className="text-[9px] text-white/30">
|
||||
{new Date(session.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 line-clamp-4 text-[11px] font-medium leading-relaxed text-white/80">
|
||||
{session.characterSpec?.name || session.prompt || '未填写'}
|
||||
<div className="mt-2 line-clamp-2 text-[12px] font-semibold leading-snug">
|
||||
{projectTitle(session)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-3 gap-1.5 text-[9px] text-white/44">
|
||||
<div className="rounded-[6px] bg-black/22 px-2 py-1.5">
|
||||
<div>图片</div>
|
||||
<b className="mt-0.5 block text-[11px] text-white/78">{session.images.length}</b>
|
||||
</div>
|
||||
<div className="rounded-[6px] bg-black/22 px-2 py-1.5">
|
||||
<div>选中</div>
|
||||
<b className="mt-0.5 block text-[11px] text-[#e6f578]">{selectedCount}</b>
|
||||
</div>
|
||||
<div className="rounded-[6px] bg-black/22 px-2 py-1.5">
|
||||
<div>包</div>
|
||||
<b className="mt-0.5 block text-[11px] text-white/78">{packCount}</b>
|
||||
<div className="mt-2 flex items-center gap-2 text-[10px] text-white/40">
|
||||
<span>{session.images.length} 图</span>
|
||||
<span>{selectedCount} 选</span>
|
||||
<span>{packCount} 包</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{refs.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="mb-1.5 text-[9px] uppercase tracking-[0.14em] text-white/34">Reference</div>
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
{refs.slice(0, 4).map((ref, index) => (
|
||||
<div key={`${ref.url}-${index}`} className="relative aspect-square overflow-hidden rounded-[6px] bg-white/90 ring-1 ring-white/10">
|
||||
<img src={ref.url} alt={ref.label} className="h-full w-full object-contain" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex items-center justify-between gap-2 text-[9px] text-white/36">
|
||||
<span>{new Date(session.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>
|
||||
<span className="font-mono">{session.id.slice(0, 10)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,15 +86,14 @@ export default function Sidebar({
|
||||
onPick: (id: string) => void;
|
||||
onNew: () => void;
|
||||
}) {
|
||||
const activeSession = sessions.find(session => session.id === currentId) ?? null;
|
||||
|
||||
if (!open) {
|
||||
return (
|
||||
<aside className="glass-sidebar w-12 shrink-0 flex flex-col items-center py-4">
|
||||
<aside className="project-dock project-dock--closed w-14 shrink-0">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-8 h-8 rounded-lg hover:bg-white/[0.08] flex items-center justify-center text-white/60 transition-colors"
|
||||
title="展开侧栏"
|
||||
className="mt-4 grid h-10 w-10 cursor-pointer place-items-center rounded-[8px] bg-white/[0.06] text-white/60 transition-colors hover:bg-white/[0.10] hover:text-white"
|
||||
title="展开项目栏"
|
||||
aria-label="展开项目栏"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
@@ -106,33 +104,21 @@ export default function Sidebar({
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="glass-sidebar w-[304px] shrink-0 overflow-hidden">
|
||||
<div className="flex h-full min-h-0">
|
||||
<div className="flex w-[58px] shrink-0 flex-col items-center border-r border-white/[0.06] py-5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-[#d9df2a] via-[#a7c83b] to-[#49c7b6] text-[11px] font-bold text-[#081006] shadow-[0_12px_34px_-18px_rgba(185,194,42,0.9)]">
|
||||
Lo
|
||||
<aside className="project-dock w-[276px] shrink-0 overflow-hidden">
|
||||
<div className="flex h-full min-h-0 flex-col p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="grid h-10 w-10 place-items-center rounded-[8px] bg-[#e6f578] text-sm font-black text-[#081006]">
|
||||
AI
|
||||
</div>
|
||||
<div className="mt-7 flex flex-col gap-4 text-white/32">
|
||||
{['M4 6h16M4 12h16M4 18h16', 'M4 5h7v7H4zM13 5h7v7h-7zM4 14h7v5H4zM13 14h7v5h-7z', 'M5 7h14M7 7v12h10V7M9 7V5h6v2', 'M6 6h12v12H6z'].map((path, index) => (
|
||||
<span key={index} className={`flex h-8 w-8 items-center justify-center rounded-[8px] ${index === 0 ? 'bg-white/[0.08] text-[#d9df2a]' : 'hover:bg-white/[0.06] hover:text-white/70'}`}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d={path} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="flex items-center gap-3 px-4 pb-3 pt-5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[13px] font-semibold tracking-tight">Toy Patent</div>
|
||||
<div className="text-[10px] text-white/40">AI 玩具工作流</div>
|
||||
<div className="text-[13px] font-semibold tracking-tight text-white">Project Dock</div>
|
||||
<div className="text-[10px] text-white/40">一个会话 = 一个项目</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[8px] text-white/60 transition-colors hover:bg-white/[0.08]"
|
||||
title="收起侧栏"
|
||||
className="grid h-9 w-9 cursor-pointer place-items-center rounded-[8px] text-white/55 transition-colors hover:bg-white/[0.08] hover:text-white"
|
||||
title="收起项目栏"
|
||||
aria-label="收起项目栏"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
@@ -140,72 +126,36 @@ export default function Sidebar({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<button
|
||||
onClick={onNew}
|
||||
className="btn btn-primary w-full text-sm"
|
||||
>
|
||||
<button onClick={onNew} className="btn btn-primary mt-5 w-full justify-center text-sm">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
|
||||
</svg>
|
||||
新会话
|
||||
新项目
|
||||
</button>
|
||||
|
||||
<div className="mt-6 flex items-center justify-between text-[10px] font-semibold uppercase tracking-[0.18em] text-white/35">
|
||||
<span>Projects</span>
|
||||
<span className="tracking-normal">{sessions.length}</span>
|
||||
</div>
|
||||
|
||||
{activeSession && (
|
||||
<div className="px-3 pb-4">
|
||||
<ActiveSessionDetail session={activeSession} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="px-5 pb-2 pt-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white/35">
|
||||
最近会话
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-1 overflow-y-auto px-3 pb-4">
|
||||
<div className="project-dock-list mt-3 flex-1 space-y-2 overflow-y-auto pr-1">
|
||||
{sessions.length === 0 && (
|
||||
<div className="px-3 py-12 text-center text-xs text-white/30">
|
||||
还没有生成记录
|
||||
<div className="rounded-[8px] border border-dashed border-white/10 px-4 py-12 text-center text-xs text-white/30">
|
||||
还没有项目
|
||||
</div>
|
||||
)}
|
||||
{sessions.map(s => {
|
||||
const selectedCount = s.images.filter(i => i.status === 'selected').length;
|
||||
const active = currentId === s.id;
|
||||
return (
|
||||
<div key={s.id} className="relative">
|
||||
<button
|
||||
onClick={() => onPick(s.id)}
|
||||
className={`w-full rounded-[8px] px-3 py-2.5 text-left text-xs transition-all ${
|
||||
active
|
||||
? 'bg-gradient-to-r from-[#d9df2a]/22 via-[#6e63c8]/16 to-[#49c7b6]/14 text-white ring-1 ring-[#d9df2a]/35 shadow-[0_18px_48px_-32px_rgba(185,194,42,0.86)]'
|
||||
: 'text-white/75 ring-1 ring-transparent hover:bg-white/[0.055] hover:text-white/90 hover:ring-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className={`line-clamp-2 font-medium leading-relaxed ${active ? 'text-white' : 'text-white/85'}`}>
|
||||
{s.characterSpec?.name || s.prompt}
|
||||
</div>
|
||||
<div className={`mt-1.5 flex items-center justify-between text-[10px] ${active ? 'text-white/65' : 'text-white/40'}`}>
|
||||
<span>
|
||||
{new Date(s.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span>{s.images.length} 张</span>
|
||||
{selectedCount > 0 && (
|
||||
<span className={active ? 'font-semibold text-[#d9df2a]' : 'font-semibold text-[#d6b36a]'}>
|
||||
✓{selectedCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sessions.map(session => (
|
||||
<ProjectCard
|
||||
key={session.id}
|
||||
session={session}
|
||||
active={currentId === session.id}
|
||||
onPick={() => onPick(session.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
|
||||
本地端口 4560 · 数据 / data
|
||||
</div>
|
||||
<div className="mt-4 border-t border-white/[0.06] pt-3 text-[10px] text-white/30">
|
||||
本地 Docker · 4560
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user