feat: 增强 CodePod 和 NodeInfo 组件,添加 AI 助手功能和项目路径显示;优化响应式布局
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 助手弹窗 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user