feat: 更新 README,添加 3D 图展示和项目结构;增强 Code3DGraph 组件,支持节点发光效果和屏幕坐标投影;优化 CodePod 组件,增加文件保存和刷新功能;调整 NodeInfo 组件位置;扩展状态管理,增加保存文件功能
This commit is contained in:
47
README.md
47
README.md
@@ -1,20 +1,41 @@
|
|||||||
# vite-react-template
|
# code-graph
|
||||||
|
|
||||||
## download template
|
使用 Graphology 分析代码依赖关系,生成关系图谱,并使用 SigmaJS 可视化展示。
|
||||||
|
|
||||||
|
使用3d图展示
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 代码依赖关系分析
|
||||||
|
- 关系图可视化
|
||||||
|
- 交互式图谱探索
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **Graphology** - 图分析库
|
||||||
|
- **SigmaJS** - 图可视化
|
||||||
|
- **React** + **Vite**
|
||||||
|
- **TanStack Router** - 路由
|
||||||
|
- **TailwindCSS v4** - 样式
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ev sync clone -i https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template
|
# 安装依赖
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## clone auth update
|
## 项目结构
|
||||||
|
|
||||||
```bash
|
|
||||||
ev sync clone -l -i https://kevisual.cn/root/ai/kevisual/frontend/vite-react-template/public/auth.json
|
|
||||||
```
|
```
|
||||||
|
src/
|
||||||
## code-graph
|
├── components/ui/ # UI 组件
|
||||||
|
├── lib/ # 工具函数
|
||||||
使用graphology分析代码依赖关系,生成关系。
|
├── modules/ # 应用模块
|
||||||
|
├── pages/ # 页面组件
|
||||||
使用sigmajs可视化关系图。
|
├── routes/ # 路由配置
|
||||||
|
└── styles/ # 样式文件
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import ForceGraph3D from '3d-force-graph';
|
import ForceGraph3D from '3d-force-graph';
|
||||||
import type { NodeObject, LinkObject, ForceGraph3DInstance } from '3d-force-graph';
|
import type { NodeObject, LinkObject, ForceGraph3DInstance } from '3d-force-graph';
|
||||||
|
import * as THREE from 'three';
|
||||||
import { FileProjectData } from '../modules/tree';
|
import { FileProjectData } from '../modules/tree';
|
||||||
import { NodeSearchEntry } from '../modules/graph';
|
import { NodeSearchEntry } from '../modules/graph';
|
||||||
import { NodeSearchBox, NodeSearchBoxHandle } from './NodeSearchBox';
|
import { NodeSearchBox, NodeSearchBoxHandle } from './NodeSearchBox';
|
||||||
@@ -189,19 +190,26 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
const searchBoxRef = useRef<NodeSearchBoxHandle>(null);
|
const searchBoxRef = useRef<NodeSearchBoxHandle>(null);
|
||||||
const [searchIndex, setSearchIndex] = useState<NodeSearchEntry[]>([]);
|
const [searchIndex, setSearchIndex] = useState<NodeSearchEntry[]>([]);
|
||||||
|
|
||||||
const { setNodeInfo } = useCodeGraphStore(
|
const { setNodeInfo, selectedNodeId } = useCodeGraphStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
setNodeInfo: s.setNodeInfo,
|
setNodeInfo: s.setNodeInfo,
|
||||||
|
selectedNodeId: s.nodeInfoData?.fullPath ?? null,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 用 ref 避免 nodeThreeObject 回调的陈旧闭包
|
||||||
|
const selectedNodeIdRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
selectedNodeIdRef.current = selectedNodeId;
|
||||||
|
}, [selectedNodeId]);
|
||||||
|
|
||||||
// 节点跳转
|
// 节点跳转
|
||||||
const focusNode = useCallback((nodeKey: string) => {
|
const focusNode = useCallback((nodeKey: string) => {
|
||||||
const graph = graphRef.current;
|
const graph = graphRef.current;
|
||||||
if (!graph) return;
|
if (!graph) return;
|
||||||
const node = (graph.graphData().nodes as Graph3DNode[]).find((n) => n.id === nodeKey);
|
const node = (graph.graphData().nodes as Graph3DNode[]).find((n) => n.id === nodeKey);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const distance = 80;
|
const distance = 160;
|
||||||
const distRatio = 1 + distance / Math.hypot((node.x ?? 0), (node.y ?? 0), (node.z ?? 0));
|
const distRatio = 1 + distance / Math.hypot((node.x ?? 0), (node.y ?? 0), (node.z ?? 0));
|
||||||
graph.cameraPosition(
|
graph.cameraPosition(
|
||||||
{ x: (node.x ?? 0) * distRatio, y: (node.y ?? 0) * distRatio, z: (node.z ?? 0) * distRatio },
|
{ x: (node.x ?? 0) * distRatio, y: (node.y ?? 0) * distRatio, z: (node.z ?? 0) * distRatio },
|
||||||
@@ -210,6 +218,22 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 将节点世界坐标投影为屏幕坐标
|
||||||
|
const nodeToScreenPos = useCallback((node: Graph3DNode): { x: number; y: number } => {
|
||||||
|
const graph = graphRef.current;
|
||||||
|
if (!graph) return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||||
|
const camera = graph.camera();
|
||||||
|
const renderer = (graph as any).renderer?.() as THREE.WebGLRenderer | undefined;
|
||||||
|
if (!camera || !renderer) return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||||
|
const vec = new THREE.Vector3(node.x ?? 0, node.y ?? 0, node.z ?? 0);
|
||||||
|
vec.project(camera);
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: ((vec.x + 1) / 2) * rect.width + rect.left,
|
||||||
|
y: ((-vec.y + 1) / 2) * rect.height + rect.top,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 初始化 & 数据更新
|
// 初始化 & 数据更新
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
@@ -231,31 +255,65 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
.nodeRelSize(1)
|
.nodeRelSize(1)
|
||||||
.nodeVal((node) => {
|
.nodeVal((node) => {
|
||||||
const n = node as Graph3DNode;
|
const n = node as Graph3DNode;
|
||||||
return n.nodeSize * n.nodeSize;
|
return (n.nodeSize ?? 3) * (n.nodeSize ?? 3);
|
||||||
})
|
})
|
||||||
.nodeColor((node) => (node as Graph3DNode).color)
|
.nodeColor((node) => (node as Graph3DNode).color)
|
||||||
.nodeLabel((node) => (node as Graph3DNode).label)
|
.nodeLabel((node) => (node as Graph3DNode).label)
|
||||||
|
.nodeThreeObject(((node: Graph3DNode) => {
|
||||||
|
const n = node as Graph3DNode;
|
||||||
|
if (!n?.fullPath || n.fullPath !== selectedNodeIdRef.current) return null;
|
||||||
|
|
||||||
|
const geometry = new THREE.SphereGeometry(n.nodeSize * 1.2, 32, 32);
|
||||||
|
const material = new THREE.MeshStandardMaterial({
|
||||||
|
color: n.color,
|
||||||
|
emissive: n.color,
|
||||||
|
emissiveIntensity: 1.5,
|
||||||
|
roughness: 0.3,
|
||||||
|
metalness: 0.5,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
|
||||||
|
const glowGeometry = new THREE.SphereGeometry(n.nodeSize * 2, 32, 32);
|
||||||
|
const glowMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
color: n.color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.25,
|
||||||
|
});
|
||||||
|
const glowMesh = new THREE.Mesh(glowGeometry, glowMaterial);
|
||||||
|
mesh.add(glowMesh);
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}) as any)
|
||||||
.linkWidth(0)
|
.linkWidth(0)
|
||||||
.linkColor(() => 'rgba(255,255,255,0.6)')
|
.linkColor(() => 'rgba(255,255,255,0.6)')
|
||||||
.linkDirectionalParticles(2)
|
.linkDirectionalParticles(2)
|
||||||
.linkDirectionalParticleWidth(1.5)
|
.linkDirectionalParticleWidth(1.5)
|
||||||
.linkDirectionalParticleSpeed(0.004)
|
.linkDirectionalParticleSpeed(0.004)
|
||||||
.linkDirectionalParticleColor(() => '#ffffff')
|
.linkDirectionalParticleColor(() => '#ffffff')
|
||||||
.onNodeClick((node, event) => {
|
.onNodeClick((node) => {
|
||||||
|
if (!node) return;
|
||||||
const n = node as Graph3DNode;
|
const n = node as Graph3DNode;
|
||||||
|
if (!n.fullPath) return;
|
||||||
focusNode(n.id);
|
focusNode(n.id);
|
||||||
setNodeInfo(
|
// 等相机飞行动画结束(800ms)后,用节点投影坐标打开 NodeInfo
|
||||||
{
|
setTimeout(() => {
|
||||||
label: n.label,
|
const pos = nodeToScreenPos(n);
|
||||||
fullPath: n.fullPath,
|
setNodeInfo(
|
||||||
projectPath: n.projectPath,
|
{
|
||||||
kind: n.kind,
|
label: n.label,
|
||||||
color: n.color,
|
fullPath: n.fullPath,
|
||||||
fileId: n.fileId,
|
projectPath: n.projectPath,
|
||||||
nodeSize: n.nodeSize,
|
kind: n.kind,
|
||||||
},
|
color: n.color,
|
||||||
{ x: event.clientX, y: event.clientY },
|
fileId: n.fileId,
|
||||||
);
|
nodeSize: n.nodeSize,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}, 850);
|
||||||
})
|
})
|
||||||
.onNodeHover((node) => {
|
.onNodeHover((node) => {
|
||||||
if (el) el.style.cursor = node ? 'pointer' : 'default';
|
if (el) el.style.cursor = node ? 'pointer' : 'default';
|
||||||
@@ -287,7 +345,7 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
return () => {
|
return () => {
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
};
|
};
|
||||||
}, [files, focusNode, setNodeInfo]);
|
}, [files, focusNode, setNodeInfo, nodeToScreenPos]);
|
||||||
|
|
||||||
// 卸载时销毁
|
// 卸载时销毁
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -297,6 +355,14 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 选中节点变化时刷新节点渲染以更新发光效果
|
||||||
|
useEffect(() => {
|
||||||
|
const graph = graphRef.current;
|
||||||
|
if (graph) {
|
||||||
|
graph.refresh();
|
||||||
|
}
|
||||||
|
}, [selectedNodeId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full h-full overflow-hidden ${className ?? ''}`}>
|
<div className={`relative w-full h-full overflow-hidden ${className ?? ''}`}>
|
||||||
<div ref={containerRef} className='w-full h-full' />
|
<div ref={containerRef} className='w-full h-full' />
|
||||||
@@ -304,7 +370,28 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
<NodeSearchBox
|
<NodeSearchBox
|
||||||
ref={searchBoxRef}
|
ref={searchBoxRef}
|
||||||
searchIndex={searchIndex}
|
searchIndex={searchIndex}
|
||||||
onSelect={(entry) => focusNode(entry.nodeKey)}
|
onSelect={(entry) => {
|
||||||
|
focusNode(entry.nodeKey);
|
||||||
|
const graph = graphRef.current;
|
||||||
|
if (!graph) return;
|
||||||
|
const n = (graph.graphData().nodes as Graph3DNode[]).find((nd) => nd.id === entry.nodeKey);
|
||||||
|
if (!n) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
const pos = nodeToScreenPos(n);
|
||||||
|
setNodeInfo(
|
||||||
|
{
|
||||||
|
label: n.label,
|
||||||
|
fullPath: n.fullPath,
|
||||||
|
projectPath: n.projectPath,
|
||||||
|
kind: n.kind,
|
||||||
|
color: n.color,
|
||||||
|
fileId: n.fileId,
|
||||||
|
nodeSize: n.nodeSize,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}, 850);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { use, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Save, RefreshCw } 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';
|
||||||
@@ -189,7 +190,12 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
const rootPath = nodeAttrs?.fullPath ?? '';
|
const rootPath = nodeAttrs?.fullPath ?? '';
|
||||||
const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
|
const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
|
||||||
getFiles: s.getFiles,
|
getFiles: s.getFiles,
|
||||||
|
saveFile: s.saveFile,
|
||||||
})));
|
})));
|
||||||
|
const projectPath = nodeAttrs?.projectPath ?? '';
|
||||||
|
const displayPath = rootPath.startsWith(projectPath)
|
||||||
|
? rootPath.slice(projectPath.length) || '/'
|
||||||
|
: rootPath;
|
||||||
|
|
||||||
// 打开时重置
|
// 打开时重置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -198,10 +204,10 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
setDirFiles([]);
|
setDirFiles([]);
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setFileContent('');
|
setFileContent('');
|
||||||
init(nodeAttrs);
|
refresh(nodeAttrs);
|
||||||
setSidebarOpen(isDir ? true : false);
|
setSidebarOpen(isDir ? true : false);
|
||||||
}, [open, nodeAttrs]);
|
}, [open, nodeAttrs]);
|
||||||
const init = async (nodeAttrs: GraphNode) => {
|
const refresh = async (nodeAttrs: GraphNode) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await codeGraphStore.getFiles({
|
const res = await codeGraphStore.getFiles({
|
||||||
filepath: nodeAttrs.fullPath,
|
filepath: nodeAttrs.fullPath,
|
||||||
@@ -218,7 +224,21 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
if (fileList.length === 0) return;
|
if (fileList.length === 0) return;
|
||||||
const file = fileList[0];
|
const file = fileList[0];
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
if (!nodeAttrs) return;
|
||||||
|
setDirFiles([]);
|
||||||
|
setSelectedFile(null);
|
||||||
|
setFileContent('');
|
||||||
|
refresh(nodeAttrs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const filepath = selectedFile?.filepath ?? (isDir ? '' : nodeAttrs?.fullPath ?? '');
|
||||||
|
if (!filepath) return;
|
||||||
|
await codeGraphStore.saveFile(filepath, fileContent);
|
||||||
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
if (selectedFile.content) {
|
if (selectedFile.content) {
|
||||||
@@ -255,11 +275,11 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
<aside
|
<aside
|
||||||
className={`${sidebarOpen ? 'w-56 sm:w-64' : 'w-0'
|
className={`${sidebarOpen ? 'w-56 sm:w-64' : 'w-0'
|
||||||
} shrink-0 border-r border-white/10 bg-slate-950 flex flex-col overflow-hidden transition-all duration-200`}>
|
} shrink-0 border-r border-white/10 bg-slate-950 flex flex-col overflow-hidden transition-all duration-200`}>
|
||||||
{/* 标题:显示 rootPath */}
|
{/* 标题:显示相对路径(去掉 projectPath) */}
|
||||||
<div className='px-3 py-2.5 border-b border-white/10 shrink-0 min-w-[14rem]'>
|
<div className='px-3 py-2.5 border-b border-white/10 shrink-0 min-w-[14rem]'>
|
||||||
<div className='text-[10px] text-slate-500 mb-0.5'>路径</div>
|
<div className='text-[10px] text-slate-500 mb-0.5'>路径</div>
|
||||||
<div className='text-xs font-semibold text-slate-300 truncate' title={rootPath}>
|
<div className='text-xs font-semibold text-slate-300 truncate' title={rootPath}>
|
||||||
{rootPath}
|
{displayPath}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 目录树 */}
|
{/* 目录树 */}
|
||||||
@@ -293,7 +313,21 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
<span className='truncate text-slate-200'>
|
<span className='truncate text-slate-200'>
|
||||||
{selectedFile?.filepath ?? nodeAttrs.fullPath}
|
{selectedFile?.filepath ?? nodeAttrs.fullPath}
|
||||||
</span>
|
</span>
|
||||||
{loading && <span className='ml-auto text-slate-500'>加载中…</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'
|
||||||
|
title='刷新'>
|
||||||
|
<RefreshCw size={13} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className='flex items-center justify-center w-6 h-6 rounded text-slate-400 hover:text-white hover:bg-white/10 transition-colors'
|
||||||
|
title='保存'>
|
||||||
|
<Save size={13} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CodeMirror */}
|
{/* CodeMirror */}
|
||||||
@@ -303,7 +337,7 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
height='100%'
|
height='100%'
|
||||||
theme={vscodeDark}
|
theme={vscodeDark}
|
||||||
extensions={langExt}
|
extensions={langExt}
|
||||||
readOnly
|
onChange={(val) => setFileContent(val)}
|
||||||
className='scrollbar'
|
className='scrollbar'
|
||||||
basicSetup={{
|
basicSetup={{
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function NodeInfo() {
|
|||||||
|
|
||||||
const posStyle = pinLeft
|
const posStyle = 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 + 16, top: nodeInfoPos.y + offset.y - 16 };
|
: { left: nodeInfoPos.x + offset.x + 40, top: nodeInfoPos.y + offset.y - 40 };
|
||||||
|
|
||||||
const name = nodeInfoData.fullPath.split('/').pop() || nodeInfoData.label;
|
const name = nodeInfoData.fullPath.split('/').pop() || nodeInfoData.label;
|
||||||
const projectPath = nodeInfoData.projectPath || '';
|
const projectPath = nodeInfoData.projectPath || '';
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type State = {
|
|||||||
getContent?: boolean; // 是否获取文件内容,默认为 false
|
getContent?: boolean; // 是否获取文件内容,默认为 false
|
||||||
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
||||||
createQuestion: (opts: { question: string, projectPath: string, engine?: 'openclaw' | 'opencode' }) => any;
|
createQuestion: (opts: { question: string, projectPath: string, engine?: 'openclaw' | 'opencode' }) => any;
|
||||||
|
saveFile: (filepath: string, content: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCodeGraphStore = create<State>()((set, get) => ({
|
export const useCodeGraphStore = create<State>()((set, get) => ({
|
||||||
@@ -130,7 +131,7 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
nodeInfoData: data,
|
nodeInfoData: data,
|
||||||
nodeInfoPos: pos ?? { x: 0, y: 0 },
|
nodeInfoPos: pos ?? { x: 0, y: 0 },
|
||||||
}),
|
}),
|
||||||
closeNodeInfo: () => set({ nodeInfoOpen: false }),
|
closeNodeInfo: () => set({ nodeInfoOpen: false, nodeInfoData: null }),
|
||||||
url: API_URL,
|
url: API_URL,
|
||||||
init: async (user) => {
|
init: async (user) => {
|
||||||
// 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等
|
// 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等
|
||||||
@@ -161,6 +162,20 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
saveFile: async (filepath, content) => {
|
||||||
|
const url = get().url || API_URL;
|
||||||
|
try {
|
||||||
|
const b64 = btoa(new TextEncoder().encode(content).reduce((s, b) => s + String.fromCharCode(b), ''));
|
||||||
|
const res = await projectApi['project-file']['update-content']({ filepath, content: b64 }, { url });
|
||||||
|
if (res.code === 200) {
|
||||||
|
toast.success('保存成功');
|
||||||
|
} else {
|
||||||
|
toast.error(res.message ?? '保存失败');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('保存失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
createQuestion: async (opts) => {
|
createQuestion: async (opts) => {
|
||||||
const { question, projectPath, engine = 'opencode' } = opts;
|
const { question, projectPath, engine = 'opencode' } = opts;
|
||||||
const url = get().url
|
const url = get().url
|
||||||
@@ -169,6 +184,7 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
项目路径: ${projectPath}`
|
项目路径: ${projectPath}`
|
||||||
const res = await opencodeApi["opencode-cnb"].question({
|
const res = await opencodeApi["opencode-cnb"].question({
|
||||||
question: q,
|
question: q,
|
||||||
|
directory: projectPath,
|
||||||
}, {
|
}, {
|
||||||
url
|
url
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user