TableEdit.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. import React, { useEffect } from "react";
  2. import { EditableProTable, ProColumns } from "@ant-design/pro-components";
  3. import type { ColumnItem } from "@/type";
  4. import { createColumn } from "@/utils";
  5. import { DataType } from "@/enum";
  6. import { DATA_TYPE_OPTIONS } from "@/constants";
  7. import {
  8. Button,
  9. Input,
  10. InputNumber,
  11. message,
  12. Switch,
  13. Tooltip,
  14. Upload,
  15. UploadFile,
  16. } from "antd";
  17. import LangInput from "./LangInput";
  18. import { validateColumnCode } from "@/utils/validator";
  19. import VariableModal from "./VariableModal";
  20. import ColumnVariableModal from "./ColumnVariableModal";
  21. import { FormInstance } from "antd/lib";
  22. import {
  23. DownloadOutlined,
  24. InfoCircleOutlined,
  25. UploadOutlined,
  26. } from "@ant-design/icons";
  27. import { parseExcel } from "@/utils/parseExcel";
  28. import ImportResultModal from "./ImportResultModal";
  29. export default function TableEdit(props: {
  30. tableId?: string;
  31. data: any[];
  32. modelId?: string;
  33. onChange?: (data: readonly ColumnItem[]) => void;
  34. }) {
  35. const [editableKeys, setEditableRowKeys] = React.useState<React.Key[]>([]);
  36. const [dataSource, setDataSource] = React.useState<readonly ColumnItem[]>(
  37. props.data
  38. );
  39. const boxRef = React.useRef<HTMLDivElement>(null);
  40. const importResultRef = React.useRef<{
  41. open: (data: any[], columns: readonly ColumnItem[]) => void;
  42. }>();
  43. useEffect(() => {
  44. props.onChange?.(dataSource);
  45. }, [dataSource]);
  46. useEffect(() => {
  47. setDataSource(props.data);
  48. }, [props.data]);
  49. const DefaultValueComp = ({
  50. value,
  51. onChange,
  52. }: {
  53. value?: string;
  54. onChange?: (value: string) => void;
  55. }) => {
  56. return (
  57. <div className="flex gap-2px">
  58. <Input
  59. placeholder="默认值"
  60. value={value}
  61. onChange={(e) => {
  62. onChange?.(e.target.value);
  63. }}
  64. />
  65. <div>
  66. <VariableModal
  67. trigger={
  68. <Button
  69. size="small"
  70. style={{ width: 40, fontSize: 12 }}
  71. type="text"
  72. >
  73. +变量
  74. </Button>
  75. }
  76. onOk={(val) => onChange?.(val)}
  77. />
  78. <ColumnVariableModal
  79. trigger={
  80. <Button
  81. size="small"
  82. style={{ width: 40, fontSize: 12 }}
  83. type="text"
  84. >
  85. +字段
  86. </Button>
  87. }
  88. onOk={(val) => onChange?.(val)}
  89. />
  90. </div>
  91. </div>
  92. );
  93. };
  94. const LengthComp = ({
  95. value,
  96. onChange,
  97. model,
  98. form,
  99. rowKey,
  100. }: {
  101. value?: string;
  102. onChange?: (value: string) => void;
  103. model: ColumnItem;
  104. form: FormInstance;
  105. rowKey?: React.Key | React.Key[];
  106. }) => {
  107. return (
  108. <span className="flex gap-2px">
  109. <InputNumber
  110. min={0}
  111. placeholder="总长度"
  112. defaultValue={model.precision}
  113. onChange={(value) =>
  114. form.setFieldValue([rowKey || "", "precision"], value)
  115. }
  116. />
  117. <InputNumber
  118. min={0}
  119. placeholder="小数位数"
  120. defaultValue={model.scale}
  121. onChange={(value) =>
  122. form.setFieldValue([rowKey || "", "scale"], value)
  123. }
  124. />
  125. </span>
  126. );
  127. };
  128. const columns: ProColumns[] = [
  129. {
  130. title: "操作",
  131. valueType: "option",
  132. width: 130,
  133. render: (_text, record, _, action) =>
  134. record.isPreDefined
  135. ? []
  136. : [
  137. <a
  138. key="editable"
  139. onClick={() => {
  140. action?.startEditable?.(record.id);
  141. }}
  142. >
  143. 编辑
  144. </a>,
  145. <a
  146. key="delete"
  147. onClick={() => {
  148. setDataSource(
  149. dataSource.filter((item) => item.id !== record.id)
  150. );
  151. }}
  152. >
  153. 删除
  154. </a>,
  155. ],
  156. },
  157. {
  158. title: "字段代码",
  159. dataIndex: "schemaName",
  160. valueType: "text",
  161. width: 150,
  162. formItemProps: {
  163. rules: [
  164. {
  165. required: true,
  166. message: "请输入字段名称",
  167. },
  168. {
  169. max: 50,
  170. message: "字段名称不能超过50个字符",
  171. },
  172. validateColumnCode,
  173. ],
  174. },
  175. },
  176. {
  177. title: "字段名称",
  178. dataIndex: "langName",
  179. valueType: "text",
  180. width: 150,
  181. render: (_dom, entity) => {
  182. return (
  183. entity.langNameList?.find(
  184. (item: Record<string, string>) => item.name === "zh-CN"
  185. )?.value || "-"
  186. );
  187. },
  188. renderFormItem: (_schema, config, form) => {
  189. const model = config.record;
  190. const rowKey = config.recordKey;
  191. return (
  192. <span>
  193. <LangInput
  194. style={{ width: 150 }}
  195. value={model.langNameList}
  196. onChange={(langValue, key) => {
  197. form.setFieldValue([rowKey || "", "langNameList"], langValue);
  198. form.setFieldValue([rowKey || "", "langName"], "");
  199. }}
  200. />
  201. </span>
  202. );
  203. },
  204. },
  205. {
  206. title: "类型",
  207. dataIndex: "type",
  208. valueType: "select",
  209. width: 90,
  210. fieldProps: (form, { rowKey }) => {
  211. return {
  212. options: DATA_TYPE_OPTIONS,
  213. onChange: () => {
  214. form.setFieldValue([rowKey || "", "maxLength"], undefined);
  215. form.setFieldValue([rowKey || "", "scale"], undefined);
  216. form.setFieldValue([rowKey || "", "precision"], undefined);
  217. },
  218. };
  219. },
  220. formItemProps: {
  221. rules: [
  222. {
  223. required: true,
  224. message: "请选择类型",
  225. },
  226. ],
  227. },
  228. },
  229. {
  230. title: "长度",
  231. dataIndex: "maxLength",
  232. valueType: "digit",
  233. width: 90,
  234. fieldProps: {
  235. precision: 0,
  236. },
  237. render: (text, record) => {
  238. return record.type === DataType.Decimal
  239. ? `${record.precision}${record.scale ? `,${record.scale}` : ""}`
  240. : text;
  241. },
  242. renderFormItem: (_schema, config, form) => {
  243. const model = config.record;
  244. const rowKey = config.recordKey;
  245. return model.type === DataType.Nvarchar ? (
  246. <InputNumber min={0} max={4000} placeholder="字符长度" />
  247. ) : model.type === DataType.Decimal ? (
  248. <LengthComp model={model} form={form} rowKey={rowKey} />
  249. ) : (
  250. "-"
  251. );
  252. },
  253. },
  254. {
  255. title: "必填",
  256. dataIndex: "isRequired",
  257. valueType: "switch",
  258. render: (text, record) => {
  259. return <Switch disabled checked={record.isRequired} />;
  260. },
  261. width: 80,
  262. },
  263. {
  264. title: "唯一",
  265. dataIndex: "isUnique",
  266. valueType: "switch",
  267. render: (text, record) => {
  268. return <Switch disabled checked={record.isUnique} />;
  269. },
  270. width: 80,
  271. },
  272. {
  273. title: "默认值",
  274. dataIndex: "defaultValue",
  275. valueType: "text",
  276. width: 150,
  277. renderFormItem() {
  278. return <DefaultValueComp />;
  279. },
  280. },
  281. {
  282. title: "字符集",
  283. dataIndex: "chartset",
  284. valueType: "text",
  285. width: 120,
  286. renderFormItem: (_schema, config) => {
  287. return config.record.type === DataType.Nvarchar ? (
  288. <Input placeholder="字符集" />
  289. ) : null;
  290. },
  291. },
  292. {
  293. title: "内容",
  294. dataIndex: "whereInputContent",
  295. valueType: "text",
  296. width: 120,
  297. renderFormItem: (_schema, config) => {
  298. return config.record.type === DataType.Nvarchar ? (
  299. <Input.TextArea placeholder="内容..." />
  300. ) : null;
  301. },
  302. },
  303. {
  304. title: "描述",
  305. dataIndex: "memo",
  306. valueType: "textarea",
  307. width: 150,
  308. renderFormItem: () => {
  309. return <Input.TextArea placeholder="描述..." />;
  310. },
  311. },
  312. {
  313. title: "预定义字段",
  314. dataIndex: "isPreDefined",
  315. valueType: "switch",
  316. readonly: true,
  317. render: (text, record) => {
  318. return record.isPreDefined ? "是" : "否";
  319. },
  320. renderFormItem: (schema, config) => {
  321. return config.record.isPreDefined ? "是" : "否";
  322. },
  323. },
  324. ];
  325. const handleAdd = () => {
  326. return createColumn(props?.tableId, dataSource.length + 1);
  327. };
  328. // 上传字段模版文件
  329. const handleUpload = (file: UploadFile) => {
  330. message.loading("正在解析文件...", 0);
  331. parseExcel<any>(file)
  332. .then((res) => {
  333. console.log("加载数据:", res);
  334. const list = res?.["表单字段"];
  335. message.destroy();
  336. if (!list || !list.length) {
  337. message.warning("当前文件无字段数据,请检查");
  338. } else {
  339. importResultRef.current?.open(list, dataSource);
  340. }
  341. })
  342. .catch((err) => {
  343. console.error("加载数据失败:", err);
  344. message.error("文件解析失败");
  345. message.destroy();
  346. });
  347. };
  348. // 解析粘贴板数据
  349. // 解析出每一行的数据
  350. const parseClipBoard = (text: string) => {
  351. const arr = text.split("\r\n");
  352. return arr.map((item) => item.split("\t"));
  353. };
  354. const regex = /^[a-z][a-z0-9_]*$/;
  355. const handlePaste = () => {
  356. // 获取粘贴板数据
  357. navigator.clipboard.readText().then((text) => {
  358. console.log("粘贴板数据:", text);
  359. // 无文本,或格式不对
  360. if (!text || !text.includes("\t")) {
  361. message.warning("粘贴数据格式不正确,请检查");
  362. return;
  363. }
  364. const parseList = parseClipBoard(text);
  365. // 过滤无效数据
  366. const list = parseList
  367. .filter((item) => item.length >= 3 && regex.test(item?.[0]))
  368. .map((item) => ({
  369. 字段名: item[0],
  370. 类型: item[1],
  371. 长度: item[2],
  372. 中文名: item[3],
  373. 英文名: item[4],
  374. 是否必填: item[5],
  375. 描述: item[6],
  376. 默认值: item[7],
  377. }));
  378. if (!list.length) {
  379. message.warning("粘贴板无有效数据,请检查");
  380. return;
  381. }
  382. importResultRef.current?.open(list, dataSource);
  383. });
  384. };
  385. useEffect(() => {
  386. // 监听ctrl+v 粘贴表格数据
  387. document.addEventListener("paste", handlePaste);
  388. return () => {
  389. document.removeEventListener("paste", handlePaste);
  390. };
  391. }, []);
  392. const handleChangeColumn = (list: ColumnItem[]) => {
  393. setDataSource(list);
  394. };
  395. return (
  396. <div className="w-full h-full overflow-auto" ref={boxRef}>
  397. <ImportResultModal
  398. ref={importResultRef}
  399. tableId={props?.tableId}
  400. onChange={handleChangeColumn}
  401. />
  402. <EditableProTable
  403. columns={columns}
  404. rowKey="id"
  405. size="small"
  406. value={dataSource}
  407. onChange={setDataSource}
  408. recordCreatorProps={{
  409. record: handleAdd,
  410. }}
  411. editable={{
  412. type: "multiple",
  413. editableKeys,
  414. onChange: setEditableRowKeys,
  415. }}
  416. toolBarRender={() => [
  417. <span key="paste" className="text-gray-500">可使用粘贴导入</span>,
  418. <Tooltip
  419. key="info"
  420. title={
  421. <div>
  422. <p>填写规范:</p>
  423. <p>
  424. 1、字段名第一位必须为大写字母,之后的对象可以字母大小写,数字或下划线;
  425. </p>
  426. <p>2、Nvarchar类型长度最大不可以超过4000;</p>
  427. <p>
  428. 3、Decimal类型精度的填写方式:总精度,有效小数位,eg: 18,6;
  429. </p>
  430. <p>4、Int、Datetime等类型长度需填写为0;</p>
  431. </div>
  432. }
  433. >
  434. <InfoCircleOutlined />
  435. </Tooltip>,
  436. <Button key="download" icon={<DownloadOutlined />}>
  437. <a
  438. download={"BusinessTableColumnsImportTemplate.xlsx"}
  439. href="/Content/Template/BusinessTableColumnsImportTemplate.xlsx"
  440. >
  441. 下载模版
  442. </a>
  443. </Button>,
  444. <Upload
  445. key="upload"
  446. accept=".xlsx"
  447. showUploadList={false}
  448. beforeUpload={(file) => handleUpload(file)}
  449. >
  450. <Button
  451. type="primary"
  452. icon={<UploadOutlined />}
  453. onClick={() => {
  454. handleAdd();
  455. }}
  456. >
  457. 导入字段
  458. </Button>
  459. </Upload>,
  460. ]}
  461. />
  462. </div>
  463. );
  464. }