SentCell.tsx 17 KB

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