Sfoglia il codice sorgente

Merge pull request #1046 from visuddhinanda/agile

显示discussion的原文锚点内容
visuddhinanda 3 anni fa
parent
commit
c66d6fcf65
45 ha cambiato i file con 1058 aggiunte e 548 eliminazioni
  1. 29 0
      dashboard/src/Router.tsx
  2. 1 1
      dashboard/src/components/api/Auth.ts
  3. 5 0
      dashboard/src/components/api/Comment.ts
  4. 33 1
      dashboard/src/components/api/Group.ts
  5. 27 0
      dashboard/src/components/api/Share.ts
  6. 1 1
      dashboard/src/components/article/ArticleCard.tsx
  7. 1 5
      dashboard/src/components/auth/ToStudio.tsx
  8. 56 0
      dashboard/src/components/comment/AnchorCard.tsx
  9. 33 0
      dashboard/src/components/comment/CommentAnchor.tsx
  10. 6 12
      dashboard/src/components/comment/CommentBox.tsx
  11. 3 3
      dashboard/src/components/comment/CommentCreate.tsx
  12. 4 11
      dashboard/src/components/comment/CommentEdit.tsx
  13. 7 7
      dashboard/src/components/comment/CommentItem.tsx
  14. 11 74
      dashboard/src/components/comment/CommentTopic.tsx
  15. 91 0
      dashboard/src/components/comment/CommentTopicChildren.tsx
  16. 0 30
      dashboard/src/components/comment/CommentTopicHead.tsx
  17. 68 0
      dashboard/src/components/comment/CommentTopicInfo.tsx
  18. 0 30
      dashboard/src/components/comment/CommentTopicList.tsx
  19. 35 12
      dashboard/src/components/group/AddMember.tsx
  20. 101 54
      dashboard/src/components/group/GroupFile.tsx
  21. 126 65
      dashboard/src/components/group/GroupMember.tsx
  22. 6 1
      dashboard/src/components/nut/users/NonSignInSharedLinks.tsx
  23. 1 1
      dashboard/src/components/nut/users/SignIn.tsx
  24. 25 29
      dashboard/src/components/studio/HeadBar.tsx
  25. 155 148
      dashboard/src/components/studio/LeftSider.tsx
  26. 6 2
      dashboard/src/components/template/MdView.tsx
  27. 3 0
      dashboard/src/components/template/SentEdit/SentTab.tsx
  28. 1 1
      dashboard/src/components/template/Wbw/WbwPali.tsx
  29. 20 37
      dashboard/src/layouts/anonymous/index.tsx
  30. 1 0
      dashboard/src/locales/zh-Hans/buttons.ts
  31. 3 0
      dashboard/src/locales/zh-Hans/forms.ts
  32. 2 1
      dashboard/src/locales/zh-Hans/nut/index.ts
  33. 24 0
      dashboard/src/pages/library/discussion/index.tsx
  34. 15 0
      dashboard/src/pages/library/discussion/list.tsx
  35. 19 0
      dashboard/src/pages/library/discussion/show.tsx
  36. 22 0
      dashboard/src/pages/library/discussion/topic.tsx
  37. 7 5
      dashboard/src/pages/nut/users/forgot-password.tsx
  38. 10 1
      dashboard/src/pages/nut/users/reset-password.tsx
  39. 11 13
      dashboard/src/pages/nut/users/sign-in.tsx
  40. 10 2
      dashboard/src/pages/nut/users/sign-up.tsx
  41. 19 0
      dashboard/src/pages/studio/course/edit.tsx
  42. 22 0
      dashboard/src/pages/studio/course/index.tsx
  43. 10 0
      dashboard/src/pages/studio/course/list.tsx
  44. 27 0
      dashboard/src/pages/studio/course/show.tsx
  45. 1 1
      dashboard/src/pages/studio/group/show.tsx

+ 29 - 0
dashboard/src/Router.tsx

@@ -44,6 +44,10 @@ import LibraryBlogCourse from "./pages/library/blog/course";
 import LibraryBlogAnthology from "./pages/library/blog/anthology";
 import LibraryBlogTerm from "./pages/library/blog/term";
 
+import LibraryDiscussion from "./pages/library/discussion";
+import LibraryDiscussionList from "./pages/library/discussion/list";
+import LibraryDiscussionTopic from "./pages/library/discussion/topic";
+
 import Studio from "./pages/studio";
 import StudioHome from "./pages/studio/home";
 
@@ -59,6 +63,11 @@ import StudioGroupList from "./pages/studio/group/list";
 import StudioGroupEdit from "./pages/studio/group/edit";
 import StudioGroupShow from "./pages/studio/group/show";
 
+import StudioCourse from "./pages/studio/course";
+import StudioCourseList from "./pages/studio/course/list";
+import StudioCourseEdit from "./pages/studio/course/edit";
+import StudioCourseShow from "./pages/studio/course/show";
+
 import StudioDict from "./pages/studio/dict";
 import StudioDictList from "./pages/studio/dict/list";
 
@@ -147,6 +156,12 @@ const Widget = () => {
         <Route path=":type/:id/:mode/:param" element={<LibraryArticleShow />} />
       </Route>
 
+      <Route path="discussion" element={<LibraryDiscussion />}>
+        <Route path="list" element={<LibraryDiscussionList />} />
+        <Route path="topic/:id" element={<LibraryDiscussionTopic />} />
+        <Route path="discussion/:id" element={<LibraryDiscussion />} />
+      </Route>
+
       <Route path="blog/:studio" element={<LibraryBlog />}>
         <Route path="overview" element={<LibraryBlogOverview />} />
         <Route path="palicanon" element={<LibraryBlogTranslation />} />
@@ -159,30 +174,44 @@ const Widget = () => {
         <Route path="home" element={<StudioHome />} />
         <Route path="palicanon" element={<StudioPalicanon />}></Route>
         <Route path="recent" element={<StudioRecent />}></Route>
+
         <Route path="channel" element={<StudioChannel />}>
           <Route path="list" element={<StudioChannelList />} />
           <Route path=":channelid/edit" element={<StudioChannelEdit />} />
         </Route>
+
         <Route path="group" element={<StudioGroup />}>
           <Route path="list" element={<StudioGroupList />} />
           <Route path=":groupid" element={<StudioGroupShow />} />
           <Route path=":groupid/edit" element={<StudioGroupEdit />} />
           <Route path=":groupid/show" element={<StudioGroupShow />} />
         </Route>
+
+        <Route path="course" element={<StudioCourse />}>
+          <Route path="list" element={<StudioCourseList />} />
+          <Route path=":courseId" element={<StudioCourseShow />} />
+          <Route path=":courseId/edit" element={<StudioCourseEdit />} />
+          <Route path=":courseId/show" element={<StudioCourseShow />} />
+        </Route>
+
         <Route path="dict" element={<StudioDict />}>
           <Route path="list" element={<StudioDictList />} />
         </Route>
+
         <Route path="term" element={<StudioTerm />}>
           <Route path="list" element={<StudioTermList />} />
         </Route>
+
         <Route path="article" element={<StudioArticle />}>
           <Route path="list" element={<StudioArticleList />} />
           <Route path=":articleid/edit" element={<StudioArticleEdit />} />
         </Route>
+
         <Route path="anthology" element={<StudioAnthology />}>
           <Route path="list" element={<StudioAnthologyList />}></Route>
           <Route path=":anthology_id/edit" element={<StudioAnthologyEdit />} />
         </Route>
+
         <Route path="analysis" element={<StudioAnalysis />}>
           <Route path="list" element={<StudioAnalysisList />} />
         </Route>

+ 1 - 1
dashboard/src/components/api/Auth.ts

@@ -1,4 +1,4 @@
-export type Role = "owner" | "manager" | "editor" | "member";
+export type Role = "owner" | "manager" | "editor" | "member" | "unknown";
 
 export interface IUserRequest {
   id?: string;

+ 5 - 0
dashboard/src/components/api/Comment.ts

@@ -36,3 +36,8 @@ export interface ICommentListResponse {
   message: string;
   data: { rows: ICommentApiData[]; count: number };
 }
+export interface ICommentAnchorResponse {
+  ok: boolean;
+  message: string;
+  data: string;
+}

+ 33 - 1
dashboard/src/components/api/Group.ts

@@ -1,4 +1,4 @@
-import { IStudioApiResponse, Role } from "./Auth";
+import { IStudioApiResponse, IUserRequest, Role } from "./Auth";
 
 export interface IGroupDataRequest {
   uid: string;
@@ -21,3 +21,35 @@ export interface IGroupListResponse {
     count: number;
   };
 }
+
+export interface IGroupMemberData {
+  id?: number;
+  user_id: string;
+  group_id: string;
+  power?: number;
+  level?: number;
+  status?: number;
+  user?: IUserRequest;
+  created_at?: string;
+  updated_at?: string;
+}
+export interface IGroupMemberResponse {
+  ok: boolean;
+  message: string;
+  data: IGroupMemberData;
+}
+export interface IGroupMemberListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IGroupMemberData[];
+    role: Role;
+    count: number;
+  };
+}
+
+export interface IGroupMemberDeleteResponse {
+  ok: boolean;
+  message: string;
+  data: boolean;
+}

+ 27 - 0
dashboard/src/components/api/Share.ts

@@ -0,0 +1,27 @@
+import { IUser } from "../auth/User";
+import { Role } from "./Auth";
+
+export interface IShareData {
+  id?: number;
+  res_id: string;
+  res_type: string;
+  power?: number;
+  res_name: string;
+  owner?: IUser;
+  created_at?: string;
+  updated_at?: string;
+}
+export interface IShareResponse {
+  ok: boolean;
+  message: string;
+  data: IShareData;
+}
+export interface IShareListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IShareData[];
+    role: Role;
+    count: number;
+  };
+}

+ 1 - 1
dashboard/src/components/article/ArticleCard.tsx

@@ -1,3 +1,4 @@
+import { useNavigate } from "react-router-dom";
 import { useIntl } from "react-intl";
 import { useState } from "react";
 import { Button, Card, Dropdown, Space, Segmented } from "antd";
@@ -9,7 +10,6 @@ import { modeChange } from "../../reducers/article-mode";
 import { IWidgetArticleData } from "./ArticleView";
 import ArticleCardMainMenu from "./ArticleCardMainMenu";
 import { ArticleMode } from "./Article";
-import { useNavigate } from "react-router-dom";
 
 interface IWidgetArticleCard {
   type?: string;

+ 1 - 5
dashboard/src/components/auth/ToStudio.tsx

@@ -14,11 +14,7 @@ const Widget = () => {
     return (
       <>
         <Link to={`/studio/${user.realName}/home`}>
-          <Button
-            type="primary"
-            size="small"
-            style={{ paddingLeft: 18, paddingRight: 18 }}
-          >
+          <Button type="primary" style={{ paddingLeft: 18, paddingRight: 18 }}>
             {intl.formatMessage({
               id: "columns.studio.title",
             })}

+ 56 - 0
dashboard/src/components/comment/AnchorCard.tsx

@@ -0,0 +1,56 @@
+import { useIntl } from "react-intl";
+import { useState } from "react";
+import { Card, Space, Segmented } from "antd";
+
+import store from "../../store";
+import { modeChange } from "../../reducers/article-mode";
+import { ArticleMode } from "../article/Article";
+
+interface IWidgetArticleCard {
+  children?: React.ReactNode;
+  onModeChange?: Function;
+}
+const Widget = ({ children, onModeChange }: IWidgetArticleCard) => {
+  const intl = useIntl();
+  const [mode, setMode] = useState<string>("read");
+
+  const modeSwitch = (
+    <Segmented
+      size="middle"
+      options={[
+        {
+          label: intl.formatMessage({ id: "buttons.translate" }),
+          value: "edit",
+        },
+        {
+          label: intl.formatMessage({ id: "buttons.wbw" }),
+          value: "wbw",
+        },
+      ]}
+      value={mode}
+      onChange={(value) => {
+        const newMode = value.toString();
+        if (typeof onModeChange !== "undefined") {
+          if (mode === "read" || newMode === "read") {
+            onModeChange(newMode);
+          }
+        }
+        setMode(newMode);
+        //发布mode变更
+        store.dispatch(modeChange(newMode as ArticleMode));
+      }}
+    />
+  );
+
+  return (
+    <Card
+      size="small"
+      title={<Space>{"title"}</Space>}
+      extra={<Space>{modeSwitch}</Space>}
+    >
+      {children}
+    </Card>
+  );
+};
+
+export default Widget;

+ 33 - 0
dashboard/src/components/comment/CommentAnchor.tsx

@@ -0,0 +1,33 @@
+import { useEffect, useState } from "react";
+import { get } from "../../request";
+import { ICommentAnchorResponse } from "../api/Comment";
+import MdView from "../template/MdView";
+import AnchorCard from "./AnchorCard";
+
+interface IWidget {
+  id?: string;
+}
+const Widget = ({ id }: IWidget) => {
+  const [content, setContent] = useState<string>();
+  useEffect(() => {
+    if (typeof id === "string") {
+      get<ICommentAnchorResponse>(`/v2/discussion-anchor/${id}`).then(
+        (json) => {
+          console.log(json);
+          if (json.ok) {
+            setContent(json.data);
+          }
+        }
+      );
+    }
+  }, [id]);
+  return (
+    <div>
+      <AnchorCard>
+        <MdView html={content} />
+      </AnchorCard>
+    </div>
+  );
+};
+
+export default Widget;

+ 6 - 12
dashboard/src/components/comment/CommentBox.tsx

@@ -67,18 +67,12 @@ const Widget = ({ trigger, resId, resType, onCommentCountChange }: IWidget) => {
           onClose={onChildrenDrawerClose}
           open={childrenDrawer}
         >
-          {resId && resType ? (
-            <CommentTopic
-              comment={topicComment}
-              resId={resId}
-              resType={resType}
-              onItemCountChange={(count: number, parent: string) => {
-                setAnswerCount({ id: parent, count: count });
-              }}
-            />
-          ) : (
-            <></>
-          )}
+          <CommentTopic
+            topicId={topicComment?.id}
+            onItemCountChange={(count: number, parent: string) => {
+              setAnswerCount({ id: parent, count: count });
+            }}
+          />
         </Drawer>
       </Drawer>
     </>

+ 3 - 3
dashboard/src/components/comment/CommentCreate.tsx

@@ -16,12 +16,12 @@ import { currentUser as _currentUser } from "../../reducers/current-user";
 import { useRef } from "react";
 
 interface IWidget {
-  resId: string;
-  resType: string;
+  resId?: string;
+  resType?: string;
   parent?: string;
   onCreated?: Function;
 }
-const Widget = ({ resId, resType, parent, onCreated }: IWidget) => {
+const Widget = ({ resId = "", resType = "", parent, onCreated }: IWidget) => {
   const intl = useIntl();
   const formRef = useRef<ProFormInstance>();
   const _currUser = useAppSelector(_currentUser);

+ 4 - 11
dashboard/src/components/comment/CommentEdit.tsx

@@ -1,21 +1,14 @@
-import { useState } from "react";
 import { useIntl } from "react-intl";
 import { Button, Card } from "antd";
-import { Input, message } from "antd";
-import { SaveOutlined } from "@ant-design/icons";
-import {
-  ProForm,
-  ProFormText,
-  ProFormTextArea,
-} from "@ant-design/pro-components";
+import { message } from "antd";
+
+import { ProForm, ProFormTextArea } from "@ant-design/pro-components";
 import { Col, Row, Space } from "antd";
 
 import { IComment } from "./CommentItem";
-import { post, put } from "../../request";
+import { put } from "../../request";
 import { ICommentRequest, ICommentResponse } from "../api/Comment";
 
-const { TextArea } = Input;
-
 interface IWidget {
   data: IComment;
   onCreated?: Function;

+ 7 - 7
dashboard/src/components/comment/CommentItem.tsx

@@ -1,4 +1,4 @@
-import { Avatar } from "antd";
+import { Avatar, Col, Row } from "antd";
 import { useState } from "react";
 import { IUser } from "../auth/User";
 import CommentShow from "./CommentShow";
@@ -26,11 +26,11 @@ const Widget = ({ data, onSelect, onCreated }: IWidget) => {
   const [edit, setEdit] = useState(false);
   console.log(data);
   return (
-    <div style={{ display: "flex" }}>
-      <div style={{ width: "auto", padding: 8 }}>
+    <Row>
+      <Col flex={"2em"} style={{ padding: 8 }}>
         <Avatar>{data.user?.nickName?.slice(0, 1)}</Avatar>
-      </div>
-      <div style={{ flex: "auto" }}>
+      </Col>
+      <Col flex={"auto"}>
         {edit ? (
           <CommentEdit
             data={data}
@@ -48,8 +48,8 @@ const Widget = ({ data, onSelect, onCreated }: IWidget) => {
             }}
           />
         )}
-      </div>
-    </div>
+      </Col>
+    </Row>
   );
 };
 

+ 11 - 74
dashboard/src/components/comment/CommentTopic.tsx

@@ -1,87 +1,24 @@
-import { useEffect, useState } from "react";
-import { Divider, message } from "antd";
+import { Divider } from "antd";
 
-import CommentItem, { IComment } from "./CommentItem";
-import CommentTopicList from "./CommentTopicList";
-import CommentTopicHead from "./CommentTopicHead";
-import { get } from "../../request";
-import { ICommentListResponse } from "../api/Comment";
-import { useIntl } from "react-intl";
-import CommentCreate from "./CommentCreate";
-
-const defaultData: IComment[] = Array(5)
-  .fill(3)
-  .map((item, id) => {
-    return {
-      id: "dd",
-      content: "评论内容",
-      title: "评论标题" + id,
-      user: {
-        id: "string",
-        nickName: "Visuddhinanda",
-        realName: "Visuddhinanda",
-        avatar: "",
-      },
-    };
-  });
+import CommentTopicInfo from "./CommentTopicInfo";
+import CommentTopicChildren from "./CommentTopicChildren";
 
 interface IWidget {
-  resId: string;
-  resType: string;
-  comment?: IComment;
+  topicId?: string;
   onItemCountChange?: Function;
 }
-const Widget = ({ resId, resType, comment, onItemCountChange }: IWidget) => {
-  const intl = useIntl();
-  const [childrenData, setChildrenData] = useState<IComment[]>(defaultData);
-  useEffect(() => {
-    get<ICommentListResponse>(`/v2/discussion?view=answer&id=${comment?.id}`)
-      .then((json) => {
-        console.log(json);
-        if (json.ok) {
-          console.log(intl.formatMessage({ id: "flashes.success" }));
-          const discussions: IComment[] = json.data.rows.map((item) => {
-            return {
-              id: item.id,
-              resId: item.res_id,
-              resType: item.res_type,
-              user: {
-                id: item.editor?.id ? item.editor.id : "null",
-                nickName: item.editor?.nickName ? item.editor.nickName : "null",
-                realName: item.editor?.userName ? item.editor.userName : "null",
-                avatar: item.editor?.avatar ? item.editor.avatar : "null",
-              },
-              title: item.title,
-              content: item.content,
-              createdAt: item.created_at,
-              updatedAt: item.updated_at,
-            };
-          });
-          setChildrenData(discussions);
-        } else {
-          message.error(json.message);
-        }
-      })
-      .catch((e) => {
-        message.error(e.message);
-      });
-  }, [comment]);
+const Widget = ({ topicId, onItemCountChange }: IWidget) => {
   return (
     <div>
-      <CommentTopicHead data={comment} />
+      <CommentTopicInfo topicId={topicId} />
       <Divider />
-      <CommentTopicList data={childrenData} />
-      <CommentCreate
-        resId={resId}
-        resType={resType}
-        parent={comment?.id}
-        onCreated={(e: IComment) => {
-          console.log("create", e);
-          const newData = JSON.parse(JSON.stringify(e));
+      <CommentTopicChildren
+        topicId={topicId}
+        onItemCountChange={(count: number, e: string) => {
+          //把新建回答的消息传出去。
           if (typeof onItemCountChange !== "undefined") {
-            onItemCountChange(childrenData.length + 1, e.parent);
+            onItemCountChange(count, e);
           }
-          setChildrenData([...childrenData, newData]);
         }}
       />
     </div>

+ 91 - 0
dashboard/src/components/comment/CommentTopicChildren.tsx

@@ -0,0 +1,91 @@
+import { List, message } from "antd";
+import { useEffect, useState } from "react";
+import { useIntl } from "react-intl";
+import { get } from "../../request";
+import { ICommentListResponse } from "../api/Comment";
+import CommentCreate from "./CommentCreate";
+
+import CommentItem, { IComment } from "./CommentItem";
+
+interface IWidget {
+  topicId?: string;
+  onItemCountChange?: Function;
+}
+const Widget = ({ topicId, onItemCountChange }: IWidget) => {
+  const intl = useIntl();
+  const [data, setData] = useState<IComment[]>();
+  useEffect(() => {
+    if (typeof topicId === "undefined") {
+      return;
+    }
+    get<ICommentListResponse>(`/v2/discussion?view=answer&id=${topicId}`)
+      .then((json) => {
+        console.log(json);
+        if (json.ok) {
+          console.log(intl.formatMessage({ id: "flashes.success" }));
+          const discussions: IComment[] = json.data.rows.map((item) => {
+            return {
+              id: item.id,
+              resId: item.res_id,
+              resType: item.res_type,
+              user: {
+                id: item.editor?.id ? item.editor.id : "null",
+                nickName: item.editor?.nickName ? item.editor.nickName : "null",
+                realName: item.editor?.userName ? item.editor.userName : "null",
+                avatar: item.editor?.avatar ? item.editor.avatar : "null",
+              },
+              title: item.title,
+              content: item.content,
+              createdAt: item.created_at,
+              updatedAt: item.updated_at,
+            };
+          });
+          setData(discussions);
+        } else {
+          message.error(json.message);
+        }
+      })
+      .catch((e) => {
+        message.error(e.message);
+      });
+  }, [topicId]);
+  return (
+    <div>
+      <List
+        pagination={{
+          onChange: (page) => {
+            console.log(page);
+          },
+          pageSize: 10,
+        }}
+        itemLayout="horizontal"
+        dataSource={data}
+        renderItem={(item) => (
+          <List.Item>
+            <CommentItem data={item} />
+          </List.Item>
+        )}
+      />
+      <CommentCreate
+        parent={topicId}
+        onCreated={(e: IComment) => {
+          console.log("create", e);
+          const newData = JSON.parse(JSON.stringify(e));
+          let count = 0;
+          if (typeof data === "undefined") {
+            count = 1;
+            setData([newData]);
+          } else {
+            count = data.length + 1;
+            setData([...data, newData]);
+          }
+          if (typeof onItemCountChange !== "undefined") {
+            onItemCountChange(count, e.parent);
+          }
+        }}
+      />
+    </div>
+  );
+};
+
+export default Widget;

+ 0 - 30
dashboard/src/components/comment/CommentTopicHead.tsx

@@ -1,30 +0,0 @@
-import { Typography, Space } from "antd";
-import TimeShow from "../general/TimeShow";
-
-import { IComment } from "./CommentItem";
-
-const { Title, Text } = Typography;
-
-interface IWidget {
-  data?: IComment;
-}
-const Widget = ({ data }: IWidget) => {
-  return (
-    <div>
-      <Title editable level={1} style={{ margin: 0 }}>
-        {data?.title}
-      </Title>
-      <div>
-        <Text type="secondary">
-          <Space>
-            {" "}
-            {data?.user.nickName}{" "}
-            <TimeShow time={data?.createdAt} title="创建" />
-          </Space>
-        </Text>
-      </div>
-    </div>
-  );
-};
-
-export default Widget;

+ 68 - 0
dashboard/src/components/comment/CommentTopicInfo.tsx

@@ -0,0 +1,68 @@
+import { Typography, Space, message } from "antd";
+import { useEffect, useState } from "react";
+import { get } from "../../request";
+import { ICommentResponse } from "../api/Comment";
+import TimeShow from "../general/TimeShow";
+
+import { IComment } from "./CommentItem";
+
+const { Title, Text } = Typography;
+
+interface IWidget {
+  topicId?: string;
+}
+const Widget = ({ topicId }: IWidget) => {
+  const [data, setData] = useState<IComment>();
+  useEffect(() => {
+    if (typeof topicId === "undefined") {
+      return;
+    }
+    get<ICommentResponse>(`/v2/discussion/${topicId}`)
+      .then((json) => {
+        console.log(json);
+        if (json.ok) {
+          console.log("flashes.success");
+          const item = json.data;
+          const discussion: IComment = {
+            id: item.id,
+            resId: item.res_id,
+            resType: item.res_type,
+            user: {
+              id: item.editor?.id ? item.editor.id : "null",
+              nickName: item.editor?.nickName ? item.editor.nickName : "null",
+              realName: item.editor?.userName ? item.editor.userName : "null",
+              avatar: item.editor?.avatar ? item.editor.avatar : "null",
+            },
+            title: item.title,
+            content: item.content,
+            createdAt: item.created_at,
+            updatedAt: item.updated_at,
+          };
+          setData(discussion);
+        } else {
+          message.error(json.message);
+        }
+      })
+      .catch((e) => {
+        message.error(e.message);
+      });
+  }, [topicId]);
+  return (
+    <div>
+      <Title editable level={1} style={{ margin: 0 }}>
+        {data?.title}
+      </Title>
+      <div>
+        <Text type="secondary">
+          <Space>
+            {" "}
+            {data?.user.nickName}{" "}
+            <TimeShow time={data?.createdAt} title="创建" />
+          </Space>
+        </Text>
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 0 - 30
dashboard/src/components/comment/CommentTopicList.tsx

@@ -1,30 +0,0 @@
-import { List } from "antd";
-
-import CommentItem, { IComment } from "./CommentItem";
-
-interface IWidget {
-  data: IComment[];
-}
-const Widget = ({ data }: IWidget) => {
-  return (
-    <div>
-      <List
-        pagination={{
-          onChange: (page) => {
-            console.log(page);
-          },
-          pageSize: 10,
-        }}
-        itemLayout="horizontal"
-        dataSource={data}
-        renderItem={(item) => (
-          <List.Item>
-            <CommentItem data={item} />
-          </List.Item>
-        )}
-      />
-    </div>
-  );
-};
-
-export default Widget;

+ 35 - 12
dashboard/src/components/group/AddMember.tsx

@@ -1,13 +1,11 @@
 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, post } from "../../request";
 import { IUserListResponse } from "../api/Auth";
+import { IGroupMemberData, IGroupMemberResponse } from "../api/Group";
+import { useState } from "react";
 
 interface IFormData {
   userId: string;
@@ -15,16 +13,32 @@ interface IFormData {
 
 interface IWidget {
   groupId?: string;
+  onCreated?: Function;
 }
-const Widget = ({ groupId }: IWidget) => {
+const Widget = ({ groupId, onCreated }: IWidget) => {
   const intl = useIntl();
+  const [open, setOpen] = useState(false);
 
   const form = (
     <ProForm<IFormData>
       onFinish={async (values: IFormData) => {
         // TODO
         console.log(values);
-        message.success(intl.formatMessage({ id: "flashes.success" }));
+        if (typeof groupId !== "undefined") {
+          post<IGroupMemberData, IGroupMemberResponse>("/v2/group-member", {
+            user_id: values.userId,
+            group_id: groupId,
+          }).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>
@@ -33,9 +47,11 @@ const Widget = ({ groupId }: IWidget) => {
           label={intl.formatMessage({ id: "forms.fields.user.label" })}
           showSearch
           debounceTime={300}
-          request={async ({ keyWord }) => {
-            console.log("keyWord", keyWord);
-            const json = await get<IUserListResponse>(`/v2/user?view=key&key=`);
+          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,
@@ -45,7 +61,9 @@ const Widget = ({ groupId }: IWidget) => {
             console.log("json", userList);
             return userList;
           }}
-          placeholder={intl.formatMessage({ id: "forms.fields.user.required" })}
+          placeholder={intl.formatMessage({
+            id: "forms.message.user.required",
+          })}
           rules={[
             {
               required: true,
@@ -58,12 +76,17 @@ const Widget = ({ groupId }: IWidget) => {
       </ProForm.Group>
     </ProForm>
   );
+  const handleClickChange = (open: boolean) => {
+    setOpen(open);
+  };
   return (
     <Popover
       placement="bottom"
       arrowPointAtCenter
       content={form}
       trigger="click"
+      open={open}
+      onOpenChange={handleClickChange}
     >
       <Button icon={<UserAddOutlined />} key="add" type="primary">
         {intl.formatMessage({ id: "buttons.group.add.member" })}

+ 101 - 54
dashboard/src/components/group/GroupFile.tsx

@@ -1,61 +1,38 @@
 import { useIntl } from "react-intl";
 import { useState } from "react";
 import { ProList } from "@ant-design/pro-components";
-import { Space, Tag, Button, Layout } from "antd";
+import { Space, Tag, Button, Layout, Popconfirm } from "antd";
+import { get } from "../../request";
+import { IShareListResponse } from "../api/Share";
 
 const { Content } = Layout;
 
-const defaultData = [
-  {
-    id: "1",
-    name: "庄春江工作站",
-    tag: [{ title: "可编辑", color: "success" }],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-    description: "IAPT|2022-1-3",
-  },
-  {
-    id: "2",
-    name: "元亨寺·CBETA",
-    tag: [{ title: "可编辑", color: "success" }],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-    description: "我是一条测试的描述",
-  },
-  {
-    id: "3",
-    name: "叶均居士",
-    tag: [{ title: "只读", color: "default" }],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-    description: "我是一条测试的描述",
-  },
-  {
-    id: "4",
-    name: "玛欣德尊者",
-    tag: [{ title: "只读", color: "default" }],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-    description: "我是一条测试的描述",
-  },
-];
-type DataItem = typeof defaultData[number];
-type IWidgetGroupFile = {
-  groupid?: string;
-};
-const Widget = ({ groupid = "" }: IWidgetGroupFile) => {
+interface IRoleTag {
+  title: string;
+  color: string;
+}
+interface DataItem {
+  id: string;
+  name?: string;
+  tag: IRoleTag[];
+  image: string;
+  description?: string;
+}
+interface IWidget {
+  groupId?: string;
+}
+const Widget = ({ groupId }: IWidget) => {
   const intl = useIntl(); //i18n
-  const [dataSource, setDataSource] = useState<DataItem[]>(defaultData);
-
+  const [canDelete, setCanDelete] = useState(false);
+  const [resCount, setResCount] = useState(0);
   return (
     <Content>
-      <Space>{groupid}</Space>
       <ProList<DataItem>
         rowKey="id"
-        headerTitle={intl.formatMessage({ id: "group.files" })}
-        dataSource={dataSource}
+        headerTitle={
+          intl.formatMessage({ id: "group.files" }) + "-" + resCount.toString()
+        }
         showActions="hover"
-        onDataSourceChange={setDataSource}
         metas={{
           title: {
             dataIndex: "name",
@@ -85,17 +62,87 @@ const Widget = ({ groupid = "" }: IWidgetGroupFile) => {
           },
           actions: {
             render: (text, row, index, action) => [
-              <Button
-                onClick={() => {
-                  action?.startEditable(row.id);
-                }}
-                key="link"
-              >
-                删除
-              </Button>,
+              canDelete ? (
+                <Popconfirm
+                  title={intl.formatMessage({
+                    id: "forms.message.member.delete",
+                  })}
+                  onConfirm={(
+                    e?: React.MouseEvent<HTMLElement, MouseEvent>
+                  ) => {
+                    console.log("delete", row.id);
+                  }}
+                  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>
+              ) : (
+                <></>
+              ),
             ],
           },
         }}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+
+          let url = `/v2/share?view=group&id=${groupId}`;
+          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<IShareListResponse>(url);
+          if (res.ok) {
+            console.log(res.data.rows);
+            setResCount(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.res_id,
+                name: item.res_name,
+                tag: [],
+                image: "",
+                description: item.owner?.nickName,
+              };
+              switch (item.power) {
+                case 0:
+                  member.tag.push({ title: "拥有者", color: "success" });
+                  break;
+                case 1:
+                  member.tag.push({ title: "管理员", color: "default" });
+                  break;
+              }
+
+              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,

+ 126 - 65
dashboard/src/components/group/GroupMember.tsx

@@ -1,76 +1,120 @@
 import { useIntl } from "react-intl";
-import { useState } from "react";
-import { ProList } from "@ant-design/pro-components";
+import { useRef, useState } from "react";
+import { ActionType, ProList } from "@ant-design/pro-components";
 import { UserAddOutlined } from "@ant-design/icons";
-import { Space, Tag, Button, Layout } from "antd";
+import { Space, Tag, Button, Layout, Popconfirm } from "antd";
 import GroupAddMember from "./AddMember";
+import { delete_, get } from "../../request";
+import {
+  IGroupMemberDeleteResponse,
+  IGroupMemberListResponse,
+} from "../api/Group";
 
 const { Content } = Layout;
 
-const defaultData = [
-  {
-    id: "1",
-    name: "小僧善巧",
-    tag: [{ title: "管理员", color: "success" }],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-  },
-  {
-    id: "2",
-    name: "无语",
-    tag: [{ title: "管理员", color: "success" }],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-  },
-  {
-    id: "3",
-    name: "慧欣",
-    tag: [],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-  },
-  {
-    id: "4",
-    name: "谭博文",
-    tag: [],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-  },
-  {
-    id: "4",
-    name: "豆沙猫",
-    tag: [],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-  },
-  {
-    id: "4",
-    name: "visuddhinanda",
-    tag: [],
-    image:
-      "https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg",
-  },
-];
-type DataItem = typeof defaultData[number];
+interface IRoleTag {
+  title: string;
+  color: string;
+}
+interface DataItem {
+  id: number;
+  userId: string;
+  name?: string;
+  tag: IRoleTag[];
+  image: string;
+}
 interface IWidgetGroupFile {
   groupId?: string;
 }
 const Widget = ({ groupId }: IWidgetGroupFile) => {
   const intl = useIntl(); //i18n
-  const [dataSource, setDataSource] = useState<DataItem[]>(defaultData);
+  const [canDelete, setCanDelete] = useState(false);
+  const [memberCount, setMemberCount] = useState<number>();
 
+  const ref = useRef<ActionType>();
   return (
     <Content>
-      <Space>{groupId}</Space>
       <ProList<DataItem>
         rowKey="id"
-        headerTitle={intl.formatMessage({ id: "group.member" })}
+        actionRef={ref}
+        headerTitle={
+          intl.formatMessage({ id: "group.member" }) +
+          "-" +
+          memberCount?.toString()
+        }
         toolBarRender={() => {
-          return [<GroupAddMember groupId={groupId} />];
+          return [
+            <GroupAddMember
+              groupId={groupId}
+              onCreated={() => {
+                ref.current?.reload();
+              }}
+            />,
+          ];
         }}
-        dataSource={dataSource}
         showActions="hover"
-        onDataSourceChange={setDataSource}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+
+          let url = `/v2/group-member?view=group&id=${groupId}`;
+          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<IGroupMemberListResponse>(url);
+          if (res.ok) {
+            console.log(res.data.rows);
+            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: "",
+              };
+              switch (item.power) {
+                case 0:
+                  member.tag.push({ title: "拥有者", color: "success" });
+                  break;
+                case 1:
+                  member.tag.push({ title: "管理员", color: "default" });
+                  break;
+              }
+
+              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",
@@ -93,17 +137,34 @@ const Widget = ({ groupId }: IWidgetGroupFile) => {
           },
           actions: {
             render: (text, row, index, action) => [
-              <Button
-                style={{ padding: 0, margin: 0 }}
-                type="link"
-                danger
-                onClick={() => {
-                  action?.startEditable(row.id);
-                }}
-                key="link"
-              >
-                {intl.formatMessage({ id: "buttons.remove" })}
-              </Button>,
+              canDelete ? (
+                <Popconfirm
+                  title={intl.formatMessage({
+                    id: "forms.message.member.delete",
+                  })}
+                  onConfirm={(
+                    e?: React.MouseEvent<HTMLElement, MouseEvent>
+                  ) => {
+                    console.log("delete", row.id);
+                    delete_<IGroupMemberDeleteResponse>(
+                      "/v2/group-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>
+              ) : (
+                <></>
+              ),
             ],
           },
         }}

+ 6 - 1
dashboard/src/components/nut/users/NonSignInSharedLinks.tsx

@@ -1,6 +1,6 @@
 import { FormattedMessage } from "react-intl";
 import { Link } from "react-router-dom";
-import { Space } from "antd";
+import { Divider, Space } from "antd";
 
 const Widget = () => {
   return (
@@ -8,9 +8,14 @@ const Widget = () => {
       <Link to="/anonymous/users/sign-in">
         <FormattedMessage id="nut.users.sign-in.title" />
       </Link>
+      <Divider type="vertical" />
       <Link to="/anonymous/users/sign-up">
         <FormattedMessage id="nut.users.sign-up.title" />
       </Link>
+      <Divider type="vertical" />
+      <Link to="/anonymous/users/forgot-password">
+        <FormattedMessage id="nut.users.forgot-password.title" />
+      </Link>
     </Space>
   );
 };

+ 1 - 1
dashboard/src/components/nut/users/SignIn.tsx

@@ -66,7 +66,7 @@ const Widget = () => {
           name="email"
           required
           label={intl.formatMessage({
-            id: "forms.fields.email.label",
+            id: "forms.fields.email.or.username.label",
           })}
           rules={[{ required: true, max: 255, min: 6 }]}
         />

+ 25 - 29
dashboard/src/components/studio/HeadBar.tsx

@@ -12,35 +12,31 @@ const { Header } = Layout;
 const onSearch = (value: string) => console.log(value);
 
 const Widget = () => {
-	return (
-		<Header className="header">
-			<Row justify="space-between">
-				<Col flex="80px">
-					<Link to="/">
-						<img
-							alt="code"
-							style={{ height: "3em" }}
-							src={img_banner}
-						/>
-					</Link>
-				</Col>
-				<Col span={8}>
-					<Search
-						placeholder="input search text"
-						onSearch={onSearch}
-						style={{ width: "100%" }}
-					/>
-				</Col>
-				<Col span={4}>
-					<Space>
-						<ToLibaray />
-						<SignInAvatar />
-						<UiLangSelect />
-					</Space>
-				</Col>
-			</Row>
-		</Header>
-	);
+  return (
+    <Header className="header" style={{ lineHeight: "44px", height: 44 }}>
+      <Row justify="space-between">
+        <Col flex="80px">
+          <Link to="/">
+            <img alt="code" style={{ height: 36 }} src={img_banner} />
+          </Link>
+        </Col>
+        <Col span={8}>
+          <Search
+            placeholder="input search text"
+            onSearch={onSearch}
+            style={{ width: "100%" }}
+          />
+        </Col>
+        <Col span={4}>
+          <Space>
+            <ToLibaray />
+            <SignInAvatar />
+            <UiLangSelect />
+          </Space>
+        </Col>
+      </Row>
+    </Header>
+  );
 };
 
 export default Widget;

+ 155 - 148
dashboard/src/components/studio/LeftSider.tsx

@@ -5,166 +5,173 @@ import type { MenuProps } from "antd";
 import { Affix, Layout } from "antd";
 import { Menu } from "antd";
 import {
-	AppstoreOutlined,
-	HomeOutlined,
-	TeamOutlined,
+  AppstoreOutlined,
+  HomeOutlined,
+  TeamOutlined,
 } from "@ant-design/icons";
 
 const { Sider } = Layout;
 
 const onClick: MenuProps["onClick"] = (e) => {
-	console.log("click ", e);
+  console.log("click ", e);
 };
 
 type IWidgetHeadBar = {
-	selectedKeys?: string;
+  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 linkArticle = "/studio/" + studioname + "/article/list";
-	const linkAnthology = "/studio/" + studioname + "/anthology/list";
-	const linkAnalysis = "/studio/" + studioname + "/analysis/list";
+  //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={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",
-				},
-			],
-		},
-	];
+  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}>
-			<Sider
-				width={200}
-				breakpoint="lg"
-				className="site-layout-background"
-			>
-				<Menu
-					theme="light"
-					onClick={onClick}
-					defaultSelectedKeys={[selectedKeys]}
-					defaultOpenKeys={["basic", "advance", "collaboration"]}
-					mode="inline"
-					items={items}
-				/>
-			</Sider>
-		</Affix>
-	);
+  return (
+    <Affix offsetTop={0}>
+      <Sider width={200} breakpoint="lg" className="site-layout-background">
+        <Menu
+          theme="light"
+          onClick={onClick}
+          defaultSelectedKeys={[selectedKeys]}
+          defaultOpenKeys={["basic", "advance", "collaboration"]}
+          mode="inline"
+          items={items}
+        />
+      </Sider>
+    </Affix>
+  );
 };
 
 export default Widget;

+ 6 - 2
dashboard/src/components/template/MdView.tsx

@@ -1,11 +1,15 @@
 import { TCodeConvertor, XmlToReact } from "./utilities";
 
 interface IWidget {
-  html: string;
+  html?: string;
   wordWidget?: boolean;
   convertor?: TCodeConvertor;
 }
-const Widget = ({ html, wordWidget = false, convertor }: IWidget) => {
+const Widget = ({
+  html = "<div></div>",
+  wordWidget = false,
+  convertor,
+}: IWidget) => {
   const jsx = XmlToReact(html, wordWidget, convertor);
   return <>{jsx}</>;
 };

+ 3 - 0
dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -25,6 +25,9 @@ const Widget = ({
   const [nissayaActive, setNissayaActive] = useState<boolean>(false);
   const [commentaryActive, setCommentaryActive] = useState<boolean>(false);
   const [originalActive, setOriginalActive] = useState<boolean>(false);
+  if (typeof id === "undefined") {
+    return <></>;
+  }
   const sentId = id.split("_");
 
   const onChange = (key: string) => {

+ 1 - 1
dashboard/src/components/template/Wbw/WbwPali.tsx

@@ -1,5 +1,5 @@
 import { useState } from "react";
-import { Popover, Typography, Button, Space } from "antd";
+import { Popover, Typography } from "antd";
 import {
   TagTwoTone,
   InfoCircleOutlined,

+ 20 - 37
dashboard/src/layouts/anonymous/index.tsx

@@ -4,43 +4,26 @@ import UiLangSelect from "../../components/general/UiLangSelect";
 import img_banner from "../../assets/library/images/wikipali_logo_library.svg";
 
 const Widget = () => {
-	return (
-		<>
-			<Layout style={{ textAlign: "right", backgroundColor: "#3e3e3e" }}>
-				<UiLangSelect />
-			</Layout>
-			<div style={{ paddingTop: "3em", backgroundColor: "#3e3e3e" }}>
-				<Row>
-					<Col flex="auto"></Col>
-					<Col flex="400px" style={{ padding: "1em" }}>
-						<img
-							alt="logo"
-							style={{ height: "5em" }}
-							src={img_banner}
-						/>
-					</Col>
-					<Col flex="400px" style={{ padding: "1em" }}>
-						<Outlet />
-						<div>
-							<Space>
-								<Link to="/anonymous/users/sign-in">
-									Sign in
-								</Link>
-								<Link to="/anonymous/users/sign-up">
-									Sign up
-								</Link>
-								<Link to="/anonymous/users/forgot-password">
-									Forgot password
-								</Link>
-							</Space>
-						</div>
-					</Col>
-					<Col flex="auto"></Col>
-				</Row>
-			</div>
-			<div>anonymous layout footer</div>
-		</>
-	);
+  return (
+    <>
+      <Layout style={{ textAlign: "right", backgroundColor: "#3e3e3e" }}>
+        <UiLangSelect />
+      </Layout>
+      <div style={{ paddingTop: "3em", backgroundColor: "#3e3e3e" }}>
+        <Row>
+          <Col flex="auto"></Col>
+          <Col flex="400px" style={{ padding: "1em" }}>
+            <img alt="logo" style={{ height: "5em" }} src={img_banner} />
+          </Col>
+          <Col flex="400px" style={{ padding: "1em" }}>
+            <Outlet />
+          </Col>
+          <Col flex="auto"></Col>
+        </Row>
+      </div>
+      <div>anonymous layout footer</div>
+    </>
+  );
 };
 
 export default Widget;

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

@@ -22,6 +22,7 @@ const items = {
   "buttons.click.upload": "点击上传",
   "buttons.group.exit": "退群",
   "buttons.group.add.member": "加人",
+  "buttons.ok": "确定",
 };
 
 export default items;

+ 3 - 0
dashboard/src/locales/zh-Hans/forms.ts

@@ -1,5 +1,6 @@
 const items = {
   "forms.fields.email.label": "电子邮箱",
+  "forms.fields.email.or.username.label": "电子邮箱/用户名",
   "forms.fields.password.label": "密码",
   "forms.fields.id.label": "ID",
   "forms.fields.message.label": "消息",
@@ -42,6 +43,8 @@ const items = {
   "forms.fields.case.tooltip": "语法信息参见……",
   "forms.fields.user.label": "用户",
   "forms.message.user.required": "请选择用户",
+  "forms.message.user.delete": "删除用户吗?此操作无法恢复。",
+  "forms.message.member.delete": "删除此成员吗?此操作无法恢复。",
 };
 
 export default items;

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

@@ -1,7 +1,8 @@
 const items = {
-  "nut.users.sign-in.title": "欢迎登录",
+  "nut.users.sign-in.title": "登录",
   "nut.users.sign-up.title": "新用户注册",
   "nut.users.logs.title": "日志列表",
+  "nut.users.forgot-password.title": "忘记密码",
 };
 
 export default items;

+ 24 - 0
dashboard/src/pages/library/discussion/index.tsx

@@ -0,0 +1,24 @@
+import { Outlet } from "react-router-dom";
+
+import HeadBar from "../../../components/library/HeadBar";
+import FooterBar from "../../../components/library/FooterBar";
+import { Col, Row } from "antd";
+
+const Widget = () => {
+  // TODO
+  return (
+    <div>
+      <HeadBar selectedKeys="discussion" />
+      <Row>
+        <Col flex={"auto"}></Col>
+        <Col flex={"960px"}>
+          <Outlet />
+        </Col>
+        <Col flex={"auto"}></Col>
+      </Row>
+      <FooterBar />
+    </div>
+  );
+};
+
+export default Widget;

+ 15 - 0
dashboard/src/pages/library/discussion/list.tsx

@@ -0,0 +1,15 @@
+import { Link } from "react-router-dom";
+
+const Widget = () => {
+  // TODO
+  return (
+    <div>
+      <div>
+        <Link to="/course/show/12345">课程1</Link>
+        <Link to="/course/show/23456">课程2</Link>
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 19 - 0
dashboard/src/pages/library/discussion/show.tsx

@@ -0,0 +1,19 @@
+import { useParams } from "react-router-dom";
+
+import CommentTopic from "../../../components/comment/CommentTopic";
+
+const Widget = () => {
+  // TODO
+  const { id } = useParams(); //url 参数
+
+  return (
+    <div>
+      <div>锚点</div>
+      <div>
+        <CommentTopic topicId={id} />
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 22 - 0
dashboard/src/pages/library/discussion/topic.tsx

@@ -0,0 +1,22 @@
+import { useParams } from "react-router-dom";
+import CommentAnchor from "../../../components/comment/CommentAnchor";
+
+import CommentTopic from "../../../components/comment/CommentTopic";
+
+const Widget = () => {
+  // TODO
+  const { id } = useParams(); //url 参数
+
+  return (
+    <div>
+      <div>
+        <CommentAnchor id={id} />
+      </div>
+      <div>
+        <CommentTopic topicId={id} />
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 7 - 5
dashboard/src/pages/nut/users/forgot-password.tsx

@@ -1,12 +1,14 @@
 import { Card } from "antd";
 import ForgotPassword from "../../../components/nut/users/ForgotPassword";
+import NonSignInSharedLinks from "../../../components/nut/users/NonSignInSharedLinks";
 
 const Widget = () => {
-	return (
-		<Card title="忘记密码">
-			<ForgotPassword />
-		</Card>
-	);
+  return (
+    <Card title="重置密码">
+      <ForgotPassword />
+      <NonSignInSharedLinks />
+    </Card>
+  );
 };
 
 export default Widget;

+ 10 - 1
dashboard/src/pages/nut/users/reset-password.tsx

@@ -1,5 +1,14 @@
+import { Card } from "antd";
+import ForgotPassword from "../../../components/nut/users/ForgotPassword";
+import NonSignInSharedLinks from "../../../components/nut/users/NonSignInSharedLinks";
+
 const Widget = () => {
-  return <div>reset password</div>;
+  return (
+    <Card title="重置密码">
+      <ForgotPassword />
+      <NonSignInSharedLinks />
+    </Card>
+  );
 };
 
 export default Widget;

+ 11 - 13
dashboard/src/pages/nut/users/sign-in.tsx

@@ -1,20 +1,18 @@
 import SignInForm from "../../../components/nut/users/SignIn";
 import SharedLinks from "../../../components/nut/users/NonSignInSharedLinks";
-import { Card } from "antd";
+import { Card, Space } from "antd";
 
 const Widget = () => {
-	return (
-		<div>
-			<Card title="登录">
-				<div>
-					<SignInForm />
-				</div>
-				<div>
-					<SharedLinks />
-				</div>
-			</Card>
-		</div>
-	);
+  return (
+    <div>
+      <Card title="登录">
+        <Space direction="vertical">
+          <SignInForm />
+          <SharedLinks />
+        </Space>
+      </Card>
+    </div>
+  );
 };
 
 export default Widget;

+ 10 - 2
dashboard/src/pages/nut/users/sign-up.tsx

@@ -1,12 +1,20 @@
+import { Card } from "antd";
+import { useIntl } from "react-intl";
 import SharedLinks from "../../../components/nut/users/NonSignInSharedLinks";
 
 const Widget = () => {
+  const intl = useIntl();
+
   return (
-    <div>
+    <Card
+      title={intl.formatMessage({
+        id: "nut.users.sign-in.title",
+      })}
+    >
       sign up
       <br />
       <SharedLinks />
-    </div>
+    </Card>
   );
 };
 

+ 19 - 0
dashboard/src/pages/studio/course/edit.tsx

@@ -0,0 +1,19 @@
+import { Card } from "antd";
+import { useState } from "react";
+import { useIntl } from "react-intl";
+import { useParams } from "react-router-dom";
+import GoBack from "../../../components/studio/GoBack";
+
+const Widget = () => {
+  const intl = useIntl();
+  const { studioname, courseId } = useParams(); //url 参数
+  const [title, setTitle] = useState("Loading");
+
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/course/list`} title={title} />}
+    ></Card>
+  );
+};
+
+export default Widget;

+ 22 - 0
dashboard/src/pages/studio/course/index.tsx

@@ -0,0 +1,22 @@
+import { Outlet } from "react-router-dom";
+import { Layout } from "antd";
+
+import LeftSider from "../../../components/studio/LeftSider";
+import { styleStudioContent } from "../style";
+
+const { Content } = Layout;
+
+const Widget = () => {
+  return (
+    <Layout>
+      <Layout>
+        <LeftSider selectedKeys="course" />
+        <Content style={styleStudioContent}>
+          <Outlet />
+        </Content>
+      </Layout>
+    </Layout>
+  );
+};
+
+export default Widget;

+ 10 - 0
dashboard/src/pages/studio/course/list.tsx

@@ -0,0 +1,10 @@
+import { useParams } from "react-router-dom";
+import { useIntl } from "react-intl";
+
+const Widget = () => {
+  const intl = useIntl(); //i18n
+  const { studioname } = useParams(); //url 参数
+  return <>{studioname}</>;
+};
+
+export default Widget;

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

@@ -0,0 +1,27 @@
+import { useIntl } from "react-intl";
+import { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+import { Button, Card } from "antd";
+
+import GoBack from "../../../components/studio/GoBack";
+
+const Widget = () => {
+  const intl = useIntl();
+  const { studioname, courseId } = useParams(); //url 参数
+  const [title, setTitle] = useState("loading");
+  useEffect(() => {
+    setTitle("title");
+  }, [courseId]);
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/course/list`} title={title} />}
+      extra={
+        <Button type="link" danger>
+          {intl.formatMessage({ id: "buttons.group.exit" })}
+        </Button>
+      }
+    ></Card>
+  );
+};
+
+export default Widget;

+ 1 - 1
dashboard/src/pages/studio/group/show.tsx

@@ -30,7 +30,7 @@ const Widget = () => {
     >
       <Row>
         <Col flex="auto">
-          <GroupFile groupid={groupid} />
+          <GroupFile groupId={groupid} />
         </Col>
         <Col flex="400px">
           <GroupMember groupId={groupid} />