show.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import { Affix, Button, Divider, Space, Tooltip } from "antd";
  2. import { Header } from "antd/lib/layout/layout";
  3. import { Key } from "antd/lib/table/interface";
  4. import { useEffect, useState } from "react";
  5. import { useNavigate, useParams, useSearchParams } from "react-router-dom";
  6. import { FieldTimeOutlined, UnorderedListOutlined } from "@ant-design/icons";
  7. import { ColumnOutlinedIcon } from "../../../assets/icon";
  8. import { IArticleDataResponse } from "../../../components/api/Article";
  9. import { IApiResponseDictList } from "../../../components/api/Dict";
  10. import AnchorNav from "../../../components/article/AnchorNav";
  11. import Article, {
  12. ArticleMode,
  13. ArticleType,
  14. } from "../../../components/article/Article";
  15. import MainMenu from "../../../components/article/MainMenu";
  16. import ModeSwitch from "../../../components/article/ModeSwitch";
  17. import Navigate from "../../../components/article/Navigate";
  18. import RightPanel, { TPanelName } from "../../../components/article/RightPanel";
  19. import ToolButtonDiscussion from "../../../components/article/ToolButtonDiscussion";
  20. import ToolButtonNav from "../../../components/article/ToolButtonNav";
  21. import ToolButtonPr from "../../../components/article/ToolButtonPr";
  22. import ToolButtonSearch from "../../../components/article/ToolButtonSearch";
  23. import ToolButtonSetting from "../../../components/article/ToolButtonSetting";
  24. import ToolButtonTag from "../../../components/article/ToolButtonTag";
  25. import ToolButtonToc from "../../../components/article/ToolButtonToc";
  26. import Avatar from "../../../components/auth/Avatar";
  27. import { IChannel } from "../../../components/channel/Channel";
  28. import NetStatus from "../../../components/general/NetStatus";
  29. import RecentModal from "../../../components/recent/RecentModal";
  30. import { useAppSelector } from "../../../hooks";
  31. import { add } from "../../../reducers/inline-dict";
  32. import { paraParam } from "../../../reducers/para-change";
  33. import { get } from "../../../request";
  34. import store from "../../../store";
  35. import { IRecent } from "../../../components/recent/RecentList";
  36. import { convertToPlain, fullUrl } from "../../../utils";
  37. import ThemeSelect from "../../../components/general/ThemeSelect";
  38. import {
  39. IShowDiscussion,
  40. show,
  41. showAnchor,
  42. } from "../../../reducers/discussion";
  43. import { openPanel } from "../../../reducers/right-panel";
  44. import { TResType } from "../../../components/discussion/DiscussionListCard";
  45. import { modeChange } from "../../../reducers/article-mode";
  46. import SearchButton from "../../../components/general/SearchButton";
  47. import ToStudio from "../../../components/auth/ToStudio";
  48. import { currentUser as _currentUser } from "../../../reducers/current-user";
  49. import LoginAlertModal from "../../../components/auth/LoginAlertModal";
  50. import ShareButton from "../../../components/export/ShareButton";
  51. /**
  52. * type:
  53. * sent 句子
  54. * sim 相似句
  55. * v_para vri 自然段
  56. * page 页码
  57. * chapter 段落
  58. * article 文章
  59. * textbook 课本
  60. * @returns
  61. */
  62. const Widget = () => {
  63. const { type, id } = useParams(); //url 参数
  64. const [searchParams, setSearchParams] = useSearchParams();
  65. const navigate = useNavigate();
  66. const [rightPanel, setRightPanel] = useState<TPanelName>("close");
  67. const [anchorNavOpen, setAnchorNavOpen] = useState(false);
  68. const [anchorNavShow, setAnchorNavShow] = useState(true);
  69. const [recentModalOpen, setRecentModalOpen] = useState(false);
  70. const [loadedArticleData, setLoadedArticleData] =
  71. useState<IArticleDataResponse>();
  72. const paraChange = useAppSelector(paraParam);
  73. const user = useAppSelector(_currentUser);
  74. useEffect(() => {
  75. if (typeof paraChange === "undefined") {
  76. return;
  77. }
  78. let newType: string = paraChange?.type;
  79. let newId: string;
  80. switch (paraChange?.type) {
  81. case "chapter":
  82. newId = `${paraChange.book}-${paraChange.para}`;
  83. break;
  84. case "para":
  85. newId = `${paraChange.book}-${paraChange.para}`;
  86. break;
  87. default:
  88. newId = "";
  89. break;
  90. }
  91. let url = `/article/${newType}/${newId}?`;
  92. let param: string[] = [];
  93. searchParams.forEach((value, key) => {
  94. if (key !== "book" && key !== "par") {
  95. param.push(`${key}=${value}`);
  96. }
  97. });
  98. param.push(`book=${paraChange.book}`);
  99. param.push(`par=${paraChange.para}`);
  100. navigate(url + param.join("&"));
  101. }, [paraChange]);
  102. useEffect(() => {
  103. /**
  104. * 启动时载入格位公式字典
  105. */
  106. //TODO 存储到session storage
  107. const url = `/v2/userdict?view=dict&id=2142c229-8860-4ca5-a82e-1afc7e4f1e5d`;
  108. console.log("url", url);
  109. get<IApiResponseDictList>(url).then((json) => {
  110. console.log("_formula_ ok", json.data.count);
  111. //存储到redux
  112. store.dispatch(add(json.data.rows));
  113. });
  114. }, []);
  115. const rightBarWidth = "48px";
  116. let currMode: ArticleMode;
  117. if (searchParams.get("mode") !== null) {
  118. currMode = searchParams.get("mode") as ArticleMode;
  119. } else {
  120. currMode = "read";
  121. }
  122. useEffect(() => {
  123. //发布mode变更
  124. console.log("发布mode变更", currMode);
  125. store.dispatch(modeChange(currMode as ArticleMode));
  126. }, [currMode]);
  127. console.log(anchorNavOpen, anchorNavShow);
  128. const scrollToTop = () => {
  129. document.getElementById("article-root")?.scrollIntoView();
  130. };
  131. return (
  132. <div id="article-root">
  133. <Affix offsetTop={0}>
  134. <Header
  135. style={{
  136. height: 44,
  137. lineHeight: 44,
  138. display: "flex",
  139. justifyContent: "space-between",
  140. padding: "5px",
  141. }}
  142. >
  143. <div style={{ display: "flex" }} key="left">
  144. <MainMenu />
  145. <NetStatus style={{ color: "white" }} />
  146. </div>
  147. <div style={{ display: "flex" }} key="middle"></div>
  148. <div style={{ display: "flex" }} key="right">
  149. {type === "article" && loadedArticleData ? (
  150. <>
  151. <Button
  152. ghost
  153. onClick={(event) => {
  154. const url = `/studio/${loadedArticleData.studio?.realName}/article/edit/${loadedArticleData.uid}`;
  155. if (event.ctrlKey || event.metaKey) {
  156. window.open(fullUrl(url), "_blank");
  157. } else {
  158. navigate(url);
  159. }
  160. }}
  161. >
  162. Edit
  163. </Button>
  164. <Button
  165. disabled={user ? false : true}
  166. ghost
  167. onClick={(event) => {
  168. const url = `/studio/${user?.nickName}/article/create?parent=${loadedArticleData.uid}`;
  169. if (event.ctrlKey || event.metaKey) {
  170. window.open(fullUrl(url), "_blank");
  171. } else {
  172. navigate(url);
  173. }
  174. }}
  175. >
  176. Fork
  177. </Button>
  178. </>
  179. ) : undefined}
  180. <ShareButton
  181. type={type as ArticleType}
  182. book={searchParams.get("book")}
  183. para={searchParams.get("par")}
  184. channelId={searchParams.get("channel")}
  185. articleId={id}
  186. anthologyId={searchParams.get("anthology")}
  187. />
  188. <SearchButton />
  189. <Divider type="vertical" />
  190. <ToStudio />
  191. <Divider type="vertical" />
  192. <Avatar placement="bottom" />
  193. <Divider type="vertical" />
  194. <ThemeSelect />
  195. <Divider type="vertical" />
  196. <ModeSwitch
  197. channel={searchParams.get("channel")}
  198. currMode={currMode}
  199. onModeChange={(e: ArticleMode) => {
  200. let output: any = { mode: e };
  201. searchParams.forEach((value, key) => {
  202. console.log(value, key);
  203. if (key !== "mode") {
  204. output[key] = value;
  205. }
  206. });
  207. setSearchParams(output);
  208. }}
  209. onChannelChange={(channels: IChannel[], mode: ArticleMode) => {
  210. let output: any = {
  211. mode: mode,
  212. channel: channels.map((item) => item.id).join("_"),
  213. };
  214. searchParams.forEach((value, key) => {
  215. console.log(value, key);
  216. if (key !== "mode" && key !== "channel") {
  217. output[key] = value;
  218. }
  219. });
  220. setSearchParams(output);
  221. }}
  222. />
  223. <Tooltip title="文章目录" placement="bottomLeft">
  224. <Button
  225. style={{ display: "block", color: "white" }}
  226. icon={<UnorderedListOutlined />}
  227. type="text"
  228. onClick={() => setAnchorNavOpen((value) => !value)}
  229. />
  230. </Tooltip>
  231. <Tooltip title="侧边栏" placement="bottomLeft">
  232. <Button
  233. style={{ display: "block", color: "white" }}
  234. icon={<ColumnOutlinedIcon />}
  235. type="text"
  236. onClick={() =>
  237. setRightPanel((value) => {
  238. if (value === "close") {
  239. setAnchorNavShow(false);
  240. } else {
  241. setAnchorNavShow(true);
  242. }
  243. return value === "close" ? "open" : "close";
  244. })
  245. }
  246. />
  247. </Tooltip>
  248. </div>
  249. </Header>
  250. </Affix>
  251. <div style={{ width: "100%", display: "flex" }}>
  252. <Affix offsetTop={44}>
  253. <div
  254. style={{
  255. height: `calc(100% - 44px)`,
  256. padding: 8,
  257. display: "flex",
  258. flexDirection: "column",
  259. justifyContent: "space-between",
  260. }}
  261. >
  262. <div>
  263. <Space direction="vertical">
  264. <RecentModal
  265. trigger={<Button icon={<FieldTimeOutlined />} />}
  266. open={recentModalOpen}
  267. onOpen={(isOpen: boolean) => setRecentModalOpen(isOpen)}
  268. onSelect={(
  269. event: React.MouseEvent<HTMLElement, MouseEvent>,
  270. param: IRecent
  271. ) => {
  272. setRecentModalOpen(false);
  273. let url = `/article/${param.type}/${param.articleId}?mode=`;
  274. url += param.param?.mode ? param.param?.mode : "read";
  275. url += param.param?.channel
  276. ? `&channel=${param.param?.channel}`
  277. : "";
  278. url += param.param?.book
  279. ? `&book=${param.param?.book}`
  280. : "";
  281. url += param.param?.para ? `&par=${param.param?.para}` : "";
  282. if (event.ctrlKey || event.metaKey) {
  283. window.open(fullUrl(url), "_blank");
  284. } else {
  285. navigate(url);
  286. }
  287. }}
  288. />
  289. <ToolButtonToc
  290. type={type as ArticleType}
  291. articleId={id}
  292. anthologyId={searchParams.get("anthology")}
  293. onSelect={(key: Key) => {
  294. console.log("toc click", key);
  295. const newType = type === "para" ? "chapter" : type;
  296. let url = `/article/${newType}/${key}?`;
  297. let param: string[] = [];
  298. searchParams.forEach((value, searchKey) => {
  299. if (type !== "para") {
  300. param.push(`${searchKey}=${value}`);
  301. } else {
  302. if (searchKey !== "book" && searchKey !== "par") {
  303. param.push(`${searchKey}=${value}`);
  304. }
  305. }
  306. });
  307. navigate(url + param.join("&"));
  308. scrollToTop();
  309. }}
  310. />
  311. <ToolButtonNav type={type} articleId={id} />
  312. <ToolButtonTag type={type} articleId={id} />
  313. <ToolButtonPr type={type} articleId={id} />
  314. <ToolButtonDiscussion type={type} articleId={id} />
  315. <ToolButtonSearch type={type} articleId={id} />
  316. <ToolButtonSetting type={type} articleId={id} />
  317. </Space>
  318. </div>
  319. </div>
  320. </Affix>
  321. <div
  322. key="main"
  323. style={{ width: `calc(100% - ${rightBarWidth})`, display: "flex" }}
  324. >
  325. <div
  326. key="Article"
  327. style={{ marginLeft: "auto", marginRight: "auto", width: 1100 }}
  328. >
  329. <LoginAlertModal mode={currMode} />
  330. <Article
  331. active={true}
  332. type={type as ArticleType}
  333. book={searchParams.get("book")}
  334. para={searchParams.get("par")}
  335. channelId={searchParams.get("channel")}
  336. articleId={id}
  337. anthologyId={searchParams.get("anthology")}
  338. mode={searchParams.get("mode") as ArticleMode}
  339. onArticleChange={(article: string, target?: string) => {
  340. console.log("article change", article, target);
  341. let mType = type;
  342. if (article.split("-").length === 2) {
  343. mType = "chapter";
  344. }
  345. let url = `/article/${mType}/${article}?mode=${currMode}`;
  346. searchParams.forEach((value, key) => {
  347. console.log(value, key);
  348. if (key !== "mode") {
  349. url += `&${key}=${value}`;
  350. }
  351. });
  352. if (target === "_blank") {
  353. window.open(fullUrl(url), "_blank");
  354. } else {
  355. navigate(url);
  356. }
  357. }}
  358. onLoad={(article: IArticleDataResponse) => {
  359. setLoadedArticleData(article);
  360. const windowTitle = article.title_text
  361. ? article.title_text
  362. : article.title;
  363. document.title = windowTitle.slice(0, 128);
  364. const paramTopic = searchParams.get("topic");
  365. const paramComment = searchParams.get("comment");
  366. const paramType = searchParams.get("dis_type");
  367. if (paramTopic !== null && paramType !== null) {
  368. const anchorInfo: IShowDiscussion = {
  369. type: "discussion",
  370. topic: paramTopic,
  371. resType: paramType as TResType,
  372. comment: paramComment ? paramComment : undefined,
  373. };
  374. store.dispatch(show(anchorInfo));
  375. store.dispatch(showAnchor(anchorInfo));
  376. store.dispatch(openPanel("discussion"));
  377. }
  378. }}
  379. onAnthologySelect={(id: string) => {
  380. let output: any = { anthology: id };
  381. searchParams.forEach((value, key) => {
  382. console.log(value, key);
  383. if (key !== "anthology") {
  384. output[key] = value;
  385. }
  386. });
  387. setSearchParams(output);
  388. }}
  389. />
  390. <Navigate
  391. type={type as ArticleType}
  392. articleId={id}
  393. onChange={(
  394. event: React.MouseEvent<HTMLElement, MouseEvent>,
  395. newId: string
  396. ) => {
  397. let url = `/article/${type}/${newId}?mode=${currMode}`;
  398. searchParams.forEach((value, key) => {
  399. if (key !== "mode" && key !== "par") {
  400. url += `&${key}=${value}`;
  401. }
  402. });
  403. if (type === "para") {
  404. url += "&par=" + newId.split("-")[1];
  405. }
  406. if (event.ctrlKey || event.metaKey) {
  407. window.open(fullUrl(url), "_blank");
  408. } else {
  409. navigate(url);
  410. scrollToTop();
  411. }
  412. }}
  413. />
  414. </div>
  415. <div key="RightPanel" id="article_right_panel">
  416. <AnchorNav open={anchorNavOpen && anchorNavShow} />
  417. <RightPanel
  418. curr={rightPanel}
  419. type={type as ArticleType}
  420. articleId={id ? id : ""}
  421. selectedChannelsId={searchParams.get("channel")?.split("_")}
  422. onClose={() => {
  423. setRightPanel("close");
  424. console.log("right panel", "close");
  425. setAnchorNavShow(true);
  426. }}
  427. onTabChange={(curr: TPanelName) => setRightPanel(curr)}
  428. onChannelSelect={(e: IChannel[]) => {
  429. //channel 改变
  430. console.log("onChannelSelect", e);
  431. const channels = e.map((item) => item.id).join("_");
  432. console.log("channels", channels);
  433. let url = `/article/${type}/${id}?mode=${currMode}`;
  434. searchParams.forEach((value, key) => {
  435. console.log(value, key);
  436. if (key !== "mode" && key !== "channel") {
  437. url += `&${key}=${value}`;
  438. }
  439. });
  440. if (e.length > 0) {
  441. url += `&channel=${channels}`;
  442. }
  443. console.log("url", url);
  444. navigate(url);
  445. scrollToTop();
  446. }}
  447. />
  448. </div>
  449. </div>
  450. </div>
  451. </div>
  452. );
  453. };
  454. export default Widget;