فهرست منبع

Merge pull request #2161 from visuddhinanda/agile

Agile
visuddhinanda 1 سال پیش
والد
کامیت
e038e5d74a

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

@@ -289,3 +289,9 @@ export interface ISentenceWbwListResponse {
   message: string;
   data: { rows: IWidgetSentEditInner[]; count: number };
 }
+
+export interface IEditableSentence {
+  ok: boolean;
+  message: string;
+  data: IWidgetSentEditInner;
+}

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

@@ -3,7 +3,7 @@ import { Typography, Divider, Skeleton, Space } from "antd";
 import MdView from "../template/MdView";
 import TocPath, { ITocPathNode } from "../corpus/TocPath";
 import PaliChapterChannelList from "../corpus/PaliChapterChannelList";
-import { ArticleType } from "./Article";
+import { ArticleMode, ArticleType } from "./Article";
 import VisibleObserver from "../general/VisibleObserver";
 import { IStudio } from "../auth/Studio";
 
@@ -21,6 +21,7 @@ export interface IWidgetArticleData {
   content?: string;
   html?: string[];
   path?: ITocPathNode[];
+  mode?: ArticleMode | null;
   created_at?: string;
   updated_at?: string;
   owner?: IStudio;
@@ -50,6 +51,7 @@ const ArticleViewWidget = ({
   articleId,
   anthology,
   hideTitle,
+  mode = "read",
   onEnd,
   remains,
   onPathChange,

+ 2 - 1
dashboard/src/components/discussion/DiscussionAnchor.tsx

@@ -48,10 +48,11 @@ const DiscussionAnchorWidget = ({
     switch (resType) {
       case "sentence":
         url = `/v2/sentence/${resId}`;
-        console.log("url", url);
+        console.info("api request", url);
         setLoading(true);
         get<ISentenceResponse>(url)
           .then((json) => {
+            console.info("api response", json);
             if (json.ok) {
               const id = `${json.data.book}-${json.data.paragraph}-${json.data.word_start}-${json.data.word_end}`;
               const channel = json.data.channel.id;

+ 32 - 11
dashboard/src/components/discussion/DiscussionButton.tsx

@@ -1,6 +1,6 @@
 import { Space, Tooltip } from "antd";
 import store from "../../store";
-import { IShowDiscussion, show } from "../../reducers/discussion";
+import { IShowDiscussion, count, show } from "../../reducers/discussion";
 import { openPanel } from "../../reducers/right-panel";
 import { CommentFillIcon, CommentOutlinedIcon } from "../../assets/icon";
 import { TResType } from "./DiscussionListCard";
@@ -8,6 +8,19 @@ import { useAppSelector } from "../../hooks";
 import { currentUser } from "../../reducers/current-user";
 import { discussionList } from "../../reducers/discussion-count";
 import { IDiscussionCountData, IDiscussionCountWbw } from "../api/Comment";
+import { useEffect, useState } from "react";
+
+export const openDiscussion = (resId: string, withStudent: boolean) => {
+  const data: IShowDiscussion = {
+    type: "discussion",
+    resId: resId,
+    resType: "sentence",
+    withStudent: withStudent,
+  };
+  console.debug("discussion show", data);
+  store.dispatch(show(data));
+  store.dispatch(openPanel("discussion"));
+};
 
 interface IWidget {
   initCount?: number;
@@ -27,8 +40,22 @@ const DiscussionButton = ({
   onlyMe = false,
   wbw,
 }: IWidget) => {
+  const [CommentCount, setCommentCount] = useState<number | undefined>(
+    initCount
+  );
+
   const user = useAppSelector(currentUser);
   const discussions = useAppSelector(discussionList);
+  const discussionCount = useAppSelector(count);
+
+  useEffect(() => {
+    if (
+      discussionCount?.resType === "sentence" &&
+      discussionCount.resId === resId
+    ) {
+      setCommentCount(discussionCount.count);
+    }
+  }, [resId, discussionCount]);
 
   const all = discussions?.filter((value) => value.res_id === resId);
   const my = all?.filter((value) => value.editor_uid === user?.id);
@@ -45,7 +72,7 @@ const DiscussionButton = ({
 
   console.debug("DiscussionButton", discussions, wbw, withStudent);
 
-  let currCount = initCount;
+  let currCount = CommentCount;
   if (onlyMe) {
     if (my) {
       currCount = my.length;
@@ -79,15 +106,9 @@ const DiscussionButton = ({
           color: currCount && currCount > 0 ? "#1890ff" : "unset",
         }}
         onClick={(event) => {
-          const data: IShowDiscussion = {
-            type: "discussion",
-            resId: resId,
-            resType: resType,
-            withStudent: wbw ? true : false,
-          };
-          console.debug("discussion show", data);
-          store.dispatch(show(data));
-          store.dispatch(openPanel("discussion"));
+          if (resId) {
+            openDiscussion(resId, wbw ? true : false);
+          }
         }}
       >
         {myCount ? <CommentFillIcon /> : <CommentOutlinedIcon />}

+ 2 - 1
dashboard/src/components/discussion/DiscussionListCard.tsx

@@ -221,8 +221,9 @@ const DiscussionListCardWidget = ({
             switch (resType) {
               case "sentence":
                 const url = `/v2/sentence/${resId}`;
-                console.log("url", url);
+                console.info("api request", url);
                 const sentInfo = await get<ISentenceResponse>(url);
+                console.info("api response", sentInfo);
                 studioName = sentInfo.data.studio.realName;
                 break;
             }

+ 7 - 6
dashboard/src/components/template/ParaShell.tsx

@@ -41,12 +41,15 @@ const ParaShellCtl = ({
       setIsFocus(false);
     }
   }, [book, focus, para]);
+
+  const borderColor = isFocus ? "#e35f00bd " : "rgba(128, 128, 128, 0.3)";
+
+  const border = mode === "read" ? "" : "2px solid " + borderColor;
+
   return (
     <div
       style={{
-        border: isFocus
-          ? "2px solid #e35f00bd "
-          : "2px solid rgba(128, 128, 128, 0.3)",
+        border: border,
         borderRadius: 6,
         marginTop: 20,
         marginBottom: 28,
@@ -58,9 +61,7 @@ const ParaShellCtl = ({
           position: "absolute",
           marginTop: -31,
           marginLeft: -6,
-          border: isFocus
-            ? "2px solid #e35f00bd "
-            : "2px solid rgba(128, 128, 128, 0.3)",
+          border: border,
           borderRadius: "6px",
         }}
       >

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

@@ -97,6 +97,7 @@ export interface IWidgetSentEditInner {
   readonly?: boolean;
   wbwProgress?: number;
   wbwScore?: number;
+  onTranslationChange?: (data: ISentence) => void;
 }
 export const SentEditInner = ({
   id,
@@ -119,6 +120,7 @@ export const SentEditInner = ({
   mode,
   showWbwProgress = false,
   readonly = false,
+  onTranslationChange,
 }: IWidgetSentEditInner) => {
   const [wbwData, setWbwData] = useState<IWbw[]>();
   const [magicDict, setMagicDict] = useState<string>();
@@ -203,6 +205,7 @@ export const SentEditInner = ({
         setMagicDictLoading(false);
         setMagicDict(undefined);
       }}
+      onTranslationChange={onTranslationChange}
     />
   );
 

+ 108 - 0
dashboard/src/components/template/SentEdit/InteractiveButton.tsx

@@ -0,0 +1,108 @@
+import { Divider, Space } from "antd";
+import SuggestionButton from "./SuggestionButton";
+import DiscussionButton from "../../discussion/DiscussionButton";
+import { ISentence } from "../SentEdit";
+import { MouseEventHandler, useEffect, useState } from "react";
+
+interface IWidget {
+  data: ISentence;
+  compact?: boolean;
+  float?: boolean;
+  hideCount?: boolean;
+  hideInZero?: boolean;
+  onMouseEnter?: MouseEventHandler | undefined;
+  onMouseLeave?: MouseEventHandler | undefined;
+}
+
+const InteractiveButton = ({
+  data,
+  compact = false,
+  float = false,
+  hideCount = false,
+  hideInZero = false,
+  onMouseEnter,
+  onMouseLeave,
+}: IWidget) => {
+  const [left, setLeft] = useState(0);
+  const [width, setWidth] = useState(0);
+
+  useEffect(() => {
+    // 获取目标元素
+    const targetNode = document.getElementsByClassName("article_shell")[0];
+
+    const rect = targetNode.getBoundingClientRect();
+    setLeft(rect.left);
+    setWidth(rect.width);
+    // 创建ResizeObserver实例并传入回调函数
+    const resizeObserver = new ResizeObserver((entries) => {
+      for (let entry of entries) {
+        const { width, height } = entry.contentRect;
+        //console.log(`Element size: ${width}px x ${height}px`);
+        setWidth((origin) => width);
+      }
+    });
+
+    // 观察目标节点
+    resizeObserver.observe(targetNode);
+
+    // 定义一个函数来处理窗口大小变化
+    function handleResize() {
+      // 获取窗口宽度
+      const windowWidth = window.innerWidth;
+      // 在控制台中输出新的窗口宽度
+      //console.log(`新的窗口宽度是:${windowWidth}px`);
+
+      // 假设你有一个div元素,它的id是"myDiv"
+      const myDiv = document.getElementsByClassName("article_shell")[0];
+
+      // 使用getBoundingClientRect()方法获取元素的位置和大小
+      const rect = myDiv.getBoundingClientRect();
+
+      // 从返回的对象中获取left属性
+      const leftPosition = rect.left;
+
+      // 输出left位置
+      //console.log(`div的left位置是:${leftPosition}px`);
+      setLeft((origin) => leftPosition);
+
+      // 在这里,你可以根据窗口宽度来调整页面布局或样式
+    }
+
+    // 为resize事件添加监听器
+    window.addEventListener("resize", handleResize);
+  }, []);
+
+  const ButtonInner = (
+    <Space
+      size={"small"}
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
+    >
+      <SuggestionButton
+        data={data}
+        hideCount={hideCount}
+        hideInZero={hideInZero}
+      />
+      {compact ? undefined : <Divider type="vertical" />}
+      <DiscussionButton
+        hideCount={hideCount}
+        hideInZero={hideInZero}
+        initCount={data.suggestionCount?.discussion}
+        resId={data.id}
+      />
+    </Space>
+  );
+
+  return float ? (
+    <span
+      className="sent_read_interactive_button"
+      style={{ position: "absolute", left: left + width }}
+    >
+      {ButtonInner}
+    </span>
+  ) : (
+    ButtonInner
+  );
+};
+
+export default InteractiveButton;

+ 1 - 1
dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -49,7 +49,7 @@ interface IWidget {
   compact?: boolean;
   showDiff?: boolean;
   diffText?: string | null;
-  onChange?: Function;
+  onChange?: (data: ISentence) => void;
   onDelete?: Function;
 }
 const SentCellWidget = ({

+ 8 - 1
dashboard/src/components/template/SentEdit/SentContent.tsx

@@ -33,6 +33,7 @@ interface IWidgetSentContent {
   wbwProgress?: boolean;
   readonly?: boolean;
   onWbwChange?: Function;
+  onTranslationChange?: (data: ISentence) => void;
   onMagicDictDone?: Function;
 }
 const SentContentWidget = ({
@@ -51,6 +52,7 @@ const SentContentWidget = ({
   wbwProgress = false,
   readonly = false,
   onWbwChange,
+  onTranslationChange,
   onMagicDictDone,
 }: IWidgetSentContent) => {
   const [layoutDirection, setLayoutDirection] = useState<TDirection>(layout);
@@ -194,7 +196,12 @@ const SentContentWidget = ({
               end={item.wordEnd}
               channelId={item.channel.id}
             >
-              <SentCell key={id} initValue={item} compact={compact} />
+              <SentCell
+                key={id}
+                initValue={item}
+                compact={compact}
+                onChange={onTranslationChange}
+              />
             </SuggestionFocus>
           );
         })}

+ 61 - 0
dashboard/src/components/template/SentEdit/SuggestionButton.tsx

@@ -0,0 +1,61 @@
+import { Space, Tooltip } from "antd";
+import { LikeOutlined, DeleteOutlined } from "@ant-design/icons";
+
+import { ISentence } from "../SentEdit";
+import { HandOutlinedIcon } from "../../../assets/icon";
+import SuggestionPopover from "./SuggestionPopover";
+import store from "../../../store";
+import { openPanel } from "../../../reducers/right-panel";
+import { show } from "../../../reducers/discussion";
+
+export const prOpen = (data: ISentence) => {
+  store.dispatch(
+    show({
+      type: "pr",
+      sent: data,
+    })
+  );
+  store.dispatch(openPanel("suggestion"));
+};
+
+interface IWidget {
+  data: ISentence;
+  hideCount?: boolean;
+  hideInZero?: boolean;
+}
+
+const SuggestionButton = ({
+  data,
+  hideCount = false,
+  hideInZero = false,
+}: IWidget) => {
+  const prNumber = data.suggestionCount?.suggestion;
+
+  return hideInZero && prNumber === 0 ? (
+    <></>
+  ) : (
+    <Space
+      style={{
+        cursor: "pointer",
+        color: prNumber && prNumber > 0 ? "#1890ff" : "unset",
+      }}
+      onClick={(event) => {
+        prOpen(data);
+      }}
+    >
+      <Tooltip title="修改建议">
+        <HandOutlinedIcon />
+      </Tooltip>
+      <SuggestionPopover
+        book={data.book}
+        para={data.para}
+        start={data.wordStart}
+        end={data.wordEnd}
+        channelId={data.channel.id}
+      />
+      {hideCount ? <></> : prNumber}
+    </Space>
+  );
+};
+
+export default SuggestionButton;

+ 4 - 44
dashboard/src/components/template/SentEdit/SuggestionToolbar.tsx

@@ -1,5 +1,6 @@
 import { Divider, Popconfirm, Space, Tooltip, Typography } from "antd";
 import { LikeOutlined, DeleteOutlined } from "@ant-design/icons";
+
 import { ISentence } from "../SentEdit";
 import { useEffect, useState } from "react";
 import PrAcceptButton from "./PrAcceptButton";
@@ -11,6 +12,8 @@ import { openPanel } from "../../../reducers/right-panel";
 import { useIntl } from "react-intl";
 import SuggestionPopover from "./SuggestionPopover";
 import DiscussionButton from "../../discussion/DiscussionButton";
+import SuggestionButton from "./SuggestionButton";
+import InteractiveButton from "./InteractiveButton";
 
 const { Paragraph } = Typography;
 
@@ -34,21 +37,8 @@ const SuggestionToolbarWidget = ({
   onPrClose,
   onDelete,
 }: IWidget) => {
-  const [CommentCount, setCommentCount] = useState<number | undefined>(
-    data.suggestionCount?.discussion
-  );
-  const discussionCount = useAppSelector(count);
   const intl = useIntl();
 
-  useEffect(() => {
-    if (
-      discussionCount?.resType === "sentence" &&
-      discussionCount.resId === data.id
-    ) {
-      setCommentCount(discussionCount.count);
-    }
-  }, [data.id, discussionCount]);
-  const prNumber = data.suggestionCount?.suggestion;
   return (
     <Paragraph type="secondary" style={style}>
       {isPr ? (
@@ -91,37 +81,7 @@ const SuggestionToolbarWidget = ({
           </Popconfirm>
         </Space>
       ) : (
-        <Space size={"small"}>
-          <Space
-            style={{
-              cursor: "pointer",
-              color: prNumber && prNumber > 0 ? "#1890ff" : "unset",
-            }}
-            onClick={(event) => {
-              store.dispatch(
-                show({
-                  type: "pr",
-                  sent: data,
-                })
-              );
-              store.dispatch(openPanel("suggestion"));
-            }}
-          >
-            <Tooltip title="修改建议">
-              <HandOutlinedIcon />
-            </Tooltip>
-            <SuggestionPopover
-              book={data.book}
-              para={data.para}
-              start={data.wordStart}
-              end={data.wordEnd}
-              channelId={data.channel.id}
-            />
-            {prNumber}
-          </Space>
-          {compact ? undefined : <Divider type="vertical" />}
-          <DiscussionButton initCount={CommentCount} resId={data.id} />
-        </Space>
+        <InteractiveButton data={data} compact={compact} />
       )}
     </Paragraph>
   );

+ 145 - 17
dashboard/src/components/template/SentRead.tsx

@@ -1,5 +1,6 @@
 import { useEffect, useRef, useState } from "react";
-import { Typography } from "antd";
+import { Button, Dropdown, MenuProps, Typography } from "antd";
+import { LoadingOutlined, CloseOutlined } from "@ant-design/icons";
 
 import { useAppSelector } from "../../hooks";
 import {
@@ -9,12 +10,38 @@ import {
 } from "../../reducers/setting";
 import { GetUserSetting } from "../auth/setting/default";
 import { TCodeConvertor } from "./utilities";
-import { ISentence } from "./SentEdit";
+import { ISentence, IWidgetSentEditInner, SentEditInner } from "./SentEdit";
 import MdView from "./MdView";
 import store from "../../store";
 import { push } from "../../reducers/sentence";
+import "./style.css";
+import InteractiveButton from "./SentEdit/InteractiveButton";
+import { prOpen } from "./SentEdit/SuggestionButton";
+import { openDiscussion } from "../discussion/DiscussionButton";
+import { IEditableSentence } from "../api/Corpus";
+import { get } from "../../request";
+
 const { Text } = Typography;
 
+const items: MenuProps["items"] = [
+  {
+    label: "编辑",
+    key: "edit",
+  },
+  {
+    label: "讨论",
+    key: "discussion",
+  },
+  {
+    label: "修改建议",
+    key: "pr",
+  },
+  {
+    label: "标签",
+    key: "tag",
+  },
+];
+
 interface IWidgetSentReadFrame {
   origin?: ISentence[];
   translation?: ISentence[];
@@ -38,6 +65,14 @@ const SentReadFrame = ({
   error,
 }: IWidgetSentReadFrame) => {
   const [paliCode1, setPaliCode1] = useState<TCodeConvertor>("roman");
+  const [active, setActive] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [sentData, setSentData] = useState<IWidgetSentEditInner>();
+  const [showEdit, SetShowEdit] = useState(false);
+  const [translationData, setTranslationData] = useState<
+    ISentence[] | undefined
+  >(translation);
+
   const key = useAppSelector(onChangeKey);
   const value = useAppSelector(onChangeValue);
   const settings = useAppSelector(settingInfo);
@@ -83,20 +118,18 @@ const SentReadFrame = ({
       setPaliCode1(_paliCode1.toString() as TCodeConvertor);
     }
   }, [key, value, settings]);
+
   return (
-    <div
-      style={{ display: "flex", flexDirection: layout, marginBottom: 10 }}
-      ref={boxSent}
-    >
+    <span ref={boxSent} className="sent_read_shell">
       <Text type="danger" mark>
         {error}
       </Text>
-      <div
+      <span
         dangerouslySetInnerHTML={{
-          __html: `<div class="pcd_sent" id="sent_${book}-${para}-${wordStart}-${wordEnd}"></div>`,
+          __html: `<span class="pcd_sent" id="sent_${book}-${para}-${wordStart}-${wordEnd}"></span>`,
         }}
       />
-      <div style={{ flex: "5", color: "#9f3a01" }} ref={boxOrg}>
+      <span style={{ flex: "5", color: "#9f3a01" }} ref={boxOrg}>
         {origin?.map((item, id) => {
           return (
             <Text key={id}>
@@ -109,17 +142,112 @@ const SentReadFrame = ({
             </Text>
           );
         })}
-      </div>
-      <div style={{ flex: "5" }}>
-        {translation?.map((item, id) => {
+      </span>
+      <span className="sent_read" style={{ flex: "5" }}>
+        {translationData?.map((item, id) => {
           return (
-            <Text key={id}>
-              <MdView html={item.html} />
-            </Text>
+            <span key={id}>
+              {loading ? <LoadingOutlined /> : <></>}
+              <Dropdown
+                menu={{
+                  items,
+                  onClick: (e) => {
+                    console.log("click ", e);
+                    switch (e.key) {
+                      case "edit":
+                        const url = `/v2/editable-sentence/${item.id}`;
+                        console.info("api request", url);
+                        setLoading(true);
+                        get<IEditableSentence>(url)
+                          .then((json) => {
+                            console.info("api response", json);
+                            if (json.ok) {
+                              setSentData(json.data);
+                              SetShowEdit(true);
+                            }
+                          })
+                          .finally(() => setLoading(false));
+                        break;
+                      case "discussion":
+                        if (item.id) {
+                          openDiscussion(item.id, false);
+                        }
+                        break;
+                      case "pr":
+                        prOpen(item);
+                        break;
+                      default:
+                        break;
+                    }
+                  },
+                }}
+                trigger={["contextMenu"]}
+              >
+                <Text
+                  key={id}
+                  className="sent_read_translation"
+                  style={{ display: showEdit ? "none" : "inline" }}
+                >
+                  <MdView
+                    html={item.html}
+                    style={{ backgroundColor: active ? "beige" : "unset" }}
+                  />
+                </Text>
+              </Dropdown>
+              <div style={{ display: showEdit ? "block" : "none" }}>
+                <div style={{ textAlign: "right" }}>
+                  <Button
+                    size="small"
+                    icon={<CloseOutlined />}
+                    onClick={() => {
+                      SetShowEdit(false);
+                    }}
+                  >
+                    返回审阅模式
+                  </Button>
+                </div>
+
+                {sentData ? (
+                  <SentEditInner
+                    mode="edit"
+                    {...sentData}
+                    onTranslationChange={(data: ISentence) => {
+                      console.log("onTranslationChange", data);
+                      if (translationData) {
+                        let newData = [...translationData];
+                        newData.forEach(
+                          (
+                            value: ISentence,
+                            index: number,
+                            array: ISentence[]
+                          ) => {
+                            if (index === id) {
+                              array[index] = data;
+                            }
+                          }
+                        );
+                        setTranslationData(newData);
+                      }
+                    }}
+                  />
+                ) : (
+                  "无数据"
+                )}
+              </div>
+              <InteractiveButton
+                data={item}
+                compact={true}
+                float={true}
+                hideCount
+                hideInZero
+                onMouseEnter={() => setActive(true)}
+                onMouseLeave={() => setActive(false)}
+              />
+            </span>
           );
         })}
-      </div>
-    </div>
+      </span>
+    </span>
   );
 };
 

+ 15 - 0
dashboard/src/components/template/style.css

@@ -6,3 +6,18 @@
 .pcd_word:hover {
   text-decoration: underline dotted;
 }
+.sent_read p {
+  display: inline;
+}
+
+.sent_read_translation:hover {
+  background-color: beige;
+}
+
+.sent_read_interactive_button:hover ~ .sent_read_translation {
+  background-color: beige;
+}
+
+.sent_read_translation:hover ~ .sent_read_interactive_button {
+  border: 1px solid black;
+}

+ 7 - 1
dashboard/src/pages/library/article/show.tsx

@@ -337,8 +337,14 @@ const Widget = () => {
           style={{ width: `calc(100% - ${rightBarWidth})`, display: "flex" }}
         >
           <div
+            className="article_shell"
             key="Article"
-            style={{ marginLeft: "auto", marginRight: "auto", width: 1100 }}
+            style={{
+              marginLeft: "auto",
+              marginRight: "auto",
+              width: 1100,
+              maxWidth: currMode === "read" ? 750 : "unset",
+            }}
           >
             <LoginAlertModal mode={currMode} />
             {type !== "textbook" ? (