visuddhinanda 2 anni fa
parent
commit
5f5077735c

+ 132 - 0
dashboard/src/components/course/CourseMemberTimeLine.tsx

@@ -0,0 +1,132 @@
+import { useEffect, useRef } from "react";
+import { useIntl } from "react-intl";
+import { Space, Tag } from "antd";
+import { ActionType, ProList } from "@ant-design/pro-components";
+
+import { get } from "../../request";
+import { ICourseMemberData, ICourseMemberListResponse } from "../api/Course";
+import User from "../auth/User";
+import TimeShow from "../general/TimeShow";
+import { getStatusColor } from "./CourseMemberList";
+
+interface IWidget {
+  courseId?: string;
+  userId?: string;
+}
+
+const CourseMemberTimeLineWidget = ({ courseId, userId }: IWidget) => {
+  const intl = useIntl(); //i18n
+
+  const ref = useRef<ActionType>();
+
+  useEffect(() => {
+    ref.current?.reload();
+  }, [courseId, userId]);
+
+  return (
+    <>
+      <ProList<ICourseMemberData>
+        actionRef={ref}
+        search={{
+          filterType: "light",
+        }}
+        metas={{
+          title: {
+            dataIndex: "name",
+            search: false,
+            render(dom, entity, index, action, schema) {
+              return entity.user?.nickName;
+            },
+          },
+          avatar: {
+            render(dom, entity, index, action, schema) {
+              return <User {...entity.user} showName={false} />;
+            },
+            editable: false,
+          },
+          description: {
+            dataIndex: "desc",
+            search: false,
+            render(dom, entity, index, action, schema) {
+              return (
+                <Space>
+                  <User {...entity.editor} showAvatar={false} />
+                  <TimeShow type="secondary" updatedAt={entity.updated_at} />
+                </Space>
+              );
+            },
+          },
+          subTitle: {
+            search: false,
+            render: (
+              dom: React.ReactNode,
+              entity: ICourseMemberData,
+              index: number
+            ) => {
+              return (
+                <Tag>
+                  {intl.formatMessage({
+                    id: `auth.role.${entity.role}`,
+                  })}
+                </Tag>
+              );
+            },
+          },
+          actions: {
+            search: false,
+            render: (text, row, index, action) => {
+              const statusColor = getStatusColor(row.status);
+              return [
+                <span style={{ color: statusColor }}>
+                  {intl.formatMessage({
+                    id: `course.member.status.${row.status}.label`,
+                  })}
+                </span>,
+              ];
+            },
+          },
+        }}
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+
+          let url = `/v2/course-member?view=timeline&course=${courseId}&userId=${userId}`;
+          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 : "");
+          }
+          console.info("api request", url);
+          const res = await get<ICourseMemberListResponse>(url);
+          if (res.ok) {
+            console.debug("api response", res.data);
+            return {
+              total: res.data.count,
+              succcess: true,
+              data: res.data.rows,
+            };
+          } else {
+            console.error(res.message);
+            return {
+              total: 0,
+              succcess: false,
+              data: [],
+            };
+          }
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        options={{
+          search: false,
+        }}
+      />
+    </>
+  );
+};
+
+export default CourseMemberTimeLineWidget;

+ 292 - 0
dashboard/src/components/course/RolePower.ts

@@ -0,0 +1,292 @@
+import moment from "moment";
+import {
+  TCourseJoinMode,
+  TCourseMemberAction,
+  TCourseMemberStatus,
+} from "../api/Course";
+
+export interface IAction {
+  mode: TCourseJoinMode[];
+  status: TCourseMemberStatus;
+  before: TCourseMemberAction[];
+  duration: TCourseMemberAction[];
+  after: TCourseMemberAction[];
+}
+
+export const getStudentActionsByStatus = (
+  status?: TCourseMemberStatus,
+  mode?: TCourseJoinMode,
+  startAt?: string,
+  endAt?: string
+): TCourseMemberAction[] | undefined => {
+  return getActionsByStatus(studentData, status, mode, startAt, endAt);
+};
+const getActionsByStatus = (
+  data: IAction[],
+  status?: TCourseMemberStatus,
+  mode?: TCourseJoinMode,
+  startAt?: string,
+  endAt?: string
+): TCourseMemberAction[] | undefined => {
+  if (!startAt || !endAt || !mode || !status) {
+    return undefined;
+  }
+  const actions = data.find((value) => {
+    if (value.mode.includes(mode) && value.status === status) {
+      if (moment().isBefore(moment(startAt))) {
+        if (value.before && value.before.length > 0) {
+          return true;
+        }
+      } else if (moment().isBefore(moment(endAt))) {
+        if (value.duration && value.duration.length > 0) {
+          return true;
+        }
+      } else {
+        if (value.after && value.after.length > 0) {
+          return true;
+        }
+      }
+    }
+    return false;
+  });
+
+  if (actions) {
+    if (moment().isBefore(moment(startAt))) {
+      return actions.before;
+    } else if (moment().isBefore(moment(endAt))) {
+      return actions.duration;
+    } else {
+      return actions.after;
+    }
+  } else {
+    return undefined;
+  }
+};
+
+export const test = (
+  data: IAction[],
+  action: TCourseMemberAction,
+  startAt?: string,
+  endAt?: string,
+  mode?: TCourseJoinMode,
+  status?: TCourseMemberStatus
+): boolean => {
+  if (!startAt || !endAt || !mode || !status) {
+    return false;
+  }
+  const canDo = getActionsByStatus(
+    data,
+    status,
+    mode,
+    startAt,
+    endAt
+  )?.includes(action);
+
+  if (canDo) {
+    return true;
+  } else {
+    return false;
+  }
+};
+
+export const managerCanDo = (
+  action: TCourseMemberAction,
+  startAt?: string,
+  endAt?: string,
+  mode?: TCourseJoinMode,
+  status?: TCourseMemberStatus
+): boolean => {
+  if (!startAt || !endAt || !mode || !status) {
+    return false;
+  }
+
+  return test(managerData, action, startAt, endAt, mode, status);
+};
+
+export const studentCanDo = (
+  action: TCourseMemberAction,
+  startAt?: string,
+  endAt?: string,
+  mode?: TCourseJoinMode,
+  status?: TCourseMemberStatus
+): boolean => {
+  if (!startAt || !endAt || !mode || !status) {
+    return false;
+  }
+
+  return test(studentData, action, startAt, endAt, mode, status);
+};
+
+const studentData: IAction[] = [
+  {
+    mode: ["open"],
+    status: "none",
+    before: [],
+    duration: ["join"],
+    after: [],
+  },
+  {
+    mode: ["open"],
+    status: "applied",
+    before: [],
+    duration: ["leave"],
+    after: [],
+  },
+  {
+    mode: ["open"],
+    status: "left",
+    before: [],
+    duration: ["join"],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "none",
+    before: ["apply"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "invited",
+    before: ["agree", "disagree"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "revoked",
+    before: ["apply"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "accepted",
+    before: ["leave"],
+    duration: ["leave"],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "rejected",
+    before: [],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "blocked",
+    before: [],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "applied",
+    before: ["cancel"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "canceled",
+    before: ["apply"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "agreed",
+    before: ["leave"],
+    duration: ["leave"],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "disagreed",
+    before: [],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "left",
+    before: [],
+    duration: [],
+    after: [],
+  },
+];
+
+const managerData: IAction[] = [
+  {
+    mode: ["manual", "invite"],
+    status: "none",
+    before: ["invite"],
+    duration: ["invite"],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "invited",
+    before: ["revoke"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "revoked",
+    before: ["invite"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "accepted",
+    before: [],
+    duration: ["block"],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "rejected",
+    before: [],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "blocked",
+    before: [],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "applied",
+    before: ["accept", "reject"],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "agreed",
+    before: [],
+    duration: ["block"],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "disagreed",
+    before: [],
+    duration: [],
+    after: [],
+  },
+  {
+    mode: ["manual", "invite"],
+    status: "left",
+    before: [],
+    duration: [],
+    after: [],
+  },
+];

+ 69 - 0
dashboard/src/components/course/UserAction.tsx

@@ -0,0 +1,69 @@
+/**
+ * 学生接受课程管理员的邀请 参加课程
+ */
+import { Button } from "antd";
+import { useIntl } from "react-intl";
+
+import {
+  ICourseMemberData,
+  TCourseMemberAction,
+  actionMap,
+} from "../api/Course";
+import { ISetStatus, setStatus } from "./Status";
+
+interface IWidget {
+  action: TCourseMemberAction;
+  currUser?: ICourseMemberData;
+  courseName?: string;
+  onStatusChanged?: Function;
+}
+const UserActionWidget = ({
+  action,
+  currUser,
+  courseName,
+  onStatusChanged,
+}: IWidget) => {
+  const intl = useIntl();
+
+  const statusChange = (status: ICourseMemberData | undefined) => {
+    if (typeof onStatusChanged !== "undefined") {
+      onStatusChanged(status);
+    }
+  };
+  const status = actionMap(action);
+  return (
+    <>
+      {status ? (
+        <Button
+          type={action === "join" || action === "apply" ? "primary" : undefined}
+          onClick={() => {
+            if (currUser?.id) {
+              const actionParam: ISetStatus = {
+                courseMemberId: currUser.id,
+                message: intl.formatMessage(
+                  {
+                    id: `course.member.status.${action}.message`,
+                  },
+                  { course: courseName }
+                ),
+                status: status,
+                onSuccess: (data: ICourseMemberData) => {
+                  statusChange(data);
+                },
+              };
+              setStatus(actionParam);
+            }
+          }}
+        >
+          {intl.formatMessage({
+            id: `course.member.status.${action}.button`,
+          })}
+        </Button>
+      ) : (
+        <></>
+      )}
+    </>
+  );
+};
+
+export default UserActionWidget;

+ 122 - 0
dashboard/src/components/course/readme.md

@@ -0,0 +1,122 @@
+# 公开性
+
+| 报名方式 | 公开性          |
+| -------- | --------------- |
+| 自学     | 公开            |
+| 需审核   | 公开/不公开列出 |
+| 仅邀请   | 不公开          |
+
+# 可能的操作和状态
+
+| 学员               | 状态           | 管理员           | 状态          |
+| ------------------ | -------------- | ---------------- | ------------- |
+| 加入自学课程(join) | joined         | ---              | ---           |
+| 报名(apply)        | applied        | 邀请(invite)     | invited       |
+| 取消报名(cancel)   | **_canceled_** | 撤销邀请(revoke) | **_revoked_** |
+| 参加(agree)        | agreed         | 录取(accept)     | accepted      |
+| 拒绝(disagree)     | disagreed      | 不录取(reject)   | rejected      |
+| 退出(leave)        | left           | 清退(block)      | blocked       |
+
+> canceled , revoked 与无记录等效
+> 提问不公开
+
+# 角色权限
+
+| 角色   | 修改课程信息 | 增删管理员 | 增删助教 | 学员录取 | 学员清退 | 查看作业 | 回答问题 |
+| ------ | ------------ | ---------- | -------- | -------- | -------- | -------- | -------- |
+| 拥有者 | √            | √          | √        | √        | √        | √        | √        |
+| 管理员 | ----         | √          | √        | √        | √        | √        | √        |
+| 助教   | ----         | ----       | ----     | ---      | √        | √        | √        |
+
+# 管理员操作
+
+| 报名方式 | 学生状态   | 开始前      | 课程中   | 结束后 |
+| -------- | ---------- | ----------- | -------- | ------ |
+| 自学     | 未报名     | ---         | ---      | ---    |
+| 自学     | 已报名     | ---         | ---      | ---    |
+| 自学     | 未注册     | ---         | ---      | ---    |
+| 需审核   | 未注册     | ---         | ---      | ---    |
+| 需审核   | 未报名     | 邀请        | 邀请助教 | ---    |
+| 需审核   | 已邀请     | undo        | ---      | ---    |
+| 需审核   | 已撤销邀请 | undo        | ---      | ---    |
+| 需审核   | 已录取     | undo        | 清退     | ---    |
+| 需审核   | 不录取     | undo        | undo     | ---    |
+| 需审核   | 已清退     | ---         | undo     | ---    |
+| ---      | ---        | ---         | ---      | ---    |
+| 需审核   | 已报名     | 录取/不录取 | ---      | ---    |
+| 需审核   | 取消报名   | ---         | ---      | ---    |
+| 需审核   | 用户已接受 | ---         | 清退     | ---    |
+| 需审核   | 用户已拒绝 | ---         | ---      | ---    |
+| 需审核   | 已退出     | ---         | ---      | ---    |
+| 仅邀请   | 同上       | ---         | ---      | ---    |
+
+> 仅邀请与需审核相同,但是无已经报名状态
+> 所有需审核的操作均可撤销
+> 横线上为管理员设置的状态,一下为用户设置的状态
+
+# 助教-管理员 操作
+
+| 报名方式 | 学生状态   | 开始前    | 课程中    | 结束后 |
+| -------- | ---------- | --------- | --------- | ------ |
+| 自学     | 未报名     | ---       | 参加      | ---    |
+| 自学     | 已报名     | ---       | 退出      | ---    |
+| 自学     | 已退出     | ---       | 参加      | ---    |
+| 自学     | 未注册     | ---       | ---       | ---    |
+| 需审核   | 未注册     | ---       | ---       | ---    |
+| 需审核   | 已邀请     | 参加/拒绝 | 参加/拒绝 | ---    |
+| 需审核   | 已撤销邀请 | ---       | ---       | ---    |
+| 需审核   | 已录取     | 退出      | 退出      | ---    |
+| 需审核   | 不录取     | ---       | ---       | ---    |
+| 需审核   | 已清退     | ---       | 留言      | ---    |
+| ---      | ---        | ---       | ---       | ---    |
+| 需审核   | 已报名     | 取消      | ---       | ---    |
+| 需审核   | 取消报名   | ---       | ---       | ---    |
+| 需审核   | 用户已接受 | 退出      | 退出      | ---    |
+| 需审核   | 用户已拒绝 | ---       | ---       | ---    |
+| 需审核   | 已退出     | ---       | ---       | ---    |
+| 仅邀请   | 同上       | ---       | ---       | ---    |
+
+# 学生操作
+
+| 报名方式 | 学生状态   | 开始前    | 课程中 | 结束后 |
+| -------- | ---------- | --------- | ------ | ------ |
+| 自学     | 未报名     | ---       | 参加   | ---    |
+| 自学     | 已报名     | ---       | 退出   | ---    |
+| 自学     | 已退出     | ---       | 参加   | ---    |
+| 自学     | 未注册     | ---       | ---    | ---    |
+| 需审核   | 未注册     | ---       | ---    | ---    |
+| 需审核   | 未报名     | 报名      | ---    | ---    |
+| 需审核   | 已邀请     | 参加/拒绝 | ---    | ---    |
+| 需审核   | 已撤销邀请 | 报名      | ---    | ---    |
+| 需审核   | 已录取     | 退出      | 退出   | ---    |
+| 需审核   | 不录取     | ---       | ---    | ---    |
+| 需审核   | 已清退     | ---       | 留言   | ---    |
+| ---      | ---        | ---       | ---    | ---    |
+| 需审核   | 已报名     | 取消      | ---    | ---    |
+| 需审核   | 取消报名   | ---       | ---    | ---    |
+| 需审核   | 用户已接受 | 退出      | 退出   | ---    |
+| 需审核   | 用户已拒绝 | ---       | ---    | ---    |
+| 需审核   | 已退出     | ---       | ---    | ---    |
+| 仅邀请   | 同上       | ---       | ---    | ---    |
+
+> 仅邀请与需审核相同,但是无已经报名状态
+> 横线上为管理员设置的状态,一下为用户设置的状态
+
+# 课程状态显示-学生
+
+| 报名方式 | 学生状态 | 开始前   | 课程中     | 结束后   |
+| -------- | -------- | -------- | ---------- | -------- |
+| 自学     | 未报名   | 尚未开始 | 可报名     | 已经结束 |
+| 自学     | 已报名   | 尚未开始 | 已报名     | 已经结束 |
+| 需审核   | 未报名   | 可以报名 | 录取已结束 | 已经结束 |
+| 需审核   | 已经报名 | 审核中   | 未通过     | 已经结束 |
+| 需审核   | 已被邀请 | 已被邀请 | 邀请过期   | 已经结束 |
+| 需审核   | 已被录取 | 已被录取 | 进行中     | 已经结束 |
+| 需审核   | 已被拒绝 | 未被录取 | 未被录取   | 已经结束 |
+| 仅邀请   | 同上     |
+
+> 仅邀请与需审核相同,但是无已经报名状态
+
+# 测试帐号权限
+
+课程报名及向相关权限