SentCell.tsx 12 KB

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