Răsfoiți Sursa

fix(dashboard-v6): 修正 /article/* 残留链接并新增 /workspace/tipitaka/page 路由

visuddhinanda 2 zile în urmă
părinte
comite
ce14030699

+ 15 - 0
dashboard-v6/src/api/article.ts

@@ -625,3 +625,18 @@ export async function csParaLoader({ params }: LoaderFunctionArgs) {
   // title 使用 `book_para_page` 中的 page(第三段),例如 `169_3_64` -> `64`
   return { title: id.split("_")[2] ?? id };
 }
+
+/**
+ * 页码引用路由 loader。
+ *
+ * 仅用于提供面包屑标题,页面内容由 TypePage 内部的 usePageNav 解析渲染。
+ */
+export async function pageLoader({ params }: LoaderFunctionArgs) {
+  const id = params.id;
+
+  if (!id) {
+    throw new Response("Missing page id", { status: 400 });
+  }
+
+  return { title: id };
+}

+ 2 - 2
dashboard-v6/src/components/anthology/EditableTocTree.tsx

@@ -17,7 +17,7 @@ import type {
 } from "../../api/article";
 import ArticleListModal from "../article/ArticleListModal";
 
-import { fullUrl, randomString } from "../../utils";
+import { articlePath, fullUrl, randomString } from "../../utils";
 import type {
   ListNodeData,
   TreeNodeData,
@@ -172,7 +172,7 @@ const EditableTocTree = ({
           node: TreeNodeData
         ) => {
           if (e.ctrlKey || e.metaKey) {
-            window.open(fullUrl(`/article/article/${node.id}`), "_blank");
+            window.open(fullUrl(articlePath("article", node.id)), "_blank");
           } else {
             setViewArticle(node);
             setOpenViewer(true);

+ 2 - 1
dashboard-v6/src/components/article/ArticleEditTools.tsx

@@ -7,6 +7,7 @@ import ShareModal from "../share/ShareModal";
 import TplBuilder from "../tpl-builder/TplBuilder";
 import AddToAnthology from "../anthology/AddToAnthology";
 import { EResType } from "../share/utils";
+import { articlePath } from "../../utils";
 
 interface IWidget {
   studioName?: string;
@@ -42,7 +43,7 @@ const ArticleEditToolsWidget = ({
           resType={EResType.article}
         />
       ) : undefined}
-      <Link to={`/article/article/${articleId}`} target="_blank">
+      <Link to={articlePath("article", articleId ?? "")} target="_blank">
         {intl.formatMessage({ id: "buttons.open.in.tab" })}
       </Link>
       <TplBuilder

+ 2 - 2
dashboard-v6/src/components/article/ArticleList.tsx

@@ -32,7 +32,7 @@ import Share from "../share/Share";
 import AnthologySelect from "../anthology/AnthologySelect";
 import StudioName from "../auth/Studio";
 
-import { getSorterUrl } from "../../utils";
+import { articlePath, getSorterUrl } from "../../utils";
 import TransferCreate from "../transfer/TransferCreate";
 import { TransferOutLinedIcon } from "../../assets/icon";
 import type { IStudio, IUser } from "../../api/Auth";
@@ -392,7 +392,7 @@ const ArticleList = ({
                 >
                   <Link
                     key={index}
-                    to={`/article/article/${row.id}`}
+                    to={articlePath("article", row.id)}
                     target="_blank"
                   >
                     {intl.formatMessage({

+ 2 - 2
dashboard-v6/src/components/article/TypePage.tsx

@@ -4,7 +4,7 @@ import { useIntl } from "react-intl";
 
 import type { ArticleMode, ArticleType } from "../../api/article";
 import type { TTarget } from "../../types";
-import { fullUrl } from "../../utils";
+import { articlePath, fullUrl } from "../../utils";
 import TypePali from "./TypePali";
 import NavigateButton from "./components/NavigateButton";
 import ArticleSkeleton from "./components/ArticleSkeleton";
@@ -82,7 +82,7 @@ const TypePageWidget = ({
       onArticleChange("page", id, target);
     } else {
       if (target === "_blank") {
-        let url = `/article/page/${id}?mode=${mode}`;
+        let url = `${articlePath("page", id)}?mode=${mode}`;
         if (channelId) {
           url += `&channel=${channelId}`;
         }

+ 2 - 2
dashboard-v6/src/components/article/components/ArticleReaderToolbar.tsx

@@ -16,7 +16,7 @@ import { useAppSelector } from "../../../hooks";
 import { currentUser } from "../../../reducers/current-user";
 
 import { useState } from "react";
-import { fullUrl } from "../../../utils";
+import { articlePath, fullUrl } from "../../../utils";
 
 import type { TRole } from "../../../api/Auth";
 import { useIntl } from "react-intl";
@@ -129,7 +129,7 @@ const TypeArticleReaderToolbarWidget = ({
                 switch (key) {
                   case "open_in_tab":
                     window.open(
-                      fullUrl(`/article/article/${articleId}`),
+                      fullUrl(articlePath("article", articleId ?? "")),
                       "_blank"
                     );
                     break;

+ 2 - 1
dashboard-v6/src/components/discussion/DiscussionAnchor.tsx

@@ -10,6 +10,7 @@ import { Link } from "react-router";
 import type { TResType } from "../../api/discussion";
 import type { ISentenceData, ISentenceResponse } from "../../api/sentence";
 import MdView from "../general/MdView";
+import { articlePath } from "../../utils";
 
 export interface IAnchor {
   type: TResType;
@@ -81,7 +82,7 @@ const DiscussionAnchorWidget = ({
           .then((json) => {
             if (json.ok) {
               setTitle(
-                <Link to={`/article/article/${resId}`}>{json.data.title}</Link>
+                <Link to={articlePath("article", resId ?? "")}>{json.data.title}</Link>
               );
               setContent(json.data.content?.substring(0, 200));
             }

+ 2 - 2
dashboard-v6/src/components/navigation/MainMenu.tsx

@@ -80,8 +80,8 @@ function findSelectedKey(
 
 /* ================= 最近编辑跳转地址 ================= */
 
-// 巴利三藏文本类型(chapter/para 等)路由位于 /workspace/tipitaka 下
-const TIPITAKA_TYPES = new Set(["chapter", "para"]);
+// 巴利三藏文本类型(chapter/para/cs-para/page 等)路由位于 /workspace/tipitaka 下
+const TIPITAKA_TYPES = new Set(["chapter", "para", "cs-para", "page"]);
 
 function recentPath(type: string, id: string): string {
   return TIPITAKA_TYPES.has(type)

+ 1 - 1
dashboard-v6/src/components/related-para/RelatedPara.tsx

@@ -127,7 +127,7 @@ const RelatedParaWidget = ({ book, para, trigger }: IWidget) => {
                     <Card
                       title={
                         <Link
-                          to={`/article/para/${item.book}-${firstPara}?book=${item.book}&par=${item.para}`}
+                          to={`/workspace/tipitaka/para/${item.book}-${firstPara}?book=${item.book}&par=${item.para}`}
                           target="_blank"
                         >
                           {item.book_title_pali}

+ 2 - 2
dashboard-v6/src/components/sentence/SentEditMenu.tsx

@@ -22,7 +22,7 @@ import {
   PasteOutLinedIcon,
 } from "../../assets/icon";
 import { useIntl } from "react-intl";
-import { fullUrl } from "../../utils";
+import { articlePath, fullUrl } from "../../utils";
 import SentHistoryModal from "../sentence-history/SentHistoryModal";
 
 interface IWidget {
@@ -67,7 +67,7 @@ const SentEditMenuWidget = ({
         break;
       case "copy-link":
         if (data) {
-          let link = `/article/para/${data.book}-${data.para}?mode=edit`;
+          let link = `${articlePath("para", `${data.book}-${data.para}`)}?mode=edit`;
           link += `&book=${data.book}&par=${data.para}`;
           link += `&channel=${data.channel.id}`;
 

+ 2 - 2
dashboard-v6/src/components/sentence/SentTab.tsx

@@ -15,7 +15,7 @@ import TocPath from "../../../src/components/tipitaka/TocPath";
 import SentMenu from "./SentMenu";
 
 import SentTabCopy from "./SentTabCopy";
-import { fullUrl } from "../../utils";
+import { articlePath, fullUrl } from "../../utils";
 import SentWbw from "./SentWbw";
 import SentTabButtonWbw from "./SentTabButtonWbw";
 
@@ -177,7 +177,7 @@ const SentTabWidget = ({
                   break;
                 }
                 case "copy-link": {
-                  let link = `/article/para/${book}-${para}?mode=edit`;
+                  let link = `${articlePath("para", `${book}-${para}`)}?mode=edit`;
                   link += `&book=${book}&par=${para}`;
                   if (channelsId) {
                     link += `&channel=` + channelsId?.join("_");

+ 2 - 2
dashboard-v6/src/components/template/Article.tsx

@@ -9,7 +9,7 @@ import TypeArticle from "../article/TypeArticle";
 import TypeAnthology from "../article/TypeAnthology";
 import TypePali from "../article/TypePali";
 import TypePage from "../article/TypePage";
-import { fullUrl } from "../../utils";
+import { articlePath, fullUrl } from "../../utils";
 
 const { Text } = Typography;
 
@@ -175,7 +175,7 @@ export const ArticleCtl = ({
     />
   );
   let output = <></>;
-  let articleLink = `/article/${type}/${id}?mode=${currMode}`;
+  let articleLink = `${articlePath(type ?? "", id ?? "")}?mode=${currMode}`;
   articleLink += channel ? `&channel=${currChannels.join("_")}` : "";
 
   const OpenLink = (

+ 2 - 2
dashboard-v6/src/components/template/ParaHandle.tsx

@@ -1,6 +1,6 @@
 import { Button, Dropdown, type MenuProps, message, notification } from "antd";
 import { useNavigate, useSearchParams } from "react-router";
-import { fullUrl, scrollToTop } from "../../utils";
+import { articlePath, fullUrl, scrollToTop } from "../../utils";
 import { useIntl } from "react-intl";
 import store from "../../store";
 import { modeChange } from "../../reducers/article-mode";
@@ -124,7 +124,7 @@ export const ParaHandleCtl = ({
      * TODO 临时的解决方案。以后应该从传参获取其他参数,然后reducer 通知更新。
      * 因为如果是Article组件被嵌入其他页面。不能直接更新浏览器,而是应该更新Article组件内部
      */
-    let url = `/article/para/${book}-${para}?book=${book}&par=${para}`;
+    let url = `${articlePath("para", `${book}-${para}`)}?book=${book}&par=${para}`;
     const param: string[] = [];
     searchParams.forEach((value: unknown, key: unknown) => {
       if (key !== "book" && key !== "par") {

+ 2 - 1
dashboard-v6/src/components/term/TermItem.tsx

@@ -20,6 +20,7 @@ import Discussion from "../discussion/DiscussionMock";
 import { useIntl } from "react-intl";
 import User from "../auth/User";
 import MdView from "../general/MdView";
+import { articlePath } from "../../utils";
 
 const { Text } = Typography;
 
@@ -95,7 +96,7 @@ const TermItemWidget = ({ data, onTermClick }: IWidget) => {
                     setOpenTermModal(true);
                     break;
                   case "translate":
-                    navigate(`/article/term/${data?.guid}`);
+                    navigate(articlePath("term", data?.guid ?? ""));
                     break;
                   default:
                     break;

+ 2 - 2
dashboard-v6/src/components/tipitaka/TocPath.tsx

@@ -2,7 +2,7 @@ import { useNavigate, useSearchParams } from "react-router";
 import { Breadcrumb, Popover, Tag, Typography } from "antd";
 
 import React, { type JSX } from "react";
-import { fullUrl } from "../../utils";
+import { articlePath, fullUrl } from "../../utils";
 import type { ITocPathNode } from "../../api/pali-text";
 import PaliText from "../general/PaliText";
 
@@ -49,7 +49,7 @@ const TocPathWidget = ({
             : searchParams.get("channel");
           const mode = searchParams.get("mode");
           const urlMode = mode ? mode : "read";
-          let url = `/article/${type}/${item.book}-${item.paragraph}?mode=${urlMode}${param}`;
+          let url = `${articlePath(type, `${item.book}-${item.paragraph}`)}?mode=${urlMode}${param}`;
           url += channel ? `&channel=${channel}` : "";
           if (e.ctrlKey || e.metaKey) {
             window.open(fullUrl(url), "_blank");

+ 1 - 0
dashboard-v6/src/locales/en-US/pages.ts

@@ -12,6 +12,7 @@ const items = {
   "pages.tipitaka.chapter.title": "Chapter",
   "pages.tipitaka.para.title": "Paragraph",
   "pages.tipitaka.cs-para.title": "CS Paragraph",
+  "pages.tipitaka.page.title": "Page",
 };
 
 export default items;

+ 1 - 0
dashboard-v6/src/locales/zh-Hans/pages.ts

@@ -12,6 +12,7 @@ const items = {
   "pages.tipitaka.chapter.title": "章节",
   "pages.tipitaka.para.title": "段落",
   "pages.tipitaka.cs-para.title": "CS段落",
+  "pages.tipitaka.page.title": "页码",
 };
 
 export default items;

+ 2 - 2
dashboard-v6/src/pages/workspace/channel/show.tsx

@@ -9,7 +9,7 @@ import { get } from "../../../request";
 import ChapterInChannelList from "../../../components/channel/ChapterInChannelList";
 import ShareModal from "../../../components/share/ShareModal";
 
-import { fullUrl } from "../../../utils";
+import { articlePath, fullUrl } from "../../../utils";
 import type { IArticleParam } from "../../../types/article";
 import { EResType } from "../../../components/share/utils";
 import type { IApiResponseChannel } from "../../../api/channel";
@@ -66,7 +66,7 @@ const Widget = () => {
                     chapter: IArticleParam
                   ) => {
                     if (event.ctrlKey || event.metaKey) {
-                      let url = `/article/${chapter.type}/${chapter.articleId}?mode=`;
+                      let url = `${articlePath(chapter.type, chapter.articleId)}?mode=`;
                       url += chapter?.mode ? chapter?.mode : "read";
                       url += chapter?.channelId
                         ? `&channel=${chapter.channelId}`

+ 71 - 0
dashboard-v6/src/pages/workspace/tipitaka/page.tsx

@@ -0,0 +1,71 @@
+import { useEffect } from "react";
+import {
+  useLocation,
+  useMatches,
+  useNavigate,
+  useParams,
+  useSearchParams,
+} from "react-router";
+import { useIntl } from "react-intl";
+import type { ArticleMode } from "../../../api/article";
+import TypePage from "../../../components/article/TypePage";
+import { useAppSelector } from "../../../hooks";
+import { currentUser } from "../../../reducers/current-user";
+import { useSaveRecent } from "../../../hooks/useSaveRecent";
+
+const Widget = () => {
+  const { id } = useParams();
+  const [searchParams] = useSearchParams();
+  const navigate = useNavigate();
+  const { search } = useLocation();
+  const intl = useIntl();
+  const matches = useMatches() as {
+    data?: { title?: string; name?: string; word?: string };
+  }[];
+  const data = [...matches].reverse().find((m) => m.data)?.data;
+  const name = data?.title ?? data?.name ?? data?.word;
+  const prefix = intl.formatMessage({ id: "pages.tipitaka.page.title" });
+
+  const mode = searchParams.get("mode") ?? "read";
+  const channelId = searchParams.get("channel");
+
+  const currUser = useAppSelector(currentUser);
+  const { save } = useSaveRecent();
+
+  // 记录最近访问
+  useEffect(() => {
+    if (!currUser?.id || !id) return;
+    const paramObj = search
+      ? Object.fromEntries(new URLSearchParams(search))
+      : undefined;
+    save({
+      type: "page",
+      article_id: id,
+      param: JSON.stringify(paramObj),
+    });
+  }, [currUser?.id, id, search, save]);
+
+  return (
+    <>
+      <title>{name ? `${prefix}-${name}` : prefix}</title>
+      <TypePage
+        articleId={id}
+        mode={mode as ArticleMode}
+        channelId={channelId}
+        onArticleChange={(type, articleId, target) => {
+          const url = `workspace/tipitaka/${type}/${articleId}`;
+          if (target === "_blank") {
+            window.open(
+              `${window.location.origin}${import.meta.env.BASE_URL}${url}${search}`,
+              "_blank"
+            );
+          } else {
+            navigate(`/${url}${search}`);
+          }
+        }}
+      />
+    </>
+  );
+};
+
+export default Widget;

+ 15 - 1
dashboard-v6/src/routes/tipitakaRoutes.ts

@@ -2,7 +2,7 @@
 import { lazy } from "react";
 import type { RouteObject } from "react-router";
 import { chapterLoader, paraLoader } from "../api/pali-text";
-import { csParaLoader } from "../api/article";
+import { csParaLoader, pageLoader } from "../api/article";
 
 const WorkspaceTipitaka = lazy(
   () => import("../pages/workspace/tipitaka/bypath")
@@ -16,6 +16,9 @@ const WorkspaceTipitakaPara = lazy(
 const WorkspaceTipitakaCsPara = lazy(
   () => import("../pages/workspace/tipitaka/cs-para")
 );
+const WorkspaceTipitakaPage = lazy(
+  () => import("../pages/workspace/tipitaka/page")
+);
 
 const tipitakaRoutes: RouteObject[] = [
   {
@@ -78,6 +81,17 @@ const tipitakaRoutes: RouteObject[] = [
           },
         ],
       },
+      {
+        path: "page",
+        children: [
+          {
+            path: ":id",
+            Component: WorkspaceTipitakaPage,
+            loader: pageLoader,
+            handle: { id: "workspace.tipitaka.page", crumb: "page" },
+          },
+        ],
+      },
     ],
   },
 ];

+ 16 - 0
dashboard-v6/src/utils.ts

@@ -1,6 +1,22 @@
 import type { SortOrder } from "antd/lib/table/interface";
 import lodash from "lodash";
 
+/**
+ * 巴利三藏文本类型(chapter/para 等)路由位于 /workspace/tipitaka 下。
+ */
+const TIPITAKA_ARTICLE_TYPES = new Set(["chapter", "para", "cs-para", "page"]);
+
+/**
+ * 根据 article 类型和 id 生成 v6 前端路由路径。
+ * - chapter / para → /workspace/tipitaka/:type/:id
+ * - 其余已迁移类型(article / term / anthology / tag ...)→ /workspace/:type/:id
+ */
+export function articlePath(type: string, id: string): string {
+  return TIPITAKA_ARTICLE_TYPES.has(type)
+    ? `/workspace/tipitaka/${type}/${id}`
+    : `/workspace/${type}/${id}`;
+}
+
 export function dashboardBasePath(): string {
   if (import.meta.env.BASE_URL.includes("http")) {
     //for CDN