visuddhinanda há 1 mês atrás
pai
commit
314a7b17bb

+ 13 - 0
dashboard-v6/documents/development/v6-todo-list.md

@@ -45,3 +45,16 @@
 ### 新增
 
 - [ ] `/discussion`=> [1]
+
+### article page
+
+- [x] "anthology"
+- [x] "article"
+- [ ] "textbook"
+- [x] "term"
+- [ ] "task"
+- [ ] "chapter"
+- [ ] "para"
+- [ ] "series"
+- [ ] "cs-para"
+- [ ] "page"

+ 34 - 64
dashboard-v6/src/Router.tsx

@@ -5,12 +5,12 @@ import { channelLoader } from "./api/channel";
 import { testRoutes } from "./routes/testRoutes";
 import { buildRouteConfig } from "./routes/buildRoutes";
 import { anthologyLoader, articleLoader } from "./api/Article";
+import { termLoader } from "./api/Term";
 
 const RootLayout = lazy(() => import("./layouts/Root"));
 const AnonymousLayout = lazy(() => import("./layouts/anonymous"));
 const DashboardLayout = lazy(() => import("./layouts/dashboard"));
 const WorkspaceLayout = lazy(() => import("./layouts/workspace"));
-const WorkspaceEditorLayout = lazy(() => import("./layouts/workspace/editor"));
 
 const UsersSignIn = lazy(() => import("./pages/users/sign-in"));
 const UsersSignUp = lazy(() => import("./pages/users/sign-up"));
@@ -28,15 +28,16 @@ const WorkspaceChannelSetting = lazy(
 const WorkspaceTipitaka = lazy(
   () => import("./pages/workspace/tipitaka/bypath")
 );
+const WorkspaceTipitakaChapter = lazy(
+  () => import("./pages/workspace/tipitaka/chapter")
+);
 const WorkspaceHome = lazy(() => import("./pages/workspace/home"));
 const WorkspaceChat = lazy(() => import("./pages/workspace/chat"));
 
 const WorkspaceTerm = lazy(() => import("./pages/workspace/term/list"));
 const WorkspaceTermShow = lazy(() => import("./pages/workspace/term/show"));
 const WorkspaceTermEdit = lazy(() => import("./pages/workspace/term/edit"));
-const WorkspaceEditChapter = lazy(
-  () => import("./pages/workspace/editor/chapter")
-);
+
 // 文集
 const WorkspaceAnthologyList = lazy(
   () => import("./pages/workspace/anthology")
@@ -165,25 +166,39 @@ const router = createBrowserRouter(
             },
             {
               path: "tipitaka",
-              Component: WorkspaceTipitaka,
               handle: { id: "workspace.tipitaka", crumb: "tipitaka" },
               children: [
                 {
-                  path: ":root",
+                  path: "lib",
                   Component: WorkspaceTipitaka,
                   children: [
                     {
-                      path: ":path",
+                      path: ":root",
                       Component: WorkspaceTipitaka,
                       children: [
                         {
-                          path: ":tag",
+                          path: ":path",
                           Component: WorkspaceTipitaka,
+                          children: [
+                            {
+                              path: ":tag",
+                              Component: WorkspaceTipitaka,
+                            },
+                          ],
                         },
                       ],
                     },
                   ],
                 },
+                {
+                  path: "chapter",
+                  children: [
+                    {
+                      path: ":id",
+                      Component: WorkspaceTipitakaChapter,
+                    },
+                  ],
+                },
               ],
             },
             {
@@ -218,66 +233,21 @@ const router = createBrowserRouter(
             {
               path: "term",
               handle: { id: "workspace.term", crumb: "term" },
-              Component: WorkspaceTerm,
-            },
-            {
-              path: "edit",
-              Component: WorkspaceEditorLayout,
-              handle: { crumb: "edit" },
               children: [
+                { index: true, Component: WorkspaceTerm },
                 {
-                  path: "article",
-                  children: [
-                    {
-                      path: ":id",
-                      children: [
-                        {
-                          path: "edit",
-                        },
-                      ],
-                    },
-                  ],
-                },
-                {
-                  path: "anthology",
-                  children: [{ path: ":id" }],
-                },
-                {
-                  path: "series",
-                  children: [{ path: ":id" }],
-                },
-                {
-                  path: "chapter",
-                  children: [
-                    {
-                      path: ":id",
-                      children: [
-                        { index: true, Component: WorkspaceEditChapter },
-                      ],
-                    },
-                  ],
-                },
-                {
-                  path: "para",
-                  children: [{ path: ":id" }],
-                },
-                {
-                  path: "cs-para",
-                  children: [{ path: ":id" }],
-                },
-                {
-                  path: "wiki",
-                  handle: { crumb: "wiki" },
+                  path: ":id",
+                  loader: termLoader,
+                  handle: {
+                    crumb: (match: { data: { word: string } }) =>
+                      match.data.word,
+                  },
                   children: [
+                    { index: true, Component: WorkspaceTermShow },
                     {
-                      path: ":id",
-                      children: [
-                        { index: true, Component: WorkspaceTermShow },
-                        {
-                          path: "edit",
-                          Component: WorkspaceTermEdit,
-                        },
-                      ],
+                      path: "edit",
+                      handle: { crumb: "edit" },
+                      Component: WorkspaceTermEdit,
                     },
                   ],
                 },

+ 25 - 4
dashboard-v6/src/api/Term.ts

@@ -1,6 +1,7 @@
 import type { IStudio, IUser, TRole } from "./Auth";
 import type { IChannel } from "./channel";
 import { get } from "../request";
+import type { LoaderFunctionArgs } from "react-router";
 
 export interface ITerm {
   id?: string;
@@ -110,7 +111,7 @@ export interface ITermDeleteRequest {
 
 export interface IGetTermParams {
   id: string;
-  mode: "read" | "edit";
+  mode?: "read" | "edit";
   channelsId?: string | null;
 }
 
@@ -119,9 +120,29 @@ export function getTerm({
   mode,
   channelsId,
 }: IGetTermParams): Promise<ITermResponse> {
-  const url =
-    `/api/v2/terms/${id}?mode=${mode}` +
-    (channelsId ? `&channel=${channelsId}` : "");
+  let url = `/api/v2/terms/${id}?a=a`;
+  if (mode) {
+    url += `&mode=${mode}`;
+  }
+  if (channelsId) {
+    url += `&channel=${channelsId}`;
+  }
 
   return get<ITermResponse>(url);
 }
+
+export async function termLoader({ params }: LoaderFunctionArgs) {
+  const termId = params.id;
+
+  if (!termId) {
+    throw new Response("Missing termId", { status: 400 });
+  }
+
+  const res = await getTerm({ id: termId });
+
+  if (!res.ok) {
+    throw new Response("term not found", { status: 404 });
+  }
+
+  return res.data;
+}

+ 1 - 1
dashboard-v6/src/components/article/TypePali.tsx

@@ -25,7 +25,7 @@ import TplBuilder from "../tpl-builder/TplBuilder";
 import ArticleHeader from "./components/ArticleHeader";
 import { TaskBuilderChapterModal } from "../task/TaskBuilderChapterModal";
 
-interface ISearchParams {
+export interface ISearchParams {
   key: string;
   value: string;
 }

+ 19 - 9
dashboard-v6/src/components/article/TypeTerm.tsx

@@ -1,4 +1,4 @@
-import { Breadcrumb, Button } from "antd";
+import { Breadcrumb, Button, Space } from "antd";
 import type { ArticleMode } from "../../api/Article";
 import { useAppSelector } from "../../hooks";
 import { currentUser } from "../../reducers/current-user";
@@ -12,10 +12,17 @@ interface IWidget {
   id?: string;
   mode?: ArticleMode | null;
   channelId?: string | null;
+  headerExtra?: React.ReactNode;
   onEdit?: () => void;
 }
 
-const TypeTermWidget = ({ channelId, id, mode = "read", onEdit }: IWidget) => {
+const TypeTermWidget = ({
+  channelId,
+  id,
+  mode = "read",
+  headerExtra,
+  onEdit,
+}: IWidget) => {
   const { articleData, term, errorCode, loading } = useTerm({
     id,
     channelId,
@@ -33,13 +40,16 @@ const TypeTermWidget = ({ channelId, id, mode = "read", onEdit }: IWidget) => {
       <title>{articleData?.title}-百科</title>
       <ArticleHeader
         header={
-          <Breadcrumb
-            items={path}
-            style={{
-              whiteSpace: "nowrap",
-              width: "100%",
-            }}
-          />
+          <Space>
+            {headerExtra}
+            <Breadcrumb
+              items={path}
+              style={{
+                whiteSpace: "nowrap",
+                width: "100%",
+              }}
+            />
+          </Space>
         }
         action={
           <Button type="primary" onClick={onEdit}>

+ 1 - 1
dashboard-v6/src/components/channel/ChapterInChannelList.tsx

@@ -131,7 +131,7 @@ const ChapterInChannelListWidget = ({ channelId, onSelect }: IWidget) => {
           width: 120,
           valueType: "option",
           render: (_text, row, index) => {
-            let editLink = `/article/chapter/${row.book}-${row.paragraph}?mode=edit`;
+            let editLink = `/workspace/tipitaka/chapter/${row.book}-${row.paragraph}?mode=edit`;
             editLink += channelId ? `&channel=${channelId}` : "";
             return [
               <Dropdown.Button

+ 1 - 3
dashboard-v6/src/components/term/TermList.tsx

@@ -109,9 +109,7 @@ const TermListWidget = ({ studioName, channelId }: IWidget) => {
             ellipsis: true,
             render(_dom, entity) {
               return (
-                <Link to={`/workspace/edit/wiki/${entity.id}`}>
-                  {entity.word}
-                </Link>
+                <Link to={`/workspace/term/${entity.id}`}>{entity.word}</Link>
               );
             },
           },

+ 1 - 1
dashboard-v6/src/components/tipitaka/ChapterHead.tsx

@@ -19,7 +19,7 @@ const ChapterHeadWidget = (prop: IWidgetPaliChapterHeading) => {
     <>
       <Title level={4}>
         <Link
-          to={`/workspace/edit/chapter/${prop.data.book}-${prop.data.para}`}
+          to={`/workspace/tipitaka/chapter/${prop.data.book}-${prop.data.para}`}
         >
           {prop.data.title}
         </Link>

+ 1 - 1
dashboard-v6/src/components/tipitaka/ChapterInChannel.tsx

@@ -62,7 +62,7 @@ const ChapterInChannelWidget = ({
               }
         }
         renderItem={(item, id) => {
-          let url = `/article/chapter/${book}-${para}`;
+          let url = `/workspace/tipitaka/chapter/${book}-${para}`;
           const currMode = searchParams.get("mode");
           url += currMode ? `?mode=${currMode}` : "?mode=read";
           url += item.channel.id ? `&channel=${item.channel.id}` : "";

+ 56 - 0
dashboard-v6/src/features/editor/Chapter.tsx

@@ -0,0 +1,56 @@
+// ─────────────────────────────────────────────
+// Props
+// ─────────────────────────────────────────────
+
+import type { ArticleMode, ArticleType } from "../../api/Article";
+import TypePali, {
+  type ISearchParams,
+} from "../../components/article/TypePali";
+import Editor from "../../components/editor";
+
+export interface ChapterEditorProps {
+  chapterId?: string;
+  mode?: ArticleMode;
+  channelId?: string | null;
+
+  // ── 路由事件回调(由 page 层处理导航)──
+  /** 选择了新的 chapter 时触发 */
+  onSelect?: (id: string) => void;
+  onArticleChange?: (
+    type: ArticleType,
+    id: string,
+    target: string,
+    param?: ISearchParams[]
+  ) => void;
+}
+
+// ─────────────────────────────────────────────
+// Component
+// ─────────────────────────────────────────────
+
+export default function ChapterEditor({
+  chapterId,
+  mode = "read",
+  channelId,
+  onArticleChange,
+}: ChapterEditorProps) {
+  return (
+    <Editor
+      sidebarTitle="recent scan"
+      sidebar={<>recent list</>}
+      articleId={chapterId}
+      channelId={channelId}
+    >
+      {({ expandButton }) => (
+        <TypePali
+          id={chapterId}
+          type="chapter"
+          mode={mode}
+          channelId={channelId}
+          headerExtra={expandButton}
+          onArticleChange={onArticleChange}
+        />
+      )}
+    </Editor>
+  );
+}

+ 54 - 0
dashboard-v6/src/features/editor/Term.tsx

@@ -0,0 +1,54 @@
+// ─────────────────────────────────────────────
+// Props
+// ─────────────────────────────────────────────
+
+import type { ArticleMode } from "../../api/Article";
+import TypeTerm from "../../components/article/TypeTerm";
+import Editor from "../../components/editor";
+
+export interface ArticleEditorProps {
+  termId?: string;
+  anthologyId?: string;
+  /** 来自 query param "anthology"(无 anthologyId 路由参数时的备用) */
+  anthology?: string | null;
+  mode?: ArticleMode;
+  channelId?: string | null;
+
+  // ── 路由事件回调(由 page 层处理导航)──
+  /** 选择了新的 term 时触发 */
+  onTermSelect?: (id: string) => void;
+
+  onEdit?: () => void;
+}
+
+// ─────────────────────────────────────────────
+// Component
+// ─────────────────────────────────────────────
+
+export default function TermEditor({
+  termId,
+  anthologyId,
+  mode = "read",
+  channelId,
+  onEdit,
+}: ArticleEditorProps) {
+  return (
+    <Editor
+      sidebarTitle="recent scan"
+      sidebar={<>recent list</>}
+      articleId={termId}
+      anthologyId={anthologyId}
+      channelId={channelId}
+    >
+      {({ expandButton }) => (
+        <TypeTerm
+          id={termId}
+          mode={mode}
+          channelId={channelId}
+          headerExtra={expandButton}
+          onEdit={onEdit}
+        />
+      )}
+    </Editor>
+  );
+}

+ 0 - 26
dashboard-v6/src/features/tipitaka/ChapterPage.tsx

@@ -1,26 +0,0 @@
-import type { ArticleMode } from "../../api/Article";
-import TypePali from "../../components/article/TypePali";
-import SplitLayout from "../../components/general/SplitLayout";
-
-interface IWidget {
-  id?: string;
-  mode?: ArticleMode | null;
-  channelId?: string | null;
-}
-const Chapter = ({ id, mode, channelId }: IWidget) => {
-  return (
-    <SplitLayout key="mode-a" sidebarTitle="mint / deploy" sidebar={<></>}>
-      {({ expandButton }) => (
-        <TypePali
-          id={id}
-          type="chapter"
-          mode={mode}
-          channelId={channelId}
-          headerExtra={expandButton}
-        />
-      )}
-    </SplitLayout>
-  );
-};
-
-export default Chapter;

+ 0 - 11
dashboard-v6/src/pages/workspace/editor/chapter.tsx

@@ -1,11 +0,0 @@
-import { useParams } from "react-router";
-
-import ChapterPage from "../../../features/tipitaka/ChapterPage";
-
-const Widget = () => {
-  const { id } = useParams();
-  console.log("chapter", id);
-  return <ChapterPage id={id} />;
-};
-
-export default Widget;

+ 4 - 4
dashboard-v6/src/pages/workspace/term/show.tsx

@@ -1,16 +1,16 @@
 import { useNavigate, useParams } from "react-router";
 
-import TypeTerm from "../../../components/article/TypeTerm";
+import TermEditor from "../../../features/editor/Term";
 
 const Widget = () => {
   const { id } = useParams();
   const navigate = useNavigate();
 
   return (
-    <TypeTerm
-      id={id}
+    <TermEditor
+      termId={id}
       onEdit={() => {
-        navigate(`/workspace/edit/wiki/${id}/edit`);
+        navigate(`/workspace/term/${id}/edit`);
       }}
     />
   );

+ 5 - 3
dashboard-v6/src/pages/workspace/tipitaka/bypath.tsx

@@ -65,7 +65,7 @@ const Widget = () => {
   // 3. 处理纯粹的副作用:重定向
   useEffect(() => {
     if (typeof root === "undefined") {
-      navigate("/workspace/tipitaka/" + bookRoot, { replace: true });
+      navigate("/workspace/tipitaka/lib/" + bookRoot, { replace: true });
     } else {
       localStorage.setItem("pali_path_root", root);
     }
@@ -90,7 +90,9 @@ const Widget = () => {
                     onChange={(_, newPath) => {
                       // 只需要改变 URL,剩下的交给 useMemo 自动计算
                       const pathStr = newPath?.join("_").toLowerCase();
-                      navigate(`/workspace/tipitaka/${bookRoot}/${pathStr}`);
+                      navigate(
+                        `/workspace/tipitaka/lib/${bookRoot}/${pathStr}`
+                      );
                     }}
                   />
                 </div>
@@ -103,7 +105,7 @@ const Widget = () => {
                 path={bookPath}
                 onChange={(e: IEventBookTreeOnchange) => {
                   navigate(
-                    `/workspace/tipitaka/${bookRoot}/${e.path.join("_")}`
+                    `/workspace/tipitaka/lib/${bookRoot}/${e.path.join("_")}`
                   );
                 }}
                 onTocLoad={setTocData} // 直接设置,简化写法

+ 36 - 0
dashboard-v6/src/pages/workspace/tipitaka/chapter.tsx

@@ -0,0 +1,36 @@
+import { useNavigate, useParams, useSearchParams } from "react-router";
+import type { ArticleMode } from "../../../api/Article";
+import ChapterEditor from "../../../features/editor/Chapter";
+
+const Widget = () => {
+  const { id } = useParams();
+  const [searchParams] = useSearchParams();
+  const navigate = useNavigate();
+
+  const mode = searchParams.get("mode") ?? "read";
+  const channelId = searchParams.get("channel");
+
+  return (
+    <ChapterEditor
+      chapterId={id}
+      mode={mode as ArticleMode}
+      channelId={channelId}
+      onSelect={(id) => {
+        navigate(`/workspace/tipitaka/chapter/${id}`);
+      }}
+      onArticleChange={(type, id, target) => {
+        const url = `workspace/tipitaka/${type}/${id}`;
+        if (target === "_blank") {
+          window.open(
+            `${window.location.origin}${import.meta.env.BASE_URL}${url}`,
+            "_blank"
+          );
+        } else {
+          navigate(`/${url}`);
+        }
+      }}
+    />
+  );
+};
+
+export default Widget;