Parcourir la source

Merge pull request #2127 from visuddhinanda/agile

按照完成度排序
visuddhinanda il y a 1 an
Parent
commit
637eb2754b

+ 3 - 2
dashboard/src/components/api/Tag.ts

@@ -53,8 +53,9 @@ export interface ITagMapData {
   table_name: string;
   anchor_id: string;
   tag_id: string;
-  name?: string;
-  color?: number;
+  name?: string | null;
+  color?: number | null;
+  description?: string | null;
   title?: string;
   editor?: IUser;
   owner?: IStudio;

+ 4 - 2
dashboard/src/components/channel/ChannelTable.tsx

@@ -154,9 +154,11 @@ const ChannelTableWidget = ({
         id: "buttons.no",
       }),
       onOk() {
-        console.log("delete", id);
-        return delete_<IDeleteResponse>(`/v2/channel/${id}`)
+        const url = `/v2/channel/${id}`;
+        console.log("delete api request", url);
+        return delete_<IDeleteResponse>(url)
           .then((json) => {
+            console.info("api response", json);
             if (json.ok) {
               message.success("删除成功");
               ref.current?.reload();

+ 10 - 0
dashboard/src/components/tag/TagList.tsx

@@ -163,6 +163,16 @@ const TagsList = ({ studioName, readonly = false, onSelect }: IWidget) => {
           },
         },
       }}
+      onItem={(record: ITagData, index: number) => {
+        return {
+          onClick: (event) => {
+            // 点击行
+            if (typeof onSelect !== "undefined") {
+              onSelect(record);
+            }
+          },
+        };
+      }}
     />
   );
 };

+ 1 - 1
dashboard/src/components/tag/TagSelect.tsx

@@ -27,7 +27,7 @@ const TagSelectWidget = ({ studioName, trigger, onSelect }: IWidget) => {
     <>
       <span onClick={showModal}>{trigger}</span>
       <Modal
-        width={"70%"}
+        width={500}
         title="标签列表"
         open={isModalOpen}
         onOk={handleOk}

+ 24 - 55
dashboard/src/components/tag/TagSelectButton.tsx

@@ -1,18 +1,16 @@
-import { Button, message } from "antd";
+import { Button } from "antd";
 import { TagOutlined } from "@ant-design/icons";
 
-import TagSelect from "./TagSelect";
-import { ITagData, ITagMapRequest, ITagMapResponseList } from "../api/Tag";
 import { useAppSelector } from "../../hooks";
 import { courseInfo } from "../../reducers/current-course";
 import { currentUser } from "../../reducers/current-user";
-import { post } from "../../request";
-import { useIntl } from "react-intl";
+import TagsManager from "./TagsManager";
 
 interface IWidget {
   resId?: string;
   resType?: string;
   disabled?: boolean;
+  trigger?: React.ReactNode;
   onSelect?: Function;
   onCreate?: Function;
   onOpen?: Function;
@@ -22,70 +20,41 @@ const TagSelectButtonWidget = ({
   resId,
   resType,
   disabled = false,
+  trigger,
   onSelect,
   onCreate,
   onOpen,
 }: IWidget) => {
-  const intl = useIntl();
   const course = useAppSelector(courseInfo);
   const user = useAppSelector(currentUser);
 
   const studioName =
     course?.course?.studio?.realName ?? user?.nickName ?? undefined;
 
+  console.debug("TagSelectButton studioName", studioName);
+
   return (
-    <TagSelect
+    <TagsManager
       studioName={studioName}
+      resId={resId}
+      resType={resType}
       trigger={
-        <Button
-          disabled={disabled}
-          type="text"
-          icon={
-            <TagOutlined
-              onClick={() => {
-                if (typeof onOpen !== "undefined") {
-                  onOpen();
-                }
-              }}
-            />
-          }
-        />
-      }
-      onSelect={(tag: ITagData) => {
-        if (typeof onSelect !== "undefined") {
-          onSelect(tag);
-        } else {
-          if (studioName || course) {
-            const data: ITagMapRequest = {
-              table_name: resType,
-              anchor_id: resId,
-              tag_id: tag.id,
-              course: course ? course.courseId : undefined,
-              studio: studioName,
-            };
-
-            const url = `/v2/tag-map`;
-            console.info("tag-map  api request", url, data);
-            post<ITagMapRequest, ITagMapResponseList>(url, data)
-              .then((json) => {
-                console.info("tag-map api response", json);
-                if (json.ok) {
-                  message.success(
-                    intl.formatMessage({ id: "flashes.success" })
-                  );
-                  if (typeof onCreate !== "undefined") {
-                    onCreate(json.data.rows);
+        trigger ?? (
+          <Button
+            disabled={disabled}
+            type="text"
+            icon={
+              <TagOutlined
+                onClick={() => {
+                  if (typeof onOpen !== "undefined") {
+                    onOpen();
                   }
-                } else {
-                  message.error(json.message);
-                }
-              })
-              .catch((e) => console.error(e));
-          } else {
-            console.error("no studio");
-          }
-        }
-      }}
+                }}
+              />
+            }
+          />
+        )
+      }
     />
   );
 };

+ 35 - 2
dashboard/src/components/tag/TagsArea.tsx

@@ -1,14 +1,16 @@
-import { Tag } from "antd";
+import { Badge, Popover, Tag } from "antd";
 import { ITagMapData } from "../api/Tag";
 import { useEffect, useState } from "react";
 import { useAppSelector } from "../../hooks";
 import { tagList } from "../../reducers/discussion-count";
 import { numToHex } from "./TagList";
+import TagSelectButton from "./TagSelectButton";
 
 interface IWidget {
   data?: ITagMapData[];
   max?: number;
   resId?: string;
+  resType?: string;
   onTagClose?: Function;
   onTagClick?: Function;
 }
@@ -16,6 +18,7 @@ const TagsAreaWidget = ({
   data = [],
   max = 5,
   resId,
+  resType,
   onTagClose,
   onTagClick,
 }: IWidget) => {
@@ -39,7 +42,37 @@ const TagsAreaWidget = ({
       </Tag>
     ) : undefined;
   });
-  return <div style={{ width: "100%", lineHeight: "2em" }}>{currTags}</div>;
+
+  const extraTags = tags?.map((item, id) => {
+    return id >= max ? (
+      <Tag key={id} color={"#" + numToHex(item.color ?? 13684944)}>
+        {item.name}
+      </Tag>
+    ) : undefined;
+  });
+  let extra = 0;
+  if (tags && typeof max !== "undefined") {
+    extra = tags.length - max;
+  }
+  if (extra < 0) {
+    extra = 0;
+  }
+
+  return (
+    <div style={{ width: "100%", lineHeight: "2em" }}>
+      <TagSelectButton
+        resId={resId}
+        resType={resType}
+        trigger={<span style={{ cursor: "pointer" }}>{currTags}</span>}
+      />
+      <Popover content={<div>{extraTags}</div>}>
+        <Badge
+          count={extra}
+          style={{ backgroundColor: "#52c41a", cursor: "pointer" }}
+        />
+      </Popover>
+    </div>
+  );
 };
 
 export default TagsAreaWidget;

+ 52 - 0
dashboard/src/components/tag/TagsManager.tsx

@@ -0,0 +1,52 @@
+import { useState } from "react";
+import { Modal } from "antd";
+
+import TagsOnItem from "./TagsOnItem";
+
+interface IWidget {
+  studioName?: string;
+  resId?: string;
+  resType?: string;
+  trigger?: React.ReactNode;
+  onSelect?: Function;
+}
+const TagsManagerWidget = ({
+  studioName,
+  resId,
+  resType,
+  trigger,
+  onSelect,
+}: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  const showModal = () => {
+    setIsModalOpen(true);
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
+  return (
+    <>
+      <span onClick={showModal}>{trigger}</span>
+      <Modal
+        width={500}
+        title={`${studioName}标签列表`}
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+        destroyOnClose
+        footer={false}
+      >
+        <TagsOnItem studioName={studioName} resId={resId} resType={resType} />
+      </Modal>
+    </>
+  );
+};
+
+export default TagsManagerWidget;

+ 202 - 0
dashboard/src/components/tag/TagsOnItem.tsx

@@ -0,0 +1,202 @@
+import { useIntl } from "react-intl";
+import { ActionType, ProList } from "@ant-design/pro-components";
+import {
+  Button,
+  Popconfirm,
+  PopconfirmProps,
+  Popover,
+  Tag,
+  message,
+} from "antd";
+import { PlusOutlined } from "@ant-design/icons";
+
+import {
+  ITagData,
+  ITagMapData,
+  ITagMapRequest,
+  ITagMapResponse,
+  ITagMapResponseList,
+} from "../api/Tag";
+import { getSorterUrl } from "../../utils";
+import { delete_, get, post } from "../../request";
+import { useRef, useState } from "react";
+
+import TagsList, { numToHex } from "./TagList";
+import { IDeleteResponse } from "../api/Article";
+import store from "../../store";
+import { tagsUpgrade } from "../../reducers/discussion-count";
+
+interface IWidget {
+  studioName?: string;
+  courseId?: string;
+  resId?: string;
+  resType?: string;
+  onSelect?: Function;
+}
+
+const TagsOnItem = ({
+  studioName,
+  courseId,
+  resId,
+  resType,
+  onSelect,
+}: IWidget) => {
+  const intl = useIntl(); //i18n
+  const ref = useRef<ActionType>();
+  const [openCreate, setOpenCreate] = useState(false);
+
+  const cancel: PopconfirmProps["onCancel"] = (e) => {
+    console.debug(e);
+  };
+
+  return (
+    <ProList<ITagMapData>
+      actionRef={ref}
+      toolBarRender={() => {
+        return [
+          <Popover
+            overlayStyle={{ width: 300 }}
+            content={
+              <TagsList
+                studioName={studioName}
+                readonly
+                onSelect={async (record: ITagData) => {
+                  //新建记录
+                  const url = "/v2/tag-map";
+                  const data: ITagMapRequest = {
+                    table_name: resType,
+                    anchor_id: resId,
+                    tag_id: record.id,
+                    studio: studioName,
+                    course: courseId,
+                  };
+                  const json = await post<ITagMapRequest, ITagMapResponse>(
+                    url,
+                    data
+                  );
+                  if (json.ok) {
+                    //新建课程成功后刷新
+                    ref.current?.reload();
+                  } else {
+                    console.error(json.message);
+                  }
+                  setOpenCreate(false);
+                }}
+              />
+            }
+            style={{ width: 300 }}
+            title="select"
+            placement="bottom"
+            trigger="click"
+            open={openCreate}
+            onOpenChange={(newOpen: boolean) => {
+              setOpenCreate(newOpen);
+            }}
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.add" })}
+            </Button>
+          </Popover>,
+        ];
+      }}
+      search={{
+        filterType: "light",
+      }}
+      rowKey="name"
+      request={async (params = {}, sorter, filter) => {
+        console.log(params, sorter, filter);
+        let url = `/v2/tag-map?view=item&studio=${studioName}&res_id=${resId}`;
+        const offset =
+          ((params.current ? params.current : 1) - 1) *
+          (params.pageSize ? params.pageSize : 10);
+        url += `&limit=${params.pageSize}&offset=${offset}`;
+        url += params.keyword ? "&search=" + params.keyword : "";
+
+        url += getSorterUrl(sorter);
+
+        console.info("api request", url);
+        const res = await get<ITagMapResponseList>(url);
+        console.info("api response", res);
+        if (res.ok) {
+          if (resId) {
+            store.dispatch(
+              tagsUpgrade({
+                resId: resId,
+                tags: res.data.rows,
+              })
+            );
+          }
+        }
+        return {
+          total: res.data.count,
+          succcess: true,
+          data: res.data.rows,
+        };
+      }}
+      pagination={{
+        pageSize: 10,
+      }}
+      options={{
+        search: true,
+      }}
+      metas={{
+        title: {
+          dataIndex: "name",
+          title: "用户",
+          search: false,
+          render(dom, entity, index, action, schema) {
+            return (
+              <Tag
+                color={"#" + numToHex(entity.color ?? 13684944)}
+                onClick={() => {
+                  if (typeof onSelect !== "undefined") {
+                    onSelect(entity);
+                  }
+                }}
+              >
+                {entity.name}
+              </Tag>
+            );
+          },
+        },
+        subTitle: {
+          dataIndex: "description",
+          search: false,
+        },
+        actions: {
+          render: (dom, entity, index, action, schema) => [
+            <Popconfirm
+              title="Delete the tag?"
+              onConfirm={async () => {
+                const url = `/v2/tag-map/${entity.id}?course=${courseId}`;
+                console.log("delete api request", url);
+                try {
+                  const json = await delete_<IDeleteResponse>(url);
+                  console.info("api response", json);
+                  if (json.ok) {
+                    message.success("删除成功");
+                    ref.current?.reload();
+                  } else {
+                    message.error(json.message);
+                  }
+                } catch (e) {
+                  return console.log("Oops errors!", e);
+                }
+              }}
+              onCancel={cancel}
+              okText="Yes"
+              cancelText="No"
+            >
+              <Button type="text" danger>
+                Delete
+              </Button>
+            </Popconfirm>,
+          ],
+          search: false,
+        },
+      }}
+    />
+  );
+};
+
+export default TagsOnItem;

+ 5 - 3
dashboard/src/components/template/SentEdit.tsx

@@ -93,8 +93,10 @@ export interface IWidgetSentEditInner {
   simNum?: number;
   compact?: boolean;
   mode?: ArticleMode;
-  wbwProgress?: boolean;
+  showWbwProgress?: boolean;
   readonly?: boolean;
+  wbwProgress?: number;
+  wbwScore?: number;
 }
 export const SentEditInner = ({
   id,
@@ -115,7 +117,7 @@ export const SentEditInner = ({
   simNum,
   compact = false,
   mode,
-  wbwProgress = false,
+  showWbwProgress = false,
   readonly = false,
 }: IWidgetSentEditInner) => {
   const [wbwData, setWbwData] = useState<IWbw[]>();
@@ -192,7 +194,7 @@ export const SentEditInner = ({
       magicDict={magicDict}
       compact={isCompact}
       mode={articleMode}
-      wbwProgress={wbwProgress}
+      wbwProgress={showWbwProgress}
       readonly={readonly}
       onWbwChange={(data: IWbw[]) => {
         setWbwData(data);

+ 75 - 12
dashboard/src/components/template/SentEdit/SentWbw.tsx

@@ -1,4 +1,4 @@
-import { Button, List, Space, message } from "antd";
+import { Button, List, Select, Space, message } from "antd";
 import { useEffect, useState } from "react";
 import { ReloadOutlined } from "@ant-design/icons";
 
@@ -9,6 +9,9 @@ import { useAppSelector } from "../../../hooks";
 import { courseInfo, memberInfo } from "../../../reducers/current-course";
 import { courseUser } from "../../../reducers/course-user";
 import User, { IUser } from "../../auth/User";
+import { IWbw } from "../Wbw/WbwWord";
+import { getWbwProgress } from "../WbwSent";
+import moment from "moment";
 
 interface IWidget {
   book: number;
@@ -33,6 +36,7 @@ const SentWbwWidget = ({
   const [sentData, setSentData] = useState<IWidgetSentEditInner[]>([]);
   const [answer, setAnswer] = useState<ISentence>();
   const [loading, setLoading] = useState<boolean>(false);
+  const [order, setOrder] = useState("progress");
   const course = useAppSelector(courseInfo);
   const courseMember = useAppSelector(memberInfo);
 
@@ -76,18 +80,33 @@ const SentWbwWidget = ({
         console.info("wbw sentence api response", json);
         if (json.ok) {
           console.debug("wbw sentence course", course);
+          let response: IWidgetSentEditInner[] = json.data.rows;
           if (course && myCourse && myCourse.role !== "student") {
-            setSentData(
-              json.data.rows.filter((value) =>
-                value.translation
-                  ? value.translation[0].channel.id !== course.channelId
-                  : true
-              )
+            response = json.data.rows.filter((value) =>
+              value.translation
+                ? value.translation[0].channel.id !== course.channelId
+                : true
             );
-          } else {
-            setSentData(json.data.rows);
           }
-
+          response.forEach(
+            (
+              value: IWidgetSentEditInner,
+              index: number,
+              array: IWidgetSentEditInner[]
+            ) => {
+              if (value.origin) {
+                if (value.origin.length > 0) {
+                  if (value.origin[0].content) {
+                    const json: IWbw[] = JSON.parse(value.origin[0].content);
+                    const progress = getWbwProgress(json);
+                    array[index].wbwProgress = progress;
+                  }
+                }
+              }
+            }
+          );
+          console.debug("response with progress", response);
+          setSentData(response);
           if (myCourse && course) {
             const answerData = json.data.rows.find((value) =>
               value.origin
@@ -139,6 +158,41 @@ const SentWbwWidget = ({
       });
   }
   console.debug("没交作业", courseMember, sentData, nonWbwUser);
+
+  let aaa = [...sentData].sort(
+    (a: IWidgetSentEditInner, b: IWidgetSentEditInner) => {
+      switch (order) {
+        case "progress":
+          if (a.wbwProgress && b.wbwProgress) {
+            return b.wbwProgress - a.wbwProgress;
+          } else {
+            return 0;
+          }
+          break;
+        case "updated":
+          if (a.origin && b.origin) {
+            if (
+              moment(b.origin[0].updateAt).isBefore(
+                moment(a.origin[0].updateAt)
+              )
+            ) {
+              return 1;
+            } else {
+              return -1;
+            }
+          } else {
+            return 0;
+          }
+          break;
+      }
+      if (a.wbwProgress && b.wbwProgress) {
+        return b.wbwProgress - a.wbwProgress;
+      } else {
+        return 0;
+      }
+    }
+  );
+
   return (
     <>
       <List
@@ -147,6 +201,15 @@ const SentWbwWidget = ({
           <div style={{ display: "flex", justifyContent: "space-between" }}>
             <span></span>
             <Space>
+              <Select
+                disabled
+                defaultValue={"progress"}
+                options={[
+                  { value: "progress", label: "完成度" },
+                  { value: "updated", label: "更新时间" },
+                ]}
+                onChange={(value: string) => setOrder(value)}
+              />
               <Button
                 type="link"
                 shape="round"
@@ -158,14 +221,14 @@ const SentWbwWidget = ({
         }
         itemLayout="horizontal"
         split={false}
-        dataSource={sentData}
+        dataSource={aaa}
         renderItem={(item, index) => (
           <List.Item key={index}>
             <SentEditInner
               {...item}
               readonly={isCourse}
               answer={answer}
-              wbwProgress={isCourse ?? wbwProgress}
+              showWbwProgress={isCourse ?? wbwProgress}
             />
           </List.Item>
         )}

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

@@ -356,7 +356,7 @@ const WbwPaliWidget = ({
     return (
       <div className="pali_shell" ref={divShell}>
         <div style={{ position: "absolute", marginTop: -24 }}>
-          <TagsArea resId={data.uid} data={tags} max={1} />
+          <TagsArea resId={data.uid} resType="wbw" data={tags} max={1} />
         </div>
         <span className="pali_shell_spell">
           {data.grammarId ? (

+ 38 - 36
dashboard/src/components/template/WbwSent.tsx

@@ -29,12 +29,46 @@ import { IChannel } from "../channel/Channel";
 import TimeShow from "../general/TimeShow";
 import moment from "moment";
 
+export const getWbwProgress = (data: IWbw[]) => {
+  //计算完成度
+  //祛除标点符号
+  const allWord = data.filter(
+    (value) =>
+      value.real.value &&
+      value.real.value?.length > 0 &&
+      value.type?.value !== ".ctl."
+  );
+
+  const final = allWord.filter(
+    (value) =>
+      value.meaning?.value &&
+      value.factors?.value &&
+      value.factorMeaning?.value &&
+      value.case?.value &&
+      value.parent?.value
+  );
+  console.debug("wbw progress", allWord, final);
+  let finalLen: number = 0;
+  final.forEach((value) => {
+    if (value.real.value) {
+      finalLen += value.real.value?.length;
+    }
+  });
+  let allLen: number = 0;
+  allWord.forEach((value) => {
+    if (value.real.value) {
+      allLen += value.real.value?.length;
+    }
+  });
+  const progress = Math.round((finalLen * 100) / allLen);
+  return progress;
+};
+
 export const paraMark = (wbwData: IWbw[]): IWbw[] => {
   //处理段落标记,支持点击段落引用弹窗
   let start = false;
   let bookCode = "";
   let count = 0;
-  let currPara = 0;
   let bookCodeStack: string[] = [];
   wbwData.forEach((value: IWbw, index: number, array: IWbw[]) => {
     if (value.word.value === "(") {
@@ -187,7 +221,7 @@ export const WbwSentCtl = ({
   const [fieldDisplay, setFieldDisplay] = useState(fields);
   const [displayMode, setDisplayMode] = useState<ArticleMode>();
   const [loading, setLoading] = useState(false);
-  const [progress, setProgress] = useState(0);
+
   const [showProgress, setShowProgress] = useState(false);
   const user = useAppSelector(currentUser);
 
@@ -200,40 +234,8 @@ export const WbwSentCtl = ({
     (value) => value.tag === ":collocation:"
   );
 
-  useEffect(() => {
-    //计算完成度
-    //祛除标点符号
-    const allWord = wordData.filter(
-      (value) =>
-        value.real.value &&
-        value.real.value?.length > 0 &&
-        value.type?.value !== ".ctl."
-    );
-
-    const final = allWord.filter(
-      (value) =>
-        value.meaning?.value &&
-        value.factors?.value &&
-        value.factorMeaning?.value &&
-        value.case?.value &&
-        value.parent?.value
-    );
-    console.debug("wbw progress", allWord, final);
-    let finalLen: number = 0;
-    final.forEach((value) => {
-      if (value.real.value) {
-        finalLen += value.real.value?.length;
-      }
-    });
-    let allLen: number = 0;
-    allWord.forEach((value) => {
-      if (value.real.value) {
-        allLen += value.real.value?.length;
-      }
-    });
-    const progress = Math.round((finalLen * 100) / allLen);
-    setProgress(progress);
-  }, [wordData]);
+  //计算完成度
+  const progress = getWbwProgress(wordData);
 
   const newMode = useAppSelector(_mode);
 

+ 27 - 3
dashboard/src/reducers/discussion-count.ts

@@ -9,7 +9,7 @@ import { ITagMapData } from "../components/api/Tag";
 
 export interface IUpgrade {
   resId: string;
-  data: IDiscussionCountData[];
+  data?: IDiscussionCountData[];
   tags?: ITagMapData[];
 }
 
@@ -37,12 +37,36 @@ export const slice = createSlice({
       const old = state.list.filter(
         (value) => value.res_id !== action.payload.resId
       );
-      state.list = [...old, ...action.payload.data];
+
+      if (old) {
+        if (action.payload.data) {
+          state.list = [...old, ...action.payload.data];
+        }
+      } else {
+        if (action.payload.data) {
+          state.list = [...action.payload.data];
+        }
+      }
+    },
+    tagsUpgrade: (state, action: PayloadAction<IUpgrade>) => {
+      console.debug("discussion-count publish", action.payload);
+      const old = state.tags?.filter(
+        (value) => value.anchor_id !== action.payload.resId
+      );
+      if (old) {
+        if (action.payload.tags) {
+          state.tags = [...old, ...action.payload.tags];
+        }
+      } else {
+        if (action.payload.tags) {
+          state.tags = [...action.payload.tags];
+        }
+      }
     },
   },
 });
 
-export const { discussions, tags, upgrade } = slice.actions;
+export const { discussions, tags, upgrade, tagsUpgrade } = slice.actions;
 
 export const discussionList = (
   state: RootState