Browse Source

Merge pull request #1131 from visuddhinanda/agile

:sparkles: 自动查词
visuddhinanda 2 years ago
parent
commit
c20fc52f80
100 changed files with 1944 additions and 708 deletions
  1. 6 0
      dashboard/src/Router.tsx
  2. 23 0
      dashboard/src/assets/icon/index.tsx
  3. BIN
      dashboard/src/assets/library/images/download_bg.png
  4. 5 5
      dashboard/src/components/admin/HeadBar.tsx
  5. 2 2
      dashboard/src/components/admin/LeftSider.tsx
  6. 2 2
      dashboard/src/components/admin/relation/CaseSelect.tsx
  7. 9 8
      dashboard/src/components/admin/relation/DataImport.tsx
  8. 6 2
      dashboard/src/components/admin/relation/NissayaEndingEdit.tsx
  9. 6 2
      dashboard/src/components/admin/relation/RelationEdit.tsx
  10. 2 2
      dashboard/src/components/anthology/AnthologyCreate.tsx
  11. 39 15
      dashboard/src/components/anthology/AnthologyList.tsx
  12. 7 2
      dashboard/src/components/anthology/AnthologyModal.tsx
  13. 2 2
      dashboard/src/components/anthology/AnthologySelect.tsx
  14. 6 2
      dashboard/src/components/anthology/AnthologyTocTree.tsx
  15. 34 6
      dashboard/src/components/anthology/EditableTocTree.tsx
  16. 1 1
      dashboard/src/components/api/Article.ts
  17. 3 0
      dashboard/src/components/api/Corpus.ts
  18. 6 5
      dashboard/src/components/api/Dict.ts
  19. 1 0
      dashboard/src/components/api/Term.ts
  20. 7 2
      dashboard/src/components/article/AddToAnthology.tsx
  21. 2 2
      dashboard/src/components/article/AnthologyCard.tsx
  22. 6 2
      dashboard/src/components/article/AnthologyDetail.tsx
  23. 4 6
      dashboard/src/components/article/AnthologyInfoEdit.tsx
  24. 2 2
      dashboard/src/components/article/AnthologyList.tsx
  25. 2 2
      dashboard/src/components/article/AnthologyStudioList.tsx
  26. 24 25
      dashboard/src/components/article/Article.tsx
  27. 3 2
      dashboard/src/components/article/ArticleCard.tsx
  28. 2 2
      dashboard/src/components/article/ArticleCardMainMenu.tsx
  29. 2 2
      dashboard/src/components/article/ArticleCreate.tsx
  30. 512 0
      dashboard/src/components/article/ArticleList.tsx
  31. 58 0
      dashboard/src/components/article/ArticleListModal.tsx
  32. 2 2
      dashboard/src/components/article/ArticleSkeleton.tsx
  33. 2 2
      dashboard/src/components/article/ArticleTplMaker.tsx
  34. 2 2
      dashboard/src/components/article/ArticleView.tsx
  35. 26 11
      dashboard/src/components/article/EditableTree.tsx
  36. 2 2
      dashboard/src/components/article/ExerciseList.tsx
  37. 2 2
      dashboard/src/components/article/Find.tsx
  38. 4 4
      dashboard/src/components/article/MainMenu.tsx
  39. 69 41
      dashboard/src/components/article/ModeSwitch.tsx
  40. 2 2
      dashboard/src/components/article/Nav.tsx
  41. 15 3
      dashboard/src/components/article/PaliTextToc.tsx
  42. 2 2
      dashboard/src/components/article/ProTabs.tsx
  43. 90 47
      dashboard/src/components/article/RightPanel.tsx
  44. 19 3
      dashboard/src/components/article/RightToolsSwitch.tsx
  45. 2 2
      dashboard/src/components/article/TermShell.tsx
  46. 2 2
      dashboard/src/components/article/TocTree.tsx
  47. 2 2
      dashboard/src/components/article/ToolButton.tsx
  48. 3 3
      dashboard/src/components/article/ToolButtonDiscussion.tsx
  49. 230 0
      dashboard/src/components/article/ToolButtonNav.tsx
  50. 47 0
      dashboard/src/components/article/ToolButtonNavMore.tsx
  51. 37 0
      dashboard/src/components/article/ToolButtonNavSliceTitle.tsx
  52. 3 3
      dashboard/src/components/article/ToolButtonPr.tsx
  53. 2 2
      dashboard/src/components/article/ToolButtonSearch.tsx
  54. 2 2
      dashboard/src/components/article/ToolButtonSetting.tsx
  55. 2 2
      dashboard/src/components/article/ToolButtonTag.tsx
  56. 33 11
      dashboard/src/components/article/ToolButtonToc.tsx
  57. 6 5
      dashboard/src/components/auth/Avatar.tsx
  58. 10 9
      dashboard/src/components/auth/SignInAvatar.tsx
  59. 2 2
      dashboard/src/components/auth/StudioCard.tsx
  60. 2 2
      dashboard/src/components/auth/StudioName.tsx
  61. 2 2
      dashboard/src/components/auth/ToLibrary.tsx
  62. 2 2
      dashboard/src/components/auth/ToStudio.tsx
  63. 2 2
      dashboard/src/components/auth/User.tsx
  64. 2 2
      dashboard/src/components/auth/UserName.tsx
  65. 22 8
      dashboard/src/components/auth/setting/SettingArticle.tsx
  66. 6 2
      dashboard/src/components/auth/setting/SettingItem.tsx
  67. 45 5
      dashboard/src/components/auth/setting/default.ts
  68. 2 2
      dashboard/src/components/blog/BlogNav.tsx
  69. 16 16
      dashboard/src/components/blog/Profile.tsx
  70. 40 40
      dashboard/src/components/blog/TimeLine.tsx
  71. 2 2
      dashboard/src/components/blog/TopArticleCard.tsx
  72. 2 2
      dashboard/src/components/blog/TopArticles.tsx
  73. 2 2
      dashboard/src/components/channel/Channel.tsx
  74. 2 2
      dashboard/src/components/channel/ChannelCreate.tsx
  75. 4 2
      dashboard/src/components/channel/ChannelList.tsx
  76. 7 2
      dashboard/src/components/channel/ChannelListItem.tsx
  77. 37 11
      dashboard/src/components/channel/ChannelPicker.tsx
  78. 260 264
      dashboard/src/components/channel/ChannelPickerTable.tsx
  79. 10 4
      dashboard/src/components/channel/ChannelSelect.tsx
  80. 2 2
      dashboard/src/components/channel/ChannelSentDiff.tsx
  81. 2 2
      dashboard/src/components/channel/ChannelTypeSelect.tsx
  82. 2 2
      dashboard/src/components/channel/ChapterInChannelList.tsx
  83. 2 2
      dashboard/src/components/channel/CopyToModal.tsx
  84. 2 2
      dashboard/src/components/channel/CopyToResult.tsx
  85. 2 2
      dashboard/src/components/channel/CopyToStep.tsx
  86. 2 2
      dashboard/src/components/channel/ProgressSvg.tsx
  87. 2 2
      dashboard/src/components/channel/StudioSelect.tsx
  88. 2 2
      dashboard/src/components/comment/AnchorCard.tsx
  89. 2 2
      dashboard/src/components/comment/CommentAnchor.tsx
  90. 23 20
      dashboard/src/components/comment/CommentBox.tsx
  91. 3 3
      dashboard/src/components/comment/CommentCreate.tsx
  92. 2 2
      dashboard/src/components/comment/CommentEdit.tsx
  93. 2 2
      dashboard/src/components/comment/CommentItem.tsx
  94. 2 2
      dashboard/src/components/comment/CommentList.tsx
  95. 8 4
      dashboard/src/components/comment/CommentListCard.tsx
  96. 2 2
      dashboard/src/components/comment/CommentListItem.tsx
  97. 2 2
      dashboard/src/components/comment/CommentShow.tsx
  98. 2 2
      dashboard/src/components/comment/CommentTopic.tsx
  99. 5 2
      dashboard/src/components/comment/CommentTopicChildren.tsx
  100. 2 2
      dashboard/src/components/comment/CommentTopicInfo.tsx

+ 6 - 0
dashboard/src/Router.tsx

@@ -55,6 +55,8 @@ import LibraryDiscussionTopic from "./pages/library/discussion/topic";
 
 import LibrarySearch from "./pages/library/search";
 import LibrarySearchKey from "./pages/library/search/search";
+import LibraryDownload from "./pages/library/download";
+import LibraryDownloadPage from "./pages/library/download/Download";
 
 import Studio from "./pages/studio";
 import StudioHome from "./pages/studio/home";
@@ -214,6 +216,10 @@ const Widget = () => {
           <Route path="key/:key" element={<LibrarySearchKey />} />
         </Route>
 
+        <Route path="download" element={<LibraryDownload />}>
+          <Route path="download" element={<LibraryDownloadPage />} />
+        </Route>
+
         <Route path="studio/:studioname" element={<Studio />}>
           <Route path="home" element={<StudioHome />} />
           <Route path="palicanon" element={<StudioPalicanon />}></Route>

+ 23 - 0
dashboard/src/assets/icon/index.tsx

@@ -81,6 +81,25 @@ const HandOutlined = () => (
     ></path>
   </svg>
 );
+
+const ColumnOutlined = () => (
+  <svg
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="865"
+    width="1em"
+    height="1em"
+    fill="currentColor"
+  >
+    <path
+      d="M176 177h672v672H176V177z m-56-64a8 8 0 0 0-8 8v760c0 17.673 14.327 32 32 32h736c17.673 0 32-14.327 32-32V121a8 8 0 0 0-8-8H120z"
+      p-id="866"
+    ></path>
+    <path d="M164 159h700v82H164v-82z" p-id="867"></path>
+    <path d="M546 177v700h-64V177h64z" p-id="868"></path>
+  </svg>
+);
 export const DictIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={DictSvg} {...props} />
 );
@@ -103,3 +122,7 @@ export const UnLockIcon = (props: Partial<CustomIconComponentProps>) => (
 export const HandOutlinedIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={HandOutlined} {...props} />
 );
+
+export const ColumnOutlinedIcon = (
+  props: Partial<CustomIconComponentProps>
+) => <Icon component={ColumnOutlined} {...props} />;

BIN
dashboard/src/assets/library/images/download_bg.png


+ 5 - 5
dashboard/src/components/admin/HeadBar.tsx

@@ -1,10 +1,10 @@
 import { Link } from "react-router-dom";
-import { Col, Row, Input, Layout, Space } from "antd";
+import { Input, Layout, Space } from "antd";
 
 import img_banner from "../../assets/studio/images/wikipali_banner.svg";
 import UiLangSelect from "../general/UiLangSelect";
 import SignInAvatar from "../auth/SignInAvatar";
-import ToLibaray from "../auth/ToLibaray";
+import ToLibrary from "../auth/ToLibrary";
 import ThemeSelect from "../general/ThemeSelect";
 
 const { Search } = Input;
@@ -12,7 +12,7 @@ const { Header } = Layout;
 
 const onSearch = (value: string) => console.log(value);
 
-const Widget = () => {
+const HeadBarWidget = () => {
   return (
     <Header
       className="header"
@@ -45,7 +45,7 @@ const Widget = () => {
         </div>
         <div>
           <Space>
-            <ToLibaray />
+            <ToLibrary />
             <SignInAvatar />
             <UiLangSelect />
             <ThemeSelect />
@@ -56,4 +56,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default HeadBarWidget;

+ 2 - 2
dashboard/src/components/admin/LeftSider.tsx

@@ -13,7 +13,7 @@ const onClick: MenuProps["onClick"] = (e) => {
 type IWidgetHeadBar = {
   selectedKeys?: string;
 };
-const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
+const LeftSiderWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
   const items: MenuProps["items"] = [
     {
       label: "管理",
@@ -54,4 +54,4 @@ const Widget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
   );
 };
 
-export default Widget;
+export default LeftSiderWidget;

+ 2 - 2
dashboard/src/components/admin/relation/CaseSelect.tsx

@@ -6,7 +6,7 @@ interface IWidget {
   name?: string;
   width?: number | "md" | "sm" | "xl" | "xs" | "lg";
 }
-const Widget = ({ name = "case", width = "md" }: IWidget) => {
+const CaseSelectWidget = ({ name = "case", width = "md" }: IWidget) => {
   const intl = useIntl();
   const _case = [
     "nom",
@@ -38,4 +38,4 @@ const Widget = ({ name = "case", width = "md" }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CaseSelectWidget;

+ 9 - 8
dashboard/src/components/admin/relation/DataImport.tsx

@@ -5,6 +5,7 @@ import { API_HOST, get } from "../../../request";
 import { UploadFile } from "antd/es/upload/interface";
 import { IAttachmentResponse } from "../../api/Attachments";
 import modal from "antd/lib/modal";
+import { useIntl } from "react-intl";
 
 interface INissayaEndingUpload {
   filename: UploadFile<IAttachmentResponse>[];
@@ -19,22 +20,25 @@ export interface INissayaEndingImportResponse {
 }
 
 interface IWidget {
+  title?: string;
   url: string;
   urlExtra?: string;
   trigger?: JSX.Element;
   onSuccess?: Function;
 }
-const Widget = ({
+const DataImportWidget = ({
+  title,
   url,
   urlExtra,
   trigger = <>{"trigger"}</>,
   onSuccess,
 }: IWidget) => {
+  const intl = useIntl();
   const [form] = Form.useForm<INissayaEndingUpload>();
-
+  const formTitle = title ? title : intl.formatMessage({ id: "labels.upload" });
   return (
     <ModalForm<INissayaEndingUpload>
-      title="upload"
+      title={formTitle}
       trigger={trigger}
       form={form}
       autoFocusFirstInput
@@ -59,10 +63,7 @@ const Widget = ({
         }
 
         const queryUrl = `${url}?filename=${_filename}&${urlExtra}`;
-        console.log("url", queryUrl);
         const res = await get<INissayaEndingImportResponse>(queryUrl);
-
-        console.log("import", res);
         if (res.ok) {
           if (res.data.fail > 0) {
             modal.info({
@@ -85,7 +86,7 @@ const Widget = ({
     >
       <ProFormUploadDragger
         max={1}
-        label="上传xlsx"
+        label="请确保您的xlsx文件是用导出功能导出的。word为空可以删除该词条。使用其他studio导出的数据,请将channel_id设置为空。否则该术语将被忽略。"
         name="filename"
         fieldProps={{
           name: "file",
@@ -96,4 +97,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default DataImportWidget;

+ 6 - 2
dashboard/src/components/admin/relation/NissayaEndingEdit.tsx

@@ -17,7 +17,11 @@ interface IWidget {
   id?: string;
   onSuccess?: Function;
 }
-const Widget = ({ trigger = <>{"trigger"}</>, id, onSuccess }: IWidget) => {
+const NissayaEndingWidget = ({
+  trigger = <>{"trigger"}</>,
+  id,
+  onSuccess,
+}: IWidget) => {
   const [title, setTitle] = useState<string | undefined>(id ? "" : "新建");
   const [form] = Form.useForm<INissayaEnding>();
   const intl = useIntl();
@@ -108,4 +112,4 @@ const Widget = ({ trigger = <>{"trigger"}</>, id, onSuccess }: IWidget) => {
   );
 };
 
-export default Widget;
+export default NissayaEndingWidget;

+ 6 - 2
dashboard/src/components/admin/relation/RelationEdit.tsx

@@ -22,7 +22,11 @@ interface IWidget {
   id?: string;
   onSuccess?: Function;
 }
-const Widget = ({ trigger = <>{"trigger"}</>, id, onSuccess }: IWidget) => {
+const RelationEditWidget = ({
+  trigger = <>{"trigger"}</>,
+  id,
+  onSuccess,
+}: IWidget) => {
   const [title, setTitle] = useState<string | undefined>(id ? "" : "新建");
   const [form] = Form.useForm<IRelation>();
   const formRef = useRef<ProFormInstance>();
@@ -124,4 +128,4 @@ const Widget = ({ trigger = <>{"trigger"}</>, id, onSuccess }: IWidget) => {
   );
 };
 
-export default Widget;
+export default RelationEditWidget;

+ 2 - 2
dashboard/src/components/anthology/AnthologyCreate.tsx

@@ -21,7 +21,7 @@ interface IWidget {
   studio?: string;
   onSuccess?: Function;
 }
-const Widget = ({ studio, onSuccess }: IWidget) => {
+const AnthologyCreateWidget = ({ studio, onSuccess }: IWidget) => {
   const intl = useIntl();
   const formRef = useRef<ProFormInstance>();
 
@@ -76,4 +76,4 @@ const Widget = ({ studio, onSuccess }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AnthologyCreateWidget;

+ 39 - 15
dashboard/src/components/anthology/AnthologyList.tsx

@@ -19,8 +19,7 @@ import {
 import { delete_, get } from "../../request";
 import { PublicityValueEnum } from "../../components/studio/table";
 import { useEffect, useRef, useState } from "react";
-import ShareModal from "../share/ShareModal";
-import { EResType } from "../share/Share";
+import Share, { EResType } from "../share/Share";
 import {
   IResNumberResponse,
   renderBadge,
@@ -47,7 +46,7 @@ interface IWidget {
   showOption?: boolean;
   onTitleClick?: Function;
 }
-const Widget = ({
+const AnthologyListWidget = ({
   title,
   studioName,
   showCol,
@@ -110,6 +109,26 @@ const Widget = ({
       },
     });
   };
+
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [shareResId, setShareResId] = useState<string>("");
+  const [shareResType, setShareResType] = useState<EResType>(
+    EResType.collection
+  );
+  const showShareModal = (resId: string, resType: EResType) => {
+    setShareResId(resId);
+    setShareResType(resType);
+    setIsModalOpen(true);
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
   const ref = useRef<ActionType>();
   return (
     <>
@@ -221,15 +240,9 @@ const Widget = ({
                     },
                     {
                       key: "share",
-                      label: (
-                        <ShareModal
-                          trigger={intl.formatMessage({
-                            id: "buttons.share",
-                          })}
-                          resId={row.id}
-                          resType={EResType.collection}
-                        />
-                      ),
+                      label: intl.formatMessage({
+                        id: "buttons.share",
+                      }),
                       icon: <TeamOutlined />,
                     },
                     {
@@ -247,12 +260,12 @@ const Widget = ({
                         window.open(`/anthology/${row.id}`, "_blank");
                         break;
                       case "share":
+                        console.log("share");
+                        showShareModal(row.id, EResType.collection);
                         break;
                       case "remove":
                         showDeleteConfirm(row.id, row.title);
                         break;
-                      default:
-                        break;
                     }
                   },
                 }}
@@ -371,8 +384,19 @@ const Widget = ({
           },
         }}
       />
+
+      <Modal
+        destroyOnClose={true}
+        width={700}
+        title="协作"
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+      >
+        <Share resId={shareResId} resType={shareResType} />
+      </Modal>
     </>
   );
 };
 
-export default Widget;
+export default AnthologyListWidget;

+ 7 - 2
dashboard/src/components/anthology/AnthologyModal.tsx

@@ -8,7 +8,12 @@ interface IWidget {
   onSelect?: Function;
   onCancel?: Function;
 }
-const Widget = ({ studioName, trigger, onSelect, onCancel }: IWidget) => {
+const AnthologyModalWidget = ({
+  studioName,
+  trigger,
+  onSelect,
+  onCancel,
+}: IWidget) => {
   const [isModalOpen, setIsModalOpen] = useState(false);
 
   const showModal = () => {
@@ -49,4 +54,4 @@ const Widget = ({ studioName, trigger, onSelect, onCancel }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AnthologyModalWidget;

+ 2 - 2
dashboard/src/components/anthology/AnthologySelect.tsx

@@ -11,7 +11,7 @@ interface IWidget {
   studioName?: string;
   onSelect?: Function;
 }
-const Widget = ({ studioName, onSelect }: IWidget) => {
+const AnthologyTocTreeWidget = ({ studioName, onSelect }: IWidget) => {
   const [anthology, setAnthology] = useState<IOptions[]>([
     { value: "all", label: "全部" },
     { value: "none", label: "没有加入文集的" },
@@ -49,4 +49,4 @@ const Widget = ({ studioName, onSelect }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AnthologyTocTreeWidget;

+ 6 - 2
dashboard/src/components/anthology/AnthologyTocTree.tsx

@@ -11,7 +11,11 @@ interface IWidget {
   onSelect?: Function;
   onArticleSelect?: Function;
 }
-const Widget = ({ anthologyId, onSelect, onArticleSelect }: IWidget) => {
+const AnthologyTocTreeWidget = ({
+  anthologyId,
+  onSelect,
+  onArticleSelect,
+}: IWidget) => {
   const navigate = useNavigate();
   const [tocData, setTocData] = useState<ListNodeData[]>([]);
 
@@ -49,4 +53,4 @@ const Widget = ({ anthologyId, onSelect, onArticleSelect }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AnthologyTocTreeWidget;

+ 34 - 6
dashboard/src/components/anthology/EditableTocTree.tsx

@@ -1,5 +1,6 @@
-import { message } from "antd";
+import { Button, message } from "antd";
 import { useEffect, useState } from "react";
+import { FileAddOutlined } from "@ant-design/icons";
 
 import { get, put } from "../../request";
 import {
@@ -7,20 +8,28 @@ import {
   IArticleMapListResponse,
   IArticleMapUpdateRequest,
 } from "../api/Article";
-import EditableTree, { ListNodeData } from "../article/EditableTree";
+import ArticleListModal from "../article/ArticleListModal";
+import EditableTree, {
+  ListNodeData,
+  TreeNodeData,
+} from "../article/EditableTree";
 
 interface IWidget {
   anthologyId?: string;
+  studioName?: string;
   onSelect?: Function;
 }
-const Widget = ({ anthologyId, onSelect }: IWidget) => {
+const EditableTocTreeWidget = ({
+  anthologyId,
+  studioName,
+  onSelect,
+}: IWidget) => {
   const [tocData, setTocData] = useState<ListNodeData[]>([]);
-
+  const [addArticle, setAddArticle] = useState<TreeNodeData>();
   useEffect(() => {
     get<IArticleMapListResponse>(
       `/v2/article-map?view=anthology&id=${anthologyId}`
     ).then((json) => {
-      console.log("文集get", json);
       if (json.ok) {
         const toc: ListNodeData[] = json.data.rows.map((item) => {
           return {
@@ -34,10 +43,29 @@ const Widget = ({ anthologyId, onSelect }: IWidget) => {
       }
     });
   }, [anthologyId]);
+
   return (
     <div>
       <EditableTree
         treeData={tocData}
+        addOnArticle={addArticle}
+        addFileButton={
+          <ArticleListModal
+            studioName={studioName}
+            trigger={<Button icon={<FileAddOutlined />}>添加</Button>}
+            multiple={false}
+            onSelect={(id: string, title: string) => {
+              console.log("add article", id);
+              const newNode: TreeNodeData = {
+                key: id,
+                title: title,
+                children: [],
+                level: 1,
+              };
+              setAddArticle(newNode);
+            }}
+          />
+        }
         onChange={(data: ListNodeData[]) => {
           console.log("onChange", data);
         }}
@@ -77,4 +105,4 @@ const Widget = ({ anthologyId, onSelect }: IWidget) => {
   );
 };
 
-export default Widget;
+export default EditableTocTreeWidget;

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

@@ -12,7 +12,7 @@ export interface IArticleListApiResponse {
 export interface IAnthologyDataRequest {
   title: string;
   subtitle: string;
-  summary: string;
+  summary?: string;
   article_list?: IArticleListApiResponse[];
   lang: string;
   status: number;

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

@@ -1,6 +1,7 @@
 import { IStudio } from "../auth/StudioName";
 import { IUser } from "../auth/User";
 import { IChannel } from "../channel/Channel";
+import { TContentType } from "../comment/CommentCreate";
 import { ISuggestionCount, IWidgetSentEditInner } from "../template/SentEdit";
 import { TChannelType } from "./Channel";
 import { TagNode } from "./Tag";
@@ -144,6 +145,7 @@ export interface ISentenceRequest {
   wordEnd: number;
   channel: string;
   content: string;
+  contentType?: TContentType;
   prEditor?: string;
   prId?: string;
   prEditAt?: string;
@@ -156,6 +158,7 @@ export interface ISentenceData {
   word_start: number;
   word_end: number;
   content: string;
+  content_type?: TContentType;
   html: string;
   editor: IUser;
   channel: IChannel;

+ 6 - 5
dashboard/src/components/api/Dict.ts

@@ -1,4 +1,3 @@
-import { useIntl } from "react-intl";
 import { IStudio } from "../auth/StudioName";
 import { IUser } from "../auth/User";
 import { ICaseListData } from "../dict/CaseList";
@@ -8,11 +7,11 @@ export interface IDictRequest {
   word: string;
   type?: string;
   grammar?: string;
-  mean?: string;
-  parent?: string;
+  mean?: string | null;
+  parent?: string | null;
   note?: string;
-  factors?: string;
-  factormean?: string;
+  factors?: string | null;
+  factormean?: string | null;
   confidence: number;
   dict_id?: string;
   dict_name?: string;
@@ -46,6 +45,7 @@ export interface IApiResponseDictData {
   dict_id: string;
   dict_name?: string;
   dict_shortname?: string;
+  shortname?: string;
   confidence: number;
   creator_id: number;
   updated_at: string;
@@ -63,6 +63,7 @@ export interface IApiResponseDictList {
   data: {
     rows: IApiResponseDictData[];
     count: number;
+    time?: number;
   };
 }
 

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

@@ -11,6 +11,7 @@ export interface ITermDataRequest {
   note?: string;
   channal?: string;
   studioName?: string;
+  studioId?: string;
   language?: string;
 }
 export interface ITermDataResponse {

+ 7 - 2
dashboard/src/components/article/AddToAnthology.tsx

@@ -9,7 +9,12 @@ interface IWidget {
   articleIds?: string[];
   onFinally?: Function;
 }
-const Widget = ({ trigger, studioName, articleIds, onFinally }: IWidget) => {
+const AddToAnthologyWidget = ({
+  trigger,
+  studioName,
+  articleIds,
+  onFinally,
+}: IWidget) => {
   return (
     <AnthologyModal
       studioName={studioName}
@@ -43,4 +48,4 @@ const Widget = ({ trigger, studioName, articleIds, onFinally }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AddToAnthologyWidget;

+ 2 - 2
dashboard/src/components/article/AnthologyCard.tsx

@@ -33,7 +33,7 @@ interface IWidgetAnthologyCard {
   data: IAnthologyData;
 }
 
-const Widget = (prop: IWidgetAnthologyCard) => {
+const AnthologyCardWidget = (prop: IWidgetAnthologyCard) => {
   const articleList = prop.data.articles.map((item, id) => {
     return <div key={id}>{item.title}</div>;
   });
@@ -65,4 +65,4 @@ const Widget = (prop: IWidgetAnthologyCard) => {
   );
 };
 
-export default Widget;
+export default AnthologyCardWidget;

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

@@ -20,7 +20,11 @@ interface IWidgetAnthologyDetail {
   channels?: string[];
   onArticleSelect?: Function;
 }
-const Widget = ({ aid, channels, onArticleSelect }: IWidgetAnthologyDetail) => {
+const AnthologyDetailWidget = ({
+  aid,
+  channels,
+  onArticleSelect,
+}: IWidgetAnthologyDetail) => {
   const [tableData, setTableData] = useState<IAnthologyData>();
   const navigate = useNavigate();
 
@@ -87,4 +91,4 @@ const Widget = ({ aid, channels, onArticleSelect }: IWidgetAnthologyDetail) => {
   );
 };
 
-export default Widget;
+export default AnthologyDetailWidget;

+ 4 - 6
dashboard/src/components/article/AnthologyInfoEdit.tsx

@@ -11,7 +11,7 @@ import PublicitySelect from "../studio/PublicitySelect";
 interface IFormData {
   title: string;
   subtitle: string;
-  summary: string;
+  summary?: string;
   lang: string;
   status: number;
 }
@@ -20,15 +20,13 @@ interface IWidget {
   anthologyId?: string;
   onTitleChange?: Function;
 }
-const Widget = ({ anthologyId, onTitleChange }: IWidget) => {
+const AnthologyInfoEditWidget = ({ anthologyId, onTitleChange }: IWidget) => {
   const intl = useIntl();
 
   return anthologyId ? (
     <ProForm<IFormData>
       onFinish={async (values: IFormData) => {
-        // TODO
         console.log(values);
-
         const res = await put<IAnthologyDataRequest, IAnthologyResponse>(
           `/v2/anthology/${anthologyId}`,
           {
@@ -66,7 +64,7 @@ const Widget = ({ anthologyId, onTitleChange }: IWidget) => {
           return {
             title: res.data.title,
             subtitle: res.data.subtitle,
-            summary: res.data.summary,
+            summary: res.data.summary ? res.data.summary : undefined,
             lang: res.data.lang,
             status: res.data.status,
           };
@@ -125,4 +123,4 @@ const Widget = ({ anthologyId, onTitleChange }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AnthologyInfoEditWidget;

+ 2 - 2
dashboard/src/components/article/AnthologyList.tsx

@@ -10,7 +10,7 @@ interface IWidget {
   studioName?: string;
   searchKey?: string;
 }
-const Widget = ({ studioName, searchKey }: IWidget) => {
+const AnthologyListWidget = ({ studioName, searchKey }: IWidget) => {
   const [tableData, setTableData] = useState<IAnthologyData[]>([]);
   const [total, setTotal] = useState<number>();
   const [currPage, setCurrPage] = useState<number>(1);
@@ -84,4 +84,4 @@ const Widget = ({ studioName, searchKey }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AnthologyListWidget;

+ 2 - 2
dashboard/src/components/article/AnthologStudioList.tsx → dashboard/src/components/article/AnthologyStudioList.tsx

@@ -16,7 +16,7 @@ interface IWidgetAnthologyList {
 	data: IAnthologyData[];
 }
 */
-const Widget = () => {
+const AnthologyStudioListWidget = () => {
   const [tableData, setTableData] = useState<IAnthologyStudioData[]>([]);
   useEffect(() => {
     console.log("useEffect");
@@ -54,4 +54,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default AnthologyStudioListWidget;

+ 24 - 25
dashboard/src/components/article/Article.tsx

@@ -20,6 +20,7 @@ import {
   IViewRequest,
   IViewStoreResponse,
 } from "../../pages/studio/recent/list";
+import { modeChange } from "../../reducers/article-mode";
 
 export type ArticleMode = "read" | "edit" | "wbw";
 export type ArticleType =
@@ -62,12 +63,12 @@ interface IWidgetArticle {
   courseId?: string;
   exerciseId?: string;
   userName?: string;
-  mode?: ArticleMode;
+  mode?: ArticleMode | null;
   active?: boolean;
   onArticleChange?: Function;
   onFinal?: Function;
 }
-const Widget = ({
+const ArticleWidget = ({
   type,
   id,
   book,
@@ -84,7 +85,7 @@ const Widget = ({
   onFinal,
 }: IWidgetArticle) => {
   const [articleData, setArticleData] = useState<IArticleDataResponse>();
-  const [articleMode, setArticleMode] = useState<ArticleMode>();
+
   const [extra, setExtra] = useState(<></>);
   const [showSkeleton, setShowSkeleton] = useState(true);
   const [unauthorized, setUnauthorized] = useState(false);
@@ -127,47 +128,46 @@ const Widget = ({
   }, [articleId, type]);
 
   useEffect(() => {
-    console.log("mode", mode, articleMode);
-    if (!active) {
-      return;
-    }
-
     //发布mode变更
-    //store.dispatch(modeChange(mode));
-    if (mode !== articleMode && mode !== "read" && articleMode !== "read") {
-      console.log("set mode", mode, articleMode);
-      setArticleMode(mode);
+    console.log("发布mode变更", mode);
+    store.dispatch(modeChange(mode as ArticleMode));
+  }, [mode]);
+
+  const srcDataMode = mode === "edit" || mode === "wbw" ? "edit" : "read";
+  useEffect(() => {
+    console.log("srcDataMode", srcDataMode);
+    if (!active) {
       return;
     }
-    setArticleMode(mode);
     if (typeof type !== "undefined") {
       let url = "";
       switch (type) {
         case "chapter":
           if (typeof articleId !== "undefined") {
-            url = `/v2/corpus-chapter/${articleId}?mode=${mode}`;
+            url = `/v2/corpus-chapter/${articleId}?mode=${srcDataMode}`;
             url += channelId ? `&channels=${channelId}` : "";
           }
           break;
         case "para":
-          url = `/v2/corpus?view=para&book=${book}&par=${para}&mode=${mode}`;
+          const _book = book ? book : articleId;
+          url = `/v2/corpus?view=para&book=${_book}&par=${para}&mode=${srcDataMode}`;
           url += channelId ? `&channels=${channelId}` : "";
           break;
         case "article":
           if (typeof articleId !== "undefined") {
-            url = `/v2/article/${articleId}?mode=${mode}`;
+            url = `/v2/article/${articleId}?mode=${srcDataMode}`;
             url += channelId ? `&channel=${channelId}` : "";
             url += anthologyId ? `&anthology=${anthologyId}` : "";
           }
           break;
         case "textbook":
           if (typeof articleId !== "undefined") {
-            url = `/v2/article/${articleId}?view=textbook&course=${courseId}&mode=${mode}`;
+            url = `/v2/article/${articleId}?view=textbook&course=${courseId}&mode=${srcDataMode}`;
           }
           break;
         case "exercise":
           if (typeof articleId !== "undefined") {
-            url = `/v2/article/${articleId}?mode=${mode}&course=${courseId}&exercise=${exerciseId}&user=${userName}`;
+            url = `/v2/article/${articleId}?mode=${srcDataMode}&course=${courseId}&exercise=${exerciseId}&user=${userName}`;
             setExtra(
               <ExerciseAnswer
                 courseId={courseId}
@@ -179,7 +179,7 @@ const Widget = ({
           break;
         case "exercise-list":
           if (typeof articleId !== "undefined") {
-            url = `/v2/article/${articleId}?mode=${mode}&course=${courseId}&exercise=${exerciseId}`;
+            url = `/v2/article/${articleId}?mode=${srcDataMode}&course=${courseId}&exercise=${exerciseId}`;
 
             setExtra(
               <ExerciseList
@@ -192,12 +192,12 @@ const Widget = ({
           break;
         default:
           if (typeof articleId !== "undefined") {
-            url = `/v2/corpus/${type}/${articleId}/${mode}?mode=${mode}`;
+            url = `/v2/corpus/${type}/${articleId}/${srcDataMode}?mode=${srcDataMode}`;
             url += channelId ? `&channel=${channelId}` : "";
           }
           break;
       }
-      console.log("url", url);
+      console.log("article url", url);
       setShowSkeleton(true);
       get<IArticleResponse>(url)
         .then((json) => {
@@ -246,7 +246,7 @@ const Widget = ({
                     book: parseInt(book),
                     para: parseInt(para),
                     channel: channelId,
-                    mode: mode,
+                    mode: srcDataMode,
                   }).then((json) => {
                     console.log("view", json.data);
                   });
@@ -270,8 +270,7 @@ const Widget = ({
     active,
     type,
     articleId,
-    mode,
-    articleMode,
+    srcDataMode,
     book,
     para,
     channelId,
@@ -316,4 +315,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default ArticleWidget;

+ 3 - 2
dashboard/src/components/article/ArticleCard.tsx

@@ -16,7 +16,7 @@ interface IWidgetArticleCard {
   openInCol?: Function;
   showCol?: Function;
 }
-const Widget = ({
+const ArticleCardWidget = ({
   type,
   articleId,
   data,
@@ -64,6 +64,7 @@ const Widget = ({
       extra={
         <Space>
           <ModeSwitch
+            channel={null}
             onModeChange={(mode: string) => {
               if (typeof onModeChange !== "undefined") {
                 onModeChange(mode);
@@ -86,4 +87,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default ArticleCardWidget;

+ 2 - 2
dashboard/src/components/article/ArticleCardMainMenu.tsx

@@ -9,7 +9,7 @@ interface IWidget {
   type?: string;
   articleId?: string;
 }
-const Widget = ({ type, articleId }: IWidget) => {
+const ArticleCardMainMenuWidget = ({ type, articleId }: IWidget) => {
   const id = articleId?.split("_");
   let tocWidget = <></>;
   if (id && id.length > 0) {
@@ -71,4 +71,4 @@ const Widget = ({ type, articleId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ArticleCardMainMenuWidget;

+ 2 - 2
dashboard/src/components/article/ArticleCreate.tsx

@@ -23,7 +23,7 @@ interface IWidget {
   anthologyId?: string;
   onSuccess?: Function;
 }
-const Widget = ({ studio, anthologyId, onSuccess }: IWidget) => {
+const ArticleCreateWidget = ({ studio, anthologyId, onSuccess }: IWidget) => {
   const intl = useIntl();
   const formRef = useRef<ProFormInstance>();
 
@@ -76,4 +76,4 @@ const Widget = ({ studio, anthologyId, onSuccess }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ArticleCreateWidget;

+ 512 - 0
dashboard/src/components/article/ArticleList.tsx

@@ -0,0 +1,512 @@
+import { Link } from "react-router-dom";
+import { useIntl } from "react-intl";
+import {
+  Button,
+  Popover,
+  Dropdown,
+  Typography,
+  Modal,
+  message,
+  Space,
+  Table,
+  Badge,
+} from "antd";
+import { ActionType, ProTable } from "@ant-design/pro-components";
+import {
+  PlusOutlined,
+  DeleteOutlined,
+  TeamOutlined,
+  ExclamationCircleOutlined,
+  FolderAddOutlined,
+  ReconciliationOutlined,
+} from "@ant-design/icons";
+
+import ArticleCreate from "../../components/article/ArticleCreate";
+import { delete_, get } from "../../request";
+import {
+  IArticleListResponse,
+  IDeleteResponse,
+} from "../../components/api/Article";
+import { PublicityValueEnum } from "../../components/studio/table";
+import { useEffect, useRef, useState } from "react";
+import ArticleTplMaker from "../../components/article/ArticleTplMaker";
+import Share, { EResType } from "../../components/share/Share";
+import AddToAnthology from "../../components/article/AddToAnthology";
+import AnthologySelect from "../../components/anthology/AnthologySelect";
+import StudioName, { IStudio } from "../../components/auth/StudioName";
+import { IUser } from "../../components/auth/User";
+
+const { Text } = Typography;
+
+interface IArticleNumberResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    my: number;
+    collaboration: number;
+  };
+}
+
+const renderBadge = (count: number, active = false) => {
+  return (
+    <Badge
+      count={count}
+      style={{
+        marginBlockStart: -2,
+        marginInlineStart: 4,
+        color: active ? "#1890FF" : "#999",
+        backgroundColor: active ? "#E6F7FF" : "#eee",
+      }}
+    />
+  );
+};
+
+interface DataItem {
+  sn: number;
+  id: string;
+  title: string;
+  subtitle: string;
+  summary: string;
+  anthologyCount?: number;
+  anthologyTitle?: string;
+  publicity: number;
+  createdAt: number;
+  studio?: IStudio;
+  editor?: IUser;
+}
+
+interface IWidget {
+  studioName?: string;
+  editable?: boolean;
+  multiple?: boolean;
+  onSelect?: Function;
+}
+const ArticleListWidget = ({
+  studioName,
+  multiple = true,
+  editable = false,
+  onSelect,
+}: IWidget) => {
+  const intl = useIntl(); //i18n
+  const [openCreate, setOpenCreate] = useState(false);
+  const [anthologyId, setAnthologyId] = useState<string>();
+  const [activeKey, setActiveKey] = useState<React.Key | undefined>("my");
+  const [myNumber, setMyNumber] = useState<number>(0);
+  const [collaborationNumber, setCollaborationNumber] = useState<number>(0);
+
+  useEffect(() => {
+    /**
+     * 获取各种课程的数量
+     */
+    const url = `/v2/article-my-number?studio=${studioName}`;
+    console.log("url", url);
+    get<IArticleNumberResponse>(url).then((json) => {
+      if (json.ok) {
+        setMyNumber(json.data.my);
+        setCollaborationNumber(json.data.collaboration);
+      }
+    });
+  }, [studioName]);
+
+  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/article/${id}`)
+          .then((json) => {
+            if (json.ok) {
+              message.success("删除成功");
+              ref.current?.reload();
+            } else {
+              message.error(json.message);
+            }
+          })
+          .catch((e) => console.log("Oops errors!", e));
+      },
+    });
+  };
+  const ref = useRef<ActionType>();
+
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [shareResId, setShareResId] = useState<string>("");
+  const [shareResType, setShareResType] = useState<EResType>(EResType.article);
+  const showShareModal = (resId: string, resType: EResType) => {
+    setShareResId(resId);
+    setShareResType(resType);
+    setIsModalOpen(true);
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
+  return (
+    <>
+      <ProTable<DataItem>
+        actionRef={ref}
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "dict.fields.sn.label",
+            }),
+            dataIndex: "sn",
+            key: "sn",
+            width: 50,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.title.label",
+            }),
+            dataIndex: "title",
+            key: "title",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              return (
+                <>
+                  <div key={1}>
+                    <Typography.Link
+                      onClick={() => {
+                        if (typeof onSelect !== "undefined") {
+                          onSelect(row.id, row.title);
+                        }
+                      }}
+                    >
+                      {row.title}
+                    </Typography.Link>
+                  </div>
+                  <div key={2}>
+                    <Text type="secondary">{row.subtitle}</Text>
+                  </div>
+                  {activeKey !== "my" ? (
+                    <div key={3}>
+                      <Text type="secondary">
+                        <StudioName data={row.studio} />
+                      </Text>
+                    </div>
+                  ) : undefined}
+                </>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "columns.library.anthology.title",
+            }),
+            dataIndex: "subtitle",
+            key: "subtitle",
+            render: (text, row, index, action) => {
+              return (
+                <Space>
+                  {row.anthologyTitle}
+                  {row.anthologyCount ? (
+                    <Badge color="geekblue" count={row.anthologyCount} />
+                  ) : undefined}
+                </Space>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.summary.label",
+            }),
+            dataIndex: "summary",
+            key: "summary",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.publicity.label",
+            }),
+            dataIndex: "publicity",
+            key: "publicity",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: PublicityValueEnum(),
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.created-at.label",
+            }),
+            key: "created-at",
+            width: 100,
+            search: false,
+            dataIndex: "createdAt",
+            valueType: "date",
+            sorter: (a, b) => a.createdAt - b.createdAt,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            hideInTable: !editable,
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button
+                  trigger={["click", "contextMenu"]}
+                  key={index}
+                  type="link"
+                  menu={{
+                    items: [
+                      {
+                        key: "tpl",
+                        label: (
+                          <ArticleTplMaker
+                            title={row.title}
+                            type="article"
+                            id={row.id}
+                            trigger={<>模版</>}
+                          />
+                        ),
+                        icon: <ReconciliationOutlined />,
+                      },
+                      {
+                        key: "share",
+                        label: intl.formatMessage({
+                          id: "buttons.share",
+                        }),
+                        icon: <TeamOutlined />,
+                      },
+                      {
+                        key: "addToAnthology",
+                        label: (
+                          <AddToAnthology
+                            trigger="加入文集"
+                            studioName={studioName}
+                            articleIds={[row.id]}
+                          />
+                        ),
+                        icon: <FolderAddOutlined />,
+                      },
+                      {
+                        key: "remove",
+                        label: intl.formatMessage({
+                          id: "buttons.delete",
+                        }),
+                        icon: <DeleteOutlined />,
+                        danger: true,
+                      },
+                    ],
+                    onClick: (e) => {
+                      switch (e.key) {
+                        case "share":
+                          showShareModal(row.id, EResType.article);
+                          break;
+                        case "remove":
+                          showDeleteConfirm(row.id, row.title);
+                          break;
+                        default:
+                          break;
+                      }
+                    },
+                  }}
+                >
+                  <Link
+                    key={index}
+                    to={`/article/article/${row.id}`}
+                    target="_blank"
+                  >
+                    {intl.formatMessage({
+                      id: "buttons.view",
+                    })}
+                  </Link>
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        rowSelection={
+          multiple
+            ? {
+                // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+                // 注释该行则默认不显示下拉选项
+                selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+              }
+            : undefined
+        }
+        tableAlertRender={({
+          selectedRowKeys,
+          selectedRows,
+          onCleanSelected,
+        }) => (
+          <Space size={24}>
+            <span>
+              {intl.formatMessage({ id: "buttons.selected" })}
+              {selectedRowKeys.length}
+              <Button type="link" onClick={onCleanSelected}>
+                {intl.formatMessage({ id: "buttons.unselect" })}
+              </Button>
+            </span>
+          </Space>
+        )}
+        tableAlertOptionRender={({
+          intl,
+          selectedRowKeys,
+          selectedRows,
+          onCleanSelected,
+        }) => {
+          return (
+            <AddToAnthology
+              studioName={studioName}
+              articleIds={selectedRowKeys.map((item) => item.toString())}
+              onFinally={() => {
+                onCleanSelected();
+              }}
+            />
+          );
+        }}
+        request={async (params = {}, sorter, filter) => {
+          let url = `/v2/article?view=studio&view2=${activeKey}&name=${studioName}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 10);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          url += params.keyword ? "&search=" + params.keyword : "";
+
+          if (typeof anthologyId !== "undefined") {
+            url += "&anthology=" + anthologyId;
+          }
+          const res = await get<IArticleListResponse>(url);
+          const items: DataItem[] = res.data.rows.map((item, id) => {
+            const date = new Date(item.created_at);
+            return {
+              sn: id + 1,
+              id: item.uid,
+              title: item.title,
+              subtitle: item.subtitle,
+              summary: item.summary,
+              anthologyCount: item.anthology_count,
+              anthologyTitle: item.anthology_first?.title,
+              publicity: item.status,
+              createdAt: date.getTime(),
+              studio: item.studio,
+              editor: item.editor,
+            };
+          });
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+          pageSize: 10,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          activeKey === "my" ? (
+            <AnthologySelect
+              studioName={studioName}
+              onSelect={(value: string) => {
+                setAnthologyId(value);
+                ref.current?.reload();
+              }}
+            />
+          ) : undefined,
+          <Popover
+            content={
+              <ArticleCreate
+                studio={studioName}
+                anthologyId={anthologyId}
+                onSuccess={() => {
+                  setOpenCreate(false);
+                  ref.current?.reload();
+                }}
+              />
+            }
+            placement="bottomRight"
+            trigger="click"
+            open={openCreate}
+            onOpenChange={(open: boolean) => {
+              setOpenCreate(open);
+            }}
+          >
+            <Button key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({ id: "buttons.create" })}
+            </Button>
+          </Popover>,
+        ]}
+        toolbar={{
+          menu: {
+            activeKey,
+            items: [
+              {
+                key: "my",
+                label: (
+                  <span>
+                    此工作室的
+                    {renderBadge(myNumber, activeKey === "my")}
+                  </span>
+                ),
+              },
+              {
+                key: "collaboration",
+                label: (
+                  <span>
+                    协作
+                    {renderBadge(
+                      collaborationNumber,
+                      activeKey === "collaboration"
+                    )}
+                  </span>
+                ),
+              },
+            ],
+            onChange(key) {
+              console.log("show course", key);
+              setActiveKey(key);
+              setAnthologyId(undefined);
+              ref.current?.reload();
+            },
+          },
+        }}
+      />
+
+      <Modal
+        destroyOnClose={true}
+        width={700}
+        title="协作"
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+      >
+        <Share resId={shareResId} resType={shareResType} />
+      </Modal>
+    </>
+  );
+};
+
+export default ArticleListWidget;

+ 58 - 0
dashboard/src/components/article/ArticleListModal.tsx

@@ -0,0 +1,58 @@
+import { useState } from "react";
+import { Modal } from "antd";
+
+import ArticleList from "./ArticleList";
+
+interface IWidget {
+  studioName?: string;
+  trigger?: React.ReactNode;
+  multiple?: boolean;
+  onSelect?: Function;
+}
+const ArticleListModalWidget = ({
+  studioName,
+  trigger = "Article",
+  multiple = true,
+  onSelect,
+}: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  const showModal = () => {
+    setIsModalOpen(true);
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+  };
+
+  return (
+    <>
+      <span onClick={showModal}>{trigger}</span>
+      <Modal
+        width={"80%"}
+        title="文章列表"
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+      >
+        <ArticleList
+          studioName={studioName}
+          editable={false}
+          multiple={multiple}
+          onSelect={(id: string, title: string) => {
+            if (typeof onSelect !== "undefined") {
+              onSelect(id, title);
+            }
+            handleOk();
+          }}
+        />
+      </Modal>
+    </>
+  );
+};
+
+export default ArticleListModalWidget;

+ 2 - 2
dashboard/src/components/article/ArticleSkeleton.tsx

@@ -1,6 +1,6 @@
 import { Divider, Skeleton } from "antd";
 
-const Widget = () => {
+const ArticleSkeletonWidget = () => {
   return (
     <div style={{ paddingTop: "1em" }}>
       <Skeleton title={{ width: 200 }} paragraph={{ rows: 1 }} active />
@@ -10,4 +10,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default ArticleSkeletonWidget;

+ 2 - 2
dashboard/src/components/article/ArticleTplMaker.tsx

@@ -13,7 +13,7 @@ interface IWidget {
   onSelect?: Function;
   onCancel?: Function;
 }
-const Widget = ({
+const ArticleTplMakerWidget = ({
   type,
   id,
   title,
@@ -112,4 +112,4 @@ style=${styleText}`;
   );
 };
 
-export default Widget;
+export default ArticleTplMakerWidget;

+ 2 - 2
dashboard/src/components/article/ArticleView.tsx

@@ -23,7 +23,7 @@ export interface IWidgetArticleData {
   articleId?: string;
 }
 
-const Widget = ({
+const ArticleViewWidget = ({
   id,
   title = "",
   subTitle,
@@ -94,4 +94,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default ArticleViewWidget;

+ 26 - 11
dashboard/src/components/article/EditableTree.tsx

@@ -3,17 +3,13 @@ import { useEffect } from "react";
 import { Tree, Typography } from "antd";
 import type { DataNode, TreeProps } from "antd/es/tree";
 import { Key } from "antd/lib/table/interface";
-import {
-  FileAddOutlined,
-  DeleteOutlined,
-  SaveOutlined,
-} from "@ant-design/icons";
+import { DeleteOutlined, SaveOutlined } from "@ant-design/icons";
 import { Button, Divider, Space } from "antd";
 import { useIntl } from "react-intl";
 
 const { Text } = Typography;
 
-interface TreeNodeData {
+export interface TreeNodeData {
   key: string;
   title: string | React.ReactNode;
   children: TreeNodeData[];
@@ -119,24 +115,42 @@ function treeToList(treeNode: TreeNodeData[]): ListNodeData[] {
 
   return arrTocTree;
 }
-interface IWidgetEditableTree {
+interface IWidget {
   treeData: ListNodeData[];
+  addFileButton?: React.ReactNode;
+  addOnArticle?: TreeNodeData;
   onChange?: Function;
   onSelect?: Function;
   onSave?: Function;
+  onAddFile?: Function;
 }
-const Widget = ({
+const EditableTreeWidget = ({
   treeData,
+  addFileButton,
+  addOnArticle,
   onChange,
   onSelect,
   onSave,
-}: IWidgetEditableTree) => {
+  onAddFile,
+}: IWidget) => {
   const intl = useIntl();
 
   const [gData, setGData] = useState<TreeNodeData[]>([]);
   const [listTreeData, setListTreeData] = useState<ListNodeData[]>();
   const [keys, setKeys] = useState<Key>("");
 
+  useEffect(() => {
+    if (typeof addOnArticle === "undefined") {
+      return;
+    }
+    console.log("add ", addOnArticle);
+
+    const newTreeData = [...gData, addOnArticle];
+    setGData(newTreeData);
+    const list = treeToList(newTreeData);
+    setListTreeData(list);
+  }, [addOnArticle]);
+
   useEffect(() => {
     const data = tocGetTreeData(treeData);
     console.log("tree data", data);
@@ -223,7 +237,8 @@ const Widget = ({
   return (
     <>
       <Space>
-        <Button icon={<FileAddOutlined />}>添加</Button>
+        {addFileButton}
+
         <Button
           icon={<DeleteOutlined />}
           danger
@@ -302,4 +317,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default EditableTreeWidget;

+ 2 - 2
dashboard/src/components/article/ExerciseList.tsx

@@ -22,7 +22,7 @@ interface IWidget {
   articleId?: string;
   exerciseId?: string;
 }
-const Widget = ({ courseId, articleId, exerciseId }: IWidget) => {
+const ExerciseListWidget = ({ courseId, articleId, exerciseId }: IWidget) => {
   const [data, setData] = useState<DataItem[]>();
 
   useEffect(() => {
@@ -80,4 +80,4 @@ const Widget = ({ courseId, articleId, exerciseId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ExerciseListWidget;

+ 2 - 2
dashboard/src/components/article/Find.tsx

@@ -3,7 +3,7 @@ import { Input, Space, Select } from "antd";
 
 const { Search } = Input;
 
-const Widget = () => {
+const FindWidget = () => {
   const [isLoading, setIsLoading] = useState(false);
 
   const onSearch = (value: string) => {
@@ -53,4 +53,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default FindWidget;

+ 4 - 4
dashboard/src/components/article/MainMenu.tsx

@@ -2,7 +2,7 @@ import { Button, Dropdown } from "antd";
 import { AppstoreOutlined } from "@ant-design/icons";
 import { mainMenuItems } from "../library/HeadBar";
 
-const Widget = () => {
+const MainMenuWidget = () => {
   return (
     <Dropdown
       menu={{ items: mainMenuItems }}
@@ -11,11 +11,11 @@ const Widget = () => {
     >
       <Button
         type="text"
-        style={{ display: "block" }}
+        style={{ display: "block", color: "white" }}
         icon={<AppstoreOutlined />}
-      ></Button>
+      />
     </Dropdown>
   );
 };
 
-export default Widget;
+export default MainMenuWidget;

+ 69 - 41
dashboard/src/components/article/ModeSwitch.tsx

@@ -1,53 +1,81 @@
 import { Segmented } from "antd";
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
-import { modeChange } from "../../reducers/article-mode";
-import store from "../../store";
-import { ArticleMode } from "./Article";
+import { IChannel } from "../channel/Channel";
+import ChannelPicker from "../channel/ChannelPicker";
 
 interface IWidget {
-  initMode?: string;
+  currMode?: string;
+  channel: string | null;
   onModeChange?: Function;
+  onChannelChange?: Function;
 }
-const Widget = ({ initMode = "read", onModeChange }: IWidget) => {
+const ModeSwitchWidget = ({
+  currMode = "read",
+  onModeChange,
+  channel,
+  onChannelChange,
+}: IWidget) => {
   const intl = useIntl();
-  const [mode, setMode] = useState<string>(initMode);
+  const [mode, setMode] = useState<string>(currMode);
+  const [newMode, setNewMode] = useState<string>();
+  const [channelPickerOpen, setChannelPickerOpen] = useState(false);
+  useEffect(() => {
+    setMode(currMode);
+  }, [currMode]);
   return (
-    <Segmented
-      size="middle"
-      style={{
-        color: "rgb(134 134 134 / 90%)",
-        backgroundColor: "rgb(129 129 129 / 17%)",
-        display: "block",
-      }}
-      options={[
-        {
-          label: intl.formatMessage({ id: "buttons.read" }),
-          value: "read",
-        },
-        {
-          label: intl.formatMessage({ id: "buttons.translate" }),
-          value: "edit",
-        },
-        {
-          label: intl.formatMessage({ id: "buttons.wbw" }),
-          value: "wbw",
-        },
-      ]}
-      value={mode}
-      onChange={(value) => {
-        const newMode = value.toString();
-        if (typeof onModeChange !== "undefined") {
-          if (mode === "read" || newMode === "read") {
-            onModeChange(newMode);
+    <>
+      <Segmented
+        size="middle"
+        style={{
+          color: "rgb(134 134 134 / 90%)",
+          backgroundColor: "rgb(129 129 129 / 17%)",
+          display: "block",
+        }}
+        defaultValue={currMode}
+        options={[
+          {
+            label: intl.formatMessage({ id: "buttons.read" }),
+            value: "read",
+          },
+          {
+            label: intl.formatMessage({ id: "buttons.translate" }),
+            value: "edit",
+          },
+          {
+            label: intl.formatMessage({ id: "buttons.wbw" }),
+            value: "wbw",
+          },
+        ]}
+        value={mode}
+        onChange={(value) => {
+          const _mode = value.toString();
+
+          if (_mode !== "read" && channel === null) {
+            setChannelPickerOpen(true);
+            setNewMode(_mode);
+          } else {
+            if (typeof onModeChange !== "undefined") {
+              onModeChange(_mode);
+            }
+            setMode(_mode);
+          }
+        }}
+      />
+      <ChannelPicker
+        open={channelPickerOpen}
+        onClose={() => setChannelPickerOpen(false)}
+        onSelect={(channels: IChannel[]) => {
+          if (newMode) {
+            setMode(newMode);
+          }
+          if (typeof onChannelChange !== "undefined") {
+            onChannelChange(channels, newMode);
           }
-        }
-        setMode(newMode);
-        //发布mode变更
-        store.dispatch(modeChange(newMode as ArticleMode));
-      }}
-    />
+        }}
+      />
+    </>
   );
 };
 
-export default Widget;
+export default ModeSwitchWidget;

+ 2 - 2
dashboard/src/components/article/Nav.tsx

@@ -1,6 +1,6 @@
 import { Space, Select } from "antd";
 
-const Widget = () => {
+const NavWidget = () => {
   return (
     <div>
       <Space direction="vertical">
@@ -35,4 +35,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default NavWidget;

+ 15 - 3
dashboard/src/components/article/PaliTextToc.tsx

@@ -1,3 +1,4 @@
+import { Key } from "antd/lib/table/interface";
 import { useState, useEffect } from "react";
 
 import { get } from "../../request";
@@ -9,8 +10,9 @@ interface IWidget {
   book?: number;
   para?: number;
   channel?: string;
+  onSelect?: Function;
 }
-const Widget = ({ book, para, channel }: IWidget) => {
+const PaliTextTocWidget = ({ book, para, channel, onSelect }: IWidget) => {
   const [tocList, setTocList] = useState<ListNodeData[]>([]);
   useEffect(() => {
     get<IPaliTocListResponse>(
@@ -26,7 +28,17 @@ const Widget = ({ book, para, channel }: IWidget) => {
       setTocList(toc);
     });
   }, [book, para]);
-  return <TocTree treeData={tocList} expandedKey={[`${book}-${para}`]} />;
+  return (
+    <TocTree
+      treeData={tocList}
+      expandedKey={[`${book}-${para}`]}
+      onSelect={(selectedKeys: Key[]) => {
+        if (typeof onSelect !== "undefined") {
+          onSelect(selectedKeys);
+        }
+      }}
+    />
+  );
 };
 
-export default Widget;
+export default PaliTextTocWidget;

+ 2 - 2
dashboard/src/components/article/ProTabs.tsx

@@ -23,7 +23,7 @@ const setting = (
   </>
 );
 
-const Widget = () => {
+const ProTabsWidget = () => {
   const [value2, setValue2] = useState("close");
   const divSetting = useRef<HTMLDivElement>(null);
   const divDict = useRef<HTMLDivElement>(null);
@@ -170,4 +170,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default ProTabsWidget;

+ 90 - 47
dashboard/src/components/article/RightPanel.tsx

@@ -1,85 +1,128 @@
-import { Affix } from "antd";
+import { Affix, Button, Tabs } from "antd";
 import { useEffect, useState } from "react";
+import { CloseOutlined } from "@ant-design/icons";
+
 import { IChannel } from "../channel/Channel";
 import ChannelPickerTable from "../channel/ChannelPickerTable";
-
 import DictComponent from "../dict/DictComponent";
 import { ArticleType } from "./Article";
+import { useAppSelector } from "../../hooks";
+import { openPanel, rightPanel } from "../../reducers/right-panel";
+import store from "../../store";
 
-export type TPanelName = "dict" | "channel" | "close";
+export type TPanelName = "dict" | "channel" | "close" | "open";
 interface IWidget {
   curr?: TPanelName;
   type: ArticleType;
   articleId: string;
   selectedChannelKeys?: string[];
   onChannelSelect?: Function;
-  channelReload?: boolean;
+  onClose?: Function;
+  onTabChange?: Function;
 }
-const Widget = ({
+const RightPanelWidget = ({
   curr = "close",
   type,
   articleId,
   onChannelSelect,
   selectedChannelKeys,
-  channelReload = false,
+  onClose,
+  onTabChange,
 }: IWidget) => {
-  const [dict, setDict] = useState("none");
-  const [channel, setChannel] = useState("none");
+  const [open, setOpen] = useState(false);
+  const [activeTab, setActiveTab] = useState<string>("dict");
+
+  const _openPanel = useAppSelector(rightPanel);
+
+  useEffect(() => {
+    console.log("panel", _openPanel);
+    if (typeof _openPanel !== "undefined") {
+      if (typeof onTabChange !== "undefined") {
+        onTabChange(_openPanel);
+      }
+      store.dispatch(openPanel(undefined));
+    }
+  }, [_openPanel]);
 
   useEffect(() => {
     switch (curr) {
+      case "open":
+        setOpen(true);
+        break;
       case "dict":
-        setDict("block");
-        setChannel("none");
+        setOpen(true);
+        setActiveTab(curr);
         break;
       case "channel":
-        setDict("none");
-        setChannel("block");
+        setOpen(true);
+        setActiveTab(curr);
+        break;
+      case "close":
+        setOpen(false);
         break;
       default:
-        setDict("none");
-        setChannel("none");
+        setOpen(false);
         break;
     }
   }, [curr]);
   return (
     <Affix offsetTop={44}>
-      <div key="panel">
-        <div
-          key="DictComponent"
-          style={{
-            width: 350,
-            height: `calc(100vh - 44px)`,
-            overflowY: "scroll",
-            display: dict,
-          }}
-        >
-          <DictComponent />
-        </div>
-        <div
-          key="ChannelPickerTable"
-          style={{
-            width: 350,
-            height: `calc(100vh - 44px)`,
-            overflowY: "scroll",
-            display: channel,
+      <div
+        key="panel"
+        style={{
+          width: 350,
+          height: `calc(100vh - 44px)`,
+          overflowY: "scroll",
+          display: open ? "block" : "none",
+        }}
+      >
+        <Tabs
+          size="small"
+          defaultActiveKey={curr}
+          activeKey={activeTab}
+          onChange={(activeKey: string) => setActiveTab(activeKey)}
+          tabBarExtraContent={{
+            right: (
+              <Button
+                type="text"
+                size="small"
+                icon={<CloseOutlined />}
+                onClick={() => {
+                  if (typeof onClose !== "undefined") {
+                    onClose();
+                  }
+                }}
+              />
+            ),
           }}
-        >
-          <ChannelPickerTable
-            type={type}
-            articleId={articleId}
-            selectedKeys={selectedChannelKeys}
-            onSelect={(e: IChannel[]) => {
-              console.log(e);
-              if (typeof onChannelSelect !== "undefined") {
-                onChannelSelect(e);
-              }
-            }}
-          />
-        </div>
+          items={[
+            {
+              label: `字典`,
+              key: "dict",
+              children: <DictComponent />,
+            },
+            {
+              label: `channel`,
+              key: "channel",
+              children: (
+                <ChannelPickerTable
+                  type={type}
+                  articleId={articleId}
+                  selectedKeys={selectedChannelKeys}
+                  onSelect={(e: IChannel[]) => {
+                    console.log(e);
+                    if (typeof onChannelSelect !== "undefined") {
+                      onChannelSelect(e);
+                    }
+                  }}
+                />
+              ),
+            },
+          ]}
+        />
       </div>
     </Affix>
   );
 };
 
-export default Widget;
+export default RightPanelWidget;

+ 19 - 3
dashboard/src/components/article/RightToolsSwitch.tsx

@@ -1,15 +1,31 @@
 import { Segmented } from "antd";
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import { useIntl } from "react-intl";
+import { useAppSelector } from "../../hooks";
+import { rightPanel } from "../../reducers/right-panel";
 import { TPanelName } from "./RightPanel";
 
 interface IWidget {
   initMode?: string;
   onModeChange?: Function;
 }
-const Widget = ({ initMode = "close", onModeChange }: IWidget) => {
+const RightToolsSwitchWidget = ({
+  initMode = "close",
+  onModeChange,
+}: IWidget) => {
   const intl = useIntl();
   const [mode, setMode] = useState<string>(initMode);
+  const _openPanel = useAppSelector(rightPanel);
+
+  useEffect(() => {
+    if (typeof _openPanel !== "undefined") {
+      if (typeof onModeChange !== "undefined") {
+        onModeChange(_openPanel);
+      }
+      setMode(_openPanel);
+    }
+  }, [_openPanel]);
+
   return (
     <Segmented
       size="middle"
@@ -44,4 +60,4 @@ const Widget = ({ initMode = "close", onModeChange }: IWidget) => {
   );
 };
 
-export default Widget;
+export default RightToolsSwitchWidget;

+ 2 - 2
dashboard/src/components/article/TermShell.tsx

@@ -5,7 +5,7 @@ import { message } from "../../reducers/command";
 
 import TermEdit, { ITerm } from "../term/TermEdit";
 
-const Widget = () => {
+const TermShellWidget = () => {
   const [termProps, setTermProps] = useState<ITerm>();
   //接收术语消息
   const commandMsg = useAppSelector(message);
@@ -22,4 +22,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default TermShellWidget;

+ 2 - 2
dashboard/src/components/article/TocTree.tsx

@@ -95,7 +95,7 @@ interface IWidgetTocTree {
   onSelect?: Function;
 }
 
-const Widget = ({ treeData, expandedKey, onSelect }: IWidgetTocTree) => {
+const TocTreeWidget = ({ treeData, expandedKey, onSelect }: IWidgetTocTree) => {
   const [tree, setTree] = useState<TreeNodeData[]>();
   const [expanded, setExpanded] = useState(expandedKey);
   console.log("new tree data", treeData);
@@ -142,4 +142,4 @@ const Widget = ({ treeData, expandedKey, onSelect }: IWidgetTocTree) => {
   );
 };
 
-export default Widget;
+export default TocTreeWidget;

+ 2 - 2
dashboard/src/components/article/ToolButton.tsx

@@ -6,7 +6,7 @@ interface IWidget {
   content?: JSX.Element;
   title?: string;
 }
-const Widget = ({ icon, content, title }: IWidget) => {
+const ToolButtonWidget = ({ icon, content, title }: IWidget) => {
   const [open, setOpen] = useState(false);
 
   return (
@@ -35,4 +35,4 @@ const Widget = ({ icon, content, title }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ToolButtonWidget;

+ 3 - 3
dashboard/src/components/article/ToolButtonDiscussion.tsx

@@ -50,11 +50,11 @@ interface IWidget {
   type?: string;
   articleId?: string;
 }
-const Widget = ({ type, articleId }: IWidget) => {
+const ToolButtonDiscussionWidget = ({ type, articleId }: IWidget) => {
   const [treeData, setTreeData] = useState<DataNode[]>([]);
 
   const refresh = () => {
-    const pr = document.querySelectorAll("div.pr_icon[has-disc='true']");
+    const pr = document.querySelectorAll("div.tran_sent");
 
     let prRequestData: IPrTreeRequestData[] = [];
     for (let index = 0; index < pr.length; index++) {
@@ -128,4 +128,4 @@ const Widget = ({ type, articleId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ToolButtonDiscussionWidget;

+ 230 - 0
dashboard/src/components/article/ToolButtonNav.tsx

@@ -0,0 +1,230 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Button, message, Space, Tag, Tree, Typography } from "antd";
+import { CompassOutlined, ReloadOutlined } from "@ant-design/icons";
+
+import ToolButton from "./ToolButton";
+import { useAppSelector } from "../../hooks";
+import { sentenceList } from "../../reducers/sentence";
+import ToolButtonNavMore from "./ToolButtonNavMore";
+import ToolButtonNavSliceTitle from "./ToolButtonNavSliceTitle";
+
+const { Text } = Typography;
+
+interface DataNode {
+  title: React.ReactNode;
+  key: string;
+  isLeaf?: boolean;
+  children?: DataNode[];
+}
+interface ISlice {
+  id: number;
+  para: string;
+  len: number;
+}
+interface IWidget {
+  type?: string;
+  articleId?: string;
+}
+const ToolButtonNavWidget = ({ type, articleId }: IWidget) => {
+  const [treeData, setTreeData] = useState<DataNode[]>([]);
+  const [slice, setSlice] = useState<number>(1);
+  const navigate = useNavigate();
+
+  const allSentList = useAppSelector(sentenceList);
+
+  const refresh = () => {
+    const divList = document.querySelectorAll("div.pcd_sent");
+
+    let sentList: string[] = [];
+    for (let index = 0; index < divList.length; index++) {
+      const element = divList[index];
+      const id = element.id.split("_");
+      sentList.push(id[1]);
+    }
+
+    //计算总字符数
+    let allStrLen = 0;
+    allSentList.forEach((value) => {
+      allStrLen += value.origin ? value.origin[0].length : 0;
+    });
+    const oneSliceLen = allStrLen / slice;
+
+    let paraSlice: ISlice[] = [];
+    let currSliceId = 0;
+    let currSliceLen = 0;
+    for (let index = 0; index < sentList.length; index++) {
+      const sent = sentList[index];
+      const currPara = sent.split("-").slice(0, 2).join("-");
+      if (!paraSlice.find((value) => value.para === currPara)) {
+        let paraLen = 0; //段落字符长度
+        allSentList
+          .filter(
+            (value) => value.id.split("-").slice(0, 2).join("-") === currPara
+          )
+          .map((item) => (item.origin ? item.origin[0] : ""))
+          .forEach((value) => {
+            paraLen += value.length;
+          });
+
+        //计算如果放进去或者不放进去哪个更接近块的预设大小
+        let next = currSliceId;
+        if (currSliceLen + paraLen <= oneSliceLen) {
+          //放进去还不到一个块,直接放进去
+          currSliceLen = currSliceLen + paraLen;
+        } else if (currSliceLen === 0) {
+          //当前块里没东西直接放进去
+          if (paraLen >= oneSliceLen) {
+            //此块比一个块大
+            next = currSliceId + 1;
+            currSliceLen = 0;
+          } else {
+            currSliceLen = paraLen;
+          }
+        } else {
+          //放进去超过一个块,需要比较是放进去好还是不放进去好
+          const remain = oneSliceLen - currSliceLen;
+          const extra = currSliceLen + paraLen - oneSliceLen;
+          if (remain < extra) {
+            //移到下一个块
+            currSliceId++;
+            next = currSliceId;
+            currSliceLen = paraLen;
+          } else {
+            //放这个块
+            currSliceLen += paraLen;
+          }
+        }
+        paraSlice.push({ id: currSliceId, para: currPara, len: paraLen });
+        currSliceId = next;
+      }
+    }
+
+    const mSlice: DataNode[] = new Array(currSliceId + 1)
+      .fill(1)
+      .map((item, index) => {
+        let sliceStrLen = 0;
+        let sliceChildren: string[] = [];
+        const newTree: DataNode[] = paraSlice
+          .filter((value) => value.id === index)
+          .map((item) => {
+            const children = sentList
+              .filter(
+                (value) => value.split("-").slice(0, 2).join("-") === item.para
+              )
+              .map((item1) => {
+                const str = allSentList.find((value) => value.id === item1);
+                return {
+                  title: str
+                    ? str.origin
+                      ? str.origin[0].slice(0, 30)
+                      : item1
+                    : item1,
+                  key: item1,
+                };
+              });
+            sliceStrLen += item.len;
+            sliceChildren.push(item.para);
+            return {
+              title: item.para + "-" + item.len.toString(),
+              key: item.para,
+              children: children,
+            };
+          });
+
+        return {
+          title: (
+            <ToolButtonNavSliceTitle
+              label={
+                <Space>
+                  <Text>{`第${index + 1}组`}</Text>
+                  <Tag>{`${sliceStrLen}`}</Tag>
+                </Space>
+              }
+              onMenuClick={(key: string) => {
+                if (sliceChildren.length > 0) {
+                  const [book, para] = sliceChildren[0].split("-");
+                  const paraList = sliceChildren.map(
+                    (item) => item.split("-")[1]
+                  );
+                  let url = `/article/para/${book}?par=${paraList.join(",")}`;
+                  console.log("url", url);
+
+                  switch (key) {
+                    case "copy-link":
+                      const fullUrl =
+                        process.env.REACT_APP_WEB_HOST +
+                        process.env.PUBLIC_URL +
+                        url;
+                      navigator.clipboard.writeText(fullUrl).then(() => {
+                        message.success("链接地址已经拷贝到剪贴板");
+                      });
+
+                      break;
+                    case "open":
+                      navigate(url);
+                      break;
+                  }
+                }
+              }}
+            />
+          ),
+          key: `slice_${index}`,
+          children: newTree,
+        };
+      });
+    if (mSlice.length > 1) {
+      setTreeData(mSlice);
+    } else if (mSlice.length === 1) {
+      setTreeData(mSlice[0].children ? mSlice[0].children : []);
+    }
+  };
+
+  useEffect(refresh, [slice]);
+  return (
+    <ToolButton
+      title="导航"
+      icon={<CompassOutlined />}
+      content={
+        <>
+          <div style={{ textAlign: "right" }}>
+            <Space>
+              <Button
+                onClick={() => {
+                  refresh();
+                }}
+                size="small"
+                type="link"
+                icon={<ReloadOutlined />}
+              />
+              <ToolButtonNavMore
+                onSliceChange={(value: number) => {
+                  console.log(`selected ${value}`);
+                  setSlice(value);
+                }}
+              />
+            </Space>
+          </div>
+
+          <Tree
+            treeData={treeData}
+            titleRender={(node) => {
+              const ele = document.getElementById(node.key);
+              return (
+                <div
+                  onClick={() => {
+                    ele?.scrollIntoView();
+                  }}
+                >
+                  {node.title}
+                </div>
+              );
+            }}
+          />
+        </>
+      }
+    />
+  );
+};
+
+export default ToolButtonNavWidget;

+ 47 - 0
dashboard/src/components/article/ToolButtonNavMore.tsx

@@ -0,0 +1,47 @@
+import { SettingOutlined } from "@ant-design/icons";
+import { Button, Dropdown, MenuProps } from "antd";
+
+interface IWidget {
+  onSliceChange?: Function;
+}
+const CaseFormulaWidget = ({ onSliceChange }: IWidget) => {
+  const sliceOption = new Array(8).fill(1).map((item, index) => {
+    return { key: index + 2, label: index + 2 };
+  });
+  const items: MenuProps["items"] = [
+    {
+      label: "分组",
+      key: "slice",
+      children: [
+        {
+          label: "不分组",
+          key: 1,
+        },
+        ...sliceOption,
+      ],
+    },
+  ];
+  return (
+    <Dropdown
+      menu={{
+        items: items,
+        onClick: (e) => {
+          console.log("click ", e.key);
+          if (typeof onSliceChange !== "undefined") {
+            onSliceChange(e.key);
+          }
+        },
+      }}
+      placement="bottomRight"
+    >
+      <Button
+        type="text"
+        size="small"
+        icon={<SettingOutlined />}
+        onClick={(e) => e.preventDefault()}
+      />
+    </Dropdown>
+  );
+};
+
+export default CaseFormulaWidget;

+ 37 - 0
dashboard/src/components/article/ToolButtonNavSliceTitle.tsx

@@ -0,0 +1,37 @@
+import { Dropdown } from "antd";
+import React from "react";
+
+interface IWidget {
+  label?: React.ReactNode;
+  onMenuClick?: Function;
+}
+
+const ToolButtonNavSliceTitleWidget = ({ label, onMenuClick }: IWidget) => {
+  return (
+    <Dropdown.Button
+      type="text"
+      trigger={["click"]}
+      menu={{
+        items: [
+          {
+            key: "copy-link",
+            label: "复制链接",
+          },
+          {
+            key: "open",
+            label: "打开",
+          },
+        ],
+        onClick: (e) => {
+          if (typeof onMenuClick !== "undefined") {
+            onMenuClick(e.key);
+          }
+        },
+      }}
+    >
+      <>{label}</>
+    </Dropdown.Button>
+  );
+};
+
+export default ToolButtonNavSliceTitleWidget;

+ 3 - 3
dashboard/src/components/article/ToolButtonPr.tsx

@@ -49,11 +49,11 @@ interface IWidget {
   type?: string;
   articleId?: string;
 }
-const Widget = ({ type, articleId }: IWidget) => {
+const ToolButtonPrWidget = ({ type, articleId }: IWidget) => {
   const [treeData, setTreeData] = useState<DataNode[]>([]);
 
   const refresh = () => {
-    const pr = document.querySelectorAll("div.pr_icon[has-pr='true']");
+    const pr = document.querySelectorAll("div.tran_sent");
 
     let prRequestData: IPrTreeRequestData[] = [];
     for (let index = 0; index < pr.length; index++) {
@@ -127,4 +127,4 @@ const Widget = ({ type, articleId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ToolButtonPrWidget;

+ 2 - 2
dashboard/src/components/article/ToolButtonSearch.tsx

@@ -6,7 +6,7 @@ interface IWidget {
   type?: string;
   articleId?: string;
 }
-const Widget = ({ type, articleId }: IWidget) => {
+const ToolButtonSearchWidget = ({ type, articleId }: IWidget) => {
   const id = articleId?.split("_");
   let tocWidget = <></>;
   if (id && id.length > 0) {
@@ -21,4 +21,4 @@ const Widget = ({ type, articleId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ToolButtonSearchWidget;

+ 2 - 2
dashboard/src/components/article/ToolButtonSetting.tsx

@@ -7,7 +7,7 @@ interface IWidget {
   type?: string;
   articleId?: string;
 }
-const Widget = ({ type, articleId }: IWidget) => {
+const ToolButtonSettingWidget = ({ type, articleId }: IWidget) => {
   return (
     <ToolButton
       title="设置"
@@ -17,4 +17,4 @@ const Widget = ({ type, articleId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ToolButtonSettingWidget;

+ 2 - 2
dashboard/src/components/article/ToolButtonTag.tsx

@@ -6,7 +6,7 @@ interface IWidget {
   type?: string;
   articleId?: string;
 }
-const Widget = ({ type, articleId }: IWidget) => {
+const ToolButtonTagWidget = ({ type, articleId }: IWidget) => {
   const id = articleId?.split("_");
   let tocWidget = <></>;
   if (id && id.length > 0) {
@@ -19,4 +19,4 @@ const Widget = ({ type, articleId }: IWidget) => {
   return <ToolButton title="标签" icon={<TagOutlined />} content={tocWidget} />;
 };
 
-export default Widget;
+export default ToolButtonTagWidget;

+ 33 - 11
dashboard/src/components/article/ToolButtonToc.tsx

@@ -1,22 +1,44 @@
 import { MenuOutlined } from "@ant-design/icons";
+import { Key } from "antd/lib/table/interface";
+import { ArticleType } from "./Article";
 
 import PaliTextToc from "./PaliTextToc";
 import ToolButton from "./ToolButton";
 
 interface IWidget {
-  type?: string;
+  type?: ArticleType;
   articleId?: string;
+  onSelect?: Function;
 }
-const Widget = ({ type, articleId }: IWidget) => {
-  const id = articleId?.split("_");
+const ToolButtonTocWidget = ({ type, articleId, onSelect }: IWidget) => {
   let tocWidget = <></>;
-  if (id && id.length > 0) {
-    const sentId = id[0].split("-");
-    if (sentId.length > 1) {
-      tocWidget = (
-        <PaliTextToc book={parseInt(sentId[0])} para={parseInt(sentId[1])} />
-      );
-    }
+
+  switch (type) {
+    case "chapter":
+      const id = articleId?.split("_");
+      if (id && id.length > 0) {
+        const sentId = id[0].split("-");
+        if (sentId.length > 1) {
+          tocWidget = (
+            <PaliTextToc
+              book={parseInt(sentId[0])}
+              para={parseInt(sentId[1])}
+              onSelect={(selectedKeys: Key[]) => {
+                if (
+                  typeof onSelect !== "undefined" &&
+                  selectedKeys.length > 0
+                ) {
+                  onSelect(selectedKeys[0]);
+                }
+              }}
+            />
+          );
+        }
+      }
+      break;
+
+    default:
+      break;
   }
 
   return (
@@ -24,4 +46,4 @@ const Widget = ({ type, articleId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ToolButtonTocWidget;

+ 6 - 5
dashboard/src/components/auth/Avatar.tsx

@@ -1,7 +1,7 @@
 import { useIntl } from "react-intl";
 import { useEffect, useState } from "react";
 import { Link, useNavigate } from "react-router-dom";
-import { Tooltip } from "antd";
+import { Tooltip, Typography } from "antd";
 import { Avatar } from "antd";
 import { Popover } from "antd";
 import { ProCard } from "@ant-design/pro-components";
@@ -16,11 +16,12 @@ import { useAppSelector } from "../../hooks";
 import { currentUser as _currentUser } from "../../reducers/current-user";
 import { TooltipPlacement } from "antd/lib/tooltip";
 
+const { Title } = Typography;
+
 interface IWidget {
   placement?: TooltipPlacement;
 }
-const Widget = ({ placement = "bottomRight" }: IWidget) => {
-  // TODO
+const AvatarWidget = ({ placement = "bottomRight" }: IWidget) => {
   const intl = useIntl();
   const navigate = useNavigate();
   const [userName, setUserName] = useState<string>();
@@ -68,7 +69,7 @@ const Widget = ({ placement = "bottomRight" }: IWidget) => {
       ]}
     >
       <div>
-        <h2>{nickName}</h2>
+        <Title level={4}>{nickName}</Title>
         <div style={{ textAlign: "right" }}>
           {intl.formatMessage({
             id: "buttons.welcome",
@@ -93,4 +94,4 @@ const Widget = ({ placement = "bottomRight" }: IWidget) => {
   );
 };
 
-export default Widget;
+export default AvatarWidget;

+ 10 - 9
dashboard/src/components/auth/SignInAvatar.tsx

@@ -1,7 +1,7 @@
 import { useIntl } from "react-intl";
 import { useEffect, useState } from "react";
 import { Link, useNavigate } from "react-router-dom";
-import { Tooltip } from "antd";
+import { Tooltip, Typography } from "antd";
 import { Avatar } from "antd";
 import { Popover } from "antd";
 import { ProCard } from "@ant-design/pro-components";
@@ -15,8 +15,9 @@ import {
 import { useAppSelector } from "../../hooks";
 import { currentUser as _currentUser } from "../../reducers/current-user";
 
-const Widget = () => {
-  // TODO
+const { Title, Paragraph } = Typography;
+
+const SignInAvatarWidget = () => {
   const intl = useIntl();
   const navigate = useNavigate();
   const [userName, setUserName] = useState<string>();
@@ -64,14 +65,14 @@ const Widget = () => {
           </Tooltip>,
         ]}
       >
-        <div>
-          <h2>{nickName}</h2>
-          <div style={{ textAlign: "right" }}>
+        <Paragraph>
+          <Title level={3}>{nickName}</Title>
+          <Paragraph style={{ textAlign: "right" }}>
             {intl.formatMessage({
               id: "buttons.welcome",
             })}
-          </div>
-        </div>
+          </Paragraph>
+        </Paragraph>
       </ProCard>
     </>
   );
@@ -95,4 +96,4 @@ const Widget = () => {
   }
 };
 
-export default Widget;
+export default SignInAvatarWidget;

+ 2 - 2
dashboard/src/components/auth/StudioCard.tsx

@@ -7,7 +7,7 @@ interface IWidget {
   studio?: IStudio;
   children?: JSX.Element;
 }
-const Widget = ({ studio, children }: IWidget) => {
+const StudioCardWidget = ({ studio, children }: IWidget) => {
   const intl = useIntl();
 
   return (
@@ -46,4 +46,4 @@ const Widget = ({ studio, children }: IWidget) => {
   );
 };
 
-export default Widget;
+export default StudioCardWidget;

+ 2 - 2
dashboard/src/components/auth/StudioName.tsx

@@ -15,7 +15,7 @@ interface IWidghtStudio {
   showName?: boolean;
   onClick?: Function;
 }
-const Widget = ({
+const StudioNameWidget = ({
   data,
   showAvatar = true,
   showName = true,
@@ -39,4 +39,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default StudioNameWidget;

+ 2 - 2
dashboard/src/components/auth/ToLibaray.tsx → dashboard/src/components/auth/ToLibrary.tsx

@@ -2,7 +2,7 @@ import { useIntl } from "react-intl";
 import { Button } from "antd";
 import { Link } from "react-router-dom";
 
-const Widget = () => {
+const ToLibraryWidget = () => {
   const intl = useIntl();
 
   return (
@@ -25,4 +25,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default ToLibraryWidget;

+ 2 - 2
dashboard/src/components/auth/ToStudio.tsx

@@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
 import { useAppSelector } from "../../hooks";
 import { currentUser as _currentUser } from "../../reducers/current-user";
 
-const Widget = () => {
+const ToStudioWidget = () => {
   const intl = useIntl();
 
   const user = useAppSelector(_currentUser);
@@ -34,4 +34,4 @@ const Widget = () => {
   }
 };
 
-export default Widget;
+export default ToStudioWidget;

+ 2 - 2
dashboard/src/components/auth/User.tsx

@@ -8,7 +8,7 @@ export interface IUser {
   showAvatar?: boolean;
   showName?: boolean;
 }
-const Widget = ({
+const UserWidget = ({
   nickName,
   userName,
   avatar,
@@ -25,4 +25,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default UserWidget;

+ 2 - 2
dashboard/src/components/auth/UserName.tsx

@@ -4,7 +4,7 @@ export interface IUser {
   realName?: string;
   onClick?: Function;
 }
-const Widget = ({ id, nickName, realName, onClick }: IUser) => {
+const UserNameWidget = ({ id, nickName, realName, onClick }: IUser) => {
   return (
     <span
       onClick={(e) => {
@@ -18,4 +18,4 @@ const Widget = ({ id, nickName, realName, onClick }: IUser) => {
   );
 };
 
-export default Widget;
+export default UserNameWidget;

+ 22 - 8
dashboard/src/components/auth/setting/SettingArticle.tsx

@@ -1,24 +1,38 @@
 import { Divider } from "antd";
+import { useAppSelector } from "../../../hooks";
+import { settingInfo } from "../../../reducers/setting";
 
 import { SettingFind } from "./default";
 import SettingItem from "./SettingItem";
 
-const Widget = () => {
+const SettingArticleWidget = () => {
+  const settings = useAppSelector(settingInfo);
   return (
     <div>
       <Divider>阅读</Divider>
-      <SettingItem data={SettingFind("setting.display.original")} />
-      <SettingItem data={SettingFind("setting.layout.direction")} />
-      <SettingItem data={SettingFind("setting.layout.paragraph")} />
-      <SettingItem data={SettingFind("setting.pali.script.primary")} />
-      <SettingItem data={SettingFind("setting.pali.script.secondary")} />
+      <SettingItem data={SettingFind("setting.display.original", settings)} />
+      <SettingItem data={SettingFind("setting.layout.direction", settings)} />
+      <SettingItem data={SettingFind("setting.layout.paragraph", settings)} />
+      <SettingItem
+        data={SettingFind("setting.pali.script.primary", settings)}
+      />
+      <SettingItem
+        data={SettingFind("setting.pali.script.secondary", settings)}
+      />
       <Divider>翻译</Divider>
 
       <Divider>逐词解析</Divider>
+      <Divider>Nissaya</Divider>
+      <SettingItem
+        data={SettingFind("setting.nissaya.layout.read", settings)}
+      />
+      <SettingItem
+        data={SettingFind("setting.nissaya.layout.edit", settings)}
+      />
       <Divider>字典</Divider>
-      <SettingItem data={SettingFind("setting.dict.lang")} />
+      <SettingItem data={SettingFind("setting.dict.lang", settings)} />
     </div>
   );
 };
 
-export default Widget;
+export default SettingArticleWidget;

+ 6 - 2
dashboard/src/components/auth/setting/SettingItem.tsx

@@ -26,7 +26,11 @@ interface IWidgetSettingItem {
   autoSave?: boolean;
   onChange?: Function;
 }
-const Widget = ({ data, onChange, autoSave = true }: IWidgetSettingItem) => {
+const SettingItemWidget = ({
+  data,
+  onChange,
+  autoSave = true,
+}: IWidgetSettingItem) => {
   const intl = useIntl();
   const settings: ISettingItem[] | undefined = useAppSelector(settingInfo);
   const [value, setValue] = useState(data?.defaultValue);
@@ -206,4 +210,4 @@ const Widget = ({ data, onChange, autoSave = true }: IWidgetSettingItem) => {
   }
 };
 
-export default Widget;
+export default SettingItemWidget;

+ 45 - 5
dashboard/src/components/auth/setting/default.ts

@@ -25,17 +25,19 @@ export const GetUserSetting = (
   if (typeof currSetting !== "undefined") {
     return currSetting.value;
   } else {
-    const defaultSetting = SettingFind(key);
-    if (typeof defaultSetting !== "undefined") {
-      return defaultSetting.defaultValue;
+    const _default = defaultSetting.find((element) => element.key === key);
+    if (typeof _default !== "undefined") {
+      return _default.defaultValue;
     } else {
       return undefined;
     }
   }
 };
 
-export const SettingFind = (key: string): ISetting | undefined => {
-  const settings = useAppSelector(settingInfo);
+export const SettingFind = (
+  key: string,
+  settings?: ISettingItem[]
+): ISetting | undefined => {
   const userSetting = GetUserSetting(key, settings);
   let result = defaultSetting.find((element) => element.key === key);
   if (userSetting && result) {
@@ -184,4 +186,42 @@ export const defaultSetting: ISetting[] = [
       },
     ],
   },
+  {
+    /**
+     * nissaya 显示模式切换
+     */
+    key: "setting.nissaya.layout.read",
+    label: "setting.nissaya.layout.read.label",
+    defaultValue: "inline",
+    options: [
+      {
+        value: "inline",
+        label: "setting.nissaya.layout.inline.label",
+      },
+      {
+        value: "list",
+        label: "setting.nissaya.layout.list.label",
+      },
+    ],
+    widget: "radio-button",
+  },
+  {
+    /**
+     * nissaya 显示模式切换
+     */
+    key: "setting.nissaya.layout.edit",
+    label: "setting.nissaya.layout.edit.label",
+    defaultValue: "list",
+    options: [
+      {
+        value: "inline",
+        label: "setting.nissaya.layout.inline.label",
+      },
+      {
+        value: "list",
+        label: "setting.nissaya.layout.list.label",
+      },
+    ],
+    widget: "radio-button",
+  },
 ];

+ 2 - 2
dashboard/src/components/blog/BlogNav.tsx

@@ -8,7 +8,7 @@ interface IWidgetBlogNav {
   selectedKey: string;
   studio?: string;
 }
-const Widget = ({ selectedKey, studio }: IWidgetBlogNav) => {
+const BlogNavWidget = ({ selectedKey, studio }: IWidgetBlogNav) => {
   //Library head bar
   const intl = useIntl(); //i18n
   // TODO
@@ -70,4 +70,4 @@ const Widget = ({ selectedKey, studio }: IWidgetBlogNav) => {
     </Row>
   );
 };
-export default Widget;
+export default BlogNavWidget;

+ 16 - 16
dashboard/src/components/blog/Profile.tsx

@@ -1,20 +1,20 @@
 import { Card } from "antd";
 
-const Widget = () => {
-	return (
-		<>
-			<Card title="简介" bordered={false} style={{ width: "100%" }}>
-				<p>Card content</p>
-				<p>Card content</p>
-				<p>Card content</p>
-			</Card>
-			<Card title="团队" bordered={false} style={{ width: "100%" }}>
-				<p>Card content</p>
-				<p>Card content</p>
-				<p>Card content</p>
-			</Card>
-		</>
-	);
+const ProfileWidget = () => {
+  return (
+    <>
+      <Card title="简介" bordered={false} style={{ width: "100%" }}>
+        <p>Card content</p>
+        <p>Card content</p>
+        <p>Card content</p>
+      </Card>
+      <Card title="团队" bordered={false} style={{ width: "100%" }}>
+        <p>Card content</p>
+        <p>Card content</p>
+        <p>Card content</p>
+      </Card>
+    </>
+  );
 };
 
-export default Widget;
+export default ProfileWidget;

+ 40 - 40
dashboard/src/components/blog/TimeLine.tsx

@@ -1,47 +1,47 @@
 import { Timeline } from "antd";
 
 interface IAuthorTimeLine {
-	lable: string;
-	content: string;
-	type: string;
+  label: string;
+  content: string;
+  type: string;
 }
-const Widget = () => {
-	const data: IAuthorTimeLine[] = [
-		{
-			lable: "2015-09-1",
-			content: "Technical testing",
-			type: "translation",
-		},
-		{
-			lable: "2015-09-1",
-			content: "Technical testing",
-			type: "translation",
-		},
-		{
-			lable: "2015-09-1",
-			content: "Technical testing",
-			type: "translation",
-		},
-		{
-			lable: "2015-09-1",
-			content: "Technical testing",
-			type: "translation",
-		},
-	];
+const TimeLineWidget = () => {
+  const data: IAuthorTimeLine[] = [
+    {
+      label: "2015-09-1",
+      content: "Technical testing",
+      type: "translation",
+    },
+    {
+      label: "2015-09-1",
+      content: "Technical testing",
+      type: "translation",
+    },
+    {
+      label: "2015-09-1",
+      content: "Technical testing",
+      type: "translation",
+    },
+    {
+      label: "2015-09-1",
+      content: "Technical testing",
+      type: "translation",
+    },
+  ];
 
-	return (
-		<>
-			<Timeline mode={"left"} style={{ width: "100%" }}>
-				{data.map((item, id) => {
-					return (
-						<Timeline.Item key={id} label={item.lable}>
-							{item.content}
-						</Timeline.Item>
-					);
-				})}
-			</Timeline>
-		</>
-	);
+  return (
+    <>
+      <Timeline mode={"left"} style={{ width: "100%" }}>
+        {data.map((item, id) => {
+          return (
+            <Timeline.Item key={id} label={item.label}>
+              {item.content}
+            </Timeline.Item>
+          );
+        })}
+      </Timeline>
+    </>
+  );
 };
 
-export default Widget;
+export default TimeLineWidget;

+ 2 - 2
dashboard/src/components/blog/TopArticleCard.tsx

@@ -41,7 +41,7 @@ export interface ITopArticleCardData {
 interface IWidgetTopArticleCard {
   data: ITopArticleCardData;
 }
-const Widget = (prop: IWidgetTopArticleCard) => {
+const TopArticleCardWidget = (prop: IWidgetTopArticleCard) => {
   const items: IIconParamListData[] = [
     {
       label: "经藏",
@@ -74,4 +74,4 @@ const Widget = (prop: IWidgetTopArticleCard) => {
   );
 };
 
-export default Widget;
+export default TopArticleCardWidget;

+ 2 - 2
dashboard/src/components/blog/TopArticles.tsx

@@ -5,7 +5,7 @@ import TopArticleCard, { ITopArticleCardData } from "./TopArticleCard";
 interface IWidgetTopArticles {
   studio: string;
 }
-const Widget = (prop: IWidgetTopArticles) => {
+const TopArticlesWidget = (prop: IWidgetTopArticles) => {
   const data: ITopArticleCardData[] = [
     {
       title: "法句心品",
@@ -47,4 +47,4 @@ const Widget = (prop: IWidgetTopArticles) => {
   return <Row>{list}</Row>;
 };
 
-export default Widget;
+export default TopArticlesWidget;

+ 2 - 2
dashboard/src/components/channel/Channel.tsx

@@ -5,8 +5,8 @@ export interface IChannel {
   id: string;
   type?: TChannelType;
 }
-const Widget = ({ name, id }: IChannel) => {
+const ChannelWidget = ({ name, id }: IChannel) => {
   return <span>{name}</span>;
 };
 
-export default Widget;
+export default ChannelWidget;

+ 2 - 2
dashboard/src/components/channel/ChannelCreate.tsx

@@ -23,7 +23,7 @@ interface IWidget {
   studio?: string;
   onSuccess?: Function;
 }
-const Widget = ({ studio, onSuccess }: IWidget) => {
+const ChannelCreateWidget = ({ studio, onSuccess }: IWidget) => {
   const intl = useIntl();
   const formRef = useRef<ProFormInstance>();
 
@@ -71,4 +71,4 @@ const Widget = ({ studio, onSuccess }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ChannelCreateWidget;

+ 4 - 2
dashboard/src/components/channel/ChannelList.tsx

@@ -26,7 +26,9 @@ const defaultChannelFilterProps: ChannelFilterProps = {
   channelType: "translation",
 };
 
-const Widget = ({ filter = defaultChannelFilterProps }: IWidgetChannelList) => {
+const ChannelListWidget = ({
+  filter = defaultChannelFilterProps,
+}: IWidgetChannelList) => {
   const [tableData, setTableData] = useState<IChannelList[]>([]);
 
   useEffect(() => {
@@ -72,4 +74,4 @@ const Widget = ({ filter = defaultChannelFilterProps }: IWidgetChannelList) => {
   );
 };
 
-export default Widget;
+export default ChannelListWidget;

+ 7 - 2
dashboard/src/components/channel/ChannelListItem.tsx

@@ -11,7 +11,12 @@ interface IWidget {
   showLike?: boolean;
 }
 
-const Widget = ({ channel, studio, showProgress, showLike }: IWidget) => {
+const ChannelListItemWidget = ({
+  channel,
+  studio,
+  showProgress,
+  showLike,
+}: IWidget) => {
   const studioName = studio.nickName.slice(0, 2);
   return (
     <>
@@ -23,4 +28,4 @@ const Widget = ({ channel, studio, showProgress, showLike }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ChannelListItemWidget;

+ 37 - 11
dashboard/src/components/channel/ChannelPicker.tsx

@@ -1,38 +1,58 @@
-import { useState } from "react";
-import { Button, Modal } from "antd";
+import React, { useEffect, useState } from "react";
+import { Modal } from "antd";
 
 import ChannelPickerTable from "./ChannelPickerTable";
 import { IChannel } from "./Channel";
 import { ArticleType } from "../article/Article";
 
 interface IWidget {
+  trigger?: React.ReactNode;
   type?: ArticleType | "editable";
   articleId?: string;
   multiSelect?: boolean;
+  open?: boolean;
+  onClose?: Function;
+  onSelect?: Function;
 }
-const Widget = ({ type, articleId, multiSelect }: IWidget) => {
-  const [isModalOpen, setIsModalOpen] = useState(false);
+const ChannelPickerWidget = ({
+  trigger,
+  type,
+  articleId,
+  multiSelect,
+  open = false,
+  onClose,
+  onSelect,
+}: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(open);
 
+  useEffect(() => {
+    setIsModalOpen(open);
+  }, [open]);
   const showModal = () => {
     setIsModalOpen(true);
   };
 
   const handleOk = () => {
     setIsModalOpen(false);
+    if (typeof onClose !== "undefined") {
+      onClose();
+    }
   };
 
   const handleCancel = () => {
     setIsModalOpen(false);
+    if (typeof onClose !== "undefined") {
+      onClose();
+    }
   };
 
   return (
     <>
-      <Button type="primary" onClick={showModal}>
-        Select channel
-      </Button>
+      <span onClick={showModal}>{trigger}</span>
       <Modal
         width={"80%"}
         title="选择版本风格"
+        footer={false}
         open={isModalOpen}
         onOk={handleOk}
         onCancel={handleCancel}
@@ -40,10 +60,16 @@ const Widget = ({ type, articleId, multiSelect }: IWidget) => {
         <ChannelPickerTable
           type={type}
           articleId={articleId}
-          multiSelect={multiSelect}
-          onSelect={(e: IChannel) => {
-            console.log(e);
+          multiSelect={true}
+          onSelect={(channels: IChannel[]) => {
+            console.log(channels);
             handleCancel();
+            if (typeof onClose !== "undefined") {
+              onClose();
+            }
+            if (typeof onSelect !== "undefined") {
+              onSelect(channels);
+            }
           }}
         />
       </Modal>
@@ -51,4 +77,4 @@ const Widget = ({ type, articleId, multiSelect }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ChannelPickerWidget;

+ 260 - 264
dashboard/src/components/channel/ChannelPickerTable.tsx

@@ -49,7 +49,7 @@ interface IWidget {
   reload?: boolean;
   onSelect?: Function;
 }
-const Widget = ({
+const ChannelPickerTableWidget = ({
   type,
   articleId,
   multiSelect = true,
@@ -71,99 +71,99 @@ const Widget = ({
   }, [reload]);
 
   return (
-    <>
-      <ProList<IItem>
-        actionRef={ref}
-        rowSelection={
-          showCheckBox
-            ? {
-                // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
-                // 注释该行则默认不显示下拉选项
-                alwaysShowAlert: true,
-                selectedRowKeys: selectedRowKeys,
-                onChange: (selectedRowKeys: React.Key[]) => {
-                  setSelectedRowKeys(selectedRowKeys);
-                },
-                selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
-              }
-            : undefined
-        }
-        tableAlertRender={
-          showCheckBox
-            ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
-                console.log(selectedRowKeys);
-                return (
-                  <Space>
-                    {intl.formatMessage({ id: "buttons.selected" })}
-                    <Badge color="geekblue" count={selectedRowKeys.length} />
-                    <Link onClick={onCleanSelected}>
-                      {intl.formatMessage({ id: "buttons.empty" })}
-                    </Link>
-                  </Space>
-                );
-              }
-            : undefined
-        }
-        tableAlertOptionRender={
-          showCheckBox
-            ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
-                return (
-                  <Space>
-                    <Link
-                      onClick={() => {
-                        console.log("select", selectedRowKeys);
-                        if (typeof onSelect !== "undefined") {
-                          onSelect(
-                            selectedRows.map((item) => {
-                              return {
-                                id: item.uid,
-                                name: item.title,
-                              };
-                            })
-                          );
-                          setShowCheckBox(false);
-                          ref.current?.reload();
-                        }
-                      }}
-                    >
-                      {intl.formatMessage({
-                        id: "buttons.ok",
-                      })}
-                    </Link>
-                    <Link
-                      type="danger"
-                      onClick={() => {
+    <ProList<IItem>
+      actionRef={ref}
+      rowSelection={
+        showCheckBox
+          ? {
+              // 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
+              // 注释该行则默认不显示下拉选项
+              alwaysShowAlert: true,
+              selectedRowKeys: selectedRowKeys,
+              onChange: (selectedRowKeys: React.Key[]) => {
+                setSelectedRowKeys(selectedRowKeys);
+              },
+              selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
+            }
+          : undefined
+      }
+      tableAlertRender={
+        showCheckBox
+          ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
+              console.log(selectedRowKeys);
+              return (
+                <Space>
+                  {intl.formatMessage({ id: "buttons.selected" })}
+                  <Badge color="geekblue" count={selectedRowKeys.length} />
+                  <Link onClick={onCleanSelected}>
+                    {intl.formatMessage({ id: "buttons.empty" })}
+                  </Link>
+                </Space>
+              );
+            }
+          : undefined
+      }
+      tableAlertOptionRender={
+        showCheckBox
+          ? ({ selectedRowKeys, selectedRows, onCleanSelected }) => {
+              return (
+                <Space>
+                  <Link
+                    onClick={() => {
+                      console.log("select", selectedRowKeys);
+                      if (typeof onSelect !== "undefined") {
+                        onSelect(
+                          selectedRows.map((item) => {
+                            return {
+                              id: item.uid,
+                              name: item.title,
+                            };
+                          })
+                        );
                         setShowCheckBox(false);
-                      }}
-                    >
-                      {intl.formatMessage({
-                        id: "buttons.cancel",
-                      })}
-                    </Link>
-                  </Space>
-                );
-              }
-            : undefined
+                        ref.current?.reload();
+                      }
+                    }}
+                  >
+                    {intl.formatMessage({
+                      id: "buttons.ok",
+                    })}
+                  </Link>
+                  <Link
+                    type="danger"
+                    onClick={() => {
+                      setShowCheckBox(false);
+                    }}
+                  >
+                    {intl.formatMessage({
+                      id: "buttons.cancel",
+                    })}
+                  </Link>
+                </Space>
+              );
+            }
+          : undefined
+      }
+      request={async (params = {}, sorter, filter) => {
+        console.log(params, sorter, filter);
+        const sentElement = document.querySelectorAll(".pcd_sent");
+        let sentList: string[] = [];
+        for (let index = 0; index < sentElement.length; index++) {
+          const element = sentElement[index];
+          const id = element.id.split("_")[1];
+          sentList.push(id);
         }
-        request={async (params = {}, sorter, filter) => {
-          // TODO
-          console.log(params, sorter, filter);
-          const sentElement = document.querySelectorAll(".pcd_sent");
-          let sentList: string[] = [];
-          for (let index = 0; index < sentElement.length; index++) {
-            const element = sentElement[index];
-            const id = element.id.split("_")[1];
-            sentList.push(id);
+        console.log("sentList", sentList);
+        const res = await post<IProgressRequest, IApiResponseChannelList>(
+          `/v2/channel-progress`,
+          {
+            sentence: sentList,
           }
-          console.log("sentList", sentList);
-          const res = await post<IProgressRequest, IApiResponseChannelList>(
-            `/v2/channel-progress`,
-            {
-              sentence: sentList,
-            }
-          );
-          console.log("progress data", res.data.rows);
-          const items: IItem[] = res.data.rows.map((item, id) => {
+        );
+        console.log("progress data", res.data.rows);
+        const items: IItem[] = res.data.rows
+          .filter((value) => value.name.substring(0, 4) !== "_Sys")
+          .map((item, id) => {
             const date = new Date(item.created_at);
             let all: number = 0;
             let finished: number = 0;
@@ -187,193 +187,189 @@ const Widget = ({
               progress: progress,
             };
           });
-          //当前被选择的
-          const currChannel = items.filter((value) =>
-            selectedRowKeys.includes(value.uid)
-          );
-          let show = selectedRowKeys;
-          //有进度的
-          const progressing = items.filter(
-            (value) => value.progress > 0 && !show.includes(value.uid)
-          );
-          show = [...show, ...progressing.map((item) => item.uid)];
-          //我自己的
-          const myChannel = items.filter(
-            (value) => value.role === "owner" && !show.includes(value.uid)
-          );
-          show = [...show, ...myChannel.map((item) => item.uid)];
-          //其他的
-          const others = items.filter(
-            (value) => !show.includes(value.uid) && value.role !== "member"
-          );
-          console.log("user:", user);
-          setSelectedRowKeys(selectedRowKeys);
-          const channelData = [
-            ...currChannel,
-            ...progressing,
-            ...myChannel,
-            ...others,
-          ];
-          console.log("channel list ", channelData);
-          return {
-            total: res.data.count,
-            succcess: true,
-            data: channelData,
-          };
-        }}
-        rowKey="uid"
-        bordered
-        options={false}
-        search={{
-          filterType: "light",
-        }}
-        toolBarRender={() => [
+        //当前被选择的
+        const currChannel = items.filter((value) =>
+          selectedRowKeys.includes(value.uid)
+        );
+        let show = selectedRowKeys;
+        //有进度的
+        const progressing = items.filter(
+          (value) => value.progress > 0 && !show.includes(value.uid)
+        );
+        show = [...show, ...progressing.map((item) => item.uid)];
+        //我自己的
+        const myChannel = items.filter(
+          (value) => value.role === "owner" && !show.includes(value.uid)
+        );
+        show = [...show, ...myChannel.map((item) => item.uid)];
+        //其他的
+        const others = items.filter(
+          (value) => !show.includes(value.uid) && value.role !== "member"
+        );
+        console.log("user:", user);
+        setSelectedRowKeys(selectedRowKeys);
+        const channelData = [
+          ...currChannel,
+          ...progressing,
+          ...myChannel,
+          ...others,
+        ];
+        console.log("channel list ", channelData);
+        return {
+          total: res.data.count,
+          succcess: true,
+          data: channelData,
+        };
+      }}
+      rowKey="uid"
+      bordered
+      options={false}
+      search={{
+        filterType: "light",
+      }}
+      toolBarRender={() => [
+        <Button
+          onClick={() => {
+            ref.current?.reload();
+          }}
+        >
+          reload
+        </Button>,
+        multiSelect ? (
           <Button
             onClick={() => {
-              ref.current?.reload();
+              setShowCheckBox(true);
+              console.log("user:", user);
             }}
           >
-            reload
-          </Button>,
-          multiSelect ? (
-            <Button
-              onClick={() => {
-                setShowCheckBox(true);
-                console.log("user:", user);
-              }}
-            >
-              选择
-            </Button>
-          ) : undefined,
-        ]}
-        metas={{
-          title: {
-            render(dom, entity, index, action, schema) {
-              let pIcon = <></>;
-              switch (entity.publicity) {
-                case 10:
-                  pIcon = <LockIcon />;
-                  break;
-                case 30:
-                  pIcon = <GlobalOutlined />;
-                  break;
-              }
+            选择
+          </Button>
+        ) : undefined,
+      ]}
+      metas={{
+        title: {
+          render(dom, entity, index, action, schema) {
+            let pIcon = <></>;
+            switch (entity.publicity) {
+              case 10:
+                pIcon = <LockIcon />;
+                break;
+              case 30:
+                pIcon = <GlobalOutlined />;
+                break;
+            }
 
-              return (
-                <div
-                  key={index}
-                  style={{
-                    width: "100%",
-                    borderRadius: 5,
-                    padding: "0 5px",
-                    background:
-                      selectedKeys.includes(entity.uid) && !showCheckBox
-                        ? "linear-gradient(to right,#006112,rgba(0,0,0,0))"
-                        : undefined,
-                  }}
-                >
-                  <div
-                    key="info"
-                    style={{ overflowX: "clip", display: "flex" }}
+            return (
+              <div
+                key={index}
+                style={{
+                  width: "100%",
+                  borderRadius: 5,
+                  padding: "0 5px",
+                  background:
+                    selectedKeys.includes(entity.uid) && !showCheckBox
+                      ? "linear-gradient(to right,#006112,rgba(0,0,0,0))"
+                      : undefined,
+                }}
+              >
+                <div key="info" style={{ overflowX: "clip", display: "flex" }}>
+                  <Space>
+                    {pIcon}
+                    {entity.role !== "member" ? <EditOutlined /> : undefined}
+                  </Space>
+                  <Button
+                    type="link"
+                    onClick={() => {
+                      if (typeof onSelect !== "undefined") {
+                        const e: IChannel = {
+                          name: entity.title,
+                          id: entity.uid,
+                        };
+                        onSelect([e]);
+                      }
+                    }}
                   >
                     <Space>
-                      {pIcon}
-                      {entity.role !== "member" ? <EditOutlined /> : undefined}
+                      <StudioName data={entity.studio} showName={false} />
+                      {entity.title}
                     </Space>
-                    <Button
-                      type="link"
-                      onClick={() => {
-                        if (typeof onSelect !== "undefined") {
-                          const e: IChannel = {
-                            name: entity.title,
-                            id: entity.uid,
-                          };
-                          onSelect([e]);
-                        }
-                      }}
-                    >
-                      <Space>
-                        <StudioName data={entity.studio} showName={false} />
-                        {entity.title}
-                      </Space>
-                    </Button>
-                  </div>
-                  <div key="progress">
-                    <ProgressSvg data={entity.final} width={200} />
-                  </div>
+                  </Button>
                 </div>
-              );
-            },
-            search: false,
+                <div key="progress">
+                  <ProgressSvg data={entity.final} width={200} />
+                </div>
+              </div>
+            );
           },
-          actions: {
-            render: (dom, entity, index, action, schema) => {
-              return (
-                <Dropdown
-                  key={index}
-                  trigger={["click"]}
-                  menu={{
-                    items: [
-                      {
-                        key: "copy_to",
-                        label: (
-                          <CopyToModal
-                            trigger={intl.formatMessage({
-                              id: "buttons.copy.to",
-                            })}
-                            channel={{
-                              id: entity.uid,
-                              name: entity.title,
-                              type: entity.type,
-                            }}
-                          />
-                        ),
-                        icon: <CopyOutlined />,
-                      },
-                    ],
-                    onClick: (e) => {
-                      console.log("click ", e);
-                      switch (e.key) {
-                        case "copy_to":
-                          break;
-
-                        default:
-                          break;
-                      }
+          search: false,
+        },
+        actions: {
+          render: (dom, entity, index, action, schema) => {
+            return (
+              <Dropdown
+                key={index}
+                trigger={["click"]}
+                menu={{
+                  items: [
+                    {
+                      key: "copy_to",
+                      label: (
+                        <CopyToModal
+                          trigger={intl.formatMessage({
+                            id: "buttons.copy.to",
+                          })}
+                          channel={{
+                            id: entity.uid,
+                            name: entity.title,
+                            type: entity.type,
+                          }}
+                        />
+                      ),
+                      icon: <CopyOutlined />,
                     },
-                  }}
-                  placement="bottomRight"
-                >
-                  <Button
-                    type="link"
-                    size="small"
-                    icon={<MoreOutlined />}
-                  ></Button>
-                </Dropdown>
-              );
-            },
+                  ],
+                  onClick: (e) => {
+                    console.log("click ", e);
+                    switch (e.key) {
+                      case "copy_to":
+                        break;
+
+                      default:
+                        break;
+                    }
+                  },
+                }}
+                placement="bottomRight"
+              >
+                <Button
+                  type="link"
+                  size="small"
+                  icon={<MoreOutlined />}
+                ></Button>
+              </Dropdown>
+            );
           },
-          status: {
-            // 自己扩展的字段,主要用于筛选,不在列表中显示
-            title: "版本筛选",
-            valueType: "select",
-            valueEnum: {
-              all: { text: "全部", status: "Default" },
-              my: {
-                text: "我的",
-              },
-              closed: {
-                text: "协作",
-              },
-              processing: {
-                text: "社区公开",
-              },
+        },
+        status: {
+          // 自己扩展的字段,主要用于筛选,不在列表中显示
+          title: "版本筛选",
+          valueType: "select",
+          valueEnum: {
+            all: { text: "全部", status: "Default" },
+            my: {
+              text: "我的",
+            },
+            closed: {
+              text: "协作",
+            },
+            processing: {
+              text: "社区公开",
             },
           },
-        }}
-      />
-    </>
+        },
+      }}
+    />
   );
 };
 
-export default Widget;
+export default ChannelPickerTableWidget;

+ 10 - 4
dashboard/src/components/channel/ChannelSelect.tsx

@@ -15,14 +15,18 @@ interface IWidget {
   name?: string;
   tooltip?: string;
   label?: string;
+  parentChannelId?: string;
+  parentStudioId?: string;
   onSelect?: Function;
 }
-const Widget = ({
+const ChannelSelectWidget = ({
   width = "md",
   channelId,
   name = "channel",
   tooltip,
   label,
+  parentChannelId,
+  parentStudioId,
   onSelect,
 }: IWidget) => {
   return (
@@ -42,8 +46,10 @@ const Widget = ({
           for (const iterator of json.data.rows) {
             studio.set(iterator.studio.id, iterator.studio.nickName);
           }
-          let channels: IOption[] = [];
-
+          let channels: IOption[] = [{ value: "", label: "通用于此Studio" }];
+          if (typeof parentChannelId === "string") {
+            channels.push({ value: parentChannelId, label: "仅此版本" });
+          }
           studio.forEach((value, key, map) => {
             const node = {
               value: key,
@@ -69,4 +75,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default ChannelSelectWidget;

+ 2 - 2
dashboard/src/components/channel/ChannelSentDiff.tsx

@@ -35,7 +35,7 @@ interface IWidget {
   goPrev?: Function;
   onSubmit?: Function;
 }
-const Widget = ({
+const ChannelSentDiffWidget = ({
   srcChannel,
   destChannel,
   sentences,
@@ -206,4 +206,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default ChannelSentDiffWidget;

+ 2 - 2
dashboard/src/components/channel/ChannelTypeSelect.tsx

@@ -1,7 +1,7 @@
 import { useIntl } from "react-intl";
 import { ProFormSelect } from "@ant-design/pro-components";
 
-const Widget = () => {
+const ChannelTypeSelectWidget = () => {
   const intl = useIntl();
 
   const channelTypeOptions = [
@@ -46,4 +46,4 @@ const Widget = () => {
   );
 };
 
-export default Widget;
+export default ChannelTypeSelectWidget;

+ 2 - 2
dashboard/src/components/channel/ChapterInChannelList.tsx

@@ -29,7 +29,7 @@ interface IWidget {
   channelId?: string;
   onChange?: Function;
 }
-const Widget = ({ channelId, onChange }: IWidget) => {
+const ChpaterInChannelListWidget = ({ channelId, onChange }: IWidget) => {
   const intl = useIntl();
 
   return (
@@ -245,4 +245,4 @@ const Widget = ({ channelId, onChange }: IWidget) => {
   );
 };
 
-export default Widget;
+export default ChpaterInChannelListWidget;

+ 2 - 2
dashboard/src/components/channel/CopyToModal.tsx

@@ -8,7 +8,7 @@ interface IWidget {
   trigger: JSX.Element | string;
   channel?: IChannel;
 }
-const Widget = ({ trigger, channel }: IWidget) => {
+const CopyToModalWidget = ({ trigger, channel }: IWidget) => {
   const [isModalOpen, setIsModalOpen] = useState(false);
   const [initStep, setInitStep] = useState(0);
 
@@ -50,4 +50,4 @@ const Widget = ({ trigger, channel }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CopyToModalWidget;

+ 2 - 2
dashboard/src/components/channel/CopyToResult.tsx

@@ -4,7 +4,7 @@ interface IWidget {
   onClose?: Function;
   onInit?: Function;
 }
-const Widget = ({ onClose, onInit }: IWidget) => {
+const CopytoResultWidget = ({ onClose, onInit }: IWidget) => {
   return (
     <Result
       status="success"
@@ -37,4 +37,4 @@ const Widget = ({ onClose, onInit }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CopytoResultWidget;

+ 2 - 2
dashboard/src/components/channel/CopyToStep.tsx

@@ -16,7 +16,7 @@ interface IWidget {
   stepChange?: Function;
   onClose?: Function;
 }
-const Widget = ({
+const CopyToStepWidget = ({
   initStep = 0,
   channel,
   type,
@@ -121,4 +121,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default CopyToStepWidget;

+ 2 - 2
dashboard/src/components/channel/ProgressSvg.tsx

@@ -4,7 +4,7 @@ interface IWidget {
   data?: IFinal[];
   width?: number;
 }
-const Widget = ({ data, width = 300 }: IWidget) => {
+const ProgressSvgWidget = ({ data, width = 300 }: IWidget) => {
   //绘制句子进度
   if (typeof data === "undefined" || data.length === 0) {
     return <></>;
@@ -92,4 +92,4 @@ const Widget = ({ data, width = 300 }: IWidget) => {
   return <div style={{ width: width }}>{progress}</div>;
 };
 
-export default Widget;
+export default ProgressSvgWidget;

+ 2 - 2
dashboard/src/components/channel/StudioSelect.tsx

@@ -20,7 +20,7 @@ interface IWidget {
   studioName?: string;
   onSelect?: Function;
 }
-const Widget = ({ studioName, onSelect }: IWidget) => {
+const StudioSelectWidget = ({ studioName, onSelect }: IWidget) => {
   const [anthology, setAnthology] = useState<IOptions[]>([
     { value: "all", label: "全部" },
   ]);
@@ -53,4 +53,4 @@ const Widget = ({ studioName, onSelect }: IWidget) => {
   );
 };
 
-export default Widget;
+export default StudioSelectWidget;

+ 2 - 2
dashboard/src/components/comment/AnchorCard.tsx

@@ -10,7 +10,7 @@ interface IWidgetArticleCard {
   children?: React.ReactNode;
   onModeChange?: Function;
 }
-const Widget = ({ children, onModeChange }: IWidgetArticleCard) => {
+const AnchorCardWidget = ({ children, onModeChange }: IWidgetArticleCard) => {
   const intl = useIntl();
   const [mode, setMode] = useState<string>("read");
 
@@ -53,4 +53,4 @@ const Widget = ({ children, onModeChange }: IWidgetArticleCard) => {
   );
 };
 
-export default Widget;
+export default AnchorCardWidget;

+ 2 - 2
dashboard/src/components/comment/CommentAnchor.tsx

@@ -7,7 +7,7 @@ import AnchorCard from "./AnchorCard";
 interface IWidget {
   id?: string;
 }
-const Widget = ({ id }: IWidget) => {
+const CommentAnchorWidget = ({ id }: IWidget) => {
   const [content, setContent] = useState<string>();
   useEffect(() => {
     if (typeof id === "string") {
@@ -30,4 +30,4 @@ const Widget = ({ id }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentAnchorWidget;

+ 23 - 20
dashboard/src/components/comment/CommentBox.tsx

@@ -14,23 +14,17 @@ interface IWidget {
   resType?: TResType;
   onCommentCountChange?: Function;
 }
-const Widget = ({ trigger, resId, resType, onCommentCountChange }: IWidget) => {
+const CommentBoxWidget = ({
+  trigger,
+  resId,
+  resType,
+  onCommentCountChange,
+}: IWidget) => {
   const [open, setOpen] = useState(false);
   const [childrenDrawer, setChildrenDrawer] = useState(false);
   const [topicComment, setTopicComment] = useState<IComment>();
   const [answerCount, setAnswerCount] = useState<IAnswerCount>();
 
-  const showDrawer = () => {
-    setOpen(true);
-  };
-
-  const onClose = () => {
-    setOpen(false);
-    if (document.getElementsByTagName("body")[0].hasAttribute("style")) {
-      document.getElementsByTagName("body")[0].removeAttribute("style");
-    }
-  };
-
   const showChildrenDrawer = (
     e: React.MouseEvent<HTMLSpanElement, MouseEvent>,
     comment: IComment
@@ -39,17 +33,24 @@ const Widget = ({ trigger, resId, resType, onCommentCountChange }: IWidget) => {
     setTopicComment(comment);
   };
 
-  const onChildrenDrawerClose = () => {
-    setChildrenDrawer(false);
-  };
-
   return (
     <>
-      <span onClick={showDrawer}>{trigger}</span>
+      <span
+        onClick={() => {
+          setOpen(true);
+        }}
+      >
+        {trigger}
+      </span>
       <Drawer
         title="Discussion"
         width={520}
-        onClose={onClose}
+        onClose={() => {
+          setOpen(false);
+          if (document.getElementsByTagName("body")[0].hasAttribute("style")) {
+            document.getElementsByTagName("body")[0].removeAttribute("style");
+          }
+        }}
         open={open}
         maskClosable={false}
       >
@@ -67,7 +68,9 @@ const Widget = ({ trigger, resId, resType, onCommentCountChange }: IWidget) => {
         <Drawer
           title="回答"
           width={480}
-          onClose={onChildrenDrawerClose}
+          onClose={() => {
+            setChildrenDrawer(false);
+          }}
           open={childrenDrawer}
         >
           <CommentTopic
@@ -82,4 +85,4 @@ const Widget = ({ trigger, resId, resType, onCommentCountChange }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentBoxWidget;

+ 3 - 3
dashboard/src/components/comment/CommentCreate.tsx

@@ -18,7 +18,7 @@ import { currentUser as _currentUser } from "../../reducers/current-user";
 import { useRef } from "react";
 import MDEditor from "@uiw/react-md-editor";
 
-export type TContentType = "text" | "markdown" | "html";
+export type TContentType = "text" | "markdown" | "html" | "json";
 
 interface IWidget {
   resId?: string;
@@ -27,7 +27,7 @@ interface IWidget {
   onCreated?: Function;
   contentType?: TContentType;
 }
-const Widget = ({
+const CommentCreateWidget = ({
   resId,
   resType,
   contentType = "html",
@@ -179,4 +179,4 @@ const Widget = ({
   }
 };
 
-export default Widget;
+export default CommentCreateWidget;

+ 2 - 2
dashboard/src/components/comment/CommentEdit.tsx

@@ -12,7 +12,7 @@ interface IWidget {
   data: IComment;
   onCreated?: Function;
 }
-const Widget = ({ data, onCreated }: IWidget) => {
+const CommentEditWidget = ({ data, onCreated }: IWidget) => {
   const intl = useIntl();
   const formItemLayout = {
     labelCol: { span: 4 },
@@ -85,4 +85,4 @@ const Widget = ({ data, onCreated }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentEditWidget;

+ 2 - 2
dashboard/src/components/comment/CommentItem.tsx

@@ -22,7 +22,7 @@ interface IWidget {
   onSelect?: Function;
   onCreated?: Function;
 }
-const Widget = ({ data, onSelect, onCreated }: IWidget) => {
+const CommentItemWidget = ({ data, onSelect, onCreated }: IWidget) => {
   const [edit, setEdit] = useState(false);
   console.log(data);
   return (
@@ -53,4 +53,4 @@ const Widget = ({ data, onSelect, onCreated }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentItemWidget;

+ 2 - 2
dashboard/src/components/comment/CommentList.tsx

@@ -7,7 +7,7 @@ interface IWidget {
   data: IComment[];
   onSelect?: Function;
 }
-const Widget = ({ data, onSelect }: IWidget) => {
+const CommentListWidget = ({ data, onSelect }: IWidget) => {
   return (
     <div>
       <List
@@ -53,4 +53,4 @@ const Widget = ({ data, onSelect }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentListWidget;

+ 8 - 4
dashboard/src/components/comment/CommentListCard.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect } from "react";
 import { useIntl } from "react-intl";
-import { Card, message } from "antd";
+import { Card, message, Typography } from "antd";
 
 import { get } from "../../request";
 import { ICommentListResponse } from "../api/Comment";
@@ -18,7 +18,7 @@ interface IWidget {
   onSelect?: Function;
   onItemCountChange?: Function;
 }
-const Widget = ({
+const CommentListCardWidget = ({
   resId,
   resType,
   topicId,
@@ -80,7 +80,11 @@ const Widget = ({
   }, [intl, resId, topicId]);
 
   if (typeof resId === "undefined" && typeof topicId === "undefined") {
-    return <div>该资源尚未创建,不能发表讨论。</div>;
+    return (
+      <Typography.Paragraph>
+        该资源尚未创建,不能发表讨论。
+      </Typography.Paragraph>
+    );
   }
 
   return (
@@ -120,4 +124,4 @@ const Widget = ({
   );
 };
 
-export default Widget;
+export default CommentListCardWidget;

+ 2 - 2
dashboard/src/components/comment/CommentListItem.tsx

@@ -1,5 +1,5 @@
-const Widget = () => {
+const commentListItemWidget = () => {
   return <div>change password</div>;
 };
 
-export default Widget;
+export default commentListItemWidget;

+ 2 - 2
dashboard/src/components/comment/CommentShow.tsx

@@ -10,7 +10,7 @@ interface IWidget {
   onEdit?: Function;
   onSelect?: Function;
 }
-const Widget = ({ data, onEdit, onSelect }: IWidget) => {
+const CommentShowWidget = ({ data, onEdit, onSelect }: IWidget) => {
   const onClick: MenuProps["onClick"] = (e) => {
     console.log("click ", e);
     switch (e.key) {
@@ -87,4 +87,4 @@ const Widget = ({ data, onEdit, onSelect }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentShowWidget;

+ 2 - 2
dashboard/src/components/comment/CommentTopic.tsx

@@ -7,7 +7,7 @@ interface IWidget {
   topicId?: string;
   onItemCountChange?: Function;
 }
-const Widget = ({ topicId, onItemCountChange }: IWidget) => {
+const CommentTopicWidget = ({ topicId, onItemCountChange }: IWidget) => {
   return (
     <div>
       <CommentTopicInfo topicId={topicId} />
@@ -25,4 +25,4 @@ const Widget = ({ topicId, onItemCountChange }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentTopicWidget;

+ 5 - 2
dashboard/src/components/comment/CommentTopicChildren.tsx

@@ -11,7 +11,10 @@ interface IWidget {
   topicId?: string;
   onItemCountChange?: Function;
 }
-const Widget = ({ topicId, onItemCountChange }: IWidget) => {
+const CommentTopicChildrenWidget = ({
+  topicId,
+  onItemCountChange,
+}: IWidget) => {
   const intl = useIntl();
   const [data, setData] = useState<IComment[]>();
   useEffect(() => {
@@ -84,4 +87,4 @@ const Widget = ({ topicId, onItemCountChange }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentTopicChildrenWidget;

+ 2 - 2
dashboard/src/components/comment/CommentTopicInfo.tsx

@@ -11,7 +11,7 @@ const { Title, Text } = Typography;
 interface IWidget {
   topicId?: string;
 }
-const Widget = ({ topicId }: IWidget) => {
+const CommentTopicInfoWidget = ({ topicId }: IWidget) => {
   const [data, setData] = useState<IComment>();
   useEffect(() => {
     if (typeof topicId === "undefined") {
@@ -65,4 +65,4 @@ const Widget = ({ topicId }: IWidget) => {
   );
 };
 
-export default Widget;
+export default CommentTopicInfoWidget;

Some files were not shown because too many files changed in this diff