Article.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import { useEffect, useState } from "react";
  2. import { Divider, message, Result, Tag } from "antd";
  3. import { get, post } from "../../request";
  4. import store from "../../store";
  5. import { IArticleDataResponse, IArticleResponse } from "../api/Article";
  6. import ArticleView 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 CommentListCard from "../comment/CommentListCard";
  14. import TocTree from "./TocTree";
  15. import PaliText from "../template/Wbw/PaliText";
  16. import ArticleSkeleton from "./ArticleSkeleton";
  17. import { modeChange } from "../../reducers/article-mode";
  18. import { IViewRequest, IViewStoreResponse } from "../api/view";
  19. import {
  20. IRecentRequest,
  21. IRecentResponse,
  22. } from "../../pages/studio/recent/list";
  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;
  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. }
  69. const ArticleWidget = ({
  70. type,
  71. book,
  72. para,
  73. channelId,
  74. articleId,
  75. anthologyId,
  76. courseId,
  77. exerciseId,
  78. userName,
  79. mode = "read",
  80. active = false,
  81. onArticleChange,
  82. onFinal,
  83. onLoad,
  84. }: IWidget) => {
  85. const [articleData, setArticleData] = useState<IArticleDataResponse>();
  86. const [articleHtml, setArticleHtml] = useState<string[]>(["<span />"]);
  87. const [extra, setExtra] = useState(<></>);
  88. const [showSkeleton, setShowSkeleton] = useState(true);
  89. const [unauthorized, setUnauthorized] = useState(false);
  90. const [remains, setRemains] = useState(false);
  91. const channels = channelId?.split("_");
  92. useEffect(() => {
  93. /**
  94. * 由课本进入查询当前用户的权限和channel
  95. */
  96. if (
  97. type === "textbook" ||
  98. type === "exercise" ||
  99. type === "exercise-list"
  100. ) {
  101. if (typeof articleId !== "undefined") {
  102. const id = articleId.split("_");
  103. get<ICourseCurrUserResponse>(`/v2/course-curr?course_id=${id[0]}`).then(
  104. (response) => {
  105. console.log("course user", response);
  106. if (response.ok) {
  107. const it: ICourseUser = {
  108. channelId: response.data.channel_id,
  109. role: response.data.role,
  110. };
  111. store.dispatch(signIn(it));
  112. /**
  113. * redux发布课程信息
  114. */
  115. const ic: ITextbook = {
  116. courseId: id[0],
  117. articleId: id[1],
  118. };
  119. store.dispatch(refresh(ic));
  120. }
  121. }
  122. );
  123. }
  124. }
  125. }, [articleId, type]);
  126. useEffect(() => {
  127. //发布mode变更
  128. console.log("发布mode变更", mode);
  129. store.dispatch(modeChange(mode as ArticleMode));
  130. }, [mode]);
  131. const srcDataMode = mode === "edit" || mode === "wbw" ? "edit" : "read";
  132. useEffect(() => {
  133. console.log("srcDataMode", srcDataMode);
  134. if (!active) {
  135. return;
  136. }
  137. if (typeof type !== "undefined") {
  138. let url = "";
  139. switch (type) {
  140. case "chapter":
  141. if (typeof articleId !== "undefined") {
  142. url = `/v2/corpus-chapter/${articleId}?mode=${srcDataMode}`;
  143. url += channelId ? `&channels=${channelId}` : "";
  144. }
  145. break;
  146. case "para":
  147. const _book = book ? book : articleId;
  148. url = `/v2/corpus?view=para&book=${_book}&par=${para}&mode=${srcDataMode}`;
  149. url += channelId ? `&channels=${channelId}` : "";
  150. break;
  151. case "article":
  152. if (typeof articleId !== "undefined") {
  153. url = `/v2/article/${articleId}?mode=${srcDataMode}`;
  154. url += channelId ? `&channel=${channelId}` : "";
  155. url += anthologyId ? `&anthology=${anthologyId}` : "";
  156. }
  157. break;
  158. case "textbook":
  159. if (typeof articleId !== "undefined") {
  160. url = `/v2/article/${articleId}?view=textbook&course=${courseId}&mode=${srcDataMode}`;
  161. }
  162. break;
  163. case "exercise":
  164. if (typeof articleId !== "undefined") {
  165. url = `/v2/article/${articleId}?mode=${srcDataMode}&course=${courseId}&exercise=${exerciseId}&user=${userName}`;
  166. setExtra(
  167. <ExerciseAnswer
  168. courseId={courseId}
  169. articleId={articleId}
  170. exerciseId={exerciseId}
  171. />
  172. );
  173. }
  174. break;
  175. case "exercise-list":
  176. if (typeof articleId !== "undefined") {
  177. url = `/v2/article/${articleId}?mode=${srcDataMode}&course=${courseId}&exercise=${exerciseId}`;
  178. setExtra(
  179. <ExerciseList
  180. courseId={courseId}
  181. articleId={articleId}
  182. exerciseId={exerciseId}
  183. />
  184. );
  185. }
  186. break;
  187. default:
  188. if (typeof articleId !== "undefined") {
  189. url = `/v2/corpus/${type}/${articleId}/${srcDataMode}?mode=${srcDataMode}`;
  190. url += channelId ? `&channel=${channelId}` : "";
  191. }
  192. break;
  193. }
  194. console.log("article url", url);
  195. setShowSkeleton(true);
  196. if (typeof articleId !== "undefined") {
  197. const param = {
  198. mode: srcDataMode,
  199. channel: channelId !== null ? channelId : undefined,
  200. book: book !== null ? book : undefined,
  201. para: para !== null ? para : undefined,
  202. };
  203. post<IRecentRequest, IRecentResponse>("/v2/recent", {
  204. type: type,
  205. article_id: articleId,
  206. param: JSON.stringify(param),
  207. }).then((json) => {
  208. console.log("recent", json);
  209. });
  210. }
  211. get<IArticleResponse>(url)
  212. .then((json) => {
  213. console.log("article", json);
  214. if (json.ok) {
  215. setArticleData(json.data);
  216. if (json.data.html) {
  217. setArticleHtml([json.data.html]);
  218. } else if (json.data.content) {
  219. setArticleHtml([json.data.content]);
  220. }
  221. if (json.data.from) {
  222. setRemains(true);
  223. }
  224. setShowSkeleton(false);
  225. setExtra(
  226. <TocTree
  227. treeData={json.data.toc?.map((item) => {
  228. const strTitle = item.title ? item.title : item.pali_title;
  229. const progress = item.progress?.map((item, id) => (
  230. <Tag key={id}>{Math.round(item * 100)}</Tag>
  231. ));
  232. return {
  233. key: `${item.book}-${item.paragraph}`,
  234. title: (
  235. <>
  236. <PaliText text={strTitle} />
  237. {progress}
  238. </>
  239. ),
  240. level: item.level,
  241. };
  242. })}
  243. onSelect={(keys: string[]) => {
  244. console.log(keys);
  245. if (
  246. typeof onArticleChange !== "undefined" &&
  247. keys.length > 0
  248. ) {
  249. onArticleChange(keys[0]);
  250. }
  251. }}
  252. />
  253. );
  254. switch (type) {
  255. case "chapter":
  256. if (typeof articleId === "string" && channelId) {
  257. const [book, para] = articleId?.split("-");
  258. post<IViewRequest, IViewStoreResponse>("/v2/view", {
  259. target_type: type,
  260. book: parseInt(book),
  261. para: parseInt(para),
  262. channel: channelId,
  263. mode: srcDataMode,
  264. }).then((json) => {
  265. console.log("view", json.data);
  266. });
  267. }
  268. break;
  269. default:
  270. break;
  271. }
  272. if (typeof onLoad !== "undefined") {
  273. onLoad(json.data);
  274. }
  275. } else {
  276. setShowSkeleton(false);
  277. setUnauthorized(true);
  278. message.error(json.message);
  279. }
  280. })
  281. .catch((e) => {
  282. console.error(e);
  283. });
  284. }
  285. }, [
  286. active,
  287. type,
  288. articleId,
  289. srcDataMode,
  290. book,
  291. para,
  292. channelId,
  293. anthologyId,
  294. courseId,
  295. exerciseId,
  296. userName,
  297. ]);
  298. const getNextPara = (next: IArticleDataResponse): void => {
  299. if (
  300. typeof next.paraId === "undefined" ||
  301. typeof next.mode === "undefined" ||
  302. typeof next.from === "undefined" ||
  303. typeof next.to === "undefined"
  304. ) {
  305. setRemains(false);
  306. return;
  307. }
  308. let url = `/v2/corpus-chapter/${next.paraId}?mode=${next.mode}`;
  309. url += `&from=${next.from}`;
  310. url += `&to=${next.to}`;
  311. url += channels ? `&channels=${channels}` : "";
  312. console.log("lazy load", url);
  313. get<IArticleResponse>(url).then((json) => {
  314. if (json.ok) {
  315. if (typeof json.data.content === "string") {
  316. const content: string = json.data.content;
  317. setArticleData((origin) => {
  318. if (origin) {
  319. origin.from = json.data.from;
  320. }
  321. return origin;
  322. });
  323. setArticleHtml((origin) => {
  324. return [...origin, content];
  325. });
  326. }
  327. //getNextPara(json.data);
  328. }
  329. });
  330. return;
  331. };
  332. //const comment = <CommentListCard resId={articleData?.uid} resType="article" />
  333. return (
  334. <div>
  335. {showSkeleton ? (
  336. <ArticleSkeleton />
  337. ) : unauthorized ? (
  338. <Result
  339. status="403"
  340. title="无权访问"
  341. subTitle="您无权访问该内容。您可能没有登录,或者内容的所有者没有给您所需的权限。"
  342. extra={<></>}
  343. />
  344. ) : (
  345. <ArticleView
  346. id={articleData?.uid}
  347. title={articleData?.title}
  348. subTitle={articleData?.subtitle}
  349. summary={articleData?.summary}
  350. content={articleData ? articleData.content : ""}
  351. html={articleHtml}
  352. path={articleData?.path}
  353. created_at={articleData?.created_at}
  354. updated_at={articleData?.updated_at}
  355. channels={channels}
  356. type={type}
  357. articleId={articleId}
  358. remains={remains}
  359. onEnd={() => {
  360. if (type === "chapter" && articleData) {
  361. getNextPara(articleData);
  362. }
  363. }}
  364. />
  365. )}
  366. {extra}
  367. <Divider />
  368. </div>
  369. );
  370. };
  371. export default ArticleWidget;