update
This commit is contained in:
@@ -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 配置读取)",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user