import { FormEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Archive, Clock3, FolderKanban, FolderOpen, Plus, RotateCcw, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
CollaborationProject,
getStageName,
isProjectInactive,
useCollaborationProjectsStore,
} from '@/stores/collaboration-projects';
import { cn } from '@/lib/utils';
import { invokeIpc } from '@/lib/api-client';
type OpenDialogResult = {
canceled: boolean;
filePaths: string[];
};
function formatRelativeTime(value: string) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const diff = Date.now() - date.getTime();
const hours = Math.max(1, Math.floor(diff / (60 * 60 * 1000)));
if (hours < 24) return `${hours} 小时前`;
return `${Math.floor(hours / 24)} 天前`;
}
function ProjectCard({
project,
archived = false,
onOpen,
onArchive,
onRestore,
}: {
project: CollaborationProject;
archived?: boolean;
onOpen: () => void;
onArchive: () => void;
onRestore: () => void;
}) {
return (
{project.name}
{archived ? (
归档
) : null}
{formatRelativeTime(project.lastActiveAt)}
{getStageName(project.currentStage)}
{project.workspacePath ? (
{project.workspacePath}
) : null}
{project.lastResult}
交付物 {project.deliverables.length} 个 · 文件 {project.files.length} 个
{archived ? (
) : (
)}
);
}
export function Home() {
const navigate = useNavigate();
const projects = useCollaborationProjectsStore((state) => state.projects);
const initialized = useCollaborationProjectsStore((state) => state.initialized);
const init = useCollaborationProjectsStore((state) => state.init);
const createProject = useCollaborationProjectsStore((state) => state.createProject);
const archiveProject = useCollaborationProjectsStore((state) => state.archiveProject);
const restoreProject = useCollaborationProjectsStore((state) => state.restoreProject);
const touchProject = useCollaborationProjectsStore((state) => state.touchProject);
const [name, setName] = useState('');
const [workspacePath, setWorkspacePath] = useState('');
const [nameError, setNameError] = useState('');
const [createOpen, setCreateOpen] = useState(false);
const [showInactive, setShowInactive] = useState(false);
const nameInputRef = useRef(null);
const folderInputRef = useRef(null);
useEffect(() => {
init();
}, [init]);
const resetCreateForm = () => {
setName('');
setWorkspacePath('');
setNameError('');
};
const openCreateDialog = () => {
resetCreateForm();
setCreateOpen(true);
window.setTimeout(() => nameInputRef.current?.focus(), 50);
};
const cancelCreateDialog = () => {
resetCreateForm();
setCreateOpen(false);
};
useEffect(() => {
const handleOpenCreateProject = () => openCreateDialog();
window.addEventListener('niancode:create-project', handleOpenCreateProject);
return () => window.removeEventListener('niancode:create-project', handleOpenCreateProject);
}, []);
const visibleGroups = useMemo(() => {
const active = projects.filter((project) => !project.archivedAt && !isProjectInactive(project));
const inactive = projects.filter((project) => !project.archivedAt && isProjectInactive(project));
const archived = projects.filter((project) => Boolean(project.archivedAt));
return { active, inactive, archived };
}, [projects]);
const openProject = (project: CollaborationProject) => {
touchProject(project.id);
navigate(`/workbench/${project.id}`);
};
const submit = (event: FormEvent) => {
event.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setNameError('请输入项目名称');
nameInputRef.current?.focus();
return;
}
const project = createProject(trimmed, workspacePath);
resetCreateForm();
setCreateOpen(false);
navigate(`/workbench/${project.id}`);
};
const chooseProjectFolder = async () => {
const electronApi = (window as Partial).electron;
if (electronApi?.ipcRenderer?.invoke) {
const result = await invokeIpc('dialog:open', {
properties: ['openDirectory'],
title: '选择项目储存地址',
});
if (!result.canceled && result.filePaths[0]) {
setWorkspacePath(result.filePaths[0]);
}
return;
}
folderInputRef.current?.click();
};
if (!initialized) {
return (
正在读取本地项目
);
}
return (
进行中项目
归档项目
{visibleGroups.active.length === 0 && visibleGroups.inactive.length === 0 ? (
还没有项目
创建一个项目后,会直接进入阶段一:项目背景。
) : (
{visibleGroups.active.map((project) => (
openProject(project)}
onArchive={() => archiveProject(project.id)}
onRestore={() => restoreProject(project.id)}
/>
))}
)}
{visibleGroups.inactive.length > 0 ? (
{showInactive ? (
{visibleGroups.inactive.map((project) => (
openProject(project)}
onArchive={() => archiveProject(project.id)}
onRestore={() => restoreProject(project.id)}
/>
))}
) : null}
) : null}
{visibleGroups.archived.length === 0 ? (
暂无归档项目
) : (
{visibleGroups.archived.map((project) => (
openProject(project)}
onArchive={() => archiveProject(project.id)}
onRestore={() => restoreProject(project.id)}
/>
))}
)}
);
}
export default Home;