Browse Source

Merge pull request #1056 from visuddhinanda/agile

课程模块前后端连接完成
visuddhinanda 3 years ago
parent
commit
fc32d7c7aa
54 changed files with 1776 additions and 1262 deletions
  1. 2 2
      dashboard/src/Router.tsx
  2. 5 4
      dashboard/src/components/api/Article.ts
  3. 2 2
      dashboard/src/components/api/Attachments.ts
  4. 72 36
      dashboard/src/components/api/Course.ts
  5. 1 0
      dashboard/src/components/api/Dict.ts
  6. 2 3
      dashboard/src/components/article/AnthologStudioList.tsx
  7. 17 11
      dashboard/src/components/article/AnthologyDetail.tsx
  8. 16 1
      dashboard/src/components/article/Article.tsx
  9. 4 2
      dashboard/src/components/article/ArticleView.tsx
  10. 23 0
      dashboard/src/components/auth/UserName.tsx
  11. 61 51
      dashboard/src/components/blog/BlogNav.tsx
  12. 2 2
      dashboard/src/components/course/AddLesson.tsx
  13. 115 0
      dashboard/src/components/course/AddMember.tsx
  14. 7 11
      dashboard/src/components/course/AddStudent.tsx
  15. 3 7
      dashboard/src/components/course/AddTeacher.tsx
  16. 21 9
      dashboard/src/components/course/CourseCreate.tsx
  17. 24 0
      dashboard/src/components/course/CourseIntro.tsx
  18. 71 0
      dashboard/src/components/course/CourseList.tsx
  19. 180 0
      dashboard/src/components/course/CourseMember.tsx
  20. 66 0
      dashboard/src/components/course/CourseShow.tsx
  21. 72 0
      dashboard/src/components/course/LecturerList.tsx
  22. 0 0
      dashboard/src/components/course/LessonSelect.tsx
  23. 0 0
      dashboard/src/components/course/LessonTreeShow.tsx
  24. 8 11
      dashboard/src/components/course/StudentsSelect.tsx
  25. 0 0
      dashboard/src/components/course/TeacherSelect.tsx
  26. 31 0
      dashboard/src/components/course/TextBook.tsx
  27. 0 0
      dashboard/src/components/course/UploadTexture.tsx
  28. 1 1
      dashboard/src/components/group/GroupMember.tsx
  29. 0 51
      dashboard/src/components/library/course/CourseIntro.tsx
  30. 0 62
      dashboard/src/components/library/course/CourseList.tsx
  31. 0 45
      dashboard/src/components/library/course/CourseShow.tsx
  32. 0 116
      dashboard/src/components/library/course/LecturerList.tsx
  33. 136 137
      dashboard/src/components/studio/LeftSider.tsx
  34. 67 67
      dashboard/src/components/studio/table.ts
  35. 6 1
      dashboard/src/components/template/Wbw/WbwFactorMeaning.tsx
  36. 13 2
      dashboard/src/components/template/Wbw/WbwMeaning.tsx
  37. 137 103
      dashboard/src/components/template/Wbw/WbwMeaningSelect.tsx
  38. 3 0
      dashboard/src/components/template/Wbw/WbwWord.tsx
  39. 5 1
      dashboard/src/components/template/utilities.ts
  40. 1 1
      dashboard/src/locales/zh-Hans/dict/index.ts
  41. 2 1
      dashboard/src/locales/zh-Hans/forms.ts
  42. 2 3
      dashboard/src/pages/library/anthology/show.tsx
  43. 7 7
      dashboard/src/pages/library/blog/course.tsx
  44. 14 14
      dashboard/src/pages/library/community/index.tsx
  45. 27 27
      dashboard/src/pages/library/community/list.tsx
  46. 19 23
      dashboard/src/pages/library/community/recent.tsx
  47. 60 71
      dashboard/src/pages/library/course/course.tsx
  48. 4 9
      dashboard/src/pages/library/course/lesson.tsx
  49. 24 48
      dashboard/src/pages/library/course/list.tsx
  50. 2 2
      dashboard/src/pages/studio/article/edit.tsx
  51. 0 3
      dashboard/src/pages/studio/channel/edit.tsx
  52. 312 208
      dashboard/src/pages/studio/course/edit.tsx
  53. 102 98
      dashboard/src/pages/studio/course/list.tsx
  54. 27 9
      dashboard/src/pages/studio/course/show.tsx

+ 2 - 2
dashboard/src/Router.tsx

@@ -27,7 +27,7 @@ import LibraryCourse from "./pages/library/course";
 import LibraryCourseList from "./pages/library/course/list";
 import LibraryCourseShow from "./pages/library/course/course";
 import LibraryLessonShow from "./pages/library/course/lesson";
-//import LibraryCourseManage from "./pages/library/course/courseManage";
+
 import LibraryTerm from "./pages/library/term/show";
 import LibraryDict from "./pages/library/dict";
 import LibraryDictShow from "./pages/library/dict/show";
@@ -135,7 +135,7 @@ const Widget = () => {
       </Route>
       <Route path="course" element={<LibraryCourse />}>
         <Route path="list" element={<LibraryCourseList />}></Route>
-        <Route path="show" element={<LibraryCourseShow />}></Route>
+        <Route path="show/:id" element={<LibraryCourseShow />}></Route>
         <Route path="lesson" element={<LibraryLessonShow />}></Route>
       </Route>
 

+ 5 - 4
dashboard/src/components/api/Article.ts

@@ -60,8 +60,8 @@ export interface IArticleDataRequest {
   title: string;
   subtitle: string;
   summary: string;
-  content: string;
-  content_type: string;
+  content?: string;
+  content_type?: string;
   status: number;
   lang: string;
 }
@@ -70,8 +70,9 @@ export interface IArticleDataResponse {
   title: string;
   subtitle: string;
   summary: string;
-  content: string;
-  content_type: string;
+  content?: string;
+  content_type?: string;
+  html?: string;
   path?: ITocPathNode[];
   status: number;
   lang: string;

+ 2 - 2
dashboard/src/components/api/Attachments.ts

@@ -8,8 +8,8 @@ export interface IAttachmentRequest {
   uid: string;
   name?: string;
   size?: number;
-  type?: string;
-  url?: string;
+  type: string;
+  url: string;
 }
 export interface IAttachmentResponse {
   ok: boolean;

+ 72 - 36
dashboard/src/components/api/Course.ts

@@ -1,5 +1,6 @@
-import { ITocPathNode } from "../corpus/TocPath";
-import type { IStudioApiResponse } from "./Auth";
+import { IStudio } from "../auth/StudioName";
+import { IUser } from "../auth/User";
+import { IUserRequest, Role } from "./Auth";
 
 export interface ICourseListApiResponse {
   article: string;
@@ -9,42 +10,38 @@ export interface ICourseListApiResponse {
 }
 
 export interface ICourseDataRequest {
-  uid: string;//课程ID
-  title: string;//标题
-  subtitle: string;//副标题
-  teacher: number;//UserID
-  course_count: number;//课程数
-  //content: string;
-  //content_type: string;
-  //path?: ITocPathNode[];
-  type: number;//类型-公开/内部
-  //lang: string;
-  created_at: string;//创建时间
-  updated_at: string;//修改时间
-  article_id: number;//文集ID
-  course_start_at: string;//课程开始时间
-  course_end_at: string;//课程结束时间
-  intro_markdown: string;//简介
-  cover_img_name: string;//封面图片文件名
+  id?: string; //课程ID
+  title: string; //标题
+  subtitle?: string; //副标题
+  content?: string;
+  cover?: string; //封面图片文件名
+  teacher_id?: string; //UserID
+  publicity: number; //类型-公开/内部
+  anthology_id?: string; //文集ID
+  channel_id?: string; //标准答案channel
+  start_at?: string; //课程开始时间
+  end_at?: string; //课程结束时间
 }
 export interface ICourseDataResponse {
-  uid: string;//课程ID
-  title: string;//标题
-  subtitle: string;//副标题
-  teacher: number;//UserID
-  course_count: number;//课程数
-  //content: string;
-  //content_type: string;
-  //path?: ITocPathNode[];
-  type: number;//类型-公开/内部
-  //lang: string;
-  created_at: string;//创建时间
-  updated_at: string;//修改时间
-  article_id: number;//文集ID
-  course_start_at: string;//课程开始时间
-  course_end_at: string;//课程结束时间
-  intro_markdown: string;//简介
-  cover_img_name: string;//封面图片文件名
+  id: string; //课程ID
+  title: string; //标题
+  subtitle: string; //副标题
+  teacher?: IUser; //UserID
+  course_count?: number; //课程数
+  publicity: number; //类型-公开/内部
+  anthology_id?: string; //文集ID
+  anthology_title?: string; //文集标题
+  anthology_owner?: IStudio; //文集拥有者
+  channel_id: string; //标准答案ID
+  channel_name?: string; //文集标题
+  channel_owner?: IStudio; //文集拥有者
+  start_at: string; //课程开始时间
+  end_at: string; //课程结束时间
+  content: string; //简介
+  cover: string; //封面图片文件名
+  member_count: number;
+  created_at: string; //创建时间
+  updated_at: string; //修改时间
 }
 export interface ICourseResponse {
   ok: boolean;
@@ -71,3 +68,42 @@ export interface IAnthologyCreateRequest {
   lang: string;
   studio: string;
 }
+export interface ICourseNumberResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    create: number;
+    teach: number;
+    study: number;
+  };
+}
+
+export interface ICourseMemberData {
+  id?: number;
+  user_id: string;
+  course_id: string;
+  role?: string;
+  user?: IUserRequest;
+  created_at?: string;
+  updated_at?: string;
+}
+export interface ICourseMemberResponse {
+  ok: boolean;
+  message: string;
+  data: ICourseMemberData;
+}
+export interface ICourseMemberListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: ICourseMemberData[];
+    role: Role;
+    count: number;
+  };
+}
+
+export interface ICourseMemberDeleteResponse {
+  ok: boolean;
+  message: string;
+  data: boolean;
+}

+ 1 - 0
dashboard/src/components/api/Dict.ts

@@ -26,6 +26,7 @@ export interface IApiResponseDictData {
   language: string;
   dict_id: string;
   dict_name?: string;
+  dict_shortname?: string;
   confidence: number;
   creator_id: number;
   updated_at: string;

+ 2 - 3
dashboard/src/components/article/AnthologStudioList.tsx

@@ -25,9 +25,8 @@ const Widget = () => {
 
   function fetchData() {
     let url = `/v2/anthology?view=studio_list`;
-    get(url).then(function (myJson) {
-      console.log("ajex", myJson);
-      const json = myJson as unknown as IAnthologyStudioListApiResponse;
+    get<IAnthologyStudioListApiResponse>(url).then(function (json) {
+      console.log("ajex", json);
       let newTree: IAnthologyStudioData[] = json.data.rows.map((item) => {
         return {
           count: item.count,

+ 17 - 11
dashboard/src/components/article/AnthologyDetail.tsx

@@ -12,11 +12,6 @@ import TocTree from "./TocTree";
 
 const { Title, Text } = Typography;
 
-interface IWidgetAnthologyDetail {
-  aid: string;
-  channels?: string[];
-}
-
 const defaultData: IAnthologyData = {
   id: "",
   title: "",
@@ -32,16 +27,20 @@ const defaultData: IAnthologyData = {
   created_at: "",
   updated_at: "",
 };
-
-const Widget = (prop: IWidgetAnthologyDetail) => {
+interface IWidgetAnthologyDetail {
+  aid?: string;
+  channels?: string[];
+  onArticleSelect?: Function;
+}
+const Widget = ({ aid, channels, onArticleSelect }: IWidgetAnthologyDetail) => {
   const [tableData, setTableData] = useState(defaultData);
 
   useEffect(() => {
     console.log("useEffect");
-    fetchData(prop.aid);
-  }, [prop.aid]);
+    fetchData(aid);
+  }, [aid]);
 
-  function fetchData(id: string) {
+  function fetchData(id?: string) {
     get<IAnthologyResponse>(`/v2/anthology/${id}`)
       .then((response) => {
         const item: IAnthologyDataResponse = response.data;
@@ -79,7 +78,14 @@ const Widget = (prop: IWidgetAnthologyDetail) => {
       </div>
       <Title level={5}>目录</Title>
 
-      <TocTree treeData={tableData.articles} />
+      <TocTree
+        treeData={tableData.articles}
+        onSelect={(keys: string[]) => {
+          if (typeof onArticleSelect !== "undefined") {
+            onArticleSelect(keys);
+          }
+        }}
+      />
     </>
   );
 };

+ 16 - 1
dashboard/src/components/article/Article.tsx

@@ -54,7 +54,20 @@ const Widget = ({
     }
 
     if (typeof type !== "undefined" && typeof articleId !== "undefined") {
-      get<IArticleResponse>(`/v2/${type}/${articleId}/${mode}`).then((json) => {
+      let url = "";
+      switch (type) {
+        case "corpus/article":
+          url = `/v2/article/${articleId}?mode=${mode}`;
+          break;
+        case "corpus/textbook":
+          url = `/v2/article/${mode}?mode=read`;
+          break;
+        default:
+          url = `/v2/${type}/${articleId}/${mode}`;
+          break;
+      }
+      get<IArticleResponse>(url).then((json) => {
+        console.log("article", json);
         if (json.ok) {
           setArticleData(json.data);
         } else {
@@ -63,6 +76,7 @@ const Widget = ({
       });
     }
   }, [active, type, articleId, mode, articleMode]);
+
   return (
     <ArticleView
       id={articleData?.uid}
@@ -70,6 +84,7 @@ const Widget = ({
       subTitle={articleData?.subtitle}
       summary={articleData?.summary}
       content={articleData ? articleData.content : ""}
+      html={articleData?.html}
       path={articleData?.path}
       created_at={articleData?.created_at}
       updated_at={articleData?.updated_at}

+ 4 - 2
dashboard/src/components/article/ArticleView.tsx

@@ -11,7 +11,8 @@ export interface IWidgetArticleData {
   title?: string;
   subTitle?: string;
   summary?: string;
-  content: string;
+  content?: string;
+  html?: string;
   path?: ITocPathNode[];
   created_at?: string;
   updated_at?: string;
@@ -24,6 +25,7 @@ const Widget = ({
   subTitle,
   summary,
   content,
+  html,
   path = [],
   created_at,
   updated_at,
@@ -54,7 +56,7 @@ const Widget = ({
         <Divider />
       </div>
       <div>
-        <MdView html={content} />
+        <MdView html={html ? html : content} />
       </div>
     </>
   );

+ 23 - 0
dashboard/src/components/auth/UserName.tsx

@@ -0,0 +1,23 @@
+import { Avatar } from "antd";
+
+export interface IUser {
+  id?: string;
+  nickName?: string;
+  realName?: string;
+  onClick?: Function;
+}
+const Widget = ({ id, nickName, realName, onClick }: IUser) => {
+  return (
+    <span
+      onClick={(e) => {
+        if (typeof onClick !== "undefined") {
+          onClick(e);
+        }
+      }}
+    >
+      {nickName}
+    </span>
+  );
+};
+
+export default Widget;

+ 61 - 51
dashboard/src/components/blog/BlogNav.tsx

@@ -5,59 +5,69 @@ import type { MenuProps } from "antd";
 import { Menu, Row, Col } from "antd";
 
 interface IWidgetBlogNav {
-	selectedKey: string;
-	studio: string;
+  selectedKey: string;
+  studio?: string;
 }
-const Widget = (prop: IWidgetBlogNav) => {
-	//Library head bar
-	const intl = useIntl(); //i18n
-	// TODO
+const Widget = ({ selectedKey, studio }: IWidgetBlogNav) => {
+  //Library head bar
+  const intl = useIntl(); //i18n
+  // TODO
 
-	const items: MenuProps["items"] = [
-		{
-			label: <Link to={`/blog/${prop.studio}/overview`}>{intl.formatMessage({ id: "blog.overview" })}</Link>,
-			key: "overview",
-			icon: <MailOutlined />,
-		},
-		{
-			label: <Link to={`/blog/${prop.studio}/palicanon`}>{intl.formatMessage({ id: "blog.palicanon" })}</Link>,
-			key: "palicanon",
-			icon: <MailOutlined />,
-		},
-		{
-			label: (
-				<Link to={`/blog/${prop.studio}/course`}>
-					{intl.formatMessage({ id: "columns.library.course.title" })}
-				</Link>
-			),
-			key: "course",
-			icon: <MailOutlined />,
-		},
-		{
-			label: (
-				<Link to={`/blog/${prop.studio}/anthology`}>
-					{intl.formatMessage({ id: "columns.library.anthology.title" })}
-				</Link>
-			),
-			key: "anthology",
-			icon: <MailOutlined />,
-		},
-		{
-			label: (
-				<Link to={`/blog/${prop.studio}/term`}>{intl.formatMessage({ id: "columns.library.term.title" })}</Link>
-			),
-			key: "term",
-			icon: <MailOutlined />,
-		},
-	];
-	return (
-		<Row>
-			<Col flex="300px"></Col>
+  const items: MenuProps["items"] = [
+    {
+      label: (
+        <Link to={`/blog/${studio}/overview`}>
+          {intl.formatMessage({ id: "blog.overview" })}
+        </Link>
+      ),
+      key: "overview",
+      icon: <MailOutlined />,
+    },
+    {
+      label: (
+        <Link to={`/blog/${studio}/palicanon`}>
+          {intl.formatMessage({ id: "blog.palicanon" })}
+        </Link>
+      ),
+      key: "palicanon",
+      icon: <MailOutlined />,
+    },
+    {
+      label: (
+        <Link to={`/blog/${studio}/course`}>
+          {intl.formatMessage({ id: "columns.library.course.title" })}
+        </Link>
+      ),
+      key: "course",
+      icon: <MailOutlined />,
+    },
+    {
+      label: (
+        <Link to={`/blog/${studio}/anthology`}>
+          {intl.formatMessage({ id: "columns.library.anthology.title" })}
+        </Link>
+      ),
+      key: "anthology",
+      icon: <MailOutlined />,
+    },
+    {
+      label: (
+        <Link to={`/blog/${studio}/term`}>
+          {intl.formatMessage({ id: "columns.library.term.title" })}
+        </Link>
+      ),
+      key: "term",
+      icon: <MailOutlined />,
+    },
+  ];
+  return (
+    <Row>
+      <Col flex="300px"></Col>
 
-			<Col flex="auto">
-				<Menu selectedKeys={[prop.selectedKey]} mode="horizontal" items={items} />
-			</Col>
-		</Row>
-	);
+      <Col flex="auto">
+        <Menu selectedKeys={[selectedKey]} mode="horizontal" items={items} />
+      </Col>
+    </Row>
+  );
 };
 export default Widget;

+ 2 - 2
dashboard/src/components/library/course/AddLesson.tsx → dashboard/src/components/course/AddLesson.tsx

@@ -6,9 +6,9 @@ import {
 } from "@ant-design/pro-components";
 import { Button, message, Popover } from "antd";
 import { UserAddOutlined } from "@ant-design/icons";
-import { get } from "../../../request";
+import { get } from "../../request";
 
-import { IUserListResponse } from "../../api/Auth";
+import { IUserListResponse } from "../api/Auth";
 
 interface IFormData {
   userId: string;

+ 115 - 0
dashboard/src/components/course/AddMember.tsx

@@ -0,0 +1,115 @@
+import { useIntl } from "react-intl";
+import { ProForm, ProFormSelect } from "@ant-design/pro-components";
+import { Button, message, Popover } from "antd";
+import { UserAddOutlined } from "@ant-design/icons";
+
+import { get, post } from "../../request";
+import { IUserListResponse } from "../api/Auth";
+import { useState } from "react";
+import { ICourseMemberData, ICourseMemberResponse } from "../api/Course";
+
+interface IFormData {
+  userId: string;
+  role: string;
+}
+
+interface IWidget {
+  courseId?: string;
+  onCreated?: Function;
+}
+const Widget = ({ courseId, onCreated }: IWidget) => {
+  const intl = useIntl();
+  const [open, setOpen] = useState(false);
+
+  const form = (
+    <ProForm<IFormData>
+      onFinish={async (values: IFormData) => {
+        // TODO
+        console.log(values);
+        if (typeof courseId !== "undefined") {
+          post<ICourseMemberData, ICourseMemberResponse>("/v2/course-member", {
+            user_id: values.userId,
+            role: values.role,
+            course_id: courseId,
+          }).then((json) => {
+            console.log("add member", json);
+            if (json.ok) {
+              message.success(intl.formatMessage({ id: "flashes.success" }));
+              setOpen(false);
+              if (typeof onCreated !== "undefined") {
+                onCreated();
+              }
+            }
+          });
+        }
+      }}
+    >
+      <ProForm.Group>
+        <ProFormSelect
+          width="sm"
+          name="userId"
+          label={intl.formatMessage({ id: "forms.fields.user.label" })}
+          showSearch
+          debounceTime={300}
+          request={async ({ keyWords }) => {
+            console.log("keyWord", keyWords);
+            const json = await get<IUserListResponse>(
+              `/v2/user?view=key&key=${keyWords}`
+            );
+            const userList = json.data.rows.map((item) => {
+              return {
+                value: item.id,
+                label: `${item.userName}-${item.nickName}`,
+              };
+            });
+            console.log("json", userList);
+            return userList;
+          }}
+          placeholder={intl.formatMessage({
+            id: "forms.message.user.required",
+          })}
+          rules={[
+            {
+              required: true,
+              message: intl.formatMessage({
+                id: "forms.message.user.required",
+              }),
+            },
+          ]}
+        />
+      </ProForm.Group>
+      <ProForm.Group>
+        <ProFormSelect
+          width="sm"
+          name="role"
+          label={intl.formatMessage({ id: "forms.fields.type.label" })}
+          valueEnum={{
+            student: intl.formatMessage({ id: "forms.fields.student.label" }),
+            assistant: intl.formatMessage({
+              id: "forms.fields.assistant.label",
+            }),
+          }}
+        />
+      </ProForm.Group>
+    </ProForm>
+  );
+  const handleClickChange = (open: boolean) => {
+    setOpen(open);
+  };
+  return (
+    <Popover
+      placement="bottomLeft"
+      arrowPointAtCenter
+      content={form}
+      trigger="click"
+      open={open}
+      onOpenChange={handleClickChange}
+    >
+      <Button icon={<UserAddOutlined />} key="add" type="primary">
+        {intl.formatMessage({ id: "buttons.group.add.member" })}
+      </Button>
+    </Popover>
+  );
+};
+
+export default Widget;

+ 7 - 11
dashboard/src/components/library/course/AddStudent.tsx → dashboard/src/components/course/AddStudent.tsx

@@ -1,24 +1,20 @@
 import { useIntl } from "react-intl";
-import {
-  ProForm,
-  ProFormSelect,
-  ProFormText,
-} from "@ant-design/pro-components";
-import { Button, message, Popover, MenuProps } from "antd";
+import { ProForm, ProFormSelect } from "@ant-design/pro-components";
+import { Button, message, Popover } from "antd";
 import { UserAddOutlined } from "@ant-design/icons";
-import { get } from "../../../request";
+import { get } from "../../request";
 
-import { IUserListResponse } from "../../api/Auth";
+import { IUserListResponse } from "../api/Auth";
 
 interface IFormData {
   userId: string;
 }
 
 interface IWidget {
-  groupId?: string;
+  courseId?: string;
 }
 
-const Widget = ({ groupId }: IWidget) => {
+const Widget = ({ courseId }: IWidget) => {
   const intl = useIntl();
 
   const form = (
@@ -62,7 +58,7 @@ const Widget = ({ groupId }: IWidget) => {
           name="userType"
           label={intl.formatMessage({ id: "forms.fields.type.label" })}
           valueEnum={{
-            1: intl.formatMessage({ id: "forms.fields.students.label" }),
+            3: intl.formatMessage({ id: "forms.fields.student.label" }),
             2: intl.formatMessage({ id: "forms.fields.assistant.label" }),
           }}
         />

+ 3 - 7
dashboard/src/components/library/course/AddTeacher.tsx → dashboard/src/components/course/AddTeacher.tsx

@@ -1,14 +1,10 @@
 import { useIntl } from "react-intl";
-import {
-  ProForm,
-  ProFormSelect,
-  ProFormText,
-} from "@ant-design/pro-components";
+import { ProForm, ProFormSelect } from "@ant-design/pro-components";
 import { Button, message, Popover } from "antd";
 import { UserAddOutlined } from "@ant-design/icons";
-import { get } from "../../../request";
+import { get } from "../../request";
 
-import { IUserListResponse } from "../../api/Auth";
+import { IUserListResponse } from "../api/Auth";
 
 interface IFormData {
   userId: string;

+ 21 - 9
dashboard/src/components/library/course/CourseCreate.tsx → dashboard/src/components/course/CourseCreate.tsx

@@ -1,10 +1,15 @@
 import { useIntl } from "react-intl";
-import { ProForm, ProFormText } from "@ant-design/pro-components";
+import {
+  ProForm,
+  ProFormInstance,
+  ProFormText,
+} from "@ant-design/pro-components";
 import { message } from "antd";
 
-import { post } from "../../../request";
-import { ICourseCreateRequest, ICourseResponse } from "../../api/Course";
-import LangSelect from "../../general/LangSelect";
+import { post } from "../../request";
+import { ICourseCreateRequest, ICourseResponse } from "../api/Course";
+import LangSelect from "../general/LangSelect";
+import { useRef } from "react";
 
 interface IFormData {
   title: string;
@@ -12,24 +17,31 @@ interface IFormData {
   studio: string;
 }
 
-type IWidgetCourseCreate = {
+interface IWidgetCourseCreate {
   studio?: string;
-};
-const Widget = (prop: IWidgetCourseCreate) => {
+  onCreate?: Function;
+}
+const Widget = ({ studio = "", onCreate }: IWidgetCourseCreate) => {
   const intl = useIntl();
+  const formRef = useRef<ProFormInstance>();
 
   return (
     <ProForm<IFormData>
+      formRef={formRef}
       onFinish={async (values: IFormData) => {
         console.log(values);
-        values.studio = prop.studio ? prop.studio : "";
+        values.studio = studio;
         const res = await post<ICourseCreateRequest, ICourseResponse>(
-          `/v2/article`,
+          `/v2/course`,
           values
         );
         console.log(res);
         if (res.ok) {
           message.success(intl.formatMessage({ id: "flashes.success" }));
+          formRef.current?.resetFields(["title"]);
+          if (typeof onCreate !== "undefined") {
+            onCreate();
+          }
         } else {
           message.error(res.message);
         }

+ 24 - 0
dashboard/src/components/course/CourseIntro.tsx

@@ -0,0 +1,24 @@
+//课程详情简介
+import { Col, Row } from "antd";
+import { marked } from "marked";
+
+interface IWidget {
+  intro?: string;
+}
+const Widget = ({ intro }: IWidget) => {
+  return (
+    <Row>
+      <Col flex="auto"></Col>
+      <Col flex="960px">
+        <div
+          dangerouslySetInnerHTML={{
+            __html: marked.parse(intro ? intro : ""),
+          }}
+        />
+      </Col>
+      <Col flex="auto"></Col>
+    </Row>
+  );
+};
+
+export default Widget;

+ 71 - 0
dashboard/src/components/course/CourseList.tsx

@@ -0,0 +1,71 @@
+//课程列表
+import { Link } from "react-router-dom";
+import { useEffect, useState } from "react";
+
+import { Avatar, List, message, Typography } from "antd";
+import { ICourse } from "../../pages/library/course/course";
+import { ICourseListResponse } from "../api/Course";
+import { API_HOST, get } from "../../request";
+
+const { Paragraph } = Typography;
+
+interface IWidget {
+  type: "open" | "close";
+}
+const Widget = ({ type }: IWidget) => {
+  const [data, setData] = useState<ICourse[]>();
+
+  useEffect(() => {
+    get<ICourseListResponse>(`/v2/course?view=${type}`).then((json) => {
+      if (json.ok) {
+        console.log(json.data);
+        const course: ICourse[] = json.data.rows.map((item) => {
+          return {
+            id: item.id,
+            title: item.title,
+            subtitle: item.subtitle,
+            teacher: item.teacher,
+            intro: item.content,
+            coverUrl: item.cover,
+          };
+        });
+        setData(course);
+      } else {
+        message.error(json.message);
+      }
+    });
+  }, []);
+
+  return (
+    <List
+      itemLayout="vertical"
+      size="default"
+      pagination={{
+        onChange: (page) => {
+          console.log(page);
+        },
+        pageSize: 5,
+      }}
+      dataSource={data}
+      renderItem={(item) => (
+        <List.Item
+          key={item.title}
+          extra={
+            <img width={128} alt="logo" src={API_HOST + "/" + item.coverUrl} />
+          }
+        >
+          <List.Item.Meta
+            avatar={<Avatar />}
+            title={<Link to={`/course/show/${item.id}`}>{item.title}</Link>}
+            description={<div>主讲:{item.teacher?.nickName}</div>}
+          />
+          <Paragraph ellipsis={{ rows: 2, expandable: false }}>
+            {item.intro}
+          </Paragraph>
+        </List.Item>
+      )}
+    />
+  );
+};
+
+export default Widget;

+ 180 - 0
dashboard/src/components/course/CourseMember.tsx

@@ -0,0 +1,180 @@
+import { useIntl } from "react-intl";
+import { useRef, useState } from "react";
+import { ActionType, ProList } from "@ant-design/pro-components";
+import { Space, Tag, Button, Layout, Popconfirm } from "antd";
+
+import CourseAddMember from "./AddMember";
+import { delete_, get } from "../../request";
+
+import {
+  ICourseMemberDeleteResponse,
+  ICourseMemberListResponse,
+} from "../api/Course";
+
+const { Content } = Layout;
+
+interface IRoleTag {
+  title: string;
+  color: string;
+}
+interface DataItem {
+  id: number;
+  userId: string;
+  name?: string;
+  tag: IRoleTag[];
+  image: string;
+}
+interface IWidget {
+  courseId?: string;
+}
+const Widget = ({ courseId }: IWidget) => {
+  const intl = useIntl(); //i18n
+  const [canDelete, setCanDelete] = useState(false);
+  const [memberCount, setMemberCount] = useState<number>();
+
+  const ref = useRef<ActionType>();
+  return (
+    <Content>
+      <ProList<DataItem>
+        rowKey="id"
+        actionRef={ref}
+        headerTitle={
+          intl.formatMessage({ id: "group.member" }) +
+          "-" +
+          memberCount?.toString()
+        }
+        toolBarRender={() => {
+          return [
+            canDelete ? (
+              <CourseAddMember
+                courseId={courseId}
+                onCreated={() => {
+                  ref.current?.reload();
+                }}
+              />
+            ) : (
+              <></>
+            ),
+          ];
+        }}
+        showActions="hover"
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+
+          let url = `/v2/course-member?view=course&id=${courseId}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          if (typeof params.keyword !== "undefined") {
+            url += "&search=" + (params.keyword ? params.keyword : "");
+          }
+          const res = await get<ICourseMemberListResponse>(url);
+          if (res.ok) {
+            console.log(res.data);
+            setMemberCount(res.data.count);
+            switch (res.data.role) {
+              case "owner":
+                setCanDelete(true);
+                break;
+              case "manager":
+                setCanDelete(true);
+                break;
+            }
+            const items: DataItem[] = res.data.rows.map((item, id) => {
+              let member: DataItem = {
+                id: item.id ? item.id : 0,
+                userId: item.user_id,
+                name: item.user?.nickName,
+                tag: [],
+                image: "",
+              };
+              member.tag.push({
+                title: intl.formatMessage({
+                  id: "forms.fields." + item.role + ".label",
+                }),
+                color: "default",
+              });
+
+              return member;
+            });
+            console.log(items);
+            return {
+              total: res.data.count,
+              succcess: true,
+              data: items,
+            };
+          } else {
+            console.error(res.message);
+            return {
+              total: 0,
+              succcess: false,
+              data: [],
+            };
+          }
+        }}
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        metas={{
+          title: {
+            dataIndex: "name",
+          },
+          avatar: {
+            dataIndex: "image",
+            editable: false,
+          },
+          subTitle: {
+            render: (text, row, index, action) => {
+              const showtag = row.tag.map((item, id) => {
+                return (
+                  <Tag color={item.color} key={id}>
+                    {item.title}
+                  </Tag>
+                );
+              });
+              return <Space size={0}>{showtag}</Space>;
+            },
+          },
+          actions: {
+            render: (text, row, index, action) => [
+              canDelete ? (
+                <Popconfirm
+                  placement="bottomLeft"
+                  title={intl.formatMessage({
+                    id: "forms.message.member.delete",
+                  })}
+                  onConfirm={(
+                    e?: React.MouseEvent<HTMLElement, MouseEvent>
+                  ) => {
+                    console.log("delete", row.id);
+                    delete_<ICourseMemberDeleteResponse>(
+                      "/v2/course-member/" + row.id
+                    ).then((json) => {
+                      if (json.ok) {
+                        console.log("delete ok");
+                        ref.current?.reload();
+                      }
+                    });
+                  }}
+                  okText={intl.formatMessage({ id: "buttons.ok" })}
+                  cancelText={intl.formatMessage({ id: "buttons.cancel" })}
+                >
+                  <Button size="small" type="link" danger key="link">
+                    {intl.formatMessage({ id: "buttons.remove" })}
+                  </Button>
+                </Popconfirm>
+              ) : (
+                <></>
+              ),
+            ],
+          },
+        }}
+      />
+    </Content>
+  );
+};
+
+export default Widget;

+ 66 - 0
dashboard/src/components/course/CourseShow.tsx

@@ -0,0 +1,66 @@
+//课程详情图片标题按钮主讲人组合
+import { Link } from "react-router-dom";
+import { Image, Button, Space, Col, Row, Breadcrumb } from "antd";
+import { Typography } from "antd";
+import { HomeOutlined } from "@ant-design/icons";
+
+import { IUser } from "../auth/User";
+import { API_HOST } from "../../request";
+import UserName from "../auth/UserName";
+
+const { Title } = Typography;
+
+interface IWidget {
+  title?: string;
+  subtitle?: string;
+  coverUrl?: string;
+  teacher?: IUser;
+}
+const Widget = ({ title, subtitle, coverUrl, teacher }: IWidget) => {
+  return (
+    <>
+      <Row>
+        <Col flex="auto"></Col>
+        <Col flex="960px">
+          <Space direction="vertical">
+            <Breadcrumb>
+              <Breadcrumb.Item>
+                <HomeOutlined />
+              </Breadcrumb.Item>
+              <Breadcrumb.Item>
+                <Link to="/course/list">课程</Link>
+              </Breadcrumb.Item>
+              <Breadcrumb.Item>{title}</Breadcrumb.Item>
+            </Breadcrumb>
+            <Space>
+              <Image
+                width={200}
+                style={{ borderRadius: 12 }}
+                src={API_HOST + "/" + coverUrl}
+                fallback={`${API_HOST}/app/course/img/default.jpg`}
+              />
+              <Space direction="vertical">
+                <Title level={3}>{title}</Title>
+                <Title level={5}>{subtitle}</Title>
+                <Button type="primary">关注</Button>
+              </Space>
+            </Space>
+            <div>
+              主讲人: <UserName {...teacher} />
+            </div>
+          </Space>
+        </Col>
+        <Col flex="auto"></Col>
+      </Row>
+    </>
+  );
+};
+
+export default Widget;
+
+/*
+<Button type="primary">关注</Button>
+<Button type="primary" disabled>
+  已关注
+</Button>
+*/

+ 72 - 0
dashboard/src/components/course/LecturerList.tsx

@@ -0,0 +1,72 @@
+//主讲人列表
+import { useNavigate } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { Card, List, message, Typography } from "antd";
+
+import { ICourse } from "../../pages/library/course/course";
+import { ICourseListResponse } from "../api/Course";
+import { API_HOST, get } from "../../request";
+
+const { Meta } = Card;
+const { Paragraph } = Typography;
+
+const Widget = () => {
+  const [data, setData] = useState<ICourse[]>();
+  const navigate = useNavigate();
+
+  useEffect(() => {
+    get<ICourseListResponse>(`/v2/course?view=new&limit=4`).then((json) => {
+      if (json.ok) {
+        console.log(json.data);
+        const course: ICourse[] = json.data.rows.map((item) => {
+          return {
+            id: item.id,
+            title: item.title,
+            subtitle: item.subtitle,
+            teacher: item.teacher,
+            intro: item.content,
+            coverUrl: item.cover,
+          };
+        });
+        setData(course);
+      } else {
+        message.error(json.message);
+      }
+    });
+  }, []);
+  return (
+    <List
+      grid={{ gutter: 16, column: 4 }}
+      dataSource={data}
+      renderItem={(item) => (
+        <List.Item>
+          <Card
+            hoverable
+            style={{ width: "100%", height: 300 }}
+            cover={
+              <img
+                alt="example"
+                src={API_HOST + "/" + item.coverUrl}
+                width="240"
+                height="200"
+              />
+            }
+            onClick={(e) => {
+              navigate(`/course/show/${item.id}`);
+            }}
+          >
+            <Meta
+              title={item.title}
+              description={
+                <Paragraph ellipsis={{ rows: 2, expandable: false }}>
+                  {item.intro}
+                </Paragraph>
+              }
+            />
+          </Card>
+        </List.Item>
+      )}
+    />
+  );
+};
+export default Widget;

+ 0 - 0
dashboard/src/components/library/course/LessonSelect.tsx → dashboard/src/components/course/LessonSelect.tsx


+ 0 - 0
dashboard/src/components/library/course/LessonTreeShow.tsx → dashboard/src/components/course/LessonTreeShow.tsx


+ 8 - 11
dashboard/src/components/library/course/StudentsSelect.tsx → dashboard/src/components/course/StudentsSelect.tsx

@@ -33,23 +33,22 @@ const defaultData = [
   },
 ];
 type DataItem = typeof defaultData[number];
-interface IWidgetGroupFile {
-  groupId?: string;
+interface IWidge {
+  courseId?: string;
 }
-const Widget = ({ groupId }: IWidgetGroupFile) => {
+const Widget = ({ courseId }: IWidge) => {
   const intl = useIntl(); //i18n
   const [dataSource, setDataSource] = useState<DataItem[]>(defaultData);
 
   return (
-    <Content>
-      <Space>{groupId}</Space>
+    <>
       <ProList<DataItem>
         rowKey="id"
         headerTitle={intl.formatMessage({
           id: "forms.fields.studentsassistant.label",
         })}
         toolBarRender={() => {
-          return [<AddStudent groupId={groupId} />];
+          return [<AddStudent courseId={courseId} />];
         }}
         dataSource={dataSource}
         showActions="hover"
@@ -77,12 +76,10 @@ const Widget = ({ groupId }: IWidgetGroupFile) => {
           actions: {
             render: (text, row, index, action) => [
               <Button
-                style={{ padding: 0, margin: 0 }}
+                size="small"
                 type="link"
                 danger
-                onClick={() => {
-                  action?.startEditable(row.id);
-                }}
+                onClick={() => {}}
                 key="link"
               >
                 {intl.formatMessage({ id: "buttons.remove" })}
@@ -91,7 +88,7 @@ const Widget = ({ groupId }: IWidgetGroupFile) => {
           },
         }}
       />
-    </Content>
+    </>
   );
 };
 

+ 0 - 0
dashboard/src/components/library/course/TeacherSelect.tsx → dashboard/src/components/course/TeacherSelect.tsx


+ 31 - 0
dashboard/src/components/course/TextBook.tsx

@@ -0,0 +1,31 @@
+import { Col, Row } from "antd";
+import { useNavigate } from "react-router-dom";
+import AnthologyDetail from "../article/AnthologyDetail";
+
+interface IWidget {
+  anthologyId?: string;
+  courseId?: string;
+}
+const Widget = ({ anthologyId, courseId }: IWidget) => {
+  const navigate = useNavigate();
+
+  console.log("anthologyId", anthologyId);
+  return (
+    <div style={{ backgroundColor: "#f5f5f5" }}>
+      <Row>
+        <Col flex="auto"></Col>
+        <Col flex="960px">
+          <AnthologyDetail
+            aid={anthologyId}
+            onArticleSelect={(keys: string[]) => {
+              navigate(`/article/textbook/${courseId}/${keys[0]}`);
+            }}
+          />
+        </Col>
+        <Col flex="auto"></Col>
+      </Row>
+    </div>
+  );
+};
+
+export default Widget;

+ 0 - 0
dashboard/src/components/library/course/UploadTexture.tsx → dashboard/src/components/course/UploadTexture.tsx


+ 1 - 1
dashboard/src/components/group/GroupMember.tsx

@@ -1,8 +1,8 @@
 import { useIntl } from "react-intl";
 import { useRef, useState } from "react";
 import { ActionType, ProList } from "@ant-design/pro-components";
-import { UserAddOutlined } from "@ant-design/icons";
 import { Space, Tag, Button, Layout, Popconfirm } from "antd";
+
 import GroupAddMember from "./AddMember";
 import { delete_, get } from "../../request";
 import {

+ 0 - 51
dashboard/src/components/library/course/CourseIntro.tsx

@@ -1,51 +0,0 @@
-//课程详情简介
-import { Link } from "react-router-dom";
-import React from 'react';
-import { ProForm, ProFormText } from "@ant-design/pro-components";
-import {Layout,  Descriptions, Space , Col, Row } from 'antd';
-
-import ReactPlayer from 'react-player'
-
-const Widget = () => {
-  
-
-  return (
-    <ProForm.Group>
-        <Layout>
-        <Descriptions title="课程简介">
-    <Descriptions.Item label=" ">每一尊佛体证后,都会有一次殊胜的大聚会,那是十方天人的相聚,由此宣说《大集会经》。
-佛陀观察到:天人们内心有种种问题,他们却不知该如何表达……
-于是便有了化身佛在问,本尊佛在答……
- 
-根据众生的根性,佛陀共开示六部经,分别针对贪、瞋、痴、信、觉、寻六种性格习性的天人。
-此部《纷争分歧经》便是专门为瞋行者量身而作,瞋行者往往多思、多慧,佛陀便以智慧循循善诱,抽丝剥茧,层层深入,探究纷争、分歧等八种烦恼根源何在……
- 
-听,佛陀在说——
-让我们以佛陀当年的语言——古老的巴利语——去聆听佛陀的教诲……</Descriptions.Item>
-  </Descriptions>
-  <Descriptions title="电子平台课堂笔记">
-    <Descriptions.Item label="快速预览(课前预习)">
-      <Link to="/course/lesson/12345">原文</Link>  <Link to="/course/lesson/23456">原文+义注</Link> </Descriptions.Item>
-  </Descriptions>
-  <ReactPlayer
-            className='react-player fixed-bottom'
-            url= 'https://assets-hk.wikipali.org/video/admissions1080p.mp4'
-            width='50%'
-            height='50%'
-            controls = {true}
-
-          />
-    </Layout>
-      </ProForm.Group>
-    );
-};
-
-export default Widget;
-
-
-/*
-<Button type="primary">关注</Button>
-<Button type="primary" disabled>
-  已关注
-</Button>
-*/

+ 0 - 62
dashboard/src/components/library/course/CourseList.tsx

@@ -1,62 +0,0 @@
-//课程列表
-import React from 'react';
-import { LikeOutlined, MessageOutlined, StarOutlined } from '@ant-design/icons';
-import { Avatar, List, Space } from 'antd';
-
-const data = Array.from({ length: 23 }).map((_, i) => ({
-  href: '../course/show',
-  title: `课程 ${i}`,
-  avatar: 'https://joeschmoe.io/api/v1/random',
-  description:
-    '主讲人: 小僧善巧',
-  content:
-    '一年之计在于春,新春佳节修善行; 一周学习与精进,法为伊始吉祥年',
-}));
-
-const IconText = ({ icon, text }: { icon: React.FC; text: string }) => (
-  <Space>
-    {React.createElement(icon)}
-    {text}
-  </Space>
-);
-
-const App: React.FC = () => (
-  <List
-    itemLayout="vertical"
-    size="large"
-    pagination={{
-      onChange: (page) => {
-        console.log(page);
-      },
-      pageSize: 5,
-    }}
-    dataSource={data}
-    footer={
-      <div>
-        <b>ant design</b> footer part
-      </div>
-    }
-    renderItem={(item) => (
-      <List.Item
-        key={item.title}
-
-        extra={
-          <img
-            width={272}
-            alt="logo"
-            src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg95.699pic.com%2Fxsj%2F0g%2Fk2%2F7d.jpg%21%2Ffw%2F700%2Fwatermark%2Furl%2FL3hzai93YXRlcl9kZXRhaWwyLnBuZw%2Falign%2Fsoutheast&refer=http%3A%2F%2Fimg95.699pic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1673839902&t=d8f4306ddd6935313c66efb936cbe268"
-          />
-        }
-      >
-        <List.Item.Meta
-          avatar={<Avatar src={item.avatar} />}
-          title={<a href={item.href}>{item.title}</a>}
-          description={item.description}
-        />
-        {item.content}
-      </List.Item>
-    )}
-  />
-);
-
-export default App;

+ 0 - 45
dashboard/src/components/library/course/CourseShow.tsx

@@ -1,45 +0,0 @@
-//课程详情图片标题按钮主讲人组合
-import { Link } from "react-router-dom";
-import React from 'react';
-import { ProForm, ProFormText } from "@ant-design/pro-components";
-import {Layout,  Image, Button, Space , Col, Row } from 'antd';
-
-const Widget = () => {
-  
-
-  return (
-    <ProForm.Group>
-        <Layout>
-          <Row>
-    <Image
-    width={200}
-    src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg95.699pic.com%2Fxsj%2F0g%2Fk2%2F7d.jpg%21%2Ffw%2F700%2Fwatermark%2Furl%2FL3hzai93YXRlcl9kZXRhaWwyLnBuZw%2Falign%2Fsoutheast&refer=http%3A%2F%2Fimg95.699pic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1673839902&t=d8f4306ddd6935313c66efb936cbe268"
-  />
-    <h1 style={{ "fontWeight": 'bold', "fontSize": 30}}>wikipali课程</h1>
-
-          <Col flex="auto"></Col>
-          <Col flex="1260px">   </Col>
-          </Row>
-          <Col>
-          <Button type="primary">关注</Button>
-          </Col>
-
-
-
-
-    <p style={{ "fontWeight": 'bold', "fontSize": 15}}>主讲人: <Link to="/course/lesson/12345">小僧善巧</Link> </p>
-    
-    </Layout>
-      </ProForm.Group>
-    );
-};
-
-export default Widget;
-
-
-/*
-<Button type="primary">关注</Button>
-<Button type="primary" disabled>
-  已关注
-</Button>
-*/

+ 0 - 116
dashboard/src/components/library/course/LecturerList.tsx

@@ -1,116 +0,0 @@
-//主讲人列表
-import { useIntl } from "react-intl";
-import { ProForm, ProFormText } from "@ant-design/pro-components";
-//import { message } from "antd";
-
-import { post } from "../../../request";
-import { useState } from "react";
-//import React from 'react';
-import { Card, List , Col, Row , Space} from 'antd';
-const { Meta } = Card;
-//const {  Card, Col, Row  } = antd;
-
-const data = [
-  {
-    title: 'U Kuṇḍadhāna Sayadaw',
-    introduction: 'U Kuṇḍadhāna Sayadaw简介 U Kun西亚多今年51岁,30个瓦萨, - 1969年,出生于缅甸...',
-    portrait:'https://img2.baidu.com/it/u=2930319359,2500787374&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=334'
-
-  },
-  {
-    title: '某尊者',
-    introduction: '某尊者简介...',
-    portrait:'https://img2.baidu.com/it/u=2930319359,2500787374&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=334'
-  },
-  {
-    title: '小僧善巧',
-    introduction: '小僧善巧尊者简介...',
-    portrait:'https://avatars.githubusercontent.com/u/58804044?v=4'
-  },
-  {
-    title: 'Kalyāṇamitta',
-    introduction: 'Kalyāṇamitta尊者简介...',
-    portrait:'https://img2.baidu.com/it/u=2930319359,2500787374&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=334'
-  },
-];
-/*栅格卡片实现方案 https://ant.design/components/card-cn/
-
-const App = () => (
-  <div className="site-card-wrapper">
-    <Row gutter={16}>
-      <Col span={4}>
-      <Card
-    hoverable
-    style={{ width: 240,  height: 300}}
-    cover={<img alt="example" src={data[0].portrait}  width="240" height="180"/>}
-  >
-    <Meta title={data[0].title} description={data[0].introduction} />
-  </Card>
-      </Col>
-      <Col span={4}>
-      <Card
-    hoverable
-    style={{ width: 240,  height: 300}}
-    cover={<img alt="example" src={data[1].portrait}  width="240" height="180"/>}
-  >
-    <Meta title={data[1].title} description={data[1].introduction} />
-  </Card>
-      </Col>
-      <Col span={4}>
-      <Card
-    hoverable
-    style={{ width: 240,  height: 300}}
-    cover={<img alt="example" src={data[2].portrait}  width="240" height="180"/>}
-  >
-    <Meta title={data[2].title} description={data[2].introduction} />
-  </Card>
-      </Col>
-      <Col span={4}>
-      <Card
-    hoverable
-    style={{ width: 240,  height: 300}}
-    cover={<img alt="example" src={data[3].portrait}  width="240" height="180"/>}
-  >
-    <Meta title={data[3].title} description={data[3].introduction} />
-  </Card>
-      </Col>
-    </Row>
-  </div>
-);
-
-export default App;
-*/
-
-
-/*List实现方案
-
-import { useIntl } from "react-intl";
-import { ProForm, ProFormText } from "@ant-design/pro-components";
-//import { message } from "antd";
-
-import { post } from "../../../request";
-import { useState } from "react";
-//import React from 'react';
-import { Card, List } from 'antd';
-const { Meta } = Card;
-*/
-const App: React.FC = () => (
-
-  <List
-    grid={{ gutter: 16, column: 4 }}
-    dataSource={data}
-    renderItem={(item) => (
-      <List.Item>
-        <Card
-          hoverable
-          style={{ width: 240,  height: 300}}
-          cover={<img alt="example" src={item.portrait}  width="240" height="180"/>}
-          >
-          <Meta title={item.title} description={item.introduction} />
-        </Card>
-      </List.Item>
-    )}
-  />
-      
-);
-export default App;

+ 136 - 137
dashboard/src/components/studio/LeftSider.tsx

@@ -20,144 +20,143 @@ type IWidgetHeadBar = {
   selectedKeys?: string;
 };
 const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
-	//Library head bar
-	const intl = useIntl(); //i18n
-	const { studioname } = useParams();
-	const linkPalicanon = "/studio/" + studioname + "/palicanon";
-	const linkRecent = "/studio/" + studioname + "/recent";
-	const linkChannel = "/studio/" + studioname + "/channel/list";
-	const linkGroup = "/studio/" + studioname + "/group/list";
-	const linkUserdict = "/studio/" + studioname + "/dict/list";
-	const linkTerm = "/studio/" + studioname + "/term/list";
-	const linkCourse = "/studio/" + studioname + "/course/list";
-	const linkArticle = "/studio/" + studioname + "/article/list";
-	const linkAnthology = "/studio/" + studioname + "/anthology/list";
-	const linkAnalysis = "/studio/" + studioname + "/analysis/list";
-
-	const items: MenuProps["items"] = [
-		{
-			label: "常用",
-			key: "basic",
-			icon: <HomeOutlined />,
-			children: [
-				{
-					label: (
-						<Link to={linkPalicanon}>
-							{intl.formatMessage({
-								id: "columns.studio.palicanon.title",
-							})}
-						</Link>
-					),
-					key: "palicanon",
-				},
-				{
-					label: (
-						<Link to={linkRecent}>
-							{intl.formatMessage({
-								id: "columns.studio.recent.title",
-							})}
-						</Link>
-					),
-					key: "recent",
-				},
-				{
-					label: (
-						<Link to={linkChannel}>
-							{intl.formatMessage({
-								id: "columns.studio.channel.title",
-							})}
-						</Link>
-					),
-					key: "channel",
-				},
-				{
-					label: (
-						<Link to={linkAnalysis}>
-							{intl.formatMessage({
-								id: "columns.studio.analysis.title",
-							})}
-						</Link>
-					),
-					key: "analysis",
-				},
-			],
-		},
-		{
-			label: "高级",
-			key: "advance",
-			icon: <AppstoreOutlined />,
-			children: [
-				{
-					label: (
-						<Link to={linkUserdict}>
-							{intl.formatMessage({
-								id: "columns.studio.userdict.title",
-							})}
-						</Link>
-					),
-					key: "userdict",
-				},
-				{
-					label: (
-						<Link to={linkTerm}>
-							{intl.formatMessage({
-								id: "columns.studio.term.title",
-							})}
-						</Link>
-					),
-					key: "term",
-				},
-				{
-					label: (
-						<Link to={linkCourse}>
-							{intl.formatMessage({
-								id: "columns.studio.course.title",
-							})}
-						</Link>
-					),
-					key: "course",
-				},
-				{
-					label: (
-						<Link to={linkArticle}>
-							{intl.formatMessage({
-								id: "columns.studio.article.title",
-							})}
-						</Link>
-					),
-					key: "article",
-				},
-				{
-					label: (
-						<Link to={linkAnthology}>
-							{intl.formatMessage({
-								id: "columns.studio.anthology.title",
-							})}
-						</Link>
-					),
-					key: "anthology",
-				},
-			],
-		},
-		{
-			label: "协作",
-			key: "collaboration",
-			icon: <TeamOutlined />,
-			children: [
-				{
-					label: (
-						<Link to={linkGroup}>
-							{intl.formatMessage({
-								id: "columns.studio.group.title",
-							})}
-						</Link>
-					),
-					key: "group",
-				},
-			],
-		},
-	];
+  //Library head bar
+  const intl = useIntl(); //i18n
+  const { studioname } = useParams();
+  const linkPalicanon = "/studio/" + studioname + "/palicanon";
+  const linkRecent = "/studio/" + studioname + "/recent";
+  const linkChannel = "/studio/" + studioname + "/channel/list";
+  const linkGroup = "/studio/" + studioname + "/group/list";
+  const linkUserdict = "/studio/" + studioname + "/dict/list";
+  const linkTerm = "/studio/" + studioname + "/term/list";
+  const linkArticle = "/studio/" + studioname + "/article/list";
+  const linkAnthology = "/studio/" + studioname + "/anthology/list";
+  const linkAnalysis = "/studio/" + studioname + "/analysis/list";
+  const linkCourse = "/studio/" + studioname + "/course/list";
 
+  const items: MenuProps["items"] = [
+    {
+      label: "常用",
+      key: "basic",
+      icon: <HomeOutlined />,
+      children: [
+        {
+          label: (
+            <Link to={linkPalicanon}>
+              {intl.formatMessage({
+                id: "columns.studio.palicanon.title",
+              })}
+            </Link>
+          ),
+          key: "palicanon",
+        },
+        {
+          label: (
+            <Link to={linkRecent}>
+              {intl.formatMessage({
+                id: "columns.studio.recent.title",
+              })}
+            </Link>
+          ),
+          key: "recent",
+        },
+        {
+          label: (
+            <Link to={linkChannel}>
+              {intl.formatMessage({
+                id: "columns.studio.channel.title",
+              })}
+            </Link>
+          ),
+          key: "channel",
+        },
+        {
+          label: (
+            <Link to={linkAnalysis}>
+              {intl.formatMessage({
+                id: "columns.studio.analysis.title",
+              })}
+            </Link>
+          ),
+          key: "analysis",
+        },
+      ],
+    },
+    {
+      label: "高级",
+      key: "advance",
+      icon: <AppstoreOutlined />,
+      children: [
+        {
+          label: (
+            <Link to={linkCourse}>
+              {intl.formatMessage({
+                id: "columns.library.course.title",
+              })}
+            </Link>
+          ),
+          key: "course",
+        },
+        {
+          label: (
+            <Link to={linkUserdict}>
+              {intl.formatMessage({
+                id: "columns.studio.userdict.title",
+              })}
+            </Link>
+          ),
+          key: "userdict",
+        },
+        {
+          label: (
+            <Link to={linkTerm}>
+              {intl.formatMessage({
+                id: "columns.studio.term.title",
+              })}
+            </Link>
+          ),
+          key: "term",
+        },
+        {
+          label: (
+            <Link to={linkArticle}>
+              {intl.formatMessage({
+                id: "columns.studio.article.title",
+              })}
+            </Link>
+          ),
+          key: "article",
+        },
+        {
+          label: (
+            <Link to={linkAnthology}>
+              {intl.formatMessage({
+                id: "columns.studio.anthology.title",
+              })}
+            </Link>
+          ),
+          key: "anthology",
+        },
+      ],
+    },
+    {
+      label: "协作",
+      key: "collaboration",
+      icon: <TeamOutlined />,
+      children: [
+        {
+          label: (
+            <Link to={linkGroup}>
+              {intl.formatMessage({
+                id: "columns.studio.group.title",
+              })}
+            </Link>
+          ),
+          key: "group",
+        },
+      ],
+    },
+  ];
 
   return (
     <Affix offsetTop={0}>

+ 67 - 67
dashboard/src/components/studio/table.ts

@@ -1,74 +1,74 @@
 import { useIntl } from "react-intl";
 
 export const PublicityValueEnum = () => {
-	const intl = useIntl();
-	return {
-		all: {
-			text: intl.formatMessage({
-				id: "tables.publicity.all",
-			}),
-			status: "Default",
-		},
-		0: {
-			text: intl.formatMessage({
-				id: "tables.publicity.disable",
-			}),
-			status: "Default",
-		},
-		10: {
-			text: intl.formatMessage({
-				id: "tables.publicity.private",
-			}),
-			status: "Processing",
-		},
-		20: {
-			text: intl.formatMessage({
-				id: "tables.publicity.public.bylink",
-			}),
-			status: "Processing",
-		},
-		30: {
-			text: intl.formatMessage({
-				id: "tables.publicity.public",
-			}),
-			status: "Success",
-		},
-		40: {
-			text: intl.formatMessage({
-				id: "tables.publicity.public.edit",
-			}),
-			status: "Success",
-		},
-	};
+  const intl = useIntl();
+  return {
+    all: {
+      text: intl.formatMessage({
+        id: "tables.publicity.all",
+      }),
+      status: "Default",
+    },
+    0: {
+      text: intl.formatMessage({
+        id: "tables.publicity.disable",
+      }),
+      status: "Default",
+    },
+    10: {
+      text: intl.formatMessage({
+        id: "tables.publicity.private",
+      }),
+      status: "Success",
+    },
+    20: {
+      text: intl.formatMessage({
+        id: "tables.publicity.public.bylink",
+      }),
+      status: "Processing",
+    },
+    30: {
+      text: intl.formatMessage({
+        id: "tables.publicity.public",
+      }),
+      status: "Processing",
+    },
+    40: {
+      text: intl.formatMessage({
+        id: "tables.publicity.public.edit",
+      }),
+      status: "Processing",
+    },
+  };
 };
 
 export const RoleValueEnum = () => {
-	const intl = useIntl();
-	return {
-		all: {
-			text: intl.formatMessage({
-				id: "tables.role.all",
-			}),
-		},
-		owner: {
-			text: intl.formatMessage({
-				id: "tables.role.owner",
-			}),
-		},
-		manager: {
-			text: intl.formatMessage({
-				id: "tables.role.manager",
-			}),
-		},
-		editor: {
-			text: intl.formatMessage({
-				id: "tables.role.editor",
-			}),
-		},
-		member: {
-			text: intl.formatMessage({
-				id: "tables.role.member",
-			}),
-		},
-	};
+  const intl = useIntl();
+  return {
+    all: {
+      text: intl.formatMessage({
+        id: "tables.role.all",
+      }),
+    },
+    owner: {
+      text: intl.formatMessage({
+        id: "tables.role.owner",
+      }),
+    },
+    manager: {
+      text: intl.formatMessage({
+        id: "tables.role.manager",
+      }),
+    },
+    editor: {
+      text: intl.formatMessage({
+        id: "tables.role.editor",
+      }),
+    },
+    member: {
+      text: intl.formatMessage({
+        id: "tables.role.member",
+      }),
+    },
+  };
 };

+ 6 - 1
dashboard/src/components/template/Wbw/WbwFactorMeaning.tsx

@@ -13,10 +13,11 @@ const { Text } = Typography;
 
 interface IWidget {
   data: IWbw;
+  factors?: string;
   display?: TWbwDisplayMode;
   onChange?: Function;
 }
-const Widget = ({ data, display, onChange }: IWidget) => {
+const Widget = ({ data, display, onChange, factors }: IWidget) => {
   const intl = useIntl();
   const defaultMenu: MenuProps["items"] = [
     {
@@ -55,6 +56,10 @@ const Widget = ({ data, display, onChange }: IWidget) => {
     }
   }, [inlineDict]);
 
+  useEffect(() => {
+    if (typeof factors !== "undefined") {
+    }
+  }, [factors]);
   const onClick: MenuProps["onClick"] = (e) => {
     console.log("click ", e);
     if (typeof onChange !== "undefined") {

+ 13 - 2
dashboard/src/components/template/Wbw/WbwMeaning.tsx

@@ -1,3 +1,4 @@
+import { useState } from "react";
 import { useIntl } from "react-intl";
 import { Popover, Typography } from "antd";
 
@@ -14,7 +15,7 @@ interface IWidget {
 }
 const Widget = ({ data, display = "block", onChange }: IWidget) => {
   const intl = useIntl();
-
+  const [open, setOpen] = useState(false);
   let meaning = <></>;
   if (
     display === "block" &&
@@ -31,16 +32,26 @@ const Widget = ({ data, display = "block", onChange }: IWidget) => {
   } else {
     meaning = <span>{data.meaning?.value}</span>;
   }
+  const hide = () => {
+    setOpen(false);
+  };
+
+  const handleOpenChange = (newOpen: boolean) => {
+    setOpen(newOpen);
+  };
   if (typeof data.real !== "undefined" && PaliReal(data.real.value) !== "") {
     //非标点符号
     return (
       <div>
         <Popover
+          open={open}
+          onOpenChange={handleOpenChange}
           content={
-            <div style={{ width: 500 }}>
+            <div style={{ width: 500, height: "60vh", overflow: "auto" }}>
               <WbwMeaningSelect
                 data={data}
                 onSelect={(e: string) => {
+                  hide();
                   if (typeof onChange !== "undefined") {
                     onChange(e);
                   }

+ 137 - 103
dashboard/src/components/template/Wbw/WbwMeaningSelect.tsx

@@ -1,12 +1,28 @@
+/**
+ * 逐词解析意思选择菜单
+ * 基本算法:
+ * 从redux 获取单词列表。找到与拼写完全相同的单词。按照词典渲染单词意思列表
+ * 词典相同语法信息不同的单独一行
+ * 在上面的单词数据里面 找到 base 列表,重复上面的步骤
+ * 菜单显示结构:
+ * 拼写1
+ *    词典1  词性  意思1 意思2
+ *    词典2  词性  意思1 意思2
+ * 拼写2
+ *    词典1  词性  意思1 意思2
+ *    词典2  词性  意思1 意思2
+ *
+ */
 import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
-import { Collapse, Tag } from "antd";
+import { Collapse, Space, Tag, Typography } from "antd";
 
 import { IWbw } from "./WbwWord";
 import { useAppSelector } from "../../../hooks";
 import { inlineDict as _inlineDict } from "../../../reducers/inline-dict";
 
 const { Panel } = Collapse;
+const { Text } = Typography;
 
 interface IMeaning {
   text: string;
@@ -14,10 +30,12 @@ interface IMeaning {
 }
 interface ICase {
   name: string;
+  local: string;
   meaning: IMeaning[];
 }
 interface IDict {
-  name: string;
+  id: string;
+  name?: string;
   case: ICase[];
 }
 interface IParent {
@@ -35,95 +53,105 @@ const Widget = ({ data, onSelect }: IWidget) => {
   const [parent, setParent] = useState<IParent[]>();
 
   useEffect(() => {
+    //判断单词列表里面是否有这个词
     if (inlineDict.wordIndex.includes(data.word.value)) {
+      let baseRemind: string[] = [];
+      let baseDone: string[] = [];
+      baseRemind.push(data.word.value);
       let mParent: IParent[] = [];
-      const word1 = data.word.value;
-      const result1 = inlineDict.wordList.filter((word) => word.word === word1);
-      mParent.push({ word: word1, dict: [] });
-      const indexParent = mParent.findIndex((item) => item.word === word1);
-      result1.forEach((value, index, array) => {
-        let indexDict = mParent[indexParent].dict.findIndex(
-          (item) => item.name === value.dict_id
-        );
-        if (indexDict === -1) {
-          //没找到,添加一个dict
-          mParent[indexParent].dict.push({ name: value.dict_id, case: [] });
-          indexDict = mParent[indexParent].dict.findIndex(
-            (item) => item.name === value.dict_id
-          );
+      while (baseRemind.length > 0) {
+        const word1 = baseRemind.pop();
+        if (typeof word1 === "undefined") {
+          break;
         }
-        const wordType = value.type === "" ? "null" : value.type;
-        let indexCase = mParent[indexParent].dict[indexDict].case.findIndex(
-          (item) => item.name === wordType
+        baseDone.push(word1);
+        const result1 = inlineDict.wordList.filter(
+          (word) => word.word === word1
         );
-        if (indexCase === -1) {
-          mParent[indexParent].dict[indexDict].case.push({
-            name: wordType,
-            meaning: [],
-          });
-          indexCase = mParent[indexParent].dict[indexDict].case.findIndex(
+        mParent.push({ word: word1, dict: [] });
+        const indexParent = mParent.findIndex((item) => item.word === word1);
+        result1.forEach((value, index, array) => {
+          if (
+            value.parent !== "" &&
+            !baseRemind.includes(value.parent) &&
+            !baseDone.includes(value.parent)
+          ) {
+            baseRemind.push(value.parent);
+          }
+          let indexDict = mParent[indexParent].dict.findIndex(
+            (item) => item.id === value.dict_id
+          );
+          if (indexDict === -1) {
+            //没找到,添加一个dict
+            mParent[indexParent].dict.push({
+              id: value.dict_id,
+              name: value.dict_shortname,
+              case: [],
+            });
+            indexDict = mParent[indexParent].dict.findIndex(
+              (item) => item.id === value.dict_id
+            );
+          }
+          const wordType =
+            value.type === "" ? "null" : value.type.replaceAll(".", "");
+          let indexCase = mParent[indexParent].dict[indexDict].case.findIndex(
             (item) => item.name === wordType
           );
-        }
-        console.log("indexCase", indexCase, value.mean);
-        if (value.mean && value.mean.trim() !== "") {
-          for (const valueMeaning of value.mean.trim().split("$")) {
-            if (valueMeaning.trim() !== "") {
-              const mValue = valueMeaning.trim();
-              console.log("meaning", mValue);
-              console.log(
-                "in",
-                mParent[indexParent].dict[indexDict].case[indexCase].meaning
-              );
-              let indexMeaning = mParent[indexParent].dict[indexDict].case[
-                indexCase
-              ].meaning.findIndex((itemMeaning) => itemMeaning.text === mValue);
+          if (indexCase === -1) {
+            //没找到,新建
+            mParent[indexParent].dict[indexDict].case.push({
+              name: wordType,
+              local: intl.formatMessage({
+                id: `dict.fields.type.${wordType}.short.label`,
+              }),
+              meaning: [],
+            });
+            indexCase = mParent[indexParent].dict[indexDict].case.findIndex(
+              (item) => item.name === wordType
+            );
+          }
+          console.log("indexCase", indexCase, value.mean);
+          if (value.mean && value.mean.trim() !== "") {
+            for (const valueMeaning of value.mean.trim().split("$")) {
+              if (valueMeaning.trim() !== "") {
+                const mValue = valueMeaning.trim();
+                let indexMeaning = mParent[indexParent].dict[indexDict].case[
+                  indexCase
+                ].meaning.findIndex(
+                  (itemMeaning) => itemMeaning.text === mValue
+                );
 
-              console.log("indexMeaning", indexMeaning);
-              let indexM: number;
-              const currMeanings =
-                mParent[indexParent].dict[indexDict].case[indexCase].meaning;
-              for (indexM = 0; indexM < currMeanings.length; indexM++) {
-                console.log("index", indexM);
-                console.log("array", currMeanings);
-                console.log("word1", currMeanings[indexM].text);
-                console.log("word2", mValue);
-                if (currMeanings[indexM].text === mValue) {
-                  break;
+                let indexM: number;
+                const currMeanings =
+                  mParent[indexParent].dict[indexDict].case[indexCase].meaning;
+                for (indexM = 0; indexM < currMeanings.length; indexM++) {
+                  if (currMeanings[indexM].text === mValue) {
+                    break;
+                  }
+                }
+
+                if (indexMeaning === -1) {
+                  mParent[indexParent].dict[indexDict].case[
+                    indexCase
+                  ].meaning.push({
+                    text: mValue,
+                    count: 1,
+                  });
+                } else {
+                  mParent[indexParent].dict[indexDict].case[indexCase].meaning[
+                    indexMeaning
+                  ].count++;
                 }
-              }
-              console.log("new index", indexM);
-              if (indexMeaning === -1) {
-                mParent[indexParent].dict[indexDict].case[
-                  indexCase
-                ].meaning.push({
-                  text: mValue,
-                  count: 1,
-                });
-              } else {
-                mParent[indexParent].dict[indexDict].case[indexCase].meaning[
-                  indexMeaning
-                ].count++;
               }
             }
           }
-        }
-      });
+        });
+      }
 
       setParent(mParent);
     }
   }, [inlineDict]);
-  /*
-  const meaning: IMeaning[] = Array.from(Array(10).keys()).map((item) => {
-    return { text: "意思" + item, count: item };
-  });
-  const dict: IDict[] = Array.from(Array(3).keys()).map((item) => {
-    return { name: "字典" + item, meaning: meaning };
-  });
-  const parent: IParent[] = Array.from(Array(3).keys()).map((item) => {
-    return { word: data.word.value + item, dict: dict };
-  });
-  */
+
   return (
     <div>
       <Collapse defaultActiveKey={["0"]}>
@@ -132,34 +160,40 @@ const Widget = ({ data, onSelect }: IWidget) => {
             <Panel header={item.word} style={{ padding: 0 }} key={id}>
               {item.dict.map((itemDict, idDict) => {
                 return (
-                  <div key={idDict}>
-                    <div>{itemDict.name}</div>
-                    {itemDict.case.map((itemCase, idCase) => {
-                      return (
-                        <div key={idCase}>
-                          <div>{itemCase.name}</div>
-                          <div>
-                            {itemCase.meaning.map((itemMeaning, idMeaning) => {
-                              return (
-                                <Tag
-                                  key={idMeaning}
-                                  onClick={(
-                                    e: React.MouseEvent<HTMLAnchorElement>
-                                  ) => {
-                                    e.preventDefault();
-                                    if (typeof onSelect !== "undefined") {
-                                      onSelect(itemMeaning.text);
-                                    }
-                                  }}
-                                >
-                                  {itemMeaning.text}-{itemMeaning.count}
-                                </Tag>
-                              );
-                            })}
+                  <div key={idDict} style={{ display: "flex" }}>
+                    <Text keyboard strong style={{ whiteSpace: "nowrap" }}>
+                      {itemDict.name}
+                    </Text>
+                    <div>
+                      {itemDict.case.map((itemCase, idCase) => {
+                        return (
+                          <div key={idCase}>
+                            <Text italic>{itemCase.local}</Text>
+                            <span>
+                              {itemCase.meaning.map(
+                                (itemMeaning, idMeaning) => {
+                                  return (
+                                    <Tag
+                                      key={idMeaning}
+                                      onClick={(
+                                        e: React.MouseEvent<HTMLAnchorElement>
+                                      ) => {
+                                        e.preventDefault();
+                                        if (typeof onSelect !== "undefined") {
+                                          onSelect(itemMeaning.text);
+                                        }
+                                      }}
+                                    >
+                                      {itemMeaning.text}-{itemMeaning.count}
+                                    </Tag>
+                                  );
+                                }
+                              )}
+                            </span>
                           </div>
-                        </div>
-                      );
-                    })}
+                        );
+                      })}
+                    </div>
                   </div>
                 );
               })}

+ 3 - 0
dashboard/src/components/template/Wbw/WbwWord.tsx

@@ -100,6 +100,7 @@ const Widget = ({
 }: IWidget) => {
   const [wordData, setWordData] = useState(data);
   const [fieldDisplay, setFieldDisplay] = useState(fields);
+  const [newFactors, setNewFactors] = useState<string>();
   const intervalRef = useRef<number | null>(null); //防抖计时器句柄
   const inlineWordIndex = useAppSelector(wordIndex);
 
@@ -214,6 +215,7 @@ const Widget = ({
                 console.log("factor change", e);
                 const newData: IWbw = JSON.parse(JSON.stringify(wordData));
                 newData.factors = { value: e, status: 5 };
+                setNewFactors(e);
                 setWordData(newData);
               }}
             />
@@ -222,6 +224,7 @@ const Widget = ({
             <WbwFactorMeaning
               data={wordData}
               display={display}
+              factors={newFactors}
               onChange={(e: string) => {
                 const newData: IWbw = JSON.parse(JSON.stringify(wordData));
                 newData.factorMeaning = { value: e, status: 5 };

+ 5 - 1
dashboard/src/components/template/utilities.ts

@@ -37,7 +37,11 @@ export function XmlToReact(
     for (let i = 0; i < attr.length; i++) {
       if (attr[i].nodeType === 2) {
         let key: string = attr[i].nodeName;
-        output[key] = attr[i].nodeValue;
+        if (key !== "style") {
+          output[key] = attr[i].nodeValue;
+        } else {
+          //TODO 把css style 转换为react style
+        }
       }
     }
     return output;

+ 1 - 1
dashboard/src/locales/zh-Hans/dict/index.ts

@@ -143,7 +143,7 @@ const items = {
   "dict.fields.type.v:base.label": "动词干",
   "dict.fields.type.v:base.short.label": "动词干",
   "dict.fields.type.adj:base.label": "形词干",
-  "dict.fields.type.ajd:base.short.label": "形词干",
+  "dict.fields.type.adj:base.short.label": "形词干",
 };
 
 export default items;

+ 2 - 1
dashboard/src/locales/zh-Hans/forms.ts

@@ -23,7 +23,7 @@ const items = {
   "forms.fields.publicity.public.label": "公开",
   "forms.fields.teacher.label": "主讲人",
   "forms.fields.studentsassistant.label": "学生与助教",
-  "forms.fields.students.label": "学生",
+  "forms.fields.student.label": "学生",
   "forms.fields.assistant.label": "助教",
   "forms.fields.lesson.label": "讲",
   "forms.fields.note.label": "注解",
@@ -53,6 +53,7 @@ const items = {
   "forms.message.user.delete": "删除用户吗?此操作无法恢复。",
   "forms.message.member.delete": "删除此成员吗?此操作无法恢复。",
   "forms.fields.description.label": "简介",
+  "forms.fields.textbook.label": "课本",
 };
 
 export default items;

+ 2 - 3
dashboard/src/pages/library/anthology/show.tsx

@@ -8,7 +8,6 @@ const { Content, Header } = Layout;
 const Widget = () => {
   // TODO
   const { id, tags } = useParams(); //url 参数
-  let aid = id ? id : "";
   let channel = tags ? tags : "";
 
   const pageMaxWidth = "1260px";
@@ -19,7 +18,7 @@ const Widget = () => {
           <Col flex="auto"></Col>
           <Col flex={pageMaxWidth}>
             <div>
-              {aid}@{channel}
+              {id}@{channel}
             </div>
           </Col>
           <Col flex="auto"></Col>
@@ -32,7 +31,7 @@ const Widget = () => {
           <Col flex={pageMaxWidth}>
             <Row>
               <Col span="18">
-                <AnthologyDetail aid={aid} />
+                <AnthologyDetail aid={id} />
               </Col>
               <Col span="6"></Col>
             </Row>

+ 7 - 7
dashboard/src/pages/library/blog/course.tsx

@@ -3,14 +3,14 @@ import { useParams } from "react-router-dom";
 import BlogNav from "../../../components/blog/BlogNav";
 
 const Widget = () => {
-	// TODO
-	const { studio } = useParams(); //url 参数
+  // TODO
+  const { studio } = useParams(); //url 参数
 
-	return (
-		<>
-			<BlogNav selectedKey="course" studio={studio ? studio : ""} />
-		</>
-	);
+  return (
+    <>
+      <BlogNav selectedKey="course" studio={studio} />
+    </>
+  );
 };
 
 export default Widget;

+ 14 - 14
dashboard/src/pages/library/community/index.tsx

@@ -5,21 +5,21 @@ import HeadBar from "../../../components/library/HeadBar";
 import FooterBar from "../../../components/library/FooterBar";
 
 const Widget = () => {
-	// TODO
-	return (
-		<Layout>
-			<HeadBar selectedKeys="community" />
-			<Row>
-				<Col flex="auto"></Col>
+  // TODO
+  return (
+    <Layout>
+      <HeadBar selectedKeys="community" />
+      <Row>
+        <Col flex="auto"></Col>
 
-				<Col flex="1260px">
-					<Outlet />
-				</Col>
-				<Col flex="auto"></Col>
-			</Row>
-			<FooterBar />
-		</Layout>
-	);
+        <Col flex="1260px">
+          <Outlet />
+        </Col>
+        <Col flex="auto"></Col>
+      </Row>
+      <FooterBar />
+    </Layout>
+  );
 };
 
 export default Widget;

+ 27 - 27
dashboard/src/pages/library/community/list.tsx

@@ -10,34 +10,34 @@ import ChapterTagList from "../../../components/corpus/ChapterTagList";
 
 const { Title } = Typography;
 const Widget = () => {
-	// TODO
-	const defaultTags: string[] = [];
-	const [tags, setTags] = useState(defaultTags);
+  // TODO
+  const defaultTags: string[] = [];
+  const [tags, setTags] = useState(defaultTags);
 
-	return (
-		<Row>
-			<Col xs={0} xl={6}>
-				<Affix offsetTop={0}>
-					<Layout style={{ height: "100vh", overflowY: "scroll" }}>
-						<BookTree />
-					</Layout>
-				</Affix>
-			</Col>
-			<Col xs={24} xl={14}>
-				<ChapterFileter />
-				<Title level={1}>{tags}</Title>
-				<ChapterList tags={tags} />
-			</Col>
-			<Col xs={0} xl={4}>
-				<ChapterTagList
-					onTagClick={(key: string) => {
-						setTags([key]);
-					}}
-				/>
-				<ChannelList />
-			</Col>
-		</Row>
-	);
+  return (
+    <Row>
+      <Col xs={0} xl={6}>
+        <Affix offsetTop={0}>
+          <Layout style={{ height: "100vh", overflowY: "scroll" }}>
+            <BookTree />
+          </Layout>
+        </Affix>
+      </Col>
+      <Col xs={24} xl={14}>
+        <ChapterFileter />
+        <Title level={1}>{tags}</Title>
+        <ChapterList tags={tags} />
+      </Col>
+      <Col xs={0} xl={4}>
+        <ChapterTagList
+          onTagClick={(key: string) => {
+            setTags([key]);
+          }}
+        />
+        <ChannelList />
+      </Col>
+    </Row>
+  );
 };
 
 export default Widget;

+ 19 - 23
dashboard/src/pages/library/community/recent.tsx

@@ -1,26 +1,22 @@
-import {Outlet,Link} from "react-router-dom";
-import {Space} from "antd";
+import { Outlet, Link } from "react-router-dom";
+import { Space } from "antd";
 
-const Widget = () =>{
-	return (
-		<div>
-			<div>
-				我的阅读
-			</div>
-			<div>
-				<Outlet />
-				<div>
-					<Space>
-						<Link to="/">Home</Link>
-						<Link to="/community">社区</Link>
-					</Space>
-				</div>
-			</div>
-			<div>
-				底部区域
-			</div>
-		</div>
-	)
-}
+const Widget = () => {
+  return (
+    <div>
+      <div>我的阅读</div>
+      <div>
+        <Outlet />
+        <div>
+          <Space>
+            <Link to="/">Home</Link>
+            <Link to="/community">社区</Link>
+          </Space>
+        </div>
+      </div>
+      <div>底部区域</div>
+    </div>
+  );
+};
 
 export default Widget;

+ 60 - 71
dashboard/src/pages/library/course/course.tsx

@@ -1,83 +1,72 @@
 //课程详情页面
-import { Link } from "react-router-dom";
 import { useParams } from "react-router-dom";
-import { Layout, Col, Row, Divider } from "antd";
-import CourseShow from "../../../components/library/course/CourseShow";
-import CourseIntro from "../../../components/library/course/CourseIntro";
-import TocTree from "../../../components/article/TocTree";
-import { ListNodeData } from "../../../components/article/EditableTree";
-import ReactMarkdown from "react-markdown";
-import rehypeRaw from "rehype-raw";
-import { marked } from "marked";
-const { Content, Header } = Layout;
+import { useEffect, useState } from "react";
+import { Divider, message } from "antd";
 
-let arrTocTree: ListNodeData[] = [];
-let i = 0;
-do {
-  ++i;
-  arrTocTree.push({
-    key: i.toString(),
-    title: `课程 ${i}`,
-    level: 1,
-  });
-} while (i < 10); // 在循环的尾部检查条件
+import CourseShow from "../../../components/course/CourseShow";
+import CourseIntro from "../../../components/course/CourseIntro";
+import TextBook from "../../../components/course/TextBook";
 
-let markdown =
-  "# 这是标题\n" +
-  "[ **M** ] arkdown + E [ **ditor** ] = **Mditor**  \n" +
-  "> Mditor 是一个简洁、易于集成、方便扩展、期望舒服的编写 markdown 的编辑器,仅此而已... \n\n" +
-  "**这是加粗的文字**\n\n" +
-  "*这是倾斜的文字*`\n\n" +
-  "***这是斜体加粗的文字***\n\n" +
-  "~~这是加删除线的文字~~ \n\n" +
-  "\n\n" +
-  "|表格头1|表格头2|表格头3| \n\n" +
-  "|------|------|------| \n\n" +
-  "| 文本 | 文本 | 文本 |\n\n" +
-  "\n\n" +
-  "```const a=2; ```";
+import { IUser } from "../../../components/auth/User";
+import { get } from "../../../request";
+import { ICourseResponse } from "../../../components/api/Course";
 
+export interface ICourse {
+  id: string; //课程ID
+  title: string; //标题
+  subtitle?: string; //副标题
+  teacher?: IUser; //UserID
+  privacy?: number; //公开性-公开/内部
+  createdAt?: string; //创建时间
+  updatedAt?: string; //修改时间
+  anthologyId?: string; //文集ID
+  channelId?: string;
+  startAt?: string; //课程开始时间
+  endAt?: string; //课程结束时间
+  intro?: string; //简介
+  coverUrl?: string; //封面图片文件名
+}
 const Widget = () => {
   // TODO
-  const { courseid } = useParams(); //url 参数
-
+  const { id } = useParams(); //url 参数
+  const [courseInfo, setCourseInfo] = useState<ICourse>();
+  useEffect(() => {
+    get<ICourseResponse>(`/v2/course/${id}`).then((json) => {
+      if (json.ok) {
+        console.log(json.data);
+        const course: ICourse = {
+          id: json.data.id,
+          title: json.data.title,
+          subtitle: json.data.subtitle,
+          teacher: json.data.teacher,
+          privacy: json.data.publicity,
+          createdAt: json.data.created_at,
+          updatedAt: json.data.updated_at,
+          anthologyId: json.data.anthology_id,
+          channelId: json.data.channel_id,
+          startAt: json.data.start_at,
+          endAt: json.data.end_at,
+          intro: json.data.content,
+          coverUrl: json.data.cover,
+        };
+        setCourseInfo(course);
+      } else {
+        message.error(json.message);
+      }
+    });
+  }, [id]);
   return (
-    <Layout>
-      <Content>
-        <Row>
-          <Col flex="auto"></Col>
-          <Col flex="1760px">
-            <div>
-              <div>
-                <CourseShow />
-                <Divider />
-                <div
-                  dangerouslySetInnerHTML={{
-                    __html: marked.parse(markdown),
-                  }}
-                ></div>
-                <CourseIntro />
-                <Divider />
-                <TocTree treeData={arrTocTree} />
-              </div>
-            </div>
-          </Col>
-        </Row>
-      </Content>
-    </Layout>
+    <div>
+      <CourseShow {...courseInfo} />
+      <Divider />
+      <CourseIntro {...courseInfo} />
+      <Divider />
+      <TextBook
+        anthologyId={courseInfo?.anthologyId}
+        courseId={courseInfo?.id}
+      />
+    </div>
   );
 };
 
 export default Widget;
-
-/*
-  return (
-    <div>
-      <div>课程{courseid} 详情</div>
-      <div>
-        <Link to="/course/lesson/12345">lesson 1</Link>
-        <Link to="/course/lesson/23456">lesson 2</Link>
-      </div>
-    </div>
-  );
-*/

+ 4 - 9
dashboard/src/pages/library/course/lesson.tsx

@@ -1,15 +1,11 @@
 //讲页面
-import { Link } from "react-router-dom";
-import { useParams } from "react-router-dom";
 import { Layout, Col, Row, Divider } from "antd";
-import CourseShow from "../../../components/library/course/CourseShow";
-import CourseIntro from "../../../components/library/course/CourseIntro";
+
 import TocTree from "../../../components/article/TocTree";
 import { ListNodeData } from "../../../components/article/EditableTree";
-import ReactMarkdown from "react-markdown";
-import rehypeRaw from "rehype-raw";
+
 import { marked } from "marked";
-const { Content, Header } = Layout;
+const { Content } = Layout;
 
 let arrTocTree: ListNodeData[] = [];
 let i = 0;
@@ -38,8 +34,7 @@ let markdown =
   "```const a=2; ```";
 
 const Widget = () => {
-  // TODO
-  const { courseid } = useParams(); //url 参数
+  // TODO delete
 
   return (
     <Layout>

+ 24 - 48
dashboard/src/pages/library/course/list.tsx

@@ -1,76 +1,52 @@
 //课程主页
-import { Link } from "react-router-dom";
-import { Space, Input } from "antd";
-import { Layout, Affix, Col, Row, Divider } from "antd";
+import { Layout, Col, Row, Divider } from "antd";
 
-import LecturerList from "../../../components/library/course/LecturerList";
-import CourseList from "../../../components/library/course/CourseList";
+import LecturerList from "../../../components/course/LecturerList";
+import CourseList from "../../../components/course/CourseList";
 const { Content, Header } = Layout;
-const { Search } = Input;
+
 const Widget = () => {
   // TODO i18n
   return (
     <Layout>
       <Header style={{ height: 200 }}>
-        <h1 style={{"color": "white", "fontWeight": 'bold', "fontSize": 40}}>课程</h1>
-        <p style={{"color": "white", "fontSize": 17}}>看看世界各地的巴利专家都是如何解析圣典的</p>
+        <h1 style={{ color: "white", fontWeight: "bold", fontSize: 40 }}>
+          课程
+        </h1>
+        <p style={{ color: "white", fontSize: 17 }}>
+          看看世界各地的巴利专家都是如何解析圣典的
+        </p>
       </Header>
 
-
       <Content>
         <Row>
           <Col flex="auto"></Col>
-          <Col flex="1260px">
-          <Row>
-              
-          <h1>主讲人</h1>
-              </Row>
+          <Col flex="960px">
+            <Row>
+              <h1>最新</h1>
+            </Row>
             <Row>
-              
-            <div>
               <LecturerList />
-            </div>
             </Row>
-          </Col>
-          <Col flex="auto"></Col>
-        </Row>
-        <Space></Space>
-        <Row>
-          <Col flex="auto"></Col>
-          <Col flex="1260px">
-          <Row>
-          <Divider />   
-          <h1>正在进行</h1>
-              </Row>
+            <Divider />
             <Row>
-              
-            <div>
-              <CourseList />
-            </div>
+              <h1>正在进行</h1>
             </Row>
-          </Col>
-          <Col flex="auto"></Col>
-        </Row>
-        <Space></Space>
-        <Row>
-          <Col flex="auto"></Col>
-          <Col flex="1260px">
-          <Row>
-          <Divider />
-          <h1>已经结束</h1>
-              </Row>
             <Row>
-              
-            <div>
-              <CourseList />
-            </div>
+              <CourseList type="open" />
+            </Row>
+            <Divider />
+            <Row>
+              <h1>已经结束</h1>
+            </Row>
+            <Row>
+              <CourseList type="close" />
             </Row>
           </Col>
           <Col flex="auto"></Col>
         </Row>
       </Content>
     </Layout>
-
   );
 };
 

+ 2 - 2
dashboard/src/pages/studio/article/edit.tsx

@@ -22,8 +22,8 @@ interface IFormData {
   title: string;
   subtitle: string;
   summary: string;
-  content: string;
-  content_type: string;
+  content?: string;
+  content_type?: string;
   status: number;
   lang: string;
 }

+ 0 - 3
dashboard/src/pages/studio/channel/edit.tsx

@@ -66,9 +66,6 @@ const Widget = () => {
             rules={[
               {
                 required: true,
-                message: intl.formatMessage({
-                  id: "channel.create.message.noname",
-                }),
               },
             ]}
           />

+ 312 - 208
dashboard/src/pages/studio/course/edit.tsx

@@ -1,240 +1,344 @@
 import { useState } from "react";
 import { useParams } from "react-router-dom";
-import { useIntl, FormattedMessage } from "react-intl";
+import { useIntl } from "react-intl";
 import {
   ProForm,
   ProFormText,
-  ProFormTextArea,
   ProFormDateRangePicker,
+  ProFormSelect,
+  ProFormUploadButton,
+  RequestOptionsType,
 } from "@ant-design/pro-components";
-import { Card, message, Col, Row, Divider, Tabs } from "antd";
-import { get, put } from "../../../request";
-import { marked } from "marked";
+import { UsergroupAddOutlined } from "@ant-design/icons";
+import { Card, message, Form, Button, Drawer } from "antd";
+
+import { API_HOST, get, put } from "../../../request";
 import {
   ICourseDataRequest,
+  ICourseDataResponse,
   ICourseResponse,
 } from "../../../components/api/Course";
 import PublicitySelect from "../../../components/studio/PublicitySelect";
 import GoBack from "../../../components/studio/GoBack";
-import UploadTexture from "../../../components/library/course/UploadTexture";
-import TeacherSelect from "../../../components/library/course/TeacherSelect";
-import StudentsSelect from "../../../components/library/course/StudentsSelect";
-import LessonSelect from "../../../components/library/course/LessonSelect";
-import LessonTreeShow from "../../../components/library/course/LessonTreeShow";
+
+import { IUserListResponse } from "../../../components/api/Auth";
+import MDEditor from "@uiw/react-md-editor";
+import { DefaultOptionType } from "antd/lib/select";
+import { UploadFile } from "antd/es/upload/interface";
+import { IAttachmentResponse } from "../../../components/api/Attachments";
+import CourseMember from "../../../components/course/CourseMember";
+import { IAnthologyListResponse } from "../../../components/api/Article";
+import { IApiResponseChannelList } from "../../../components/api/Channel";
 
 interface IFormData {
-  uid: string;
   title: string;
-
-  t_type: string;
+  subtitle: string;
+  content?: string;
+  cover?: UploadFile<IAttachmentResponse>[];
+  teacherId?: string;
+  anthologyId?: string;
+  channelId?: string;
+  dateRange?: Date[];
   status: number;
-  lang: string;
 }
-const onChange = (key: string) => {
-  console.log(key);
-};
-let groupid = "1";
 
 const Widget = () => {
   const intl = useIntl();
   const { studioname, courseId } = useParams(); //url 参数
   const [title, setTitle] = useState("loading");
+  const [contentValue, setContentValue] = useState<string>();
+  const [teacherOption, setTeacherOption] = useState<DefaultOptionType[]>([]);
+  const [currTeacher, setCurrTeacher] = useState<RequestOptionsType>();
+  const [textbookOption, setTextbookOption] = useState<DefaultOptionType[]>([]);
+  const [currTextbook, setCurrTextbook] = useState<RequestOptionsType>();
+  const [channelOption, setChannelOption] = useState<DefaultOptionType[]>([]);
+  const [currChannel, setCurrChannel] = useState<RequestOptionsType>();
+  const [openMember, setOpenMember] = useState(false);
+  const [courseData, setCourseData] = useState<ICourseDataResponse>();
 
   return (
-    <Tabs
-      onChange={onChange}
-      type="card"
-      items={[
-        {
-          label: `基本信息`,
-          key: "1",
-          children: (
-            <Card
-              title={
-                <GoBack
-                  to={`/studio/${studioname}/course/list`}
-                  title={title}
-                />
-              }
-            >
-              <ProForm<IFormData>
-                onFinish={async (values: IFormData) => {
-                  // TODO
-                  let request = {
-                    uid: courseId?.toString,
-                    title: "课程" + courseId,
-                    subtitle: "课程副标题" + courseId,
-                    teacher: 1,
-                    course_count: 2,
-                    type: 30,
-                    created_at: "",
-                    updated_at: "",
-                    article_id: 1, //"1e642dac-dcb2-468a-8cc7-0228e5ca6ac4",
-                    course_start_at: "", //课程开始时间
-                    course_end_at: "", //课程结束时间
-                    intro_markdown: "", //简介
-                    cover_img_name: "", //封面图片文件名
-                  };
-                }}
-                /*		    const request = {
-    uid: courseid ? courseid : "",
-    title: values.title,
-    subtitle: values.subtitle,
-    teacher: values.teacher,//UserID
-    course_count: values.course_count,//课程数
-    type: values.type,//类型-公开/内部
-    created_at: values.created_at,//创建时间
-    updated_at: values.updated_at,//修改时间
-    article_id: values.article_id,//文集ID
-    course_start_at: values.course_start_at,//课程开始时间
-    course_end_at: values.course_end_at,//课程结束时间
-    intro_markdown: values.intro_markdown,//简介
-    cover_img_name: values.cover_img_name,//封面图片文件名
-  };
-  console.log(request);
-  const res = await put<ICourseDataRequest, ICourseResponse>(
-    `/v2/course/${courseid}`,
-    request
-  );
-  console.log(res);
-  if (res.ok) {
-    message.success(intl.formatMessage({ id: "flashes.success" }));
-  } else {
-    message.error(res.message);
-  }
-}}
-request={async () => {
-  const res = await get<ICourseResponse>(`/v2/course/${courseid}`);
-  setTitle(res.data.title);
-  return {
-    uid: res.data.uid,
-    title: res.data.title,
-    subtitle: res.data.subtitle,
-    summary: res.data.summary,
-    content: res.data.content,
-    content_type: res.data.content_type,
-    lang: res.data.lang,
-    status: res.data.status,
-  };
-}}*/
-              >
-                <ProForm.Group>
-                  <ProFormText
-                    width="md"
-                    name="title"
-                    required
-                    label={intl.formatMessage({
-                      id: "forms.fields.title.label",
-                    })}
-                    rules={[
-                      {
-                        required: true,
-                        message: intl.formatMessage({
-                          id: "forms.message.title.required",
-                        }),
-                      },
-                    ]}
-                  />
-                </ProForm.Group>
-                <ProForm.Group>
-                  <ProFormText
-                    width="md"
-                    name="subtitle"
-                    label={intl.formatMessage({
-                      id: "forms.fields.subtitle.label",
-                    })}
-                  />
-                </ProForm.Group>
-                <ProForm.Group>
-                  <p style={{ fontWeight: "bold", fontSize: 15 }}>
-                    <FormattedMessage id="forms.fields.upload.texture" />{" "}
-                  </p>
-                  <UploadTexture />
-                </ProForm.Group>
-                <ProForm.Group>
-                  <ProFormDateRangePicker
-                    width="md"
-                    name={["contract", "createTime"]}
-                    label="课程区间"
-                  />
-                </ProForm.Group>
-                <ProForm.Group>
-                  <PublicitySelect />
-                </ProForm.Group>
-                <Divider />
+    <>
+      <Card
+        title={
+          <GoBack to={`/studio/${studioname}/course/list`} title={title} />
+        }
+        extra={
+          <Button
+            icon={<UsergroupAddOutlined />}
+            onClick={() => {
+              setOpenMember(true);
+            }}
+          >
+            成员
+          </Button>
+        }
+      >
+        <ProForm<IFormData>
+          formKey="course_edit"
+          onFinish={async (values: IFormData) => {
+            console.log("all data", values);
+            let startAt: string, endAt: string;
+            let _cover: string = "";
+            if (typeof values.dateRange === "undefined") {
+              startAt = "";
+              endAt = "";
+            } else if (
+              typeof values.dateRange[0] === "string" &&
+              typeof values.dateRange[1] === "string"
+            ) {
+              startAt = values.dateRange[0];
+              endAt = values.dateRange[1];
+            } else {
+              startAt = courseData ? courseData.start_at : "";
+              endAt = courseData ? courseData.end_at : "";
+            }
 
-                <Row>
-                  <Col flex="400px">
-                    <TeacherSelect groupId={groupid} />
-                  </Col>
-                </Row>
-                <Divider />
-                <Row>
-                  <Col flex="400px">
-                    <LessonSelect groupId={groupid} />
-                  </Col>
-                </Row>
-                <Divider />
-                <ProForm.Group>
-                  <ProFormTextArea
-                    name="summary"
-                    width="md"
-                    label={intl.formatMessage({
-                      id: "forms.fields.summary.label",
-                    })}
-                  />
+            if (
+              typeof values.cover === "undefined" ||
+              values.cover.length === 0
+            ) {
+              _cover = "";
+            } else if (typeof values.cover[0].response === "undefined") {
+              _cover = values.cover[0].uid;
+            } else {
+              _cover = values.cover[0].response.data.url;
+            }
 
-                  <p style={{ fontWeight: "bold", fontSize: 15 }}>
-                    <FormattedMessage id="forms.fields.markdown.label" />{" "}
-                  </p>
-                  <Row>
-                    <div
-                      dangerouslySetInnerHTML={{
-                        __html: marked.parse(
-                          "# 这是标题\n" +
-                            "[ **M** ] arkdown + E [ **ditor** ] = **Mditor**  \n" +
-                            "**这是加粗的文字**\n\n" +
-                            "*这是倾斜的文字*`\n\n" +
-                            "***这是斜体加粗的文字***\n\n" +
-                            "~~这是加删除线的文字~~ \n\n"
-                        ),
-                      }}
-                    ></div>
-                  </Row>
-                </ProForm.Group>
-              </ProForm>
-            </Card>
-          ),
-        },
-        {
-          label: `学生与助教选择 `,
-          key: "2",
-          children: (
-            <Card
-              title={
-                <GoBack
-                  to={`/studio/${studioname}/course/list`}
-                  title={title}
-                />
+            const res = await put<ICourseDataRequest, ICourseResponse>(
+              `/v2/course/${courseId}`,
+              {
+                title: values.title, //标题
+                subtitle: values.subtitle, //副标题
+                content: contentValue, //简介
+                cover: _cover, //封面图片文件名
+                teacher_id: values.teacherId, //UserID
+                publicity: values.status, //类型-公开/内部
+                anthology_id: values.anthologyId, //文集ID
+                channel_id: values.channelId,
+                start_at: startAt, //课程开始时间
+                end_at: endAt, //课程结束时间
               }
-            >
-              <ProForm<IFormData> onFinish={async (values: IFormData) => {}}>
-                <ProForm.Group>
-                  <LessonTreeShow />
-                </ProForm.Group>
-                <ProForm.Group></ProForm.Group>
+            );
+            console.log(res);
+            if (res.ok) {
+              message.success(intl.formatMessage({ id: "flashes.success" }));
+            } else {
+              message.error(res.message);
+            }
+          }}
+          request={async () => {
+            const res = await get<ICourseResponse>(`/v2/course/${courseId}`);
+            setCourseData(res.data);
+            setTitle(res.data.title);
+            console.log(res.data);
+            setContentValue(res.data.content);
+            if (res.data.teacher) {
+              console.log("teacher", res.data.teacher);
+              const teacher = {
+                value: res.data.teacher.id,
+                label: res.data.teacher.nickName,
+              };
+              setCurrTeacher(teacher);
+              setTeacherOption([teacher]);
+              const textbook = {
+                value: res.data.anthology_id,
+                label:
+                  res.data.anthology_owner?.nickName +
+                  "/" +
+                  res.data.anthology_title,
+              };
+              setCurrTextbook(textbook);
+              setTextbookOption([textbook]);
+              const channel = {
+                value: res.data.channel_id,
+                label:
+                  res.data.channel_owner?.nickName +
+                  "/" +
+                  res.data.channel_name,
+              };
+              setCurrChannel(channel);
+              setChannelOption([channel]);
+            }
+            return {
+              title: res.data.title,
+              subtitle: res.data.subtitle,
+              content: res.data.content,
+              cover: res.data.cover
+                ? [
+                    {
+                      uid: res.data.cover,
+                      name: "cover",
+                      thumbUrl: API_HOST + "/" + res.data.cover,
+                    },
+                  ]
+                : [],
+              teacherId: res.data.teacher?.id,
+              anthologyId: res.data.anthology_id,
+              channelId: res.data.channel_id,
+              dateRange:
+                res.data.start_at && res.data.end_at
+                  ? [new Date(res.data.start_at), new Date(res.data.end_at)]
+                  : undefined,
+              status: res.data.publicity,
+            };
+          }}
+        >
+          <ProForm.Group>
+            <ProFormUploadButton
+              name="cover"
+              label="封面"
+              max={1}
+              fieldProps={{
+                name: "file",
+                listType: "picture-card",
+                className: "avatar-uploader",
+              }}
+              action={`${API_HOST}/api/v2/attachments`}
+              extra="封面必须为正方形。最大512*512"
+            />
+          </ProForm.Group>
+          <ProForm.Group>
+            <ProFormText
+              width="md"
+              name="title"
+              required
+              label={intl.formatMessage({
+                id: "forms.fields.title.label",
+              })}
+              rules={[
+                {
+                  required: true,
+                },
+              ]}
+            />
+            <ProFormText
+              width="md"
+              name="subtitle"
+              label={intl.formatMessage({
+                id: "forms.fields.subtitle.label",
+              })}
+            />
+          </ProForm.Group>
+
+          <ProForm.Group>
+            <ProFormSelect
+              options={teacherOption}
+              width="md"
+              name="teacherId"
+              label={intl.formatMessage({ id: "forms.fields.teacher.label" })}
+              showSearch
+              debounceTime={300}
+              request={async ({ keyWords }) => {
+                console.log("keyWord", keyWords);
+                if (typeof keyWords === "undefined") {
+                  return currTeacher ? [currTeacher] : [];
+                }
+                const json = await get<IUserListResponse>(
+                  `/v2/user?view=key&key=${keyWords}`
+                );
+                const userList = json.data.rows.map((item) => {
+                  return {
+                    value: item.id,
+                    label: `${item.userName}-${item.nickName}`,
+                  };
+                });
+                console.log("json", userList);
+                return userList;
+              }}
+              placeholder={intl.formatMessage({
+                id: "forms.fields.teacher.label",
+              })}
+            />
+            <ProFormDateRangePicker
+              width="md"
+              name="dateRange"
+              label="课程区间"
+            />
+          </ProForm.Group>
 
-                <Row>
-                  <Col flex="400px">
-                    <StudentsSelect groupId={groupid} />
-                  </Col>
-                </Row>
-              </ProForm>
-            </Card>
-          ),
-        },
-      ]}
-    />
+          <ProForm.Group>
+            <ProFormSelect
+              options={textbookOption}
+              width="md"
+              name="anthologyId"
+              label={intl.formatMessage({ id: "forms.fields.textbook.label" })}
+              showSearch
+              debounceTime={300}
+              request={async ({ keyWords }) => {
+                console.log("keyWord", keyWords);
+                if (typeof keyWords === "undefined") {
+                  return currTextbook ? [currTextbook] : [];
+                }
+                const json = await get<IAnthologyListResponse>(
+                  `/v2/anthology?view=public`
+                );
+                const textbookList = json.data.rows.map((item) => {
+                  return {
+                    value: item.uid,
+                    label: `${item.studio.nickName}/${item.title}`,
+                  };
+                });
+                console.log("json", textbookList);
+                return textbookList;
+              }}
+            />
+            <ProFormSelect
+              options={channelOption}
+              width="md"
+              name="channelId"
+              label={"标准答案"}
+              showSearch
+              debounceTime={300}
+              request={async ({ keyWords }) => {
+                console.log("keyWord", keyWords);
+                if (typeof keyWords === "undefined") {
+                  return currChannel ? [currChannel] : [];
+                }
+                const json = await get<IApiResponseChannelList>(
+                  `/v2/channel?view=studio&name=${studioname}`
+                );
+                const textbookList = json.data.rows.map((item) => {
+                  return {
+                    value: item.uid,
+                    label: `${item.studio.nickName}/${item.name}`,
+                  };
+                });
+                console.log("json", textbookList);
+                return textbookList;
+              }}
+            />
+          </ProForm.Group>
+          <ProForm.Group>
+            <PublicitySelect />
+          </ProForm.Group>
+          <ProForm.Group>
+            <Form.Item
+              name="content"
+              label={intl.formatMessage({ id: "forms.fields.summary.label" })}
+            >
+              <MDEditor
+                value={contentValue}
+                onChange={(value: string | undefined) => {
+                  if (value) {
+                    setContentValue(value);
+                  }
+                }}
+              />
+            </Form.Item>
+          </ProForm.Group>
+        </ProForm>
+      </Card>
+      <Drawer
+        title="课程成员"
+        placement="right"
+        onClose={() => {
+          setOpenMember(false);
+        }}
+        open={openMember}
+      >
+        <CourseMember courseId={courseId} />
+      </Drawer>
+    </>
   );
 };
 

+ 102 - 98
dashboard/src/pages/studio/course/list.tsx

@@ -1,6 +1,6 @@
 import { useParams, Link } from "react-router-dom";
 import { useIntl } from "react-intl";
-import React, { useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
 import {
   Space,
   Badge,
@@ -10,14 +10,19 @@ import {
   MenuProps,
   Menu,
   Table,
+  Image,
 } from "antd";
-import { ProTable, ProList } from "@ant-design/pro-components";
+import { ProTable, ActionType } from "@ant-design/pro-components";
 import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
 
-import CourseCreate from "../../../components/library/course/CourseCreate";
-import { get } from "../../../request";
-import { ICourseListResponse } from "../../../components/api/Course";
+import CourseCreate from "../../../components/course/CourseCreate";
+import { API_HOST, get } from "../../../request";
+import {
+  ICourseListResponse,
+  ICourseNumberResponse,
+} from "../../../components/api/Course";
 import { PublicityValueEnum } from "../../../components/studio/table";
+
 const onMenuClick: MenuProps["onClick"] = (e) => {
   console.log("click", e);
 };
@@ -27,12 +32,12 @@ const menu = (
     onClick={onMenuClick}
     items={[
       {
-        key: "1",
-        label: "分享",
+        key: "manage",
+        label: "管理",
         icon: <SearchOutlined />,
       },
       {
-        key: "2",
+        key: "delete",
         label: "删除",
         icon: <SearchOutlined />,
       },
@@ -44,16 +49,17 @@ interface DataItem {
   id: string; //课程ID
   title: string; //标题
   subtitle: string; //副标题
-  teacher: string; //UserID
-  //course_count: number;//课程数
+  teacher?: string; //UserID
+  course_count?: number; //课程数
+  member_count: number; //成员数量
   type: number; //类型-公开/内部
   createdAt: number; //创建时间
-  //updated_at: number;//修改时间
-  //article_id: number;//文集ID
-  //course_start_at: string;//课程开始时间
-  //course_end_at: string;//课程结束时间
-  //intro_markdown: string;//简介
-  //cover_img_name: string;//封面图片文件名
+  updatedAt?: number; //修改时间
+  article_id?: string; //文集ID
+  course_start_at?: string; //课程开始时间
+  course_end_at?: string; //课程结束时间
+  intro_markdown?: string; //简介
+  cover_img_name?: string; //封面图片文件名
 }
 
 const renderBadge = (count: number, active = false) => {
@@ -72,11 +78,43 @@ const renderBadge = (count: number, active = false) => {
 const Widget = () => {
   const intl = useIntl(); //i18n
   const { studioname } = useParams(); //url 参数
-  const courseCreate = <CourseCreate studio={studioname} />;
-  const [activeKey, setActiveKey] = useState<React.Key | undefined>("tab1");
+
+  const [activeKey, setActiveKey] = useState<React.Key | undefined>("create");
+  const [createNumber, setCreateNumber] = useState<number>(0);
+  const [teachNumber, setTeachNumber] = useState<number>(0);
+  const [studyNumber, setStudyNumber] = useState<number>(0);
+  const ref = useRef<ActionType>();
+  const [openCreate, setOpenCreate] = useState(false);
+
+  const courseCreate = (
+    <CourseCreate
+      studio={studioname}
+      onCreate={() => {
+        //新建课程成功后刷新
+        setActiveKey("create");
+        setCreateNumber(createNumber + 1);
+        ref.current?.reload();
+        setOpenCreate(false);
+      }}
+    />
+  );
+  useEffect(() => {
+    /**
+     * 获取各种课程的数量
+     */
+    const url = `/v2/course-my-course?studio=${studioname}`;
+    get<ICourseNumberResponse>(url).then((json) => {
+      if (json.ok) {
+        setCreateNumber(json.data.create);
+        setTeachNumber(json.data.teach);
+        setStudyNumber(json.data.study);
+      }
+    });
+  }, [studioname]);
   return (
     <>
       <ProTable<DataItem>
+        actionRef={ref}
         columns={[
           {
             title: intl.formatMessage({
@@ -98,22 +136,24 @@ const Widget = () => {
             ellipsis: true,
             render: (text, row, index, action) => {
               return (
-                <Link to={`/studio/${studioname}/course/${row.id}/edit`}>
-                  {row.title}
-                </Link>
+                <Space>
+                  <Image
+                    src={`${API_HOST}/${row.cover_img_name}`}
+                    width={64}
+                    fallback={`${API_HOST}/app/course/img/default.jpg`}
+                  />
+                  <div>
+                    <div>
+                      <Link to={`/course/show/${row.id}`} target="_blank">
+                        {row.title}
+                      </Link>
+                    </div>
+                    <div>{row.subtitle}</div>
+                  </div>
+                </Space>
               );
             },
           },
-          {
-            //副标题
-            title: intl.formatMessage({
-              id: "forms.fields.subtitle.label",
-            }),
-            dataIndex: "subtitle",
-            key: "subtitle",
-            tip: "过长会自动收缩",
-            ellipsis: true,
-          },
           {
             //主讲人
             title: intl.formatMessage({
@@ -124,6 +164,12 @@ const Widget = () => {
             //tip: "过长会自动收缩",
             ellipsis: true,
           },
+          {
+            title: "成员",
+            dataIndex: "member_count",
+            key: "member_count",
+            width: 80,
+          },
           {
             //类型
             title: intl.formatMessage({
@@ -211,7 +257,8 @@ const Widget = () => {
         request={async (params = {}, sorter, filter) => {
           // TODO
           console.log(params, sorter, filter);
-          let url = `/v2/course?view=studio&name=${studioname}`;
+          console.log(activeKey);
+          let url = `/v2/course?view=${activeKey}&studio=${studioname}`;
           const offset =
             ((params.current ? params.current : 1) - 1) *
             (params.pageSize ? params.pageSize : 20);
@@ -220,49 +267,25 @@ const Widget = () => {
             url += "&search=" + (params.keyword ? params.keyword : "");
           }
 
-          /*const res = await get<ICourseListResponse>(url);
+          const res = await get<ICourseListResponse>(url);
+          console.log("api data", res);
           const items: DataItem[] = res.data.rows.map((item, id) => {
             const date = new Date(item.created_at);
             return {
               sn: id + 1,
-              id: item.uid,
+              id: item.id,
               title: item.title,
               subtitle: item.subtitle,
-              teacher: item.teacher,
-              type: item.type,
+              teacher: item.teacher?.nickName,
+              cover_img_name: item.cover,
+              type: item.publicity,
+              member_count: item.member_count,
               createdAt: date.getTime(),
             };
-          });*/
-
-          //const items = Array.from({ length: 23 }).map((_, i) => ({
-          const items: DataItem[] = [
-            {
-              sn: 1,
-              id: "1",
-              title: "课程" + 1,
-              subtitle: "课程副标题" + 1,
-              teacher: "小僧善巧",
-              type: 30,
-              createdAt: 20020202,
-              //updated_at: 123,
-              //article_id: 123,
-              //course_start_at: 123,
-              //course_end_at: 123,
-              //intro_markdown: 123,
-              //cover_img_name: 123,
-            },
-            {
-              sn: 2,
-              id: "2",
-              title: "课程" + 2,
-              subtitle: "课程副标题" + 2,
-              teacher: "小僧善巧",
-              type: 30,
-              createdAt: 20020202,
-            },
-          ];
+          });
+          console.log(items);
           return {
-            total: items.length, //res.data.count,
+            total: res.data.count,
             succcess: true,
             data: items,
           };
@@ -282,6 +305,11 @@ const Widget = () => {
             content={courseCreate}
             title="Create"
             placement="bottomRight"
+            trigger="click"
+            open={openCreate}
+            onOpenChange={(newOpen: boolean) => {
+              setOpenCreate(newOpen);
+            }}
           >
             <Button key="button" icon={<PlusOutlined />} type="primary">
               {intl.formatMessage({ id: "buttons.create" })}
@@ -296,7 +324,8 @@ const Widget = () => {
                 key: "create",
                 label: (
                   <span>
-                    我建立的课程{renderBadge(99, activeKey === "create")}
+                    我建立的课程
+                    {renderBadge(createNumber, activeKey === "create")}
                   </span>
                 ),
               },
@@ -304,7 +333,8 @@ const Widget = () => {
                 key: "study",
                 label: (
                   <span>
-                    我参加的课程{renderBadge(99, activeKey === "study")}
+                    我参加的课程
+                    {renderBadge(studyNumber, activeKey === "study")}
                   </span>
                 ),
               },
@@ -312,45 +342,19 @@ const Widget = () => {
                 key: "teach",
                 label: (
                   <span>
-                    我任教的课程{renderBadge(32, activeKey === "teach")}
+                    我任教的课程
+                    {renderBadge(teachNumber, activeKey === "teach")}
                   </span>
                 ),
               },
             ],
             onChange(key) {
+              console.log("show course", key);
               setActiveKey(key);
+              ref.current?.reload();
             },
           },
         }}
-        /*
-        toolbar={{
-          menu: {
-            activeKey,
-            items: [
-              {
-                key: 'tab1',
-                label: <span>全部实验室{renderBadge(99, activeKey === 'tab1')}</span>,
-              },
-              {
-                key: 'tab2',
-                label: <span>我创建的实验室{renderBadge(32, activeKey === 'tab2')}</span>,
-              },
-            ],
-            onChange(key) {
-              setActiveKey(key);
-            },
-          },
-          search: {
-            onSearch: (value: string) => {
-              alert(value);
-            },
-          },
-          actions: [
-            <Button type="primary" key="primary">
-              新建实验
-            </Button>,
-          ],
-        }}*/
       />
     </>
   );

+ 27 - 9
dashboard/src/pages/studio/course/show.tsx

@@ -1,12 +1,22 @@
-import { useIntl } from "react-intl";
 import { useEffect, useState } from "react";
 import { useParams } from "react-router-dom";
-import { Button, Card } from "antd";
+import { Card, Col, Row } from "antd";
 
 import GoBack from "../../../components/studio/GoBack";
+import { ProForm } from "@ant-design/pro-components";
+import LessonTreeShow from "../../../components/course/LessonTreeShow";
+import StudentsSelect from "../../../components/course/StudentsSelect";
+
+interface IFormData {
+  uid: string;
+  title: string;
+
+  t_type: string;
+  status: number;
+  lang: string;
+}
 
 const Widget = () => {
-  const intl = useIntl();
   const { studioname, courseId } = useParams(); //url 参数
   const [title, setTitle] = useState("loading");
   useEffect(() => {
@@ -15,12 +25,20 @@ const Widget = () => {
   return (
     <Card
       title={<GoBack to={`/studio/${studioname}/course/list`} title={title} />}
-      extra={
-        <Button type="link" danger>
-          {intl.formatMessage({ id: "buttons.group.exit" })}
-        </Button>
-      }
-    ></Card>
+    >
+      <ProForm<IFormData> onFinish={async (values: IFormData) => {}}>
+        <ProForm.Group>
+          <LessonTreeShow />
+        </ProForm.Group>
+        <ProForm.Group></ProForm.Group>
+
+        <Row>
+          <Col flex="400px">
+            <StudentsSelect />
+          </Col>
+        </Row>
+      </ProForm>
+    </Card>
   );
 };