feat: 增强 CodePod 和 NodeInfo 组件,添加 AI 助手功能和项目路径显示;优化响应式布局

This commit is contained in:
xiongxiao
2026-03-15 21:07:15 +08:00
committed by cnb
parent 9607f84b5a
commit fc533701f6
3 changed files with 71 additions and 46 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Save, RefreshCw } from 'lucide-react'; import { Save, RefreshCw, Sparkles } from 'lucide-react';
import './CodePod.css'; import './CodePod.css';
import CodeMirror from '@uiw/react-codemirror'; import CodeMirror from '@uiw/react-codemirror';
import { vscodeDark } from '@uiw/codemirror-theme-vscode'; import { vscodeDark } from '@uiw/codemirror-theme-vscode';
@@ -13,6 +13,7 @@ import { queryApi as projectApi } from '@/modules/project-api';
import { FileProjectData } from '../modules/tree'; import { FileProjectData } from '../modules/tree';
import './CodePod.css'; import './CodePod.css';
import { useCodeGraphStore } from '../store'; import { useCodeGraphStore } from '../store';
import { useBotHelperStore } from '../store/bot-helper';
import { useShallow } from 'zustand/shallow'; import { useShallow } from 'zustand/shallow';
// ─── 目录树类型 ──────────────────────────────────────────────────────────────── // ─── 目录树类型 ────────────────────────────────────────────────────────────────
@@ -162,15 +163,6 @@ function getLangExtension(filename: string) {
return []; return [];
} }
} }
/** 获取文件内容base64 解码) */
async function fetchFileContent(filepath: string): Promise<string> {
const res = await projectApi['project-file'].get({ filepath });
if (res.code !== 200) return '';
const raw = res.data?.content ?? '';
return raw ? decodeBase64(raw) : '';
}
// ─── 组件 ───────────────────────────────────────────────────────────────────── // ─── 组件 ─────────────────────────────────────────────────────────────────────
interface CodePodProps { interface CodePodProps {
@@ -239,6 +231,11 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
if (!filepath) return; if (!filepath) return;
await codeGraphStore.saveFile(filepath, fileContent); await codeGraphStore.saveFile(filepath, fileContent);
}; };
const handleAIOpen = () => {
useBotHelperStore.getState().openModal();
};
useEffect(() => { useEffect(() => {
if (!selectedFile) return; if (!selectedFile) return;
if (selectedFile.content) { if (selectedFile.content) {
@@ -315,9 +312,10 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
</span> </span>
<div className='ml-1 flex items-center gap-1 shrink-0'> <div className='ml-1 flex items-center gap-1 shrink-0'>
{loading && <span className='text-slate-500 text-xs'></span>} {loading && <span className='text-slate-500 text-xs'></span>}
{/* 大屏幕显示刷新按钮 */}
<button <button
onClick={handleRefresh} onClick={handleRefresh}
className='flex items-center justify-center w-6 h-6 rounded text-slate-400 hover:text-white hover:bg-white/10 transition-colors' className='hidden md:flex items-center justify-center w-6 h-6 rounded text-slate-400 hover:text-white hover:bg-white/10 transition-colors'
title='刷新'> title='刷新'>
<RefreshCw size={13} /> <RefreshCw size={13} />
</button> </button>
@@ -327,6 +325,12 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
title='保存'> title='保存'>
<Save size={13} /> <Save size={13} />
</button> </button>
<button
onClick={handleAIOpen}
className='flex items-center justify-center w-6 h-6 rounded text-slate-400 hover:text-white hover:bg-white/10 transition-colors'
title='AI 助手'>
<Sparkles size={13} />
</button>
</div> </div>
</div> </div>

View File

@@ -18,6 +18,39 @@ const KIND_LABEL: Record<NodeInfoData['kind'], string> = {
}; };
export function NodeInfo() { export function NodeInfo() {
const codeGraphStore = useCodeGraphStore(
useShallow((s) => ({
nodeInfoData: s.nodeInfoData,
})),
);
const projectPath = codeGraphStore.nodeInfoData?.projectPath || '';
const relativePath = codeGraphStore.nodeInfoData?.fullPath.replace(projectPath + '/', '') || '/';
return (<> {/* 内容 */}
<div className='px-3 py-2.5 flex flex-col gap-2'>
{/* projectPath */}
<div className='flex flex-col gap-0.5'>
<span className='text-[10px] uppercase tracking-wide text-slate-500 font-medium'></span>
<span
className='text-xs text-slate-400 break-all leading-relaxed font-mono'
title={projectPath}>
{projectPath}
</span>
</div>
{/* relativePath */}
<div className='flex flex-col gap-0.5'>
<span className='text-[10px] uppercase tracking-wide text-slate-500 font-medium'></span>
<span
className='text-xs text-slate-300 break-all leading-relaxed font-mono'
title={relativePath}>
{relativePath}
</span>
</div>
</div></>)
}
export const NodeInfoContainer = () => {
const { nodeInfoOpen, nodeInfoData, nodeInfoPos, closeNodeInfo, setCodePodOpen, setCodePodAttrs } = useCodeGraphStore( const { nodeInfoOpen, nodeInfoData, nodeInfoPos, closeNodeInfo, setCodePodOpen, setCodePodAttrs } = useCodeGraphStore(
useShallow((s) => ({ useShallow((s) => ({
nodeInfoOpen: s.nodeInfoOpen, nodeInfoOpen: s.nodeInfoOpen,
@@ -53,6 +86,16 @@ export function NodeInfo() {
// 拖拽偏移 // 拖拽偏移
const [offset, setOffset] = useState({ x: 0, y: 0 }); const [offset, setOffset] = useState({ x: 0, y: 0 });
const [pinLeft, setPinLeft] = useState(false); // 编辑后固定到右下角 const [pinLeft, setPinLeft] = useState(false); // 编辑后固定到右下角
const [isMobile, setIsMobile] = useState(false);
// 检测屏幕大小
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const dragging = useRef(false); const dragging = useRef(false);
const dragStart = useRef({ mx: 0, my: 0, ox: 0, oy: 0 }); const dragStart = useRef({ mx: 0, my: 0, ox: 0, oy: 0 });
@@ -93,7 +136,9 @@ export function NodeInfo() {
if (!nodeInfoOpen || !nodeInfoData) return null; if (!nodeInfoOpen || !nodeInfoData) return null;
const posStyle = pinLeft const posStyle = isMobile
? { inset: 0, width: '100%', height: '100%' }
: pinLeft
? { right: 10 - offset.x, bottom: 10 - offset.y, top: 'auto' as const, left: 'auto' as const } ? { right: 10 - offset.x, bottom: 10 - offset.y, top: 'auto' as const, left: 'auto' as const }
: { left: nodeInfoPos.x + offset.x + 40, top: nodeInfoPos.y + offset.y - 40 }; : { left: nodeInfoPos.x + offset.x + 40, top: nodeInfoPos.y + offset.y - 40 };
@@ -102,11 +147,11 @@ export function NodeInfo() {
const relativePath = nodeInfoData.fullPath.replace(projectPath + '/', '') || '/'; const relativePath = nodeInfoData.fullPath.replace(projectPath + '/', '') || '/';
return ( return (
<div <div
className='fixed z-50 w-72 rounded-xl border border-white/10 bg-slate-900/95 backdrop-blur-sm shadow-2xl select-none' className={`fixed z-50 rounded-xl border border-white/10 bg-slate-900/95 backdrop-blur-sm shadow-2xl select-none ${isMobile ? 'w-full h-full' : 'w-72'}`}
style={posStyle} style={posStyle}
onMouseDown={onMouseDown}> onMouseDown={isMobile ? undefined : onMouseDown}>
{/* 标题栏 */} {/* 标题栏 */}
<div className='flex items-center gap-2 px-3 py-2.5 border-b border-white/10 cursor-grab active:cursor-grabbing'> <div className={`flex items-center gap-2 px-3 py-2.5 border-b border-white/10 ${!isMobile ? 'cursor-grab active:cursor-grabbing' : ''}`}>
<MoveIcon className='size-3 text-slate-600 shrink-0' /> <MoveIcon className='size-3 text-slate-600 shrink-0' />
<KindIcon kind={nodeInfoData.kind} color={nodeInfoData.color} /> <KindIcon kind={nodeInfoData.kind} color={nodeInfoData.color} />
<span className='flex-1 text-sm font-medium text-slate-100 truncate' title={name}> <span className='flex-1 text-sm font-medium text-slate-100 truncate' title={name}>
@@ -135,32 +180,9 @@ export function NodeInfo() {
<XIcon className='size-3.5' /> <XIcon className='size-3.5' />
</button> </button>
</div> </div>
<NodeInfo />
{/* 内容 */}
<div className='px-3 py-2.5 flex flex-col gap-2'>
{/* projectPath */}
<div className='flex flex-col gap-0.5'>
<span className='text-[10px] uppercase tracking-wide text-slate-500 font-medium'></span>
<span
className='text-xs text-slate-400 break-all leading-relaxed font-mono'
title={projectPath}>
{projectPath}
</span>
</div>
{/* relativePath */}
<div className='flex flex-col gap-0.5'>
<span className='text-[10px] uppercase tracking-wide text-slate-500 font-medium'></span>
<span
className='text-xs text-slate-300 break-all leading-relaxed font-mono'
title={relativePath}>
{relativePath}
</span>
</div>
</div>
</div> </div>
); );
} }
export default NodeInfo; export default NodeInfoContainer;

View File

@@ -1,12 +1,11 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { FileProjectData } from './modules/tree';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { DatabaseIcon, RefreshCw } from 'lucide-react'; import { DatabaseIcon, RefreshCw } from 'lucide-react';
import { CodePod } from './components/CodePod'; import { CodePod } from './components/CodePod';
import { useCodeGraphStore } from './store'; import { useCodeGraphStore } from './store';
import CodeGraphView from './components/CodeGraph'; import CodeGraphView from './components/CodeGraph';
import { Code3DGraph } from './components/Code3DGraph'; import { Code3DGraph } from './components/Code3DGraph';
import { NodeInfo } from './components/NodeInfo'; import { NodeInfoContainer } from './components/NodeInfo';
import { ProjectDialog } from './components/ProjectDialog'; import { ProjectDialog } from './components/ProjectDialog';
import { BotHelperModal } from './components/BotHelperModal'; import { BotHelperModal } from './components/BotHelperModal';
import { useLayoutStore } from '../auth/store'; import { useLayoutStore } from '../auth/store';
@@ -90,7 +89,7 @@ export default function CodeGraphPage() {
nodeAttrs={codePodAttrs} nodeAttrs={codePodAttrs}
/> />
{/* NodeInfo 信息窗 */} {/* NodeInfo 信息窗 */}
<NodeInfo /> <NodeInfoContainer />
{/* 项目管理弹窗 */} {/* 项目管理弹窗 */}
<ProjectDialog /> <ProjectDialog />
{/* Bot AI 助手弹窗 */} {/* Bot AI 助手弹窗 */}