mirror of
https://github.com/hansjone/mind-map.git
synced 2026-10-10 20:20:47 +08:00
Initial commit: DSH mind-map canvas monorepo
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
2ecd300ada
92 changed files with 20486 additions and 0 deletions
197
apps/web/src/NodeInspector.tsx
Normal file
197
apps/web/src/NodeInspector.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import type { MindEdge, MindNode } from "@mind-map/shared";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NodePropEditor } from "./node-props";
|
||||
import { useAppStore } from "./store";
|
||||
|
||||
type ApplyOps = ReturnType<typeof useAppStore.getState>["applyOps"];
|
||||
|
||||
export function NodeInspector() {
|
||||
const canvas = useAppStore((s) => s.canvas);
|
||||
const nodes = useAppStore((s) => s.nodes);
|
||||
const edges = useAppStore((s) => s.edges);
|
||||
const selectedIds = useAppStore((s) => s.selectedIds);
|
||||
const selectedEdgeId = useAppStore((s) => s.selectedEdgeId);
|
||||
const applyOps = useAppStore((s) => s.applyOps);
|
||||
|
||||
const mode = canvas?.prefs.inspectorMode ?? "selection";
|
||||
const node = useMemo(
|
||||
() => (selectedIds[0] ? nodes.find((n) => n.id === selectedIds[0]) : undefined),
|
||||
[nodes, selectedIds],
|
||||
);
|
||||
const edge = useMemo(
|
||||
() => (selectedEdgeId ? edges.find((e) => e.id === selectedEdgeId) : undefined),
|
||||
[edges, selectedEdgeId],
|
||||
);
|
||||
|
||||
if (mode === "selection" && !node && !edge) {
|
||||
return <div className="inspector muted">选中节点或边以编辑属性</div>;
|
||||
}
|
||||
|
||||
if (edge && !node) {
|
||||
return <EdgeForm edge={edge} applyOps={applyOps} />;
|
||||
}
|
||||
if (!node) {
|
||||
return <div className="inspector muted">选中节点或边以编辑属性</div>;
|
||||
}
|
||||
return <NodeForm node={node} applyOps={applyOps} />;
|
||||
}
|
||||
|
||||
function NodeForm({ node, applyOps }: { node: MindNode; applyOps: ApplyOps }) {
|
||||
return (
|
||||
<div className="inspector">
|
||||
<div className="inspector__title">节点属性</div>
|
||||
<NodePropEditor
|
||||
node={node}
|
||||
applyOps={applyOps}
|
||||
variant="panel"
|
||||
showStylePreset
|
||||
/>
|
||||
<div className="inspector__row" style={{ marginTop: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void applyOps([
|
||||
{
|
||||
type: "set_collapsed",
|
||||
nodeId: node.id,
|
||||
collapsed: !node.collapsed,
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
{node.collapsed ? "展开" : "折叠"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EdgeForm({ edge, applyOps }: { edge: MindEdge; applyOps: ApplyOps }) {
|
||||
const [tagsText, setTagsText] = useState(edge.tags?.join(", ") ?? "");
|
||||
useEffect(() => {
|
||||
setTagsText(edge.tags?.join(", ") ?? "");
|
||||
}, [edge.id, edge.version, edge.tags]);
|
||||
|
||||
return (
|
||||
<div className="inspector">
|
||||
<div className="inspector__title">边属性</div>
|
||||
<label className="inspector__field">
|
||||
<span>标签</span>
|
||||
<input
|
||||
key={`el-${edge.id}-${edge.version}`}
|
||||
defaultValue={edge.label ?? ""}
|
||||
onBlur={(e) => {
|
||||
void applyOps([
|
||||
{
|
||||
type: "update_edge_meta",
|
||||
edgeId: edge.id,
|
||||
patch: { label: e.target.value || null },
|
||||
},
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label className="inspector__field">
|
||||
<span>备注</span>
|
||||
<textarea
|
||||
key={`en-${edge.id}-${edge.version}`}
|
||||
defaultValue={edge.note ?? ""}
|
||||
rows={2}
|
||||
onBlur={(e) => {
|
||||
void applyOps([
|
||||
{
|
||||
type: "update_edge_meta",
|
||||
edgeId: edge.id,
|
||||
patch: { note: e.target.value || null },
|
||||
},
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label className="inspector__field">
|
||||
<span>线粗 (1-5)</span>
|
||||
<input
|
||||
key={`ew-${edge.id}-${edge.version}`}
|
||||
type="number"
|
||||
min={1}
|
||||
max={5}
|
||||
defaultValue={edge.weight ?? 1}
|
||||
onBlur={(e) => {
|
||||
const n = Number(e.target.value);
|
||||
const weight = Number.isFinite(n)
|
||||
? Math.max(1, Math.min(5, Math.round(n)))
|
||||
: null;
|
||||
void applyOps([
|
||||
{
|
||||
type: "update_edge_meta",
|
||||
edgeId: edge.id,
|
||||
patch: { weight },
|
||||
},
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label className="inspector__field">
|
||||
<span>线型</span>
|
||||
<select
|
||||
key={`es-${edge.id}-${edge.version}`}
|
||||
defaultValue={edge.lineStyle ?? (edge.kind === "relation" ? "dashed" : "solid")}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value as "solid" | "dashed" | "dotted";
|
||||
void applyOps([
|
||||
{
|
||||
type: "update_edge_meta",
|
||||
edgeId: edge.id,
|
||||
patch: { lineStyle: v },
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<option value="solid">实线</option>
|
||||
<option value="dashed">虚线</option>
|
||||
<option value="dotted">点线</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="inspector__field">
|
||||
<span>箭头</span>
|
||||
<select
|
||||
key={`ed-${edge.id}-${edge.version}`}
|
||||
defaultValue={
|
||||
edge.direction ?? (edge.kind === "relation" ? "forward" : "none")
|
||||
}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value as "forward" | "both" | "none";
|
||||
void applyOps([
|
||||
{
|
||||
type: "update_edge_meta",
|
||||
edgeId: edge.id,
|
||||
patch: { direction: v },
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<option value="forward">单向 →</option>
|
||||
<option value="both">双向 ↔</option>
|
||||
<option value="none">无箭头</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="inspector__field">
|
||||
<span>标签(逗号分隔)</span>
|
||||
<input
|
||||
value={tagsText}
|
||||
onChange={(e) => setTagsText(e.target.value)}
|
||||
onBlur={() => {
|
||||
const tags = tagsText
|
||||
.split(/[,,]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
void applyOps([{ type: "update_edge_meta", edgeId: edge.id, patch: { tags } }]);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="muted" style={{ fontSize: 11 }}>
|
||||
{edge.kind} · {edge.from.slice(0, 8)} → {edge.to.slice(0, 8)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue