Explorar el Código

Merge pull request #1924 from visuddhinanda/agile

把工具菜单独立成组件
visuddhinanda hace 2 años
padre
commit
b75dfd0665

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

@@ -151,6 +151,7 @@ export interface ISentenceRequest {
   contentType?: TContentType;
   prEditor?: string;
   prId?: string;
+  prUuid?: string;
   prEditAt?: string;
   channels?: string;
 }

+ 1 - 0
dashboard/src/components/api/Suggestion.ts

@@ -3,6 +3,7 @@ import { IChannelApiData } from "./Channel";
 
 export interface ISuggestionData {
   id: string;
+  uid: string;
   book: number;
   paragraph: number;
   word_start: number;

+ 27 - 26
dashboard/src/components/article/ArticleEdit.tsx

@@ -9,7 +9,7 @@ import {
   ProFormTextArea,
 } from "@ant-design/pro-components";
 
-import { Alert, Button, Form, message, Result, Space } from "antd";
+import { Alert, Button, Form, message, Result } from "antd";
 
 import { get, put } from "../../request";
 import {
@@ -189,32 +189,33 @@ const ArticleEditWidget = ({
             })}
           />
         </ProForm.Group>
-        <ProForm.Group>
-          <Form.Item
-            name="content"
+
+        <Form.Item
+          name="content"
+          style={{ width: "100%" }}
+          label={
+            <>
+              {intl.formatMessage({
+                id: "forms.fields.content.label",
+              })}
+              {articleId ? (
+                <ArticlePrevDrawer
+                  trigger={<Button>预览</Button>}
+                  articleId={articleId}
+                  content={content}
+                />
+              ) : undefined}
+            </>
+          }
+        >
+          <MDEditor
+            onChange={(value) => setContent(value)}
+            height={450}
+            minHeight={200}
             style={{ width: "100%" }}
-            label={
-              <Space>
-                {intl.formatMessage({
-                  id: "forms.fields.content.label",
-                })}
-                {articleId ? (
-                  <ArticlePrevDrawer
-                    trigger={<Button>预览</Button>}
-                    articleId={articleId}
-                    content={content}
-                  />
-                ) : undefined}
-              </Space>
-            }
-          >
-            <MDEditor
-              onChange={(value) => setContent(value)}
-              height={550}
-              style={{ width: "100%" }}
-            />
-          </Form.Item>
-        </ProForm.Group>
+          />
+        </Form.Item>
+
         <ProForm.Group>
           <ProFormSwitch
             name="to_tpl"

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

@@ -1,5 +1,4 @@
-import { Typography, Divider, Button, Skeleton, Space } from "antd";
-import { ReloadOutlined } from "@ant-design/icons";
+import { Typography, Divider, Skeleton, Space } from "antd";
 
 import MdView from "../template/MdView";
 import TocPath, { ITocPathNode } from "../corpus/TocPath";
@@ -79,15 +78,6 @@ const ArticleViewWidget = ({
   }
   return (
     <>
-      <div style={{ textAlign: "right" }}>
-        <Button
-          type="link"
-          shape="round"
-          size="small"
-          icon={<ReloadOutlined />}
-        />
-      </div>
-
       <Space direction="vertical">
         <TocPath
           data={path}

+ 47 - 205
dashboard/src/components/article/TypeArticle.tsx

@@ -1,24 +1,10 @@
-import { useEffect, useState } from "react";
-import { Divider, message, Space, Tag } from "antd";
+import { useState } from "react";
+import { Button } from "antd";
 
-import { get } from "../../request";
-import {
-  IArticleDataResponse,
-  IArticleNavData,
-  IArticleNavResponse,
-  IArticleResponse,
-} from "../api/Article";
-import ArticleView, { IFirstAnthology } from "./ArticleView";
-import TocTree from "./TocTree";
-import PaliText from "../template/Wbw/PaliText";
-import { ITocPathNode } from "../corpus/TocPath";
+import { IArticleDataResponse } from "../api/Article";
 import { ArticleMode, ArticleType } from "./Article";
-import "./article.css";
-import ArticleSkeleton from "./ArticleSkeleton";
-import ErrorResult from "../general/ErrorResult";
-import AnthologiesAtArticle from "./AnthologiesAtArticle";
-import NavigateButton from "./NavigateButton";
-import InteractiveArea from "../discussion/InteractiveArea";
+import TypeArticleReader from "./TypeArticleReader";
+import ArticleEdit from "./ArticleEdit";
 
 interface IWidget {
   type?: ArticleType;
@@ -45,196 +31,52 @@ const TypeArticleWidget = ({
   onAnthologySelect,
 }: IWidget) => {
   const [articleData, setArticleData] = useState<IArticleDataResponse>();
-  const [articleHtml, setArticleHtml] = useState<string[]>(["<span />"]);
-  const [extra, setExtra] = useState(<></>);
-  const [loading, setLoading] = useState(false);
-  const [errorCode, setErrorCode] = useState<number>();
-  const [currPath, setCurrPath] = useState<ITocPathNode[]>();
-  const [nav, setNav] = useState<IArticleNavData>();
-
-  const channels = channelId?.split("_");
-
-  const srcDataMode = mode === "edit" || mode === "wbw" ? "edit" : "read";
-  useEffect(() => {
-    console.log("srcDataMode", srcDataMode);
-    if (!active) {
-      return;
-    }
-
-    if (typeof type === "undefined") {
-      return;
-    }
-
-    let url = `/v2/article/${articleId}?mode=${srcDataMode}`;
-    url += channelId ? `&channel=${channelId}` : "";
-    url += anthologyId ? `&anthology=${anthologyId}` : "";
-    console.info("article url", url);
-    setLoading(true);
-    get<IArticleResponse>(url)
-      .then((json) => {
-        console.log("article", json);
-        if (json.ok) {
-          setArticleData(json.data);
-          setCurrPath(json.data.path);
-          if (json.data.html) {
-            setArticleHtml([json.data.html]);
-          } else if (json.data.content) {
-            setArticleHtml([json.data.content]);
-          } else {
-            setArticleHtml([""]);
-          }
-          setExtra(
-            <TocTree
-              treeData={json.data.toc?.map((item) => {
-                const strTitle = item.title ? item.title : item.pali_title;
-                const key = item.key
-                  ? item.key
-                  : `${item.book}-${item.paragraph}`;
-                const progress = item.progress?.map((item, id) => (
-                  <Tag key={id}>{Math.round(item * 100) + "%"}</Tag>
-                ));
-                return {
-                  key: key,
-                  title: (
-                    <Space>
-                      <PaliText
-                        text={strTitle === "" ? "[unnamed]" : strTitle}
-                      />
-                      {progress}
-                    </Space>
-                  ),
-                  level: item.level,
-                };
-              })}
-              onClick={(
-                id: string,
-                e: React.MouseEvent<HTMLSpanElement, MouseEvent>
-              ) => {
-                const target = e.ctrlKey || e.metaKey ? "_blank" : "self";
-                if (typeof onArticleChange !== "undefined") {
-                  onArticleChange("article", id, target);
-                }
-              }}
-            />
-          );
-
-          if (typeof onLoad !== "undefined") {
-            onLoad(json.data);
-          }
-        } else {
-          console.error("json", json);
-          message.error(json.message);
-        }
-      })
-      .finally(() => {
-        setLoading(false);
-      })
-      .catch((e) => {
-        console.error(e);
-        setErrorCode(e);
-      });
-  }, [active, type, articleId, srcDataMode, channelId, anthologyId]);
-
-  useEffect(() => {
-    const url = `/v2/nav-article/${articleId}_${anthologyId}`;
-    get<IArticleNavResponse>(url)
-      .then((json) => {
-        if (json.ok) {
-          setNav(json.data);
-        }
-      })
-      .catch((e) => {
-        console.error(e);
-      });
-  }, [anthologyId, articleId]);
-
-  let anthology: IFirstAnthology | undefined;
-  if (articleData?.anthology_count && articleData.anthology_first) {
-    anthology = {
-      id: articleData.anthology_first.uid,
-      title: articleData.anthology_first.title,
-      count: articleData?.anthology_count,
-    };
-  }
-
+  const [edit, setEdit] = useState(false);
   return (
     <div>
-      {loading ? (
-        <ArticleSkeleton />
-      ) : errorCode ? (
-        <ErrorResult code={errorCode} />
+      <div>
+        {articleData?.role && articleData?.role !== "reader" && edit ? (
+          <Button onClick={() => setEdit(!edit)}>{"完成"}</Button>
+        ) : (
+          <></>
+        )}
+      </div>
+      {edit ? (
+        <ArticleEdit
+          anthologyId={anthologyId ? anthologyId : undefined}
+          articleId={articleId}
+        />
       ) : (
-        <>
-          <AnthologiesAtArticle
-            articleId={articleId}
-            anthologyId={anthologyId}
-            onClick={(
-              id: string,
-              e: React.MouseEvent<HTMLElement, MouseEvent>
-            ) => {
-              if (typeof onAnthologySelect !== "undefined") {
-                onAnthologySelect(id, e);
-              }
-            }}
-          />
-          <ArticleView
-            id={articleData?.uid}
-            title={
-              articleData?.title_text
-                ? articleData?.title_text
-                : articleData?.title
+        <TypeArticleReader
+          type={type}
+          channelId={channelId}
+          articleId={articleId}
+          anthologyId={anthologyId}
+          mode={mode}
+          active={active}
+          onArticleChange={(type: string, id: string, target: string) => {
+            if (typeof onArticleChange !== "undefined") {
+              onArticleChange(type, id, target);
             }
-            subTitle={articleData?.subtitle}
-            summary={articleData?.summary}
-            content={articleData ? articleData.content : ""}
-            html={articleHtml}
-            path={currPath}
-            created_at={articleData?.created_at}
-            updated_at={articleData?.updated_at}
-            channels={channels}
-            type={type}
-            articleId={articleId}
-            anthology={anthology}
-            onPathChange={(
-              node: ITocPathNode,
-              e: React.MouseEvent<
-                HTMLSpanElement | HTMLAnchorElement,
-                MouseEvent
-              >
-            ) => {
-              let newType = type;
-              if (node.level === 0) {
-                newType = "anthology";
-              } else {
-                newType = "article";
-              }
-              if (typeof onArticleChange !== "undefined") {
-                const newArticleId = node.key;
-                const target = e.ctrlKey || e.metaKey ? "_blank" : "self";
-                onArticleChange(newType, newArticleId, target);
-              }
-            }}
-          />
-          <Divider />
-          {extra}
-          <Divider />
-          <NavigateButton
-            prevTitle={nav?.prev?.title}
-            nextTitle={nav?.next?.title}
-            onNext={() => {
-              if (typeof onArticleChange !== "undefined") {
-                onArticleChange("article", nav?.next?.article_id);
-              }
-            }}
-            onPrev={() => {
-              if (typeof onArticleChange !== "undefined") {
-                onArticleChange("article", nav?.prev?.article_id);
-              }
-            }}
-          />
-
-          <InteractiveArea resType={"article"} resId={articleId} />
-        </>
+          }}
+          onLoad={(data: IArticleDataResponse) => {
+            setArticleData(data);
+            if (typeof onLoad !== "undefined") {
+              onLoad(data);
+            }
+          }}
+          onAnthologySelect={(
+            id: string,
+            e: React.MouseEvent<HTMLElement, MouseEvent>
+          ) => {
+            if (typeof onAnthologySelect !== "undefined") {
+              onAnthologySelect(id, e);
+            }
+          }}
+          onEdit={() => {
+            setEdit(true);
+          }}
+        />
       )}
     </div>
   );

+ 252 - 0
dashboard/src/components/article/TypeArticleReader.tsx

@@ -0,0 +1,252 @@
+import { useEffect, useState } from "react";
+import { Divider, message, Space, Tag } from "antd";
+
+import { get } from "../../request";
+import {
+  IArticleDataResponse,
+  IArticleNavData,
+  IArticleNavResponse,
+  IArticleResponse,
+} from "../api/Article";
+import ArticleView, { IFirstAnthology } from "./ArticleView";
+import TocTree from "./TocTree";
+import PaliText from "../template/Wbw/PaliText";
+import { ITocPathNode } from "../corpus/TocPath";
+import { ArticleMode, ArticleType } from "./Article";
+import "./article.css";
+import ArticleSkeleton from "./ArticleSkeleton";
+import ErrorResult from "../general/ErrorResult";
+import NavigateButton from "./NavigateButton";
+import InteractiveArea from "../discussion/InteractiveArea";
+import TypeArticleReaderToolbar from "./TypeArticleReaderToolbar";
+
+interface IWidget {
+  type?: ArticleType;
+  articleId?: string;
+  mode?: ArticleMode | null;
+  channelId?: string | null;
+  anthologyId?: string | null;
+  active?: boolean;
+  onArticleChange?: Function;
+  onFinal?: Function;
+  onLoad?: Function;
+  onAnthologySelect?: Function;
+  onEdit?: Function;
+}
+const TypeArticleReaderWidget = ({
+  type,
+  channelId,
+  articleId,
+  anthologyId,
+  mode = "read",
+  active = false,
+  onArticleChange,
+  onFinal,
+  onLoad,
+  onAnthologySelect,
+  onEdit,
+}: IWidget) => {
+  const [articleData, setArticleData] = useState<IArticleDataResponse>();
+  const [articleHtml, setArticleHtml] = useState<string[]>(["<span />"]);
+  const [extra, setExtra] = useState(<></>);
+  const [loading, setLoading] = useState(false);
+  const [errorCode, setErrorCode] = useState<number>();
+  const [currPath, setCurrPath] = useState<ITocPathNode[]>();
+  const [nav, setNav] = useState<IArticleNavData>();
+
+  const channels = channelId?.split("_");
+
+  const srcDataMode = mode === "edit" || mode === "wbw" ? "edit" : "read";
+  useEffect(() => {
+    console.log("srcDataMode", srcDataMode);
+    if (!active) {
+      return;
+    }
+
+    if (typeof type === "undefined") {
+      return;
+    }
+
+    let url = `/v2/article/${articleId}?mode=${srcDataMode}`;
+    url += channelId ? `&channel=${channelId}` : "";
+    url += anthologyId ? `&anthology=${anthologyId}` : "";
+    console.info("article url", url);
+    setLoading(true);
+    get<IArticleResponse>(url)
+      .then((json) => {
+        console.log("article", json);
+        if (json.ok) {
+          setArticleData(json.data);
+          setCurrPath(json.data.path);
+          if (json.data.html) {
+            setArticleHtml([json.data.html]);
+          } else if (json.data.content) {
+            setArticleHtml([json.data.content]);
+          } else {
+            setArticleHtml([""]);
+          }
+          setExtra(
+            <TocTree
+              treeData={json.data.toc?.map((item) => {
+                const strTitle = item.title ? item.title : item.pali_title;
+                const key = item.key
+                  ? item.key
+                  : `${item.book}-${item.paragraph}`;
+                const progress = item.progress?.map((item, id) => (
+                  <Tag key={id}>{Math.round(item * 100) + "%"}</Tag>
+                ));
+                return {
+                  key: key,
+                  title: (
+                    <Space>
+                      <PaliText
+                        text={strTitle === "" ? "[unnamed]" : strTitle}
+                      />
+                      {progress}
+                    </Space>
+                  ),
+                  level: item.level,
+                };
+              })}
+              onClick={(
+                id: string,
+                e: React.MouseEvent<HTMLSpanElement, MouseEvent>
+              ) => {
+                const target = e.ctrlKey || e.metaKey ? "_blank" : "self";
+                if (typeof onArticleChange !== "undefined") {
+                  onArticleChange("article", id, target);
+                }
+              }}
+            />
+          );
+
+          if (typeof onLoad !== "undefined") {
+            onLoad(json.data);
+          }
+        } else {
+          console.error("json", json);
+          message.error(json.message);
+        }
+      })
+      .finally(() => {
+        setLoading(false);
+      })
+      .catch((e) => {
+        console.error(e);
+        setErrorCode(e);
+      });
+  }, [active, type, articleId, srcDataMode, channelId, anthologyId]);
+
+  useEffect(() => {
+    const url = `/v2/nav-article/${articleId}_${anthologyId}`;
+    get<IArticleNavResponse>(url)
+      .then((json) => {
+        if (json.ok) {
+          setNav(json.data);
+        }
+      })
+      .catch((e) => {
+        console.error(e);
+      });
+  }, [anthologyId, articleId]);
+
+  let anthology: IFirstAnthology | undefined;
+  if (articleData?.anthology_count && articleData.anthology_first) {
+    anthology = {
+      id: articleData.anthology_first.uid,
+      title: articleData.anthology_first.title,
+      count: articleData?.anthology_count,
+    };
+  }
+
+  const title = articleData?.title_text
+    ? articleData?.title_text
+    : articleData?.title;
+
+  return (
+    <div>
+      {loading ? (
+        <ArticleSkeleton />
+      ) : errorCode ? (
+        <ErrorResult code={errorCode} />
+      ) : (
+        <>
+          <TypeArticleReaderToolbar
+            title={title}
+            articleId={articleId}
+            anthologyId={anthologyId}
+            role={articleData?.role}
+            onEdit={() => {
+              if (typeof onEdit !== "undefined") {
+                onEdit();
+              }
+            }}
+            onAnthologySelect={(
+              id: string,
+              e: React.MouseEvent<HTMLElement, MouseEvent>
+            ) => {
+              if (typeof onAnthologySelect !== "undefined") {
+                onAnthologySelect(id, e);
+              }
+            }}
+          />
+          <ArticleView
+            id={articleData?.uid}
+            title={title}
+            subTitle={articleData?.subtitle}
+            summary={articleData?.summary}
+            content={articleData ? articleData.content : ""}
+            html={articleHtml}
+            path={currPath}
+            created_at={articleData?.created_at}
+            updated_at={articleData?.updated_at}
+            channels={channels}
+            type={type}
+            articleId={articleId}
+            anthology={anthology}
+            onPathChange={(
+              node: ITocPathNode,
+              e: React.MouseEvent<
+                HTMLSpanElement | HTMLAnchorElement,
+                MouseEvent
+              >
+            ) => {
+              let newType = type;
+              if (node.level === 0) {
+                newType = "anthology";
+              } else {
+                newType = "article";
+              }
+              if (typeof onArticleChange !== "undefined") {
+                const newArticleId = node.key;
+                const target = e.ctrlKey || e.metaKey ? "_blank" : "self";
+                onArticleChange(newType, newArticleId, target);
+              }
+            }}
+          />
+          <Divider />
+          {extra}
+          <Divider />
+          <NavigateButton
+            prevTitle={nav?.prev?.title}
+            nextTitle={nav?.next?.title}
+            onNext={() => {
+              if (typeof onArticleChange !== "undefined") {
+                onArticleChange("article", nav?.next?.article_id);
+              }
+            }}
+            onPrev={() => {
+              if (typeof onArticleChange !== "undefined") {
+                onArticleChange("article", nav?.prev?.article_id);
+              }
+            }}
+          />
+
+          <InteractiveArea resType={"article"} resId={articleId} />
+        </>
+      )}
+    </div>
+  );
+};
+
+export default TypeArticleReaderWidget;

+ 152 - 0
dashboard/src/components/article/TypeArticleReaderToolbar.tsx

@@ -0,0 +1,152 @@
+import { Button, Dropdown } from "antd";
+import {
+  ReloadOutlined,
+  MoreOutlined,
+  InboxOutlined,
+  EditOutlined,
+  FileOutlined,
+  CopyOutlined,
+} from "@ant-design/icons";
+
+import { useAppSelector } from "../../hooks";
+import { currentUser } from "../../reducers/current-user";
+import AddToAnthology from "./AddToAnthology";
+import { useState } from "react";
+import { fullUrl } from "../../utils";
+import { ArticleTplModal } from "../template/Builder/ArticleTpl";
+import AnthologiesAtArticle from "./AnthologiesAtArticle";
+import { TRole } from "../api/Auth";
+
+interface IWidget {
+  articleId?: string;
+  anthologyId?: string | null;
+  title?: string;
+  role?: TRole;
+  onEdit?: Function;
+  onAnthologySelect?: Function;
+}
+const TypeArticleReaderToolbarWidget = ({
+  articleId,
+  anthologyId,
+  title,
+  role = "reader",
+  onEdit,
+  onAnthologySelect,
+}: IWidget) => {
+  const user = useAppSelector(currentUser);
+  const [addToAnthologyOpen, setAddToAnthologyOpen] = useState(false);
+  const [tplOpen, setTplOpen] = useState(false);
+
+  const editable = role === "owner" || role === "manager" || role === "editor";
+
+  return (
+    <div>
+      <div
+        style={{ padding: 4, display: "flex", justifyContent: "space-between" }}
+      >
+        <div>
+          <AnthologiesAtArticle
+            articleId={articleId}
+            anthologyId={anthologyId}
+            onClick={(
+              id: string,
+              e: React.MouseEvent<HTMLElement, MouseEvent>
+            ) => {
+              if (typeof onAnthologySelect !== "undefined") {
+                onAnthologySelect(id, e);
+              }
+            }}
+          />
+        </div>
+        <div>
+          <Button
+            type="link"
+            shape="round"
+            size="small"
+            icon={<ReloadOutlined />}
+          />
+          <Dropdown
+            menu={{
+              items: [
+                {
+                  label: "添加到文集",
+                  key: "add_to_anthology",
+                  icon: <InboxOutlined />,
+                  disabled: user ? false : true,
+                },
+                {
+                  label: "编辑",
+                  key: "edit",
+                  icon: <EditOutlined />,
+                  disabled: !editable,
+                },
+                {
+                  label: "在Studio中打开",
+                  key: "open-studio",
+                  icon: <EditOutlined />,
+                  disabled: user ? false : true,
+                },
+                {
+                  label: "获取文章引用模版",
+                  key: "tpl",
+                  icon: <FileOutlined />,
+                },
+                {
+                  label: "创建副本",
+                  key: "fork",
+                  icon: <CopyOutlined />,
+                  disabled: user ? false : true,
+                },
+              ],
+              onClick: ({ key }) => {
+                console.log(`Click on item ${key}`);
+                switch (key) {
+                  case "add_to_anthology":
+                    setAddToAnthologyOpen(true);
+                    break;
+                  case "fork":
+                    const url = `/studio/${user?.nickName}/article/create?parent=${articleId}`;
+                    window.open(fullUrl(url), "_blank");
+                    break;
+                  case "tpl":
+                    setTplOpen(true);
+                    break;
+                  case "edit":
+                    if (typeof onEdit !== "undefined") {
+                      onEdit();
+                    }
+                    break;
+                }
+              },
+            }}
+            placement="bottomRight"
+          >
+            <Button
+              onClick={(e) => e.preventDefault()}
+              icon={<MoreOutlined />}
+              size="small"
+              type="link"
+            />
+          </Dropdown>
+        </div>
+      </div>
+      {articleId ? (
+        <AddToAnthology
+          open={addToAnthologyOpen}
+          onClose={(isOpen: boolean) => setAddToAnthologyOpen(isOpen)}
+          articleIds={[articleId]}
+        />
+      ) : undefined}
+
+      <ArticleTplModal
+        title={title}
+        type="article"
+        id={articleId}
+        open={tplOpen}
+        onOpenChange={(visible: boolean) => setTplOpen(visible)}
+      />
+    </div>
+  );
+};
+
+export default TypeArticleReaderToolbarWidget;

+ 1 - 0
dashboard/src/components/auth/setting/SettingArticle.tsx

@@ -36,6 +36,7 @@ const SettingArticleWidget = () => {
           id: `buttons.wbw`,
         })}
       </Divider>
+      <SettingItem data={SettingFind("setting.wbw.order", settings)} />
       <Divider>Nissaya</Divider>
       <SettingItem
         data={SettingFind("setting.nissaya.layout.read", settings)}

+ 8 - 0
dashboard/src/components/auth/setting/default.ts

@@ -224,4 +224,12 @@ export const defaultSetting: ISetting[] = [
     ],
     widget: "radio-button",
   },
+  {
+    /**
+     * 是否显示逐词解析输入顺序提示
+     */
+    key: "setting.wbw.order",
+    label: "setting.wbw.order.label",
+    defaultValue: false,
+  },
 ];

+ 13 - 4
dashboard/src/components/corpus/SentHistory.tsx

@@ -100,7 +100,7 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
         title: {
           render: (text, row, index, action) => {
             return (
-              <Paragraph copyable={{ text: row.content }}>
+              <Paragraph style={{ margin: 0 }} copyable={{ text: row.content }}>
                 {row.content}
               </Paragraph>
             );
@@ -117,10 +117,15 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
           render: (text, row, index, action) => {
             return (
               <Space style={{ fontSize: "80%" }}>
+                <User {...row.editor} showAvatar={false} />
+                <>{"edited"}</>
+
                 {row.accepter ? (
-                  <User {...row.accepter} showAvatar={false} />
+                  <>
+                    <User {...row.accepter} showAvatar={false} /> {"accept"}
+                  </>
                 ) : (
-                  <User {...row.editor} showAvatar={false} />
+                  <></>
                 )}
 
                 {row.fork_from ? (
@@ -131,7 +136,11 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
                 ) : (
                   <></>
                 )}
-                <TimeShow type="secondary" createdAt={row.createdAt} />
+                <TimeShow
+                  type="secondary"
+                  createdAt={row.createdAt}
+                  showLabel={false}
+                />
               </Space>
             );
           },

+ 11 - 1
dashboard/src/components/template/Builder/ArticleTpl.tsx

@@ -200,20 +200,26 @@ const ArticleTplWidget = ({
 };
 
 interface IModalWidget {
+  open?: boolean;
   type?: ArticleType;
   id?: string;
   title?: string;
   style?: TDisplayStyle;
   trigger?: JSX.Element;
+  onOpenChange?: Function;
 }
 export const ArticleTplModal = ({
+  open = false,
   type,
   id,
   title,
   style = "modal",
   trigger,
+  onOpenChange,
 }: IModalWidget) => {
-  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isModalOpen, setIsModalOpen] = useState(open);
+
+  useEffect(() => setIsModalOpen(open), [open]);
 
   const showModal = () => {
     setIsModalOpen(true);
@@ -225,6 +231,9 @@ export const ArticleTplModal = ({
 
   const handleCancel = () => {
     setIsModalOpen(false);
+    if (typeof onOpenChange !== "undefined") {
+      onOpenChange(false);
+    }
   };
 
   return (
@@ -236,6 +245,7 @@ export const ArticleTplModal = ({
         open={isModalOpen}
         onOk={handleOk}
         onCancel={handleCancel}
+        destroyOnClose
       >
         <ArticleTplWidget type={type} id={id} title={title} style={style} />
       </Modal>

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

@@ -29,6 +29,7 @@ export interface ISuggestionCount {
 }
 export interface ISentence {
   id?: string;
+  uid?: string;
   content: string | null;
   contentType?: TContentType;
   html: string;

+ 6 - 3
dashboard/src/components/template/SentEdit/EditInfo.tsx

@@ -46,15 +46,18 @@ const Fork = ({ sentId, highlight = false }: IFork) => {
           dataSource={data}
           renderItem={(item) => (
             <List.Item>
-              <Text>
-                {item.fork_studio?.nickName}-{item.fork_from?.name}
-              </Text>
               <Text type="secondary" style={{ fontSize: "85%" }}>
                 <Space>
                   <User {...item.accepter} showAvatar={false} />
+                  {"fork from"}
+                  <Text>
+                    {item.fork_studio?.nickName}-{item.fork_from?.name}
+                  </Text>
+                  {"on"}
                   <TimeShow
                     type="secondary"
                     title="复制"
+                    showLabel={false}
                     createdAt={item.created_at}
                   />
                 </Space>

+ 15 - 14
dashboard/src/components/template/SentEdit/PrAcceptButton.tsx

@@ -20,20 +20,21 @@ const PrAcceptButtonWidget = ({ data, onAccept }: IWidget) => {
 
   const save = () => {
     setSaving(true);
-    put<ISentenceRequest, ISentenceResponse>(
-      `/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`,
-      {
-        book: data.book,
-        para: data.para,
-        wordStart: data.wordStart,
-        wordEnd: data.wordEnd,
-        channel: data.channel.id,
-        content: data.content,
-        prEditor: data.editor.id,
-        prId: data.id,
-        prEditAt: data.updateAt,
-      }
-    )
+    const url = `/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`;
+    const prData = {
+      book: data.book,
+      para: data.para,
+      wordStart: data.wordStart,
+      wordEnd: data.wordEnd,
+      channel: data.channel.id,
+      content: data.content,
+      prEditor: data.editor.id,
+      prId: data.id,
+      prUuid: data.uid,
+      prEditAt: data.updateAt,
+    };
+    console.debug("pr accept url", url, prData);
+    put<ISentenceRequest, ISentenceResponse>(url, prData)
       .then((json) => {
         console.log(json);
         setSaving(false);

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

@@ -47,6 +47,7 @@ const SuggestionListWidget = ({
           const newData: ISentence[] = json.data.rows.map((item) => {
             return {
               id: item.id,
+              uid: item.uid,
               content: item.content,
               html: item.html,
               book: item.book,

+ 113 - 71
dashboard/src/components/template/Wbw/WbwDetailBasic.tsx

@@ -13,6 +13,7 @@ import WbwDetailFactor from "./WbwDetailFactor";
 import WbwDetailBasicRelation from "./WbwDetailBasicRelation";
 import WbwDetailParent from "./WbwDetailParent";
 import WbwDetailCase from "./WbwDetailCase";
+import WbwDetailOrder from "./WbwDetailOrder";
 
 const { Panel } = Collapse;
 
@@ -43,6 +44,8 @@ const WbwDetailBasicWidget = ({
   );
   const [openCreate, setOpenCreate] = useState(false);
   const [_meaning, setMeaning] = useState<string | undefined>();
+  const [currTip, setCurrTip] = useState(1);
+
   useEffect(() => {
     if (typeof data.meaning?.value === "string") {
       setMeaning(data.meaning?.value);
@@ -80,39 +83,50 @@ const WbwDetailBasicWidget = ({
           tooltip={intl.formatMessage({ id: "forms.fields.meaning.tooltip" })}
         >
           <div style={{ display: "flex" }}>
-            <Input
-              value={_meaning}
-              allowClear
-              onChange={(e) => {
-                console.log("meaning input", e.target.value);
-                setMeaning(e.target.value);
-                onMeaningChange(e.target.value);
-              }}
-            />
-            <Popover
-              content={
-                <WbwMeaningSelect
-                  data={data}
-                  onSelect={(meaning: string) => {
-                    console.log(meaning);
-                    setMeaning(meaning);
-                    form.setFieldsValue({
-                      meaning: meaning,
-                    });
-                    onMeaningChange(meaning);
-                  }}
+            <div style={{ display: "flex", width: "100%" }}>
+              <Input
+                value={_meaning}
+                allowClear
+                onChange={(e) => {
+                  console.log("meaning input", e.target.value);
+                  setMeaning(e.target.value);
+                  onMeaningChange(e.target.value);
+                }}
+              />
+              <Popover
+                content={
+                  <WbwMeaningSelect
+                    data={data}
+                    onSelect={(meaning: string) => {
+                      console.log(meaning);
+                      setMeaning(meaning);
+                      form.setFieldsValue({
+                        meaning: meaning,
+                      });
+                      onMeaningChange(meaning);
+                    }}
+                  />
+                }
+                overlayStyle={{ width: 500 }}
+                placement="bottom"
+                trigger="click"
+                open={openCreate}
+                onOpenChange={(open: boolean) => {
+                  setOpenCreate(open);
+                }}
+              >
+                <Button
+                  type="text"
+                  icon={<MoreOutlined />}
+                  onClick={() => {}}
                 />
-              }
-              overlayStyle={{ width: 500 }}
-              placement="bottom"
-              trigger="click"
-              open={openCreate}
-              onOpenChange={(open: boolean) => {
-                setOpenCreate(open);
-              }}
-            >
-              <Button type="text" icon={<MoreOutlined />} onClick={() => {}} />
-            </Popover>
+              </Popover>
+            </div>
+            <WbwDetailOrder
+              sn={3}
+              curr={currTip}
+              onChange={() => setCurrTip(currTip + 1)}
+            />
           </div>
         </Form.Item>
         <Form.Item
@@ -121,15 +135,22 @@ const WbwDetailBasicWidget = ({
           label={intl.formatMessage({ id: "forms.fields.factors.label" })}
           tooltip={intl.formatMessage({ id: "forms.fields.factors.tooltip" })}
         >
-          <WbwDetailFactor
-            data={data}
-            onChange={(value: string) => {
-              setFactors(value.split("+"));
-              if (typeof onChange !== "undefined") {
-                onChange({ field: "factors", value: value });
-              }
-            }}
-          />
+          <div style={{ display: "flex" }}>
+            <WbwDetailFactor
+              data={data}
+              onChange={(value: string) => {
+                setFactors(value.split("+"));
+                if (typeof onChange !== "undefined") {
+                  onChange({ field: "factors", value: value });
+                }
+              }}
+            />
+            <WbwDetailOrder
+              sn={4}
+              curr={currTip}
+              onChange={() => setCurrTip(currTip + 1)}
+            />
+          </div>
         </Form.Item>
         <Form.Item
           style={{ marginBottom: 6 }}
@@ -141,20 +162,27 @@ const WbwDetailBasicWidget = ({
             id: "forms.fields.factor.meaning.tooltip",
           })}
         >
-          <WbwDetailFm
-            factors={factors}
-            initValue={data.factorMeaning?.value?.split("+")}
-            onChange={(value: string[]) => {
-              console.log("fm change", value);
-              if (typeof onChange !== "undefined") {
-                onChange({ field: "factorMeaning", value: value.join("+") });
-              }
-            }}
-            onJoin={(value: string) => {
-              setMeaning(value);
-              onMeaningChange(value);
-            }}
-          />
+          <div style={{ display: "flex" }}>
+            <WbwDetailFm
+              factors={factors}
+              initValue={data.factorMeaning?.value?.split("+")}
+              onChange={(value: string[]) => {
+                console.log("fm change", value);
+                if (typeof onChange !== "undefined") {
+                  onChange({ field: "factorMeaning", value: value.join("+") });
+                }
+              }}
+              onJoin={(value: string) => {
+                setMeaning(value);
+                onMeaningChange(value);
+              }}
+            />
+            <WbwDetailOrder
+              sn={5}
+              curr={currTip}
+              onChange={() => setCurrTip(currTip + 1)}
+            />
+          </div>
         </Form.Item>
         <Form.Item
           style={{ marginBottom: 6 }}
@@ -162,14 +190,21 @@ const WbwDetailBasicWidget = ({
           tooltip={intl.formatMessage({ id: "forms.fields.case.tooltip" })}
           name="case"
         >
-          <WbwDetailCase
-            data={data}
-            onChange={(value: string) => {
-              if (typeof onChange !== "undefined") {
-                onChange({ field: "case", value: value });
-              }
-            }}
-          />
+          <div style={{ display: "flex" }}>
+            <WbwDetailCase
+              data={data}
+              onChange={(value: string) => {
+                if (typeof onChange !== "undefined") {
+                  onChange({ field: "case", value: value });
+                }
+              }}
+            />
+            <WbwDetailOrder
+              sn={2}
+              curr={currTip}
+              onChange={() => setCurrTip(currTip + 1)}
+            />
+          </div>
         </Form.Item>
         <Form.Item
           style={{ marginBottom: 6 }}
@@ -181,14 +216,21 @@ const WbwDetailBasicWidget = ({
             id: "forms.fields.parent.tooltip",
           })}
         >
-          <WbwDetailParent
-            data={data}
-            onChange={(value: string) => {
-              if (typeof onChange !== "undefined") {
-                onChange({ field: "parent", value: value });
-              }
-            }}
-          />
+          <div style={{ display: "flex" }}>
+            <WbwDetailParent
+              data={data}
+              onChange={(value: string) => {
+                if (typeof onChange !== "undefined") {
+                  onChange({ field: "parent", value: value });
+                }
+              }}
+            />
+            <WbwDetailOrder
+              sn={1}
+              curr={currTip}
+              onChange={() => setCurrTip(currTip + 1)}
+            />
+          </div>
         </Form.Item>
         <Collapse bordered={false}>
           <Panel header="词源" key="parent2">

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

@@ -20,7 +20,7 @@ const WbwDetailCaseWidget = ({ data, onChange }: IWidget) => {
   const intl = useIntl();
 
   return (
-    <div style={{ display: "flex" }}>
+    <div style={{ display: "flex", width: "100%" }}>
       <SelectCase
         value={data.case?.value}
         onCaseChange={(value: string) => {

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

@@ -205,7 +205,7 @@ const WbwDetailFmWidget = ({
 
   return (
     <div>
-      <div style={{ display: "flex" }}>
+      <div style={{ display: "flex", width: "100%" }}>
         <Input
           key="input"
           allowClear

+ 53 - 0
dashboard/src/components/template/Wbw/WbwDetailOrder.tsx

@@ -0,0 +1,53 @@
+import { Button, Tooltip } from "antd";
+import { useEffect, useState } from "react";
+import { useAppSelector } from "../../../hooks";
+import { settingInfo } from "../../../reducers/setting";
+import { GetUserSetting } from "../../auth/setting/default";
+
+interface IWidget {
+  sn: number;
+  curr: number;
+  onChange?: Function;
+}
+
+const WbwDetailOrderWidget = ({ sn, curr, onChange }: IWidget) => {
+  const [show, setShow] = useState(true);
+  const [enable, setEnable] = useState(false);
+  const settings = useAppSelector(settingInfo);
+
+  useEffect(() => {
+    const showOrder = GetUserSetting("setting.wbw.order", settings);
+    if (typeof showOrder === "boolean") {
+      setEnable(showOrder);
+    }
+  }, [settings]);
+
+  useEffect(() => {
+    setShow(sn === curr);
+  }, [curr, sn]);
+  return enable ? (
+    <Tooltip
+      open={show}
+      placement="right"
+      title={
+        <Button
+          type="link"
+          size="small"
+          onClick={() => {
+            if (typeof onChange !== "undefined") {
+              onChange();
+            }
+          }}
+        >
+          {curr === 5 ? "完成" : "下一步"}
+        </Button>
+      }
+    >
+      <span style={{ display: "inline-box", width: 1, height: 30 }}></span>
+    </Tooltip>
+  ) : (
+    <></>
+  );
+};
+
+export default WbwDetailOrderWidget;

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

@@ -32,6 +32,7 @@ const items = {
   "setting.nissaya.layout.edit.label": "layout(edit)",
   "setting.nissaya.layout.inline.label": "inline",
   "setting.nissaya.layout.list.label": "list",
+  "setting.wbw.order.label": "wbw input order",
 };
 
 export default items;

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

@@ -32,6 +32,7 @@ const items = {
   "setting.nissaya.layout.edit.label": "阅读排版方式",
   "setting.nissaya.layout.inline.label": "连续",
   "setting.nissaya.layout.list.label": "列表",
+  "setting.wbw.order.label": "显示输入顺序提示",
 };
 
 export default items;