index.tsx 16 KB

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