Преглед на файлове

Merge pull request #1469 from visuddhinanda/agile

评论和修改混排
visuddhinanda преди 2 години
родител
ревизия
b3634dfde5

+ 2 - 2
dashboard/src/components/corpus/SentHistory.tsx

@@ -6,7 +6,7 @@ import User from "../auth/User";
 import { IUser } from "../auth/UserName";
 import TimeShow from "../general/TimeShow";
 
-interface ISentHistoryData {
+export interface ISentHistoryData {
   id: string;
   sent_uid: string;
   content: string;
@@ -15,7 +15,7 @@ interface ISentHistoryData {
   created_at: string;
 }
 
-interface ISentHistoryListResponse {
+export interface ISentHistoryListResponse {
   ok: boolean;
   message: string;
   data: { rows: ISentHistoryData[]; count: number };

+ 46 - 0
dashboard/src/components/corpus/SentHistoryItem.tsx

@@ -0,0 +1,46 @@
+import { Space, Tooltip, Typography } from "antd";
+import { Change, diffChars } from "diff";
+
+import User from "../auth/User";
+import TimeShow from "../general/TimeShow";
+import { ISentHistoryData } from "./SentHistory";
+
+const { Text, Paragraph } = Typography;
+
+interface IWidget {
+  data?: ISentHistoryData;
+  oldContent?: string;
+}
+const SentHistoryItemWidget = ({ data, oldContent }: IWidget) => {
+  let content = <Text>{data?.content}</Text>;
+  if (data?.content && oldContent) {
+    const diff: Change[] = diffChars(oldContent, data.content);
+    const diffResult = diff.map((item, id) => {
+      return (
+        <Text
+          key={id}
+          type={item.added ? "success" : item.removed ? "danger" : "secondary"}
+          delete={item.removed ? true : undefined}
+        >
+          {item.value}
+        </Text>
+      );
+    });
+    content = <Tooltip title={data.content}>{diffResult}</Tooltip>;
+  }
+  return (
+    <Paragraph style={{ paddingLeft: 12 }}>
+      <blockquote>
+        {content}
+        <div>
+          <Space style={{ fontSize: "80%" }}>
+            <User {...data?.editor} showAvatar={false} />
+            <TimeShow type="secondary" createdAt={data?.created_at} />
+          </Space>
+        </div>
+      </blockquote>
+    </Paragraph>
+  );
+};
+
+export default SentHistoryItemWidget;

+ 2 - 0
dashboard/src/components/discussion/DiscussionBox.tsx

@@ -50,6 +50,8 @@ const DiscussionBoxWidget = () => {
             onClick={() => setChildrenDrawer(false)}
           />
           <DiscussionTopic
+            resId={discussionMessage?.resId}
+            resType={discussionMessage?.resType}
             topicId={topicComment?.id}
             onItemCountChange={(count: number, parent: string) => {
               setAnswerCount({ id: parent, count: count });

+ 5 - 1
dashboard/src/components/discussion/DiscussionItem.tsx

@@ -1,5 +1,5 @@
 import { Avatar } from "antd";
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import { IUser } from "../auth/User";
 import DiscussionShow from "./DiscussionShow";
 import DiscussionEdit from "./DiscussionEdit";
@@ -39,6 +39,10 @@ const DiscussionItemWidget = ({
 }: IWidget) => {
   const [edit, setEdit] = useState(false);
   const [currData, setCurrData] = useState<IComment>(data);
+  useEffect(() => {
+    console.log("data", data);
+    setCurrData(data);
+  }, [data]);
   return (
     <div
       id={`answer-${data.id}`}

+ 40 - 54
dashboard/src/components/discussion/DiscussionListCard.tsx

@@ -1,21 +1,17 @@
 import { useEffect, useRef, useState } from "react";
+import { Space, Typography } from "antd";
+import { CommentOutlined } from "@ant-design/icons";
 
-import { Collapse, Typography } from "antd";
-
-import { get, put } from "../../request";
-import {
-  ICommentListResponse,
-  ICommentRequest,
-  ICommentResponse,
-} from "../api/Comment";
-
-import DiscussionItem, { IComment } from "./DiscussionItem";
-
+import { get } from "../../request";
+import { ICommentListResponse } from "../api/Comment";
+import { IComment } from "./DiscussionItem";
 import { IAnswerCount } from "./DiscussionDrawer";
 import { ActionType, ProList } from "@ant-design/pro-components";
 import { renderBadge } from "../channel/ChannelTable";
 import DiscussionCreate from "./DiscussionCreate";
-const { Panel } = Collapse;
+import User from "../auth/User";
+
+const { Link } = Typography;
 
 export type TResType = "article" | "channel" | "chapter" | "sentence" | "wbw";
 
@@ -59,65 +55,55 @@ const DiscussionListCardWidget = ({
       </Typography.Paragraph>
     );
   }
-
   return (
     <>
       <ProList<IComment>
-        itemLayout="vertical"
         rowKey="id"
         actionRef={ref}
         metas={{
           avatar: {
             render(dom, entity, index, action, schema) {
-              return <></>;
+              return (
+                <>
+                  <User {...entity.user} showName={false} />
+                </>
+              );
             },
           },
           title: {
             render(dom, entity, index, action, schema) {
-              return <></>;
-            },
-          },
-          content: {
-            render: (text, row, index, action) => {
               return (
-                <DiscussionItem
-                  data={row}
-                  onSelect={(
-                    e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
-                    data: IComment
-                  ) => {
-                    if (typeof onSelect !== "undefined") {
-                      onSelect(e, data);
-                    }
-                  }}
-                  onDelete={() => {
-                    ref.current?.reload();
-                  }}
-                  onReply={() => {
-                    if (typeof onReply !== "undefined") {
-                      onReply(row);
-                    }
-                  }}
-                  onClose={(value: boolean) => {
-                    console.log("comment", row);
-                    put<ICommentRequest, ICommentResponse>(
-                      `/v2/discussion/${row.id}`,
-                      {
-                        title: row.title,
-                        content: row.content,
-                        status: value ? "close" : "active",
-                      }
-                    ).then((json) => {
-                      console.log(json);
-                      if (json.ok) {
-                        ref.current?.reload();
+                <>
+                  <Link
+                    strong
+                    onClick={(event) => {
+                      if (typeof onSelect !== "undefined") {
+                        onSelect(event, entity);
                       }
-                    });
-                  }}
-                />
+                    }}
+                  >
+                    {entity.title}
+                  </Link>
+                </>
               );
             },
           },
+          description: {
+            dataIndex: "content",
+            search: false,
+          },
+          actions: {
+            render: (text, row, index, action) => [
+              row.childrenCount ? (
+                <Space key={index}>
+                  <CommentOutlined />
+                  {row.childrenCount}
+                </Space>
+              ) : (
+                <></>
+              ),
+            ],
+          },
         }}
         request={async (params = {}, sorter, filter) => {
           let url: string = "/v2/discussion?";

+ 49 - 56
dashboard/src/components/discussion/DiscussionShow.tsx

@@ -6,6 +6,7 @@ import {
   message,
   Modal,
   Space,
+  Tag,
   Typography,
 } from "antd";
 import {
@@ -13,7 +14,7 @@ import {
   EditOutlined,
   DeleteOutlined,
   LinkOutlined,
-  CommentOutlined,
+  CheckOutlined,
   MessageOutlined,
   ExclamationCircleOutlined,
   CloseOutlined,
@@ -23,9 +24,11 @@ import type { MenuProps } from "antd";
 import { IComment } from "./DiscussionItem";
 import TimeShow from "../general/TimeShow";
 import Marked from "../general/Marked";
-import { delete_ } from "../../request";
+import { delete_, put } from "../../request";
 import { IDeleteResponse } from "../api/Article";
 import { fullUrl } from "../../utils";
+import { ICommentRequest, ICommentResponse } from "../api/Comment";
+import { useState } from "react";
 
 const { Text } = Typography;
 
@@ -46,6 +49,7 @@ const DiscussionShowWidget = ({
   onClose,
 }: IWidget) => {
   const intl = useIntl();
+  const [closed, setClosed] = useState(data.status);
   const showDeleteConfirm = (id: string, title: string) => {
     Modal.confirm({
       icon: <ExclamationCircleOutlined />,
@@ -82,6 +86,23 @@ const DiscussionShowWidget = ({
       },
     });
   };
+
+  const close = (value: boolean) => {
+    put<ICommentRequest, ICommentResponse>(`/v2/discussion/${data.id}`, {
+      title: data.title,
+      content: data.content,
+      status: value ? "close" : "active",
+    }).then((json) => {
+      console.log(json);
+      if (json.ok) {
+        setClosed(json.data.status);
+        if (typeof onClose !== "undefined") {
+          onClose(value);
+        }
+      }
+    });
+  };
+
   const onClick: MenuProps["onClick"] = (e) => {
     console.log("click ", e);
     switch (e.key) {
@@ -96,26 +117,19 @@ const DiscussionShowWidget = ({
           message.success("链接地址已经拷贝到剪贴板");
         });
         break;
-      case "reply":
-        if (typeof onReply !== "undefined") {
-          onReply();
-        }
-        break;
       case "edit":
         if (typeof onEdit !== "undefined") {
           onEdit();
         }
         break;
       case "close":
-        if (typeof onClose !== "undefined") {
-          onClose(true);
-        }
+        close(true);
+
         break;
 
       case "reopen":
-        if (typeof onClose !== "undefined") {
-          onClose(false);
-        }
+        close(false);
+
         break;
       case "delete":
         if (data.id) {
@@ -126,7 +140,7 @@ const DiscussionShowWidget = ({
         break;
     }
   };
-
+  console.log("children", data.childrenCount);
   const items: MenuProps["items"] = [
     {
       key: "copy-link",
@@ -135,14 +149,6 @@ const DiscussionShowWidget = ({
       }),
       icon: <LinkOutlined />,
     },
-    {
-      key: "reply",
-      label: intl.formatMessage({
-        id: "buttons.reply",
-      }),
-      icon: <CommentOutlined />,
-      disabled: data.parent ? true : false,
-    },
     {
       type: "divider",
     },
@@ -159,15 +165,15 @@ const DiscussionShowWidget = ({
         id: "buttons.close",
       }),
       icon: <CloseOutlined />,
-      disabled: data.status === "close",
+      disabled: closed === "close",
     },
     {
       key: "reopen",
       label: intl.formatMessage({
         id: "buttons.open",
       }),
-      icon: <CloseOutlined />,
-      disabled: data.status === "active",
+      icon: <CheckOutlined />,
+      disabled: closed === "active",
     },
     {
       key: "delete",
@@ -176,33 +182,17 @@ const DiscussionShowWidget = ({
       }),
       icon: <DeleteOutlined />,
       danger: true,
-      disabled: data.childrenCount ? true : false,
-    },
-    {
-      type: "divider",
-    },
-    {
-      key: "report-content",
-      label: "举报",
+      disabled: data.childrenCount && data.childrenCount > 0 ? true : false,
     },
   ];
   return (
     <Card
       size="small"
       title={
-        <Space direction="vertical">
-          <Text type="secondary" style={{ fontSize: "80%" }}>
-            <Space>
-              {data.user.nickName}
-              <TimeShow
-                type="secondary"
-                updatedAt={data.updatedAt}
-                createdAt={data.createdAt}
-              />
-            </Space>
-          </Text>
+        <Space direction="vertical" size={"small"}>
           {data.title ? (
             <Text
+              style={{ fontSize: 16 }}
               strong
               onClick={(e) => {
                 if (typeof onSelect !== "undefined") {
@@ -213,6 +203,21 @@ const DiscussionShowWidget = ({
               {data.title}
             </Text>
           ) : undefined}
+          <Text type="secondary" style={{ fontSize: "80%" }}>
+            <Space>
+              {closed === "close" ? (
+                <Tag style={{ backgroundColor: "#8250df", color: "white" }}>
+                  {"closed"}
+                </Tag>
+              ) : undefined}
+              {data.user.nickName}
+              <TimeShow
+                type="secondary"
+                updatedAt={data.updatedAt}
+                createdAt={data.createdAt}
+              />
+            </Space>
+          </Text>
         </Space>
       }
       extra={
@@ -234,18 +239,6 @@ const DiscussionShowWidget = ({
               </>
             ) : undefined}
           </span>
-          <Button
-            type="text"
-            onClick={() => {
-              if (typeof onReply !== "undefined") {
-                onReply();
-              }
-            }}
-          >
-            {intl.formatMessage({
-              id: "buttons.reply",
-            })}
-          </Button>
           <Dropdown
             menu={{ items, onClick }}
             placement="bottomRight"

+ 11 - 2
dashboard/src/components/discussion/DiscussionTopic.tsx

@@ -1,37 +1,46 @@
-import { Divider } from "antd";
+import { useState } from "react";
 
 import DiscussionTopicInfo from "./DiscussionTopicInfo";
 import DiscussionTopicChildren from "./DiscussionTopicChildren";
 import { IComment } from "./DiscussionItem";
+import { TResType } from "./DiscussionListCard";
 
 interface IWidget {
+  resId?: string;
+  resType?: TResType;
   topicId?: string;
   focus?: string;
   onItemCountChange?: Function;
   onTopicReady?: Function;
 }
 const DiscussionTopicWidget = ({
+  resId,
+  resType,
   topicId,
   focus,
   onTopicReady,
   onItemCountChange,
 }: IWidget) => {
+  const [count, setCount] = useState<number>();
   return (
     <>
       <DiscussionTopicInfo
         topicId={topicId}
+        childrenCount={count}
         onReady={(value: IComment) => {
           if (typeof onTopicReady !== "undefined") {
             onTopicReady(value);
           }
         }}
       />
-      <Divider />
       <DiscussionTopicChildren
+        resId={resId}
+        resType={resType}
         focus={focus}
         topicId={topicId}
         onItemCountChange={(count: number, e: string) => {
           //把新建回答的消息传出去。
+          setCount(count);
           if (typeof onItemCountChange !== "undefined") {
             onItemCountChange(count, e);
           }

+ 94 - 20
dashboard/src/components/discussion/DiscussionTopicChildren.tsx

@@ -1,18 +1,36 @@
 import { List, message, Skeleton } from "antd";
 import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
+
 import { get } from "../../request";
 import { ICommentListResponse } from "../api/Comment";
+import {
+  ISentHistoryData,
+  ISentHistoryListResponse,
+} from "../corpus/SentHistory";
+import SentHistoryItemWidget from "../corpus/SentHistoryItem";
 import DiscussionCreate from "./DiscussionCreate";
-
 import DiscussionItem, { IComment } from "./DiscussionItem";
+import { TResType } from "./DiscussionListCard";
+
+interface IItem {
+  type: "comment" | "sent";
+  comment?: IComment;
+  sent?: ISentHistoryData;
+  oldSent?: string;
+  date: number;
+}
 
 interface IWidget {
+  resId?: string;
+  resType?: TResType;
   topicId?: string;
   focus?: string;
   onItemCountChange?: Function;
 }
 const DiscussionTopicChildrenWidget = ({
+  resId,
+  resType,
   topicId,
   focus,
   onItemCountChange,
@@ -20,6 +38,9 @@ const DiscussionTopicChildrenWidget = ({
   const intl = useIntl();
   const [data, setData] = useState<IComment[]>([]);
   const [loading, setLoading] = useState(true);
+  const [history, setHistory] = useState<ISentHistoryData[]>([]);
+  const [items, setItems] = useState<IItem[]>();
+
   useEffect(() => {
     if (loading === false) {
       const ele = document.getElementById(`answer-${focus}`);
@@ -27,6 +48,48 @@ const DiscussionTopicChildrenWidget = ({
       console.log("after render");
     }
   });
+
+  useEffect(() => {
+    let first = new Date().getTime();
+    const comment: IItem[] = data.map((item) => {
+      const date = new Date(item.createdAt ? item.createdAt : "").getTime();
+      if (date < first) {
+        first = date;
+      }
+      return {
+        type: "comment",
+        comment: item,
+        date: date,
+      };
+    });
+    const hisFiltered = history.filter(
+      (value) =>
+        new Date(value.created_at ? value.created_at : "").getTime() > first
+    );
+    const his: IItem[] = hisFiltered.map((item, index) => {
+      return {
+        type: "sent",
+        sent: item,
+        date: new Date(item.created_at ? item.created_at : "").getTime(),
+        oldSent: index > 0 ? hisFiltered[index - 1].content : undefined,
+      };
+    });
+    const mixItems = [...comment, ...his];
+    mixItems.sort((a, b) => a.date - b.date);
+    setItems(mixItems);
+  }, [data, history]);
+
+  useEffect(() => {
+    if (resType === "sentence" && resId) {
+      let url = `/v2/sent_history?view=sentence&id=${resId}&order=created_at&dir=asc`;
+      get<ISentHistoryListResponse>(url).then((res) => {
+        if (res.ok) {
+          setHistory(res.data.rows);
+        }
+      });
+    }
+  }, [resId, resType]);
+
   useEffect(() => {
     if (typeof topicId === "undefined") {
       return;
@@ -45,6 +108,8 @@ const DiscussionTopicChildrenWidget = ({
               parent: item.parent,
               title: item.title,
               content: item.content,
+              status: item.status,
+              childrenCount: item.children_count,
               createdAt: item.created_at,
               updatedAt: item.updated_at,
             };
@@ -67,29 +132,38 @@ const DiscussionTopicChildrenWidget = ({
         <Skeleton title={{ width: 200 }} paragraph={{ rows: 1 }} active />
       ) : (
         <List
-          pagination={{
-            onChange: (page) => {
-              console.log(page);
-            },
-            pageSize: 10,
-          }}
+          pagination={false}
           itemLayout="horizontal"
-          dataSource={data}
+          dataSource={items}
           renderItem={(item) => {
             return (
               <List.Item>
-                <DiscussionItem
-                  data={item}
-                  isFocus={item.id === focus ? true : false}
-                  onDelete={() => {
-                    if (typeof onItemCountChange !== "undefined") {
-                      onItemCountChange(data.length - 1, item.parent);
-                    }
-                    setData((origin) => {
-                      return origin.filter((value) => value.id !== item.id);
-                    });
-                  }}
-                />
+                {item.type === "comment" ? (
+                  item.comment ? (
+                    <DiscussionItem
+                      data={item.comment}
+                      isFocus={item.comment.id === focus ? true : false}
+                      onDelete={() => {
+                        if (typeof onItemCountChange !== "undefined") {
+                          onItemCountChange(
+                            data.length - 1,
+                            item.comment?.parent
+                          );
+                        }
+                        setData((origin) => {
+                          return origin.filter(
+                            (value) => value.id !== item.comment?.id
+                          );
+                        });
+                      }}
+                    />
+                  ) : undefined
+                ) : (
+                  <SentHistoryItemWidget
+                    data={item.sent}
+                    oldContent={item.oldSent}
+                  />
+                )}
               </List.Item>
             );
           }}

+ 49 - 24
dashboard/src/components/discussion/DiscussionTopicInfo.tsx

@@ -1,20 +1,37 @@
-import { Typography, Space, message } from "antd";
+import { message } from "antd";
 import { useEffect, useState } from "react";
+
 import { get } from "../../request";
 import { ICommentResponse } from "../api/Comment";
-import Marked from "../general/Marked";
-import TimeShow from "../general/TimeShow";
-
-import { IComment } from "./DiscussionItem";
-
-const { Title, Text } = Typography;
+import DiscussionItem, { IComment } from "./DiscussionItem";
 
 interface IWidget {
   topicId?: string;
+  childrenCount?: number;
+  onDelete?: Function;
+  onReply?: Function;
+  onClose?: Function;
   onReady?: Function;
 }
-const DiscussionTopicInfoWidget = ({ topicId, onReady }: IWidget) => {
+const DiscussionTopicInfoWidget = ({
+  topicId,
+  childrenCount,
+  onReady,
+  onDelete,
+  onReply,
+  onClose,
+}: IWidget) => {
   const [data, setData] = useState<IComment>();
+
+  useEffect(() => {
+    setData((origin) => {
+      if (typeof origin !== "undefined") {
+        origin.childrenCount = childrenCount;
+        return origin;
+      }
+    });
+  }, [childrenCount]);
+
   useEffect(() => {
     if (typeof topicId === "undefined") {
       return;
@@ -34,6 +51,8 @@ const DiscussionTopicInfoWidget = ({ topicId, onReady }: IWidget) => {
             user: item.editor,
             title: item.title,
             content: item.content,
+            status: item.status,
+            childrenCount: item.children_count,
             createdAt: item.created_at,
             updatedAt: item.updated_at,
           };
@@ -53,22 +72,28 @@ const DiscussionTopicInfoWidget = ({ topicId, onReady }: IWidget) => {
 
   return (
     <div>
-      <Title editable level={5} style={{ margin: 0 }}>
-        {data?.title}
-      </Title>
-      <Space direction="vertical">
-        <Text type="secondary">
-          <Space>
-            {data?.user.nickName}
-            <TimeShow
-              type="secondary"
-              updatedAt={data?.updatedAt}
-              createdAt={data?.createdAt}
-            />
-          </Space>
-        </Text>
-        <Marked text={data?.content} />
-      </Space>
+      {data ? (
+        <DiscussionItem
+          data={data}
+          onDelete={() => {
+            if (typeof onDelete !== "undefined") {
+              onDelete(data.id);
+            }
+          }}
+          onReply={() => {
+            if (typeof onReply !== "undefined") {
+              onReply(data);
+            }
+          }}
+          onClose={() => {
+            if (typeof onClose !== "undefined") {
+              onClose(data);
+            }
+          }}
+        />
+      ) : (
+        <></>
+      )}
     </div>
   );
 };