SentCell.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { useEffect, useState } from "react";
  2. import { useIntl } from "react-intl";
  3. import { Divider, message as AntdMessage, Modal } from "antd";
  4. import { ExclamationCircleOutlined } 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_ } from "../../../request";
  23. import "./style.css";
  24. import StudioName from "../../auth/StudioName";
  25. import CopyToModal from "../../channel/CopyToModal";
  26. import store from "../../../store";
  27. import { randomString } from "../../../utils";
  28. interface IWidget {
  29. initValue?: ISentence;
  30. value?: ISentence;
  31. wordWidget?: boolean;
  32. isPr?: boolean;
  33. editMode?: boolean;
  34. compact?: boolean;
  35. showDiff?: boolean;
  36. diffText?: string | null;
  37. onChange?: Function;
  38. onDelete?: Function;
  39. }
  40. const SentCellWidget = ({
  41. initValue,
  42. value,
  43. wordWidget = false,
  44. isPr = false,
  45. editMode = false,
  46. compact = false,
  47. showDiff = false,
  48. diffText,
  49. onChange,
  50. onDelete,
  51. }: IWidget) => {
  52. const intl = useIntl();
  53. const [isEditMode, setIsEditMode] = useState(editMode);
  54. const [sentData, setSentData] = useState<ISentence | undefined>(initValue);
  55. const [bgColor, setBgColor] = useState<string>();
  56. const [uuid] = useState(randomString());
  57. const endings = useAppSelector(getEnding);
  58. const acceptPr = useAppSelector(sentence);
  59. const changedSent = useAppSelector(doneSent);
  60. const [prOpen, setPrOpen] = useState(false);
  61. const discussionMessage = useAppSelector(message);
  62. const anchorInfo = useAppSelector(anchor);
  63. const [copyOpen, setCopyOpen] = useState<boolean>(false);
  64. const sentId = `${sentData?.book}-${sentData?.para}-${sentData?.wordStart}-${sentData?.wordEnd}`;
  65. const sid = `${sentData?.book}_${sentData?.para}_${sentData?.wordStart}_${sentData?.wordEnd}_${sentData?.channel.id}`;
  66. useEffect(() => {
  67. if (
  68. discussionMessage &&
  69. discussionMessage.resId &&
  70. discussionMessage.resId === initValue?.id
  71. ) {
  72. setBgColor("#1890ff33");
  73. } else {
  74. setBgColor(undefined);
  75. }
  76. }, [discussionMessage, initValue?.id]);
  77. useEffect(() => {
  78. if (anchorInfo && anchorInfo?.resId === initValue?.id) {
  79. const ele = document.getElementById(sid);
  80. if (ele !== null) {
  81. ele.scrollIntoView({
  82. behavior: "smooth",
  83. block: "center",
  84. inline: "nearest",
  85. });
  86. }
  87. }
  88. }, [anchorInfo, initValue?.id, sid]);
  89. useEffect(() => {
  90. if (value) {
  91. setSentData(value);
  92. }
  93. }, [value]);
  94. useEffect(() => {
  95. console.debug("sent cell acceptPr", acceptPr, sentData);
  96. if (isPr) {
  97. console.debug("sent cell is pr");
  98. return;
  99. }
  100. if (typeof acceptPr === "undefined" || acceptPr.length === 0) {
  101. console.debug("sent cell acceptPr is empty");
  102. return;
  103. }
  104. if (!sentData) {
  105. console.debug("sent cell sentData is empty");
  106. return;
  107. }
  108. if (changedSent?.includes(uuid)) {
  109. console.debug("sent cell already apply", sid);
  110. return;
  111. }
  112. const found = acceptPr.findIndex((value) => {
  113. const vId = `${value.book}_${value.para}_${value.wordStart}_${value.wordEnd}_${value.channel.id}`;
  114. return vId === sid;
  115. });
  116. if (found !== -1) {
  117. console.debug("sent cell sentence changed", found, acceptPr[found]);
  118. setSentData(acceptPr[found]);
  119. store.dispatch(done(uuid));
  120. }
  121. }, [acceptPr, sentData, isPr, uuid, changedSent, sid]);
  122. const deletePr = (id: string) => {
  123. delete_<IDeleteResponse>(`/v2/sentpr/${id}`)
  124. .then((json) => {
  125. if (json.ok) {
  126. AntdMessage.success("删除成功");
  127. if (typeof onDelete !== "undefined") {
  128. onDelete();
  129. }
  130. } else {
  131. AntdMessage.error(json.message);
  132. }
  133. })
  134. .catch((e) => console.log("Oops errors!", e));
  135. };
  136. return (
  137. <div style={{ marginBottom: "8px", backgroundColor: bgColor }}>
  138. {isPr ? undefined : (
  139. <div
  140. dangerouslySetInnerHTML={{
  141. __html: `<div class="tran_sent" id="${sid}" ></div>`,
  142. }}
  143. />
  144. )}
  145. <SentEditMenu
  146. isPr={isPr}
  147. data={sentData}
  148. onModeChange={(mode: string) => {
  149. if (mode === "edit") {
  150. setIsEditMode(true);
  151. }
  152. }}
  153. onMenuClick={(key: string) => {
  154. switch (key) {
  155. case "copy-to":
  156. setCopyOpen(true);
  157. break;
  158. case "suggestion":
  159. setPrOpen(true);
  160. break;
  161. case "paste":
  162. navigator.clipboard.readText().then((value: string) => {
  163. if (sentData && value !== "") {
  164. sentData.content = value;
  165. _sentSave(
  166. sentData,
  167. (res: ISentence) => {
  168. //setSentData(res);
  169. //发布句子的改变,让同样的句子更新
  170. store.dispatch(accept([res]));
  171. if (typeof onChange !== "undefined") {
  172. onChange(res);
  173. }
  174. },
  175. () => {}
  176. );
  177. }
  178. });
  179. break;
  180. case "delete":
  181. Modal.confirm({
  182. icon: <ExclamationCircleOutlined />,
  183. title: intl.formatMessage({
  184. id: "message.delete.confirm",
  185. }),
  186. content: "",
  187. okText: intl.formatMessage({
  188. id: "buttons.delete",
  189. }),
  190. okType: "danger",
  191. cancelText: intl.formatMessage({
  192. id: "buttons.no",
  193. }),
  194. onOk() {
  195. if (isPr && sentData && sentData.id) {
  196. deletePr(sentData.id);
  197. }
  198. },
  199. });
  200. break;
  201. default:
  202. break;
  203. }
  204. }}
  205. onConvert={(format: string) => {
  206. switch (format) {
  207. case "json":
  208. const wbw: IWbw[] = sentData?.content
  209. ? sentData.content.split("\n").map((item, id) => {
  210. const parts = item.split("=");
  211. const word = my_to_roman(parts[0]);
  212. const meaning: string =
  213. parts.length > 1 ? parts[1].trim() : "";
  214. let parent: string = "";
  215. let factors: string = "";
  216. if (!meaning.includes(" ") && endings) {
  217. const base = nissayaBase(meaning, endings);
  218. parent = base.base;
  219. const end = base.ending ? base.ending : [];
  220. factors = [base.base, ...end].join("+");
  221. } else {
  222. factors = meaning.replaceAll(" ", "+");
  223. }
  224. return {
  225. book: sentData.book,
  226. para: sentData.para,
  227. sn: [id],
  228. word: { value: word ? word : parts[0], status: 0 },
  229. real: { value: meaning, status: 0 },
  230. meaning: { value: "", status: 0 },
  231. parent: { value: parent, status: 0 },
  232. factors: {
  233. value: factors,
  234. status: 0,
  235. },
  236. confidence: 0.5,
  237. };
  238. })
  239. : [];
  240. setSentData((origin) => {
  241. if (origin) {
  242. origin.contentType = "json";
  243. origin.content = JSON.stringify(wbw);
  244. sentSave(origin, intl);
  245. return origin;
  246. }
  247. });
  248. setIsEditMode(true);
  249. break;
  250. case "markdown":
  251. setSentData((origin) => {
  252. if (origin) {
  253. const wbwData: IWbw[] = origin.content
  254. ? JSON.parse(origin.content)
  255. : [];
  256. const newContent = wbwData
  257. .map((item) => {
  258. return [
  259. item.word.value,
  260. item.real.value,
  261. item.meaning?.value,
  262. ].join("=");
  263. })
  264. .join("\n");
  265. origin.content = newContent;
  266. origin.contentType = "markdown";
  267. sentSave(origin, intl);
  268. return origin;
  269. }
  270. });
  271. setIsEditMode(true);
  272. break;
  273. }
  274. }}
  275. >
  276. {sentData ? (
  277. <div style={{ display: "flex" }}>
  278. <div style={{ marginRight: 8 }}>
  279. <StudioName
  280. data={sentData.studio}
  281. showName={false}
  282. popOver={
  283. compact ? <Details data={sentData} isPr={isPr} /> : undefined
  284. }
  285. />
  286. </div>
  287. <div
  288. style={{
  289. display: "flex",
  290. flexDirection: compact ? "row" : "column",
  291. alignItems: "flex-start",
  292. width: "100%",
  293. }}
  294. >
  295. {isEditMode ? (
  296. sentData?.contentType === "json" ? (
  297. <SentWbwEdit
  298. data={sentData}
  299. onClose={() => {
  300. setIsEditMode(false);
  301. }}
  302. onSave={(data: ISentence) => {
  303. console.debug("sent cell onSave", data);
  304. setSentData(data);
  305. }}
  306. />
  307. ) : (
  308. <SentCellEditable
  309. data={sentData}
  310. isPr={isPr}
  311. onClose={() => {
  312. setIsEditMode(false);
  313. }}
  314. onSave={(data: ISentence) => {
  315. console.debug("sent cell onSave", data);
  316. //setSentData(data);
  317. store.dispatch(accept([data]));
  318. setIsEditMode(false);
  319. if (typeof onChange !== "undefined") {
  320. onChange(data);
  321. }
  322. }}
  323. />
  324. )
  325. ) : showDiff ? (
  326. <TextDiff
  327. showToolTip={false}
  328. content={sentData.content}
  329. oldContent={diffText}
  330. />
  331. ) : (
  332. <MdView
  333. className="sentence"
  334. style={{
  335. width: "100%",
  336. marginBottom: 0,
  337. }}
  338. placeholder={intl.formatMessage({
  339. id: "labels.input",
  340. })}
  341. html={sentData.html ? sentData.html : sentData.content}
  342. wordWidget={wordWidget}
  343. />
  344. )}
  345. <div
  346. style={{
  347. display: "flex",
  348. justifyContent: "space-between",
  349. width: compact ? undefined : "100%",
  350. paddingRight: 20,
  351. flexWrap: "wrap",
  352. }}
  353. >
  354. <EditInfo data={sentData} compact={compact} />
  355. <SuggestionToolbar
  356. style={{
  357. marginBottom: 0,
  358. justifyContent: "flex-end",
  359. marginLeft: "auto",
  360. }}
  361. compact={compact}
  362. data={sentData}
  363. isPr={isPr}
  364. prOpen={prOpen}
  365. onPrClose={() => setPrOpen(false)}
  366. onDelete={() => {
  367. if (isPr && sentData.id) {
  368. deletePr(sentData.id);
  369. }
  370. }}
  371. />
  372. </div>
  373. </div>
  374. </div>
  375. ) : undefined}
  376. </SentEditMenu>
  377. {compact ? undefined : <Divider style={{ margin: "10px 0" }} />}
  378. <CopyToModal
  379. important
  380. sentencesId={[sentId]}
  381. channel={sentData?.channel}
  382. open={copyOpen}
  383. onClose={() => setCopyOpen(false)}
  384. />
  385. </div>
  386. );
  387. };
  388. export default SentCellWidget;