Browse Source

Merge pull request #1901 from visuddhinanda/agile

逐词解析弹窗增加下拉菜单
visuddhinanda 2 years ago
parent
commit
ccb4743c2f

+ 20 - 0
dashboard/src/components/article/Article.tsx

@@ -57,6 +57,7 @@ interface IWidget {
   onFinal?: Function;
   onLoad?: Function;
   onAnthologySelect?: Function;
+  onTitle?: Function;
 }
 const ArticleWidget = ({
   type,
@@ -75,6 +76,7 @@ const ArticleWidget = ({
   onFinal,
   onLoad,
   onAnthologySelect,
+  onTitle,
 }: IWidget) => {
   return (
     <div>
@@ -95,6 +97,9 @@ const ArticleWidget = ({
             if (typeof onLoad !== "undefined") {
               onLoad(data);
             }
+            if (typeof onTitle !== "undefined") {
+              onTitle(data.title);
+            }
           }}
           onAnthologySelect={(id: string) => {
             if (typeof onAnthologySelect !== "undefined") {
@@ -112,6 +117,11 @@ const ArticleWidget = ({
               onArticleChange(type, id, target);
             }
           }}
+          onTitle={(value: string) => {
+            if (typeof onTitle !== "undefined") {
+              onTitle(value);
+            }
+          }}
         />
       ) : type === "term" ? (
         <TypeTerm
@@ -143,6 +153,16 @@ const ArticleWidget = ({
               onArticleChange(type, id, target, param);
             }
           }}
+          onLoad={(data: IArticleDataResponse) => {
+            if (typeof onLoad !== "undefined") {
+              onLoad(data);
+            }
+          }}
+          onTitle={(value: string) => {
+            if (typeof onTitle !== "undefined") {
+              onTitle(value);
+            }
+          }}
         />
       ) : type === "page" ? (
         <TypePage

+ 9 - 2
dashboard/src/components/article/TypeAnthology.tsx

@@ -13,6 +13,7 @@ interface IWidget {
   onArticleChange?: Function;
   onFinal?: Function;
   onLoad?: Function;
+  onTitle?: Function;
 }
 const TypeAnthologyWidget = ({
   type,
@@ -20,6 +21,7 @@ const TypeAnthologyWidget = ({
   articleId,
   mode = "read",
   onArticleChange,
+  onTitle,
 }: IWidget) => {
   const [loading, setLoading] = useState(false);
   const [errorCode, setErrorCode] = useState<number>();
@@ -36,6 +38,8 @@ const TypeAnthologyWidget = ({
       )}
       <AnthologyDetail
         visible={!loading}
+        channels={channels}
+        aid={articleId}
         onArticleClick={(
           anthologyId: string,
           articleId: string,
@@ -53,8 +57,11 @@ const TypeAnthologyWidget = ({
         onError={(code: number, message: string) => {
           setErrorCode(code);
         }}
-        channels={channels}
-        aid={articleId}
+        onTitle={(value: string) => {
+          if (typeof onTitle !== "undefined") {
+            onTitle(value);
+          }
+        }}
       />
     </div>
   );

+ 12 - 1
dashboard/src/components/article/TypePali.tsx

@@ -34,6 +34,7 @@ interface IWidget {
   onArticleChange?: Function;
   onFinal?: Function;
   onLoad?: Function;
+  onTitle?: Function;
 }
 const TypePaliWidget = ({
   type,
@@ -48,6 +49,7 @@ const TypePaliWidget = ({
   onArticleChange,
   onFinal,
   onLoad,
+  onTitle,
 }: IWidget) => {
   const [articleData, setArticleData] = useState<IArticleDataResponse>();
   const [articleHtml, setArticleHtml] = useState<string[]>(["<span />"]);
@@ -62,7 +64,12 @@ const TypePaliWidget = ({
   const srcDataMode = mode === "edit" || mode === "wbw" ? "edit" : "read";
 
   useEffect(() => {
-    store.dispatch(refresh({ type: "para", id: focus }));
+    const parts = focus?.split("-");
+    if (parts?.length === 2) {
+      store.dispatch(refresh({ type: "para", id: focus }));
+    } else if (parts?.length === 4) {
+      store.dispatch(refresh({ type: "sentence", id: focus }));
+    }
   }, [focus]);
 
   useEffect(() => {
@@ -134,6 +141,10 @@ const TypePaliWidget = ({
           if (typeof onLoad !== "undefined") {
             onLoad(json.data);
           }
+          if (typeof onTitle !== "undefined") {
+            const bookTitle = json.data.path ? json.data.path[0].title : "";
+            onTitle(`${bookTitle}-${json.data.title}`);
+          }
         } else {
           message.error(json.message);
         }

+ 29 - 2
dashboard/src/components/template/SentEdit.tsx

@@ -1,5 +1,5 @@
 import { Card } from "antd";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
 import { IStudio } from "../auth/StudioName";
 
 import type { IUser } from "../auth/User";
@@ -11,6 +11,8 @@ import SentTab from "./SentEdit/SentTab";
 import { IWbw } from "./Wbw/WbwWord";
 import { ArticleMode } from "../article/Article";
 import { TChannelType } from "../api/Channel";
+import { useAppSelector } from "../../hooks";
+import { currFocus } from "../../reducers/focus";
 
 export interface IResNumber {
   translation?: number;
@@ -94,6 +96,28 @@ export const SentEditInner = ({
   const [isCompact, setIsCompact] = useState(compact);
   const [articleMode, setArticleMode] = useState<ArticleMode | undefined>(mode);
   const [loadedRes, setLoadedRes] = useState<IResNumber>();
+  const [isFocus, setIsFocus] = useState(false);
+  const focus = useAppSelector(currFocus);
+  const divRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (focus) {
+      if (focus.focus?.type === "sentence") {
+        if (focus.focus.id === id) {
+          setIsFocus(true);
+          divRef.current?.scrollIntoView({
+            behavior: "smooth",
+            block: "nearest",
+            inline: "nearest",
+          });
+        } else {
+          setIsFocus(false);
+        }
+      }
+    } else {
+      setIsFocus(false);
+    }
+  }, [focus, id]);
 
   useEffect(() => {
     const validRes = (value: ISentence, type: TChannelType) =>
@@ -127,9 +151,12 @@ export const SentEditInner = ({
 
   return (
     <Card
+      ref={divRef}
       bodyStyle={{ paddingBottom: 0, paddingLeft: 0, paddingRight: 0 }}
       style={{
-        border: "1px solid rgb(128 128 128 / 10%)",
+        border: isFocus
+          ? "2px solid rgb(0 0 200 / 50%)"
+          : "1px solid rgb(128 128 128 / 10%)",
         marginTop: 4,
         borderRadius: 6,
         backgroundColor: "rgb(255 255 255 / 8%)",

+ 18 - 1
dashboard/src/components/template/SentEdit/SentEditMenu.tsx

@@ -19,6 +19,7 @@ import {
   PasteOutLinedIcon,
 } from "../../../assets/icon";
 import { useIntl } from "react-intl";
+import { fullUrl } from "../../../utils";
 
 interface IWidget {
   data?: ISentence;
@@ -58,6 +59,18 @@ const SentEditMenuWidget = ({
       case "timeline":
         setTimelineOpen(true);
         break;
+      case "copy-link":
+        if (data) {
+          let link = `/article/para/${data.book}-${data.para}?mode=edit`;
+          link += `&book=${data.book}&par=${data.para}`;
+          link += `&channel=${data.channel.id}`;
+
+          link += `&focus=${data.book}-${data.para}-${data.wordStart}-${data.wordEnd}`;
+          navigator.clipboard.writeText(fullUrl(link)).then(() => {
+            message.success("链接地址已经拷贝到剪贴板");
+          });
+        }
+        break;
       default:
         break;
     }
@@ -99,7 +112,11 @@ const SentEditMenuWidget = ({
       key: "json",
       label: "To Json",
       icon: <JsonOutlinedIcon />,
-      disabled: !data || data.contentType === "json" || isPr,
+      disabled:
+        !data ||
+        data.channel.type !== "nissaya" ||
+        data.contentType === "json" ||
+        isPr,
     },
     {
       type: "divider",

+ 23 - 3
dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -1,5 +1,5 @@
 import { useEffect, useState } from "react";
-import { Badge, Space, Tabs, Typography } from "antd";
+import { Badge, Space, Tabs, Typography, message } from "antd";
 import {
   TranslationOutlined,
   CloseOutlined,
@@ -17,6 +17,7 @@ import SentMenu from "./SentMenu";
 import { ArticleMode } from "../../article/Article";
 import { IResNumber } from "../SentEdit";
 import SentTabCopy from "./SentTabCopy";
+import { fullUrl } from "../../../utils";
 
 const { Text } = Typography;
 
@@ -107,11 +108,13 @@ const SentTabWidget = ({
               width: "100%",
               marginRight: 10,
               backgroundColor:
-                hover || currKey !== "close" ? "#80808030" : "unset",
+                hover || currKey !== "close"
+                  ? "rgba(128, 128, 128, 0.1)"
+                  : "unset",
             }
           : {
               padding: "0 8px",
-              backgroundColor: "#80808030",
+              backgroundColor: "rgba(128, 128, 128, 0.1)",
             }
       }
       tabBarStyle={{ marginBottom: 0 }}
@@ -161,6 +164,23 @@ const SentTabWidget = ({
                     onModeChange("wbw");
                   }
                   break;
+                case "copy-id":
+                  const id = `{{${book}-${para}-${wordStart}-${wordEnd}}}`;
+                  navigator.clipboard.writeText(id).then(() => {
+                    message.success("编号已经拷贝到剪贴板");
+                  });
+                  break;
+                case "copy-link":
+                  let link = `/article/para/${book}-${para}?mode=edit`;
+                  link += `&book=${book}&par=${para}`;
+                  if (channelsId) {
+                    link += `&channel=` + channelsId?.join("_");
+                  }
+                  link += `&focus=${book}-${para}-${wordStart}-${wordEnd}`;
+                  navigator.clipboard.writeText(fullUrl(link)).then(() => {
+                    message.success("链接地址已经拷贝到剪贴板");
+                  });
+                  break;
                 default:
                   break;
               }

+ 56 - 31
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -1,5 +1,5 @@
 import { useEffect, useState } from "react";
-import { useIntl } from "react-intl";
+import { IntlShape, useIntl } from "react-intl";
 import { Typography, Button, Space } from "antd";
 import { SwapOutlined } from "@ant-design/icons";
 import type { MenuProps } from "antd";
@@ -11,9 +11,56 @@ import "./wbw.css";
 import { useAppSelector } from "../../../hooks";
 import { inlineDict as _inlineDict } from "../../../reducers/inline-dict";
 import WbwParent2 from "./WbwParent2";
+import { IApiResponseDictData } from "../../api/Dict";
+
+export interface ValueType {
+  key: string;
+  label: string;
+}
 
 const { Text } = Typography;
 
+export const caseInDict = (
+  wordIn: string,
+  wordIndex: string[],
+  wordList: IApiResponseDictData[],
+  intl: IntlShape
+): MenuProps["items"] => {
+  if (!wordIn) {
+    return [];
+  }
+  if (wordIndex.includes(wordIn)) {
+    const result = wordList.filter((word) => word.word === wordIn);
+    //查重
+    //TODO 加入信心指数并排序
+    let myMap = new Map<string, number>();
+    let factors: string[] = [];
+    for (const iterator of result) {
+      myMap.set(iterator.type + "#" + iterator.grammar, 1);
+    }
+    myMap.forEach((value, key, map) => {
+      factors.push(key);
+    });
+
+    const menu = factors.map((item) => {
+      const arrItem: string[] = item
+        .replaceAll(".", "")
+        .replaceAll("#", "$")
+        .split("$");
+      let noNull = arrItem.filter((item) => item !== "");
+      noNull.forEach((item, index, arr) => {
+        arr[index] = intl.formatMessage({
+          id: `dict.fields.type.${item}.short.label`,
+        });
+      });
+      return { key: item, label: noNull.join(" ") };
+    });
+    return menu;
+  } else {
+    return [];
+  }
+};
+
 interface IWidget {
   data: IWbw;
   display?: TWbwDisplayMode;
@@ -40,36 +87,14 @@ const WbwCaseWidget = ({ data, display, onSplit, onChange }: IWidget) => {
     if (!data.real.value) {
       return;
     }
-    if (inlineDict.wordIndex.includes(data.real.value)) {
-      const result = inlineDict.wordList.filter(
-        (word) => word.word === data.real.value
-      );
-      //查重
-      //TODO 加入信心指数并排序
-      let myMap = new Map<string, number>();
-      let factors: string[] = [];
-      for (const iterator of result) {
-        myMap.set(iterator.type + "#" + iterator.grammar, 1);
-      }
-      myMap.forEach((value, key, map) => {
-        factors.push(key);
-      });
-
-      const menu = factors.map((item) => {
-        const arrItem: string[] = item
-          .replaceAll(".", "")
-          .replaceAll("#", "$")
-          .split("$");
-        let noNull = arrItem.filter((item) => item !== "");
-        noNull.forEach((item, index, arr) => {
-          arr[index] = intl.formatMessage({
-            id: `dict.fields.type.${item}.short.label`,
-          });
-        });
-        return { key: item, label: noNull.join(" ") };
-      });
-      setItems(menu);
-    }
+    setItems(
+      caseInDict(
+        data.real.value,
+        inlineDict.wordIndex,
+        inlineDict.wordList,
+        intl
+      )
+    );
   }, [data.real.value, inlineDict, intl]);
   const onClick: MenuProps["onClick"] = (e) => {
     console.log("click ", e);

+ 12 - 70
dashboard/src/components/template/Wbw/WbwDetailBasic.tsx

@@ -1,37 +1,21 @@
 import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
-import {
-  Form,
-  Input,
-  AutoComplete,
-  Button,
-  Popover,
-  Space,
-  Badge,
-  Tooltip,
-} from "antd";
+import { Form, Input, Button, Popover } from "antd";
 import { Collapse } from "antd";
-import { MoreOutlined, QuestionCircleOutlined } from "@ant-design/icons";
+import { MoreOutlined } from "@ant-design/icons";
 
-import SelectCase from "../../dict/SelectCase";
 import { IWbw, IWbwField } from "./WbwWord";
 import WbwMeaningSelect from "./WbwMeaningSelect";
-import { useAppSelector } from "../../../hooks";
-import { inlineDict as _inlineDict } from "../../../reducers/inline-dict";
-import { IApiResponseDictData } from "../../api/Dict";
+
 import WbwDetailFm from "./WbwDetailFm";
 import WbwDetailParent2 from "./WbwDetailParent2";
 import WbwDetailFactor from "./WbwDetailFactor";
 import WbwDetailBasicRelation from "./WbwDetailBasicRelation";
+import WbwDetailParent from "./WbwDetailParent";
+import WbwDetailCase from "./WbwDetailCase";
 
 const { Panel } = Collapse;
 
-interface ValueType {
-  key?: string;
-  label: React.ReactNode;
-  value: string | number;
-}
-
 export interface IWordBasic {
   meaning?: string[];
   case?: string;
@@ -39,30 +23,6 @@ export interface IWordBasic {
   factorMeaning?: string;
   parent?: string;
 }
-export const getParentInDict = (
-  wordIn: string,
-  wordIndex: string[],
-  wordList: IApiResponseDictData[]
-): string[] => {
-  if (wordIndex.includes(wordIn)) {
-    const result = wordList.filter((word) => word.word === wordIn);
-    //查重
-    //TODO 加入信心指数并排序
-    let myMap = new Map<string, number>();
-    let parent: string[] = [];
-    for (const iterator of result) {
-      if (iterator.parent) {
-        myMap.set(iterator.parent, 1);
-      }
-    }
-    myMap.forEach((value, key, map) => {
-      parent.push(key);
-    });
-    return parent;
-  } else {
-    return [];
-  }
-};
 
 interface IWidget {
   data: IWbw;
@@ -78,8 +38,6 @@ const WbwDetailBasicWidget = ({
 }: IWidget) => {
   const [form] = Form.useForm();
   const intl = useIntl();
-  const inlineDict = useAppSelector(_inlineDict);
-  const [parentOptions, setParentOptions] = useState<ValueType[]>([]);
   const [factors, setFactors] = useState<string[] | undefined>(
     data.factors?.value?.split("+")
   );
@@ -97,21 +55,6 @@ const WbwDetailBasicWidget = ({
     }
   };
 
-  useEffect(() => {
-    const parent = getParentInDict(
-      data.word.value,
-      inlineDict.wordIndex,
-      inlineDict.wordList
-    );
-    const parentOptions = parent.map((item) => {
-      return {
-        label: item,
-        value: item,
-      };
-    });
-    setParentOptions(parentOptions);
-  }, [inlineDict, data]);
-
   return (
     <>
       <Form
@@ -219,8 +162,9 @@ const WbwDetailBasicWidget = ({
           tooltip={intl.formatMessage({ id: "forms.fields.case.tooltip" })}
           name="case"
         >
-          <SelectCase
-            onCaseChange={(value: string) => {
+          <WbwDetailCase
+            data={data}
+            onChange={(value: string) => {
               if (typeof onChange !== "undefined") {
                 onChange({ field: "case", value: value });
               }
@@ -237,16 +181,14 @@ const WbwDetailBasicWidget = ({
             id: "forms.fields.parent.tooltip",
           })}
         >
-          <AutoComplete
-            options={parentOptions}
-            onChange={(value: any, option: ValueType | ValueType[]) => {
+          <WbwDetailParent
+            data={data}
+            onChange={(value: string) => {
               if (typeof onChange !== "undefined") {
                 onChange({ field: "parent", value: value });
               }
             }}
-          >
-            <Input allowClear />
-          </AutoComplete>
+          />
         </Form.Item>
         <Collapse bordered={false}>
           <Panel header="词源" key="parent2">

+ 57 - 0
dashboard/src/components/template/Wbw/WbwDetailCase.tsx

@@ -0,0 +1,57 @@
+import { Button, Dropdown } from "antd";
+
+import { MoreOutlined } from "@ant-design/icons";
+
+import { useAppSelector } from "../../../hooks";
+import { inlineDict as _inlineDict } from "../../../reducers/inline-dict";
+
+import { IWbw } from "./WbwWord";
+
+import SelectCase from "../../dict/SelectCase";
+import { caseInDict } from "./WbwCase";
+import { useIntl } from "react-intl";
+
+interface IWidget {
+  data: IWbw;
+  onChange?: Function;
+}
+const WbwDetailCaseWidget = ({ data, onChange }: IWidget) => {
+  const inlineDict = useAppSelector(_inlineDict);
+  const intl = useIntl();
+
+  return (
+    <div style={{ display: "flex" }}>
+      <SelectCase
+        value={data.case?.value}
+        onCaseChange={(value: string) => {
+          if (typeof onChange !== "undefined") {
+            onChange(value);
+          }
+        }}
+      />
+      <Dropdown
+        menu={{
+          items: data.real.value
+            ? caseInDict(
+                data.real.value,
+                inlineDict.wordIndex,
+                inlineDict.wordList,
+                intl
+              )
+            : [],
+          onClick: (e) => {
+            console.log("click ", e.key);
+            if (typeof onChange !== "undefined") {
+              onChange(e.key);
+            }
+          },
+        }}
+        placement="bottomRight"
+      >
+        <Button type="text" icon={<MoreOutlined />} />
+      </Dropdown>
+    </div>
+  );
+};
+
+export default WbwDetailCaseWidget;

+ 4 - 1
dashboard/src/components/template/Wbw/WbwDetailFactor.tsx

@@ -69,7 +69,10 @@ const WbwDetailFactorWidget = ({ data, onChange }: IWidget) => {
         value: item,
       };
     });
-    setFactorOptions(options);
+    setFactorOptions([
+      ...options,
+      { label: data.real.value, value: data.real.value },
+    ]);
   }, [data.real.value, inlineDict.wordIndex, inlineDict.wordList]);
 
   return (

+ 84 - 0
dashboard/src/components/template/Wbw/WbwDetailParent.tsx

@@ -0,0 +1,84 @@
+import { AutoComplete, Input } from "antd";
+import { useEffect, useState } from "react";
+import { useAppSelector } from "../../../hooks";
+
+import { inlineDict as _inlineDict } from "../../../reducers/inline-dict";
+
+import { IWbw } from "./WbwWord";
+import { IApiResponseDictData } from "../../api/Dict";
+
+export const getParentInDict = (
+  wordIn: string,
+  wordIndex: string[],
+  wordList: IApiResponseDictData[]
+): string[] => {
+  if (wordIndex.includes(wordIn)) {
+    const result = wordList.filter((word) => word.word === wordIn);
+    //查重
+    //TODO 加入信心指数并排序
+    let myMap = new Map<string, number>();
+    let parent: string[] = [];
+    for (const iterator of result) {
+      if (iterator.parent) {
+        myMap.set(iterator.parent, 1);
+      }
+    }
+    myMap.forEach((value, key, map) => {
+      parent.push(key);
+    });
+    return parent;
+  } else {
+    return [];
+  }
+};
+
+interface ValueType {
+  key?: string;
+  label: React.ReactNode;
+  value: string | number;
+}
+interface IWidget {
+  data: IWbw;
+  onChange?: Function;
+}
+const WbwDetailParentWidget = ({ data, onChange }: IWidget) => {
+  const [parentOptions, setParentOptions] = useState<ValueType[]>([]);
+  const inlineDict = useAppSelector(_inlineDict);
+
+  useEffect(() => {
+    if (!data.real.value) {
+      return;
+    }
+    const parent = getParentInDict(
+      data.word.value,
+      inlineDict.wordIndex,
+      inlineDict.wordList
+    );
+    const parentOptions = parent.map((item) => {
+      return {
+        label: item,
+        value: item,
+      };
+    });
+    setParentOptions([
+      ...parentOptions,
+      { label: data.real.value, value: data.real.value },
+    ]);
+  }, [inlineDict, data]);
+
+  return (
+    <AutoComplete
+      options={parentOptions}
+      value={data.parent?.value}
+      onChange={(value: any, option: ValueType | ValueType[]) => {
+        if (typeof onChange !== "undefined") {
+          onChange({ field: "parent", value: value });
+        }
+      }}
+    >
+      <Input allowClear />
+    </AutoComplete>
+  );
+};
+
+export default WbwDetailParentWidget;

+ 5 - 6
dashboard/src/components/template/Wbw/WbwPali.tsx

@@ -21,7 +21,7 @@ import { useAppSelector } from "../../../hooks";
 import { add, relationAddParam } from "../../../reducers/relation-add";
 import { ArticleMode } from "../../article/Article";
 import { anchor, showWbw } from "../../../reducers/wbw";
-import { CommentOutlinedIcon, HandBookIcon } from "../../../assets/icon";
+import { CommentOutlinedIcon } from "../../../assets/icon";
 import { ParaLinkCtl } from "../ParaLink";
 
 const { Paragraph } = Typography;
@@ -207,16 +207,15 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
         <TagTwoTone twoToneColor={color} />
       </Popover>
     ) : undefined;
-  let classPali: string;
+  let classPali: string = "pali";
   switch (data.style?.value) {
     case "note":
       classPali = "wbw_note";
       break;
     case "bld":
-      classPali = "pali wbw_bold";
-      break;
-    default:
-      classPali = "pali";
+      if (!data.word.value.includes("{")) {
+        classPali = "pali wbw_bold";
+      }
       break;
   }
   let padding: string;

+ 3 - 4
dashboard/src/pages/library/article/show.tsx

@@ -397,12 +397,11 @@ const Widget = () => {
                   navigate(url);
                 }
               }}
+              onTitle={(value: string) => {
+                document.title = value.slice(0, 128);
+              }}
               onLoad={(article: IArticleDataResponse) => {
                 setLoadedArticleData(article);
-                const windowTitle = article.title_text
-                  ? article.title_text
-                  : article.title;
-                document.title = windowTitle.slice(0, 128);
                 const paramTopic = searchParams.get("topic");
                 const paramComment = searchParams.get("comment");
                 const paramType = searchParams.get("dis_type");