2
0
Эх сурвалжийг харах

Merge pull request #1873 from visuddhinanda/agile

添加语法手册
visuddhinanda 2 жил өмнө
parent
commit
c57e7030b7

+ 8 - 4
dashboard/src/components/dict/DictSearch.tsx

@@ -13,6 +13,7 @@ interface IWidget {
 
 const DictSearchWidget = ({ word, compact = false }: IWidget) => {
   const [loading, setLoading] = useState(false);
+
   const defaultData: IDictContentData = {
     dictlist: [],
     words: [],
@@ -20,10 +21,7 @@ const DictSearchWidget = ({ word, compact = false }: IWidget) => {
   };
   const [tableData, setTableData] = useState<IDictContentData>(defaultData);
 
-  useEffect(() => {
-    if (typeof word === "undefined") {
-      return;
-    }
+  const fetchDict = (word: string) => {
     const url = `/v2/dict?word=${word}`;
     console.info("url", url);
     setLoading(true);
@@ -35,6 +33,12 @@ const DictSearchWidget = ({ word, compact = false }: IWidget) => {
       .catch((error) => {
         console.error(error);
       });
+  };
+  useEffect(() => {
+    if (typeof word === "undefined") {
+      return;
+    }
+    fetchDict(word);
   }, [word, setTableData]);
 
   return (

+ 67 - 16
dashboard/src/components/dict/Dictionary.tsx

@@ -4,6 +4,7 @@ import { Layout, Affix, Col, Row } from "antd";
 import DictSearch from "./DictSearch";
 import SearchVocabulary from "./SearchVocabulary";
 import Compound from "./Compound";
+import TermShow from "../term/TermShow";
 
 const { Content } = Layout;
 
@@ -16,24 +17,59 @@ const DictionaryWidget = ({ word, compact = false, onSearch }: IWidget) => {
   const [split, setSplit] = useState<string>();
   const [wordSearch, setWordSearch] = useState<string>();
   const [container, setContainer] = useState<HTMLDivElement | null>(null);
+  const [dictType, setDictType] = useState("dict");
+  const [wordInput, setWordInput] = useState(word);
+  const [wordId, setWordId] = useState<string>();
 
   useEffect(() => {
-    if (word !== wordSearch) {
-      setWordSearch(word?.toLowerCase());
-      document.getElementById("pcd_dict_top")?.scrollIntoView();
-    }
+    console.debug("param word change", word);
+    setWordInput(word);
   }, [word]);
 
-  const dictSearch = (value: string, isFactor?: boolean) => {
-    console.log("onSearch", value);
-    const currWord = value.toLowerCase();
+  const wordChange = (value?: string) => {
+    console.debug("has ':'", value, value?.includes(":"));
+    let currWord: string | undefined = "";
+    if (value?.includes(":")) {
+      const param = value.split(" ");
+      param.forEach((value) => {
+        const kv = value.split(":");
+        if (kv.length === 2) {
+          switch (kv[0]) {
+            case "type":
+              setDictType(kv[1]);
+              break;
+            case "word":
+              currWord = kv[1];
+              break;
+            case "id":
+              setWordId(kv[1]);
+              break;
+            default:
+              break;
+          }
+        }
+      });
+    } else {
+      setDictType("dict");
+      currWord = value?.toLowerCase();
+    }
     document.getElementById("pcd_dict_top")?.scrollIntoView();
-    if (typeof onSearch !== "undefined") {
-      if (!isFactor) {
-        onSearch(currWord);
-      } else {
-        setWordSearch(currWord);
-      }
+    return currWord;
+  };
+
+  useEffect(() => {
+    console.debug("wordInput change", wordInput);
+    if (wordInput !== wordSearch) {
+      const currWord = wordChange(wordInput);
+      setWordSearch(currWord);
+    }
+  }, [wordInput]);
+
+  const dictSearch = (value: string, isFactor?: boolean) => {
+    console.info("onSearch", value);
+    const currWord = wordChange(value);
+    if (typeof onSearch !== "undefined" && !isFactor) {
+      onSearch(currWord);
     } else {
       setWordSearch(currWord);
     }
@@ -52,7 +88,7 @@ const DictionaryWidget = ({ word, compact = false, onSearch }: IWidget) => {
             {compact ? <></> : <Col flex="auto"></Col>}
             <Col flex="560px">
               <SearchVocabulary
-                value={word}
+                value={wordInput?.toLowerCase()}
                 onSearch={dictSearch}
                 onSplit={(word: string | undefined) => {
                   console.log("onSplit", word);
@@ -68,8 +104,23 @@ const DictionaryWidget = ({ word, compact = false, onSearch }: IWidget) => {
         <Row>
           {compact ? <></> : <Col flex="auto"></Col>}
           <Col flex="1260px">
-            <Compound word={word} add={split} onSearch={dictSearch} />
-            <DictSearch word={wordSearch} compact={compact} />
+            {dictType === "dict" ? (
+              <div>
+                <Compound word={word} add={split} onSearch={dictSearch} />
+                <DictSearch word={wordSearch} compact={compact} />
+              </div>
+            ) : (
+              <TermShow
+                word={wordSearch}
+                wordId={wordId}
+                hideInput
+                onIdChange={(value: string) => {
+                  const newInput = `type:term id:${value}`;
+                  console.debug("term onIdChange setWordInput", newInput);
+                  setWordInput(newInput);
+                }}
+              />
+            )}
           </Col>
           {compact ? <></> : <Col flex="auto"></Col>}
         </Row>

+ 3 - 2
dashboard/src/components/dict/SearchVocabulary.tsx

@@ -30,8 +30,9 @@ const SearchVocabularyWidget = ({
   const [factors, setFactors] = useState<string[]>([]);
   const intervalRef = useRef<number | null>(null); //防抖计时器句柄
 
+  console.debug("value", value);
   useEffect(() => {
-    console.log("dict input", value);
+    console.debug("dict input", value);
     setInput(value);
     factorChange(value);
   }, [value]);
@@ -72,7 +73,7 @@ const SearchVocabularyWidget = ({
   };
 
   const factorChange = (word?: string) => {
-    if (typeof word === "undefined") {
+    if (typeof word === "undefined" || word.includes(":")) {
       setFactors([]);
       return;
     }

+ 98 - 0
dashboard/src/components/discussion/Discussion.tsx

@@ -0,0 +1,98 @@
+import { useEffect, useState } from "react";
+import { ArrowLeftOutlined } from "@ant-design/icons";
+
+import DiscussionTopic from "./DiscussionTopic";
+import DiscussionListCard, { TResType } from "./DiscussionListCard";
+import { IComment } from "./DiscussionItem";
+import { useAppSelector } from "../../hooks";
+import {
+  countChange,
+  IShowDiscussion,
+  message,
+  show,
+  showAnchor,
+} from "../../reducers/discussion";
+import { Button } from "antd";
+import store from "../../store";
+
+export interface IAnswerCount {
+  id: string;
+  count: number;
+}
+
+interface IWidget {
+  resId?: string;
+  resType: TResType;
+  focus?: string;
+}
+
+const DiscussionWidget = ({ resId, resType, focus }: IWidget) => {
+  const [childrenDrawer, setChildrenDrawer] = useState(false);
+  const [topicId, setTopicId] = useState<string>();
+  const [topic, setTopic] = useState<IComment>();
+  const [answerCount, setAnswerCount] = useState<IAnswerCount>();
+  const [currTopic, setCurrTopic] = useState<IComment>();
+
+  const showChildrenDrawer = (comment: IComment) => {
+    setChildrenDrawer(true);
+    if (comment.id) {
+      setTopicId(comment.id);
+      setTopic(undefined);
+    } else {
+      setTopicId(undefined);
+      setTopic(comment);
+    }
+  };
+
+  return (
+    <>
+      {childrenDrawer ? (
+        <div>
+          <Button
+            shape="circle"
+            icon={<ArrowLeftOutlined />}
+            onClick={() => setChildrenDrawer(false)}
+          />
+          <DiscussionTopic
+            resType={resType}
+            topicId={topicId}
+            topic={topic}
+            focus={focus}
+            onItemCountChange={(count: number, parent: string) => {
+              setAnswerCount({ id: parent, count: count });
+            }}
+            onTopicReady={(value: IComment) => {
+              setCurrTopic(value);
+            }}
+            onTopicDelete={() => {
+              setChildrenDrawer(false);
+            }}
+          />
+        </div>
+      ) : (
+        <DiscussionListCard
+          resId={resId}
+          resType={resType}
+          onSelect={(
+            e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
+            comment: IComment
+          ) => showChildrenDrawer(comment)}
+          onReply={(comment: IComment) => showChildrenDrawer(comment)}
+          onReady={() => {}}
+          changedAnswerCount={answerCount}
+          onItemCountChange={(count: number) => {
+            store.dispatch(
+              countChange({
+                count: count,
+                resId: resId,
+                resType: resType,
+              })
+            );
+          }}
+        />
+      )}
+    </>
+  );
+};
+
+export default DiscussionWidget;

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

@@ -16,7 +16,13 @@ import { currentUser as _currentUser } from "../../reducers/current-user";
 import { CommentOutlinedIcon, TemplateOutlinedIcon } from "../../assets/icon";
 import { ISentenceResponse } from "../api/Corpus";
 
-export type TResType = "article" | "channel" | "chapter" | "sentence" | "wbw";
+export type TResType =
+  | "article"
+  | "channel"
+  | "chapter"
+  | "sentence"
+  | "wbw"
+  | "term";
 
 interface IWidget {
   resId?: string;

+ 0 - 1
dashboard/src/components/export/ExportModal.tsx

@@ -70,7 +70,6 @@ const ExportModalWidget = ({
     filenameRef.current = filename;
   });
   const queryStatus = () => {
-    console.debug("timer", filenameRef.current);
     if (typeof filenameRef.current === "undefined") {
       return;
     }

+ 10 - 7
dashboard/src/components/notification/NotificationIcon.tsx

@@ -49,13 +49,16 @@ const NotificationIconWidget = () => {
             localStorage.setItem("notification/new", newMessageTime);
             if (window.Notification && Notification.permission !== "denied") {
               Notification.requestPermission(function (status) {
-                const notification = new Notification("通知标题", {
-                  body: json.data.rows[0].content,
-                  icon:
-                    process.env.REACT_APP_API_HOST +
-                    "/assets/images/wikipali_logo.png",
-                  tag: json.data.rows[0].id,
-                });
+                const notification = new Notification(
+                  json.data.rows[0].res_type,
+                  {
+                    body: json.data.rows[0].content,
+                    icon:
+                      process.env.REACT_APP_API_HOST +
+                      "/assets/images/wikipali_logo.png",
+                    tag: json.data.rows[0].id,
+                  }
+                );
                 notification.onclick = (event) => {
                   event.preventDefault(); // 阻止浏览器聚焦于 Notification 的标签页
                   window.open(json.data.rows[0].url, "_blank");

+ 2 - 2
dashboard/src/components/template/ParaShell.tsx

@@ -46,7 +46,7 @@ const ParaShellCtl = ({
       style={{
         border: isFocus
           ? "2px solid #e35f00bd "
-          : "2px solid rgba(0, 107, 255, 0.2)",
+          : "2px solid rgba(128, 128, 128, 0.3)",
         borderRadius: 6,
         marginTop: 20,
         marginBottom: 28,
@@ -60,7 +60,7 @@ const ParaShellCtl = ({
           marginLeft: -6,
           border: isFocus
             ? "2px solid #e35f00bd "
-            : "2px solid rgba(0, 107, 255, 0.2)",
+            : "2px solid rgba(128, 128, 128, 0.3)",
           borderRadius: "6px",
         }}
       >

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

@@ -129,7 +129,7 @@ export const SentEditInner = ({
     <Card
       bodyStyle={{ paddingBottom: 0, paddingLeft: 0, paddingRight: 0 }}
       style={{
-        border: "2px solid rgb(128 128 128 / 30%)",
+        border: "1px solid rgb(128 128 128 / 10%)",
         marginTop: 4,
         borderRadius: 6,
         backgroundColor: "rgb(255 255 255 / 8%)",

+ 24 - 4
dashboard/src/components/template/Wbw/WbwDetailBasic.tsx

@@ -14,6 +14,8 @@ import WbwDetailFm from "./WbwDetailFm";
 import WbwDetailParent2 from "./WbwDetailParent2";
 import WbwDetailRelation from "./WbwDetailRelation";
 import WbwDetailFactor from "./WbwDetailFactor";
+import store from "../../../store";
+import { lookup } from "../../../reducers/command";
 
 const { Panel } = Collapse;
 
@@ -256,10 +258,28 @@ const WbwDetailBasicWidget = ({
           </Panel>
           <Panel
             header={
-              <Space>
-                {"关联"}
-                <Badge color="geekblue" count={relationCount} />
-              </Space>
+              <div style={{ display: "flex", justifyContent: "space-between" }}>
+                <Space>
+                  {intl.formatMessage({ id: "buttons.relate" })}
+                  <Badge color="geekblue" count={relationCount} />
+                </Space>
+                <Button
+                  type="link"
+                  onClick={() => {
+                    if (data.case && data.case?.value) {
+                      const caseParts = data.case?.value
+                        .split("$")
+                        .map((item) => item.replaceAll(".", ""));
+                      const endCase = caseParts[caseParts.length - 1];
+                      store.dispatch(
+                        lookup(`type:term word:${endCase}.relations`)
+                      );
+                    }
+                  }}
+                >
+                  {"语法手册"}
+                </Button>
+              </div>
             }
             key="relation"
             style={{ display: showRelation ? "block" : "none" }}

+ 23 - 3
dashboard/src/components/term/TermItem.tsx

@@ -14,7 +14,10 @@ import { useEffect, useState } from "react";
 import StudioName from "../auth/StudioName";
 import { Link, useNavigate } from "react-router-dom";
 import { useAppSelector } from "../../hooks";
-import { clickedTerm } from "../../reducers/term-click";
+import { click, clickedTerm } from "../../reducers/term-click";
+import store from "../../store";
+import "../article/article.css";
+import Discussion from "../discussion/Discussion";
 
 const { Text } = Typography;
 
@@ -24,21 +27,27 @@ interface IWidget {
 }
 const TermItemWidget = ({ data, onTermClick }: IWidget) => {
   const [openTermModal, setOpenTermModal] = useState(false);
+  const [showDiscussion, setShowDiscussion] = useState(false);
   const navigate = useNavigate();
   const termClicked = useAppSelector(clickedTerm);
 
   useEffect(() => {
     console.debug("on redux", termClicked, data);
+    if (typeof termClicked === "undefined") {
+      return;
+    }
     if (termClicked?.channelId === data?.channel?.id) {
+      store.dispatch(click(null));
       if (typeof onTermClick !== "undefined") {
         onTermClick(termClicked);
       }
     }
-  }, [data?.channel?.id, termClicked]);
+  }, [data, termClicked]);
 
   return (
     <>
       <Card
+        bodyStyle={{ padding: 8 }}
         title={
           <Space direction="vertical" size={3}>
             <Space>
@@ -98,7 +107,18 @@ const TermItemWidget = ({ data, onTermClick }: IWidget) => {
           </Dropdown>
         }
       >
-        <MdView html={data?.html} />
+        <div className="pcd_article">
+          <MdView html={data?.html} />
+        </div>
+        {showDiscussion ? (
+          <Discussion resType="term" resId={data?.guid} />
+        ) : (
+          <div style={{ textAlign: "right" }}>
+            <Button type="link" onClick={() => setShowDiscussion(true)}>
+              纠错
+            </Button>
+          </div>
+        )}
       </Card>
       <TermModal
         id={data?.guid}

+ 32 - 25
dashboard/src/components/term/TermSearch.tsx

@@ -1,5 +1,5 @@
 import { useState, useEffect } from "react";
-import { Col, List, Row, Typography } from "antd";
+import { Col, List, Row, Skeleton, Typography } from "antd";
 
 import { get } from "../../request";
 import {
@@ -24,14 +24,17 @@ const TermSearchWidget = ({
   onIdChange,
 }: IWidget) => {
   const [tableData, setTableData] = useState<ITermDataResponse[]>();
+  const [loading, setLoading] = useState(false);
 
   const loadById = (id: string) => {
     const url = `/v2/terms/${id}`;
     console.info("term url", url);
+    setLoading(true);
     get<ITermResponse>(url)
       .then((json) => {
         setTableData([json.data]);
       })
+      .finally(() => setLoading(false))
       .catch((error) => {
         console.error(error);
       });
@@ -40,16 +43,19 @@ const TermSearchWidget = ({
     if (typeof word === "undefined" && typeof wordId === "undefined") {
       return;
     }
-    if (word) {
+    if (word && word.length > 0) {
       const url = `/v2/terms?view=word&word=${word}`;
+      console.info("term url", url);
+      setLoading(true);
       get<ITermListResponse>(url)
         .then((json) => {
           setTableData(json.data.rows);
         })
+        .finally(() => setLoading(false))
         .catch((error) => {
           console.error(error);
         });
-    } else if (wordId) {
+    } else if (wordId && wordId.length > 0) {
       loadById(wordId);
     }
   }, [word, wordId]);
@@ -59,29 +65,30 @@ const TermSearchWidget = ({
       <Row>
         <Col flex="200px">{compact ? <></> : <></>}</Col>
         <Col flex="760px">
-          <Title>{word}</Title>
-          <List
-            itemLayout="vertical"
-            size="large"
-            dataSource={tableData}
-            renderItem={(item) => (
-              <List.Item>
-                <TermItem
-                  data={item}
-                  onTermClick={(value: ITerm) => {
-                    console.debug("on term click", value);
-                    if (typeof onIdChange === "undefined") {
-                      if (value.id) {
-                        loadById(value.id);
+          <Title level={4}>{word}</Title>
+          {loading ? (
+            <Skeleton active />
+          ) : (
+            <div>
+              {tableData?.map((item, id) => {
+                return (
+                  <TermItem
+                    data={item}
+                    onTermClick={(value: ITerm) => {
+                      console.debug("on term click", value);
+                      if (typeof onIdChange === "undefined") {
+                        if (value.id) {
+                          loadById(value.id);
+                        }
+                      } else {
+                        onIdChange(value.id);
                       }
-                    } else {
-                      onIdChange(value.id);
-                    }
-                  }}
-                />
-              </List.Item>
-            )}
-          />
+                    }}
+                  />
+                );
+              })}
+            </div>
+          )}
         </Col>
         <Col flex="200px"></Col>
       </Row>

+ 43 - 24
dashboard/src/components/term/TermShow.tsx

@@ -10,13 +10,17 @@ interface IWidget {
   word?: string;
   wordId?: string;
   compact?: boolean;
+  hideInput?: boolean;
   onSearch?: Function;
+  onIdChange?: Function;
 }
 const TermShowWidget = ({
   word,
   wordId,
   compact = false,
+  hideInput = false,
   onSearch,
+  onIdChange,
 }: IWidget) => {
   const [split, setSplit] = useState<string>();
   const [wordSearch, setWordSearch] = useState<string>();
@@ -35,34 +39,49 @@ const TermShowWidget = ({
   };
   return (
     <div ref={setContainer}>
-      <Affix offsetTop={0} target={compact ? () => container : undefined}>
-        <div
-          style={{
-            backgroundColor: "rgba(100,100,100,0.3)",
-            backdropFilter: "blur(5px)",
-          }}
-        >
-          <Row style={{ paddingTop: "0.5em", paddingBottom: "0.5em" }}>
-            {compact ? <></> : <Col flex="auto"></Col>}
-            <Col flex="560px">
-              <SearchVocabulary
-                value={word}
-                onSearch={dictSearch}
-                onSplit={(word: string | undefined) => {
-                  console.log("onSplit", word);
-                  setSplit(word);
-                }}
-              />
-            </Col>
-            {compact ? <></> : <Col flex="auto"></Col>}
-          </Row>
-        </div>
-      </Affix>
+      {hideInput ? (
+        <></>
+      ) : (
+        <Affix offsetTop={0} target={compact ? () => container : undefined}>
+          <div
+            style={{
+              backgroundColor: "rgba(100,100,100,0.3)",
+              backdropFilter: "blur(5px)",
+            }}
+          >
+            <Row style={{ paddingTop: "0.5em", paddingBottom: "0.5em" }}>
+              {compact ? <></> : <Col flex="auto"></Col>}
+              <Col flex="560px">
+                <SearchVocabulary
+                  value={word}
+                  onSearch={dictSearch}
+                  onSplit={(word: string | undefined) => {
+                    console.log("onSplit", word);
+                    setSplit(word);
+                  }}
+                />
+              </Col>
+              {compact ? <></> : <Col flex="auto"></Col>}
+            </Row>
+          </div>
+        </Affix>
+      )}
+
       <Content style={{ minHeight: 700 }}>
         <Row>
           {compact ? <></> : <Col flex="auto"></Col>}
           <Col flex="1260px">
-            <TermSearch word={wordSearch} wordId={wordId} compact={compact} />
+            <TermSearch
+              word={wordSearch}
+              wordId={wordId}
+              compact={compact}
+              onIdChange={(value: string) => {
+                console.debug("term onIdChange", value);
+                if (typeof onIdChange !== "undefined") {
+                  onIdChange(value);
+                }
+              }}
+            />
           </Col>
           {compact ? <></> : <Col flex="auto"></Col>}
         </Row>

+ 2 - 6
dashboard/src/components/transfer/TransferCreate.tsx

@@ -1,15 +1,11 @@
 import { ModalForm, ProForm } from "@ant-design/pro-components";
 import { Alert, Form, message, notification } from "antd";
-import { TResType } from "./TransferList";
 import { post } from "../../request";
-import {
-  ITransferCreateResponse,
-  ITransferRequest,
-  ITransferResponse,
-} from "../api/Transfer";
+import { ITransferCreateResponse, ITransferRequest } from "../api/Transfer";
 import { useIntl } from "react-intl";
 import UserSelect from "../template/UserSelect";
 import { useEffect, useState } from "react";
+import { TResType } from "../discussion/DiscussionListCard";
 
 interface IWidget {
   studioName?: string;

+ 1 - 2
dashboard/src/components/transfer/TransferList.tsx

@@ -17,11 +17,10 @@ import {
 } from "../api/Transfer";
 import { useIntl } from "react-intl";
 import { BaseType } from "antd/lib/typography/Base";
+import { TResType } from "../discussion/DiscussionListCard";
 
 const { Text } = Typography;
 
-export type TResType = "article" | "channel" | "chapter" | "sentence" | "wbw";
-
 interface ITransfer {
   id: string;
   origin_owner: IStudio;

+ 1 - 0
dashboard/src/locales/en-US/buttons.ts

@@ -78,6 +78,7 @@ const items = {
   "buttons.use.as.guest": "use as guest",
   "buttons.got.it": "I got it",
   "buttons.statistic": "statistic",
+  "buttons.relate": "relate",
 };
 
 export default items;

+ 1 - 0
dashboard/src/locales/zh-Hans/buttons.ts

@@ -78,6 +78,7 @@ const items = {
   "buttons.use.as.guest": "以访客身份继续使用",
   "buttons.got.it": "知道了",
   "buttons.statistic": "统计",
+  "buttons.relate": "关联",
 };
 
 export default items;

+ 3 - 3
dashboard/src/reducers/term-click.ts

@@ -4,7 +4,7 @@ import type { RootState } from "../store";
 import { ITerm } from "../components/term/TermEdit";
 
 interface IState {
-  word?: ITerm;
+  word?: ITerm | null;
 }
 
 const initialState: IState = {};
@@ -13,7 +13,7 @@ export const slice = createSlice({
   name: "term-change",
   initialState,
   reducers: {
-    click: (state, action: PayloadAction<ITerm>) => {
+    click: (state, action: PayloadAction<ITerm | null>) => {
       state.word = action.payload;
     },
   },
@@ -21,7 +21,7 @@ export const slice = createSlice({
 
 export const { click } = slice.actions;
 
-export const clickedTerm = (state: RootState): ITerm | undefined =>
+export const clickedTerm = (state: RootState): ITerm | null | undefined =>
   state.termClick.word;
 
 export default slice.reducer;