Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
0959e43b20
|
@ -3,6 +3,7 @@ backup
|
|||
# dependencies
|
||||
**/node_modules/
|
||||
volumes
|
||||
web-dist
|
||||
/.pnp
|
||||
.pnp.js
|
||||
*.tar
|
||||
|
@ -69,4 +70,4 @@ yarn-error.log*
|
|||
**/.idea/
|
||||
uploads
|
||||
packages/mind-elixir-core
|
||||
config/nginx/conf.d/web.conf
|
||||
config/nginx/conf.d/web.conf
|
||||
|
|
|
@ -108,21 +108,15 @@ CMD ["/usr/bin/entrypoint.sh"]
|
|||
|
||||
# 使用 Nginx 的 Alpine 版本作为基础镜像
|
||||
FROM nginx:stable-alpine as nginx
|
||||
|
||||
# 替换 Alpine 的软件源为阿里云镜像
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# 设置环境变量
|
||||
ENV NODE_ENV production
|
||||
|
||||
# 安装 envsubst 和 inotify-tools
|
||||
RUN apk add --no-cache gettext inotify-tools
|
||||
|
||||
# 创建 /data/uploads 目录
|
||||
RUN mkdir -p /data/uploads
|
||||
|
||||
# 暴露 80 端口
|
||||
EXPOSE 80
|
|
@ -7,12 +7,14 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script>
|
||||
window.env = {
|
||||
VITE_APP_SERVER_IP: "$VITE_APP_SERVER_IP",
|
||||
VITE_APP_APP_NAME: "$VITE_APP_APP_NAME",
|
||||
VITE_APP_VERSION: "$VITE_APP_VERSION",
|
||||
VITE_APP_SERVER_IP: "$SERVER_IP",
|
||||
VITE_APP_SERVER_IP: "$SERVER_PORT",
|
||||
VITE_APP_APP_NAME: "$APP_NAME",
|
||||
VITE_APP_VERSION: "$VERSION",
|
||||
VITE_APP_FILE_PORT: "$FILE_PORT",
|
||||
};
|
||||
</script>
|
||||
<title>%VITE_APP_APP_NAME%</title>
|
||||
<title>$APP_NAME</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
0
apps/web/src/app/main/my-duty-path/components/MyDutyPathContainer.tsx
Normal file → Executable file
0
apps/web/src/app/main/my-duty-path/components/MyDutyPathContainer.tsx
Normal file → Executable file
0
apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx
Normal file → Executable file
0
apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx
Normal file → Executable file
0
apps/web/src/components/models/course/detail/CourseOperationBtns/JoinButton.tsx
Normal file → Executable file
0
apps/web/src/components/models/course/detail/CourseOperationBtns/JoinButton.tsx
Normal file → Executable file
|
@ -0,0 +1,12 @@
|
|||
import { api } from "@nice/client";
|
||||
|
||||
export default function PostSelect() {
|
||||
const { data } = api.post.findMany.useQuery({
|
||||
where: {
|
||||
title: {
|
||||
contains: ""
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"name": "@nice/ui",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@nice/utils": "workspace:^",
|
||||
"@xyflow/react": "^12.3.6",
|
||||
"dagre": "^0.8.5",
|
||||
"nanoid": "^5.0.9",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dagre": "^0.7.52",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/react": "18.2.38",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"concurrently": "^8.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
import { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Controls,
|
||||
Background,
|
||||
useReactFlow,
|
||||
Panel,
|
||||
ReactFlowProvider,
|
||||
NodeOrigin,
|
||||
ConnectionLineType,
|
||||
useStoreApi,
|
||||
InternalNode,
|
||||
} from '@xyflow/react';
|
||||
import MindMapNode from './MindMapNode';
|
||||
import useMindMapStore, { RFState } from './store';
|
||||
import { shallow, useShallow } from 'zustand/shallow';
|
||||
import MindMapEdge from './MindMapEdge';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useFlowKeyboardControls } from './hooks/useFlowKeyboardControl';
|
||||
|
||||
const selector = (state: RFState) => ({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges,
|
||||
onNodesChange: state.onNodesChange,
|
||||
onEdgesChange: state.onEdgesChange,
|
||||
addChildNode: state.addChildNode,
|
||||
addSiblingNode: state.addSiblingNode,
|
||||
selectedNodeId: state.selectedNodeId,
|
||||
setSelectedNodeIdId: state.setSelectedNodeId,
|
||||
undo: state.undo,
|
||||
redo: state.redo,
|
||||
canUndo: state.canUndo,
|
||||
canRedo: state.canRedo
|
||||
|
||||
});
|
||||
const nodeOrigin: NodeOrigin = [0.5, 0.5];
|
||||
// 节点类型定义
|
||||
const nodeTypes = {
|
||||
mindmap: MindMapNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
mindmap: MindMapEdge,
|
||||
};
|
||||
const connectionLineStyle = {
|
||||
stroke: '#999',
|
||||
strokeWidth: 2,
|
||||
radius: 20 // Add corner radius for orthogonal lines
|
||||
};
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
style: connectionLineStyle,
|
||||
type: 'mindmap',
|
||||
animated: false
|
||||
};
|
||||
export function Flow() {
|
||||
const { nodes, edges, onNodesChange, undo, redo, setSelectedNodeIdId, onEdgesChange, addChildNode, addSiblingNode, selectedNodeId } = useMindMapStore(
|
||||
useShallow(selector)
|
||||
);
|
||||
useFlowKeyboardControls()
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
nodeOrigin={nodeOrigin}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
fitView
|
||||
panOnDrag={[2]}
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
nodesConnectable={false}
|
||||
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<Panel position="top-left">React Flow Mind Map</Panel>
|
||||
</ReactFlow>
|
||||
);
|
||||
}
|
||||
export function MindMap() {
|
||||
return <ReactFlowProvider>
|
||||
<Flow></Flow>
|
||||
</ReactFlowProvider>
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { BaseEdge, EdgeProps, getBezierPath, getStraightPath } from '@xyflow/react';
|
||||
|
||||
function MindMapEdge(props: EdgeProps) {
|
||||
const { sourceX, sourceY, targetX, targetY } = props;
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
return <BaseEdge path={edgePath} {...props} />;
|
||||
}
|
||||
|
||||
export default MindMapEdge;
|
|
@ -1,83 +0,0 @@
|
|||
import { Handle, NodeProps, Position, useEdges } from '@xyflow/react';
|
||||
import { MindMapNodeType } from './types';
|
||||
import useMindMapStore, { RFState } from './store';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
const selector = (state: RFState) => ({
|
||||
selectedNodeId: state.selectedNodeId,
|
||||
editingNodeId: state.editingNodeId,
|
||||
updateNodeLabel: state.updateNodeLabel,
|
||||
setSelectedNodeId: state.setSelectedNodeId,
|
||||
setEditingNodeId: state.setEditingNodeId
|
||||
|
||||
});
|
||||
function MindMapNode({ id, data }: NodeProps<MindMapNodeType>) {
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(data.label);
|
||||
|
||||
const {
|
||||
updateNodeLabel,
|
||||
selectedNodeId,
|
||||
setSelectedNodeId,
|
||||
setEditingNodeId,
|
||||
editingNodeId
|
||||
} = useMindMapStore(useShallow(selector));
|
||||
useEffect(() => {
|
||||
if (editingNodeId === id) {
|
||||
setEditingNodeId(id);
|
||||
setInputValue(data.label);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 0);
|
||||
} else {
|
||||
inputRef.current?.blur()
|
||||
}
|
||||
}, [editingNodeId])
|
||||
const handleDoubleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditingNodeId(id)
|
||||
};
|
||||
|
||||
useHotkeys("space", (e) => {
|
||||
if (selectedNodeId === id)
|
||||
setEditingNodeId(id)
|
||||
}, { preventDefault: true });
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
setSelectedNodeId(id);
|
||||
};
|
||||
|
||||
useClickOutside(nodeRef, () => {
|
||||
console.log(selectedNodeId, id)
|
||||
if (selectedNodeId === id)
|
||||
setSelectedNodeId(null)
|
||||
if (editingNodeId === id) {
|
||||
setEditingNodeId(null)
|
||||
updateNodeLabel(id, inputValue)
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={nodeRef}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={handleClick}
|
||||
className={`mindmap-node ${id === selectedNodeId ? 'selected' : ''}`}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
className="input"
|
||||
readOnly={id !== editingNodeId}
|
||||
/>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<Handle type="source" position={Position.Top} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MindMapNode;
|
|
@ -1,11 +0,0 @@
|
|||
import type { Edge, EdgeTypes } from "@xyflow/react";
|
||||
|
||||
export const initialEdges = [
|
||||
{ id: "a->c", source: "a", target: "c", animated: true },
|
||||
{ id: "b->d", source: "b", target: "d" },
|
||||
{ id: "c->d", source: "c", target: "d", animated: true },
|
||||
] satisfies Edge[];
|
||||
|
||||
export const edgeTypes = {
|
||||
// Add your custom edge types here!
|
||||
} satisfies EdgeTypes;
|
|
@ -1,145 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useStoreApi } from '@xyflow/react';
|
||||
import useMindMapStore, { RFState } from '../store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
const controlsSelector = (state: RFState) => ({
|
||||
selectedNodeId: state.selectedNodeId,
|
||||
setSelectedNodeId: state.setSelectedNodeId,
|
||||
addChildNode: state.addChildNode,
|
||||
addSiblingNode: state.addSiblingNode,
|
||||
undo: state.undo,
|
||||
redo: state.redo,
|
||||
});
|
||||
|
||||
export function useFlowKeyboardControls() {
|
||||
const {
|
||||
selectedNodeId,
|
||||
setSelectedNodeId,
|
||||
addChildNode,
|
||||
addSiblingNode,
|
||||
undo,
|
||||
redo,
|
||||
} = useMindMapStore(useShallow(controlsSelector));
|
||||
|
||||
const store = useStoreApi();
|
||||
|
||||
const getNextNodeInDirection = useCallback((direction: 'left' | 'right' | 'up' | 'down') => {
|
||||
const { nodeLookup, edges } = store.getState();
|
||||
if (!selectedNodeId) return null;
|
||||
|
||||
const currentNode = nodeLookup.get(selectedNodeId);
|
||||
if (!currentNode) return null;
|
||||
|
||||
// 构建节点关系图
|
||||
const nodeRelations = new Map<string, { parent: string | null; children: string[]; siblings: string[] }>();
|
||||
|
||||
edges.forEach(edge => {
|
||||
const source = edge.source;
|
||||
const target = edge.target;
|
||||
|
||||
if (!nodeRelations.has(source)) {
|
||||
nodeRelations.set(source, { parent: null, children: [], siblings: [] });
|
||||
}
|
||||
if (!nodeRelations.has(target)) {
|
||||
nodeRelations.set(target, { parent: null, children: [], siblings: [] });
|
||||
}
|
||||
|
||||
nodeRelations.get(target)!.parent = source;
|
||||
nodeRelations.get(source)!.children.push(target);
|
||||
});
|
||||
|
||||
// 找出同级节点
|
||||
const currentRelation = nodeRelations.get(selectedNodeId);
|
||||
if (currentRelation?.parent) {
|
||||
const parentRelation = nodeRelations.get(currentRelation.parent);
|
||||
if (parentRelation) {
|
||||
currentRelation.siblings = parentRelation.children.filter(id => id !== selectedNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据方向决定下一个节点
|
||||
switch (direction) {
|
||||
case 'left': {
|
||||
// 如果当前节点是子节点,优先选择父节点
|
||||
if (currentRelation?.parent) {
|
||||
const parentNode = nodeLookup.get(currentRelation.parent);
|
||||
if (parentNode && parentNode.position.x < currentNode.position.x) {
|
||||
return currentRelation.parent;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'right': {
|
||||
// 如果有子节点,选择第一个子节点
|
||||
const children = currentRelation?.children || [];
|
||||
if (children.length > 0) {
|
||||
return children[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'up': {
|
||||
// 在同级节点中找位置靠上的节点
|
||||
const siblings = currentRelation?.siblings || [];
|
||||
const upperSiblings = siblings
|
||||
.map(id => nodeLookup.get(id))
|
||||
.filter(node => node && node.position.y < currentNode.position.y)
|
||||
.sort((a, b) => b!.position.y - a!.position.y);
|
||||
|
||||
if (upperSiblings.length > 0) {
|
||||
return upperSiblings[0]!.id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'down': {
|
||||
// 在同级节点中找位置靠下的节点
|
||||
const siblings = currentRelation?.siblings || [];
|
||||
const lowerSiblings = siblings
|
||||
.map(id => nodeLookup.get(id))
|
||||
.filter(node => node && node.position.y > currentNode.position.y)
|
||||
.sort((a, b) => a!.position.y - b!.position.y);
|
||||
|
||||
if (lowerSiblings.length > 0) {
|
||||
return lowerSiblings[0]!.id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [selectedNodeId, store]);
|
||||
|
||||
// Tab 键添加子节点
|
||||
useHotkeys('tab', (e) => {
|
||||
e.preventDefault();
|
||||
if (selectedNodeId) addChildNode(selectedNodeId);
|
||||
}, { enableOnFormTags: true, preventDefault: true });
|
||||
|
||||
// Enter 键添加同级节点
|
||||
useHotkeys('enter', (e) => {
|
||||
e.preventDefault();
|
||||
if (selectedNodeId) addSiblingNode(selectedNodeId);
|
||||
}, { enableOnFormTags: true, preventDefault: true });
|
||||
|
||||
// 撤销重做
|
||||
// useHotkeys('ctrl+z, cmd+z', (e) => {
|
||||
|
||||
// undo();
|
||||
// }, { enableOnFormTags: false });
|
||||
|
||||
// useHotkeys('ctrl+y, cmd+y', (e) => {
|
||||
|
||||
// redo();
|
||||
// }, { enableOnFormTags: false });
|
||||
|
||||
// 方向键导航
|
||||
const directions = ['left', 'right', 'up', 'down'] as const;
|
||||
directions.forEach(direction => {
|
||||
useHotkeys(direction, (e) => {
|
||||
e.preventDefault();
|
||||
const nextNodeId = getNextNodeInDirection(direction);
|
||||
if (nextNodeId) setSelectedNodeId(nextNodeId);
|
||||
}, { enableOnFormTags: true });
|
||||
});
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./MindMap"
|
|
@ -1,43 +0,0 @@
|
|||
import { Edge, Node } from '@xyflow/react';
|
||||
import dagre from 'dagre';
|
||||
|
||||
export const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'LR') => {
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
const nodeWidth = 200;
|
||||
const nodeHeight = 50;
|
||||
|
||||
dagreGraph.setGraph({
|
||||
rankdir: direction,
|
||||
nodesep: 80,
|
||||
ranksep: 100
|
||||
});
|
||||
|
||||
// 添加节点
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||||
});
|
||||
|
||||
// 添加边
|
||||
edges.forEach((edge) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
// 计算布局
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
// 获取新的节点位置
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { nodes: layoutedNodes, edges };
|
||||
};
|
|
@ -1,234 +0,0 @@
|
|||
import {
|
||||
Edge,
|
||||
EdgeChange,
|
||||
Node,
|
||||
NodeChange,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
XYPosition,
|
||||
} from '@xyflow/react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { create } from 'zustand';
|
||||
import { HistoryData, HistoryState, NodeLayout, NodeRelationType } from './types';
|
||||
import { getLayoutedElements } from './layout';
|
||||
|
||||
|
||||
const createHistoryState = (initialPresent: HistoryData): HistoryState<HistoryData> => ({
|
||||
past: [],
|
||||
present: initialPresent,
|
||||
future: [],
|
||||
});
|
||||
|
||||
const initialNodes: Node[] = [{
|
||||
id: 'root',
|
||||
type: 'mindmap',
|
||||
data: { label: 'React Flow Mind Map' },
|
||||
position: { x: 0, y: 0 },
|
||||
}];
|
||||
|
||||
export type RFState = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
onNodesChange: OnNodesChange;
|
||||
onEdgesChange: OnEdgesChange;
|
||||
history: HistoryState<HistoryData>;
|
||||
addChildNode: (nodeId: string, position?: XYPosition) => void;
|
||||
updateNodeLabel: (nodeId: string, label: string) => void
|
||||
addSiblingNode: (nodeId: string, position?: XYPosition) => void
|
||||
selectedNodeId: string | null;
|
||||
setSelectedNodeId: (nodeId: string | null) => void;
|
||||
editingNodeId: string | null;
|
||||
setEditingNodeId: (nodeId: string | null) => void;
|
||||
isEditing: boolean;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
};
|
||||
const useMindMapStore = create<RFState>((set, get) => {
|
||||
const updateHistory = (newState: Partial<HistoryData>) => {
|
||||
const currentState = get().history.present;
|
||||
return {
|
||||
past: [...get().history.past, currentState],
|
||||
present: { ...currentState, ...newState },
|
||||
future: [],
|
||||
};
|
||||
};
|
||||
|
||||
const createNewNode = (label: string = 'New Node'): Node => ({
|
||||
id: nanoid(),
|
||||
type: 'mindmap',
|
||||
data: { label },
|
||||
position: { x: 0, y: 0 }
|
||||
});
|
||||
const addNode = (
|
||||
parentId: string,
|
||||
relationType: NodeRelationType
|
||||
) => {
|
||||
const { nodes, edges, editingNodeId } = get();
|
||||
const parentNode = nodes.find(node => node.id === parentId);
|
||||
if (!parentNode) return;
|
||||
const newNode = createNewNode();
|
||||
const newEdge = {
|
||||
id: nanoid(),
|
||||
source: relationType === 'child' ? parentId : edges.find(e => e.target === parentId)?.source ?? parentId,
|
||||
target: newNode.id,
|
||||
type: 'smoothstep',
|
||||
};
|
||||
const newNodes = [...nodes, newNode];
|
||||
const newEdges = [...edges, newEdge];
|
||||
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(newNodes, newEdges);
|
||||
set({
|
||||
nodes: layoutedNodes,
|
||||
edges: layoutedEdges,
|
||||
selectedNodeId: newNode.id,
|
||||
history: updateHistory({
|
||||
nodes: layoutedNodes,
|
||||
edges: layoutedEdges,
|
||||
selectedNodeId: newNode.id,
|
||||
editingNodeId,
|
||||
}),
|
||||
});
|
||||
};
|
||||
return {
|
||||
nodes: initialNodes,
|
||||
edges: [],
|
||||
isEditing: false,
|
||||
history: createHistoryState({ nodes: initialNodes, edges: [], selectedNodeId: null, editingNodeId: null }),
|
||||
editingNodeId: null,
|
||||
setEditingNodeId: (nodeId: string | null) => {
|
||||
const { nodes, edges, selectedNodeId } = get();
|
||||
set({
|
||||
editingNodeId: nodeId,
|
||||
isEditing: Boolean(nodeId),
|
||||
history: {
|
||||
past: [...get().history.past, get().history.present],
|
||||
present: {
|
||||
nodes,
|
||||
edges,
|
||||
selectedNodeId,
|
||||
editingNodeId: nodeId
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
selectedNodeId: null,
|
||||
setSelectedNodeId: (nodeId: string | null) => {
|
||||
const { nodes, edges, editingNodeId } = get();
|
||||
set({
|
||||
selectedNodeId: nodeId,
|
||||
history: {
|
||||
past: [...get().history.past, get().history.present],
|
||||
present: { nodes, edges, selectedNodeId: nodeId, editingNodeId },
|
||||
future: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
updateNodeLabel: (nodeId: string, label: string) => {
|
||||
const { nodes, edges, selectedNodeId, editingNodeId } = get();
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
return { ...node, data: { ...node.data, label } };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
set({
|
||||
nodes: newNodes,
|
||||
edges,
|
||||
selectedNodeId,
|
||||
history: {
|
||||
past: [...get().history.past, get().history.present],
|
||||
present: { nodes: newNodes, edges, selectedNodeId, editingNodeId },
|
||||
future: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
onNodesChange: (changes: NodeChange[]) => {
|
||||
console.log('on node change', changes)
|
||||
const { nodes, edges, selectedNodeId } = get();
|
||||
const newNodes = applyNodeChanges(changes, nodes);
|
||||
set({ nodes: newNodes });
|
||||
},
|
||||
onEdgesChange: (changes: EdgeChange[]) => {
|
||||
const { nodes, edges, selectedNodeId } = get();
|
||||
const newEdges = applyEdgeChanges(changes, edges);
|
||||
|
||||
set({ edges: newEdges });
|
||||
},
|
||||
addChildNode: (nodeId: string) =>
|
||||
addNode(nodeId, 'child'),
|
||||
addSiblingNode: (nodeId: string) =>
|
||||
addNode(nodeId, 'sibling'),
|
||||
undo: () => {
|
||||
const { history } = get();
|
||||
console.log('[Undo] Starting undo operation');
|
||||
|
||||
if (history.past.length === 0) {
|
||||
console.log('[Undo] No past states available, undo skipped');
|
||||
return;
|
||||
}
|
||||
const previous = history.past[history.past.length - 1];
|
||||
const newPast = history.past.slice(0, -1);
|
||||
const newPresent = { ...history.present };
|
||||
console.log('[Undo] Previous state:', previous);
|
||||
console.log('[Undo] New past length:', newPast.length);
|
||||
|
||||
set({
|
||||
nodes: previous.nodes,
|
||||
edges: previous.edges,
|
||||
selectedNodeId: previous.selectedNodeId,
|
||||
editingNodeId: previous.editingNodeId,
|
||||
history: {
|
||||
past: newPast,
|
||||
present: previous,
|
||||
future: [newPresent, ...history.future],
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[Undo] Operation completed');
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
const { history } = get();
|
||||
console.log('[Redo] Starting redo operation');
|
||||
|
||||
if (history.future.length === 0) {
|
||||
console.log('[Redo] No future states available, redo skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
const next = history.future[0];
|
||||
const newFuture = history.future.slice(1);
|
||||
|
||||
console.log('[Redo] Next state:', next);
|
||||
console.log('[Redo] New future length:', newFuture.length);
|
||||
|
||||
set({
|
||||
nodes: next.nodes,
|
||||
edges: next.edges,
|
||||
selectedNodeId: next.selectedNodeId,
|
||||
editingNodeId: next.editingNodeId,
|
||||
history: {
|
||||
past: [...history.past, history.present],
|
||||
present: next,
|
||||
future: newFuture,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[Redo] Operation completed');
|
||||
},
|
||||
get canUndo() {
|
||||
return get().history.past.length > 0;
|
||||
},
|
||||
|
||||
get canRedo() {
|
||||
return get().history.future.length > 0;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default useMindMapStore;
|
|
@ -1,32 +0,0 @@
|
|||
import type { Node, NodeTypes, BuiltInNode, Edge } from "@xyflow/react";
|
||||
|
||||
export type MindMapNodeType = Node<
|
||||
{
|
||||
label: string
|
||||
level: number
|
||||
isExpanded?: boolean
|
||||
metadata?: Record<string, number>
|
||||
}, "mindmap">
|
||||
|
||||
export type MindMapEdgeType = Edge<{
|
||||
label: string
|
||||
}, "mindmap">
|
||||
|
||||
export type HistoryState<T> = {
|
||||
past: T[];
|
||||
present: T;
|
||||
future: T[];
|
||||
};
|
||||
export type HistoryData = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
selectedNodeId: string | null;
|
||||
editingNodeId: string | null; // Add this
|
||||
};
|
||||
export type NodeRelationType = 'child' | 'sibling';
|
||||
export type NodeLayout = {
|
||||
horizontalSpacing: number;
|
||||
verticalSpacing: number;
|
||||
nodeWidth: number;
|
||||
nodeHeight: number;
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
import { useEffect, RefObject } from 'react';
|
||||
|
||||
export function useClickOutside<T extends HTMLElement>(ref: RefObject<T>, handler: () => void) {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
console.log(event.target)
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [ref, handler]);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./components/mindmap"
|
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "esnext",
|
||||
"allowJs": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"es2022",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"noImplicitOverride": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"outDir": "dist",
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/__tests__"
|
||||
]
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
minify: true,
|
||||
external: ['react', 'react-dom'],
|
||||
bundle: true,
|
||||
target: "esnext"
|
||||
})
|
|
@ -1,27 +1,26 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script>
|
||||
window.env = {
|
||||
VITE_APP_SERVER_IP: "192.168.139.239",
|
||||
VITE_APP_SERVER_PORT: "3006",
|
||||
VITE_APP_FILE_PORT: "8092",
|
||||
VITE_APP_VERSION: "0.3.0",
|
||||
VITE_APP_APP_NAME: "烽火慕课",
|
||||
};
|
||||
</script>
|
||||
<title>fhmooc</title>
|
||||
<script
|
||||
type="module"
|
||||
crossorigin
|
||||
src="/assets/index-De5TXRhh.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BL-ZztvJ.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script>
|
||||
window.env = {
|
||||
VITE_APP_SERVER_IP: "$SERVER_IP",
|
||||
VITE_APP_SERVER_PORT: "$SERVER_PORT",
|
||||
VITE_APP_FILE_PORT: "$FILE_PORT",
|
||||
VITE_APP_VERSION: "$VERSION",
|
||||
VITE_APP_APP_NAME: "$APP_NAME",
|
||||
};
|
||||
</script>
|
||||
<title>$APP_NAME</title>
|
||||
<script type="module" crossorigin src="/assets/index-De5TXRhh.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BL-ZztvJ.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue