Kaynağa Gözat

Merge pull request #1316 from visuddhinanda/agile

显示文章在文集中的路径
visuddhinanda 2 yıl önce
ebeveyn
işleme
b1c2893986
53 değiştirilmiş dosya ile 1813 ekleme ve 979 silme
  1. 1 1
      dashboard/src/Router.tsx
  2. 11 0
      dashboard/src/assets/icon/index.tsx
  3. 2 1
      dashboard/src/components/anthology/AnthologyList.tsx
  4. 11 9
      dashboard/src/components/anthology/AnthologyTocTree.tsx
  5. 113 29
      dashboard/src/components/anthology/EditableTocTree.tsx
  6. 2 1
      dashboard/src/components/api/Article.ts
  7. 2 6
      dashboard/src/components/article/AnthologyDetail.tsx
  8. 41 10
      dashboard/src/components/article/Article.tsx
  9. 208 0
      dashboard/src/components/article/ArticleEdit.tsx
  10. 89 0
      dashboard/src/components/article/ArticleEditDrawer.tsx
  11. 53 0
      dashboard/src/components/article/ArticleEditTools.tsx
  12. 43 5
      dashboard/src/components/article/ArticleView.tsx
  13. 90 17
      dashboard/src/components/article/EditableTree.tsx
  14. 78 0
      dashboard/src/components/article/EditableTreeNode.tsx
  15. 2 5
      dashboard/src/components/article/ToolButtonNav.tsx
  16. 22 3
      dashboard/src/components/article/ToolButtonToc.tsx
  17. 26 20
      dashboard/src/components/auth/StudioCard.tsx
  18. 5 3
      dashboard/src/components/auth/StudioName.tsx
  19. 54 56
      dashboard/src/components/corpus/ChapterCard.tsx
  20. 15 5
      dashboard/src/components/corpus/PaliChapterHead.tsx
  21. 27 1
      dashboard/src/components/corpus/RelatedPara.tsx
  22. 52 51
      dashboard/src/components/corpus/TocPath.tsx
  23. 1 1
      dashboard/src/components/course/TextBook.tsx
  24. 6 5
      dashboard/src/components/discussion/DiscussionBox.tsx
  25. 56 66
      dashboard/src/components/discussion/DiscussionCreate.tsx
  26. 93 66
      dashboard/src/components/discussion/DiscussionEdit.tsx
  27. 34 9
      dashboard/src/components/discussion/DiscussionItem.tsx
  28. 33 44
      dashboard/src/components/discussion/DiscussionList.tsx
  29. 24 7
      dashboard/src/components/discussion/DiscussionListCard.tsx
  30. 128 23
      dashboard/src/components/discussion/DiscussionShow.tsx
  31. 4 4
      dashboard/src/components/discussion/DiscussionTopic.tsx
  32. 46 30
      dashboard/src/components/discussion/DiscussionTopicChildren.tsx
  33. 7 1
      dashboard/src/components/template/MdView.tsx
  34. 6 0
      dashboard/src/components/template/SentEdit.tsx
  35. 25 15
      dashboard/src/components/template/SentEdit/EditInfo.tsx
  36. 58 37
      dashboard/src/components/template/SentEdit/SentCell.tsx
  37. 4 2
      dashboard/src/components/template/SentEdit/SentContent.tsx
  38. 38 7
      dashboard/src/components/template/SentEdit/SentEditMenu.tsx
  39. 22 2
      dashboard/src/components/template/SentEdit/SentMenu.tsx
  40. 36 11
      dashboard/src/components/template/SentEdit/SentTab.tsx
  41. 18 7
      dashboard/src/components/template/SentEdit/SuggestionBox.tsx
  42. 29 7
      dashboard/src/components/template/SentEdit/SuggestionToolbar.tsx
  43. 5 5
      dashboard/src/components/template/Term.tsx
  44. 12 2
      dashboard/src/pages/library/anthology/show.tsx
  45. 27 11
      dashboard/src/pages/library/article/show.tsx
  46. 97 102
      dashboard/src/pages/library/discussion/list.tsx
  47. 2 5
      dashboard/src/pages/library/palicanon/bypath.tsx
  48. 8 10
      dashboard/src/pages/studio/anthology/index.tsx
  49. 19 211
      dashboard/src/pages/studio/article/edit.tsx
  50. 21 8
      dashboard/src/pages/studio/article/list.tsx
  51. 2 5
      dashboard/src/pages/studio/channel/show.tsx
  52. 3 3
      dashboard/src/pages/studio/recent/list.tsx
  53. 2 50
      dashboard/src/utils.ts

+ 1 - 1
dashboard/src/Router.tsx

@@ -288,7 +288,7 @@ const Widget = () => {
 
           <Route path="article" element={<StudioArticle />}>
             <Route path="list" element={<StudioArticleList />} />
-            <Route path=":articleid/edit" element={<StudioArticleEdit />} />
+            <Route path=":articleId/edit" element={<StudioArticleEdit />} />
           </Route>
 
           <Route path="anthology" element={<StudioAnthology />}>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 11 - 0
dashboard/src/assets/icon/index.tsx


+ 2 - 1
dashboard/src/components/anthology/AnthologyList.tsx

@@ -23,6 +23,7 @@ import Share, { EResType } from "../share/Share";
 
 import StudioName, { IStudio } from "../auth/StudioName";
 import { IResNumberResponse, renderBadge } from "../channel/ChannelTable";
+import { fullUrl } from "../../utils";
 
 const { Text } = Typography;
 
@@ -255,7 +256,7 @@ const AnthologyListWidget = ({
                   onClick: (e) => {
                     switch (e.key) {
                       case "open":
-                        window.open(`/anthology/${row.id}`, "_blank");
+                        window.open(fullUrl(`/anthology/${row.id}`), "_blank");
                         break;
                       case "share":
                         console.log("share");

+ 11 - 9
dashboard/src/components/anthology/AnthologyTocTree.tsx

@@ -16,14 +16,15 @@ const AnthologyTocTreeWidget = ({
   onSelect,
   onArticleSelect,
 }: IWidget) => {
-  const navigate = useNavigate();
   const [tocData, setTocData] = useState<ListNodeData[]>([]);
 
   useEffect(() => {
-    get<IArticleMapListResponse>(
-      `/v2/article-map?view=anthology&id=${anthologyId}`
-    ).then((json) => {
-      console.log("文集get", json);
+    if (typeof anthologyId === "undefined") {
+      return;
+    }
+    const url = `/v2/article-map?view=anthology&id=${anthologyId}`;
+    console.log("url", url);
+    get<IArticleMapListResponse>(url).then((json) => {
       if (json.ok) {
         const toc: ListNodeData[] = json.data.rows.map((item) => {
           return {
@@ -42,10 +43,11 @@ const AnthologyTocTreeWidget = ({
       <TocTree
         treeData={tocData}
         onSelect={(keys: string[]) => {
-          if (typeof onArticleSelect !== "undefined") {
-            onArticleSelect(keys);
-          } else {
-            navigate(`/article/article/${keys[0]}?mode=read`);
+          if (
+            typeof onArticleSelect !== "undefined" &&
+            typeof anthologyId !== "undefined"
+          ) {
+            onArticleSelect(anthologyId, keys);
           }
         }}
       />

+ 113 - 29
dashboard/src/components/anthology/EditableTocTree.tsx

@@ -2,17 +2,25 @@ import { Button, message } from "antd";
 import { useEffect, useState } from "react";
 import { FileAddOutlined } from "@ant-design/icons";
 
-import { get, put } from "../../request";
+import { get as getUiLang } from "../../locales";
+
+import { get, post, put } from "../../request";
 import {
+  IArticleCreateRequest,
+  IArticleDataResponse,
   IArticleMapAddResponse,
   IArticleMapListResponse,
   IArticleMapUpdateRequest,
+  IArticleResponse,
 } from "../api/Article";
 import ArticleListModal from "../article/ArticleListModal";
 import EditableTree, {
   ListNodeData,
   TreeNodeData,
 } from "../article/EditableTree";
+import ArticleEditDrawer from "../article/ArticleEditDrawer";
+import ArticleDrawer from "../article/ArticleDrawer";
+import { fullUrl } from "../../utils";
 
 interface IWidget {
   anthologyId?: string;
@@ -26,6 +34,47 @@ const EditableTocTreeWidget = ({
 }: IWidget) => {
   const [tocData, setTocData] = useState<ListNodeData[]>([]);
   const [addArticle, setAddArticle] = useState<TreeNodeData>();
+  const [articleId, setArticleId] = useState<string>();
+  const [openEditor, setOpenEditor] = useState(false);
+  const [updatedArticle, setUpdatedArticle] = useState<TreeNodeData>();
+  const [openViewer, setOpenViewer] = useState(false);
+  const [viewArticleId, setViewArticleId] = useState<string>();
+
+  const save = (data?: ListNodeData[]) => {
+    console.log("onSave", data);
+    if (typeof data === "undefined") {
+      return;
+    }
+    put<IArticleMapUpdateRequest, IArticleMapAddResponse>(
+      `/v2/article-map/${anthologyId}`,
+      {
+        data: data.map((item) => {
+          let title = "";
+          if (typeof item.title === "string") {
+            title = item.title;
+          }
+          //TODO 整一个string title
+          return {
+            article_id: item.key,
+            level: item.level,
+            title: title,
+            children: item.children,
+          };
+        }),
+        operation: "anthology",
+      }
+    )
+      .finally(() => {})
+      .then((json) => {
+        if (json.ok) {
+          message.success(json.data);
+        } else {
+          message.error(json.message);
+        }
+      })
+      .catch((e) => console.error(e));
+  };
+
   useEffect(() => {
     get<IArticleMapListResponse>(
       `/v2/article-map?view=anthology&id=${anthologyId}`
@@ -66,41 +115,76 @@ const EditableTocTreeWidget = ({
             }}
           />
         }
+        updatedNode={updatedArticle}
         onChange={(data: ListNodeData[]) => {
-          console.log("onChange", data);
+          save(data);
         }}
         onSave={(data: ListNodeData[]) => {
-          console.log("onSave", data);
-          put<IArticleMapUpdateRequest, IArticleMapAddResponse>(
-            `/v2/article-map/${anthologyId}`,
+          save(data);
+        }}
+        onAppend={async (
+          node: TreeNodeData
+        ): Promise<TreeNodeData | undefined> => {
+          if (typeof studioName === "undefined") {
+            return;
+          }
+          const res = await post<IArticleCreateRequest, IArticleResponse>(
+            `/v2/article`,
             {
-              data: data.map((item) => {
-                let title = "";
-                if (typeof item.title === "string") {
-                  title = item.title;
-                }
-                //TODO 整一个string title
-                return {
-                  article_id: item.key,
-                  level: item.level,
-                  title: title,
-                  children: item.children,
-                };
-              }),
-              operation: "anthology",
+              title: "new article",
+              lang: getUiLang(),
+              studio: studioName,
             }
-          )
-            .finally(() => {})
-            .then((json) => {
-              if (json.ok) {
-                message.success(json.data);
-              } else {
-                message.error(json.message);
-              }
-            })
-            .catch((e) => console.error(e));
+          );
+
+          console.log(res);
+          if (res.ok) {
+            return {
+              key: res.data.uid,
+              title: res.data.title,
+              children: [],
+              level: node.level + 1,
+            };
+          } else {
+            return;
+          }
+        }}
+        onNodeEdit={(key: string) => {
+          setArticleId(key);
+          setOpenEditor(true);
+        }}
+        onTitleClick={(
+          e: React.MouseEvent<HTMLElement, MouseEvent>,
+          node: TreeNodeData
+        ) => {
+          if (e.ctrlKey || e.metaKey) {
+            window.open(fullUrl(`/article/article/${node.key}`), "_blank");
+          } else {
+            setViewArticleId(node.key);
+            setOpenViewer(true);
+          }
         }}
       />
+      <ArticleEditDrawer
+        articleId={articleId}
+        open={openEditor}
+        onClose={() => setOpenEditor(false)}
+        onChange={(data: IArticleDataResponse) => {
+          console.log("new title", data.title);
+          setUpdatedArticle({
+            key: data.uid,
+            title: data.title,
+            level: 0,
+            children: [],
+          });
+        }}
+      />
+      <ArticleDrawer
+        articleId={viewArticleId}
+        type="article"
+        open={openViewer}
+        onClose={() => setOpenViewer(false)}
+      />
     </div>
   );
 };

+ 2 - 1
dashboard/src/components/api/Article.ts

@@ -68,6 +68,7 @@ export interface IArticleDataRequest {
   lang: string;
 }
 export interface IChapterToc {
+  key?: string;
   book: number;
   paragraph: number;
   level: number;
@@ -88,7 +89,7 @@ export interface IArticleDataResponse {
   status: number;
   lang: string;
   anthology_count?: number;
-  anthology_first?: { title: string };
+  anthology_first?: { uid: string; title: string };
   role?: TRole;
   studio?: IStudio;
   editor?: IUser;

+ 2 - 6
dashboard/src/components/article/AnthologyDetail.tsx

@@ -26,7 +26,6 @@ const AnthologyDetailWidget = ({
   onArticleSelect,
 }: IWidgetAnthologyDetail) => {
   const [tableData, setTableData] = useState<IAnthologyData>();
-  const navigate = useNavigate();
 
   useEffect(() => {
     console.log("useEffect");
@@ -76,14 +75,11 @@ const AnthologyDetailWidget = ({
         <Marked text={tableData?.summary} />
       </Paragraph>
       <Title level={5}>目录</Title>
-
       <AnthologyTocTree
         anthologyId={aid}
-        onSelect={(keys: string[]) => {
+        onArticleSelect={(anthologyId: string, keys: string[]) => {
           if (typeof onArticleSelect !== "undefined") {
-            onArticleSelect(keys);
-          } else {
-            navigate(`/article/article/${keys[0]}?mode=read`);
+            onArticleSelect(anthologyId, keys);
           }
         }}
       />

+ 41 - 10
dashboard/src/components/article/Article.tsx

@@ -1,17 +1,16 @@
 import { useEffect, useState } from "react";
-import { Divider, message, Result, Tag } from "antd";
+import { Divider, message, Result, Space, Tag } from "antd";
 
 import { get, post } from "../../request";
 import store from "../../store";
 import { IArticleDataResponse, IArticleResponse } from "../api/Article";
-import ArticleView from "./ArticleView";
+import ArticleView, { IFirstAnthology } from "./ArticleView";
 import { ICourseCurrUserResponse } from "../api/Course";
 import { ICourseUser, signIn } from "../../reducers/course-user";
 import { ITextbook, refresh } from "../../reducers/current-course";
 import ExerciseList from "./ExerciseList";
 import ExerciseAnswer from "../course/ExerciseAnswer";
 import "./article.css";
-import CommentListCard from "../discussion/DiscussionListCard";
 import TocTree from "./TocTree";
 import PaliText from "../template/Wbw/PaliText";
 import ArticleSkeleton from "./ArticleSkeleton";
@@ -22,6 +21,7 @@ import {
   IRecentRequest,
   IRecentResponse,
 } from "../../pages/studio/recent/list";
+import { ITocPathNode } from "../corpus/TocPath";
 
 export type ArticleMode = "read" | "edit" | "wbw";
 export type ArticleType =
@@ -59,7 +59,7 @@ interface IWidget {
   book?: string | null;
   para?: string | null;
   channelId?: string | null;
-  anthologyId?: string;
+  anthologyId?: string | null;
   courseId?: string;
   exerciseId?: string;
   userName?: string;
@@ -68,6 +68,7 @@ interface IWidget {
   onArticleChange?: Function;
   onFinal?: Function;
   onLoad?: Function;
+  onAnthologySelect?: Function;
 }
 const ArticleWidget = ({
   type,
@@ -84,6 +85,7 @@ const ArticleWidget = ({
   onArticleChange,
   onFinal,
   onLoad,
+  onAnthologySelect,
 }: IWidget) => {
   const [articleData, setArticleData] = useState<IArticleDataResponse>();
   const [articleHtml, setArticleHtml] = useState<string[]>(["<span />"]);
@@ -236,17 +238,19 @@ const ArticleWidget = ({
               <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>
+                    <Tag key={id}>{Math.round(item * 100) + "%"}</Tag>
                   ));
-
                   return {
-                    key: `${item.book}-${item.paragraph}`,
+                    key: key,
                     title: (
-                      <>
+                      <Space>
                         <PaliText text={strTitle} />
                         {progress}
-                      </>
+                      </Space>
                     ),
                     level: item.level,
                   };
@@ -346,6 +350,15 @@ const ArticleWidget = ({
   };
 
   //const comment = <CommentListCard resId={articleData?.uid} resType="article" />
+  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,
+    };
+  }
+
   return (
     <div>
       {showSkeleton ? (
@@ -372,14 +385,32 @@ const ArticleWidget = ({
           type={type}
           articleId={articleId}
           remains={remains}
+          anthology={anthology}
           onEnd={() => {
             if (type === "chapter" && articleData) {
               getNextPara(articleData);
             }
           }}
+          onPathChange={(
+            node: ITocPathNode,
+            e: React.MouseEvent<HTMLSpanElement | HTMLAnchorElement, MouseEvent>
+          ) => {
+            if (typeof onArticleChange !== "undefined") {
+              const newArticle = node.key
+                ? node.key
+                : `${node.book}-${node.paragraph}`;
+              const target = e.ctrlKey || e.metaKey ? "_blank" : "self";
+              onArticleChange(newArticle, target);
+            }
+          }}
+          onAnthologySelect={(id: string) => {
+            if (typeof onAnthologySelect !== "undefined") {
+              onAnthologySelect(id);
+            }
+          }}
         />
       )}
-
+      <Divider />
       {extra}
       <Divider />
     </div>

+ 208 - 0
dashboard/src/components/article/ArticleEdit.tsx

@@ -0,0 +1,208 @@
+import { useState } from "react";
+
+import { useIntl } from "react-intl";
+import {
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
+} from "@ant-design/pro-components";
+
+import { Button, Form, message, Result, Space, Tabs } from "antd";
+
+import { get, put } from "../../request";
+import {
+  IArticleDataRequest,
+  IArticleResponse,
+} from "../../components/api/Article";
+import LangSelect from "../../components/general/LangSelect";
+import PublicitySelect from "../../components/studio/PublicitySelect";
+
+import MDEditor from "@uiw/react-md-editor";
+import ArticlePrevDrawer from "../../components/article/ArticlePrevDrawer";
+
+interface IFormData {
+  uid: string;
+  title: string;
+  subtitle: string;
+  summary: string;
+  content?: string;
+  content_type?: string;
+  status: number;
+  lang: string;
+}
+
+interface IWidget {
+  studioName?: string;
+  articleId?: string;
+  onReady?: Function;
+  onChange?: Function;
+}
+
+const ArticleEditWidget = ({
+  studioName,
+  articleId,
+  onReady,
+  onChange,
+}: IWidget) => {
+  const intl = useIntl();
+  const [unauthorized, setUnauthorized] = useState(false);
+  const [readonly, setReadonly] = useState(false);
+  const [content, setContent] = useState<string>();
+
+  return unauthorized ? (
+    <Result
+      status="403"
+      title="无权访问"
+      subTitle="您无权访问该内容。您可能没有登录,或者内容的所有者没有给您所需的权限。"
+      extra={<></>}
+    />
+  ) : (
+    <>
+      {readonly ? "只读" : undefined}
+      <ProForm<IFormData>
+        onFinish={async (values: IFormData) => {
+          // TODO
+
+          const request = {
+            uid: articleId ? articleId : "",
+            title: values.title,
+            subtitle: values.subtitle,
+            summary: values.summary,
+            content: values.content,
+            content_type: "markdown",
+            status: values.status,
+            lang: values.lang,
+          };
+          console.log("save", request);
+          put<IArticleDataRequest, IArticleResponse>(
+            `/v2/article/${articleId}`,
+            request
+          )
+            .then((res) => {
+              console.log("save response", res);
+              if (res.ok) {
+                if (typeof onChange !== "undefined") {
+                  onChange(res.data);
+                }
+                message.success(intl.formatMessage({ id: "flashes.success" }));
+              } else {
+                message.error(res.message);
+              }
+            })
+            .catch((e: IArticleResponse) => {
+              message.error(e.message);
+            });
+        }}
+        request={async () => {
+          const res = await get<IArticleResponse>(`/v2/article/${articleId}`);
+          console.log("article", res);
+          let mTitle: string,
+            mReadonly = false;
+          if (res.ok) {
+            mReadonly = res.data.role === "editor" ? false : true;
+            setReadonly(mReadonly);
+            mTitle = res.data.title;
+            setContent(res.data.content);
+          } else {
+            setUnauthorized(true);
+            mTitle = "无权访问";
+          }
+          if (typeof onReady !== "undefined") {
+            onReady(mTitle, mReadonly, res.data.studio?.realName);
+          }
+          return {
+            uid: res.data.uid,
+            title: res.data.title,
+            subtitle: res.data.subtitle,
+            summary: res.data.summary,
+            content: res.data.content,
+            content_type: res.data.content_type,
+            lang: res.data.lang,
+            status: res.data.status,
+          };
+        }}
+      >
+        <Tabs
+          items={[
+            {
+              key: "info",
+              label: intl.formatMessage({ id: "course.basic.info.label" }),
+              children: (
+                <>
+                  <ProForm.Group>
+                    <ProFormText
+                      width="md"
+                      name="title"
+                      required
+                      label={intl.formatMessage({
+                        id: "forms.fields.title.label",
+                      })}
+                      rules={[
+                        {
+                          required: true,
+                          message: intl.formatMessage({
+                            id: "forms.message.title.required",
+                          }),
+                        },
+                      ]}
+                    />
+                    <ProFormText
+                      width="md"
+                      name="subtitle"
+                      label={intl.formatMessage({
+                        id: "forms.fields.subtitle.label",
+                      })}
+                    />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <LangSelect width="md" />
+                    <PublicitySelect width="md" />
+                  </ProForm.Group>
+                  <ProForm.Group>
+                    <ProFormTextArea
+                      name="summary"
+                      width="lg"
+                      label={intl.formatMessage({
+                        id: "forms.fields.summary.label",
+                      })}
+                    />
+                  </ProForm.Group>
+                </>
+              ),
+            },
+            {
+              key: "content",
+              label: intl.formatMessage({ id: "forms.fields.content.label" }),
+              forceRender: true,
+              children: (
+                <ProForm.Group>
+                  <Form.Item
+                    name="content"
+                    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)} />
+                  </Form.Item>
+                </ProForm.Group>
+              ),
+            },
+          ]}
+        />
+      </ProForm>
+    </>
+  );
+};
+
+export default ArticleEditWidget;

+ 89 - 0
dashboard/src/components/article/ArticleEditDrawer.tsx

@@ -0,0 +1,89 @@
+import { Drawer } from "antd";
+import React, { useEffect, useState } from "react";
+import { IArticleDataResponse } from "../api/Article";
+
+import ArticleEdit from "./ArticleEdit";
+import ArticleEditTools from "./ArticleEditTools";
+
+interface IWidget {
+  trigger?: React.ReactNode;
+  articleId?: string;
+  open?: boolean;
+  onClose?: Function;
+  onChange?: Function;
+}
+
+const ArticleEditDrawerWidget = ({
+  trigger,
+  articleId,
+  open,
+  onClose,
+  onChange,
+}: IWidget) => {
+  const [openDrawer, setOpenDrawer] = useState(open);
+  const [title, setTitle] = useState("loading");
+  const [readonly, setReadonly] = useState(false);
+  const [studioName, setStudioName] = useState<string>();
+
+  useEffect(() => setOpenDrawer(open), [open]);
+  const showDrawer = () => {
+    setOpenDrawer(true);
+  };
+
+  const onDrawerClose = () => {
+    setOpenDrawer(false);
+    if (document.getElementsByTagName("body")[0].hasAttribute("style")) {
+      document.getElementsByTagName("body")[0].removeAttribute("style");
+    }
+    if (typeof onClose !== "undefined") {
+      onClose();
+    }
+  };
+  /*
+  const getUrl = (openMode?: string): string => {
+    let url = `/article/${type}/${articleId}?mode=`;
+    url += openMode ? openMode : mode ? mode : "read";
+    url += channelId ? `&channel=${channelId}` : "";
+    url += book ? `&book=${book}` : "";
+    url += para ? `&par=${para}` : "";
+    return url;
+  };
+*/
+
+  return (
+    <>
+      <span onClick={() => showDrawer()}>{trigger}</span>
+      <Drawer
+        title={title + readonly ? "(只读)" : ""}
+        width={1000}
+        placement="right"
+        onClose={onDrawerClose}
+        open={openDrawer}
+        destroyOnClose={true}
+        extra={
+          <ArticleEditTools
+            studioName={studioName}
+            articleId={articleId}
+            title={title}
+          />
+        }
+      >
+        <ArticleEdit
+          articleId={articleId}
+          onReady={(title: string, readonly: boolean, studio?: string) => {
+            setTitle(title);
+            setReadonly(readonly);
+            setStudioName(studio);
+          }}
+          onChange={(data: IArticleDataResponse) => {
+            if (typeof onChange !== "undefined") {
+              onChange(data);
+            }
+          }}
+        />
+      </Drawer>
+    </>
+  );
+};
+
+export default ArticleEditDrawerWidget;

+ 53 - 0
dashboard/src/components/article/ArticleEditTools.tsx

@@ -0,0 +1,53 @@
+import { Link } from "react-router-dom";
+import { useIntl } from "react-intl";
+import { TeamOutlined } from "@ant-design/icons";
+import { Button, Space } from "antd";
+
+import ArticleTplMaker from "../../components/article/ArticleTplMaker";
+import ShareModal from "../../components/share/ShareModal";
+import { EResType } from "../../components/share/Share";
+import AddToAnthology from "../../components/article/AddToAnthology";
+
+interface IWidget {
+  studioName?: string;
+  articleId?: string;
+  title?: string;
+}
+const ArticleEditToolsWidget = ({
+  studioName,
+  articleId,
+  title = "title",
+}: IWidget) => {
+  const intl = useIntl();
+  return (
+    <Space>
+      {articleId ? (
+        <AddToAnthology studioName={studioName} articleIds={[articleId]} />
+      ) : undefined}
+      {articleId ? (
+        <ShareModal
+          trigger={
+            <Button icon={<TeamOutlined />}>
+              {intl.formatMessage({
+                id: "buttons.share",
+              })}
+            </Button>
+          }
+          resId={articleId}
+          resType={EResType.article}
+        />
+      ) : undefined}
+      <Link to={`/article/article/${articleId}`} target="_blank">
+        {intl.formatMessage({ id: "buttons.open.in.library" })}
+      </Link>
+      <ArticleTplMaker
+        title={title}
+        type="article"
+        id={articleId}
+        trigger={<Button>获取模版</Button>}
+      />
+    </Space>
+  );
+};
+
+export default ArticleEditToolsWidget;

+ 43 - 5
dashboard/src/components/article/ArticleView.tsx

@@ -1,4 +1,4 @@
-import { Typography, Divider, Button, Skeleton } from "antd";
+import { Typography, Divider, Button, Skeleton, Space } from "antd";
 import { ReloadOutlined } from "@ant-design/icons";
 
 import MdView from "../template/MdView";
@@ -8,7 +8,11 @@ import { ArticleType } from "./Article";
 import VisibleObserver from "../general/VisibleObserver";
 
 const { Paragraph, Title, Text } = Typography;
-
+export interface IFirstAnthology {
+  id: string;
+  title: string;
+  count: number;
+}
 export interface IWidgetArticleData {
   id?: string;
   title?: string;
@@ -23,7 +27,10 @@ export interface IWidgetArticleData {
   type?: ArticleType;
   articleId?: string;
   remains?: boolean;
+  anthology?: IFirstAnthology;
   onEnd?: Function;
+  onPathChange?: Function;
+  onAnthologySelect?: Function;
 }
 
 const ArticleViewWidget = ({
@@ -39,8 +46,11 @@ const ArticleViewWidget = ({
   channels,
   type,
   articleId,
+  anthology,
   onEnd,
   remains,
+  onPathChange,
+  onAnthologySelect,
 }: IWidgetArticleData) => {
   let currChannelList = <></>;
   switch (type) {
@@ -75,8 +85,36 @@ const ArticleViewWidget = ({
         />
       </div>
 
-      <div>
-        <TocPath data={path} channel={channels} />
+      <Space direction="vertical">
+        <Text>
+          {path.length === 0 && anthology ? (
+            <>
+              <Text>{"文集:"}</Text>
+              <Button
+                type="link"
+                onClick={() => {
+                  if (typeof onAnthologySelect !== "undefined") {
+                    onAnthologySelect(anthology.id);
+                  }
+                }}
+              >
+                {anthology.title}
+              </Button>
+            </>
+          ) : undefined}
+        </Text>
+        <TocPath
+          data={path}
+          channel={channels}
+          onChange={(
+            node: ITocPathNode,
+            e: React.MouseEvent<HTMLSpanElement | HTMLAnchorElement, MouseEvent>
+          ) => {
+            if (typeof onPathChange !== "undefined") {
+              onPathChange(node, e);
+            }
+          }}
+        />
 
         <Title level={4}>
           <div
@@ -91,7 +129,7 @@ const ArticleViewWidget = ({
           {summary}
         </Paragraph>
         <Divider />
-      </div>
+      </Space>
       {html
         ? html.map((item, id) => {
             return (

+ 90 - 17
dashboard/src/components/article/EditableTree.tsx

@@ -1,13 +1,12 @@
 import React, { useState } from "react";
 import { useEffect } from "react";
-import { Tree, Typography } from "antd";
+import { message, Tree } from "antd";
 import type { DataNode, TreeProps } from "antd/es/tree";
 import { Key } from "antd/lib/table/interface";
 import { DeleteOutlined, SaveOutlined } from "@ant-design/icons";
 import { Button, Divider, Space } from "antd";
 import { useIntl } from "react-intl";
-
-const { Text } = Typography;
+import EditableTreeNode from "./EditableTreeNode";
 
 export interface TreeNodeData {
   key: string;
@@ -119,19 +118,27 @@ interface IWidget {
   treeData: ListNodeData[];
   addFileButton?: React.ReactNode;
   addOnArticle?: TreeNodeData;
+  updatedNode?: TreeNodeData;
   onChange?: Function;
   onSelect?: Function;
   onSave?: Function;
   onAddFile?: Function;
+  onAppend?: Function;
+  onNodeEdit?: Function;
+  onTitleClick?: Function;
 }
 const EditableTreeWidget = ({
   treeData,
   addFileButton,
   addOnArticle,
+  updatedNode,
   onChange,
   onSelect,
   onSave,
   onAddFile,
+  onAppend,
+  onNodeEdit,
+  onTitleClick,
 }: IWidget) => {
   const intl = useIntl();
 
@@ -139,6 +146,57 @@ const EditableTreeWidget = ({
   const [listTreeData, setListTreeData] = useState<ListNodeData[]>();
   const [keys, setKeys] = useState<Key>("");
 
+  useEffect(() => {
+    if (typeof onChange !== "undefined") {
+      onChange(listTreeData);
+    }
+  }, [listTreeData]);
+
+  useEffect(() => {
+    //找到节点并更新
+    if (typeof updatedNode === "undefined") {
+      return;
+    }
+    const update = (_node: TreeNodeData[]) => {
+      _node.forEach((value, index, array) => {
+        if (value.key === updatedNode.key) {
+          array[index].title = updatedNode.title;
+          console.log("key found");
+          return;
+        } else {
+          update(array[index].children);
+        }
+        return;
+      });
+    };
+    const newTree = [...gData];
+    update(newTree);
+    setGData(newTree);
+    const list = treeToList(newTree);
+    setListTreeData(list);
+  }, [updatedNode]);
+
+  const appendNode = (key: string, node: TreeNodeData) => {
+    console.log("key", key);
+    const append = (_node: TreeNodeData[]) => {
+      _node.forEach((value, index, array) => {
+        if (value.key === key) {
+          array[index].children.push(node);
+          console.log("key found");
+          return;
+        } else {
+          append(array[index].children);
+        }
+        return;
+      });
+    };
+    const newTree = [...gData];
+    append(newTree);
+    setGData(newTree);
+    const list = treeToList(newTree);
+    setListTreeData(list);
+  };
+
   useEffect(() => {
     if (typeof addOnArticle === "undefined") {
       return;
@@ -229,9 +287,6 @@ const EditableTreeWidget = ({
     setGData(data);
     const list = treeToList(data);
     setListTreeData(list);
-    if (typeof onChange !== "undefined") {
-      onChange(list);
-    }
   };
 
   return (
@@ -264,9 +319,6 @@ const EditableTreeWidget = ({
             setGData(tmp);
             const list = treeToList(tmp);
             setListTreeData(list);
-            if (typeof onChange !== "undefined") {
-              onChange(list);
-            }
           }}
         >
           {intl.formatMessage({ id: "buttons.remove" })}
@@ -296,20 +348,41 @@ const EditableTreeWidget = ({
           } else {
             setKeys("");
           }
-
-          console.log(selectedKeys);
           if (typeof onSelect !== "undefined") {
             onSelect(selectedKeys);
           }
         }}
         treeData={gData}
         titleRender={(node: TreeNodeData) => {
-          return node.deletedAt ? (
-            <Text delete disabled>
-              {node.title}
-            </Text>
-          ) : (
-            <>{node.title}</>
+          return (
+            <EditableTreeNode
+              node={node}
+              onEdit={() => {
+                if (typeof onNodeEdit !== "undefined") {
+                  onNodeEdit(node.key);
+                }
+              }}
+              onAdd={async () => {
+                if (typeof onAppend !== "undefined") {
+                  const newNode = await onAppend(node);
+                  console.log("newNode", newNode);
+                  if (newNode) {
+                    appendNode(node.key, newNode);
+                    return true;
+                  } else {
+                    message.error("添加失败");
+                    return false;
+                  }
+                } else {
+                  return false;
+                }
+              }}
+              onTitleClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
+                if (typeof onTitleClick !== "undefined") {
+                  onTitleClick(e, node);
+                }
+              }}
+            />
           );
         }}
       />

+ 78 - 0
dashboard/src/components/article/EditableTreeNode.tsx

@@ -0,0 +1,78 @@
+import { Button, message, Space, Typography } from "antd";
+import { useState } from "react";
+import { PlusOutlined, EditOutlined } from "@ant-design/icons";
+import { TreeNodeData } from "./EditableTree";
+const { Text } = Typography;
+
+interface IWidget {
+  node: TreeNodeData;
+  onAdd?: Function;
+  onEdit?: Function;
+  onTitleClick?: Function;
+}
+const EditableTreeNodeWidget = ({
+  node,
+  onAdd,
+  onEdit,
+  onTitleClick,
+}: IWidget) => {
+  const [showNodeMenu, setShowNodeMenu] = useState(false);
+  const [loading, setLoading] = useState(false);
+
+  const title = node.deletedAt ? (
+    <Text delete disabled>
+      {node.title}
+    </Text>
+  ) : (
+    <Text
+      onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
+        if (typeof onTitleClick !== "undefined") {
+          onTitleClick(e);
+        }
+      }}
+    >
+      {node.title}
+    </Text>
+  );
+  const menu = (
+    <Space style={{ visibility: showNodeMenu ? "visible" : "hidden" }}>
+      <Button
+        size="middle"
+        icon={<EditOutlined />}
+        type="text"
+        onClick={async () => {
+          if (typeof onEdit !== "undefined") {
+            onEdit();
+          }
+        }}
+      />
+      <Button
+        loading={loading}
+        size="middle"
+        icon={<PlusOutlined />}
+        type="text"
+        onClick={async () => {
+          if (typeof onAdd !== "undefined") {
+            setLoading(true);
+            const ok = await onAdd();
+            setLoading(false);
+            if (!ok) {
+              message.error("error");
+            }
+          }
+        }}
+      />
+    </Space>
+  );
+  return (
+    <Space
+      onMouseEnter={() => setShowNodeMenu(true)}
+      onMouseLeave={() => setShowNodeMenu(false)}
+    >
+      {title}
+      {menu}
+    </Space>
+  );
+};
+
+export default EditableTreeNodeWidget;

+ 2 - 5
dashboard/src/components/article/ToolButtonNav.tsx

@@ -8,6 +8,7 @@ import { useAppSelector } from "../../hooks";
 import { sentenceList } from "../../reducers/sentence";
 import ToolButtonNavMore from "./ToolButtonNavMore";
 import ToolButtonNavSliceTitle from "./ToolButtonNavSliceTitle";
+import { fullUrl } from "../../utils";
 
 const { Text } = Typography;
 
@@ -152,11 +153,7 @@ const ToolButtonNavWidget = ({ type, articleId }: IWidget) => {
 
                   switch (key) {
                     case "copy-link":
-                      const fullUrl =
-                        process.env.REACT_APP_WEB_HOST +
-                        process.env.PUBLIC_URL +
-                        url;
-                      navigator.clipboard.writeText(fullUrl).then(() => {
+                      navigator.clipboard.writeText(fullUrl(url)).then(() => {
                         message.success("链接地址已经拷贝到剪贴板");
                       });
 

+ 22 - 3
dashboard/src/components/article/ToolButtonToc.tsx

@@ -1,5 +1,6 @@
 import { MenuOutlined } from "@ant-design/icons";
 import { Key } from "antd/lib/table/interface";
+import AnthologyTocTree from "../anthology/AnthologyTocTree";
 import { ArticleType } from "./Article";
 
 import PaliTextToc from "./PaliTextToc";
@@ -8,11 +9,16 @@ import ToolButton from "./ToolButton";
 interface IWidget {
   type?: ArticleType;
   articleId?: string;
+  anthologyId?: string | null;
   onSelect?: Function;
 }
-const ToolButtonTocWidget = ({ type, articleId, onSelect }: IWidget) => {
+const ToolButtonTocWidget = ({
+  type,
+  articleId,
+  anthologyId,
+  onSelect,
+}: IWidget) => {
   let tocWidget = <></>;
-
   switch (type) {
     case "chapter":
       const id = articleId?.split("_");
@@ -36,7 +42,20 @@ const ToolButtonTocWidget = ({ type, articleId, onSelect }: IWidget) => {
         }
       }
       break;
-
+    case "article":
+      if (anthologyId) {
+        tocWidget = (
+          <AnthologyTocTree
+            anthologyId={anthologyId}
+            onArticleSelect={(anthologyId: string, keys: string[]) => {
+              if (typeof onSelect !== "undefined" && keys.length > 0) {
+                onSelect(keys[0]);
+              }
+            }}
+          />
+        );
+      }
+      break;
     default:
       break;
   }

+ 26 - 20
dashboard/src/components/auth/StudioCard.tsx

@@ -2,39 +2,45 @@ import { useIntl } from "react-intl";
 import { Popover, Avatar } from "antd";
 import { IStudio } from "./StudioName";
 import { Link } from "react-router-dom";
+import React from "react";
 
 interface IWidget {
   studio?: IStudio;
   children?: JSX.Element;
+  popOver?: React.ReactNode;
 }
-const StudioCardWidget = ({ studio, children }: IWidget) => {
+const StudioCardWidget = ({ studio, children, popOver }: IWidget) => {
   const intl = useIntl();
 
   return (
     <Popover
       content={
-        <>
-          <div style={{ display: "flex" }}>
-            <div style={{ paddingRight: 8 }}>
-              <Avatar style={{ backgroundColor: "#87d068" }} size="small">
-                {studio?.nickName?.slice(0, 1)}
-              </Avatar>
-            </div>
-            <div>
-              <div>{studio?.nickName}</div>
+        popOver ? (
+          popOver
+        ) : (
+          <>
+            <div style={{ display: "flex" }}>
+              <div style={{ paddingRight: 8 }}>
+                <Avatar style={{ backgroundColor: "#87d068" }} size="small">
+                  {studio?.nickName?.slice(0, 1)}
+                </Avatar>
+              </div>
               <div>
-                <Link
-                  to={`/blog/${studio?.studioName}/overview`}
-                  target="_blank"
-                >
-                  {intl.formatMessage({
-                    id: "columns.library.blog.label",
-                  })}
-                </Link>
+                <div>{studio?.nickName}</div>
+                <div>
+                  <Link
+                    to={`/blog/${studio?.studioName}/overview`}
+                    target="_blank"
+                  >
+                    {intl.formatMessage({
+                      id: "columns.library.blog.label",
+                    })}
+                  </Link>
+                </div>
               </div>
             </div>
-          </div>
-        </>
+          </>
+        )
       }
       placement="bottomRight"
     >

+ 5 - 3
dashboard/src/components/auth/StudioName.tsx

@@ -9,22 +9,24 @@ export interface IStudio {
   realName?: string;
   avatar?: string;
 }
-interface IWidghtStudio {
+interface IWidget {
   data?: IStudio;
   showAvatar?: boolean;
   showName?: boolean;
+  popOver?: React.ReactNode;
   onClick?: Function;
 }
 const StudioNameWidget = ({
   data,
   showAvatar = true,
   showName = true,
+  popOver,
   onClick,
-}: IWidghtStudio) => {
+}: IWidget) => {
   // TODO
   const avatar = <Avatar size="small">{data?.nickName?.slice(0, 1)}</Avatar>;
   return (
-    <StudioCard studio={data}>
+    <StudioCard popOver={popOver} studio={data}>
       <Space
         onClick={() => {
           if (typeof onClick !== "undefined") {

+ 54 - 56
dashboard/src/components/corpus/ChapterCard.tsx

@@ -31,72 +31,70 @@ export interface ChapterData {
   like: number;
 }
 
-interface IWidgetChapterCard {
+interface IWidget {
   data: ChapterData;
   onTagClick?: Function;
 }
 
-const ChpterCardWidget = ({ data, onTagClick }: IWidgetChapterCard) => {
+const ChapterCardWidget = ({ data, onTagClick }: IWidget) => {
   const intl = useIntl();
   const path = JSON.parse(data.path);
   let url = `/article/chapter/${data.book}-${data.paragraph}`;
   url += data.channel.id ? `?channel=${data.channel.id}` : "";
   return (
-    <>
-      <Row>
-        <Col>
-          <Row>
-            <Col span={16}>
-              <Title level={5}>
-                <Link to={url} target="_blank">
-                  {data.title ? data.title : data.paliTitle}
-                </Link>
-              </Title>
-              <Text type="secondary">{data.paliTitle}</Text>
-              <TocPath data={path} />
-            </Col>
-            <Col span={8}>
-              <Progress percent={data.progress} size="small" />
-            </Col>
-          </Row>
-          <Row>
-            <Col>
-              <Paragraph
-                ellipsis={{
-                  rows: 2,
-                  expandable: false,
-                  symbol: "more",
-                }}
-              >
-                {data.summary}
-              </Paragraph>
-            </Col>
-          </Row>
-          <div style={{ display: "flex", justifyContent: "space-between" }}>
-            <div>
-              <TagArea
-                data={data.tag}
-                onTagClick={(tag: string) => {
-                  if (typeof onTagClick !== "undefined") {
-                    onTagClick(tag);
-                  }
-                }}
-              />
-            </div>
-            <Space>
-              <ChannelListItem channel={data.channel} studio={data.studio} />
-              <TimeShow
-                time={data.updatedAt}
-                title={intl.formatMessage({
-                  id: "labels.updated-at",
-                })}
-              />
-            </Space>
+    <Row>
+      <Col>
+        <Row>
+          <Col span={16}>
+            <Title level={5}>
+              <Link to={url} target="_blank">
+                {data.title ? data.title : data.paliTitle}
+              </Link>
+            </Title>
+            <Text type="secondary">{data.paliTitle}</Text>
+            <TocPath data={path} />
+          </Col>
+          <Col span={8}>
+            <Progress percent={data.progress} size="small" />
+          </Col>
+        </Row>
+        <Row>
+          <Col>
+            <Paragraph
+              ellipsis={{
+                rows: 2,
+                expandable: false,
+                symbol: "more",
+              }}
+            >
+              {data.summary}
+            </Paragraph>
+          </Col>
+        </Row>
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <div>
+            <TagArea
+              data={data.tag}
+              onTagClick={(tag: string) => {
+                if (typeof onTagClick !== "undefined") {
+                  onTagClick(tag);
+                }
+              }}
+            />
           </div>
-        </Col>
-      </Row>
-    </>
+          <Space>
+            <ChannelListItem channel={data.channel} studio={data.studio} />
+            <TimeShow
+              time={data.updatedAt}
+              title={intl.formatMessage({
+                id: "labels.updated-at",
+              })}
+            />
+          </Space>
+        </div>
+      </Col>
+    </Row>
   );
 };
 
-export default ChpterCardWidget;
+export default ChapterCardWidget;

+ 15 - 5
dashboard/src/components/corpus/PaliChapterHead.tsx

@@ -47,11 +47,21 @@ const PaliChapterHeadWidget = ({ para, onChange }: IWidget) => {
     <>
       <TocPath
         data={pathData}
-        onChange={(e: IChapter) => {
-          message.success(e.book + ":" + e.para);
-          fetchData(e);
-          if (typeof onChange !== "undefined") {
-            onChange(e);
+        onChange={(
+          node: ITocPathNode,
+          e: React.MouseEvent<HTMLSpanElement | HTMLAnchorElement, MouseEvent>
+        ) => {
+          message.success(node.book + ":" + node.paragraph);
+          if (node.book && node.paragraph) {
+            const chapter = {
+              book: node.book,
+              para: node.paragraph,
+              level: node.level,
+            };
+            fetchData(chapter);
+            if (typeof onChange !== "undefined") {
+              onChange(chapter);
+            }
           }
         }}
         link={"none"}

+ 27 - 1
dashboard/src/components/corpus/RelatedPara.tsx

@@ -4,6 +4,8 @@ import { Badge, Card, List, message, Modal, Skeleton } from "antd";
 import { get } from "../../request";
 import { useEffect, useState } from "react";
 import TocPath, { ITocPathNode } from "./TocPath";
+import store from "../../store";
+import { change } from "../../reducers/para-change";
 
 interface ITag {
   id?: string;
@@ -122,7 +124,31 @@ const RelatedParaWidget = ({ book, para, trigger, onSelect }: IWidget) => {
                       }
                       size="small"
                     >
-                      <TocPath data={item.path} />
+                      <TocPath
+                        data={item.path}
+                        onChange={(
+                          node: ITocPathNode,
+                          e: React.MouseEvent<
+                            HTMLSpanElement | HTMLAnchorElement,
+                            MouseEvent
+                          >
+                        ) => {
+                          if (node.book && node.paragraph) {
+                            const type = node.level
+                              ? node.level < 8
+                                ? "chapter"
+                                : "para"
+                              : "para";
+                            store.dispatch(
+                              change({
+                                book: node.book,
+                                para: node.paragraph,
+                                type: type,
+                              })
+                            );
+                          }
+                        }}
+                      />
                     </Card>
                   </Badge.Ribbon>
                 </List.Item>

+ 52 - 51
dashboard/src/components/corpus/TocPath.tsx

@@ -1,12 +1,14 @@
-import { Link } from "react-router-dom";
+import { useNavigate, useSearchParams } from "react-router-dom";
 import { Breadcrumb, Popover, Tag, Typography } from "antd";
+
 import PaliText from "../template/Wbw/PaliText";
 import React from "react";
-import { IChapter } from "./BookViewer";
+import { fullUrl } from "../../utils";
 
 export interface ITocPathNode {
-  book: number;
-  paragraph: number;
+  key?: string;
+  book?: number;
+  paragraph?: number;
   title: string;
   paliTitle?: string;
   level: number;
@@ -23,60 +25,59 @@ interface IWidgetTocPath {
 }
 const TocPathWidget = ({
   data = [],
-  trigger = "toc",
+  trigger,
   link = "self",
   channel,
   onChange,
 }: IWidgetTocPath): JSX.Element => {
-  const path = data.map((item, id) => {
-    let sChannel = "";
-    if (typeof channel !== "undefined" && channel.length > 0) {
-      sChannel = "?channel=" + channel.join("_");
-    }
-    const linkChapter = `/article/chapter/${item.book}-${item.paragraph}${sChannel}`;
-    let oneItem = <></>;
-    const title = <PaliText text={item.title} />;
-    const eTitle = item.level < 9 ? title : <Tag>{title}</Tag>;
-    switch (link) {
-      case "none":
-        oneItem = <Typography.Link>{eTitle}</Typography.Link>;
-        break;
-      case "self" || "blank":
-        if (item.book === 0) {
-          oneItem = <>{eTitle}</>;
-        } else {
-          oneItem = (
-            <Link to={linkChapter} target={`_${link}`}>
-              {eTitle}
-            </Link>
-          );
-        }
+  const navigate = useNavigate();
+  const [searchParams, setSearchParams] = useSearchParams();
 
-        break;
-    }
-    return (
-      <Breadcrumb.Item
-        onClick={(
-          e: React.MouseEvent<HTMLSpanElement | HTMLAnchorElement, MouseEvent>
-        ) => {
-          if (typeof onChange !== "undefined") {
-            const para: IChapter = {
-              book: item.book,
-              para: item.paragraph,
-              level: item.level,
-            };
-            onChange(para, e);
-          }
-        }}
-        key={id}
-      >
-        {oneItem}
-      </Breadcrumb.Item>
-    );
-  });
   const fullPath = (
     <Breadcrumb style={{ whiteSpace: "nowrap", width: "100%" }}>
-      {path}
+      {data.map((item, id) => {
+        return (
+          <Breadcrumb.Item
+            onClick={(
+              e: React.MouseEvent<
+                HTMLSpanElement | HTMLAnchorElement,
+                MouseEvent
+              >
+            ) => {
+              if (typeof onChange !== "undefined") {
+                onChange(item, e);
+              } else {
+                if (item.book && item.paragraph) {
+                  const type = item.level < 8 ? "chapter" : "para";
+                  const param =
+                    type === "para"
+                      ? `&book=${item.book}&par=${item.paragraph}`
+                      : "";
+                  const channel = searchParams.get("channel");
+                  const mode = searchParams.get("mode");
+                  const urlMode = mode ? mode : "read";
+                  let url = `/article/${type}/${item.book}-${item.paragraph}?mode=${urlMode}${param}`;
+                  url += channel ? `&channel=${channel}` : "";
+                  if (e.ctrlKey || e.metaKey) {
+                    window.open(fullUrl(url), "_blank");
+                  } else {
+                    navigate(url);
+                  }
+                }
+              }
+            }}
+            key={id}
+          >
+            <Typography.Link>
+              {item.level < 99 ? (
+                <PaliText text={item.title} />
+              ) : (
+                <Tag>{item.title}</Tag>
+              )}
+            </Typography.Link>
+          </Breadcrumb.Item>
+        );
+      })}
     </Breadcrumb>
   );
   if (typeof trigger === "undefined") {

+ 1 - 1
dashboard/src/components/course/TextBook.tsx

@@ -17,7 +17,7 @@ const TextBookWidget = ({ anthologyId, courseId }: IWidget) => {
         <Col flex="960px">
           <AnthologyDetail
             aid={anthologyId}
-            onArticleSelect={(keys: string[]) => {
+            onArticleSelect={(anthologyId: string, keys: string[]) => {
               navigate(`/article/textbook/${courseId}_${keys[0]}?mode=read`);
             }}
           />

+ 6 - 5
dashboard/src/components/discussion/DiscussionBox.tsx

@@ -2,8 +2,8 @@ import { useState } from "react";
 import { Button, Divider, Drawer, Space } from "antd";
 import { FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons";
 
-import CommentTopic from "./DiscussionTopic";
-import CommentListCard, { TResType } from "./DiscussionListCard";
+import DiscussionTopic from "./DiscussionTopic";
+import DiscussionListCard, { TResType } from "./DiscussionListCard";
 import { IComment } from "./DiscussionItem";
 import DiscussionAnchor from "./DiscussionAnchor";
 
@@ -50,6 +50,7 @@ const DiscussionBoxWidget = ({
       </span>
       <Drawer
         title="Discussion"
+        destroyOnClose
         extra={
           <Space>
             {drawerWidth === drawerMinWidth ? (
@@ -79,7 +80,7 @@ const DiscussionBoxWidget = ({
       >
         <DiscussionAnchor resId={resId} resType={resType} />
         <Divider></Divider>
-        <CommentListCard
+        <DiscussionListCard
           resId={resId}
           resType={resType}
           onSelect={showChildrenDrawer}
@@ -92,13 +93,13 @@ const DiscussionBoxWidget = ({
         />
         <Drawer
           title="Answer"
-          width={480}
+          width={700}
           onClose={() => {
             setChildrenDrawer(false);
           }}
           open={childrenDrawer}
         >
-          <CommentTopic
+          <DiscussionTopic
             topicId={topicComment?.id}
             onItemCountChange={(count: number, parent: string) => {
               setAnswerCount({ id: parent, count: count });

+ 56 - 66
dashboard/src/components/discussion/DiscussionCreate.tsx

@@ -6,7 +6,6 @@ import {
   ProFormText,
   ProFormTextArea,
 } from "@ant-design/pro-components";
-import { Col, Row, Space } from "antd";
 import ReactQuill from "react-quill";
 import "react-quill/dist/quill.snow.css";
 
@@ -37,10 +36,7 @@ const DiscussionCreateWidget = ({
   const intl = useIntl();
   const formRef = useRef<ProFormInstance>();
   const _currUser = useAppSelector(_currentUser);
-  const formItemLayout = {
-    labelCol: { span: 4 },
-    wrapperCol: { span: 20 },
-  };
+
   if (typeof _currUser === "undefined") {
     return <></>;
   } else {
@@ -49,20 +45,7 @@ const DiscussionCreateWidget = ({
         <div>{_currUser?.nickName}:</div>
         <div>
           <ProForm<IComment>
-            {...formItemLayout}
-            layout="horizontal"
             formRef={formRef}
-            submitter={{
-              render: (props, doms) => {
-                return (
-                  <Row>
-                    <Col span={14} offset={4}>
-                      <Space>{doms}</Space>
-                    </Col>
-                  </Row>
-                );
-              },
-            }}
             onFinish={async (values) => {
               //新建
               console.log("create", resId, resType, parent);
@@ -115,63 +98,70 @@ const DiscussionCreateWidget = ({
             }}
             params={{}}
           >
-            {parent ? (
-              <></>
-            ) : (
+            <ProForm.Group>
               <ProFormText
                 name="title"
+                hidden={typeof parent !== "undefined"}
                 label={intl.formatMessage({ id: "forms.fields.title.label" })}
                 tooltip="最长为 24 位"
                 placeholder={intl.formatMessage({
                   id: "forms.message.title.required",
                 })}
-                rules={[{ required: true, message: "这是必填项" }]}
-              />
-            )}
-            {contentType === "text" ? (
-              <ProFormTextArea
-                name="content"
-                label={intl.formatMessage({ id: "forms.fields.content.label" })}
-                placeholder={intl.formatMessage({
-                  id: "forms.fields.content.placeholder",
-                })}
+                rules={[{ required: parent ? false : true }]}
               />
-            ) : contentType === "html" ? (
-              <Form.Item
-                name="content"
-                label={intl.formatMessage({ id: "forms.fields.content.label" })}
-                tooltip="可以直接粘贴屏幕截图"
-              >
-                <ReactQuill
-                  theme="snow"
-                  style={{ height: 180 }}
-                  modules={{
-                    toolbar: [
-                      ["bold", "italic", "underline", "strike"],
-                      ["blockquote", "code-block"],
-                      [{ header: 1 }, { header: 2 }],
-                      [{ list: "ordered" }, { list: "bullet" }],
-                      [{ indent: "-1" }, { indent: "+1" }],
-                      [{ size: ["small", false, "large", "huge"] }],
-                      [{ header: [1, 2, 3, 4, 5, 6, false] }],
-                      ["link", "image", "video"],
-                      [{ color: [] }, { background: [] }],
-                      [{ font: [] }],
-                      [{ align: [] }],
-                    ],
-                  }}
+            </ProForm.Group>
+            <ProForm.Group>
+              {contentType === "text" ? (
+                <ProFormTextArea
+                  name="content"
+                  label={intl.formatMessage({
+                    id: "forms.fields.content.label",
+                  })}
+                  placeholder={intl.formatMessage({
+                    id: "forms.fields.content.placeholder",
+                  })}
                 />
-              </Form.Item>
-            ) : contentType === "markdown" ? (
-              <Form.Item
-                name="content"
-                label={intl.formatMessage({ id: "forms.fields.content.label" })}
-              >
-                <MDEditor />
-              </Form.Item>
-            ) : (
-              <></>
-            )}
+              ) : contentType === "html" ? (
+                <Form.Item
+                  name="content"
+                  label={intl.formatMessage({
+                    id: "forms.fields.content.label",
+                  })}
+                  tooltip="可以直接粘贴屏幕截图"
+                >
+                  <ReactQuill
+                    theme="snow"
+                    style={{ height: 180 }}
+                    modules={{
+                      toolbar: [
+                        ["bold", "italic", "underline", "strike"],
+                        ["blockquote", "code-block"],
+                        [{ header: 1 }, { header: 2 }],
+                        [{ list: "ordered" }, { list: "bullet" }],
+                        [{ indent: "-1" }, { indent: "+1" }],
+                        [{ size: ["small", false, "large", "huge"] }],
+                        [{ header: [1, 2, 3, 4, 5, 6, false] }],
+                        ["link", "image", "video"],
+                        [{ color: [] }, { background: [] }],
+                        [{ font: [] }],
+                        [{ align: [] }],
+                      ],
+                    }}
+                  />
+                </Form.Item>
+              ) : contentType === "markdown" ? (
+                <Form.Item
+                  name="content"
+                  label={intl.formatMessage({
+                    id: "forms.fields.content.label",
+                  })}
+                >
+                  <MDEditor />
+                </Form.Item>
+              ) : (
+                <></>
+              )}
+            </ProForm.Group>
           </ProForm>
         </div>
       </div>

+ 93 - 66
dashboard/src/components/discussion/DiscussionEdit.tsx

@@ -1,87 +1,114 @@
 import { useIntl } from "react-intl";
-import { Button, Card } from "antd";
+import { Button, Card, Form } from "antd";
 import { message } from "antd";
-import { ProForm, ProFormTextArea } from "@ant-design/pro-components";
+import { ProForm, ProFormText } from "@ant-design/pro-components";
 import { Col, Row, Space } from "antd";
+import { CloseOutlined } from "@ant-design/icons";
 
 import { IComment } from "./DiscussionItem";
 import { put } from "../../request";
 import { ICommentRequest, ICommentResponse } from "../api/Comment";
+import MDEditor from "@uiw/react-md-editor";
 
 interface IWidget {
   data: IComment;
   onCreated?: Function;
+  onUpdated?: Function;
+  onClose?: Function;
 }
-const DiscussionEditWidget = ({ data, onCreated }: IWidget) => {
+const DiscussionEditWidget = ({
+  data,
+  onCreated,
+  onUpdated,
+  onClose,
+}: IWidget) => {
   const intl = useIntl();
-  const formItemLayout = {
-    labelCol: { span: 4 },
-    wrapperCol: { span: 20 },
-  };
+
   return (
-    <div>
-      <Card
-        title={<span>{data.user.nickName}</span>}
-        extra={
-          <Button shape="circle" size="small">
-            xxx
-          </Button>
-        }
-        style={{ width: "auto" }}
-      >
-        <ProForm<IComment>
-          {...formItemLayout}
-          layout="horizontal"
-          submitter={{
-            render: (props, doms) => {
-              return (
-                <Row>
-                  <Col span={14} offset={4}>
-                    <Space>{doms}</Space>
-                  </Col>
-                </Row>
-              );
-            },
+    <Card
+      title={<span>{data.user.nickName}</span>}
+      extra={
+        <Button
+          shape="circle"
+          size="small"
+          icon={<CloseOutlined />}
+          onClick={() => {
+            if (typeof onClose !== "undefined") {
+              onClose();
+            }
           }}
-          onFinish={async (values) => {
-            //新建
-            put<ICommentRequest, ICommentResponse>(
-              `/v2/discussion/${data.id}`,
-              {
-                title: values.title,
-                content: values.content,
-              }
-            )
-              .then((json) => {
-                console.log(json);
-                if (json.ok) {
-                  console.log(intl.formatMessage({ id: "flashes.success" }));
-                  if (typeof onCreated !== "undefined") {
-                    onCreated(json.data);
-                  }
-                } else {
-                  message.error(json.message);
+        />
+      }
+      style={{ width: "auto" }}
+    >
+      <ProForm<IComment>
+        submitter={{
+          render: (props, doms) => {
+            return (
+              <Row>
+                <Col span={14} offset={4}>
+                  <Space>{doms}</Space>
+                </Col>
+              </Row>
+            );
+          },
+        }}
+        onFinish={async (values) => {
+          put<ICommentRequest, ICommentResponse>(`/v2/discussion/${data.id}`, {
+            title: values.title,
+            content: values.content,
+          })
+            .then((json) => {
+              console.log(json);
+              if (json.ok) {
+                console.log(intl.formatMessage({ id: "flashes.success" }));
+                if (typeof onUpdated !== "undefined") {
+                  const newData = {
+                    id: json.data.id, //id未提供为新建
+                    resId: json.data.res_id,
+                    resType: json.data.res_type,
+                    user: json.data.editor,
+                    parent: json.data.parent,
+                    title: json.data.title,
+                    content: json.data.content,
+                    childrenCount: json.data.children_count,
+                    createdAt: json.data.created_at,
+                    updatedAt: json.data.updated_at,
+                  };
+                  onUpdated(newData);
                 }
-              })
-              .catch((e) => {
-                message.error(e.message);
-              });
-          }}
-          params={{}}
-          request={async () => {
-            return data;
-          }}
-        >
-          <ProFormTextArea
+              } else {
+                message.error(json.message);
+              }
+            })
+            .catch((e) => {
+              message.error(e.message);
+            });
+        }}
+        params={{}}
+        request={async () => {
+          return data;
+        }}
+      >
+        <ProForm.Group>
+          <ProFormText
+            name="title"
+            hidden={data.parent ? true : false}
+            label={intl.formatMessage({ id: "forms.fields.title.label" })}
+            tooltip="最长为 24 位"
+            rules={[{ required: data.parent ? false : true }]}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <Form.Item
             name="content"
             label={intl.formatMessage({ id: "forms.fields.content.label" })}
-            placeholder={intl.formatMessage({
-              id: "forms.fields.content.placeholder",
-            })}
-          />
-        </ProForm>
-      </Card>
-    </div>
+          >
+            <MDEditor style={{ width: "100%" }} />
+          </Form.Item>
+        </ProForm.Group>
+      </ProForm>
+    </Card>
   );
 };
 

+ 34 - 9
dashboard/src/components/discussion/DiscussionItem.tsx

@@ -1,8 +1,8 @@
 import { Avatar } from "antd";
 import { useState } from "react";
 import { IUser } from "../auth/User";
-import CommentShow from "./DiscussionShow";
-import CommentEdit from "./DiscussionEdit";
+import DiscussionShow from "./DiscussionShow";
+import DiscussionEdit from "./DiscussionEdit";
 import { TResType } from "./DiscussionListCard";
 
 export interface IComment {
@@ -10,7 +10,7 @@ export interface IComment {
   resId?: string;
   resType?: TResType;
   user: IUser;
-  parent?: string;
+  parent?: string | null;
   title?: string;
   content?: string;
   children?: IComment[];
@@ -22,30 +22,55 @@ interface IWidget {
   data: IComment;
   onSelect?: Function;
   onCreated?: Function;
+  onDelete?: Function;
 }
-const DiscussionItemWidget = ({ data, onSelect, onCreated }: IWidget) => {
+const DiscussionItemWidget = ({
+  data,
+  onSelect,
+  onCreated,
+  onDelete,
+}: IWidget) => {
   const [edit, setEdit] = useState(false);
+  const [currData, setCurrData] = useState<IComment>(data);
   return (
-    <div style={{ display: "flex" }}>
+    <div style={{ display: "flex", width: "100%" }}>
       <div style={{ width: "2em" }}>
         <Avatar size="small">{data.user?.nickName?.slice(0, 1)}</Avatar>
       </div>
       <div style={{ width: "100%" }}>
         {edit ? (
-          <CommentEdit
-            data={data}
+          <DiscussionEdit
+            data={currData}
+            onUpdated={(e: IComment) => {
+              setCurrData(e);
+              setEdit(false);
+            }}
             onCreated={(e: IComment) => {
               if (typeof onCreated !== "undefined") {
                 onCreated(e);
               }
             }}
+            onClose={() => setEdit(false)}
           />
         ) : (
-          <CommentShow
-            data={data}
+          <DiscussionShow
+            data={currData}
             onEdit={() => {
               setEdit(true);
             }}
+            onSelect={(
+              e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
+              data: IComment
+            ) => {
+              if (typeof onSelect !== "undefined") {
+                onSelect(e, data);
+              }
+            }}
+            onDelete={(id: string) => {
+              if (typeof onDelete !== "undefined") {
+                onDelete();
+              }
+            }}
           />
         )}
       </div>

+ 33 - 44
dashboard/src/components/discussion/DiscussionList.tsx

@@ -1,55 +1,44 @@
-import { List, Space } from "antd";
-import { MessageOutlined } from "@ant-design/icons";
+import { List } from "antd";
 
-import { IComment } from "./DiscussionItem";
+import DiscussionItem, { IComment } from "./DiscussionItem";
 
 interface IWidget {
   data: IComment[];
   onSelect?: Function;
+  onDelete?: Function;
 }
-const DiscussionListWidget = ({ data, onSelect }: IWidget) => {
+const DiscussionListWidget = ({ data, onSelect, onDelete }: IWidget) => {
   return (
-    <div>
-      <List
-        pagination={{
-          onChange: (page) => {
-            console.log(page);
-          },
-          pageSize: 10,
-        }}
-        itemLayout="horizontal"
-        dataSource={data}
-        renderItem={(item) => (
-          <List.Item
-            actions={[
-              item.childrenCount ? (
-                <Space>
-                  <MessageOutlined /> {item.childrenCount}
-                </Space>
-              ) : (
-                <></>
-              ),
-            ]}
-          >
-            <List.Item.Meta
-              avatar={<></>}
-              title={
-                <span
-                  onClick={(e) => {
-                    if (typeof onSelect !== "undefined") {
-                      onSelect(e, item);
-                    }
-                  }}
-                >
-                  {item.title ? item.title : item.content?.slice(0, 20)}
-                </span>
+    <List
+      pagination={{
+        onChange: (page) => {
+          console.log(page);
+        },
+        pageSize: 10,
+      }}
+      itemLayout="horizontal"
+      dataSource={data}
+      renderItem={(item) => (
+        <List.Item>
+          <DiscussionItem
+            data={item}
+            onSelect={(
+              e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
+              data: IComment
+            ) => {
+              if (typeof onSelect !== "undefined") {
+                onSelect(e, data);
               }
-              description={item.content?.slice(0, 40)}
-            />
-          </List.Item>
-        )}
-      />
-    </div>
+            }}
+            onDelete={() => {
+              if (typeof onDelete !== "undefined") {
+                onDelete(item.id);
+              }
+            }}
+          />
+        </List.Item>
+      )}
+    />
   );
 };
 

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

@@ -1,12 +1,12 @@
 import { useState, useEffect } from "react";
 import { useIntl } from "react-intl";
-import { Card, message, Typography } from "antd";
+import { Card, message, Skeleton, Typography } from "antd";
 
 import { get } from "../../request";
 import { ICommentListResponse } from "../api/Comment";
 import CommentCreate from "./DiscussionCreate";
 import { IComment } from "./DiscussionItem";
-import CommentList from "./DiscussionList";
+import DiscussionList from "./DiscussionList";
 import { IAnswerCount } from "./DiscussionBox";
 
 export type TResType = "article" | "channel" | "chapter" | "sentence" | "wbw";
@@ -28,6 +28,8 @@ const DiscussionListCardWidget = ({
 }: IWidget) => {
   const intl = useIntl();
   const [data, setData] = useState<IComment[]>([]);
+  const [loading, setLoading] = useState(true);
+
   useEffect(() => {
     console.log("changedAnswerCount", changedAnswerCount);
     const newData = [...data].map((item) => {
@@ -50,6 +52,8 @@ const DiscussionListCardWidget = ({
     if (url === "") {
       return;
     }
+    setLoading(true);
+
     get<ICommentListResponse>(url)
       .then((json) => {
         console.log(json);
@@ -62,6 +66,7 @@ const DiscussionListCardWidget = ({
               resType: item.res_type,
               user: item.editor,
               title: item.title,
+              parent: item.parent,
               content: item.content,
               childrenCount: item.children_count,
               createdAt: item.created_at,
@@ -73,6 +78,9 @@ const DiscussionListCardWidget = ({
           message.error(json.message);
         }
       })
+      .finally(() => {
+        setLoading(false);
+      })
       .catch((e) => {
         message.error(e.message);
       });
@@ -88,8 +96,11 @@ const DiscussionListCardWidget = ({
 
   return (
     <Card title="讨论" extra={"More"}>
-      {data.length > 0 ? (
-        <CommentList
+      {loading ? (
+        <Skeleton title={{ width: 200 }} paragraph={{ rows: 1 }} active />
+      ) : (
+        <DiscussionList
+          data={data}
           onSelect={(
             e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
             comment: IComment
@@ -98,9 +109,16 @@ const DiscussionListCardWidget = ({
               onSelect(e, comment);
             }
           }}
-          data={data}
+          onDelete={(id: string) => {
+            setData((origin) => {
+              return origin.filter((value) => value.id !== id);
+            });
+            if (typeof onItemCountChange !== "undefined") {
+              onItemCountChange(data.length - 1);
+            }
+          }}
         />
-      ) : undefined}
+      )}
 
       {resId && resType ? (
         <CommentCreate
@@ -109,7 +127,6 @@ const DiscussionListCardWidget = ({
           resType={resType}
           onCreated={(e: IComment) => {
             const newData = JSON.parse(JSON.stringify(e));
-            console.log("create", e);
             if (typeof onItemCountChange !== "undefined") {
               onItemCountChange(data.length + 1);
             }

+ 128 - 23
dashboard/src/components/discussion/DiscussionShow.tsx

@@ -1,18 +1,81 @@
 import { useIntl } from "react-intl";
-import { Button, Card, Dropdown, Space } from "antd";
-import { MoreOutlined } from "@ant-design/icons";
+import {
+  Button,
+  Card,
+  Dropdown,
+  message,
+  Modal,
+  Space,
+  Typography,
+} from "antd";
+import {
+  MoreOutlined,
+  EditOutlined,
+  DeleteOutlined,
+  LinkOutlined,
+  CommentOutlined,
+  MessageOutlined,
+  ExclamationCircleOutlined,
+} from "@ant-design/icons";
 import type { MenuProps } from "antd";
 
 import { IComment } from "./DiscussionItem";
 import TimeShow from "../general/TimeShow";
+import Marked from "../general/Marked";
+import { delete_ } from "../../request";
+import { IDeleteResponse } from "../api/Article";
+
+const { Text } = Typography;
 
 interface IWidget {
   data: IComment;
   onEdit?: Function;
   onSelect?: Function;
+  onDelete?: Function;
 }
-const DiscussionShowWidget = ({ data, onEdit, onSelect }: IWidget) => {
+const DiscussionShowWidget = ({
+  data,
+  onEdit,
+  onSelect,
+  onDelete,
+}: IWidget) => {
   const intl = useIntl();
+  const showDeleteConfirm = (id: string, title: string) => {
+    Modal.confirm({
+      icon: <ExclamationCircleOutlined />,
+      title:
+        intl.formatMessage({
+          id: "message.delete.sure",
+        }) +
+        intl.formatMessage({
+          id: "message.irrevocable",
+        }),
+
+      content: title,
+      okText: intl.formatMessage({
+        id: "buttons.delete",
+      }),
+      okType: "danger",
+      cancelText: intl.formatMessage({
+        id: "buttons.no",
+      }),
+      onOk() {
+        console.log("delete", id);
+        return delete_<IDeleteResponse>(`/v2/discussion/${id}`)
+          .then((json) => {
+            if (json.ok) {
+              message.success("删除成功");
+              if (typeof onDelete !== "undefined") {
+                onDelete(id);
+              }
+            } else {
+              message.error(json.message);
+            }
+          })
+          .catch((e) => console.log("Oops errors!", e));
+      },
+    });
+  };
   const onClick: MenuProps["onClick"] = (e) => {
     console.log("click ", e);
     switch (e.key) {
@@ -21,6 +84,11 @@ const DiscussionShowWidget = ({ data, onEdit, onSelect }: IWidget) => {
           onEdit();
         }
         break;
+      case "delete":
+        if (data.id) {
+          showDeleteConfirm(data.id, data.title ? data.title : "");
+        }
+        break;
       default:
         break;
     }
@@ -30,10 +98,13 @@ const DiscussionShowWidget = ({ data, onEdit, onSelect }: IWidget) => {
     {
       key: "copy-link",
       label: "复制链接",
+      icon: <LinkOutlined />,
     },
     {
       key: "reply",
       label: "回复",
+      icon: <CommentOutlined />,
+      disabled: data.parent ? true : false,
     },
     {
       type: "divider",
@@ -41,10 +112,14 @@ const DiscussionShowWidget = ({ data, onEdit, onSelect }: IWidget) => {
     {
       key: "edit",
       label: "编辑",
+      icon: <EditOutlined />,
     },
     {
       key: "delete",
       label: "删除",
+      icon: <DeleteOutlined />,
+      danger: true,
+      disabled: data.childrenCount ? true : false,
     },
     {
       type: "divider",
@@ -58,32 +133,62 @@ const DiscussionShowWidget = ({ data, onEdit, onSelect }: IWidget) => {
     <Card
       size="small"
       title={
-        <Space>
-          {data.user.nickName}
-          <TimeShow
-            time={data.updatedAt}
-            title={intl.formatMessage({
-              id: "labels.updated-at",
-            })}
-          />
+        <Space direction="vertical">
+          <Text
+            strong
+            onClick={(e) => {
+              if (typeof onSelect !== "undefined") {
+                onSelect(e, data);
+              }
+            }}
+          >
+            {data.title}
+          </Text>
+          <Text type="secondary">
+            <Space>
+              {data.user.nickName}
+              <TimeShow
+                type="secondary"
+                time={data.updatedAt}
+                title={intl.formatMessage({
+                  id: "labels.updated-at",
+                })}
+              />
+            </Space>
+          </Text>
         </Space>
       }
       extra={
-        <Dropdown menu={{ items, onClick }} placement="bottomRight">
-          <Button shape="circle" size="small" icon={<MoreOutlined />}></Button>
-        </Dropdown>
+        <Space>
+          <span
+            style={{
+              display: data.childrenCount === 0 ? "none" : "inline",
+              cursor: "pointer",
+            }}
+            onClick={(e) => {
+              if (typeof onSelect !== "undefined") {
+                onSelect(e, data);
+              }
+            }}
+          >
+            {data.childrenCount ? (
+              <>
+                <MessageOutlined /> {data.childrenCount}
+              </>
+            ) : undefined}
+          </span>
+          <Dropdown menu={{ items, onClick }} placement="bottomRight">
+            <Button
+              shape="circle"
+              size="small"
+              icon={<MoreOutlined />}
+            ></Button>
+          </Dropdown>
+        </Space>
       }
       style={{ width: "100%" }}
     >
-      <span
-        onClick={(e) => {
-          if (typeof onSelect !== "undefined") {
-            onSelect();
-          }
-        }}
-      >
-        {data.content}
-      </span>
+      <Marked text={data.content} />
     </Card>
   );
 };

+ 4 - 4
dashboard/src/components/discussion/DiscussionTopic.tsx

@@ -1,7 +1,7 @@
 import { Divider } from "antd";
 
-import CommentTopicInfo from "./DiscussionTopicInfo";
-import CommentTopicChildren from "./DiscussionTopicChildren";
+import DiscussionTopicInfo from "./DiscussionTopicInfo";
+import DiscussionTopicChildren from "./DiscussionTopicChildren";
 import { IComment } from "./DiscussionItem";
 
 interface IWidget {
@@ -16,7 +16,7 @@ const DiscussionTopicWidget = ({
 }: IWidget) => {
   return (
     <>
-      <CommentTopicInfo
+      <DiscussionTopicInfo
         topicId={topicId}
         onReady={(value: IComment) => {
           console.log("on Topic Ready", value);
@@ -26,7 +26,7 @@ const DiscussionTopicWidget = ({
         }}
       />
       <Divider />
-      <CommentTopicChildren
+      <DiscussionTopicChildren
         topicId={topicId}
         onItemCountChange={(count: number, e: string) => {
           //把新建回答的消息传出去。

+ 46 - 30
dashboard/src/components/discussion/DiscussionTopicChildren.tsx

@@ -1,11 +1,11 @@
-import { List, message } from "antd";
+import { List, message, Skeleton } from "antd";
 import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
 import { get } from "../../request";
 import { ICommentListResponse } from "../api/Comment";
-import CommentCreate from "./DiscussionCreate";
+import DiscussionCreate from "./DiscussionCreate";
 
-import CommentItem, { IComment } from "./DiscussionItem";
+import DiscussionItem, { IComment } from "./DiscussionItem";
 
 interface IWidget {
   topicId?: string;
@@ -16,22 +16,27 @@ const DiscussionTopicChildrenWidget = ({
   onItemCountChange,
 }: IWidget) => {
   const intl = useIntl();
-  const [data, setData] = useState<IComment[]>();
+  const [data, setData] = useState<IComment[]>([]);
+  const [loading, setLoading] = useState(true);
+
   useEffect(() => {
     if (typeof topicId === "undefined") {
       return;
     }
+    setLoading(true);
+
     get<ICommentListResponse>(`/v2/discussion?view=answer&id=${topicId}`)
       .then((json) => {
         console.log(json);
         if (json.ok) {
-          console.log(intl.formatMessage({ id: "flashes.success" }));
+          console.log("ok", json.data);
           const discussions: IComment[] = json.data.rows.map((item) => {
             return {
               id: item.id,
               resId: item.res_id,
               resType: item.res_type,
               user: item.editor,
+              parent: item.parent,
               title: item.title,
               content: item.content,
               createdAt: item.created_at,
@@ -43,43 +48,54 @@ const DiscussionTopicChildrenWidget = ({
           message.error(json.message);
         }
       })
+      .finally(() => {
+        setLoading(false);
+      })
       .catch((e) => {
         message.error(e.message);
       });
   }, [intl, topicId]);
   return (
     <div>
-      <List
-        pagination={{
-          onChange: (page) => {
-            console.log(page);
-          },
-          pageSize: 10,
-        }}
-        itemLayout="horizontal"
-        dataSource={data}
-        renderItem={(item) => (
-          <List.Item>
-            <CommentItem data={item} />
-          </List.Item>
-        )}
-      />
-      <CommentCreate
+      {loading ? (
+        <Skeleton title={{ width: 200 }} paragraph={{ rows: 1 }} active />
+      ) : (
+        <List
+          pagination={{
+            onChange: (page) => {
+              console.log(page);
+            },
+            pageSize: 10,
+          }}
+          itemLayout="horizontal"
+          dataSource={data}
+          renderItem={(item) => (
+            <List.Item>
+              <DiscussionItem
+                data={item}
+                onDelete={() => {
+                  console.log("delete", item.id, data);
+                  if (typeof onItemCountChange !== "undefined") {
+                    onItemCountChange(data.length - 1, item.parent);
+                  }
+                  setData((origin) => {
+                    return origin.filter((value) => value.id !== item.id);
+                  });
+                }}
+              />
+            </List.Item>
+          )}
+        />
+      )}
+      <DiscussionCreate
         contentType="markdown"
         parent={topicId}
         onCreated={(e: IComment) => {
           console.log("create", e);
           const newData = JSON.parse(JSON.stringify(e));
-          let count = 0;
-          if (typeof data === "undefined") {
-            count = 1;
-            setData([newData]);
-          } else {
-            count = data.length + 1;
-            setData([...data, newData]);
-          }
+          setData([...data, newData]);
           if (typeof onItemCountChange !== "undefined") {
-            onItemCountChange(count, e.parent);
+            onItemCountChange(data.length + 1, e.parent);
           }
         }}
       />

+ 7 - 1
dashboard/src/components/template/MdView.tsx

@@ -8,6 +8,7 @@ interface IWidget {
   placeholder?: string;
   wordWidget?: boolean;
   convertor?: TCodeConvertor;
+  style?: React.CSSProperties;
 }
 const Widget = ({
   html,
@@ -15,9 +16,14 @@ const Widget = ({
   wordWidget = false,
   placeholder,
   convertor,
+  style,
 }: IWidget) => {
   const jsx = html ? XmlToReact(html, wordWidget, convertor) : placeholder;
-  return <Paragraph className={className}>{jsx}</Paragraph>;
+  return (
+    <Paragraph style={style} className={className}>
+      {jsx}
+    </Paragraph>
+  );
 };
 
 export default Widget;

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

@@ -55,6 +55,7 @@ export interface IWidgetSentEditInner {
   commNum?: number;
   originNum: number;
   simNum?: number;
+  compact?: boolean;
 }
 export const SentEditInner = ({
   id,
@@ -72,10 +73,12 @@ export const SentEditInner = ({
   commNum,
   originNum,
   simNum,
+  compact = false,
 }: IWidgetSentEditInner) => {
   const [wbwData, setWbwData] = useState<IWbw[]>();
   const [magicDict, setMagicDict] = useState<string>();
   const [magicDictLoading, setMagicDictLoading] = useState(false);
+  const [isCompact, setIsCompact] = useState(compact);
 
   useEffect(() => {
     const content = origin?.find(
@@ -104,6 +107,7 @@ export const SentEditInner = ({
         translation={translation}
         layout={layout}
         magicDict={magicDict}
+        compact={isCompact}
         onWbwChange={(data: IWbw[]) => {
           setWbwData(data);
         }}
@@ -127,10 +131,12 @@ export const SentEditInner = ({
         simNum={simNum}
         wbwData={wbwData}
         magicDictLoading={magicDictLoading}
+        compact={isCompact}
         onMagicDict={(type: string) => {
           setMagicDict(type);
           setMagicDictLoading(true);
         }}
+        onCompact={(value: boolean) => setIsCompact(value)}
       />
     </Card>
   );

+ 25 - 15
dashboard/src/components/template/SentEdit/EditInfo.tsx

@@ -12,28 +12,38 @@ const { Text } = Typography;
 interface IWidget {
   data: ISentence;
   isPr?: boolean;
+  compact?: boolean;
 }
-const EditInfoWidget = ({ data, isPr = false }: IWidget) => {
+const EditInfoWidget = ({ data, isPr = false, compact = false }: IWidget) => {
+  const details = (
+    <Space>
+      <Channel {...data.channel} />
+      <User {...data.editor} showAvatar={isPr ? true : false} />
+      <span>edit</span>
+      {data.prEditAt ? (
+        <TimeShow time={data.prEditAt} />
+      ) : (
+        <TimeShow time={data.updateAt} />
+      )}
+      {data.acceptor ? (
+        <User {...data.acceptor} showAvatar={false} />
+      ) : undefined}
+      {data.acceptor ? "accept at" : undefined}
+      {data.prEditAt ? <TimeShow time={data.updateAt} /> : undefined}
+    </Space>
+  );
   return (
     <div style={{ fontSize: "80%" }}>
       <Text type="secondary">
         <Space>
           {isPr ? undefined : (
-            <StudioName data={data.studio} showName={false} />
-          )}
-          <Channel {...data.channel} />
-          <User {...data.editor} showAvatar={isPr ? true : false} />
-          <span>edit</span>
-          {data.prEditAt ? (
-            <TimeShow time={data.prEditAt} />
-          ) : (
-            <TimeShow time={data.updateAt} />
+            <StudioName
+              data={data.studio}
+              showName={false}
+              popOver={compact ? details : undefined}
+            />
           )}
-          {data.acceptor ? (
-            <User {...data.acceptor} showAvatar={false} />
-          ) : undefined}
-          {data.acceptor ? "accept at" : undefined}
-          {data.prEditAt ? <TimeShow time={data.updateAt} /> : undefined}
+          {compact ? undefined : details}
         </Space>
       </Text>
     </div>

+ 58 - 37
dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -6,7 +6,7 @@ import SentCellEditable from "./SentCellEditable";
 import MdView from "../MdView";
 import EditInfo from "./EditInfo";
 import SuggestionToolbar from "./SuggestionToolbar";
-import { Divider } from "antd";
+import { Divider, Space } from "antd";
 import { useAppSelector } from "../../../hooks";
 import { sentence } from "../../../reducers/accept-pr";
 import { IWbw } from "../Wbw/WbwWord";
@@ -21,18 +21,21 @@ interface IWidget {
   wordWidget?: boolean;
   isPr?: boolean;
   editMode?: boolean;
+  compact?: boolean;
 }
 const SentCellWidget = ({
   data,
   wordWidget = false,
   isPr = false,
   editMode = false,
+  compact = false,
 }: IWidget) => {
   const intl = useIntl();
   const [isEditMode, setIsEditMode] = useState(editMode);
   const [sentData, setSentData] = useState<ISentence>(data);
   const endings = useAppSelector(getEnding);
   const acceptPr = useAppSelector(sentence);
+  const [prOpen, setPrOpen] = useState(false);
 
   useEffect(() => {
     setSentData(data);
@@ -68,6 +71,16 @@ const SentCellWidget = ({
             setIsEditMode(true);
           }
         }}
+        onMenuClick={(key: string) => {
+          switch (key) {
+            case "suggestion":
+              setPrOpen(true);
+              break;
+
+            default:
+              break;
+          }
+        }}
         onConvert={(format: string) => {
           switch (format) {
             case "json":
@@ -130,47 +143,55 @@ const SentCellWidget = ({
           }
         }}
       >
-        <EditInfo data={sentData} />
-        {isEditMode ? (
-          <div>
-            {sentData.contentType === "json" ? (
-              <SentWbwEdit
-                data={sentData}
-                onClose={() => {
-                  setIsEditMode(false);
-                }}
-                onSave={(data: ISentence) => {
-                  setSentData(data);
-                }}
-              />
-            ) : (
-              <SentCellEditable
-                data={sentData}
-                isPr={isPr}
-                onClose={() => {
-                  setIsEditMode(false);
-                }}
-                onSave={(data: ISentence) => {
-                  setSentData(data);
-                  setIsEditMode(false);
-                }}
-              />
-            )}
-          </div>
-        ) : (
-          <div style={{ marginLeft: "2em" }}>
+        <Space
+          direction={compact ? "horizontal" : "vertical"}
+          style={{ alignItems: "flex-start" }}
+        >
+          <EditInfo data={sentData} compact={compact} />
+          {isEditMode ? (
+            <div>
+              {sentData.contentType === "json" ? (
+                <SentWbwEdit
+                  data={sentData}
+                  onClose={() => {
+                    setIsEditMode(false);
+                  }}
+                  onSave={(data: ISentence) => {
+                    setSentData(data);
+                  }}
+                />
+              ) : (
+                <SentCellEditable
+                  data={sentData}
+                  isPr={isPr}
+                  onClose={() => {
+                    setIsEditMode(false);
+                  }}
+                  onSave={(data: ISentence) => {
+                    setSentData(data);
+                    setIsEditMode(false);
+                  }}
+                />
+              )}
+            </div>
+          ) : (
             <MdView
+              style={{ marginLeft: compact ? 0 : "2em" }}
               html={sentData.html !== "" ? sentData.html : "请输入"}
               wordWidget={wordWidget}
             />
-          </div>
-        )}
-
-        <div style={{ marginLeft: "2em" }}>
-          <SuggestionToolbar data={sentData} isPr={isPr} />
-        </div>
+          )}
+          <SuggestionToolbar
+            style={{ marginLeft: "2em" }}
+            compact={compact}
+            data={sentData}
+            isPr={isPr}
+            prOpen={prOpen}
+            onPrClose={() => setPrOpen(false)}
+          />
+        </Space>
       </SentEditMenu>
-      <Divider style={{ margin: "10px 0" }} />
+      {compact ? undefined : <Divider style={{ margin: "10px 0" }} />}
     </div>
   );
 };

+ 4 - 2
dashboard/src/components/template/SentEdit/SentContent.tsx

@@ -23,6 +23,7 @@ interface IWidgetSentContent {
   translation?: ISentence[];
   layout?: TDirection;
   magicDict?: string;
+  compact?: boolean;
   onWbwChange?: Function;
   onMagicDictDone?: Function;
 }
@@ -35,6 +36,7 @@ const SentContentWidget = ({
   origin,
   translation,
   layout = "column",
+  compact = false,
   magicDict,
   onWbwChange,
   onMagicDictDone,
@@ -97,7 +99,7 @@ const SentContentWidget = ({
       style={{
         display: "flex",
         flexDirection: layoutDirection,
-        marginBottom: 10,
+        marginBottom: 0,
       }}
     >
       <div
@@ -137,7 +139,7 @@ const SentContentWidget = ({
       </div>
       <div style={{ flex: layoutFlex.right }}>
         {translation?.map((item, id) => {
-          return <SentCell key={id} data={item} />;
+          return <SentCell key={id} data={item} compact={compact} />;
         })}
       </div>
     </div>

+ 38 - 7
dashboard/src/components/template/SentEdit/SentEditMenu.tsx

@@ -1,4 +1,4 @@
-import { Button, Dropdown } from "antd";
+import { Button, Dropdown, message } from "antd";
 import { useState } from "react";
 import {
   EditOutlined,
@@ -6,28 +6,35 @@ import {
   MoreOutlined,
   FieldTimeOutlined,
   LinkOutlined,
+  CommentOutlined,
+  FileMarkdownOutlined,
 } from "@ant-design/icons";
 import type { MenuProps } from "antd";
 import { ISentence } from "../SentEdit";
 import SentHistoryModal from "../../corpus/SentHistoryModal";
+import { HandOutlinedIcon, JsonOutlinedIcon } from "../../../assets/icon";
 
 interface IWidget {
   data: ISentence;
   children?: React.ReactNode;
   onModeChange?: Function;
   onConvert?: Function;
+  onMenuClick?: Function;
 }
 const SentEditMenuWidget = ({
   data,
   children,
   onModeChange,
   onConvert,
+  onMenuClick,
 }: IWidget) => {
   const [isHover, setIsHover] = useState(false);
   const [timelineOpen, setTimelineOpen] = useState(false);
 
   const onClick: MenuProps["onClick"] = (e) => {
-    console.log(e);
+    if (typeof onMenuClick !== "undefined") {
+      onMenuClick(e.key);
+    }
     switch (e.key) {
       case "json":
         if (typeof onConvert !== "undefined") {
@@ -55,14 +62,29 @@ const SentEditMenuWidget = ({
     {
       type: "divider",
     },
+    {
+      key: "suggestion",
+      label: "suggestion",
+      icon: <HandOutlinedIcon />,
+    },
+    {
+      key: "discussion",
+      label: "discussion",
+      icon: <CommentOutlined />,
+    },
+    {
+      type: "divider",
+    },
     {
       key: "markdown",
-      label: "Markdown",
+      label: "To Markdown",
+      icon: <FileMarkdownOutlined />,
       disabled: data.contentType === "markdown",
     },
     {
       key: "json",
-      label: "Json",
+      label: "To Json",
+      icon: <JsonOutlinedIcon />,
       disabled: data.contentType === "json",
     },
     {
@@ -91,8 +113,8 @@ const SentEditMenuWidget = ({
       />
       <div
         style={{
-          marginTop: "-1.2em",
-          right: "0",
+          marginTop: "-4.2em",
+          right: 30,
           position: "absolute",
           display: isHover ? "block" : "none",
         }}
@@ -100,13 +122,22 @@ const SentEditMenuWidget = ({
         <Button
           icon={<EditOutlined />}
           size="small"
+          title="edit"
           onClick={() => {
             if (typeof onModeChange !== "undefined") {
               onModeChange("edit");
             }
           }}
         />
-        <Button icon={<CopyOutlined />} size="small" />
+        <Button
+          icon={<CopyOutlined />}
+          size="small"
+          onClick={() => {
+            navigator.clipboard.writeText(data.content).then(() => {
+              message.success("已经拷贝到剪贴板");
+            });
+          }}
+        />
         <Dropdown menu={{ items, onClick }} placement="bottomRight">
           <Button icon={<MoreOutlined />} size="small" />
         </Dropdown>

+ 22 - 2
dashboard/src/components/template/SentEdit/SentMenu.tsx

@@ -1,4 +1,4 @@
-import { Button, Dropdown } from "antd";
+import { Badge, Button, Dropdown, Space } from "antd";
 import { MoreOutlined } from "@ant-design/icons";
 import type { MenuProps } from "antd";
 import RelatedPara from "../../corpus/RelatedPara";
@@ -8,12 +8,14 @@ interface ISentMenu {
   para?: number;
   loading?: boolean;
   onMagicDict?: Function;
+  onMenuClick?: Function;
 }
 const SentMenuWidget = ({
   book,
   para,
   loading = false,
   onMagicDict,
+  onMenuClick,
 }: ISentMenu) => {
   const items: MenuProps["items"] = [
     {
@@ -36,16 +38,34 @@ const SentMenuWidget = ({
       key: "copy-link",
       label: "复制句子链接",
     },
+    {
+      type: "divider",
+    },
+    {
+      key: "compact",
+      label: (
+        <Space>
+          {"紧凑"}
+          <Badge count="Beta" showZero color="#faad14" />
+        </Space>
+      ),
+    },
+    {
+      key: "normal",
+      label: "正常",
+    },
   ];
   const onClick: MenuProps["onClick"] = ({ key }) => {
     console.log(`Click on item ${key}`);
+    if (typeof onMenuClick !== "undefined") {
+      onMenuClick(key);
+    }
     switch (key) {
       case "magic-dict-current":
         if (typeof onMagicDict !== "undefined") {
           onMagicDict("current");
         }
         break;
-
       default:
         break;
     }

+ 36 - 11
dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -3,6 +3,7 @@ import {
   TranslationOutlined,
   CloseOutlined,
   BlockOutlined,
+  BarsOutlined,
 } from "@ant-design/icons";
 
 import SentTabButton from "./SentTabButton";
@@ -16,6 +17,7 @@ import SentMenu from "./SentMenu";
 import { IChapter } from "../../corpus/BookViewer";
 import store from "../../../store";
 import { change } from "../../../reducers/para-change";
+import { useEffect, useState } from "react";
 
 const { Text } = Typography;
 
@@ -35,7 +37,9 @@ interface IWidget {
   simNum?: number;
   wbwData?: IWbw[];
   magicDictLoading?: boolean;
+  compact?: boolean;
   onMagicDict?: Function;
+  onCompact?: Function;
 }
 const SentTabWidget = ({
   id,
@@ -52,9 +56,13 @@ const SentTabWidget = ({
   simNum = 0,
   wbwData,
   magicDictLoading = false,
+  compact = false,
   onMagicDict,
+  onCompact,
 }: IWidget) => {
   const intl = useIntl();
+  const [isCompact, setIsCompact] = useState(compact);
+  useEffect(() => setIsCompact(compact), [compact]);
   const mPath = path
     ? [
         ...path,
@@ -70,6 +78,16 @@ const SentTabWidget = ({
   return (
     <>
       <Tabs
+        style={
+          isCompact
+            ? {
+                position: "absolute",
+                marginTop: -32,
+                width: "100%",
+                marginRight: 10,
+              }
+            : undefined
+        }
         tabBarStyle={{ marginBottom: 0 }}
         size="small"
         tabBarGutter={0}
@@ -79,17 +97,6 @@ const SentTabWidget = ({
               link="none"
               data={mPath}
               trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}
-              onChange={(para: IChapter) => {
-                //点击章节目录
-                const type = para.level
-                  ? para.level < 8
-                    ? "chapter"
-                    : "para"
-                  : "para";
-                store.dispatch(
-                  change({ book: para.book, para: para.para, type: type })
-                );
-              }}
             />
             <Text copyable={{ text: `{{${sentId[0]}}}` }}>{sentId[0]}</Text>
             <SentMenu
@@ -101,6 +108,24 @@ const SentTabWidget = ({
                   onMagicDict(type);
                 }
               }}
+              onMenuClick={(key: string) => {
+                switch (key) {
+                  case "compact" || "normal":
+                    if (typeof onCompact !== "undefined") {
+                      setIsCompact(true);
+                      onCompact(true);
+                    }
+                    break;
+                  case "normal":
+                    if (typeof onCompact !== "undefined") {
+                      setIsCompact(false);
+                      onCompact(false);
+                    }
+                    break;
+                  default:
+                    break;
+                }
+              }}
             />
           </Space>
         }

+ 18 - 7
dashboard/src/components/template/SentEdit/SuggestionBox.tsx

@@ -13,13 +13,21 @@ export interface IAnswerCount {
 interface IWidget {
   data: ISentence;
   trigger?: JSX.Element;
+  open?: boolean;
+  onClose?: Function;
 }
-const SuggestionBoxWidget = ({ trigger, data }: IWidget) => {
-  const [open, setOpen] = useState(false);
+const SuggestionBoxWidget = ({
+  trigger,
+  data,
+  open = false,
+  onClose,
+}: IWidget) => {
+  const [isOpen, setIsOpen] = useState(open);
   const [reload, setReload] = useState(false);
   const [openNotification, setOpenNotification] = useState(false);
   const [prNumber, setPrNumber] = useState(data.suggestionCount?.suggestion);
 
+  useEffect(() => setIsOpen(open), [open]);
   useEffect(() => {
     if (localStorage.getItem("read_pr_Notification") === "ok") {
       setOpenNotification(false);
@@ -28,11 +36,14 @@ const SuggestionBoxWidget = ({ trigger, data }: IWidget) => {
     }
   }, []);
   const showDrawer = () => {
-    setOpen(true);
+    setIsOpen(true);
   };
 
-  const onClose = () => {
-    setOpen(false);
+  const onBoxClose = () => {
+    setIsOpen(false);
+    if (typeof onClose !== "undefined") {
+      onClose();
+    }
   };
 
   return (
@@ -45,8 +56,8 @@ const SuggestionBoxWidget = ({ trigger, data }: IWidget) => {
       <Drawer
         title="修改建议"
         width={520}
-        onClose={onClose}
-        open={open}
+        onClose={onBoxClose}
+        open={isOpen}
         maskClosable={false}
       >
         <Space direction="vertical" style={{ width: "100%" }}>

+ 29 - 7
dashboard/src/components/template/SentEdit/SuggestionToolbar.tsx

@@ -7,14 +7,26 @@ import SuggestionBox from "./SuggestionBox";
 import PrAcceptButton from "./PrAcceptButton";
 import { HandOutlinedIcon } from "../../../assets/icon";
 
-const { Text } = Typography;
+const { Text, Paragraph } = Typography;
 
 interface IWidget {
   data: ISentence;
   isPr?: boolean;
+  style?: React.CSSProperties;
+  compact?: boolean;
+  prOpen?: boolean;
+  onPrClose?: Function;
   onAccept?: Function;
 }
-const SuggestionToolbarWidget = ({ data, isPr = false, onAccept }: IWidget) => {
+const SuggestionToolbarWidget = ({
+  data,
+  isPr = false,
+  onAccept,
+  style,
+  prOpen = false,
+  compact = false,
+  onPrClose,
+}: IWidget) => {
   const [CommentCount, setCommentCount] = useState<number | undefined>(
     data.suggestionCount?.discussion
   );
@@ -33,8 +45,14 @@ const SuggestionToolbarWidget = ({ data, isPr = false, onAccept }: IWidget) => {
     </Space>
   );
   const normalButton = (
-    <Space>
+    <Space size={"small"}>
       <SuggestionBox
+        open={prOpen}
+        onClose={() => {
+          if (typeof onPrClose !== "undefined") {
+            onPrClose();
+          }
+        }}
         data={data}
         trigger={
           <Tooltip title="修改建议">
@@ -42,7 +60,7 @@ const SuggestionToolbarWidget = ({ data, isPr = false, onAccept }: IWidget) => {
           </Tooltip>
         }
       />
-      <Divider type="vertical" />
+      {compact ? undefined : <Divider type="vertical" />}
       <CommentBox
         resId={data.id}
         resType="sentence"
@@ -56,11 +74,15 @@ const SuggestionToolbarWidget = ({ data, isPr = false, onAccept }: IWidget) => {
         }}
       />
       {CommentCount}
-      <Divider type="vertical" />
-      <Text copyable={{ text: data.content }}></Text>
+      {compact ? undefined : <Divider type="vertical" />}
+      {compact ? undefined : <Text copyable={{ text: data.content }}></Text>}
     </Space>
   );
-  return <Text type="secondary">{isPr ? prButton : normalButton}</Text>;
+  return (
+    <Paragraph type="secondary" style={style}>
+      {isPr ? prButton : normalButton}
+    </Paragraph>
+  );
 };
 
 export default SuggestionToolbarWidget;

+ 5 - 5
dashboard/src/components/template/Term.tsx

@@ -12,6 +12,7 @@ import { changedTerm, refresh } from "../../reducers/term-change";
 import { useAppSelector } from "../../hooks";
 import { get } from "../../request";
 import { Link, useNavigate } from "react-router-dom";
+import { fullUrl } from "../../utils";
 
 const { Text, Title } = Typography;
 
@@ -88,11 +89,10 @@ const TermCtl = ({
               <Space>
                 <Button
                   onClick={() => {
-                    const fullUrl =
-                      process.env.REACT_APP_WEB_HOST +
-                      process.env.PUBLIC_URL +
-                      `/term/list/${termData.word}`;
-                    window.open(fullUrl, "_blank");
+                    window.open(
+                      fullUrl(`/term/list/${termData.word}`),
+                      "_blank"
+                    );
                   }}
                   type="link"
                   size="small"

+ 12 - 2
dashboard/src/pages/library/anthology/show.tsx

@@ -1,4 +1,4 @@
-import { useParams } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
 import { Col, Row } from "antd";
 
 import AnthologyDetail from "../../../components/article/AnthologyDetail";
@@ -6,6 +6,7 @@ import AnthologyDetail from "../../../components/article/AnthologyDetail";
 const Widget = () => {
   // TODO
   const { id } = useParams(); //url 参数
+  const navigate = useNavigate();
 
   const pageMaxWidth = "960px";
   return (
@@ -13,7 +14,16 @@ const Widget = () => {
       <Row>
         <Col flex="auto"></Col>
         <Col flex={pageMaxWidth}>
-          <AnthologyDetail aid={id} />
+          <AnthologyDetail
+            onArticleSelect={(anthologyId: string, keys: string[]) => {
+              if (keys[0]) {
+                navigate(
+                  `/article/article/${keys[0]}?mode=read&anthology=${anthologyId}`
+                );
+              }
+            }}
+            aid={id}
+          />
         </Col>
         <Col flex="auto"></Col>
       </Row>

+ 27 - 11
dashboard/src/pages/library/article/show.tsx

@@ -36,6 +36,7 @@ import { paraParam } from "../../../reducers/para-change";
 import { get } from "../../../request";
 import store from "../../../store";
 import { IRecent } from "../../../components/recent/RecentList";
+import { fullUrl } from "../../../utils";
 
 /**
  * type:
@@ -219,11 +220,7 @@ const Widget = () => {
                       : "";
                     url += param.param?.para ? `&par=${param.param?.para}` : "";
                     if (event.ctrlKey || event.metaKey) {
-                      const fullUrl =
-                        process.env.REACT_APP_WEB_HOST +
-                        process.env.PUBLIC_URL +
-                        url;
-                      window.open(fullUrl, "_blank");
+                      window.open(fullUrl(url), "_blank");
                     } else {
                       navigate(url);
                     }
@@ -232,6 +229,7 @@ const Widget = () => {
                 <ToolButtonToc
                   type={type as ArticleType}
                   articleId={id}
+                  anthologyId={searchParams.get("anthology")}
                   onSelect={(key: Key) => {
                     console.log("toc click", key);
                     let url = `/article/${type}/${key}?`;
@@ -267,21 +265,40 @@ const Widget = () => {
               para={searchParams.get("par")}
               channelId={searchParams.get("channel")}
               articleId={id}
+              anthologyId={searchParams.get("anthology")}
               mode={searchParams.get("mode") as ArticleMode}
-              onArticleChange={(article: string) => {
-                console.log("article change", article);
-                let url = `/article/${type}/${article}?mode=${currMode}`;
+              onArticleChange={(article: string, target?: string) => {
+                console.log("article change", article, target);
+                let mType = type;
+                if (article.split("-").length === 2) {
+                  mType = "chapter";
+                }
+                let url = `/article/${mType}/${article}?mode=${currMode}`;
                 searchParams.forEach((value, key) => {
                   console.log(value, key);
                   if (key !== "mode") {
                     url += `&${key}=${value}`;
                   }
                 });
-                navigate(url);
+                if (target === "_blank") {
+                  window.open(fullUrl(url), "_blank");
+                } else {
+                  navigate(url);
+                }
               }}
               onLoad={(article: IArticleDataResponse) => {
                 setLoadedArticleData(article);
               }}
+              onAnthologySelect={(id: string) => {
+                let output: any = { anthology: id };
+                searchParams.forEach((value, key) => {
+                  console.log(value, key);
+                  if (key !== "anthology") {
+                    output[key] = value;
+                  }
+                });
+                setSearchParams(output);
+              }}
             />
             <Navigate
               type={type as ArticleType}
@@ -292,13 +309,12 @@ const Widget = () => {
               ) => {
                 let url = `/article/${type}/${newId}?mode=${currMode}`;
                 searchParams.forEach((value, key) => {
-                  console.log(value, key);
                   if (key !== "mode") {
                     url += `&${key}=${value}`;
                   }
                 });
                 if (event.ctrlKey || event.metaKey) {
-                  window.open(url, "_blank");
+                  window.open(fullUrl(url), "_blank");
                 } else {
                   navigate(url);
                 }

+ 97 - 102
dashboard/src/pages/library/discussion/list.tsx

@@ -18,117 +18,112 @@ interface IDiscussion {
   createdAt?: string;
 }
 const Widget = () => {
-  // TODO
-
   return (
-    <div>
-      <ProList<IDiscussion>
-        search={{
-          filterType: "light",
-        }}
-        rowKey="id"
-        headerTitle="问答&求助"
-        request={async (params = {}, sorter, filter) => {
-          console.log(params, sorter, filter);
-          const url = `/v2/discussion?view=all`;
-          const json = await get<ICommentListResponse>(url);
-          if (!json.ok) {
-            message.error(json.message);
-          }
-          const discussions: IDiscussion[] = json.data.rows.map((item) => {
-            return {
-              id: item.id,
-              resType: item.res_type,
-              user: item.editor,
-              title: item.title,
-              childrenCount: item.children_count,
-              createdAt: item.created_at,
-              updatedAt: item.updated_at,
-            };
-          });
+    <ProList<IDiscussion>
+      search={{
+        filterType: "light",
+      }}
+      rowKey="id"
+      headerTitle="问答&求助"
+      request={async (params = {}, sorter, filter) => {
+        console.log(params, sorter, filter);
+        const url = `/v2/discussion?view=all`;
+        const json = await get<ICommentListResponse>(url);
+        if (!json.ok) {
+          message.error(json.message);
+        }
+        const discussions: IDiscussion[] = json.data.rows.map((item) => {
           return {
-            total: json.data.count,
-            succcess: true,
-            data: discussions,
+            id: item.id,
+            resType: item.res_type,
+            user: item.editor,
+            title: item.title,
+            parent: item.parent,
+            childrenCount: item.children_count,
+            createdAt: item.created_at,
+            updatedAt: item.updated_at,
           };
-        }}
-        pagination={{
-          pageSize: 20,
-        }}
-        metas={{
-          title: {
-            dataIndex: "title",
-            title: "标题",
-            render: (_, row) => {
-              return (
-                <Link to={`/discussion/topic/${row.id}`}>{row.title}</Link>
-              );
-            },
-          },
-          avatar: {
-            search: false,
-            render: (_, row) => {
-              return <span>{row.user.avatar}</span>;
-            },
+        });
+        return {
+          total: json.data.count,
+          succcess: true,
+          data: discussions,
+        };
+      }}
+      pagination={{
+        pageSize: 20,
+      }}
+      metas={{
+        title: {
+          dataIndex: "title",
+          title: "标题",
+          render: (_, row) => {
+            return <Link to={`/discussion/topic/${row.id}`}>{row.title}</Link>;
           },
-          description: {
-            search: false,
-            render: (_, row) => {
-              return (
-                <Space>
-                  {`${row.user.nickName} created on`}
-                  <TimeShow time={row.createdAt} title={""} />
-                </Space>
-              );
-            },
+        },
+        avatar: {
+          search: false,
+          render: (_, row) => {
+            return <span>{row.user.avatar}</span>;
           },
-          subTitle: {
-            render: (_, row) => {
-              return (
-                <Space size={0}>
-                  <Tag color="blue" key={row.resType}>
-                    {row.resType}
-                  </Tag>
-                </Space>
-              );
-            },
-            search: false,
+        },
+        description: {
+          search: false,
+          render: (_, row) => {
+            return (
+              <Space>
+                {`${row.user.nickName} created on`}
+                <TimeShow time={row.createdAt} title={""} />
+              </Space>
+            );
           },
-          actions: {
-            render: (text, row) => [
-              row.childrenCount ? (
-                <Space>
-                  <MessageOutlined /> {row.childrenCount}
-                </Space>
-              ) : (
-                <></>
-              ),
-            ],
-            search: false,
+        },
+        subTitle: {
+          render: (_, row) => {
+            return (
+              <Space size={0}>
+                <Tag color="blue" key={row.resType}>
+                  {row.resType}
+                </Tag>
+              </Space>
+            );
           },
-          status: {
-            // 自己扩展的字段,主要用于筛选,不在列表中显示
-            title: "状态",
-            valueType: "select",
-            valueEnum: {
-              all: { text: "全部", status: "Default" },
-              open: {
-                text: "未解决",
-                status: "Error",
-              },
-              closed: {
-                text: "已解决",
-                status: "Success",
-              },
-              processing: {
-                text: "解决中",
-                status: "Processing",
-              },
+          search: false,
+        },
+        actions: {
+          render: (text, row) => [
+            row.childrenCount ? (
+              <Space>
+                <MessageOutlined /> {row.childrenCount}
+              </Space>
+            ) : (
+              <></>
+            ),
+          ],
+          search: false,
+        },
+        status: {
+          // 自己扩展的字段,主要用于筛选,不在列表中显示
+          title: "状态",
+          valueType: "select",
+          valueEnum: {
+            all: { text: "全部", status: "Default" },
+            open: {
+              text: "未解决",
+              status: "Error",
+            },
+            closed: {
+              text: "已解决",
+              status: "Success",
+            },
+            processing: {
+              text: "解决中",
+              status: "Processing",
             },
           },
-        }}
-      />
-    </div>
+        },
+      }}
+    />
   );
 };
 

+ 2 - 5
dashboard/src/pages/library/palicanon/bypath.tsx

@@ -11,6 +11,7 @@ import BookViewer from "../../../components/corpus/BookViewer";
 import { IChapterClickEvent } from "../../../components/corpus/PaliChapterList";
 import { IPaliBookListResponse } from "../../../components/api/Corpus";
 import Recent from "../../../components/corpus/Recent";
+import { fullUrl } from "../../../utils";
 
 const Widget = () => {
   const { root, path } = useParams();
@@ -127,11 +128,7 @@ const Widget = () => {
                 onChapterClick={(e: IChapterClickEvent) => {
                   if (e.event.ctrlKey) {
                     const url = `/palicanon/chapter/${e.para.Book}-${e.para.Paragraph}`;
-                    const fullUrl =
-                      process.env.REACT_APP_WEB_HOST +
-                      process.env.PUBLIC_URL +
-                      url;
-                    window.open(fullUrl, "_blank");
+                    window.open(fullUrl(url), "_blank");
                   } else {
                     setIsModalOpen(true);
                     setOpenPara({ book: e.para.Book, para: e.para.Paragraph });

+ 8 - 10
dashboard/src/pages/studio/anthology/index.tsx

@@ -7,16 +7,14 @@ import { styleStudioContent } from "../style";
 const { Content } = Layout;
 
 const Widget = () => {
-	return (
-		<Layout>
-			<Layout>
-				<LeftSider selectedKeys="anthology" />
-				<Content style={styleStudioContent}>
-					<Outlet />
-				</Content>
-			</Layout>
-		</Layout>
-	);
+  return (
+    <Layout>
+      <LeftSider selectedKeys="anthology" />
+      <Content style={styleStudioContent}>
+        <Outlet />
+      </Content>
+    </Layout>
+  );
 };
 
 export default Widget;

+ 19 - 211
dashboard/src/pages/studio/article/edit.tsx

@@ -1,49 +1,18 @@
-import { useRef, useState } from "react";
-import { Link, useParams } from "react-router-dom";
-import { useIntl } from "react-intl";
-import {
-  ProForm,
-  ProFormInstance,
-  ProFormText,
-  ProFormTextArea,
-} from "@ant-design/pro-components";
-import { TeamOutlined } from "@ant-design/icons";
-import { Button, Card, Form, message, Result, Space, Tabs } from "antd";
+import { useState } from "react";
+import { useParams } from "react-router-dom";
+
+import { Card, Space } from "antd";
 
-import { get, put } from "../../../request";
-import {
-  IArticleDataRequest,
-  IArticleResponse,
-} from "../../../components/api/Article";
-import LangSelect from "../../../components/general/LangSelect";
-import PublicitySelect from "../../../components/studio/PublicitySelect";
 import GoBack from "../../../components/studio/GoBack";
-import MDEditor from "@uiw/react-md-editor";
-import ArticleTplMaker from "../../../components/article/ArticleTplMaker";
-import ShareModal from "../../../components/share/ShareModal";
-import { EResType } from "../../../components/share/Share";
-import AddToAnthology from "../../../components/article/AddToAnthology";
 import ReadonlyLabel from "../../../components/general/ReadonlyLabel";
-import ArticlePrevDrawer from "../../../components/article/ArticlePrevDrawer";
 
-interface IFormData {
-  uid: string;
-  title: string;
-  subtitle: string;
-  summary: string;
-  content?: string;
-  content_type?: string;
-  status: number;
-  lang: string;
-}
+import ArticleEdit from "../../../components/article/ArticleEdit";
+import ArticleEditTools from "../../../components/article/ArticleEditTools";
 
 const Widget = () => {
-  const intl = useIntl();
-  const { studioname, articleid } = useParams(); //url 参数
+  const { studioname, articleId } = useParams(); //url 参数
   const [title, setTitle] = useState("loading");
-  const [unauthorized, setUnauthorized] = useState(false);
   const [readonly, setReadonly] = useState(false);
-  const [content, setContent] = useState<string>();
 
   return (
     <Card
@@ -54,181 +23,20 @@ const Widget = () => {
         </Space>
       }
       extra={
-        <Space>
-          {articleid ? (
-            <AddToAnthology studioName={studioname} articleIds={[articleid]} />
-          ) : undefined}
-          {articleid ? (
-            <ShareModal
-              trigger={
-                <Button icon={<TeamOutlined />}>
-                  {intl.formatMessage({
-                    id: "buttons.share",
-                  })}
-                </Button>
-              }
-              resId={articleid}
-              resType={EResType.article}
-            />
-          ) : undefined}
-          <Link to={`/article/article/${articleid}`} target="_blank">
-            {intl.formatMessage({ id: "buttons.open.in.library" })}
-          </Link>
-          <ArticleTplMaker
-            title={title}
-            type="article"
-            id={articleid}
-            trigger={<Button>获取模版</Button>}
-          />
-        </Space>
+        <ArticleEditTools
+          studioName={studioname}
+          articleId={articleId}
+          title={title}
+        />
       }
     >
-      {unauthorized ? (
-        <Result
-          status="403"
-          title="无权访问"
-          subTitle="您无权访问该内容。您可能没有登录,或者内容的所有者没有给您所需的权限。"
-          extra={<></>}
-        />
-      ) : (
-        <ProForm<IFormData>
-          onFinish={async (values: IFormData) => {
-            // TODO
-
-            const request = {
-              uid: articleid ? articleid : "",
-              title: values.title,
-              subtitle: values.subtitle,
-              summary: values.summary,
-              content: values.content,
-              content_type: "markdown",
-              status: values.status,
-              lang: values.lang,
-            };
-            console.log("save", request);
-            put<IArticleDataRequest, IArticleResponse>(
-              `/v2/article/${articleid}`,
-              request
-            )
-              .then((res) => {
-                console.log("save response", res);
-                if (res.ok) {
-                  message.success(
-                    intl.formatMessage({ id: "flashes.success" })
-                  );
-                } else {
-                  message.error(res.message);
-                }
-              })
-              .catch((e: IArticleResponse) => {
-                message.error(e.message);
-              });
-          }}
-          request={async () => {
-            const res = await get<IArticleResponse>(`/v2/article/${articleid}`);
-            console.log("article", res);
-            if (res.ok) {
-              const readonly = res.data.role === "editor" ? false : true;
-              setReadonly(readonly);
-              setTitle(res.data.title);
-              setContent(res.data.content);
-            } else {
-              setUnauthorized(true);
-              setTitle("无权访问");
-            }
-
-            return {
-              uid: res.data.uid,
-              title: res.data.title,
-              subtitle: res.data.subtitle,
-              summary: res.data.summary,
-              content: res.data.content,
-              content_type: res.data.content_type,
-              lang: res.data.lang,
-              status: res.data.status,
-            };
-          }}
-        >
-          <Tabs
-            items={[
-              {
-                key: "info",
-                label: intl.formatMessage({ id: "course.basic.info.label" }),
-                children: (
-                  <>
-                    <ProForm.Group>
-                      <ProFormText
-                        width="md"
-                        name="title"
-                        required
-                        label={intl.formatMessage({
-                          id: "forms.fields.title.label",
-                        })}
-                        rules={[
-                          {
-                            required: true,
-                            message: intl.formatMessage({
-                              id: "forms.message.title.required",
-                            }),
-                          },
-                        ]}
-                      />
-                      <ProFormText
-                        width="md"
-                        name="subtitle"
-                        label={intl.formatMessage({
-                          id: "forms.fields.subtitle.label",
-                        })}
-                      />
-                    </ProForm.Group>
-                    <ProForm.Group>
-                      <LangSelect width="md" />
-                      <PublicitySelect width="md" />
-                    </ProForm.Group>
-                    <ProForm.Group>
-                      <ProFormTextArea
-                        name="summary"
-                        width="lg"
-                        label={intl.formatMessage({
-                          id: "forms.fields.summary.label",
-                        })}
-                      />
-                    </ProForm.Group>
-                  </>
-                ),
-              },
-              {
-                key: "content",
-                label: intl.formatMessage({ id: "forms.fields.content.label" }),
-                forceRender: true,
-                children: (
-                  <ProForm.Group>
-                    <Form.Item
-                      name="content"
-                      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)} />
-                    </Form.Item>
-                  </ProForm.Group>
-                ),
-              },
-            ]}
-          />
-        </ProForm>
-      )}
+      <ArticleEdit
+        articleId={articleId}
+        onReady={(title: string, readonly: boolean) => {
+          setTitle(title);
+          setReadonly(readonly);
+        }}
+      />
     </Card>
   );
 };

+ 21 - 8
dashboard/src/pages/studio/article/list.tsx

@@ -1,20 +1,33 @@
+import { useState } from "react";
 import { useParams, useNavigate } from "react-router-dom";
+import ArticleEditDrawer from "../../../components/article/ArticleEditDrawer";
 
 import ArticleList from "../../../components/article/ArticleList";
 
 const Widget = () => {
   const { studioname } = useParams(); //url 参数
+  const [articleId, setArticleId] = useState<string>();
+  const [open, setOpen] = useState<boolean>(false);
   const navigate = useNavigate();
 
   return (
-    <ArticleList
-      studioName={studioname}
-      editable={true}
-      onSelect={(id: string) => {
-        const url = `/studio/${studioname}/article/${id}/edit`;
-        navigate(url);
-      }}
-    />
+    <>
+      <ArticleList
+        studioName={studioname}
+        editable={true}
+        onSelect={(id: string) => {
+          setArticleId(id);
+          setOpen(true);
+          //const url = `/studio/${studioname}/article/${id}/edit`;
+          //navigate(url);
+        }}
+      />
+      <ArticleEditDrawer
+        articleId={articleId}
+        open={open}
+        onClose={() => setOpen(false)}
+      />
+    </>
   );
 };
 

+ 2 - 5
dashboard/src/pages/studio/channel/show.tsx

@@ -13,6 +13,7 @@ import { useIntl } from "react-intl";
 import { EResType } from "../../../components/share/Share";
 import { IArticleParam } from "../recent/list";
 import ArticleDrawer from "../../../components/article/ArticleDrawer";
+import { fullUrl } from "../../../utils";
 
 const Widget = () => {
   const { channelId } = useParams(); //url 参数
@@ -65,11 +66,7 @@ const Widget = () => {
                     url += chapter?.channelId
                       ? `&channel=${chapter.channelId}`
                       : "";
-                    const fullUrl =
-                      process.env.REACT_APP_WEB_HOST +
-                      process.env.PUBLIC_URL +
-                      url;
-                    window.open(fullUrl, "_blank");
+                    window.open(fullUrl(url), "_blank");
                   } else {
                     setParam(chapter);
                     setArticleOpen(true);

+ 3 - 3
dashboard/src/pages/studio/recent/list.tsx

@@ -6,6 +6,7 @@ import { useAppSelector } from "../../../hooks";
 import { currentUser as _currentUser } from "../../../reducers/current-user";
 import ArticleDrawer from "../../../components/article/ArticleDrawer";
 import RecentList from "../../../components/recent/RecentList";
+import { fullUrl } from "../../../utils";
 
 export interface IRecentRequest {
   type: ArticleType;
@@ -73,9 +74,8 @@ const Widget = () => {
               : "";
             url += param.param?.book ? `&book=${param.param?.book}` : "";
             url += param.param?.para ? `&par=${param.param?.para}` : "";
-            const fullUrl =
-              process.env.REACT_APP_WEB_HOST + process.env.PUBLIC_URL + url;
-            window.open(fullUrl, "_blank");
+
+            window.open(fullUrl(url), "_blank");
           } else {
             setParam({
               type: param.type,

+ 2 - 50
dashboard/src/utils.ts

@@ -1,53 +1,5 @@
-export function ApiFetch(
-  url: string,
-  method = "GET",
-  data?: any
-): Promise<Response> {
-  const apiHost = process.env.REACT_APP_API_HOST;
-  interface ajaxParam {
-    method: string;
-    body?: string;
-    headers?: any;
-  }
-  let param: ajaxParam = {
-    method: method,
-  };
-  if (typeof data !== "undefined") {
-    param.body = JSON.stringify(data);
-  }
-  if (localStorage.getItem("token")) {
-    param.headers = { token: localStorage.getItem("token") };
-  }
-  return new Promise((resolve, reject) => {
-    let apiUrl = apiHost + url;
-    console.log("api", apiUrl);
-    fetch(apiUrl, param)
-      .then((response) => response.json())
-      .then((response) => {
-        resolve(response);
-      })
-      .catch((error) => {
-        reject(error);
-      });
-  });
-}
-
-export function ApiGetText(url: string): Promise<String> {
-  const apiHost = process.env.REACT_APP_API_HOST
-    ? process.env.REACT_APP_API_HOST
-    : "http://localhost/api";
-  return new Promise((resolve, reject) => {
-    let apiUrl = apiHost + url;
-    console.log("api", apiUrl);
-    fetch(apiUrl)
-      .then((response) => response.text())
-      .then((response) => {
-        resolve(response);
-      })
-      .catch((error) => {
-        reject(error);
-      });
-  });
+export function fullUrl(url: string): string {
+  return process.env.REACT_APP_WEB_HOST + process.env.PUBLIC_URL + url;
 }
 
 export function PaliToEn(pali: string): string {

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor