Article.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import { useEffect, useState } from "react";
  2. import { Divider, message, Result, Space, Tag } from "antd";
  3. import { get, post } from "../../request";
  4. import store from "../../store";
  5. import { IArticleDataResponse, IArticleResponse } from "../api/Article";
  6. import ArticleView, { IFirstAnthology } from "./ArticleView";
  7. import { ICourseCurrUserResponse } from "../api/Course";
  8. import { ICourseUser, signIn } from "../../reducers/course-user";
  9. import { ITextbook, refresh } from "../../reducers/current-course";
  10. import ExerciseList from "./ExerciseList";
  11. import ExerciseAnswer from "../course/ExerciseAnswer";
  12. import "./article.css";
  13. import TocTree from "./TocTree";
  14. import PaliText from "../template/Wbw/PaliText";
  15. import ArticleSkeleton from "./ArticleSkeleton";
  16. import { modeChange } from "../../reducers/article-mode";
  17. import { IViewRequest, IViewStoreResponse } from "../api/view";
  18. import {
  19. IRecentRequest,
  20. IRecentResponse,
  21. } from "../../pages/studio/recent/list";
  22. import { ITocPathNode } from "../corpus/TocPath";
  23. export type ArticleMode = "read" | "edit" | "wbw";
  24. export type ArticleType =
  25. | "article"
  26. | "chapter"
  27. | "para"
  28. | "cs-para"
  29. | "sent"
  30. | "sim"
  31. | "page"
  32. | "textbook"
  33. | "exercise"
  34. | "exercise-list"
  35. | "sent-original"
  36. | "sent-commentary"
  37. | "sent-nissaya"
  38. | "sent-translation"
  39. | "term";
  40. /**
  41. * 每种article type 对应的路由参数
  42. * article/id?anthology=id&channel=id1,id2&mode=ArticleMode
  43. * chapter/book-para?channel=id1,id2&mode=ArticleMode
  44. * para/book?par=para1,para2&channel=id1,id2&mode=ArticleMode
  45. * cs-para/book-para?channel=id1,id2&mode=ArticleMode
  46. * sent/id?channel=id1,id2&mode=ArticleMode
  47. * sim/id?channel=id1,id2&mode=ArticleMode
  48. * textbook/articleId?course=id&mode=ArticleMode
  49. * exercise/articleId?course=id&exercise=id&username=name&mode=ArticleMode
  50. * exercise-list/articleId?course=id&exercise=id&mode=ArticleMode
  51. * sent-original/id
  52. */
  53. interface IWidget {
  54. type?: ArticleType;
  55. articleId?: string;
  56. book?: string | null;
  57. para?: string | null;
  58. channelId?: string | null;
  59. anthologyId?: string | null;
  60. courseId?: string;
  61. exerciseId?: string;
  62. userName?: string;
  63. mode?: ArticleMode | null;
  64. active?: boolean;
  65. onArticleChange?: Function;
  66. onFinal?: Function;
  67. onLoad?: Function;
  68. onAnthologySelect?: Function;
  69. }
  70. const ArticleWidget = ({
  71. type,
  72. book,
  73. para,
  74. channelId,
  75. articleId,
  76. anthologyId,
  77. courseId,
  78. exerciseId,
  79. userName,
  80. mode = "read",
  81. active = false,
  82. onArticleChange,
  83. onFinal,
  84. onLoad,
  85. onAnthologySelect,
  86. }: IWidget) => {
  87. const [articleData, setArticleData] = useState<IArticleDataResponse>();
  88. const [articleHtml, setArticleHtml] = useState<string[]>(["<span />"]);
  89. const [extra, setExtra] = useState(<></>);
  90. const [showSkeleton, setShowSkeleton] = useState(true);
  91. const [unauthorized, setUnauthorized] = useState(false);
  92. const [remains, setRemains] = useState(false);
  93. const channels = channelId?.split("_");
  94. useEffect(() => {
  95. /**
  96. * 由课本进入查询当前用户的权限和channel
  97. */
  98. if (
  99. type === "textbook" ||
  100. type === "exercise" ||
  101. type === "exercise-list"
  102. ) {
  103. if (typeof articleId !== "undefined") {
  104. const id = articleId.split("_");
  105. get<ICourseCurrUserResponse>(`/v2/course-curr?course_id=${id[0]}`).then(
  106. (response) => {
  107. console.log("course user", response);
  108. if (response.ok) {
  109. const it: ICourseUser = {
  110. channelId: response.data.channel_id,
  111. role: response.data.role,
  112. };
  113. store.dispatch(signIn(it));
  114. /**
  115. * redux发布课程信息
  116. */
  117. const ic: ITextbook = {
  118. courseId: id[0],
  119. articleId: id[1],
  120. };
  121. store.dispatch(refresh(ic));
  122. }
  123. }
  124. );
  125. }
  126. }
  127. }, [articleId, type]);
  128. useEffect(() => {
  129. //发布mode变更
  130. console.log("发布mode变更", mode);
  131. store.dispatch(modeChange(mode as ArticleMode));
  132. }, [mode]);
  133. const srcDataMode = mode === "edit" || mode === "wbw" ? "edit" : "read";
  134. useEffect(() => {
  135. console.log("srcDataMode", srcDataMode);
  136. if (!active) {
  137. return;
  138. }
  139. if (typeof type !== "undefined") {
  140. let url = "";
  141. switch (type) {
  142. case "chapter":
  143. if (typeof articleId !== "undefined") {
  144. url = `/v2/corpus-chapter/${articleId}?mode=${srcDataMode}`;
  145. url += channelId ? `&channels=${channelId}` : "";
  146. }
  147. break;
  148. case "para":
  149. const _book = book ? book : articleId;
  150. url = `/v2/corpus?view=para&book=${_book}&par=${para}&mode=${srcDataMode}`;
  151. url += channelId ? `&channels=${channelId}` : "";
  152. break;
  153. case "article":
  154. if (typeof articleId !== "undefined") {
  155. url = `/v2/article/${articleId}?mode=${srcDataMode}`;
  156. url += channelId ? `&channel=${channelId}` : "";
  157. url += anthologyId ? `&anthology=${anthologyId}` : "";
  158. }
  159. break;
  160. case "textbook":
  161. if (typeof articleId !== "undefined") {
  162. url = `/v2/article/${articleId}?view=textbook&course=${courseId}&mode=${srcDataMode}`;
  163. }
  164. break;
  165. case "exercise":
  166. if (typeof articleId !== "undefined") {
  167. url = `/v2/article/${articleId}?mode=${srcDataMode}&course=${courseId}&exercise=${exerciseId}&user=${userName}`;
  168. setExtra(
  169. <ExerciseAnswer
  170. courseId={courseId}
  171. articleId={articleId}
  172. exerciseId={exerciseId}
  173. />
  174. );
  175. }
  176. break;
  177. case "exercise-list":
  178. if (typeof articleId !== "undefined") {
  179. url = `/v2/article/${articleId}?mode=${srcDataMode}&course=${courseId}&exercise=${exerciseId}`;
  180. setExtra(
  181. <ExerciseList
  182. courseId={courseId}
  183. articleId={articleId}
  184. exerciseId={exerciseId}
  185. />
  186. );
  187. }
  188. break;
  189. default:
  190. if (typeof articleId !== "undefined") {
  191. url = `/v2/corpus/${type}/${articleId}/${srcDataMode}?mode=${srcDataMode}`;
  192. url += channelId ? `&channel=${channelId}` : "";
  193. }
  194. break;
  195. }
  196. console.log("article url", url);
  197. setShowSkeleton(true);
  198. if (typeof articleId !== "undefined") {
  199. const param = {
  200. mode: srcDataMode,
  201. channel: channelId !== null ? channelId : undefined,
  202. book: book !== null ? book : undefined,
  203. para: para !== null ? para : undefined,
  204. };
  205. post<IRecentRequest, IRecentResponse>("/v2/recent", {
  206. type: type,
  207. article_id: articleId,
  208. param: JSON.stringify(param),
  209. }).then((json) => {
  210. console.log("recent", json);
  211. });
  212. }
  213. get<IArticleResponse>(url)
  214. .then((json) => {
  215. console.log("article", json);
  216. if (json.ok) {
  217. setArticleData(json.data);
  218. if (json.data.html) {
  219. setArticleHtml([json.data.html]);
  220. } else if (json.data.content) {
  221. setArticleHtml([json.data.content]);
  222. }
  223. if (json.data.from) {
  224. setRemains(true);
  225. }
  226. setShowSkeleton(false);
  227. setExtra(
  228. <TocTree
  229. treeData={json.data.toc?.map((item) => {
  230. const strTitle = item.title ? item.title : item.pali_title;
  231. const key = item.key
  232. ? item.key
  233. : `${item.book}-${item.paragraph}`;
  234. const progress = item.progress?.map((item, id) => (
  235. <Tag key={id}>{Math.round(item * 100) + "%"}</Tag>
  236. ));
  237. return {
  238. key: key,
  239. title: (
  240. <Space>
  241. <PaliText
  242. text={strTitle === "" ? "[unnamed]" : strTitle}
  243. />
  244. {progress}
  245. </Space>
  246. ),
  247. level: item.level,
  248. };
  249. })}
  250. onSelect={(keys: string[]) => {
  251. console.log(keys);
  252. if (
  253. typeof onArticleChange !== "undefined" &&
  254. keys.length > 0
  255. ) {
  256. onArticleChange(keys[0]);
  257. }
  258. }}
  259. />
  260. );
  261. switch (type) {
  262. case "chapter":
  263. if (typeof articleId === "string" && channelId) {
  264. const [book, para] = articleId?.split("-");
  265. post<IViewRequest, IViewStoreResponse>("/v2/view", {
  266. target_type: type,
  267. book: parseInt(book),
  268. para: parseInt(para),
  269. channel: channelId,
  270. mode: srcDataMode,
  271. }).then((json) => {
  272. console.log("view", json.data);
  273. });
  274. }
  275. break;
  276. default:
  277. break;
  278. }
  279. if (typeof onLoad !== "undefined") {
  280. onLoad(json.data);
  281. }
  282. } else {
  283. setShowSkeleton(false);
  284. setUnauthorized(true);
  285. message.error(json.message);
  286. }
  287. })
  288. .catch((e) => {
  289. console.error(e);
  290. });
  291. }
  292. }, [
  293. active,
  294. type,
  295. articleId,
  296. srcDataMode,
  297. book,
  298. para,
  299. channelId,
  300. anthologyId,
  301. courseId,
  302. exerciseId,
  303. userName,
  304. ]);
  305. const getNextPara = (next: IArticleDataResponse): void => {
  306. if (
  307. typeof next.paraId === "undefined" ||
  308. typeof next.mode === "undefined" ||
  309. typeof next.from === "undefined" ||
  310. typeof next.to === "undefined"
  311. ) {
  312. setRemains(false);
  313. return;
  314. }
  315. let url = `/v2/corpus-chapter/${next.paraId}?mode=${next.mode}`;
  316. url += `&from=${next.from}`;
  317. url += `&to=${next.to}`;
  318. url += channels ? `&channels=${channels}` : "";
  319. console.log("lazy load", url);
  320. get<IArticleResponse>(url).then((json) => {
  321. if (json.ok) {
  322. if (typeof json.data.content === "string") {
  323. const content: string = json.data.content;
  324. setArticleData((origin) => {
  325. if (origin) {
  326. origin.from = json.data.from;
  327. }
  328. return origin;
  329. });
  330. setArticleHtml((origin) => {
  331. return [...origin, content];
  332. });
  333. }
  334. //getNextPara(json.data);
  335. }
  336. });
  337. return;
  338. };
  339. //const comment = <CommentListCard resId={articleData?.uid} resType="article" />
  340. let anthology: IFirstAnthology | undefined;
  341. if (articleData?.anthology_count && articleData.anthology_first) {
  342. anthology = {
  343. id: articleData.anthology_first.uid,
  344. title: articleData.anthology_first.title,
  345. count: articleData?.anthology_count,
  346. };
  347. }
  348. return (
  349. <div>
  350. {showSkeleton ? (
  351. <ArticleSkeleton />
  352. ) : unauthorized ? (
  353. <Result
  354. status="403"
  355. title="无权访问"
  356. subTitle="您无权访问该内容。您可能没有登录,或者内容的所有者没有给您所需的权限。"
  357. extra={<></>}
  358. />
  359. ) : (
  360. <ArticleView
  361. id={articleData?.uid}
  362. title={articleData?.title}
  363. subTitle={articleData?.subtitle}
  364. summary={articleData?.summary}
  365. content={articleData ? articleData.content : ""}
  366. html={articleHtml}
  367. path={articleData?.path}
  368. created_at={articleData?.created_at}
  369. updated_at={articleData?.updated_at}
  370. channels={channels}
  371. type={type}
  372. articleId={articleId}
  373. remains={remains}
  374. anthology={anthology}
  375. onEnd={() => {
  376. if (type === "chapter" && articleData) {
  377. getNextPara(articleData);
  378. }
  379. }}
  380. onPathChange={(
  381. node: ITocPathNode,
  382. e: React.MouseEvent<HTMLSpanElement | HTMLAnchorElement, MouseEvent>
  383. ) => {
  384. if (typeof onArticleChange !== "undefined") {
  385. const newArticle = node.key
  386. ? node.key
  387. : `${node.book}-${node.paragraph}`;
  388. const target = e.ctrlKey || e.metaKey ? "_blank" : "self";
  389. onArticleChange(newArticle, target);
  390. }
  391. }}
  392. onAnthologySelect={(id: string) => {
  393. if (typeof onAnthologySelect !== "undefined") {
  394. onAnthologySelect(id);
  395. }
  396. }}
  397. />
  398. )}
  399. <Divider />
  400. {extra}
  401. <Divider />
  402. </div>
  403. );
  404. };
  405. export default ArticleWidget;