SentCell.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import { useEffect, useState } from "react";
  2. import { useIntl } from "react-intl";
  3. import { message as AntdMessage, Modal, Collapse } from "antd";
  4. import { ExclamationCircleOutlined, LoadingOutlined } from "@ant-design/icons";
  5. import { ISentence } from "../SentEdit";
  6. import SentEditMenu from "./SentEditMenu";
  7. import SentCellEditable from "./SentCellEditable";
  8. import MdView from "../MdView";
  9. import EditInfo, { Details } from "./EditInfo";
  10. import SuggestionToolbar from "./SuggestionToolbar";
  11. import { useAppSelector } from "../../../hooks";
  12. import { accept, doneSent, done, sentence } from "../../../reducers/accept-pr";
  13. import { IWbw } from "../Wbw/WbwWord";
  14. import { my_to_roman } from "../../code/my";
  15. import SentWbwEdit, { sentSave } from "./SentWbwEdit";
  16. import { getEnding } from "../../../reducers/nissaya-ending-vocabulary";
  17. import { nissayaBase } from "../Nissaya/NissayaMeaning";
  18. import { anchor, message } from "../../../reducers/discussion";
  19. import TextDiff from "../../general/TextDiff";
  20. import { sentSave as _sentSave } from "./SentCellEditable";
  21. import { IDeleteResponse } from "../../api/Article";
  22. import { delete_, get } from "../../../request";
  23. import "./style.css";
  24. import StudioName from "../../auth/Studio";
  25. import CopyToModal from "../../channel/CopyToModal";
  26. import store from "../../../store";
  27. import { randomString } from "../../../utils";
  28. import User from "../../auth/User";
  29. import { ISentenceListResponse } from "../../api/Corpus";
  30. import { toISentence } from "./SentCanRead";
  31. import SentAttachment from "./SentAttachment";
  32. interface ISnowFlakeResponse {
  33. ok: boolean;
  34. message?: string;
  35. data: {
  36. rows: string;
  37. count: number;
  38. };
  39. }
  40. interface IWidget {
  41. initValue?: ISentence;
  42. value?: ISentence;
  43. wordWidget?: boolean;
  44. isPr?: boolean;
  45. editMode?: boolean;
  46. compact?: boolean;
  47. showDiff?: boolean;
  48. diffText?: string | null;
  49. onChange?: (data: ISentence) => void;
  50. onDelete?: Function;
  51. }
  52. const SentCellWidget = ({
  53. initValue,
  54. value,
  55. wordWidget = false,
  56. isPr = false,
  57. editMode = false,
  58. compact = false,
  59. showDiff = false,
  60. diffText,
  61. onChange,
  62. onDelete,
  63. }: IWidget) => {
  64. console.debug("SentCell render", value);
  65. const intl = useIntl();
  66. const [isEditMode, setIsEditMode] = useState(editMode);
  67. const [sentData, setSentData] = useState<ISentence | undefined>(initValue);
  68. const [bgColor, setBgColor] = useState<string>();
  69. const [loading, setLoading] = useState(false);
  70. const [uuid] = useState(randomString());
  71. const endings = useAppSelector(getEnding);
  72. const acceptPr = useAppSelector(sentence);
  73. const changedSent = useAppSelector(doneSent);
  74. const [prOpen, setPrOpen] = useState(false);
  75. const discussionMessage = useAppSelector(message);
  76. const anchorInfo = useAppSelector(anchor);
  77. const [copyOpen, setCopyOpen] = useState<boolean>(false);
  78. const sentId = `${sentData?.book}-${sentData?.para}-${sentData?.wordStart}-${sentData?.wordEnd}`;
  79. const sid = `${sentData?.book}_${sentData?.para}_${sentData?.wordStart}_${sentData?.wordEnd}_${sentData?.channel?.id}`;
  80. useEffect(() => {
  81. if (
  82. discussionMessage &&
  83. discussionMessage.resId &&
  84. discussionMessage.resId === initValue?.id
  85. ) {
  86. setBgColor("#1890ff33");
  87. } else {
  88. setBgColor(undefined);
  89. }
  90. }, [discussionMessage, initValue?.id]);
  91. useEffect(() => {
  92. if (anchorInfo && anchorInfo?.resId === initValue?.id) {
  93. const ele = document.getElementById(sid);
  94. if (ele !== null) {
  95. ele.scrollIntoView({
  96. behavior: "smooth",
  97. block: "center",
  98. inline: "nearest",
  99. });
  100. }
  101. }
  102. }, [anchorInfo, initValue?.id, sid]);
  103. useEffect(() => {
  104. if (value) {
  105. setSentData(value);
  106. }
  107. }, [value]);
  108. useEffect(() => {
  109. console.debug("sent cell acceptPr", acceptPr, uuid);
  110. if (isPr) {
  111. console.debug("sent cell is pr");
  112. return;
  113. }
  114. if (typeof acceptPr === "undefined" || acceptPr.length === 0) {
  115. console.debug("sent cell acceptPr is empty");
  116. return;
  117. }
  118. if (!sentData) {
  119. console.debug("sent cell sentData is empty");
  120. return;
  121. }
  122. if (changedSent?.includes(uuid)) {
  123. console.debug("sent cell already apply", uuid);
  124. return;
  125. }
  126. const found = acceptPr
  127. .filter((value) => typeof value !== "undefined")
  128. .find((value) => {
  129. const vId = `${value.book}_${value.para}_${value.wordStart}_${value.wordEnd}_${value.channel.id}`;
  130. return vId === sid;
  131. });
  132. if (typeof found !== "undefined") {
  133. console.debug("sent cell sentence apply", uuid, found, found);
  134. setSentData(found);
  135. store.dispatch(done(uuid));
  136. }
  137. }, [acceptPr, sentData, isPr, uuid, changedSent, sid]);
  138. const deletePr = (id: string) => {
  139. delete_<IDeleteResponse>(`/v2/sentpr/${id}`)
  140. .then((json) => {
  141. if (json.ok) {
  142. AntdMessage.success("删除成功");
  143. if (typeof onDelete !== "undefined") {
  144. onDelete();
  145. }
  146. } else {
  147. AntdMessage.error(json.message);
  148. }
  149. })
  150. .catch((e) => console.log("Oops errors!", e));
  151. };
  152. const refresh = () => {
  153. if (typeof sentData === "undefined") {
  154. return;
  155. }
  156. let url = `/v2/sentence?view=channel&sentence=${sentId}&html=true`;
  157. url += `&channel=${sentData.channel.id}`;
  158. console.debug("api request", url);
  159. setLoading(true);
  160. get<ISentenceListResponse>(url)
  161. .then((json) => {
  162. console.debug("api response", json);
  163. if (json.ok && json.data.count > 0) {
  164. const newData: ISentence[] = json.data.rows.map((item) => {
  165. return toISentence(item, [sentData.channel.id]);
  166. });
  167. setSentData(newData[0]);
  168. }
  169. })
  170. .finally(() => setLoading(false));
  171. };
  172. return (
  173. <div style={{ marginBottom: "8px", backgroundColor: bgColor }}>
  174. {loading ? <LoadingOutlined /> : <></>}
  175. {isPr ? undefined : (
  176. <div
  177. dangerouslySetInnerHTML={{
  178. __html: `<div class="tran_sent" id="${sid}" ></div>`,
  179. }}
  180. />
  181. )}
  182. <SentEditMenu
  183. isPr={isPr}
  184. data={sentData}
  185. onModeChange={(mode: string) => {
  186. if (mode === "edit") {
  187. setIsEditMode(true);
  188. }
  189. }}
  190. onMenuClick={(key: string) => {
  191. switch (key) {
  192. case "refresh":
  193. refresh();
  194. break;
  195. case "copy-to":
  196. setCopyOpen(true);
  197. break;
  198. case "suggestion":
  199. setPrOpen(true);
  200. break;
  201. case "paste":
  202. navigator.clipboard.readText().then((value: string) => {
  203. if (sentData && value !== "") {
  204. sentData.content = value;
  205. _sentSave(
  206. sentData,
  207. (res: ISentence) => {
  208. //setSentData(res);
  209. //发布句子的改变,让同样的句子更新
  210. store.dispatch(accept([res]));
  211. if (typeof onChange !== "undefined") {
  212. onChange(res);
  213. }
  214. },
  215. () => {}
  216. );
  217. }
  218. });
  219. break;
  220. case "delete":
  221. Modal.confirm({
  222. icon: <ExclamationCircleOutlined />,
  223. title: intl.formatMessage({
  224. id: "message.delete.confirm",
  225. }),
  226. content: "",
  227. okText: intl.formatMessage({
  228. id: "buttons.delete",
  229. }),
  230. okType: "danger",
  231. cancelText: intl.formatMessage({
  232. id: "buttons.no",
  233. }),
  234. onOk() {
  235. if (isPr && sentData && sentData.id) {
  236. deletePr(sentData.id);
  237. }
  238. },
  239. });
  240. break;
  241. default:
  242. break;
  243. }
  244. }}
  245. onConvert={async (format: string) => {
  246. switch (format) {
  247. case "json":
  248. const wbw: IWbw[] = sentData?.content
  249. ? sentData.content
  250. .split("\n")
  251. .filter((value) => value.trim().length > 0)
  252. .map((item, id) => {
  253. const parts = item.split("=");
  254. const word = my_to_roman(parts[0]);
  255. const meaning: string =
  256. parts.length > 1
  257. ? parts[1]
  258. .trim()
  259. .replaceAll("။", "")
  260. .replaceAll("(", " ( ")
  261. .replaceAll(")", " ) ")
  262. : "";
  263. const translation: string =
  264. parts.length > 2 ? parts[2].trim() : "";
  265. let parent: string = "";
  266. let factors: string = "";
  267. const factor1 = meaning
  268. .split(" ")
  269. .filter((value) => value !== "");
  270. factors = factor1
  271. .map((item) => {
  272. if (endings) {
  273. const base = nissayaBase(item, endings);
  274. if (factor1.length === 1) {
  275. parent = base.base;
  276. }
  277. const end = base.ending ? base.ending : [];
  278. return [base.base, ...end]
  279. .filter((value) => value !== "")
  280. .join("-");
  281. } else {
  282. return item;
  283. }
  284. })
  285. .join("+");
  286. return {
  287. uid: "0",
  288. book: sentData.book,
  289. para: sentData.para,
  290. sn: [id],
  291. word: { value: word ? word : parts[0], status: 0 },
  292. real: { value: meaning, status: 0 },
  293. meaning: { value: translation, status: 0 },
  294. parent: { value: parent, status: 0 },
  295. factors: {
  296. value: factors,
  297. status: 0,
  298. },
  299. confidence: 0.5,
  300. };
  301. })
  302. : [];
  303. if (wbw.length > 0) {
  304. const snowflake = await get<ISnowFlakeResponse>(
  305. `/v2/snowflake?count=${wbw.length}`
  306. );
  307. wbw.forEach((value: IWbw, index: number, array: IWbw[]) => {
  308. array[index].uid = snowflake.data.rows[index];
  309. });
  310. }
  311. if (sentData) {
  312. const newData = JSON.parse(JSON.stringify(sentData));
  313. newData.contentType = "json";
  314. newData.content = JSON.stringify(wbw);
  315. setSentData(newData);
  316. sentSave(newData, intl);
  317. }
  318. setIsEditMode(true);
  319. break;
  320. case "markdown":
  321. Modal.confirm({
  322. title: "格式转换",
  323. content:
  324. "转换为markdown格式后,拆分意思数据会丢失。确定要转换吗?",
  325. onOk() {
  326. if (sentData) {
  327. let newData = JSON.parse(JSON.stringify(sentData));
  328. const wbwData: IWbw[] = newData.content
  329. ? JSON.parse(newData.content)
  330. : [];
  331. const newContent = wbwData
  332. .filter((value) => value.sn.length === 1)
  333. .map((item) => {
  334. return [
  335. item.word.value,
  336. item.real.value,
  337. item.meaning?.value,
  338. ].join("=");
  339. })
  340. .join("\n");
  341. newData.content = newContent;
  342. newData["contentType"] = "markdown";
  343. sentSave(newData, intl);
  344. setSentData(newData);
  345. }
  346. setIsEditMode(true);
  347. },
  348. });
  349. break;
  350. }
  351. }}
  352. >
  353. {sentData ? (
  354. <div style={{ display: "flex" }}>
  355. <div style={{ marginRight: 8 }}>
  356. {isPr ? (
  357. <User {...sentData.editor} showName={false} />
  358. ) : (
  359. <StudioName
  360. data={sentData.studio}
  361. hideName
  362. popOver={
  363. compact ? (
  364. <Details data={sentData} isPr={isPr} />
  365. ) : undefined
  366. }
  367. />
  368. )}
  369. </div>
  370. <div
  371. style={{
  372. display: "flex",
  373. flexDirection: compact ? "row" : "column",
  374. alignItems: "flex-start",
  375. width: "100%",
  376. }}
  377. >
  378. {isEditMode ? (
  379. sentData?.contentType === "json" ? (
  380. <SentWbwEdit
  381. data={sentData}
  382. onClose={() => {
  383. setIsEditMode(false);
  384. }}
  385. onSave={(data: ISentence) => {
  386. console.debug("sent cell onSave", data);
  387. setSentData(data);
  388. }}
  389. />
  390. ) : (
  391. <SentCellEditable
  392. data={sentData}
  393. isPr={isPr}
  394. onClose={() => {
  395. setIsEditMode(false);
  396. }}
  397. onSave={(data: ISentence) => {
  398. console.debug("sent cell onSave", data);
  399. //setSentData(data);
  400. store.dispatch(accept([data]));
  401. setIsEditMode(false);
  402. if (typeof onChange !== "undefined") {
  403. onChange(data);
  404. }
  405. }}
  406. />
  407. )
  408. ) : showDiff ? (
  409. <TextDiff
  410. showToolTip={false}
  411. content={sentData.content}
  412. oldContent={diffText}
  413. />
  414. ) : (
  415. <MdView
  416. className="sentence"
  417. style={{
  418. width: "100%",
  419. marginBottom: 0,
  420. }}
  421. placeholder={intl.formatMessage({
  422. id: "labels.input",
  423. })}
  424. html={sentData.html ? sentData.html : sentData.content}
  425. wordWidget={wordWidget}
  426. />
  427. )}
  428. <div
  429. style={{
  430. display: "flex",
  431. justifyContent: "space-between",
  432. width: compact ? undefined : "100%",
  433. paddingRight: 20,
  434. flexWrap: "wrap",
  435. }}
  436. >
  437. <EditInfo data={sentData} isPr={isPr} compact={compact} />
  438. <SuggestionToolbar
  439. style={{
  440. marginBottom: 0,
  441. justifyContent: "flex-end",
  442. marginLeft: "auto",
  443. }}
  444. compact={compact}
  445. data={sentData}
  446. isPr={isPr}
  447. prOpen={prOpen}
  448. onPrClose={() => setPrOpen(false)}
  449. onDelete={() => {
  450. if (isPr && sentData.id) {
  451. deletePr(sentData.id);
  452. }
  453. }}
  454. />
  455. </div>
  456. </div>
  457. </div>
  458. ) : undefined}
  459. </SentEditMenu>
  460. <CopyToModal
  461. important
  462. sentencesId={[sentId]}
  463. channel={sentData?.channel}
  464. open={copyOpen}
  465. onClose={() => setCopyOpen(false)}
  466. />
  467. <Collapse
  468. bordered={false}
  469. style={{ display: "none", backgroundColor: "unset" }}
  470. >
  471. <Collapse.Panel
  472. header={"attachment"}
  473. key="parent2"
  474. style={{ backgroundColor: "unset" }}
  475. >
  476. <SentAttachment sentenceId={sentData?.id} />
  477. </Collapse.Panel>
  478. </Collapse>
  479. </div>
  480. );
  481. };
  482. export default SentCellWidget;