ChannelSentDiff.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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 "../../request";
  5. import type {
  6. ISentenceDiffData,
  7. ISentenceDiffRequest,
  8. ISentenceDiffResponse,
  9. ISentenceListResponse,
  10. ISentenceNewRequest,
  11. } from "../../api/Corpus";
  12. import type { IChannel } from "./Channel";
  13. import { type ISentence, toISentence } from "../template/SentEdit";
  14. import store from "../../store";
  15. import { accept } from "../../reducers/accept-pr";
  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. onSubmit && onSubmit(json.data.count);
  205. } else {
  206. message.error(json.message);
  207. }
  208. })
  209. .catch((e) => {
  210. console.log(e);
  211. message.error("error");
  212. })
  213. .finally(() => {
  214. setLoading(false);
  215. });
  216. }}
  217. >
  218. 开始复制
  219. </Button>
  220. </div>
  221. <div style={{ height: 400, overflowY: "scroll" }}>
  222. <Table
  223. pagination={false}
  224. rowSelection={{
  225. type: "checkbox",
  226. selectedRowKeys: selectedRowKeys,
  227. onChange: (
  228. selectedRowKeys: React.Key[],
  229. selectedRows: IDataType[]
  230. ) => {
  231. console.log(
  232. `selectedRowKeys: ${selectedRowKeys}`,
  233. "selectedRows: ",
  234. selectedRows
  235. );
  236. setSelectedRowKeys(selectedRowKeys);
  237. },
  238. getCheckboxProps: (record: IDataType) => ({
  239. name: record.pali ? record.pali : undefined,
  240. }),
  241. }}
  242. columns={[
  243. {
  244. title: "pali",
  245. width: "33%",
  246. dataIndex: "pali",
  247. render: (_value, record, _index) => {
  248. return (
  249. <Text>
  250. <div
  251. dangerouslySetInnerHTML={{
  252. __html: record.pali ? record.pali : "",
  253. }}
  254. />
  255. </Text>
  256. );
  257. },
  258. },
  259. {
  260. title: (
  261. <>
  262. {`原文-`}
  263. <Text strong>{srcChannel?.name}</Text>
  264. </>
  265. ),
  266. width: "33%",
  267. dataIndex: "srcContent",
  268. },
  269. {
  270. title: (
  271. <>
  272. {`复制到-`}
  273. <Text strong>{destChannel?.name}</Text>
  274. </>
  275. ),
  276. width: "33%",
  277. dataIndex: "destContent",
  278. render: (_value, record, _index) => {
  279. const diff: Change[] = diffChars(
  280. record.destContent ? record.destContent : "",
  281. record.srcContent ? record.srcContent : ""
  282. );
  283. const diffResult = diff.map((item, id) => {
  284. return (
  285. <Text
  286. key={id}
  287. type={
  288. item.added
  289. ? "success"
  290. : item.removed
  291. ? "danger"
  292. : "secondary"
  293. }
  294. delete={item.removed ? true : undefined}
  295. >
  296. {item.value}
  297. </Text>
  298. );
  299. });
  300. return (
  301. <Tooltip title={record.destContent}>{diffResult}</Tooltip>
  302. );
  303. },
  304. },
  305. ]}
  306. dataSource={diffData}
  307. />
  308. </div>
  309. </div>
  310. );
  311. };
  312. export default ChannelSentDiffWidget;