This commit is contained in:
xiongxiao
2026-03-14 17:13:36 +08:00
committed by cnb
parent 0dcefbcdc8
commit c27a16ed8d
6 changed files with 1374 additions and 76 deletions

View File

@@ -21,6 +21,12 @@ const api = {
"type": "string",
"description": "项目根目录的绝对路径,必填"
},
"type": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "项目类型filepath 表示本地文件路径cnb-repo 表示 CNB 仓库,选填(默认为 filepath",
"type": "string",
"optional": true
},
"repo": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "代码仓库标识,用于搜索结果展示和过滤,格式如 owner/repo例如 kevisual/cnb选填默认自动从 git 配置读取)",

View File

@@ -1,34 +1,59 @@
import { useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { FolderOpenIcon, PlusIcon, Trash2Icon, RefreshCwIcon } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { FolderOpenIcon, PlusIcon, Trash2Icon, RefreshCwIcon, PlayCircleIcon, StopCircleIcon, FolderIcon, AlertCircleIcon, CircleOffIcon } from 'lucide-react';
import { toast } from 'sonner';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useCodeGraphStore } from '../store';
export function ProjectDialog() {
const { projectDialogOpen, setProjectDialogOpen, projects, projectsLoading, loadProjects, addProject, removeProject } =
useCodeGraphStore(
useShallow((s) => ({
projectDialogOpen: s.projectDialogOpen,
setProjectDialogOpen: s.setProjectDialogOpen,
projects: s.projects,
projectsLoading: s.projectsLoading,
loadProjects: s.loadProjects,
addProject: s.addProject,
removeProject: s.removeProject,
})),
);
const {
projectDialogOpen,
setProjectDialogOpen,
projects,
projectsLoading,
loadProjects,
addProject,
removeProject,
toggleProjectStatus,
} = useCodeGraphStore(
useShallow((s) => ({
projectDialogOpen: s.projectDialogOpen,
setProjectDialogOpen: s.setProjectDialogOpen,
projects: s.projects,
projectsLoading: s.projectsLoading,
loadProjects: s.loadProjects,
addProject: s.addProject,
removeProject: s.removeProject,
toggleProjectStatus: s.toggleProjectStatus,
})),
);
const [addLoading, setAddLoading] = useState(false);
const [newPath, setNewPath] = useState('');
const [newPath, setNewPath] = useState('/workspace/projects');
const [newName, setNewName] = useState('');
const [projectType, setProjectType] = useState<'filepath' | 'cnb-repo'>('filepath');
const [showAddProject, setShowAddProject] = useState(false);
// 状态切换确认弹窗
const [statusConfirmOpen, setStatusConfirmOpen] = useState(false);
const [pendingStatusProject, setPendingStatusProject] = useState<{ path: string; name?: string; status?: 'active' | 'inactive' | 'unlive' } | null>(null);
// 点击 unlive 状态按钮
const handleUnliveClick = () => {
toast.info('该功能正在开发中,敬请期待');
};
// 删除确认弹窗
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [pendingDeleteProject, setPendingDeleteProject] = useState<{ path: string; name?: string } | null>(null);
const handleAdd = async () => {
if (!newPath.trim()) return;
setAddLoading(true);
const ok = await addProject(newPath.trim(), newName.trim() || undefined);
const ok = await addProject(newPath.trim(), newName.trim() || undefined, projectType);
if (ok) {
setNewPath('');
setNewName('');
@@ -38,90 +63,249 @@ export function ProjectDialog() {
const handleOpenChange = (open: boolean) => {
setProjectDialogOpen(open);
if (open) loadProjects();
if (open) {
loadProjects();
// 如果项目列表为空,自动显示添加区域
setShowAddProject(projects.length === 0);
}
};
// 打开状态切换确认弹窗
const openStatusConfirm = (project: { path: string; name?: string; status?: 'active' | 'inactive' | 'unlive' }) => {
if (project.status === 'unlive') {
handleUnliveClick();
return;
}
setPendingStatusProject(project);
setStatusConfirmOpen(true);
};
// 确认切换状态
const handleConfirmStatusChange = async () => {
if (!pendingStatusProject) return;
await toggleProjectStatus(pendingStatusProject.path);
setStatusConfirmOpen(false);
setPendingStatusProject(null);
};
// 取消切换状态
const handleCancelStatusChange = () => {
setStatusConfirmOpen(false);
setPendingStatusProject(null);
};
// 打开删除确认弹窗
const openDeleteConfirm = (project: { path: string; name?: string }) => {
setPendingDeleteProject(project);
setDeleteConfirmOpen(true);
};
// 确认删除
const handleConfirmDelete = async () => {
if (!pendingDeleteProject) return;
await removeProject(pendingDeleteProject.path);
setDeleteConfirmOpen(false);
setPendingDeleteProject(null);
};
// 取消删除
const handleCancelDelete = () => {
setDeleteConfirmOpen(false);
setPendingDeleteProject(null);
};
return (
<Dialog open={projectDialogOpen} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-lg bg-slate-900 text-slate-100 border border-white/10'>
<DialogHeader>
<DialogTitle className='flex items-center gap-2 text-slate-100'>
<FolderOpenIcon className='w-4 h-4 text-indigo-400' />
<DialogContent className='sm:max-w-lg bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 border border-white/10 shadow-2xl'>
{/* 装饰性背景 */}
<div className='absolute inset-0 overflow-hidden pointer-events-none'>
<div className='absolute -top-20 -right-20 w-40 h-40 bg-indigo-500/10 rounded-full blur-3xl' />
<div className='absolute -bottom-20 -left-20 w-40 h-40 bg-purple-500/10 rounded-full blur-3xl' />
</div>
<DialogHeader className='relative'>
<DialogTitle className='flex items-center gap-3 text-slate-100 text-lg'>
<div className='p-2 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 shadow-lg shadow-indigo-500/25'>
<FolderOpenIcon className='w-5 h-5 text-white' />
</div>
</DialogTitle>
<DialogDescription className='text-slate-400'></DialogDescription>
<DialogDescription className='text-slate-400 ml-14'>
</DialogDescription>
</DialogHeader>
{/* 新增项目 */}
<div className='space-y-2 rounded-lg bg-slate-800/60 p-3 border border-white/5'>
<p className='text-xs font-medium text-slate-400 mb-2'></p>
<div className='space-y-1.5'>
<Label className='text-xs text-slate-400'> *</Label>
<Input
value={newPath}
onChange={(e) => setNewPath(e.target.value)}
placeholder='/path/to/project'
className='bg-slate-700/60 border-white/10 text-slate-100 placeholder:text-slate-500 h-8 text-xs'
/>
{showAddProject && (
<div className='relative space-y-3 rounded-xl bg-white/5 p-4 border border-white/10 shadow-lg backdrop-blur-sm'>
<div className='flex items-center gap-2 mb-1'>
<div className='w-1 h-3 rounded-full bg-gradient-to-b from-indigo-400 to-purple-500' />
<p className='text-sm font-medium text-slate-200'></p>
</div>
<div className='space-y-1.5'>
<Label className='text-xs text-slate-400'></Label>
<Input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder='My Project'
className='bg-slate-700/60 border-white/10 text-slate-100 placeholder:text-slate-500 h-8 text-xs'
/>
<div className='space-y-2'>
{/* 类型选择 */}
<div className='flex gap-2 mb-2'>
<button
type='button'
onClick={() => {
setProjectType('filepath');
if (!newPath.trim()) {
setNewPath('/workspace/projects');
}
}}
className={`flex-1 text-xs py-1.5 px-3 rounded-lg border transition-all ${
projectType === 'filepath'
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-400'
: 'bg-transparent border-white/10 text-slate-400 hover:border-white/20'
}`}>
</button>
<button
type='button'
onClick={() => {
setProjectType('cnb-repo');
setNewPath('');
}}
className={`flex-1 text-xs py-1.5 px-3 rounded-lg border transition-all ${
projectType === 'cnb-repo'
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-400'
: 'bg-transparent border-white/10 text-slate-400 hover:border-white/20'
}`}>
CNB
</button>
</div>
<div className='space-y-1.5'>
<Label className='text-xs text-slate-400 flex items-center gap-1'>
<span>{projectType === 'filepath' ? '项目路径' : 'CNB 仓库'}</span>
<span className='text-red-400'>*</span>
</Label>
<div className='relative'>
<FolderIcon className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500' />
<Input
value={newPath}
onChange={(e) => setNewPath(e.target.value)}
placeholder={projectType === 'filepath' ? '/workspace/projects' : 'kevisual/cnb'}
className='bg-slate-800/80 border-white/10 text-slate-100 placeholder:text-slate-500 h-9 text-sm pl-10 pr-4 focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all'
/>
</div>
</div>
<div className='space-y-1.5'>
<Label className='text-xs text-slate-400'></Label>
<Input
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder='My Project'
className='bg-slate-800/80 border-white/10 text-slate-100 placeholder:text-slate-500 h-9 text-sm focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all'
/>
</div>
</div>
<Button
size='sm'
onClick={handleAdd}
disabled={addLoading}
className='w-full bg-indigo-600 hover:bg-indigo-500 text-white text-xs h-8 mt-1'>
<PlusIcon className='w-3.5 h-3.5 mr-1' />
{addLoading ? '添加中…' : '添加项目'}
disabled={addLoading || !newPath.trim()}
className='w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white text-sm h-9 mt-2 shadow-lg shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:shadow-indigo-500/40'>
<PlusIcon className='w-4 h-4 mr-1.5' />
{addLoading ? (
<span className='flex items-center gap-2'>
<span className='w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin' />
</span>
) : (
'添加项目'
)}
</Button>
</div>
)}
{/* 项目列表 */}
<div>
<div className='flex items-center justify-between mb-2'>
<p className='text-xs font-medium text-slate-400'></p>
<button
onClick={loadProjects}
disabled={projectsLoading}
className='text-slate-500 hover:text-slate-300 transition-colors'>
<RefreshCwIcon className={`w-3.5 h-3.5 ${projectsLoading ? 'animate-spin' : ''}`} />
</button>
<div className='relative'>
<div className='flex items-center justify-between mb-3'>
<div className='flex items-center gap-2'>
<div className='w-1 h-3 rounded-full bg-gradient-to-b from-green-400 to-emerald-500' />
<p className='text-sm font-medium text-slate-200'></p>
<span className='text-xs text-slate-500 bg-slate-800/80 px-1.5 py-0.5 rounded-full'>
{projects.length}
</span>
</div>
<div className='flex items-center gap-1'>
<button
onClick={() => setShowAddProject(!showAddProject)}
className={`text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 ${showAddProject ? 'text-indigo-400 bg-white/5' : ''}`}
title={showAddProject ? '隐藏添加' : '添加项目'}>
<PlusIcon className={`w-4 h-4 ${showAddProject ? 'rotate-90' : ''} transition-transform`} />
</button>
<button
onClick={loadProjects}
disabled={projectsLoading}
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
title='刷新'>
<RefreshCwIcon className={`w-4 h-4 ${projectsLoading ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{projectsLoading ? (
<div className='flex items-center justify-center h-16 text-slate-500 text-xs'></div>
<div className='flex items-center justify-center h-24 text-slate-500 text-sm bg-white/5 rounded-xl border border-white/5'>
<span className='flex items-center gap-2'>
<span className='w-4 h-4 border-2 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin' />
</span>
</div>
) : projects.length === 0 ? (
<div className='flex items-center justify-center h-16 text-slate-500 text-xs border border-dashed border-white/10 rounded-lg'>
<div className='flex flex-col items-center justify-center h-24 text-slate-500 text-sm bg-white/5 rounded-xl border border-white/5 gap-2'>
<FolderIcon className='w-8 h-8 text-slate-600' />
<span></span>
</div>
) : (
<ul className='space-y-1.5 max-h-56 overflow-y-auto pr-1'>
<ul className='space-y-2 max-h-72 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-transparent scrollbar'>
{projects.map((p) => (
<li
key={p.path}
className='flex items-center gap-2 rounded-md bg-slate-800/60 px-3 py-2 border border-white/5 group'>
<div className='flex-1 min-w-0'>
<p className='text-xs font-medium text-slate-200 truncate'>{p.name ?? p.path.split('/').pop()}</p>
<p className='text-[11px] text-slate-500 truncate'>{p.path}</p>
className='group flex items-center gap-3 rounded-xl bg-white/5 px-4 py-3 border border-white/5 hover:bg-white/10 hover:border-white/10 transition-all duration-200'>
{/* 项目图标 */}
<div className={`shrink-0 p-2 rounded-lg ${p.status === 'active' ? 'bg-green-500/20' : p.status === 'unlive' ? 'bg-orange-500/20' : 'bg-slate-700/50'}`}>
<FolderIcon className={`w-4 h-4 ${p.status === 'active' ? 'text-green-400' : p.status === 'unlive' ? 'text-orange-400' : 'text-slate-400'}`} />
</div>
<div className='flex-1 min-w-0'>
<p className='text-sm font-medium text-slate-200 truncate'>{p.name ?? p.path.split('/').pop()}</p>
<p className='text-xs text-slate-500 truncate'>{p.path}</p>
</div>
{/* 状态切换按钮 */}
{p.status !== undefined && (
<span
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full ${p.status === 'active' ? 'bg-green-900/60 text-green-400' : 'bg-slate-700 text-slate-400'
<button
onClick={() => openStatusConfirm(p)}
className={`shrink-0 flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-full transition-all duration-200 ${p.status === 'active'
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30 hover:scale-105'
: p.status === 'unlive'
? 'bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 hover:scale-105'
: 'bg-slate-700/50 text-slate-400 hover:bg-slate-600/50 hover:scale-105'
}`}>
{p.status === 'active' ? '监听中' : '已停止'}
</span>
{p.status === 'active' ? (
<>
<StopCircleIcon className='w-3 h-3' />
<span></span>
</>
) : p.status === 'unlive' ? (
<>
<CircleOffIcon className='w-3 h-3' />
<span></span>
</>
) : (
<>
<PlayCircleIcon className='w-3 h-3' />
<span></span>
</>
)}
</button>
)}
{/* 删除按钮 */}
<button
onClick={() => removeProject(p.path)}
className='shrink-0 text-slate-600 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100'>
<Trash2Icon className='w-3.5 h-3.5' />
onClick={() => openDeleteConfirm(p)}
className='shrink-0 text-slate-600 hover:text-red-400 transition-all duration-200 p-1.5 rounded-lg hover:bg-red-500/10 opacity-0 group-hover:opacity-100'>
<Trash2Icon className='w-4 h-4' />
</button>
</li>
))}
@@ -129,6 +313,75 @@ export function ProjectDialog() {
)}
</div>
</DialogContent>
{/* 状态切换确认弹窗 */}
<Dialog open={statusConfirmOpen} onOpenChange={(open) => !open && handleCancelStatusChange()}>
<DialogContent className='sm:max-w-md bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 border border-white/10 shadow-2xl'>
<DialogHeader>
<div className={`w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3 ${pendingStatusProject?.status === 'active' ? 'bg-red-500/20' : 'bg-green-500/20'}`}>
{pendingStatusProject?.status === 'active' ? (
<StopCircleIcon className='w-6 h-6 text-red-400' />
) : (
<PlayCircleIcon className='w-6 h-6 text-green-400' />
)}
</div>
<DialogTitle className='text-center text-lg font-semibold'>
{pendingStatusProject?.status === 'active' ? '停止监听' : '开始监听'}
</DialogTitle>
<DialogDescription className='text-center text-slate-400'>
{pendingStatusProject?.status === 'active'
? `确定要停止监听项目「${pendingStatusProject.name ?? pendingStatusProject?.path?.split('/').pop()}」吗?停止后文件修改将不再实时同步。`
: `确定要开始监听项目「${pendingStatusProject?.name ?? pendingStatusProject?.path?.split('/').pop()}」吗?`}
</DialogDescription>
</DialogHeader>
<DialogFooter className='gap-2 sm:justify-center'>
<Button
variant='outline'
onClick={handleCancelStatusChange}
className='bg-transparent border-white/20 text-slate-300 hover:bg-white/10 hover:border-white/30 flex-1'>
</Button>
<Button
onClick={handleConfirmStatusChange}
className={`flex-1 ${pendingStatusProject?.status === 'active'
? 'bg-red-600 hover:bg-red-500 shadow-lg shadow-red-500/25'
: 'bg-green-600 hover:bg-green-500 shadow-lg shadow-green-500/25'
}`}>
{pendingStatusProject?.status === 'active' ? '停止监听' : '开始监听'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 删除确认弹窗 */}
<Dialog open={deleteConfirmOpen} onOpenChange={(open) => !open && handleCancelDelete()}>
<DialogContent className='sm:max-w-md bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 border border-white/10 shadow-2xl'>
<DialogHeader>
<div className='w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3 bg-red-500/20'>
<AlertCircleIcon className='w-6 h-6 text-red-400' />
</div>
<DialogTitle className='text-center text-lg font-semibold'>
</DialogTitle>
<DialogDescription className='text-center text-slate-400'>
{pendingDeleteProject?.name ?? pendingDeleteProject?.path?.split('/').pop()}
</DialogDescription>
</DialogHeader>
<DialogFooter className='gap-2 sm:justify-center'>
<Button
variant='outline'
onClick={handleCancelDelete}
className='bg-transparent border-white/20 text-slate-300 hover:bg-white/10 hover:border-white/30 flex-1'>
</Button>
<Button
onClick={handleConfirmDelete}
className='bg-red-600 hover:bg-red-500 text-white flex-1 shadow-lg shadow-red-500/25'>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog>
);
}

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { FileProjectData } from './modules/tree';
import { useShallow } from 'zustand/react/shallow';
import { DatabaseIcon } from 'lucide-react';
import { DatabaseIcon, RefreshCw } from 'lucide-react';
import { CodePod } from './components/CodePod';
import { useCodeGraphStore } from './store';
import CodeGraphView from './components/CodeGraph';
@@ -17,7 +17,7 @@ export default function CodeGraphPage() {
const layoutStore = useLayoutStore(useShallow((s) => ({
me: s.me,
})));
const { codePodOpen, setCodePodOpen, codePodAttrs, setProjectDialogOpen, init, files } = useCodeGraphStore(
const { codePodOpen, setCodePodOpen, codePodAttrs, setProjectDialogOpen, init, files, fetchProjects } = useCodeGraphStore(
useShallow((s) => ({
files: s.files,
setFiles: s.setFiles,
@@ -26,6 +26,7 @@ export default function CodeGraphPage() {
codePodAttrs: s.codePodAttrs,
setProjectDialogOpen: s.setProjectDialogOpen,
init: s.init,
fetchProjects: s.fetchProjects,
})),
);
@@ -57,10 +58,17 @@ export default function CodeGraphPage() {
</button>
</div>
<div className='h-4 w-px bg-white/10' />
<button
onClick={() => fetchProjects()}
title='刷新'
className='ml-auto flex items-center gap-1.5 px-2 py-1 rounded-md text-slate-400 hover:text-slate-200 hover:bg-white/5 transition-colors'>
<RefreshCw className='w-4 h-4' />
<span className='hidden sm:inline text-xs'></span>
</button>
<button
onClick={() => setProjectDialogOpen(true)}
title='项目管理'
className='ml-auto flex items-center gap-1.5 px-2 py-1 rounded-md text-slate-400 hover:text-slate-200 hover:bg-white/5 transition-colors'>
className='flex items-center gap-1.5 px-2 py-1 rounded-md text-slate-400 hover:text-slate-200 hover:bg-white/5 transition-colors'>
<DatabaseIcon className='w-4 h-4' />
<span className='hidden sm:inline text-xs'></span>
</button>

View File

@@ -11,7 +11,7 @@ export type ProjectItem = {
path: string;
name?: string;
repo?: string;
status?: 'active' | 'inactive';
status?: 'active' | 'inactive' | 'unlive';
};
const API_URL = '/root/v1/cnb-dev';
@@ -40,8 +40,9 @@ type State = {
files: FileProjectData[];
setFiles: (files: FileProjectData[]) => void;
loadProjects: () => Promise<void>;
addProject: (filepath: string, name?: string) => Promise<boolean>;
addProject: (filepath: string, name?: string, type?: 'filepath' | 'cnb-repo') => Promise<boolean>;
removeProject: (path: string) => Promise<void>;
toggleProjectStatus: (path: string) => Promise<void>;
// NodeInfo 弹窗
nodeInfoOpen: boolean;
nodeInfoData: NodeInfoData | null;
@@ -50,6 +51,7 @@ type State = {
closeNodeInfo: () => void;
url?: string;
init(user: UserInfo): Promise<void>;
fetchProjects: () => Promise<void>;
getFiles: (opts?: {
filepath?: string; // 可选的目录路径,默认为根目录
q?: string; // 可选的搜索关键词
@@ -88,11 +90,11 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
set({ projectsLoading: false });
}
},
addProject: async (filepath, name) => {
addProject: async (filepath, name, type = 'filepath') => {
try {
const url = get().url || API_URL;
const res = await projectApi.project.add(
{ filepath, name: name || undefined },
{ filepath, name: name || undefined, type },
{ url },
);
if (res.code === 200) {
@@ -122,6 +124,39 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
toast.error('移除失败');
}
},
toggleProjectStatus: async (path) => {
try {
const url = get().url || API_URL;
const project = get().projects.find((p) => p.path === path);
if (!project) return;
if (project.status === 'active') {
// 暂停项目监听
const res = await projectApi.project.stop({ filepath: path }, { url });
if (res.code === 200) {
toast.success('项目已停止监听');
set((s) => ({
projects: s.projects.map((p) => (p.path === path ? { ...p, status: 'inactive' } : p)),
}));
} else {
toast.error(res.message ?? '操作失败');
}
} else {
// 重新启动项目监听
const res = await projectApi.project.add({ filepath: path }, { url });
if (res.code === 200) {
toast.success('项目已开始监听');
set((s) => ({
projects: s.projects.map((p) => (p.path === path ? { ...p, status: 'active' } : p)),
}));
} else {
toast.error(res.message ?? '操作失败');
}
}
} catch {
toast.error('操作失败');
}
},
nodeInfoOpen: false,
nodeInfoData: null,
nodeInfoPos: { x: 0, y: 0 },
@@ -139,6 +174,9 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
const username = user.username;
const url = username ? `/${username}/v1/cnb-dev` : API_URL;
set({ url });
await get().fetchProjects();
},
fetchProjects: async () => {
get().loadProjects();
const res = await get().getFiles();
if (res.code === 200) {