feat: 增强 CodePod 和 NodeInfo 组件,添加 AI 助手功能和项目路径显示;优化响应式布局
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Save, RefreshCw } from 'lucide-react';
|
||||
import { Save, RefreshCw, Sparkles } from 'lucide-react';
|
||||
import './CodePod.css';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
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 './CodePod.css';
|
||||
import { useCodeGraphStore } from '../store';
|
||||
import { useBotHelperStore } from '../store/bot-helper';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
// ─── 目录树类型 ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -162,15 +163,6 @@ function getLangExtension(filename: string) {
|
||||
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 {
|
||||
@@ -239,6 +231,11 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
||||
if (!filepath) return;
|
||||
await codeGraphStore.saveFile(filepath, fileContent);
|
||||
};
|
||||
|
||||
const handleAIOpen = () => {
|
||||
useBotHelperStore.getState().openModal();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFile) return;
|
||||
if (selectedFile.content) {
|
||||
@@ -315,9 +312,10 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
||||
</span>
|
||||
<div className='ml-1 flex items-center gap-1 shrink-0'>
|
||||
{loading && <span className='text-slate-500 text-xs'>加载中…</span>}
|
||||
{/* 大屏幕显示刷新按钮 */}
|
||||
<button
|
||||
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='刷新'>
|
||||
<RefreshCw size={13} />
|
||||
</button>
|
||||
@@ -327,6 +325,12 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
||||
title='保存'>
|
||||
<Save size={13} />
|
||||
</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>
|
||||
|
||||
|
||||
@@ -18,6 +18,39 @@ const KIND_LABEL: Record<NodeInfoData['kind'], string> = {
|
||||
};
|
||||
|
||||
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(
|
||||
useShallow((s) => ({
|
||||
nodeInfoOpen: s.nodeInfoOpen,
|
||||
@@ -53,6 +86,16 @@ export function NodeInfo() {
|
||||
// 拖拽偏移
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
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 dragStart = useRef({ mx: 0, my: 0, ox: 0, oy: 0 });
|
||||
|
||||
@@ -93,20 +136,22 @@ export function NodeInfo() {
|
||||
|
||||
if (!nodeInfoOpen || !nodeInfoData) return null;
|
||||
|
||||
const posStyle = pinLeft
|
||||
? { 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 };
|
||||
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 }
|
||||
: { left: nodeInfoPos.x + offset.x + 40, top: nodeInfoPos.y + offset.y - 40 };
|
||||
|
||||
const name = nodeInfoData.fullPath.split('/').pop() || nodeInfoData.label;
|
||||
const projectPath = nodeInfoData.projectPath || '';
|
||||
const relativePath = nodeInfoData.fullPath.replace(projectPath + '/', '') || '/';
|
||||
return (
|
||||
<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}
|
||||
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' />
|
||||
<KindIcon kind={nodeInfoData.kind} color={nodeInfoData.color} />
|
||||
<span className='flex-1 text-sm font-medium text-slate-100 truncate' title={name}>
|
||||
@@ -124,7 +169,7 @@ export function NodeInfo() {
|
||||
className='ml-1 text-slate-500 hover:text-emerald-400 transition-colors'>
|
||||
<BotIcon className='size-3.5' />
|
||||
</button>
|
||||
|
||||
|
||||
<span className='text-[10px] text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded'>
|
||||
{KIND_LABEL[nodeInfoData.kind]}
|
||||
</span>
|
||||
@@ -135,32 +180,9 @@ export function NodeInfo() {
|
||||
<XIcon className='size-3.5' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<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>
|
||||
<NodeInfo />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeInfo;
|
||||
export default NodeInfoContainer;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FileProjectData } from './modules/tree';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { DatabaseIcon, RefreshCw } from 'lucide-react';
|
||||
import { CodePod } from './components/CodePod';
|
||||
import { useCodeGraphStore } from './store';
|
||||
import CodeGraphView from './components/CodeGraph';
|
||||
import { Code3DGraph } from './components/Code3DGraph';
|
||||
import { NodeInfo } from './components/NodeInfo';
|
||||
import { NodeInfoContainer } from './components/NodeInfo';
|
||||
import { ProjectDialog } from './components/ProjectDialog';
|
||||
import { BotHelperModal } from './components/BotHelperModal';
|
||||
import { useLayoutStore } from '../auth/store';
|
||||
@@ -90,7 +89,7 @@ export default function CodeGraphPage() {
|
||||
nodeAttrs={codePodAttrs}
|
||||
/>
|
||||
{/* NodeInfo 信息窗 */}
|
||||
<NodeInfo />
|
||||
<NodeInfoContainer />
|
||||
{/* 项目管理弹窗 */}
|
||||
<ProjectDialog />
|
||||
{/* Bot AI 助手弹窗 */}
|
||||
|
||||
Reference in New Issue
Block a user