Skip to content

Commit 4441267

Browse files
committed
feat: 增加根据纯文本生成网状结构的菜单栏按钮
1 parent eaa2be3 commit 4441267

File tree

5 files changed

+149
-87
lines changed

5 files changed

+149
-87
lines changed

app/src/core/service/GlobalMenu.tsx

Lines changed: 18 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Button } from "@/components/ui/button";
21
import { Dialog } from "@/components/ui/dialog";
32
import {
43
Menubar,
@@ -12,15 +11,14 @@ import {
1211
MenubarTrigger,
1312
} from "@/components/ui/menubar";
1413

15-
import { Input } from "@/components/ui/input";
16-
import { Textarea } from "@/components/ui/textarea";
1714
import { loadAllServicesAfterInit, loadAllServicesBeforeInit } from "@/core/loadAllServices";
1815
import { Project } from "@/core/Project";
1916
import { activeProjectAtom, isClassroomModeAtom, projectsAtom, store } from "@/state";
2017
import AIWindow from "@/sub/AIWindow";
2118
import AttachmentsWindow from "@/sub/AttachmentsWindow";
2219
import ExportPngWindow from "@/sub/ExportPngWindow";
2320
import FindWindow from "@/sub/FindWindow";
21+
import GenerateNodeTree, { GenerateNodeGraph } from "@/sub/GenerateNodeWindow";
2422
import LoginWindow from "@/sub/LoginWindow";
2523
import NodeDetailsWindow from "@/sub/NodeDetailsWindow";
2624
import RecentFilesWindow from "@/sub/RecentFilesWindow";
@@ -29,9 +27,8 @@ import TestWindow from "@/sub/TestWindow";
2927
import UserWindow from "@/sub/UserWindow";
3028
import { getDeviceId } from "@/utils/otherApi";
3129
import { PathString } from "@/utils/pathString";
32-
import { Color, Vector } from "@graphif/data-structures";
30+
import { Color } from "@graphif/data-structures";
3331
import { deserialize, serialize } from "@graphif/serializer";
34-
import { Rectangle } from "@graphif/shapes";
3532
import { Decoder } from "@msgpack/msgpack";
3633
import { getVersion } from "@tauri-apps/api/app";
3734
import { appCacheDir, dataDir, join } from "@tauri-apps/api/path";
@@ -59,23 +56,26 @@ import {
5956
FileOutput,
6057
FilePlus,
6158
Folder,
62-
Images,
6359
FolderClock,
6460
FolderCog,
6561
FolderOpen,
6662
FolderTree,
6763
Frown,
6864
Fullscreen,
65+
GitCompareArrows,
66+
Images,
6967
Keyboard,
7068
LayoutGrid,
7169
Loader,
7270
MapPin,
7371
MessageCircleWarning,
7472
MousePointer2,
73+
Network,
7574
Palette,
7675
Paperclip,
7776
PersonStanding,
7877
PictureInPicture2,
78+
Plus,
7979
Rabbit,
8080
Radiation,
8181
Redo,
@@ -101,11 +101,10 @@ import { ProjectUpgrader } from "../stage/ProjectUpgrader";
101101
import { LineEdge } from "../stage/stageObject/association/LineEdge";
102102
import { TextNode } from "../stage/stageObject/entity/TextNode";
103103
import { RecentFileManager } from "./dataFileService/RecentFileManager";
104+
import { DragFileIntoStageEngine } from "./dataManageService/dragFileIntoStageEngine/dragFileIntoStageEngine";
104105
import { FeatureFlags } from "./FeatureFlags";
105106
import { Settings } from "./Settings";
106-
import { SubWindow } from "./SubWindow";
107107
import { Telemetry } from "./Telemetry";
108-
import { DragFileIntoStageEngine } from "./dataManageService/dragFileIntoStageEngine/dragFileIntoStageEngine";
109108

110109
const Content = MenubarContent;
111110
const Item = MenubarItem;
@@ -579,90 +578,26 @@ export function GlobalMenu() {
579578
{/* 生成子菜单 */}
580579
<Sub>
581580
<SubTrigger>
582-
<RefreshCcwDot />
581+
<Plus />
583582
{t("actions.generate.title")}
584583
</SubTrigger>
585584
<SubContent>
586585
<Item
587586
onClick={async () => {
588-
// 创建自定义对话框
589-
const result = await new Promise<{ text: string; indention: number } | undefined>((resolve) => {
590-
function CustomDialog({ winId }: { winId?: string }) {
591-
const [text, setText] = useState("");
592-
const [indention, setIndention] = useState("4");
593-
594-
return (
595-
<div className="space-y-4 p-6">
596-
<div>
597-
<h3 className="mb-2 text-xl font-semibold">
598-
{t("actions.generate.generateNodeTreeByText")}
599-
</h3>
600-
<p className="text-muted-foreground mb-4">
601-
{t("actions.generate.generateNodeTreeByTextDescription")}
602-
</p>
603-
</div>
604-
<Textarea
605-
value={text}
606-
onChange={(e) => setText(e.target.value)}
607-
placeholder={t("actions.generate.generateNodeTreeByTextPlaceholder")}
608-
className="min-h-[200px]"
609-
/>
610-
<div className="flex items-center gap-2">
611-
<label htmlFor="indention">{t("actions.generate.indention")}:</label>
612-
<Input
613-
id="indention"
614-
type="number"
615-
value={indention}
616-
onChange={(e) => setIndention(e.target.value)}
617-
min="1"
618-
max="10"
619-
className="w-20"
620-
/>
621-
</div>
622-
<div className="flex justify-end gap-2">
623-
<Button
624-
variant="outline"
625-
onClick={() => {
626-
resolve(undefined);
627-
setTimeout(() => {
628-
SubWindow.close(winId!);
629-
}, 100);
630-
}}
631-
>
632-
{t("actions.cancel")}
633-
</Button>
634-
<Button
635-
onClick={() => {
636-
resolve({ text, indention: parseInt(indention) || 4 });
637-
setTimeout(() => {
638-
SubWindow.close(winId!);
639-
}, 100);
640-
}}
641-
>
642-
{t("actions.confirm")}
643-
</Button>
644-
</div>
645-
</div>
646-
);
647-
}
648-
649-
SubWindow.create({
650-
title: t("actions.generate.generateNodeTreeByText"),
651-
titleBarOverlay: true,
652-
closable: true,
653-
rect: new Rectangle(Vector.same(100), new Vector(600, 450)),
654-
children: <CustomDialog />,
655-
});
656-
});
657-
658-
if (result) {
659-
activeProject?.stageManager.generateNodeTreeByText(result.text, result.indention);
660-
}
587+
GenerateNodeTree.open();
661588
}}
662589
>
663-
<RefreshCcwDot />
590+
<Network className="-rotate-90" />
664591
{t("actions.generate.generateNodeTreeByText")}
665592
</Item>
593+
<Item
594+
onClick={async () => {
595+
GenerateNodeGraph.open();
596+
}}
597+
>
598+
<GitCompareArrows />
599+
{t("actions.generate.generateNodeGraphByText")}
600+
</Item>
666601
</SubContent>
667602
</Sub>
668603
{/* 清空舞台最不常用,放在最后一个 */}

app/src/core/stage/stageManager/StageManager.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,8 +574,12 @@ export class StageManager {
574574
}
575575

576576
generateNodeGraphByText(text: string, location = this.project.camera.location) {
577-
this.project.nodeAdder.addNodeGraphByText(text, location);
578-
this.project.historyManager.recordStep();
577+
try {
578+
this.project.nodeAdder.addNodeGraphByText(text, location);
579+
this.project.historyManager.recordStep();
580+
} catch (e: any) {
581+
toast.error(e.message);
582+
}
579583
}
580584

581585
generateNodeByMarkdown(text: string, location = this.project.camera.location) {

app/src/core/stage/stageManager/concreteMethods/StageNodeAdder.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export class NodeAdder {
179179
}
180180
/**
181181
* 通过纯文本生成网状结构
182-
*
182+
* 这个函数不稳定,可能会随时throw错误
183183
* @param text 网状结构的格式文本
184184
* @param diffLocation
185185
*/
@@ -256,7 +256,15 @@ export class NodeAdder {
256256
}
257257
const leftContentList = leftContent.split("-");
258258
if (leftContentList.length !== 2) {
259-
throw new Error(`解析时出现错误: "${line}",左侧内容应该只有两个名称`);
259+
if (leftContentList.length === 1) {
260+
throw new Error(
261+
`解析时出现错误: "${line}",此行被识别为连线上有文字的行,中间的连接线应该是 "-->",而不是 "->"`,
262+
);
263+
} else {
264+
throw new Error(
265+
`解析时出现错误: "${line}",此行被识别为连线上有文字的行,短横线 “-” 左侧内容应该确保只有两个名称`,
266+
);
267+
}
260268
}
261269
const startName = leftContentList[0].trim();
262270
const edgeText = leftContentList[1].trim();

app/src/locales/zh_CN.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ globalMenu:
6060
generateNodeTreeByTextDescription: 请输入树状结构文本,每行代表一个节点,缩进表示层级关系
6161
generateNodeTreeByTextPlaceholder: 输入树状结构文本...
6262
indention: 缩进字符数
63+
generateNodeGraphByText: 根据纯文本生成网状结构
64+
generateNodeGraphByTextDescription: 请输入网状结构文本,每行代表一个关系,每一行的格式为 `XXX --> XXX`
65+
generateNodeGraphByTextPlaceholder: |
66+
张三 -喜欢-> 李四
67+
李四 -讨厌-> 王五
68+
王五 -欣赏-> 张三
69+
A --> B
70+
B --> C
71+
C --> D
6372
settings:
6473
title: 设置
6574
appearance: 个性化

app/src/sub/GenerateNodeWindow.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Button } from "@/components/ui/button";
2+
import { Input } from "@/components/ui/input";
3+
import { Textarea } from "@/components/ui/textarea";
4+
import { SubWindow } from "@/core/service/SubWindow";
5+
import { activeProjectAtom } from "@/state";
6+
import { Vector } from "@graphif/data-structures";
7+
import { Rectangle } from "@graphif/shapes";
8+
import { useAtom } from "jotai";
9+
import { useState } from "react";
10+
import { useTranslation } from "react-i18next";
11+
12+
export default function GenerateNodeTree() {
13+
const [text, setText] = useState("");
14+
const [indention, setIndention] = useState("4");
15+
const { t } = useTranslation("globalMenu");
16+
17+
const [activeProject] = useAtom(activeProjectAtom);
18+
19+
return (
20+
<div className="space-y-4 p-6">
21+
<div>
22+
<h3 className="mb-2 text-xl font-semibold">{t("actions.generate.generateNodeTreeByText")}</h3>
23+
<p className="text-muted-foreground mb-4">{t("actions.generate.generateNodeTreeByTextDescription")}</p>
24+
<p className="text-xs opacity-50">
25+
注意:2.0.20+版本,生成树形结构后,先框选所有节点,再按ctrl键+框选所有节点,变成选中所有连线,将所有树内的连线改为从右侧发出,左侧接收,然后再alt
26+
shift f
27+
格式化,即可自动布局向右的树状结构(若感到疑惑可进群提问管理员或群主,后期此功能将会继续完善和提高新手友好性)
28+
</p>
29+
</div>
30+
<Textarea
31+
value={text}
32+
onChange={(e) => setText(e.target.value)}
33+
placeholder={t("actions.generate.generateNodeTreeByTextPlaceholder")}
34+
className="min-h-[200px]"
35+
/>
36+
<div className="flex items-center gap-2">
37+
<label htmlFor="indention">{t("actions.generate.indention")}:</label>
38+
<Input
39+
id="indention"
40+
type="number"
41+
value={indention}
42+
onChange={(e) => setIndention(e.target.value)}
43+
min="1"
44+
max="10"
45+
className="w-20"
46+
/>
47+
</div>
48+
<div className="flex justify-end gap-2">
49+
<Button
50+
onClick={() => {
51+
activeProject?.stageManager.generateNodeTreeByText(text, parseInt(indention) || 4);
52+
}}
53+
>
54+
{t("actions.confirm")}
55+
</Button>
56+
</div>
57+
</div>
58+
);
59+
}
60+
61+
GenerateNodeTree.open = () => {
62+
SubWindow.create({
63+
title: "生成节点群",
64+
children: <GenerateNodeTree />,
65+
rect: new Rectangle(new Vector(100, 100), new Vector(500, 600)),
66+
});
67+
};
68+
69+
export function GenerateNodeGraph() {
70+
const [text, setText] = useState("");
71+
const { t } = useTranslation("globalMenu");
72+
73+
const [activeProject] = useAtom(activeProjectAtom);
74+
75+
return (
76+
<div>
77+
<div>
78+
<h3 className="mb-2 text-xl font-semibold">{t("actions.generate.generateNodeGraphByText")}</h3>
79+
<p className="text-muted-foreground mb-4">{t("actions.generate.generateNodeGraphByTextDescription")}</p>
80+
</div>
81+
<Textarea
82+
value={text}
83+
onChange={(e) => setText(e.target.value)}
84+
placeholder={t("actions.generate.generateNodeGraphByTextPlaceholder")}
85+
className="min-h-[200px]"
86+
/>
87+
<div className="flex justify-end gap-2">
88+
<Button
89+
onClick={() => {
90+
activeProject?.stageManager.generateNodeGraphByText(text);
91+
}}
92+
>
93+
{t("actions.confirm")}
94+
</Button>
95+
</div>
96+
</div>
97+
);
98+
}
99+
100+
GenerateNodeGraph.open = () => {
101+
SubWindow.create({
102+
title: "生成节点网",
103+
children: <GenerateNodeGraph />,
104+
rect: new Rectangle(new Vector(100, 100), new Vector(600, 600)),
105+
});
106+
};

0 commit comments

Comments
 (0)