SyncModal.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. import {
  2. forwardRef,
  3. Key,
  4. useEffect,
  5. useImperativeHandle,
  6. useMemo,
  7. useState,
  8. } from "react";
  9. import {
  10. Modal,
  11. Collapse,
  12. Empty,
  13. Steps,
  14. Result,
  15. Button,
  16. Spin,
  17. Form,
  18. TreeSelect,
  19. Tabs,
  20. message,
  21. } from "antd";
  22. import { ProTable } from "@ant-design/pro-components";
  23. import DiffTable from "./DiffTable";
  24. import { useModel, useRequest } from "umi";
  25. import { PushDataModelTable, GetCanUseTableList } from "@/api/dataModel";
  26. import {
  27. GetAllDesignTables,
  28. SaveDataModel,
  29. ImportFromBusinessTables,
  30. } from "@/api";
  31. import { TableItemType } from "@/type";
  32. import NoData from "@/assets/no-data.png";
  33. import CustomColorPicker from "./CustomColorPicker";
  34. import { pick, set } from "lodash-es";
  35. export default forwardRef(function SyncModal(
  36. props: { onPush: () => void },
  37. ref
  38. ) {
  39. const [open, setOpen] = useState(false);
  40. const { project } = useModel("erModel");
  41. const [color, setColor] = useState<string>();
  42. const { data, loading, run } = useRequest(GetAllDesignTables, {
  43. manual: true,
  44. });
  45. const [tableList, setTableList] = useState<TableItemType[]>(project?.tables);
  46. const [step, setStep] = useState(0);
  47. const [tabActiveKey, setTabActiveKey] = useState<string>("1");
  48. // 选中需要同步的表
  49. const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([]);
  50. const [selectedRows, setSelectedRows] = useState<any[]>([]);
  51. const [okLoading, setOkLoading] = useState(false);
  52. const [resultStatus, setResultStatus] = useState<"success" | "error">(
  53. "success"
  54. );
  55. const [importTables, setImportTables] = useState<Key[]>([]);
  56. useEffect(() => {
  57. if (step === 0 && tableList.length) {
  58. setSelectedRowKeys(tableList.map((item) => item.table.id));
  59. setSelectedRows(tableList);
  60. }
  61. }, [tableList, step]);
  62. useImperativeHandle(ref, () => ({
  63. open: () => {
  64. setTabActiveKey("1");
  65. setColor(undefined);
  66. setStep(0);
  67. setOpen(true);
  68. run();
  69. run1({ type: project.type });
  70. },
  71. close: () => setOpen(false),
  72. }));
  73. useEffect(() => {
  74. setTableList(project.tables);
  75. }, [project.tables]);
  76. useEffect(() => {
  77. setStep(0);
  78. }, [tabActiveKey]);
  79. const {
  80. data: data1,
  81. loading: loading1,
  82. run: run1,
  83. } = useRequest(GetCanUseTableList, {
  84. manual: true,
  85. });
  86. // 可引入表列表
  87. const treeData = useMemo(() => {
  88. const tableMap: Record<string, any> = {};
  89. data1?.result
  90. ?.filter(
  91. (item: any) =>
  92. // 过滤当前数据表类型及存在相同schemaName、aliasName的表
  93. item?.type == project?.type
  94. // && !project.tables.find(
  95. // (tableItem) =>
  96. // tableItem.table.schemaName === item.schemaName ||
  97. // tableItem.table.aliasName === item.aliasName
  98. // )
  99. )
  100. ?.forEach((item: any) => {
  101. // 判断是否存在已引入的表
  102. const hasTable = project.tables.find(
  103. (tableItem: TableItemType) =>
  104. tableItem.table.schemaName === item.schemaName ||
  105. tableItem.table.aliasName === item.aliasName
  106. );
  107. const option = {
  108. key: item.id,
  109. title: `${item?.schemaName}(${item.name})`,
  110. value: item?.id,
  111. disabled: hasTable,
  112. selectable: !item.parentBusinessTableId,
  113. };
  114. // 子表
  115. if (item.parentBusinessTableId) {
  116. if (tableMap[item.parentBusinessTableId]) {
  117. tableMap[item.parentBusinessTableId].children.push(option);
  118. } else {
  119. tableMap[item.parentBusinessTableId] = {
  120. children: [option],
  121. };
  122. }
  123. } else {
  124. // 主表
  125. if (tableMap[item.id]) {
  126. tableMap[item.id] = {
  127. ...tableMap[item.id],
  128. ...option,
  129. };
  130. } else {
  131. tableMap[item.id] = {
  132. ...option,
  133. children: [],
  134. };
  135. }
  136. }
  137. return;
  138. });
  139. return Object.keys(tableMap).map((key) => tableMap[key]);
  140. }, [data1]);
  141. // 已存在的数据表
  142. const existTableList = useMemo(() => {
  143. const list: {
  144. modelTable: TableItemType;
  145. dataTable: any;
  146. }[] = [];
  147. const tables = data?.result?.appBusinessTables || [];
  148. selectedRows.forEach((tableItem) => {
  149. const dataTable = tables.find(
  150. (item: any) => item.id === tableItem.table?.businessTableId
  151. // item.schemaName === tableItem.table.schemaName ||
  152. // item.aliasName === tableItem.table.aliasName
  153. );
  154. if (dataTable) {
  155. list.push({
  156. modelTable: tableItem,
  157. dataTable,
  158. });
  159. }
  160. });
  161. return list;
  162. }, [selectedRows, data]);
  163. // 差异对比更新当前模型数据
  164. const handleUpdateTable = (tableItem: TableItemType) => {
  165. setSelectedRows(
  166. selectedRows.map((item) =>
  167. item.table.id === tableItem.table.id ? tableItem : item
  168. )
  169. );
  170. };
  171. const Step1Comp = () => {
  172. return (
  173. <>
  174. <ProTable
  175. title={() => "请选择模型表"}
  176. dataSource={tableList.map((item) => item.table)}
  177. rowKey="id"
  178. search={false}
  179. pagination={false}
  180. options={false}
  181. size="small"
  182. rowSelection={{
  183. selectedRowKeys,
  184. onChange(selectedRowKeys) {
  185. setSelectedRowKeys(selectedRowKeys);
  186. setSelectedRows(
  187. tableList.filter((item) =>
  188. selectedRowKeys.includes(item.table.id)
  189. )
  190. );
  191. },
  192. }}
  193. columns={[
  194. {
  195. title: "类型",
  196. dataIndex: "type",
  197. valueType: "select",
  198. valueEnum: {
  199. 3: "业务表",
  200. 2: "流程表",
  201. },
  202. },
  203. {
  204. title: "编码",
  205. dataIndex: "schemaName",
  206. valueType: "text",
  207. },
  208. {
  209. title: "别名",
  210. dataIndex: "aliasName",
  211. valueType: "text",
  212. },
  213. {
  214. title: "名称",
  215. dataIndex: "langNameList",
  216. render: (_, record) => {
  217. return (
  218. record.langNameList?.find(
  219. (item: any) => item.name === "zh-CN"
  220. )?.value || "-"
  221. );
  222. },
  223. },
  224. {
  225. title: "描述",
  226. dataIndex: "langDescriptionList",
  227. render: (_, record) => {
  228. return (
  229. record?.langDescriptionList?.find(
  230. (item: any) => item.name === "zh-CN"
  231. )?.value || "-"
  232. );
  233. },
  234. },
  235. ]}
  236. />
  237. </>
  238. );
  239. };
  240. const Step2Comp = () => {
  241. const list = selectedRows.filter(({ table }) =>
  242. existTableList.find((item) => item.modelTable.table.id === table.id)
  243. );
  244. return (
  245. <>
  246. <div className="text-14px font-bold m-y-12px">
  247. 提示:共选择了
  248. <span className="color-green">{selectedRows.length}</span>
  249. 张表格数据。
  250. {existTableList.length ? (
  251. <>
  252. 其中有
  253. <span className="color-#faad14">{existTableList.length}</span>
  254. 张表已存在,可对比调整差异后开始操作。
  255. </>
  256. ) : null}
  257. </div>
  258. <Collapse
  259. items={list.map((item) => {
  260. const { table } = item;
  261. const name = table.langNameList?.find(
  262. (item: any) => item.name === "zh-CN"
  263. )?.value;
  264. const existTable = existTableList.find(
  265. (item) => item.modelTable.table.id === table.id
  266. );
  267. return {
  268. key: table.id,
  269. label: (
  270. <span
  271. style={{
  272. color: existTable ? "#faad14" : "",
  273. }}
  274. >{`${table.schemaName}${name ? `(${name})` : ""}`}</span>
  275. ),
  276. children: (
  277. <DiffTable
  278. sourceTable={item}
  279. targetTableId={existTable?.dataTable?.id}
  280. onChange={handleUpdateTable}
  281. />
  282. ),
  283. };
  284. })}
  285. />
  286. </>
  287. );
  288. };
  289. const Step3Comp = () => {
  290. return (
  291. <div className="flex justify-center items-center h-full">
  292. {okLoading ? (
  293. <Spin spinning={okLoading} tip="正在同步数据...">
  294. <div className="w-500px h-full"></div>
  295. </Spin>
  296. ) : (
  297. <>
  298. {resultStatus === "success" ? (
  299. <Result
  300. status="success"
  301. title={`数据${tabActiveKey === '1' ? '拉取' : '推送'}完成!`}
  302. subTitle="请查看数据表的数据信息"
  303. />
  304. ) : (
  305. <Result
  306. status="error"
  307. title={`数据${tabActiveKey === '1' ? '拉取' : '推送'}失败!`}
  308. subTitle="请检查数据表的数据信息"
  309. />
  310. )}
  311. </>
  312. )}
  313. </div>
  314. );
  315. };
  316. const handlePush = async () => {
  317. setStep(2);
  318. try {
  319. setOkLoading(true);
  320. await PushDataModelTable(selectedRows);
  321. // message.success("同步推送完成");
  322. setResultStatus("success");
  323. } catch (err) {
  324. setResultStatus("error");
  325. } finally {
  326. setOkLoading(false);
  327. }
  328. };
  329. const handlePull = async () => {
  330. setOkLoading(true);
  331. try {
  332. // 1、更新原有表的数据
  333. const tableIds = selectedRows.map(({ table }) => table.id);
  334. await SaveDataModel({
  335. erDataModel: {
  336. ...project,
  337. tables: project.tables.map((item) => {
  338. if (tableIds.includes(item.table.id)) {
  339. return (
  340. selectedRows.find((t) => t.table.id === item.table.id) ||
  341. item
  342. );
  343. }
  344. return item;
  345. }),
  346. },
  347. });
  348. } catch (err) {
  349. console.log(err);
  350. message.error("拉取失败");
  351. setResultStatus("error");
  352. }
  353. // 2、添加引入数据
  354. try {
  355. const businessTables =
  356. data1?.result
  357. ?.filter((item: any) => importTables?.includes(item.id))
  358. ?.map((item: any) => {
  359. return pick(item, [
  360. "aliasName",
  361. "schemaName",
  362. "id",
  363. "parentBusinessTableId",
  364. ]);
  365. }) || [];
  366. if (!businessTables.length) {
  367. setResultStatus("success");
  368. setStep(2);
  369. return;
  370. }
  371. await ImportFromBusinessTables({
  372. dataModelId: project.id,
  373. color,
  374. businessTables,
  375. });
  376. } catch (err) {
  377. console.log(err);
  378. message.error("引入失败");
  379. setResultStatus("error");
  380. } finally {
  381. setOkLoading(false);
  382. setStep(2);
  383. }
  384. };
  385. const handleDone = () => {
  386. setOpen(false);
  387. props.onPush?.();
  388. };
  389. return (
  390. <Modal
  391. title="数据同步"
  392. width={"100%"}
  393. open={open}
  394. loading={loading}
  395. style={{
  396. top: 10,
  397. padding: 0,
  398. }}
  399. bodyProps={{
  400. style: {
  401. height: "80vh",
  402. overflowY: "auto",
  403. },
  404. }}
  405. okButtonProps={{
  406. loading: okLoading,
  407. disabled: !selectedRows.length,
  408. }}
  409. onCancel={() => setOpen(false)}
  410. footer={(_, { CancelBtn }) => {
  411. return (
  412. <>
  413. <CancelBtn />
  414. {step === 0 && (
  415. <Button
  416. type="primary"
  417. disabled={!selectedRows.length}
  418. onClick={() => setStep(step + 1)}
  419. >
  420. 下一步
  421. </Button>
  422. )}
  423. {step > 0 && (
  424. <Button onClick={() => setStep(step - 1)}>上一步</Button>
  425. )}
  426. {step === 2 && resultStatus === "success" && (
  427. <Button type="primary" onClick={handleDone}>
  428. 完成
  429. </Button>
  430. )}
  431. {step === 1 && tabActiveKey === "1" && (
  432. <Button type="primary" onClick={handlePull}>
  433. 开始
  434. </Button>
  435. )}
  436. {step === 1 && tabActiveKey === "2" && (
  437. <Button type="primary" onClick={handlePush}>
  438. 开始
  439. </Button>
  440. )}
  441. </>
  442. );
  443. }}
  444. >
  445. <Tabs
  446. activeKey={tabActiveKey}
  447. onChange={setTabActiveKey}
  448. items={[
  449. {
  450. key: "1",
  451. label: "拉取",
  452. children: (
  453. <div className="h-full flex flex-col overflow-hidden">
  454. <div className="py-12px px-30px">
  455. <Steps
  456. current={step}
  457. // progressDot
  458. items={[
  459. {
  460. title: "选择数据表",
  461. description: "选择需要拉取的表",
  462. },
  463. {
  464. title: "差异对比",
  465. description: "数据模型与数据表进行对比",
  466. },
  467. {
  468. title: "结果",
  469. description: "拉取到模型",
  470. },
  471. ]}
  472. />
  473. </div>
  474. {step === 0 && (
  475. <>
  476. <Form>
  477. <Form.Item label="引入表" name="table">
  478. <TreeSelect
  479. multiple
  480. treeCheckable
  481. allowClear
  482. placeholder="请选择"
  483. loading={loading1}
  484. treeData={treeData}
  485. value={importTables}
  486. onChange={(value) => {
  487. setImportTables(value);
  488. }}
  489. />
  490. </Form.Item>
  491. <Form.Item label="颜色" name="color">
  492. <CustomColorPicker onChange={setColor}>
  493. {color ? (
  494. <div
  495. className="rounded-4px cus-btn w-32px h-32px bg-#eee flex-none cursor-pointer shadow-inner"
  496. style={{ background: color || "#eee" }}
  497. ></div>
  498. ) : (
  499. <span className="bg-#eee px-5px py-3px rounded-4px cursor-pointer text-12px text-#666">
  500. 随机生成,点击可选择颜色
  501. </span>
  502. )}
  503. </CustomColorPicker>
  504. </Form.Item>
  505. </Form>
  506. <Step1Comp />
  507. </>
  508. )}
  509. {step === 1 && <Step2Comp />}
  510. {step === 2 && <Step3Comp />}
  511. </div>
  512. ),
  513. },
  514. {
  515. key: "2",
  516. label: "推送",
  517. children: (
  518. <div className="h-full flex flex-col overflow-hidden">
  519. <div className="py-12px px-30px">
  520. <Steps
  521. current={step}
  522. // progressDot
  523. items={[
  524. {
  525. title: "选择数据表",
  526. description: "选择需要同步的表",
  527. },
  528. {
  529. title: "差异对比",
  530. description: "数据模型与数据表进行对比",
  531. },
  532. {
  533. title: "结果",
  534. description: "推送到数据表",
  535. },
  536. ]}
  537. />
  538. </div>
  539. <div className="flex-1 overflow-auto">
  540. {step === 0 && <Step1Comp />}
  541. {step === 1 && <Step2Comp />}
  542. {step === 2 && <Step3Comp />}
  543. </div>
  544. </div>
  545. ),
  546. },
  547. ]}
  548. />
  549. {!tableList.length && (
  550. <Empty
  551. image={NoData}
  552. description="当前模型表为空,请添加后再进行同步!"
  553. />
  554. )}
  555. </Modal>
  556. );
  557. });