MindmapModal.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <template>
  2. <div v-if="visible" class="mindmap-modal" style="user-select: none">
  3. <div class="mindmap-modal_tool">
  4. <el-form ref="formRef" :model="formData" class="flex items-center" inline>
  5. <el-form-item label="收缩至" prop="level" class="mr-12px">
  6. <el-select
  7. v-model="formData.level"
  8. placeholder="请选择"
  9. style="width: 120px"
  10. @change="changeLevel"
  11. >
  12. <el-option
  13. v-for="item in 30"
  14. :key="item"
  15. :label="`${item}级`"
  16. :value="item"
  17. />
  18. </el-select>
  19. </el-form-item>
  20. <el-form-item label="布局" prop="layout" class="mr-12px">
  21. <el-select
  22. v-model="formData.layout"
  23. placeholder="请选择"
  24. style="width: 120px"
  25. @change="changeLayout"
  26. >
  27. <el-option label="逻辑右" value="logicalStructure" />
  28. <el-option label="逻辑左" value="logicalStructureLeft" />
  29. <el-option label="垂直" value="organizationStructure" />
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item label="搜索" props="search">
  33. <div class="flex">
  34. <el-input v-model="formData.search" style="width: 120px" />
  35. <el-button
  36. class="ml-12px"
  37. :icon="Search"
  38. @click="handleSearch"
  39. ></el-button>
  40. </div>
  41. </el-form-item>
  42. </el-form>
  43. <div class="flex items-center gap-4px">
  44. <span class="text-12px text-green mr-12px"
  45. >最新版本号:{{ latestVersion || "-" }}</span
  46. >
  47. <span class="text-12px mr-12px font-bold text-#f56c6c"
  48. >{{ type == "1" ? "工程BOM" : "制造BOM" }}编辑</span
  49. >
  50. <span class="text-12px text-yellow mr-12px"
  51. >当前版本号:{{ version || "-" }}</span
  52. >
  53. <el-button type="primary" link @click="handleSave" class="mr-8px"
  54. ><img class="w-1em mr-4px" :src="saveImg" alt="" />保存</el-button
  55. >
  56. <el-tooltip content="添加" v-if="type == '1' && !readonly">
  57. <el-button
  58. type="default"
  59. circle
  60. :icon="CirclePlusFilled"
  61. @click="addNode"
  62. />
  63. </el-tooltip>
  64. <el-tooltip content="删除" v-if="type == '1' && !readonly">
  65. <el-button
  66. type="default"
  67. circle
  68. :icon="DeleteFilled"
  69. @click="removeNode"
  70. />
  71. </el-tooltip>
  72. <el-tooltip content="详情">
  73. <el-button
  74. type="default"
  75. circle
  76. :icon="InfoFilled"
  77. @click="openNodeDetail"
  78. />
  79. </el-tooltip>
  80. <el-tooltip content="自适应">
  81. <el-button type="default" circle @click="autoFit">
  82. <img :src="AutoIcon" style="width: 1em" alt="" />
  83. </el-button>
  84. </el-tooltip>
  85. <el-tooltip content="定位到根节点">
  86. <el-button type="default" circle :icon="Aim" @click="zoomToRoot" />
  87. </el-tooltip>
  88. <el-tooltip content="放大">
  89. <el-button
  90. type="default"
  91. circle
  92. :icon="ZoomIn"
  93. @click="handleZoom(1)"
  94. />
  95. </el-tooltip>
  96. <el-tooltip content="缩小">
  97. <el-button
  98. type="default"
  99. circle
  100. :icon="ZoomOut"
  101. @click="handleZoom(0)"
  102. />
  103. </el-tooltip>
  104. </div>
  105. </div>
  106. <div class="mindmap-modal_body">
  107. <Mindmap
  108. v-model:data="data"
  109. ref="mindmapRef"
  110. :type="type"
  111. :readonly="type == '2' || readonly"
  112. @node-dbl-click="handleNodeDbClick"
  113. @data-change="handleDataChange"
  114. />
  115. </div>
  116. <!-- <div class="mindmap-modal_footer">
  117. </div> -->
  118. </div>
  119. <!-- 工程bom -->
  120. <ConfigDrawerProject
  121. v-if="type == '1'"
  122. ref="configDrawerRef"
  123. @ok="handleConfigOk"
  124. :readonly="readonly"
  125. />
  126. <!-- 制造bom -->
  127. <ConfigDrawerProduction
  128. v-else
  129. ref="configDrawerRef"
  130. @ok="handleConfigOk"
  131. :readonly="readonly"
  132. />
  133. </template>
  134. <script setup lang="ts">
  135. import {
  136. ref,
  137. defineExpose,
  138. reactive,
  139. defineProps,
  140. watch,
  141. onMounted,
  142. onBeforeUnmount,
  143. defineEmits,
  144. inject,
  145. computed,
  146. type Ref,
  147. } from "vue";
  148. import {
  149. Aim,
  150. CirclePlusFilled,
  151. DeleteFilled,
  152. InfoFilled,
  153. Search,
  154. ZoomIn,
  155. ZoomOut,
  156. } from "@element-plus/icons-vue";
  157. import type { MindMapInstance } from "@/components/mindmap/Mindmap.vue";
  158. import type { FormInstance } from "element-plus";
  159. import { ElMessage, ElMessageBox } from "element-plus";
  160. import { bfsWalk, walk } from "simple-mind-map/src/utils";
  161. import Mindmap from "@/components/mindmap/Mindmap.vue";
  162. import ConfigDrawerProject from "./ConfigDrawerProject.vue";
  163. import ConfigDrawerProduction from "./ConfigDrawerProduction.vue";
  164. import AutoIcon from "@/assets/auto.svg";
  165. import saveImg from "@/assets/save.svg";
  166. import { BOMItem } from "./data";
  167. import { cloneDeep } from "lodash-es";
  168. const props = defineProps<{
  169. defaultOpen?: boolean;
  170. hideClose?: boolean;
  171. defaultData?: any;
  172. version?: string;
  173. type?: string;
  174. }>();
  175. const latestVersion = inject<Ref<string>>("latestVersion");
  176. const visible = ref(!!props.defaultOpen);
  177. const mindmapRef = ref<MindMapInstance>();
  178. const configDrawerRef = ref();
  179. const formRef = ref<FormInstance>();
  180. const emit = defineEmits(["refresh"]);
  181. // const editBomStore = useEditBomStore();
  182. const data = ref(
  183. props?.defaultData || {
  184. id: "root",
  185. data: {
  186. name: "根节点",
  187. },
  188. children: [],
  189. }
  190. );
  191. // 当前版本号与最新版本号不一致时,禁止修改
  192. const readonly = computed(() => {
  193. return props.version != latestVersion?.value;
  194. });
  195. // 1、初次读取本地缓存配置
  196. const defaultConfig = JSON.parse(
  197. localStorage.getItem("mindmap-config") || "{}"
  198. );
  199. const formData = reactive({
  200. level: defaultConfig?.level,
  201. layout: defaultConfig?.layout ?? "logicalStructure",
  202. search: defaultConfig?.search || "",
  203. });
  204. // 2、监听配置项变化,保存到本地缓存
  205. watch(
  206. () => formData,
  207. (val) => {
  208. localStorage.setItem("mindmap-config", JSON.stringify(val));
  209. },
  210. {
  211. deep: true,
  212. }
  213. );
  214. // 定位到根节点
  215. const zoomToRoot = () => {
  216. const mindmap = mindmapRef.value?.getInstance();
  217. mindmap?.renderer?.clearActiveNode();
  218. mindmap?.renderer?.setRootNodeCenter();
  219. const data = mindmap?.getData(false);
  220. const root = mindmap?.renderer.findNodeByUid(data?.data?.uid);
  221. root?.active();
  222. };
  223. // 切换布局
  224. const changeLayout = (value: string) => {
  225. const mindmap = mindmapRef.value?.getInstance();
  226. mindmap?.setLayout(value);
  227. let fited = false;
  228. mindmap?.on("node_tree_render_end", () => {
  229. // @ts-ignore
  230. !fited && mindmap?.view?.fit();
  231. fited = true;
  232. });
  233. };
  234. // 切换层级
  235. const changeLevel = (level: number) => {
  236. const mindmap = mindmapRef.value?.getInstance();
  237. mindmap?.execCommand("UNEXPAND_TO_LEVEL", level);
  238. };
  239. const autoFit = () => {
  240. const mindmap = mindmapRef.value?.getInstance();
  241. // @ts-ignore
  242. mindmap?.view?.fit();
  243. };
  244. // 节点详情
  245. const openNodeDetail = () => {
  246. const activeList = mindmapRef.value?.getActiveNodeList();
  247. if (!activeList?.length) {
  248. ElMessage.warning("请选择查看节点");
  249. return;
  250. }
  251. configDrawerRef.value.open(activeList[0].nodeData);
  252. };
  253. // 缩放
  254. const handleZoom = (num: number) => {
  255. const mindmap = mindmapRef.value?.getInstance();
  256. if (num) {
  257. // @ts-ignore
  258. mindmap?.view?.enlarge();
  259. } else {
  260. // @ts-ignore
  261. mindmap?.view?.narrow();
  262. }
  263. };
  264. // 添加节点
  265. const addNode = () => {
  266. const mindmap = mindmapRef.value?.getInstance();
  267. const activeList = mindmapRef.value?.getActiveNodeList();
  268. if (!activeList?.length) {
  269. ElMessage.warning("请选择父节点");
  270. return;
  271. }
  272. mindmap?.execCommand("INSERT_CHILD_NODE");
  273. };
  274. // 删除节点
  275. const removeNode = () => {
  276. const mindmap = mindmapRef.value?.getInstance();
  277. const activeList = mindmapRef.value
  278. ?.getActiveNodeList()
  279. ?.filter((node) => !node?.isRoot);
  280. if (!activeList?.length) {
  281. ElMessage.warning("请选择删除节点");
  282. return;
  283. }
  284. // 原有数据将is_delete设置为true 新增数据直接删除
  285. const updateNodes: any[] = [];
  286. const deleteNodes: any[] = [];
  287. activeList.forEach((node) => {
  288. bfsWalk(node, (child: any) => {
  289. if (child.nodeData?.id) {
  290. updateNodes.push(child);
  291. } else {
  292. deleteNodes.push(child);
  293. }
  294. });
  295. });
  296. ElMessageBox.alert("确认删除节点?", "提示", {
  297. confirmButtonText: "确认",
  298. callback: (action: string) => {
  299. // 执行删除
  300. if (action === "confirm") {
  301. deleteNodes.length && mindmap?.execCommand("REMOVE_NODE", deleteNodes);
  302. updateNodes.forEach((node) => {
  303. handleConfigOk({
  304. ...node?.nodeData.data,
  305. is_deleted: true,
  306. bom_det: node?.nodeData.data?.bom_det || {},
  307. });
  308. });
  309. }
  310. },
  311. });
  312. };
  313. // 搜索
  314. const handleSearch = () => {
  315. const result: any[] = [];
  316. const mindmap = mindmapRef.value?.getInstance();
  317. const data = mindmap?.getData(false);
  318. mindmap?.execCommand("CLEAR_ACTIVE_NODE");
  319. bfsWalk(data, (node: any) => {
  320. if (node.data?.name?.includes(formData.search)) {
  321. result.push(node);
  322. }
  323. });
  324. if (result.length) {
  325. // 展开高亮选择目标
  326. result.forEach((node) => {
  327. mindmap?.renderer?.expandToNodeUid(node.data?.uid);
  328. });
  329. setTimeout(() => {
  330. const list = result.map((node) =>
  331. mindmap?.renderer.findNodeByUid(node.data?.uid)
  332. );
  333. mindmap?.renderer?.activeMultiNode(list);
  334. }, 300);
  335. } else {
  336. ElMessage.warning("未找到节点");
  337. }
  338. };
  339. const handleNodeDbClick = (node: any) => {
  340. console.log("handleNodeDbClick", node);
  341. if (node.isRoot) return;
  342. configDrawerRef.value.open(node.nodeData);
  343. };
  344. // 节点配置结束
  345. const handleConfigOk = (updateNodeData: any) => {
  346. const mindmap = mindmapRef.value?.getInstance();
  347. const allData = mindmap?.getData(false);
  348. bfsWalk(allData, (node: any) => {
  349. if (node.data.uid === updateNodeData.uid) {
  350. node.data = updateNodeData;
  351. }
  352. });
  353. mindmap?.updateData(allData);
  354. };
  355. // 保存
  356. const handleSave = () => {
  357. const mindmap = mindmapRef.value?.getInstance();
  358. const data = mindmap?.getData(false);
  359. let valid = true;
  360. // let hasAddData = false;
  361. bfsWalk(data, (node: any) => {
  362. delete node?.smmVersion;
  363. delete node.data?.uid;
  364. delete node.data?.expand;
  365. delete node.data?.isActive;
  366. delete node.data?.richText;
  367. delete node.data?.text;
  368. delete node.data?.id;
  369. // TODO: 业务校验
  370. // if (!node.data.name?.trim()) {
  371. // valid = false;
  372. // }
  373. // if(node.data.is_add) {
  374. // hasAddData = true;
  375. // }
  376. });
  377. console.log("node:", data);
  378. if (!valid) {
  379. ElMessage.error("请检查数据是否填写完整!");
  380. return;
  381. }
  382. // 添加版本号
  383. data.version = props.version;
  384. try {
  385. window.parent?.BpmTools?.program(
  386. {
  387. interfaceCode: "Common.doSaveBOMAiImageData",
  388. model: data,
  389. },
  390. () => {
  391. ElMessage.success(`保存成功!`);
  392. emit("refresh");
  393. }
  394. );
  395. } catch (error) {
  396. console.log(error);
  397. ElMessage.error("保存失败!");
  398. }
  399. };
  400. // 数据变化 处理新增数据
  401. const handleDataChange = (newData: any) => {
  402. // 新增数据
  403. const addItem = newData?.find((item: any) => item.action === "create");
  404. if (addItem && !addItem.data.data?.name) {
  405. const newItemData = new BOMItem();
  406. delete addItem.data.data.text;
  407. const newNodeData = {
  408. ...(addItem?.data || {}),
  409. data: {
  410. ...addItem?.data?.data,
  411. is_deleted: false,
  412. is_disable: false,
  413. is_change: false,
  414. is_add: true,
  415. qty: undefined,
  416. name: "",
  417. type: "",
  418. bom_code: "",
  419. change_content: "",
  420. bom_det: newItemData,
  421. },
  422. };
  423. handleConfigOk(newNodeData.data);
  424. const mindmap = mindmapRef.value?.getInstance();
  425. const nodeData = mindmap?.renderer.findNodeByUid(newNodeData?.data?.uid);
  426. configDrawerRef.value.open(newNodeData, "add", nodeData.layerIndex);
  427. }
  428. };
  429. watch(
  430. () => props?.defaultData,
  431. (val) => {
  432. // 设置初始化加载层级
  433. walk(val, null, (node: any, _p: any, _isRoot: any, layerIndex: number) => {
  434. node.data.id = node?.id;
  435. if (formData.level && layerIndex >= formData.level) {
  436. node.data.expand = false;
  437. }
  438. });
  439. data.value = val;
  440. }
  441. );
  442. // 打开弹窗
  443. const open = () => {
  444. visible.value = true;
  445. };
  446. // 关闭弹窗
  447. const close = () => {
  448. formRef.value?.resetFields();
  449. visible.value = false;
  450. };
  451. defineExpose({
  452. open,
  453. close,
  454. });
  455. const handlePressDel = (e: KeyboardEvent) => {
  456. // 删除节点
  457. if (
  458. e.key === "Delete" &&
  459. visible.value &&
  460. props.type == "1" &&
  461. !readonly.value
  462. ) {
  463. e.preventDefault();
  464. removeNode();
  465. }
  466. };
  467. watch(
  468. () => data.value,
  469. () => {
  470. const result = cloneDeep(data.value);
  471. if (result?.id && result.id !== "root") {
  472. bfsWalk(result, (node: any) => {
  473. delete node.smmVersion;
  474. delete node.data?.uid;
  475. delete node.data?.expand;
  476. delete node.data?.isActive;
  477. delete node.data?.richText;
  478. delete node.data?.text;
  479. delete node.data?.id;
  480. });
  481. // 缓存编辑结果
  482. sessionStorage.setItem(result.id, JSON.stringify(result));
  483. }
  484. }
  485. );
  486. onMounted(() => {
  487. addEventListener("keydown", handlePressDel);
  488. });
  489. onBeforeUnmount(() => {
  490. removeEventListener("keydown", handlePressDel);
  491. });
  492. </script>
  493. <style lang="less" scoped>
  494. .mindmap-modal {
  495. position: absolute;
  496. top: 0;
  497. left: 0;
  498. width: 100%;
  499. height: 100vh;
  500. z-index: 99;
  501. display: flex;
  502. flex-direction: column;
  503. align-items: center;
  504. &_footer {
  505. height: 40px;
  506. width: 100%;
  507. box-sizing: border-box;
  508. background: #fff;
  509. display: flex;
  510. // justify-content: end;
  511. align-items: center;
  512. padding: 0 20px;
  513. line-height: 40px;
  514. }
  515. &_tool {
  516. width: 100%;
  517. box-sizing: border-box;
  518. background: #fff;
  519. display: flex;
  520. justify-content: space-between;
  521. align-items: center;
  522. padding: 0 20px;
  523. }
  524. &_body {
  525. flex: 1;
  526. width: 100%;
  527. background: #fafafa;
  528. overflow: hidden;
  529. }
  530. }
  531. :deep(*) {
  532. .el-form-item {
  533. margin-top: 12px;
  534. margin-bottom: 12px;
  535. }
  536. }
  537. </style>