ChannelSentDiff.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import { Button, message, Select, Table, Tooltip, Typography } from "antd";
  2. import { type Change, diffChars } from "diff";
  3. import { useEffect, useState } from "react";
  4. import { post } from "../../../src/request";
  5. import type {
  6. ISentence,
  7. ISentenceDiffData,
  8. ISentenceDiffRequest,
  9. ISentenceDiffResponse,
  10. ISentenceListResponse,
  11. ISentenceNewRequest,
  12. } from "../../../src/api/Corpus";
  13. import store from "../../../src/store";
  14. import { accept } from "../../../src/reducers/accept-pr";
  15. import type { IChannel } from "../../../src/api/Channel";
  16. const { Text } = Typography;
  17. interface IDataType {
  18. key: React.Key;
  19. sentId: string;
  20. pali?: string | null;
  21. srcContent?: string | null;
  22. destContent?: string | null;
  23. }
  24. interface IWidget {
  25. srcChannel?: IChannel;
  26. destChannel?: IChannel;
  27. sentences?: string[];
  28. important?: boolean;
  29. goPrev?: Function;
  30. onSubmit?: (total: number) => void;
  31. }
  32. const ChannelSentDiffWidget = ({
  33. srcChannel,
  34. destChannel,
  35. sentences,
  36. important = false,
  37. goPrev,
  38. onSubmit,
  39. }: IWidget) => {
  40. const [srcApiData, setSrcApiData] = useState<ISentenceDiffData[]>([]);
  41. const [diffData, setDiffData] = useState<IDataType[]>();
  42. const [loading, setLoading] = useState(false);
  43. const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>();
  44. const [newRowKeys, setNewRowKeys] = useState<React.Key[]>();
  45. const [emptyRowKeys, setEmptyRowKeys] = useState<React.Key[]>();
  46. useEffect(() => {
  47. if (sentences && srcChannel && destChannel) {
  48. post<ISentenceDiffRequest, ISentenceDiffResponse>(`/v2/sent-in-channel`, {
  49. sentences: sentences,
  50. channels: ["_System_Pali_VRI_", srcChannel.id, destChannel.id],
  51. }).then((json) => {
  52. if (json.ok) {
  53. const apiData = json.data.rows;
  54. setSrcApiData(apiData);
  55. const newRows: string[] = [];
  56. const emptyRows: string[] = [];
  57. const diffList: IDataType[] = sentences?.map((item, _index) => {
  58. const id: string[] = item.split("-");
  59. const srcContent = apiData.find(
  60. (element) =>
  61. element.book_id === parseInt(id[0]) &&
  62. element.paragraph === parseInt(id[1]) &&
  63. element.word_start === parseInt(id[2]) &&
  64. element.word_end === parseInt(id[3]) &&
  65. element.channel_uid === srcChannel.id
  66. );
  67. const destContent = apiData.find(
  68. (element) =>
  69. element.book_id === parseInt(id[0]) &&
  70. element.paragraph === parseInt(id[1]) &&
  71. element.word_start === parseInt(id[2]) &&
  72. element.word_end === parseInt(id[3]) &&
  73. element.channel_uid === destChannel.id
  74. );
  75. if (srcContent && destContent) {
  76. const srcDate = new Date(srcContent.updated_at);
  77. const destDate = new Date(destContent.updated_at);
  78. if (srcDate > destDate) {
  79. newRows.push(item);
  80. }
  81. }
  82. if (
  83. typeof destContent === "undefined" ||
  84. destContent.content?.trim().length === 0
  85. ) {
  86. emptyRows.push(item);
  87. }
  88. const paliContent = apiData.find(
  89. (element) =>
  90. element.book_id === parseInt(id[0]) &&
  91. element.paragraph === parseInt(id[1]) &&
  92. element.word_start === parseInt(id[2]) &&
  93. element.word_end === parseInt(id[3]) &&
  94. element.channel_uid !== destChannel.id &&
  95. element.channel_uid !== srcChannel.id
  96. );
  97. return {
  98. key: item,
  99. sentId: item,
  100. pali: paliContent?.content,
  101. srcContent: srcContent?.content,
  102. destContent: destContent?.content,
  103. };
  104. });
  105. setDiffData(diffList);
  106. setNewRowKeys(newRows);
  107. if (important) {
  108. setSelectedRowKeys(sentences);
  109. } else {
  110. setSelectedRowKeys(newRows);
  111. }
  112. setEmptyRowKeys(emptyRows);
  113. }
  114. });
  115. }
  116. }, [srcChannel, sentences, destChannel]);
  117. return (
  118. <div>
  119. <div style={{ display: "flex", justifyContent: "space-between" }}>
  120. <Button
  121. onClick={() => {
  122. if (typeof goPrev !== "undefined") {
  123. goPrev();
  124. }
  125. }}
  126. >
  127. 上一步
  128. </Button>
  129. <Select
  130. defaultValue={important ? "all" : "new"}
  131. style={{ width: 180 }}
  132. disabled={important}
  133. onChange={(value: string) => {
  134. switch (value) {
  135. case "new":
  136. setSelectedRowKeys(newRowKeys);
  137. break;
  138. case "all":
  139. setSelectedRowKeys(sentences);
  140. break;
  141. case "empty":
  142. setSelectedRowKeys(emptyRowKeys);
  143. break;
  144. default:
  145. break;
  146. }
  147. }}
  148. options={[
  149. { value: "new", label: "仅复制较新的" },
  150. { value: "empty", label: "仅复制缺失的" },
  151. { value: "all", label: "全部复制" },
  152. ]}
  153. />
  154. <Button
  155. type="primary"
  156. loading={loading}
  157. onClick={() => {
  158. if (typeof srcChannel === "undefined") {
  159. return;
  160. }
  161. if (
  162. typeof selectedRowKeys === "undefined" ||
  163. selectedRowKeys.length === 0
  164. ) {
  165. message.warning("没有被选择的句子");
  166. return;
  167. }
  168. setLoading(true);
  169. const submitData: ISentenceDiffData[] = [];
  170. selectedRowKeys?.forEach((value) => {
  171. const id: string[] = value.toString().split("-");
  172. const srcContent = srcApiData.find(
  173. (element) =>
  174. element.book_id === parseInt(id[0]) &&
  175. element.paragraph === parseInt(id[1]) &&
  176. element.word_start === parseInt(id[2]) &&
  177. element.word_end === parseInt(id[3]) &&
  178. element.channel_uid === srcChannel.id
  179. );
  180. if (srcContent) {
  181. submitData.push(srcContent);
  182. }
  183. });
  184. if (typeof submitData === "undefined") {
  185. return;
  186. }
  187. const url = `/v2/sentence`;
  188. const postData = {
  189. sentences: submitData,
  190. channel: destChannel?.id,
  191. copy: true,
  192. fork_from: srcChannel.id,
  193. };
  194. console.info("fork post api request", url, postData);
  195. post<ISentenceNewRequest, ISentenceListResponse>(url, postData)
  196. .then((json) => {
  197. console.info("fork api response", json);
  198. if (json.ok) {
  199. //发布数据
  200. const newData: ISentence[] = json.data.rows.map((item) =>
  201. toISentence(item)
  202. );
  203. store.dispatch(accept(newData));
  204. if (onSubmit) {
  205. onSubmit(json.data.count);
  206. }
  207. } else {
  208. message.error(json.message);
  209. }
  210. })
  211. .catch((e) => {
  212. console.log(e);
  213. message.error("error");
  214. })
  215. .finally(() => {
  216. setLoading(false);
  217. });
  218. }}
  219. >
  220. 开始复制
  221. </Button>
  222. </div>
  223. <div style={{ height: 400, overflowY: "scroll" }}>
  224. <Table
  225. pagination={false}
  226. rowSelection={{
  227. type: "checkbox",
  228. selectedRowKeys: selectedRowKeys,
  229. onChange: (
  230. selectedRowKeys: React.Key[],
  231. selectedRows: IDataType[]
  232. ) => {
  233. console.log(
  234. `selectedRowKeys: ${selectedRowKeys}`,
  235. "selectedRows: ",
  236. selectedRows
  237. );
  238. setSelectedRowKeys(selectedRowKeys);
  239. },
  240. getCheckboxProps: (record: IDataType) => ({
  241. name: record.pali ? record.pali : undefined,
  242. }),
  243. }}
  244. columns={[
  245. {
  246. title: "pali",
  247. width: "33%",
  248. dataIndex: "pali",
  249. render: (_value, record) => {
  250. return (
  251. <Text>
  252. <div
  253. dangerouslySetInnerHTML={{
  254. __html: record.pali ? record.pali : "",
  255. }}
  256. />
  257. </Text>
  258. );
  259. },
  260. },
  261. {
  262. title: (
  263. <>
  264. {`原文-`}
  265. <Text strong>{srcChannel?.name}</Text>
  266. </>
  267. ),
  268. width: "33%",
  269. dataIndex: "srcContent",
  270. },
  271. {
  272. title: (
  273. <>
  274. {`复制到-`}
  275. <Text strong>{destChannel?.name}</Text>
  276. </>
  277. ),
  278. width: "33%",
  279. dataIndex: "destContent",
  280. render: (_value, record) => {
  281. const diff: Change[] = diffChars(
  282. record.destContent ? record.destContent : "",
  283. record.srcContent ? record.srcContent : ""
  284. );
  285. const diffResult = diff.map((item, id) => {
  286. return (
  287. <Text
  288. key={id}
  289. type={
  290. item.added
  291. ? "success"
  292. : item.removed
  293. ? "danger"
  294. : "secondary"
  295. }
  296. delete={item.removed ? true : undefined}
  297. >
  298. {item.value}
  299. </Text>
  300. );
  301. });
  302. return (
  303. <Tooltip title={record.destContent}>{diffResult}</Tooltip>
  304. );
  305. },
  306. },
  307. ]}
  308. dataSource={diffData}
  309. />
  310. </div>
  311. </div>
  312. );
  313. };
  314. export default ChannelSentDiffWidget;