visuddhinanda 1 неделя назад
Родитель
Сommit
06930a5562

+ 2 - 11
dashboard-v4/dashboard/src/components/article/TypeCourse.tsx

@@ -65,22 +65,13 @@ interface IWidget {
   onLoading?: Function;
   onError?: Function;
 }
-const TypeCourseWidget = ({
+const TypeCourse = ({
   type,
-  book,
-  para,
   channelId,
   articleId,
   courseId,
-  exerciseId,
-  userName,
   mode = "read",
-  active = false,
   onArticleChange,
-  onFinal,
-  onLoad,
-  onLoading,
-  onError,
 }: IWidget) => {
   const intl = useIntl();
   const [anthologyId, setAnthologyId] = useState<string>();
@@ -252,4 +243,4 @@ const TypeCourseWidget = ({
   );
 };
 
-export default TypeCourseWidget;
+export default TypeCourse;

+ 1 - 1
dashboard-v6/documents/development/v6-todo-list.md

@@ -50,7 +50,7 @@
 
 - [x] "anthology"
 - [x] "article"
-- [ ] "textbook"
+- [x] "textbook"
 - [x] "term"
 - [x] "chapter"
 - [x] "para"

+ 3 - 0
dashboard-v6/src/components/anthology/AnthologyTocTree.tsx

@@ -8,12 +8,14 @@ import type { TTarget } from "../../types";
 
 interface IWidget {
   anthologyId?: string;
+  articleId?: string;
   channels?: string[];
   onClick?: (anthologyId: string, id: string, target?: TTarget) => void;
   onArticleSelect?: (anthologyId: string, keys: string[]) => void;
 }
 const AnthologyTocTreeWidget = ({
   anthologyId,
+  articleId,
   channels,
   onClick,
   onArticleSelect,
@@ -57,6 +59,7 @@ const AnthologyTocTreeWidget = ({
     <TocTree
       treeData={tocData}
       expandedKeys={expandedKeys}
+      selectedKeys={[articleId ?? ""]}
       onSelect={(keys: string[]) => {
         if (
           typeof onArticleSelect !== "undefined" &&

+ 232 - 0
dashboard-v6/src/components/article/TypeCourse.tsx

@@ -0,0 +1,232 @@
+import { useEffect, useState } from "react";
+
+import { get } from "../../request";
+import store from "../../store";
+
+import { signIn } from "../../reducers/course-user";
+import {
+  memberRefresh,
+  refresh,
+  type ITextbook,
+} from "../../reducers/current-course";
+
+import "./article.css";
+
+import TypeArticle from "./TypeArticle";
+import { Link, useNavigate, useSearchParams } from "react-router";
+
+import SelectChannel from "../course/SelectChannel";
+import { Space, Tag, Typography } from "antd";
+import { useIntl } from "react-intl";
+import type { ArticleMode, ArticleType } from "../../api/article";
+import type { ISearchParams } from "./TypePali";
+import {
+  fetchCourse,
+  type ICourseCurrUserResponse,
+  type ICourseDataResponse,
+  type ICourseMemberListResponse,
+  type ICourseUser,
+} from "../../api/course";
+import type { TTarget } from "../../types";
+
+const { Text } = Typography;
+
+/**
+ * 每种article type 对应的路由参数
+ * article/id?anthology=id&channel=id1,id2&mode=ArticleMode
+ * chapter/book-para?channel=id1,id2&mode=ArticleMode
+ * para/book?par=para1,para2&channel=id1,id2&mode=ArticleMode
+ * cs-para/book-para?channel=id1,id2&mode=ArticleMode
+ * sent/id?channel=id1,id2&mode=ArticleMode
+ * sim/id?channel=id1,id2&mode=ArticleMode
+ * textbook/articleId?course=id&mode=ArticleMode
+ * exercise/articleId?course=id&exercise=id&username=name&mode=ArticleMode
+ * exercise-list/articleId?course=id&exercise=id&mode=ArticleMode
+ * sent-original/id
+ */
+interface IWidget {
+  articleId?: string;
+  mode?: ArticleMode | null;
+  channelId?: string | null;
+  headerExtra?: React.ReactNode;
+  courseId?: string | null;
+  userName?: string;
+  onArticleChange?: (
+    type: ArticleType,
+    id: string,
+    target: string,
+    param?: ISearchParams[]
+  ) => void;
+}
+const TypeCourseWidget = ({
+  channelId,
+  articleId,
+  courseId,
+  headerExtra,
+  mode = "read",
+  onArticleChange,
+}: IWidget) => {
+  const intl = useIntl();
+  const [anthologyId, setAnthologyId] = useState<string>();
+  const [course, setCourse] = useState<ICourseDataResponse>();
+  const [currUser, setCurrUser] = useState<ICourseUser>();
+  const [searchParams, setSearchParams] = useSearchParams();
+  const [channelPickerOpen, setChannelPickerOpen] = useState(false);
+  const navigate = useNavigate();
+
+  useEffect(() => {
+    /**
+     * 由课本进入查询当前用户的权限和channel
+     */
+    console.debug("course 由课本进入查询当前用户的权限和channel", courseId);
+    if (typeof courseId !== "undefined") {
+      const url = `/api/v2/course-curr?course_id=${courseId}`;
+      console.info("course url", url);
+      get<ICourseCurrUserResponse>(url).then((response) => {
+        console.log("course user", response);
+        if (response.ok) {
+          setCurrUser(response.data);
+          if (!response.data.channel_id) {
+            setChannelPickerOpen(true);
+          }
+          //我的角色
+          store.dispatch(
+            signIn({
+              channelId: response.data.channel_id,
+              role: response.data.role,
+            })
+          );
+
+          //如果是老师查询学生列表
+          if (response.data.role) {
+            if (response.data.role !== "student") {
+              const url = `/api/v2/course-member?view=course&id=${courseId}`;
+              console.debug("course member url", url);
+              get<ICourseMemberListResponse>(url)
+                .then((json) => {
+                  console.debug("course member data", json);
+                  if (json.ok) {
+                    store.dispatch(memberRefresh(json.data.rows));
+                  }
+                })
+                .catch((e) => console.error(e));
+            }
+          }
+        } else {
+          console.error(response.message);
+        }
+      });
+    }
+  }, [courseId]);
+
+  useEffect(() => {
+    const output: Record<string, string> = { mode: mode as string };
+    searchParams.forEach((value, key) => {
+      console.log(value, key);
+      if (key !== "mode" && key !== "channel") {
+        output[key] = value;
+      }
+    });
+    if (currUser?.role === "student") {
+      if (typeof currUser.channel_id === "string") {
+        output["channel"] = currUser.channel_id;
+      }
+    } else if (course?.channel_id) {
+      output["channel"] = course?.channel_id;
+    }
+
+    setSearchParams(output);
+  }, [currUser, course, mode, searchParams, setSearchParams]);
+
+  useEffect(() => {
+    if (!courseId) {
+      return;
+    }
+    fetchCourse(courseId).then((json) => {
+      console.debug("course data", json.data);
+      if (json.ok) {
+        setAnthologyId(json.data.anthology_id);
+        setCourse(json.data);
+        /**
+         * redux发布课程信息
+         */
+        if (courseId && articleId) {
+          const ic: ITextbook = {
+            course: json.data,
+            courseId: courseId,
+            articleId: articleId,
+            channelId: json.data.channel_id,
+          };
+          store.dispatch(refresh(ic));
+        }
+      }
+    });
+  }, [articleId, courseId]);
+
+  let channelsId = "";
+  if (currUser && course) {
+    if (currUser.role === "student") {
+      if (currUser.channel_id) {
+        channelsId = currUser.channel_id + "_" + course?.channel_id;
+      } else {
+        channelsId = course?.channel_id;
+      }
+    } else {
+      channelsId = course?.channel_id;
+    }
+  }
+
+  return anthologyId && currUser ? (
+    <>
+      {!currUser.channel_id && currUser.role === "student" ? (
+        <SelectChannel
+          courseId={courseId}
+          open={channelPickerOpen}
+          onOpenChange={(visible: boolean) => {
+            setChannelPickerOpen(visible);
+          }}
+          onSelected={() => {
+            window.location.reload();
+          }}
+        />
+      ) : (
+        <></>
+      )}
+      {headerExtra}
+      <Space>
+        <Text>
+          {"课程:"}
+          <Link to={`/course/show/${course?.id}`} target="_blank">
+            {course?.title}
+          </Link>
+        </Text>
+        <Tag>{intl.formatMessage({ id: `auth.role.${currUser.role}` })}</Tag>
+      </Space>
+      <TypeArticle
+        articleId={articleId}
+        channelId={channelsId}
+        mode={mode}
+        anthologyId={anthologyId}
+        active={true}
+        onArticleChange={(type: ArticleType, id: string, target?: TTarget) => {
+          if (type === "article" && courseId && channelId) {
+            if (typeof onArticleChange !== "undefined") {
+              const param: ISearchParams[] = [
+                { key: "course", value: courseId },
+                { key: "channel", value: channelId },
+              ];
+
+              onArticleChange("textbook", id, target ?? "_blank", param);
+            }
+          } else {
+            navigate(`/course/show/${courseId}`);
+          }
+        }}
+      />
+    </>
+  ) : (
+    <>loading</>
+  );
+};
+
+export default TypeCourseWidget;

+ 1 - 1
dashboard-v6/src/components/course/TextBook.tsx

@@ -21,7 +21,7 @@ const TextBookWidget = ({ anthologyId, courseId }: IWidget) => {
           <AnthologyDetail
             aid={anthologyId}
             onArticleClick={(_, articleId: string, target?: TTarget) => {
-              const url = `/article/textbook/${articleId}?mode=read&course=${courseId}`;
+              const url = `/workspace/course/${courseId}/textbook/${articleId}?mode=read&course=${courseId}`;
               if (target === "_blank") {
                 window.open(fullUrl(url), "_blank");
               } else {

+ 80 - 0
dashboard-v6/src/features/editor/TextBook.tsx

@@ -0,0 +1,80 @@
+// ─────────────────────────────────────────────
+// Props
+// ─────────────────────────────────────────────
+
+import type { ArticleMode } from "../../api/article";
+import AnthologyTocTree from "../../components/anthology/AnthologyTocTree";
+import TypeCourse from "../../components/article/TypeCourse";
+import { useCourse } from "../../components/course/hooks/useCourse";
+import Editor from "../../components/editor";
+
+export interface ArticleEditorProps {
+  articleId?: string;
+  courseId?: string;
+  mode?: ArticleMode;
+  channelId?: string | null;
+
+  // ── 路由事件回调(由 page 层处理导航)──
+  /** 点击目录树中的文章时触发 */
+  onArticleClick?: (
+    anthologyId: string,
+    articleId: string,
+    target?: string
+  ) => void;
+  /** 选择了新的 anthology 时触发 */
+  onAnthologySelect?: (anthologyId: string) => void;
+  /** 文章内部触发跳转(type: 'article' | 'anthology' 等) */
+  onArticleChange?: (type: string, id: string) => void;
+}
+
+// ─────────────────────────────────────────────
+// Component
+// ─────────────────────────────────────────────
+
+export default function TextBookEditor({
+  articleId,
+  courseId,
+  mode = "read",
+  channelId,
+  onArticleClick,
+  onArticleChange,
+}: ArticleEditorProps) {
+  const channels = channelId ? channelId.split("_") : undefined;
+  const { data, loading, errorCode } = useCourse(courseId);
+
+  console.error(errorCode);
+
+  return (
+    <Editor
+      sidebarTitle="table of content"
+      sidebar={
+        loading ? (
+          <>loading</>
+        ) : (
+          <AnthologyTocTree
+            anthologyId={data?.anthology_id}
+            articleId={articleId}
+            channels={channels}
+            onClick={(tocAnthology, article, target) => {
+              onArticleClick?.(tocAnthology, article, target);
+            }}
+          />
+        )
+      }
+      articleId={articleId}
+      anthologyId={data?.anthology_id}
+      channelId={channelId}
+    >
+      {({ expandButton }) => (
+        <TypeCourse
+          articleId={articleId}
+          mode={mode}
+          courseId={courseId}
+          channelId={channelId}
+          headerExtra={expandButton}
+          onArticleChange={onArticleChange}
+        />
+      )}
+    </Editor>
+  );
+}

+ 12 - 0
dashboard-v6/src/pages/workspace/course/textbook.tsx

@@ -0,0 +1,12 @@
+//课程详情页面
+import { useParams } from "react-router";
+
+import TextBookEditor from "../../../features/editor/TextBook";
+
+const Widget = () => {
+  const { courseId, articleId } = useParams(); //url 参数
+
+  return <TextBookEditor courseId={courseId} articleId={articleId} />;
+};
+
+export default Widget;

+ 13 - 0
dashboard-v6/src/routes/courseRoutes.ts

@@ -11,6 +11,9 @@ const WorkspaceCourseShow = lazy(
 const WorkspaceCourseSetting = lazy(
   () => import("../pages/workspace/course/edit")
 );
+const WorkspaceCourseTextbook = lazy(
+  () => import("../pages/workspace/course/textbook")
+);
 
 const courseRoutes: RouteObject[] = [
   {
@@ -37,6 +40,16 @@ const courseRoutes: RouteObject[] = [
             Component: WorkspaceCourseSetting,
             handle: { id: "workspace.course.setting", crumb: "setting" },
           },
+          {
+            path: "textbook",
+            handle: { id: "workspace.course.textbook", crumb: "textbook" },
+            children: [
+              {
+                path: ":articleId",
+                children: [{ index: true, Component: WorkspaceCourseTextbook }],
+              },
+            ],
+          },
         ],
       },
     ],