SentRead.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { useEffect, useMemo, useState, useCallback } from "react";
  2. import { Button, Dropdown, type MenuProps, Typography } from "antd";
  3. import { LoadingOutlined, CloseOutlined } from "@ant-design/icons";
  4. import { useAppSelector } from "../../hooks";
  5. import { settingInfo } from "../../reducers/setting";
  6. import { GetUserSetting } from "../auth/setting/default";
  7. import type { TCodeConvertor } from "./utilities";
  8. import {
  9. type ISentence,
  10. type IWidgetSentEditInner,
  11. SentEditInner,
  12. } from "./SentEdit";
  13. import MdView from "./MdView";
  14. import store from "../../store";
  15. import { push } from "../../reducers/sentence";
  16. import "./style.css";
  17. import InteractiveButton from "./SentEdit/InteractiveButton";
  18. import { prOpen } from "./SentEdit/SuggestionButton";
  19. import { openDiscussion } from "../discussion/DiscussionButton";
  20. import type { IEditableSentence } from "../../api/Corpus";
  21. import { get } from "../../request";
  22. const { Text } = Typography;
  23. const items: MenuProps["items"] = [
  24. { label: "编辑", key: "edit" },
  25. { label: "讨论", key: "discussion" },
  26. { label: "修改建议", key: "pr" },
  27. { label: "标签", key: "tag" },
  28. ];
  29. interface IWidgetSentReadFrame {
  30. origin?: ISentence[];
  31. translation?: ISentence[];
  32. layout?: "row" | "column";
  33. book?: number;
  34. para?: number;
  35. wordStart?: number;
  36. wordEnd?: number;
  37. sentId?: string;
  38. error?: string;
  39. }
  40. const SentReadFrame = ({
  41. origin,
  42. translation,
  43. book,
  44. para,
  45. wordStart,
  46. wordEnd,
  47. error,
  48. }: IWidgetSentReadFrame) => {
  49. const settings = useAppSelector(settingInfo);
  50. const [loadingId, setLoadingId] = useState<string | null>(null);
  51. const [active, setActive] = useState(false);
  52. const [sentData, setSentData] = useState<IWidgetSentEditInner>();
  53. const [showEdit, setShowEdit] = useState(false);
  54. /** 派生数据:主巴利编码 */
  55. const paliCode = useMemo(() => {
  56. const v = GetUserSetting("setting.pali.script.primary", settings);
  57. return (v ?? "roman") as TCodeConvertor;
  58. }, [settings]);
  59. /** 派生数据:是否显示原文 */
  60. const displayOriginal = useMemo(() => {
  61. return GetUserSetting("setting.display.original", settings);
  62. }, [settings]);
  63. /** 派生数据:布局方向 */
  64. const layoutDirection = useMemo<React.CSSProperties["flexDirection"]>(() => {
  65. const v = GetUserSetting("setting.layout.direction", settings);
  66. if (
  67. v === "row" ||
  68. v === "column" ||
  69. v === "row-reverse" ||
  70. v === "column-reverse"
  71. ) {
  72. return v;
  73. }
  74. return "row";
  75. }, [settings]);
  76. /** push 到 store(副作用) */
  77. useEffect(() => {
  78. store.dispatch(
  79. push({
  80. id: `${book}-${para}-${wordStart}-${wordEnd}`,
  81. origin: origin?.map((item) => item.html),
  82. translation: translation?.map((item) => item.html),
  83. })
  84. );
  85. }, [book, origin, para, translation, wordEnd, wordStart]);
  86. /** 菜单点击 */
  87. const handleMenuClick = useCallback(async (key: string, item: ISentence) => {
  88. switch (key) {
  89. case "edit":
  90. if (!item.id) return;
  91. setLoadingId(item.id);
  92. try {
  93. const json = await get<IEditableSentence>(
  94. `/v2/editable-sentence/${item.id}`
  95. );
  96. if (json.ok) {
  97. setSentData(json.data);
  98. setShowEdit(true);
  99. }
  100. } finally {
  101. setLoadingId(null);
  102. }
  103. break;
  104. case "discussion":
  105. if (item.id) {
  106. openDiscussion(item.id, "sentence", false);
  107. }
  108. break;
  109. case "pr":
  110. prOpen(item);
  111. break;
  112. }
  113. }, []);
  114. return (
  115. <span
  116. className="sent_read_shell"
  117. style={{ flexDirection: layoutDirection }}
  118. >
  119. <Text type="danger" mark>
  120. {error}
  121. </Text>
  122. {/* anchor */}
  123. <span
  124. dangerouslySetInnerHTML={{
  125. __html: `<span class="pcd_sent" id="sent_${book}-${para}-${wordStart}-${wordEnd}"></span>`,
  126. }}
  127. />
  128. {/* 原文 */}
  129. <span
  130. style={{
  131. flex: 5,
  132. color: "#9f3a01",
  133. display:
  134. displayOriginal === false && translation?.length ? "none" : "block",
  135. }}
  136. >
  137. {origin?.map((item, id) => (
  138. <Text key={id}>
  139. <MdView
  140. style={{ color: "brown" }}
  141. html={item.html}
  142. wordWidget
  143. convertor={paliCode}
  144. />
  145. </Text>
  146. ))}
  147. </span>
  148. {/* 译文 */}
  149. <span className="sent_read" style={{ flex: 5 }}>
  150. {translation?.map((item, id) => (
  151. <span key={id}>
  152. {loadingId === item.id && <LoadingOutlined />}
  153. <Dropdown
  154. trigger={["contextMenu"]}
  155. menu={{
  156. items,
  157. onClick: (e) => handleMenuClick(e.key, item),
  158. }}
  159. >
  160. <Text
  161. className="sent_read_translation"
  162. style={{ display: showEdit ? "none" : "inline" }}
  163. >
  164. <MdView
  165. html={item.html}
  166. style={{ backgroundColor: active ? "beige" : undefined }}
  167. />
  168. </Text>
  169. </Dropdown>
  170. {/* 编辑面板 */}
  171. {showEdit && (
  172. <div>
  173. <div style={{ textAlign: "right" }}>
  174. <Button
  175. size="small"
  176. icon={<CloseOutlined />}
  177. onClick={() => setShowEdit(false)}
  178. >
  179. 返回审阅模式
  180. </Button>
  181. </div>
  182. {sentData ? (
  183. <SentEditInner
  184. mode="edit"
  185. {...sentData}
  186. onTranslationChange={(data: ISentence) => {
  187. if (!translation) return;
  188. const copy = [...translation];
  189. copy[id] = data;
  190. }}
  191. />
  192. ) : (
  193. "无数据"
  194. )}
  195. </div>
  196. )}
  197. <InteractiveButton
  198. data={item}
  199. compact
  200. float
  201. hideCount
  202. hideInZero
  203. onMouseEnter={() => setActive(true)}
  204. onMouseLeave={() => setActive(false)}
  205. />
  206. </span>
  207. ))}
  208. </span>
  209. </span>
  210. );
  211. };
  212. interface IWidget {
  213. props: string;
  214. }
  215. const Widget = ({ props }: IWidget) => {
  216. const prop = JSON.parse(atob(props)) as IWidgetSentReadFrame;
  217. return <SentReadFrame {...prop} />;
  218. };
  219. export default Widget;