Explorar o código

Merge pull request #1049 from visuddhinanda/agile

🔥 重复的路由
visuddhinanda %!s(int64=3) %!d(string=hai) anos
pai
achega
f68345b3ab

+ 5 - 7
dashboard/src/Router.tsx

@@ -58,6 +58,7 @@ import StudioRecent from "./pages/studio/recent";
 import StudioChannel from "./pages/studio/channel";
 import StudioChannelList from "./pages/studio/channel/list";
 import StudioChannelEdit from "./pages/studio/channel/edit";
+import StudioChannelShow from "./pages/studio/channel/show";
 
 import StudioGroup from "./pages/studio/group";
 import StudioGroupList from "./pages/studio/group/list";
@@ -75,10 +76,6 @@ import StudioDictList from "./pages/studio/dict/list";
 import StudioTerm from "./pages/studio/term";
 import StudioTermList from "./pages/studio/term/list";
 
-import StudioCourse from "./pages/studio/course";
-import StudioCourseList from "./pages/studio/course/list";
-import StudioCourseEdit from "./pages/studio/course/edit";
-
 import StudioArticle from "./pages/studio/article";
 import StudioArticleList from "./pages/studio/article/list";
 import StudioArticleEdit from "./pages/studio/article/edit";
@@ -183,13 +180,14 @@ const Widget = () => {
         <Route path="channel" element={<StudioChannel />}>
           <Route path="list" element={<StudioChannelList />} />
           <Route path=":channelid/edit" element={<StudioChannelEdit />} />
+          <Route path=":channelId" element={<StudioChannelShow />} />
         </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 path=":groupId" element={<StudioGroupShow />} />
+          <Route path=":groupId/edit" element={<StudioGroupEdit />} />
+          <Route path=":groupId/show" element={<StudioGroupShow />} />
         </Route>
 
         <Route path="course" element={<StudioCourse />}>

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

@@ -7,9 +7,9 @@ export interface IUserRequest {
   avatar?: string;
 }
 export interface IUserApiData {
-  id?: string;
-  userName?: string;
-  nickName?: string;
+  id: string;
+  userName: string;
+  nickName: string;
   avatar?: string;
 }
 export interface IUserListResponse {

+ 3 - 3
dashboard/src/components/api/Comment.ts

@@ -1,4 +1,4 @@
-import { IUserRequest } from "./Auth";
+import { IUserApiData } from "./Auth";
 
 export interface ICommentRequest {
   id?: string;
@@ -7,7 +7,7 @@ export interface ICommentRequest {
   title?: string;
   content?: string;
   parent?: string;
-  editor?: IUserRequest;
+  editor?: IUserApiData;
   created_at?: string;
   updated_at?: string;
 }
@@ -20,7 +20,7 @@ export interface ICommentApiData {
   content?: string;
   parent?: string;
   children_count: number;
-  editor: IUserRequest;
+  editor: IUserApiData;
   created_at?: string;
   updated_at?: string;
 }

+ 2 - 0
dashboard/src/components/api/Corpus.ts

@@ -172,6 +172,8 @@ export interface IChapterData {
   summary: string;
   view: number;
   like: number;
+  status?: number;
+  progress: number;
   created_at: string;
   updated_at: string;
 }

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

@@ -1,12 +1,21 @@
 import { IUser } from "../auth/User";
 import { Role } from "./Auth";
 
+export interface IShareRequest {
+  res_id: string;
+  res_type: string;
+  role: Role;
+  user_id: string;
+  user_type: string;
+}
+
 export interface IShareData {
   id?: number;
   res_id: string;
   res_type: string;
   power?: number;
   res_name: string;
+  user?: IUser;
   owner?: IUser;
   created_at?: string;
   updated_at?: string;

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

@@ -7,8 +7,6 @@ import type { IAnthologyStudioListApiResponse } from "../api/Article";
 import type { IStudioApiResponse } from "../api/Auth";
 import { get } from "../../request";
 
-const defaultData: IAnthologyStudioData[] = [];
-
 interface IAnthologyStudioData {
   count: number;
   studio: IStudioApiResponse;
@@ -19,7 +17,7 @@ interface IWidgetAnthologyList {
 }
 */
 const Widget = () => {
-  const [tableData, setTableData] = useState(defaultData);
+  const [tableData, setTableData] = useState<IAnthologyStudioData[]>([]);
   useEffect(() => {
     console.log("useEffect");
     fetchData();

+ 1 - 1
dashboard/src/components/auth/User.tsx

@@ -4,7 +4,7 @@ export interface IUser {
   id: string;
   nickName: string;
   realName: string;
-  avatar: string;
+  avatar?: string;
 }
 const Widget = ({ nickName, realName, avatar }: IUser) => {
   return (

+ 15 - 3
dashboard/src/components/comment/CommentListCard.tsx

@@ -12,6 +12,7 @@ import { IAnswerCount } from "./CommentBox";
 interface IWidget {
   resId?: string;
   resType?: string;
+  topicId?: string;
   changedAnswerCount?: IAnswerCount;
   onSelect?: Function;
   onItemCountChange?: Function;
@@ -19,6 +20,7 @@ interface IWidget {
 const Widget = ({
   resId,
   resType,
+  topicId,
   onSelect,
   changedAnswerCount,
   onItemCountChange,
@@ -38,7 +40,17 @@ const Widget = ({
   }, [changedAnswerCount]);
 
   useEffect(() => {
-    get<ICommentListResponse>(`/v2/discussion?view=question&id=${resId}`)
+    let url: string = "";
+    if (typeof topicId !== "undefined") {
+      url = `/v2/discussion?view=question-by-topic&id=${topicId}`;
+    }
+    if (typeof resId !== "undefined") {
+      url = `/v2/discussion?view=question&id=${resId}`;
+    }
+    if (url === "") {
+      return;
+    }
+    get<ICommentListResponse>(url)
       .then((json) => {
         console.log(json);
         if (json.ok) {
@@ -69,9 +81,9 @@ const Widget = ({
       .catch((e) => {
         message.error(e.message);
       });
-  }, [resId]);
+  }, [resId, topicId]);
 
-  if (typeof resId === "undefined") {
+  if (typeof resId === "undefined" && typeof topicId === "undefined") {
     return <div>该资源尚未创建,不能发表讨论。</div>;
   }
 

+ 45 - 50
dashboard/src/components/library/HeadBar.tsx

@@ -81,59 +81,54 @@ const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
       ),
       key: "help",
     },
+
+    {
+      label: (
+        <a
+          href="https://asset-hk.wikipali.org/handbook/zh-Hans"
+          target="_blank"
+          rel="noreferrer"
+        >
+          {intl.formatMessage({
+            id: "columns.library.palihandbook.title",
+          })}
+        </a>
+      ),
+      key: "palihandbook",
+    },
     {
       label: (
-        <Space>
-          {intl.formatMessage({ id: "columns.library.more.title" })}
-        </Space>
+        <Link to="/calendar">
+          {intl.formatMessage({
+            id: "columns.library.calendar.title",
+          })}
+        </Link>
       ),
-      key: "more",
-      children: [
-        {
-          label: (
-            <a
-              href="https://asset-hk.wikipali.org/handbook/zh-Hans"
-              target="_blank"
-              rel="noreferrer"
-            >
-              {intl.formatMessage({
-                id: "columns.library.palihandbook.title",
-              })}
-            </a>
-          ),
-          key: "palihandbook",
-        },
-        {
-          label: (
-            <Link to="/calendar">
-              {intl.formatMessage({
-                id: "columns.library.calendar.title",
-              })}
-            </Link>
-          ),
-          key: "calendar",
-        },
-        {
-          label: (
-            <Link to="/convertor">
-              {intl.formatMessage({
-                id: "columns.library.convertor.title",
-              })}
-            </Link>
-          ),
-          key: "convertor",
-        },
-        {
-          label: (
-            <Link to="/statistics">
-              {intl.formatMessage({
-                id: "columns.library.statistics.title",
-              })}
-            </Link>
-          ),
-          key: "statistics",
-        },
-      ],
+      key: "calendar",
+    },
+    {
+      label: (
+        <Link to="/convertor">
+          {intl.formatMessage({
+            id: "columns.library.convertor.title",
+          })}
+        </Link>
+      ),
+      key: "convertor",
+    },
+    {
+      label: (
+        <Link to="/statistics">
+          {intl.formatMessage({
+            id: "columns.library.statistics.title",
+          })}
+        </Link>
+      ),
+      key: "statistics",
+    },
+    {
+      label: <Link to="/discussion/list">Discussion(alpha)</Link>,
+      key: "discussion",
     },
   ];
   return (

+ 4 - 0
dashboard/src/components/nut/Home.tsx

@@ -10,6 +10,7 @@ import DemoForm from "./Form";
 import WbwTest from "./WbwTest";
 import CommentList from "../comment/CommentList";
 import TreeTest from "./TreeTest";
+import Share from "../share/Share";
 
 const Widget = () => {
   const data = Array(100)
@@ -30,6 +31,9 @@ const Widget = () => {
   return (
     <div>
       <h1>Home</h1>
+      <div>
+        <Share resId="dd" resType="dd" />
+      </div>
       <TreeTest />
       <h2>comment</h2>
       <CommentList data={data} />

+ 158 - 0
dashboard/src/components/share/Share.tsx

@@ -0,0 +1,158 @@
+import { ProForm, ProFormSelect } from "@ant-design/pro-components";
+import { Divider, List, message, Select } from "antd";
+import { useState } from "react";
+import { useIntl } from "react-intl";
+import { get, post } from "../../request";
+import { IUserApiData, IUserListResponse, Role } from "../api/Auth";
+import { IShareData, IShareRequest, IShareResponse } from "../api/Share";
+
+interface IShareUserList {
+  user: IUserApiData;
+  role: Role;
+}
+interface IWidget {
+  resId: string;
+  resType: string;
+}
+const Widget = ({ resId, resType }: IWidget) => {
+  const [tableData, setTableData] = useState<IShareUserList[]>();
+  const roleList = ["owner", "manager", "editor", "member", "delete"];
+  const intl = useIntl();
+  interface IFormData {
+    userId: string;
+    userType: string;
+    role: Role;
+  }
+  return (
+    <div>
+      <ProForm<IFormData>
+        onFinish={async (values: IFormData) => {
+          // TODO
+          console.log(values);
+          if (typeof resId !== "undefined") {
+            post<IShareRequest, IShareResponse>("/v2/share", {
+              user_id: values.userId,
+              user_type: values.userType,
+              role: values.role,
+              res_id: resId,
+              res_type: resType,
+            }).then((json) => {
+              console.log("add member", json);
+              if (json.ok) {
+                message.success(intl.formatMessage({ id: "flashes.success" }));
+              }
+            });
+          }
+        }}
+      >
+        <ProForm.Group>
+          <ProFormSelect
+            name="resType"
+            label={intl.formatMessage({ id: "forms.fields.role.label" })}
+            allowClear={false}
+            options={[
+              {
+                value: "user",
+                label: intl.formatMessage({ id: "auth.type.user" }),
+              },
+              {
+                value: "group",
+                label: intl.formatMessage({ id: "auth.type.group" }),
+              },
+            ]}
+            rules={[
+              {
+                required: true,
+                message: intl.formatMessage({
+                  id: "forms.message.user.required",
+                }),
+              },
+            ]}
+          />
+          <ProFormSelect
+            name="userId"
+            label={intl.formatMessage({ id: "forms.fields.user.label" })}
+            width="md"
+            showSearch
+            debounceTime={300}
+            fieldProps={{
+              mode: "tags",
+            }}
+            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",
+                }),
+              },
+            ]}
+          />
+          <ProFormSelect
+            name="role"
+            label={intl.formatMessage({ id: "forms.fields.role.label" })}
+            allowClear={false}
+            options={roleList.map((item) => {
+              return {
+                value: item,
+                label: intl.formatMessage({ id: "auth.role." + item }),
+              };
+            })}
+            rules={[
+              {
+                required: true,
+                message: intl.formatMessage({
+                  id: "forms.message.user.required",
+                }),
+              },
+            ]}
+          />
+        </ProForm.Group>
+      </ProForm>
+      <Divider></Divider>
+      <List
+        itemLayout="vertical"
+        size="large"
+        dataSource={tableData}
+        renderItem={(item) => (
+          <List.Item>
+            <div style={{ display: "flex" }}>
+              <span>{item.user.nickName}</span>
+              <Select
+                defaultValue={item.role}
+                style={{ width: "100%" }}
+                onChange={(value: string) => {
+                  console.log(`selected ${value}`);
+                }}
+                options={roleList.map((item) => {
+                  return {
+                    value: item,
+                    label: intl.formatMessage({ id: "auth.role." + item }),
+                  };
+                })}
+              />
+            </div>
+          </List.Item>
+        )}
+      />
+    </div>
+  );
+};
+
+export default Widget;

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

@@ -4,6 +4,9 @@ const items = {
   "auth.role.manager": "管理员",
   "auth.role.editor": "编辑者",
   "auth.role.member": "成员",
+  "auth.role.unknown": "未知",
+  "auth.type.user": "用户",
+  "auth.type.group": "群组",
 };
 
 export default items;

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

@@ -52,6 +52,7 @@ const items = {
   "forms.message.user.required": "请选择用户",
   "forms.message.user.delete": "删除用户吗?此操作无法恢复。",
   "forms.message.member.delete": "删除此成员吗?此操作无法恢复。",
+  "forms.fields.description.label": "简介",
 };
 
 export default items;

+ 132 - 4
dashboard/src/pages/library/discussion/list.tsx

@@ -1,13 +1,141 @@
+import { useIntl } from "react-intl";
 import { Link } from "react-router-dom";
+import { ProList } from "@ant-design/pro-components";
+import { message, Space, Tag } from "antd";
+import { MessageOutlined } from "@ant-design/icons";
 
+import { IComment } from "../../../components/comment/CommentItem";
+import { ICommentListResponse } from "../../../components/api/Comment";
+import { get } from "../../../request";
+import { IUser } from "../../../components/auth/User";
+import TimeShow from "../../../components/general/TimeShow";
+
+interface IDiscussion {
+  id: string;
+  title?: string;
+  resType: string;
+  user: IUser;
+  childrenCount?: number;
+  updatedAt?: string;
+  createdAt?: string;
+}
 const Widget = () => {
   // TODO
+  const intl = useIntl();
+
   return (
     <div>
-      <div>
-        <Link to="/course/show/12345">课程1</Link>
-        <Link to="/course/show/23456">课程2</Link>
-      </div>
+      <ProList<IDiscussion>
+        search={{
+          filterType: "light",
+        }}
+        rowKey="id"
+        headerTitle="问答&求助"
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          const url = `/v2/discussion?view=all`;
+          const json = await get<ICommentListResponse>(url);
+          if (!json.ok) {
+            message.error(json.message);
+          }
+          const discussions: IDiscussion[] = json.data.rows.map((item) => {
+            return {
+              id: item.id,
+              resType: item.res_type,
+              user: {
+                id: item.editor.id,
+                nickName: item.editor.nickName,
+                realName: item.editor.userName,
+                avatar: item.editor.avatar,
+              },
+              title: item.title,
+              childrenCount: item.children_count,
+              createdAt: item.created_at,
+              updatedAt: item.updated_at,
+            };
+          });
+          return {
+            total: json.data.count,
+            succcess: true,
+            data: discussions,
+          };
+        }}
+        pagination={{
+          pageSize: 20,
+        }}
+        metas={{
+          title: {
+            dataIndex: "title",
+            title: "标题",
+            render: (_, row) => {
+              return (
+                <Link to={`/discussion/topic/${row.id}`}>{row.title}</Link>
+              );
+            },
+          },
+          avatar: {
+            search: false,
+            render: (_, row) => {
+              return <span>{row.user.avatar}</span>;
+            },
+          },
+          description: {
+            search: false,
+            render: (_, row) => {
+              return (
+                <Space>
+                  {`${row.user.nickName} created on`}
+                  <TimeShow time={row.createdAt} title={""} />
+                </Space>
+              );
+            },
+          },
+          subTitle: {
+            render: (_, row) => {
+              return (
+                <Space size={0}>
+                  <Tag color="blue" key={row.resType}>
+                    {row.resType}
+                  </Tag>
+                </Space>
+              );
+            },
+            search: false,
+          },
+          actions: {
+            render: (text, row) => [
+              row.childrenCount ? (
+                <Space>
+                  <MessageOutlined /> {row.childrenCount}
+                </Space>
+              ) : (
+                <></>
+              ),
+            ],
+            search: false,
+          },
+          status: {
+            // 自己扩展的字段,主要用于筛选,不在列表中显示
+            title: "状态",
+            valueType: "select",
+            valueEnum: {
+              all: { text: "全部", status: "Default" },
+              open: {
+                text: "未解决",
+                status: "Error",
+              },
+              closed: {
+                text: "已解决",
+                status: "Success",
+              },
+              processing: {
+                text: "解决中",
+                status: "Processing",
+              },
+            },
+          },
+        }}
+      />
     </div>
   );
 };

+ 35 - 2
dashboard/src/pages/library/discussion/topic.tsx

@@ -1,19 +1,52 @@
+import { useNavigate } from "react-router-dom";
+import { Tabs } from "antd";
 import { useParams } from "react-router-dom";
 import CommentAnchor from "../../../components/comment/CommentAnchor";
+import { IComment } from "../../../components/comment/CommentItem";
+import CommentListCard from "../../../components/comment/CommentListCard";
 
 import CommentTopic from "../../../components/comment/CommentTopic";
 
 const Widget = () => {
   // TODO
   const { id } = useParams(); //url 参数
-
+  const navigate = useNavigate();
   return (
     <div>
       <div>
         <CommentAnchor id={id} />
       </div>
       <div>
-        <CommentTopic topicId={id} />
+        <Tabs
+          defaultActiveKey="current"
+          items={[
+            {
+              label: `当前`,
+              key: "current",
+              children: <CommentTopic topicId={id} />,
+            },
+            {
+              label: `全部`,
+              key: "all",
+              children: (
+                <CommentListCard
+                  topicId={id}
+                  onSelect={(
+                    e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
+                    comment: IComment
+                  ) => {
+                    navigate(`/discussion/topic/${comment.id}`);
+                  }}
+                />
+              ),
+            },
+            {
+              label: `类似`,
+              key: "sim",
+              children: "",
+            },
+          ]}
+        />
       </div>
     </div>
   );

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

@@ -33,7 +33,6 @@ const Widget = () => {
     <Card
       title={<GoBack to={`/studio/${studioname}/channel/list`} title={title} />}
     >
-      <Space>{channelid}</Space>
       <ProForm<IFormData>
         onFinish={async (values: IFormData) => {
           // TODO

+ 2 - 3
dashboard/src/pages/studio/channel/list.tsx

@@ -3,10 +3,9 @@ import { ProTable } from "@ant-design/pro-components";
 import { useIntl } from "react-intl";
 import { Link } from "react-router-dom";
 import { Space, Table } from "antd";
-import { PlusOutlined } from "@ant-design/icons";
 import type { MenuProps } from "antd";
 import { Button, Dropdown, Menu, Popover } from "antd";
-import { SearchOutlined } from "@ant-design/icons";
+import { SearchOutlined, PlusOutlined } from "@ant-design/icons";
 
 import ChannelCreate from "../../../components/channel/ChannelCreate";
 import { get } from "../../../request";
@@ -77,7 +76,7 @@ const Widget = () => {
             ellipsis: true,
             render: (text, row, index, action) => {
               return (
-                <Link to={`/studio/${studioname}/channel/${row.uid}/edit`}>
+                <Link to={`/studio/${studioname}/channel/${row.uid}`}>
                   {row.title}
                 </Link>
               );

+ 239 - 0
dashboard/src/pages/studio/channel/show.tsx

@@ -0,0 +1,239 @@
+import { useState } from "react";
+import { useIntl } from "react-intl";
+import { useParams } from "react-router-dom";
+import { Card, Progress } from "antd";
+import { ProTable } from "@ant-design/pro-components";
+import { Link } from "react-router-dom";
+import { Space, Table } from "antd";
+import type { MenuProps } from "antd";
+import { Button, Dropdown, Menu } from "antd";
+import { SearchOutlined } from "@ant-design/icons";
+
+import { get } from "../../../request";
+
+import GoBack from "../../../components/studio/GoBack";
+import { IChapterListResponse } from "../../../components/api/Corpus";
+
+const onMenuClick: MenuProps["onClick"] = (e) => {
+  console.log("click", e);
+};
+
+const menu = (
+  <Menu
+    onClick={onMenuClick}
+    items={[
+      {
+        key: "share",
+        label: "分享",
+        icon: <SearchOutlined />,
+      },
+      {
+        key: "delete",
+        label: "删除",
+        icon: <SearchOutlined />,
+      },
+    ]}
+  />
+);
+
+interface IItem {
+  sn: number;
+  title: string;
+  subTitle: string;
+  summary: string;
+  book: number;
+  paragraph: number;
+  path: string;
+  progress: number;
+  view: number;
+  createdAt: number;
+  updatedAt: number;
+}
+const Widget = () => {
+  const intl = useIntl();
+  const { channelId } = useParams(); //url 参数
+  const { studioname } = useParams();
+  const [title, setTitle] = useState("");
+
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/channel/list`} title={title} />}
+    >
+      <ProTable<IItem>
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.title.label",
+            }),
+            dataIndex: "title",
+            key: "title",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <div>
+                  <div>
+                    <Link
+                      to={`/article/chapter/${row.book}-${row.paragraph}_${channelId}`}
+                    >
+                      {row.title}
+                    </Link>
+                  </div>
+                  <div>{row.subTitle}</div>
+                </div>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.summary.label",
+            }),
+            dataIndex: "summary",
+            key: "summary",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.publicity.label",
+            }),
+            dataIndex: "progress",
+            key: "progress",
+            width: 100,
+            search: false,
+            render: (text, row, index, action) => {
+              const per = Math.round(row.progress * 100);
+              return <Progress percent={per} size="small" />;
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.publicity.label",
+            }),
+            dataIndex: "view",
+            key: "view",
+            width: 100,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created-at",
+            width: 100,
+            search: false,
+            dataIndex: "createdAt",
+            valueType: "date",
+            sorter: (a, b) => a.createdAt - b.createdAt,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button key={index} type="link" overlay={menu}>
+                  <Link
+                    to={`/article/chapter/${row.book}-${row.paragraph}_${channelId}/edit`}
+                  >
+                    {intl.formatMessage({
+                      id: "buttons.edit",
+                    })}
+                  </Link>
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        rowSelection={{
+          // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+          // 注释该行则默认不显示下拉选项
+          selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+        }}
+        tableAlertRender={({
+          selectedRowKeys,
+          selectedRows,
+          onCleanSelected,
+        }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({ id: "buttons.selected" })}
+              {selectedRowKeys.length}
+              <Button
+                type="link"
+                style={{ marginInlineStart: 8 }}
+                onClick={onCleanSelected}
+              >
+                {intl.formatMessage({ id: "buttons.unselect" })}
+              </Button>
+            </span>
+          </Space>
+        )}
+        tableAlertOptionRender={() => {
+          return (
+            <Space size={16}>
+              <Button type="link">
+                {intl.formatMessage({
+                  id: "buttons.delete.all",
+                })}
+              </Button>
+            </Space>
+          );
+        }}
+        request={async (params = {}, sorter, filter) => {
+          // TODO
+          console.log(params, sorter, filter);
+          const offset = (params.current || 1 - 1) * (params.pageSize || 20);
+          const res = await get<IChapterListResponse>(
+            `/v2/progress?view=chapter&channel=${channelId}&progress=0.01&offset=${offset}`
+          );
+          console.log(res.data.rows);
+          const items: IItem[] = res.data.rows.map((item, id) => {
+            const createdAt = new Date(item.created_at);
+            const updatedAt = new Date(item.updated_at);
+            return {
+              sn: id + offset + 1,
+              book: item.book,
+              paragraph: item.para,
+              view: item.view,
+              title: item.title,
+              subTitle: item.toc,
+              summary: item.summary,
+              path: item.path,
+              progress: item.progress,
+              createdAt: createdAt.getTime(),
+              updatedAt: updatedAt.getTime(),
+            };
+          });
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+      />
+    </Card>
+  );
+};
+
+export default Widget;

+ 4 - 5
dashboard/src/pages/studio/course/edit.tsx

@@ -37,7 +37,7 @@ let groupid = "1";
 
 const Widget = () => {
   const intl = useIntl();
-  const { studioname, courseid } = useParams(); //url 参数
+  const { studioname, courseId } = useParams(); //url 参数
   const [title, setTitle] = useState("loading");
 
   return (
@@ -61,9 +61,9 @@ const Widget = () => {
                 onFinish={async (values: IFormData) => {
                   // TODO
                   let request = {
-                    uid: courseid?.toString,
-                    title: "课程" + courseid,
-                    subtitle: "课程副标题" + courseid,
+                    uid: courseId?.toString,
+                    title: "课程" + courseId,
+                    subtitle: "课程副标题" + courseId,
                     teacher: 1,
                     course_count: 2,
                     type: 30,
@@ -235,7 +235,6 @@ request={async () => {
         },
       ]}
     />
-
   );
 };
 

+ 50 - 35
dashboard/src/pages/studio/course/list.tsx

@@ -1,8 +1,17 @@
 import { useParams, Link } from "react-router-dom";
 import { useIntl } from "react-intl";
-import React, { useState } from 'react';
-import { Space, Badge,Button, Popover, Dropdown, MenuProps, Menu, Table } from "antd";
-import { ProTable,ProList  } from "@ant-design/pro-components";
+import React, { useState } from "react";
+import {
+  Space,
+  Badge,
+  Button,
+  Popover,
+  Dropdown,
+  MenuProps,
+  Menu,
+  Table,
+} from "antd";
+import { ProTable, ProList } from "@ant-design/pro-components";
 import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
 
 import CourseCreate from "../../../components/library/course/CourseCreate";
@@ -17,7 +26,6 @@ const menu = (
   <Menu
     onClick={onMenuClick}
     items={[
-
       {
         key: "1",
         label: "分享",
@@ -33,13 +41,13 @@ const menu = (
 );
 interface DataItem {
   sn: number;
-  id: string;//课程ID
-  title: string;//标题
-  subtitle: string;//副标题
-  teacher: string;//UserID
+  id: string; //课程ID
+  title: string; //标题
+  subtitle: string; //副标题
+  teacher: string; //UserID
   //course_count: number;//课程数
-  type: number;//类型-公开/内部
-  createdAt: number;//创建时间
+  type: number; //类型-公开/内部
+  createdAt: number; //创建时间
   //updated_at: number;//修改时间
   //article_id: number;//文集ID
   //course_start_at: string;//课程开始时间
@@ -55,8 +63,8 @@ const renderBadge = (count: number, active = false) => {
       style={{
         marginBlockStart: -2,
         marginInlineStart: 4,
-        color: active ? '#1890FF' : '#999',
-        backgroundColor: active ? '#E6F7FF' : '#eee',
+        color: active ? "#1890FF" : "#999",
+        backgroundColor: active ? "#E6F7FF" : "#eee",
       }}
     />
   );
@@ -65,10 +73,8 @@ 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>("tab1");
   return (
-
-    
     <>
       <ProTable<DataItem>
         columns={[
@@ -185,7 +191,6 @@ const Widget = () => {
               >
                 {intl.formatMessage({
                   id: "buttons.unselect",
-                  
                 })}
               </Button>
             </span>
@@ -228,14 +233,14 @@ const Widget = () => {
               createdAt: date.getTime(),
             };
           });*/
-          
+
           //const items = Array.from({ length: 23 }).map((_, i) => ({
-            const items: DataItem[] = [
-              {
+          const items: DataItem[] = [
+            {
               sn: 1,
               id: "1",
-              title: "课程"+1,
-              subtitle: "课程副标题"+1,
+              title: "课程" + 1,
+              subtitle: "课程副标题" + 1,
               teacher: "小僧善巧",
               type: 30,
               createdAt: 20020202,
@@ -249,15 +254,15 @@ const Widget = () => {
             {
               sn: 2,
               id: "2",
-              title: "课程"+2,
-              subtitle: "课程副标题"+2,
+              title: "课程" + 2,
+              subtitle: "课程副标题" + 2,
               teacher: "小僧善巧",
               type: 30,
               createdAt: 20020202,
-            }
+            },
           ];
           return {
-            total: items.length,//res.data.count,
+            total: items.length, //res.data.count,
             succcess: true,
             data: items,
           };
@@ -272,7 +277,6 @@ const Widget = () => {
         options={{
           search: true,
         }}
-        
         toolBarRender={() => [
           <Popover
             content={courseCreate}
@@ -289,24 +293,36 @@ const Widget = () => {
             activeKey,
             items: [
               {
-                key: 'tab1',
-                label: <span>我建立的课程{renderBadge(99, activeKey === 'tab1')}</span>,
+                key: "create",
+                label: (
+                  <span>
+                    我建立的课程{renderBadge(99, activeKey === "create")}
+                  </span>
+                ),
               },
               {
-                key: 'tab2',
-                label: <span>我参加的课程{renderBadge(99, activeKey === 'tab1')}</span>,
+                key: "study",
+                label: (
+                  <span>
+                    我参加的课程{renderBadge(99, activeKey === "study")}
+                  </span>
+                ),
               },
               {
-                key: 'tab3',
-                label: <span>我主讲的课程{renderBadge(32, activeKey === 'tab2')}</span>,
+                key: "teach",
+                label: (
+                  <span>
+                    我任教的课程{renderBadge(32, activeKey === "teach")}
+                  </span>
+                ),
               },
             ],
             onChange(key) {
               setActiveKey(key);
             },
-          }
+          },
         }}
-/*
+        /*
         toolbar={{
           menu: {
             activeKey,
@@ -338,7 +354,6 @@ const Widget = () => {
       />
     </>
   );
-
 };
 
 export default Widget;

+ 62 - 106
dashboard/src/pages/studio/group/edit.tsx

@@ -1,124 +1,80 @@
+import { useState } from "react";
 import { useIntl } from "react-intl";
 import { useParams } from "react-router-dom";
 import {
-	ProForm,
-	ProFormText,
-	ProFormSelect,
-	ProFormTextArea,
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
 } from "@ant-design/pro-components";
 import { message, Card } from "antd";
 
 import { IGroupResponse } from "../../../components/api/Group";
-import { useEffect, useState } from "react";
 import { get } from "../../../request";
 import GoBack from "../../../components/studio/GoBack";
 
 interface IFormData {
-	name: string;
-	type: string;
-	lang: string;
-	summary: string;
-	studio: string;
+  id: string;
+  name: string;
+  description: string;
+  studioId: string;
 }
 const Widget = () => {
-	const intl = useIntl();
-	const { studioname, groupid } = useParams(); //url 参数
-	const [title, setTitle] = useState("Loading");
-	useEffect(() => {
-		get<IGroupResponse>(`/v2/group/${groupid}`).then((json) => {
-			setTitle(json.data.name);
-		});
-	}, [groupid]);
+  const intl = useIntl();
+  const { studioname, groupId } = useParams(); //url 参数
+  const [title, setTitle] = useState("Loading");
 
-	return (
-		<Card
-			title={
-				<GoBack to={`/studio/${studioname}/group/list`} title={title} />
-			}
-		>
-			<ProForm<IFormData>
-				onFinish={async (values: IFormData) => {
-					// TODO
-					values.studio = "aaaa";
-					console.log(values);
-					message.success(
-						intl.formatMessage({ id: "flashes.success" })
-					);
-				}}
-			>
-				<ProForm.Group>
-					<ProFormText
-						width="md"
-						name="name"
-						required
-						label={intl.formatMessage({ id: "channel.name" })}
-						rules={[
-							{
-								required: true,
-								message: intl.formatMessage({
-									id: "channel.create.message.noname",
-								}),
-							},
-						]}
-					/>
-				</ProForm.Group>
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/group/list`} title={title} />}
+    >
+      <ProForm<IFormData>
+        onFinish={async (values: IFormData) => {
+          // TODO
+          console.log(values);
+          message.success(intl.formatMessage({ id: "flashes.success" }));
+        }}
+        formKey="group_edit"
+        request={async () => {
+          const res = await get<IGroupResponse>(`/v2/group/${groupId}`);
+          setTitle(res.data.name);
+          console.log(res.data);
+          return {
+            id: res.data.uid,
+            name: res.data.name,
+            description: res.data.description,
+            studioId: res.data.studio.id,
+          };
+        }}
+      >
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="name"
+            required
+            label={intl.formatMessage({ id: "forms.fields.name.label" })}
+            rules={[
+              {
+                required: true,
+                message: intl.formatMessage({
+                  id: "channel.create.message.noname",
+                }),
+              },
+            ]}
+          />
+        </ProForm.Group>
 
-				<ProForm.Group>
-					<ProFormSelect
-						options={[
-							{
-								value: "translation",
-								label: intl.formatMessage({
-									id: "channel.type.translation.label",
-								}),
-							},
-							{
-								value: "nissaya",
-								label: intl.formatMessage({
-									id: "channel.type.nissaya.label",
-								}),
-							},
-						]}
-						width="md"
-						name="type"
-						rules={[
-							{
-								required: true,
-								message: intl.formatMessage({
-									id: "channel.create.message.noname",
-								}),
-							},
-						]}
-						label={intl.formatMessage({ id: "channel.type" })}
-					/>
-				</ProForm.Group>
-				<ProForm.Group>
-					<ProFormSelect
-						options={[
-							{ value: "zh-Hans", label: "简体中文" },
-							{ value: "zh-Hant", label: "繁体中文" },
-							{ value: "en-US", label: "English" },
-						]}
-						width="md"
-						name="lang"
-						rules={[
-							{
-								required: true,
-								message: intl.formatMessage({
-									id: "channel.create.message.noname",
-								}),
-							},
-						]}
-						label={intl.formatMessage({ id: "channel.lang" })}
-					/>
-				</ProForm.Group>
-
-				<ProForm.Group>
-					<ProFormTextArea name="summary" label="简介" />
-				</ProForm.Group>
-			</ProForm>
-		</Card>
-	);
+        <ProForm.Group>
+          <ProFormTextArea
+            width="md"
+            name="description"
+            label={intl.formatMessage({
+              id: "forms.fields.description.label",
+            })}
+          />
+        </ProForm.Group>
+      </ProForm>
+    </Card>
+  );
 };
 
 export default Widget;

+ 5 - 3
dashboard/src/pages/studio/group/list.tsx

@@ -108,9 +108,11 @@ const Widget = () => {
             valueType: "option",
             render: (text, row, index, action) => [
               <Dropdown.Button type="link" key={index} overlay={menu}>
-                {intl.formatMessage({
-                  id: "buttons.edit",
-                })}
+                <Link to={`/studio/${studioname}/group/${row.id}/edit`}>
+                  {intl.formatMessage({
+                    id: "buttons.edit",
+                  })}
+                </Link>
               </Dropdown.Button>,
             ],
           },

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

@@ -12,13 +12,13 @@ import GoBack from "../../../components/studio/GoBack";
 
 const Widget = () => {
   const intl = useIntl();
-  const { studioname, groupid } = useParams(); //url 参数
+  const { studioname, groupId } = useParams(); //url 参数
   const [title, setTitle] = useState("loading");
   useEffect(() => {
-    get<IGroupResponse>(`/v2/group/${groupid}`).then((json) => {
+    get<IGroupResponse>(`/v2/group/${groupId}`).then((json) => {
       setTitle(json.data.name);
     });
-  }, [groupid]);
+  }, [groupId]);
   return (
     <Card
       title={<GoBack to={`/studio/${studioname}/group/list`} title={title} />}
@@ -30,10 +30,10 @@ const Widget = () => {
     >
       <Row>
         <Col flex="auto">
-          <GroupFile groupId={groupid} />
+          <GroupFile groupId={groupId} />
         </Col>
         <Col flex="400px">
-          <GroupMember groupId={groupid} />
+          <GroupMember groupId={groupId} />
         </Col>
       </Row>
     </Card>