index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import React, { useEffect, useMemo, useRef, useState } from "react";
  2. import {
  3. Layout,
  4. Menu,
  5. Button,
  6. Descriptions,
  7. Input,
  8. Tooltip,
  9. Empty,
  10. Spin,
  11. Modal,
  12. } from "antd";
  13. import { ProDescriptions } from "@ant-design/pro-components";
  14. import type { DescriptionsProps, MenuProps } from "antd";
  15. import { DownOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons";
  16. import TableEdit from "@/components/TableEdit";
  17. import ER from "./components/ER";
  18. import AddTable from "@/components/AddTable";
  19. import { useModel, useRequest, useParams } from "umi";
  20. import { GetDataModelDetail } from "@/api";
  21. import NoData from "@/assets/no-data.png";
  22. import { ColumnItem, ProjectInfo, TableItemType } from "@/type";
  23. import { useFullscreen, useLocalStorageState } from "ahooks";
  24. import AddModel from "@/components/AddModel";
  25. import insertCss from "insert-css";
  26. import LangInput from "@/components/LangInput";
  27. import LangInputTextarea from "@/components/LangInputTextarea";
  28. import { validateAliasName, validateTableCode } from "@/utils/validator";
  29. import SyncModal from "@/components/SyncModal";
  30. import AICreator from "@/pages/er/components/AICreator";
  31. const { Content, Header } = Layout;
  32. export default function index() {
  33. const [active, setActive] = useState(0);
  34. const [showNavigator, setShowNavigator] = useState(true);
  35. const addTableRef = useRef<{ open: () => void }>();
  36. const {
  37. project,
  38. setProject,
  39. setPlayModeEnable,
  40. exitPlayMode,
  41. updateTable,
  42. graph,
  43. onCreateByAi
  44. } = useModel("erModel");
  45. const [searchKeyword, setSearchKeyword] = useState("");
  46. const [selectKey, setSelectKey] = useState<string>(
  47. project.tables?.[0]?.table?.id || ""
  48. );
  49. const erRef = useRef<HTMLDivElement>(null);
  50. const addModelRef = useRef<{ edit: (info: ProjectInfo) => void }>();
  51. const syncModalRef = useRef<{ open: () => void }>();
  52. const [isFullscreen, { enterFullscreen, exitFullscreen }] =
  53. useFullscreen(erRef);
  54. const [collapsed, setCollapsed] = useState(true);
  55. const [seachColumn, setSearchColumn] = useState("");
  56. // 双击选中表数据
  57. const [selectTableItem, setSelectTableItem] = React.useState<TableItemType>();
  58. const [open, setOpen] = useState(false);
  59. useEffect(() => {
  60. graph?.on("node:dblclick", (args) => {
  61. const data = args.node.getData();
  62. if (data?.isTable) {
  63. setOpen(true);
  64. setSelectTableItem(data);
  65. }
  66. });
  67. }, [graph]);
  68. useEffect(() => {
  69. insertCss(`
  70. .ant-descriptions-header {
  71. margin-bottom: ${collapsed ? "0 !important" : "20px !important"};
  72. }
  73. `);
  74. }, [collapsed]);
  75. useEffect(() => {
  76. setPlayModeEnable(true);
  77. graph?.disableKeyboard();
  78. }, [project, graph]);
  79. useEffect(() => {
  80. // 监听浏览器tab切换 刷新数据
  81. const handleVisibilityChange = () => {
  82. if (document.visibilityState === "visible") {
  83. refresh();
  84. }
  85. };
  86. document.addEventListener("visibilitychange", handleVisibilityChange);
  87. return () => {
  88. document.removeEventListener("visibilitychange", handleVisibilityChange);
  89. };
  90. });
  91. const params = useParams();
  92. const { run, loading, refresh } = useRequest(GetDataModelDetail, {
  93. manual: true,
  94. onSuccess: (res) => {
  95. console.log("模型详情:", res);
  96. const result = res?.result;
  97. setSelectKey(result?.tables?.[0]?.table?.id || "");
  98. if (result) {
  99. setProject(result, false, true);
  100. }
  101. },
  102. });
  103. const [hideDefaultColumn, setHideDefaultColumn] = useLocalStorageState(
  104. "er-hideDefaultColumn",
  105. {
  106. defaultValue: false,
  107. listenStorageChange: true,
  108. }
  109. );
  110. useEffect(() => {
  111. if (params?.id) {
  112. run({ id: params.id });
  113. }
  114. }, []);
  115. const descItems: DescriptionsProps["items"] = useMemo(() => {
  116. return [
  117. {
  118. key: "1",
  119. label: "模型名称",
  120. children: project?.name || "-",
  121. },
  122. {
  123. key: "2",
  124. label: "创建用户",
  125. children: project?.createdByName || "-",
  126. },
  127. {
  128. key: "3",
  129. label: "创建时间",
  130. children: project?.createTime || "-",
  131. },
  132. // {
  133. // key: "5",
  134. // label: "发布状态",
  135. // children: project?.publishStatus || "-",
  136. // },
  137. {
  138. key: "4-1",
  139. label: "更新用户",
  140. children: project?.updateByName || "-",
  141. },
  142. {
  143. key: "4",
  144. label: "更新时间",
  145. children: project?.updateTime || "-",
  146. },
  147. {
  148. key: "6",
  149. label: "表",
  150. children: `${project.tables.length}张`,
  151. },
  152. {
  153. key: "7",
  154. label: "描述",
  155. children: project?.description || "-",
  156. },
  157. ];
  158. }, [project]);
  159. const tableData: MenuProps["items"] = useMemo(() => {
  160. const { tables } = project;
  161. const treeList: {
  162. key: string;
  163. label: string | React.ReactNode;
  164. icon: React.ReactNode;
  165. children?: MenuProps["items"];
  166. }[] = [];
  167. tables
  168. .filter((item) => {
  169. const tableName = item.table.langNameList?.find(
  170. (item) => item.name === "zh-CN"
  171. )?.value;
  172. return (
  173. item.table.schemaName.includes(searchKeyword) ||
  174. tableName?.includes(searchKeyword)
  175. );
  176. })
  177. .forEach((item) => {
  178. const name = item.table.langNameList?.find(
  179. (item) => item.name === "zh-CN"
  180. )?.value;
  181. const newItem = {
  182. key: item.table.id,
  183. label: item.table.schemaName + `${name ? `(${name})` : ""}`,
  184. icon: <i className="iconfont icon-biaogebeifen" />,
  185. };
  186. if (!item.table.parentBusinessTableId) {
  187. treeList.push(newItem);
  188. } else {
  189. const parent = treeList.find(
  190. (tableItem) => tableItem?.key === item.table.parentBusinessTableId
  191. );
  192. if (parent) {
  193. if (!parent?.children) {
  194. parent.children = [];
  195. }
  196. parent.children?.push(newItem);
  197. }
  198. }
  199. });
  200. return treeList;
  201. }, [project, searchKeyword]);
  202. const currentTable = useMemo(() => {
  203. return project.tables.find((item) => item.table.id === selectKey);
  204. }, [project, selectKey]);
  205. const currentColumns = useMemo(() => {
  206. return (currentTable?.tableColumnList || []).filter((item) => {
  207. if (seachColumn) {
  208. return item.langName?.includes(seachColumn);
  209. }
  210. return true;
  211. });
  212. }, [currentTable, seachColumn]);
  213. // 处理添加表
  214. const handleAddTable = (tables: TableItemType[]) => {
  215. setProject({
  216. ...project,
  217. tables: [...tables, ...project.tables],
  218. });
  219. setSelectKey(tables[0].table.id);
  220. graph?.select(tables[0].table.id);
  221. };
  222. const handleEnterFullscreen = () => {
  223. enterFullscreen();
  224. };
  225. const handleExitFullscreen = () => {
  226. exitFullscreen();
  227. };
  228. const handleSelectTable = (key: string) => {
  229. graph?.resetSelection(key);
  230. graph?.centerCell(graph.getCellById(key));
  231. setSelectKey(key);
  232. };
  233. const handleEnterEdit = () => {
  234. const { origin, pathname } = window.location;
  235. const enterpriseCode = sessionStorage.getItem("enterpriseCode");
  236. window.open(
  237. `${origin}${pathname}#/er/${project.id}?enterpriseCode=${enterpriseCode}`
  238. );
  239. };
  240. // 修改表格字段
  241. const handleChangeColumn = (columns: readonly ColumnItem[]) => {
  242. currentTable &&
  243. updateTable({
  244. ...currentTable,
  245. tableColumnList: [...columns],
  246. });
  247. };
  248. // 修改表格字段
  249. const handleChangeSelectTableColumn = (columns: readonly ColumnItem[]) => {
  250. selectTableItem &&
  251. updateTable({
  252. ...selectTableItem,
  253. tableColumnList: [...columns],
  254. });
  255. };
  256. // 同步数据表
  257. const handleSync = () => {
  258. syncModalRef.current?.open();
  259. };
  260. const extra = (
  261. <div className="flex gap-12px">
  262. <a onClick={handleSync}>
  263. <i className="iconfont icon-tongbu text-12px mr-4px" />
  264. 数据表同步
  265. </a>
  266. <a onClick={() => project.id && addModelRef.current?.edit(project)}>
  267. <i className="iconfont icon-bianji text-12px mr-4px" />
  268. 基础信息
  269. </a>
  270. <a onClick={handleEnterEdit}>
  271. <i className="iconfont icon-bianji text-12px mr-4px" />
  272. 模型编辑
  273. </a>
  274. <a>
  275. <i className="iconfont icon-moban text-14px mr-4px" />
  276. 保存为模板
  277. </a>
  278. </div>
  279. );
  280. const handleAiCreate = async (data: any) => {
  281. await onCreateByAi(data);
  282. refresh();
  283. }
  284. return (
  285. <Spin spinning={loading}>
  286. {/* 基础信息修改弹窗 */}
  287. <AddModel
  288. ref={addModelRef}
  289. onChange={(info) => {
  290. typeof info === "object" && setProject(info);
  291. }}
  292. />
  293. {/* 同步弹窗 */}
  294. <SyncModal ref={syncModalRef} onPush={refresh} />
  295. <Layout className="h-100vh flex flex-col bg-#fafafa p-12px">
  296. <Header
  297. className="shadow-sm"
  298. style={{
  299. backgroundColor: "#fff",
  300. padding: 16,
  301. height: "auto",
  302. borderRadius: 8,
  303. // boxShadow: "0 2px 4px rgba(0, 0, 0, 0.15)",
  304. marginBottom: 12,
  305. }}
  306. >
  307. <Descriptions
  308. title={
  309. <span>
  310. <svg className="iconfont w-14px h-14px m-r-4px">
  311. <use xlinkHref="#icon-shujumoxing" />
  312. </svg>
  313. {project?.name || "-"}
  314. <Button
  315. type="text"
  316. className={!collapsed ? "rotate-180" : ""}
  317. icon={<DownOutlined />}
  318. onClick={() => setCollapsed(!collapsed)}
  319. />
  320. </span>
  321. }
  322. items={collapsed ? [] : descItems}
  323. extra={extra}
  324. ></Descriptions>
  325. </Header>
  326. <Content className="flex-1 overflow-auto flex gap-12px">
  327. <div className="left w-300px shrink-0 h-full shadow-sm bg-#fff rounded-8px overflow-y-auto">
  328. <div
  329. className="
  330. flex
  331. justify-between
  332. header-tit
  333. p-x-16px
  334. p-y-12px
  335. text-16px
  336. font-bold
  337. text-rgba(0, 0, 0, 0.88)
  338. border-b-1px
  339. border-b-#eee
  340. border-b-solid"
  341. >
  342. <div>
  343. <svg className="iconfont w-14px h-14px m-r-4px">
  344. <use xlinkHref="#icon-biaoge" />
  345. </svg>
  346. <span>数据表</span>
  347. </div>
  348. <div>
  349. <Input
  350. placeholder="搜索"
  351. className="w-100px m-r-4px"
  352. suffix={<SearchOutlined />}
  353. value={searchKeyword}
  354. onChange={(e) => setSearchKeyword(e.target.value)}
  355. />
  356. <Tooltip title="添加数据表">
  357. <Button
  358. type="primary"
  359. icon={<PlusOutlined />}
  360. onClick={() => addTableRef.current?.open()}
  361. ></Button>
  362. </Tooltip>
  363. </div>
  364. </div>
  365. <Menu
  366. mode="inline"
  367. items={tableData}
  368. selectedKeys={[selectKey]}
  369. onSelect={({ key }) => handleSelectTable(key)}
  370. />
  371. {!tableData.length && (
  372. <Empty image={NoData} description="点击+添加数据表!" />
  373. )}
  374. </div>
  375. <div className="right flex-1 overflow-hidden h-full shadow-sm bg-#fff rounded-8px p-12px flex flex-col">
  376. <div className="head flex justify-between">
  377. <div className="left flex gap-8px">
  378. <Button
  379. type={active === 0 ? "primary" : "default"}
  380. onClick={() => setActive(0)}
  381. >
  382. ER图
  383. </Button>
  384. <Button
  385. type={active === 1 ? "primary" : "default"}
  386. onClick={() => setActive(1)}
  387. >
  388. 实体
  389. </Button>
  390. <AICreator
  391. position={{
  392. bottom: 10,
  393. right: 10,
  394. top: "auto"
  395. }}
  396. onChange={handleAiCreate}
  397. trigger={
  398. <Button
  399. type="text"
  400. icon={
  401. <svg
  402. className="icon color-#666"
  403. aria-hidden="true"
  404. >
  405. <use xlinkHref="#icon-AI1"></use>
  406. </svg>
  407. }
  408. >
  409. AI助手
  410. </Button>
  411. }
  412. />
  413. </div>
  414. <div className="right flex gap-8px m-b-12px">
  415. {active === 0 ? (
  416. <>
  417. {/* <Input placeholder="搜索" suffix={<SearchOutlined />} /> */}
  418. <Button
  419. type={!hideDefaultColumn ? "primary" : "default"}
  420. onClick={() => setHideDefaultColumn(!hideDefaultColumn)}
  421. >
  422. 默认字段
  423. </Button>
  424. <Button
  425. type={showNavigator ? "primary" : "default"}
  426. onClick={() => setShowNavigator(!showNavigator)}
  427. icon={<i className="iconfont icon-xiaoditu text-14px" />}
  428. >
  429. 导航
  430. </Button>
  431. <Button
  432. type="primary"
  433. icon={
  434. <i className="iconfont icon-quanping_o text-14px" />
  435. }
  436. onClick={handleEnterFullscreen}
  437. >
  438. 全屏
  439. </Button>
  440. </>
  441. ) : (
  442. <>
  443. <Input
  444. placeholder="搜索"
  445. suffix={<SearchOutlined />}
  446. value={seachColumn}
  447. onChange={(e) => setSearchColumn(e.target.value)}
  448. />
  449. </>
  450. )}
  451. </div>
  452. </div>
  453. <div className="content w-full flex-1 overflow-auto">
  454. {active === 0 ? (
  455. <div
  456. className="er w-full h-full bg-#ccc overflow-auto"
  457. ref={erRef}
  458. >
  459. <ER
  460. showNavigator={showNavigator}
  461. isFullScreen={isFullscreen}
  462. onExitFullscreen={handleExitFullscreen}
  463. onChangeShowNavigator={setShowNavigator}
  464. />
  465. </div>
  466. ) : (
  467. <div>
  468. <div className="p-y-10px p-l-20px">
  469. <ProDescriptions
  470. title={currentTable?.table.schemaName}
  471. dataSource={currentTable?.table}
  472. editable={{
  473. onSave: async (keypath, newInfo, oriInfo) => {
  474. currentTable &&
  475. updateTable({
  476. ...currentTable,
  477. table: {
  478. ...currentTable?.table,
  479. ...newInfo,
  480. langName: "",
  481. langDescription: "",
  482. },
  483. });
  484. return true;
  485. },
  486. }}
  487. columns={[
  488. {
  489. label: "类型",
  490. dataIndex: "type",
  491. valueType: "select",
  492. valueEnum: {
  493. 3: "业务表",
  494. 2: "流程表",
  495. },
  496. editable: false,
  497. },
  498. {
  499. label: "编码",
  500. dataIndex: "schemaName",
  501. valueType: "text",
  502. formItemProps: {
  503. rules: [
  504. { required: true, message: "请输入编码" },
  505. validateTableCode,
  506. ],
  507. },
  508. },
  509. {
  510. label: "别名",
  511. dataIndex: "aliasName",
  512. valueType: "text",
  513. formItemProps: {
  514. rules: [
  515. { required: true, message: "请输入别名" },
  516. validateAliasName,
  517. ],
  518. },
  519. },
  520. {
  521. label: "名称",
  522. dataIndex: "langNameList",
  523. render: (_, record) => {
  524. return (
  525. record.langNameList?.find(
  526. (item) => item.name === "zh-CN"
  527. )?.value || "-"
  528. );
  529. },
  530. renderFormItem: (_schema, config, form) => {
  531. return (
  532. <LangInput
  533. style={{ width: 200 }}
  534. onChange={(val) =>
  535. form.setFieldValue("langNameList", val)
  536. }
  537. />
  538. );
  539. },
  540. },
  541. {
  542. label: "描述",
  543. dataIndex: "langDescriptionList",
  544. render: (_, record) => {
  545. return (
  546. record?.langDescriptionList?.find(
  547. (item) => item.name === "zh-CN"
  548. )?.value || "-"
  549. );
  550. },
  551. renderFormItem: (_schema, config, form) => {
  552. return (
  553. <>
  554. <LangInputTextarea
  555. value={form.getFieldValue(
  556. "langDescriptionList"
  557. )}
  558. onChange={(val) =>
  559. form.setFieldValue(
  560. "langDescriptionList",
  561. val
  562. )
  563. }
  564. />
  565. </>
  566. );
  567. },
  568. },
  569. ]}
  570. ></ProDescriptions>
  571. </div>
  572. <TableEdit
  573. key={selectKey}
  574. data={currentColumns}
  575. tableId={currentTable?.table?.id}
  576. onChange={handleChangeColumn}
  577. />
  578. </div>
  579. )}
  580. </div>
  581. </div>
  582. </Content>
  583. <Modal
  584. title="字段详情"
  585. width={"80%"}
  586. open={open}
  587. footer={(_, { CancelBtn }) => {
  588. return <CancelBtn />;
  589. }}
  590. onCancel={() => {
  591. setOpen(false);
  592. setSelectTableItem(undefined);
  593. }}
  594. >
  595. <TableEdit
  596. key={selectTableItem?.table.id}
  597. tableId={selectTableItem?.table?.id}
  598. data={selectTableItem?.tableColumnList || []}
  599. modelId={project.id}
  600. onChange={handleChangeSelectTableColumn}
  601. />
  602. </Modal>
  603. <AddTable ref={addTableRef} onChange={handleAddTable} />
  604. </Layout>
  605. </Spin>
  606. );
  607. }