Explorar o código

:construction: create

visuddhinanda %!s(int64=3) %!d(string=hai) anos
pai
achega
0328d33393

+ 83 - 0
dashboard/src/components/article/ExerciseList.tsx

@@ -0,0 +1,83 @@
+import { useEffect, useState } from "react";
+import { Collapse, Space, Tag } from "antd";
+
+import { ICourseExerciseResponse } from "../api/Course";
+import { get } from "../../request";
+import { IUser } from "../auth/User";
+import MdView from "../template/MdView";
+
+const { Panel } = Collapse;
+
+interface DataItem {
+  sn: number;
+  name: string;
+  user: IUser;
+  wbw: number;
+  translation: number;
+  question: number;
+  html: string;
+}
+interface IWidget {
+  courseId?: string;
+  articleId?: string;
+  exerciseId?: string;
+}
+const Widget = ({ courseId, articleId, exerciseId }: IWidget) => {
+  const [data, setData] = useState<DataItem[]>();
+
+  useEffect(() => {
+    const url = `/v2/exercise?course_id=${courseId}&article_id=${articleId}&exercise_id=${exerciseId}`;
+    console.log(url);
+    get<ICourseExerciseResponse>(url)
+      .then((json) => {
+        if (json.ok) {
+          console.log(json.data);
+          const items: DataItem[] = json.data.rows.map((item, id) => {
+            let member: DataItem = {
+              sn: id,
+              name: item.user.nickName,
+              user: item.user,
+              wbw: item.wbw,
+              translation: item.translation,
+              question: item.question,
+              html: item.html,
+            };
+            return member;
+          });
+          setData(items);
+        } else {
+          console.error(json.message);
+        }
+      })
+      .catch((error) => {
+        console.error(error);
+      });
+  }, [courseId, articleId, exerciseId]);
+  return (
+    <>
+      <Collapse>
+        {data?.map((item, id) => {
+          const header = (
+            <Space>
+              <span>{item.name}</span>
+              {item.wbw === 0 ? <></> : <Tag color="blue">wbw-{item.wbw}</Tag>}
+              {item.question === 0 ? (
+                <></>
+              ) : (
+                <Tag color="#5BD8A6">Q-{item.question}</Tag>
+              )}
+            </Space>
+          );
+
+          return (
+            <Panel header={header} key={id}>
+              <MdView html={item.html} />
+            </Panel>
+          );
+        })}
+      </Collapse>
+    </>
+  );
+};
+
+export default Widget;

+ 48 - 0
dashboard/src/components/course/ExerciseAnswer.tsx

@@ -0,0 +1,48 @@
+import { useEffect, useState } from "react";
+import { Collapse, message } from "antd";
+
+import { get } from "../../request";
+import { IArticleResponse } from "../api/Article";
+import MdView from "../template/MdView";
+
+const { Panel } = Collapse;
+
+interface IWidget {
+  courseId?: string;
+  articleId?: string;
+  exerciseId?: string;
+  mode?: string;
+  active?: boolean;
+}
+const Widget = ({
+  courseId,
+  articleId,
+  exerciseId,
+  mode,
+  active = false,
+}: IWidget) => {
+  const [answer, setAnswer] = useState<string>();
+
+  useEffect(() => {
+    const url = `/v2/article/${articleId}?mode=${mode}&course=${courseId}&exercise=${exerciseId}&view=answer`;
+    get<IArticleResponse>(url).then((json) => {
+      console.log("article", json);
+      if (json.ok) {
+        setAnswer(json.data.html);
+      } else {
+        message.error(json.message);
+      }
+    });
+  }, [courseId, articleId, exerciseId, mode]);
+  return (
+    <div>
+      <Collapse defaultActiveKey={active ? ["answer"] : []}>
+        <Panel header="答案" key="answer">
+          <MdView html={answer} />
+        </Panel>
+      </Collapse>
+    </div>
+  );
+};
+
+export default Widget;

+ 114 - 0
dashboard/src/components/course/SelectChannel.tsx

@@ -0,0 +1,114 @@
+import { ModalForm, ProForm, ProFormSelect } from "@ant-design/pro-components";
+import { Button, message } from "antd";
+import { GlobalOutlined } from "@ant-design/icons";
+
+import { useAppSelector } from "../../hooks";
+import { currentUser as _currentUser } from "../../reducers/current-user";
+
+import { get, put } from "../../request";
+import { IApiResponseChannelList } from "../api/Channel";
+import { LockIcon } from "../../assets/icon";
+import { ICourseMemberData, ICourseMemberResponse } from "../api/Course";
+import { useNavigate, useParams } from "react-router-dom";
+interface IWidget {
+  courseId?: string;
+  exerciseId?: string;
+  channel?: string;
+  onSelected?: Function;
+  open?: boolean;
+  onOpenChange?: Function;
+}
+const Widget = ({
+  courseId,
+  exerciseId,
+  channel,
+  onSelected,
+  open,
+  onOpenChange,
+}: IWidget) => {
+  const user = useAppSelector(_currentUser);
+  const { type, id } = useParams(); //url 参数
+  const navigate = useNavigate();
+
+  return (
+    <ModalForm<{
+      channel: string;
+    }>
+      title="选择作业的存放位置"
+      trigger={<Button>做练习</Button>}
+      autoFocusFirstInput
+      modalProps={{
+        destroyOnClose: true,
+        onCancel: () => console.log("run"),
+      }}
+      submitTimeout={2000}
+      onFinish={async (values) => {
+        console.log(values.channel);
+        console.log("id", id);
+        const mCourseId = id?.split("_")[0];
+        if (typeof user !== "undefined" && typeof mCourseId !== "undefined") {
+          const json = await put<ICourseMemberData, ICourseMemberResponse>(
+            `/v2/course-member_set-channel`,
+            {
+              user_id: user.id,
+              course_id: mCourseId,
+              channel_id: values.channel,
+            }
+          );
+          if (json.ok) {
+            if (json.data.channel_id === courseId) {
+              message.success("提交成功");
+              navigate(
+                `/article/exercise/${id}_${exerciseId}_${user.realName}/wbw`
+              );
+            } else {
+              message.error(json.data.channel_id);
+            }
+          } else {
+            message.error(json.message);
+          }
+        } else {
+          console.log("select channel error:", user, courseId);
+        }
+
+        return true;
+      }}
+    >
+      <div>
+        您还没有选择版本。您将用一个版本保存自己的作业。这个版本将会被老师,助理老师看到。
+      </div>
+      <ProForm.Group>
+        <ProFormSelect
+          rules={[
+            {
+              required: true,
+            },
+          ]}
+          request={async () => {
+            const channelData = await get<IApiResponseChannelList>(
+              `/v2/channel?view=studio&name=${user?.realName}`
+            );
+            const channel = channelData.data.rows.map((item) => {
+              const icon =
+                item.status === 30 ? <GlobalOutlined /> : <LockIcon />;
+              return {
+                value: item.uid,
+                label: (
+                  <>
+                    {icon} {item.name}
+                  </>
+                ),
+              };
+            });
+            return channel;
+          }}
+          width="md"
+          name="channel"
+          label="版本风格"
+        />
+      </ProForm.Group>
+    </ModalForm>
+  );
+};
+
+export default Widget;

+ 59 - 0
dashboard/src/reducers/course-user.ts

@@ -0,0 +1,59 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+export const ROLE_ROOT = "root";
+export const ROLE_ASSISTANT = "assistant";
+
+const KEY = "token";
+export const DURATION = 60 * 60 * 24;
+
+export const get = (): string | null => {
+  const token = sessionStorage.getItem(KEY);
+  if (token) {
+    return token;
+  }
+
+  return null;
+};
+
+const remove = () => {
+  sessionStorage.removeItem(KEY);
+  localStorage.removeItem(KEY);
+};
+
+export interface ICourseUser {
+  channelId: string;
+  role: string;
+}
+
+interface IState {
+  payload?: ICourseUser;
+}
+
+const initialState: IState = {};
+
+export const slice = createSlice({
+  name: "course-user",
+  initialState,
+  reducers: {
+    signIn: (state, action: PayloadAction<ICourseUser>) => {
+      state.payload = action.payload;
+    },
+    signOut: (state) => {
+      state.payload = undefined;
+      remove();
+    },
+  },
+});
+
+export const { signIn, signOut } = slice.actions;
+
+export const isRoot = (state: RootState): boolean =>
+  state.courseUser.payload?.role.includes(ROLE_ROOT) || false;
+export const isAssistant = (state: RootState): boolean =>
+  state.courseUser.payload?.role.includes(ROLE_ASSISTANT) || false;
+export const courseUser = (state: RootState): ICourseUser | undefined =>
+  state.courseUser.payload;
+
+export default slice.reducer;

+ 32 - 0
dashboard/src/reducers/current-course.ts

@@ -0,0 +1,32 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+export interface ITextbook {
+  courseId: string;
+  articleId: string;
+}
+interface IState {
+  course?: ITextbook;
+}
+
+const initialState: IState = {};
+
+export const slice = createSlice({
+  name: "current-course",
+  initialState,
+  reducers: {
+    refresh: (state, action: PayloadAction<ITextbook>) => {
+      state.course = action.payload;
+    },
+  },
+});
+
+export const { refresh } = slice.actions;
+
+export const currentCourse = (state: RootState): IState => state.currentCourse;
+
+export const courseInfo = (state: RootState): ITextbook | undefined =>
+  state.currentCourse.course;
+
+export default slice.reducer;