|
@@ -0,0 +1,266 @@
|
|
|
+import {
|
|
|
+ useState,
|
|
|
+ useImperativeHandle,
|
|
|
+ forwardRef,
|
|
|
+ useEffect,
|
|
|
+ useRef,
|
|
|
+} from "react";
|
|
|
+import { Modal, Input, Button } from "antd";
|
|
|
+import "./mermaid.less";
|
|
|
+import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
|
|
|
+import { Graph } from "@antv/x6";
|
|
|
+import { uuid } from "@repo/utils";
|
|
|
+
|
|
|
+import Rectangle from "@/components/flowchart/process";
|
|
|
+import Round from "@/components/basic/roundRectangle";
|
|
|
+import Stadium from "@/components/flowchart/terminator";
|
|
|
+import DoubleCircle from "@/components/er/multivaluedAttribute";
|
|
|
+import Circle from "@/components/flowchart/onPageReference";
|
|
|
+import Diamond from "@/components/flowchart/decision";
|
|
|
+import { useModel } from "umi";
|
|
|
+
|
|
|
+export enum VERTEX_TYPE {
|
|
|
+ RECTANGLE = "rectangle", // 矩形
|
|
|
+ ROUND = "round", // 圆角矩形
|
|
|
+ STADIUM = "stadium", // 椭圆
|
|
|
+ DOUBLECIRCLE = "doublecircle", // 双圆
|
|
|
+ CIRCLE = "circle", // 圆
|
|
|
+ DIAMOND = "diamond", // 菱形
|
|
|
+}
|
|
|
+
|
|
|
+export type MermaidResult = {
|
|
|
+ type: "image" | "cell";
|
|
|
+ data: any;
|
|
|
+ width?: number;
|
|
|
+ height?: number;
|
|
|
+};
|
|
|
+
|
|
|
+export default forwardRef(function MermaidModal(
|
|
|
+ { onChange }: { onChange?: (res?: MermaidResult) => void },
|
|
|
+ ref
|
|
|
+) {
|
|
|
+ const [open, setOpen] = useState(false);
|
|
|
+ const [mermaidCode, setMermaidCode] = useState("");
|
|
|
+ const { graph } = useModel("graphModel");
|
|
|
+ const resultRef = useRef<MermaidResult>();
|
|
|
+
|
|
|
+ useImperativeHandle(ref, () => ({
|
|
|
+ open: () => {
|
|
|
+ setOpen(true);
|
|
|
+ },
|
|
|
+ close: () => {
|
|
|
+ setOpen(false);
|
|
|
+ },
|
|
|
+ }));
|
|
|
+
|
|
|
+ const mermaidRef = useRef<HTMLDivElement>(null);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转换成x6 json
|
|
|
+ * @param list
|
|
|
+ */
|
|
|
+ const toX6Json = (list: any[]) => {
|
|
|
+ const cells: any[] = [];
|
|
|
+ let comp = Rectangle;
|
|
|
+ const idMap: Record<string, string> = {};
|
|
|
+ list.forEach((item) => {
|
|
|
+ // 节点处理
|
|
|
+ if (item.type !== "arrow") {
|
|
|
+ switch (item.type) {
|
|
|
+ case VERTEX_TYPE.ROUND: {
|
|
|
+ comp = Round;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case VERTEX_TYPE.STADIUM: {
|
|
|
+ comp = Stadium;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case VERTEX_TYPE.DOUBLECIRCLE: {
|
|
|
+ comp = DoubleCircle;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case VERTEX_TYPE.CIRCLE: {
|
|
|
+ comp = Circle;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case VERTEX_TYPE.DIAMOND: {
|
|
|
+ comp = Diamond;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ comp = Rectangle;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const id = uuid();
|
|
|
+ const node = {
|
|
|
+ ...comp.node,
|
|
|
+ id,
|
|
|
+ position: {
|
|
|
+ x: item.x,
|
|
|
+ y: item.y,
|
|
|
+ },
|
|
|
+ width: parseInt(item.width),
|
|
|
+ height: parseInt(item.height),
|
|
|
+ data: {
|
|
|
+ ...comp.node.data,
|
|
|
+ text: {
|
|
|
+ ...comp.node.data.text,
|
|
|
+ fontSize: item.label.fontSize,
|
|
|
+ },
|
|
|
+ label: item.label.text,
|
|
|
+ }
|
|
|
+ };
|
|
|
+ idMap[item.id] = id;
|
|
|
+ cells.push(node);
|
|
|
+ } else {
|
|
|
+ // 连线处理
|
|
|
+ const edge = {
|
|
|
+ source: { cell: idMap[item.start.id] },
|
|
|
+ target: { cell: idMap[item.end.id] },
|
|
|
+ labels: item?.label
|
|
|
+ ? [
|
|
|
+ {
|
|
|
+ attrs: {
|
|
|
+ label: {
|
|
|
+ text: item.label.text,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ : [],
|
|
|
+ };
|
|
|
+ cells.push(edge);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return cells;
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绘制图标
|
|
|
+ */
|
|
|
+ const drawDiagram = async () => {
|
|
|
+ // 获取渲染后的 svg
|
|
|
+ try {
|
|
|
+ resultRef.current = undefined;
|
|
|
+ const { elements, files } = await parseMermaidToExcalidraw(mermaidCode);
|
|
|
+ console.log("parse elements:", elements, files);
|
|
|
+ // 转换失败,加载图片
|
|
|
+ if (files) {
|
|
|
+ resultRef.current = {
|
|
|
+ type: "image",
|
|
|
+ data: files[elements[0].fileId]?.dataURL,
|
|
|
+ width: elements[0].width,
|
|
|
+ height: elements[0].height,
|
|
|
+ };
|
|
|
+ const img = new Image();
|
|
|
+ img.src = files[elements[0].fileId]?.dataURL;
|
|
|
+ img.onload = () => {
|
|
|
+ if (mermaidRef.current) {
|
|
|
+ mermaidRef.current.innerHTML = "";
|
|
|
+ mermaidRef.current.appendChild(img);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ // 转换json
|
|
|
+ if (mermaidRef.current) {
|
|
|
+ mermaidRef.current.innerHTML = "";
|
|
|
+ const cells = toX6Json(elements);
|
|
|
+ console.log("parse cells:", cells);
|
|
|
+ resultRef.current = { type: "cell", data: cells };
|
|
|
+ const graphInstance = new Graph({
|
|
|
+ container: mermaidRef.current,
|
|
|
+ grid: false,
|
|
|
+ width: mermaidRef.current.clientWidth,
|
|
|
+ height: mermaidRef.current.clientHeight,
|
|
|
+ });
|
|
|
+ cells.forEach((cell) => {
|
|
|
+ if(cell?.source && cell?.target) {
|
|
|
+ graphInstance.addEdge(cell);
|
|
|
+ } else {
|
|
|
+ graphInstance.addNode(cell);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ graphInstance.zoomToFit({
|
|
|
+ padding: 20,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("mermaid 格式校验失败:错误信息如下:\n", error);
|
|
|
+ let html = `<div class='bl-preview-analysis-fail-block'>
|
|
|
+ <div class="fail-title">Mermaid 语法解析失败!</div><br/>
|
|
|
+ ${error}<br/><br/>
|
|
|
+ 你可以尝试前往 Mermaid 官网来校验你的内容, 或者查看<a href='https://mermaid.live/edit' target='_blank'>相关文档</a>
|
|
|
+ </div>`;
|
|
|
+ if (mermaidRef.current) {
|
|
|
+ mermaidRef.current.innerHTML = html;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (mermaidCode) {
|
|
|
+ drawDiagram();
|
|
|
+ } else {
|
|
|
+ if (mermaidRef.current) {
|
|
|
+ mermaidRef.current.innerHTML = "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, [mermaidCode]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal
|
|
|
+ open={open}
|
|
|
+ width="80%"
|
|
|
+ title="Mermaid导入"
|
|
|
+ footer={() => {
|
|
|
+ return (
|
|
|
+ <div className="flex justify-end">
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ onClick={() => {
|
|
|
+ if (onChange) {
|
|
|
+ onChange(resultRef.current);
|
|
|
+ }
|
|
|
+ setOpen(false);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 插入
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ onCancel={() => setOpen(false)}
|
|
|
+ >
|
|
|
+ <div className="tip">
|
|
|
+ <i>
|
|
|
+ 目前仅支持
|
|
|
+ <a
|
|
|
+ className="text-blue"
|
|
|
+ href="https://mermaid.js.org/syntax/flowchart.html"
|
|
|
+ >
|
|
|
+ 流程图
|
|
|
+ </a>
|
|
|
+ 导入,其他图以图形方式引入。
|
|
|
+ </i>
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-24px">
|
|
|
+ <div className="left flex-1 rounded-4px h-500px border-solid border-gray-200">
|
|
|
+ <Input.TextArea
|
|
|
+ autoSize={{ minRows: 10 }}
|
|
|
+ placeholder="请输入Mermaid代码"
|
|
|
+ className="h-full"
|
|
|
+ variant="borderless"
|
|
|
+ value={mermaidCode}
|
|
|
+ onChange={(e) => setMermaidCode(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="left flex-1 rounded-4px h-500px border-solid border-gray-200">
|
|
|
+ <div className="mermaid-container" ref={mermaidRef}></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+});
|