Przeglądaj źródła

Merge pull request #1188 from visuddhinanda/agile

编辑器添加 recent 按钮
visuddhinanda 2 lat temu
rodzic
commit
61275403e0

+ 249 - 0
dashboard/src/components/recent/RecentList.tsx

@@ -0,0 +1,249 @@
+import { useIntl } from "react-intl";
+import { useEffect, useRef } from "react";
+import { Dropdown, Space, Typography } from "antd";
+import { SearchOutlined } from "@ant-design/icons";
+import { ActionType, ProTable } from "@ant-design/pro-components";
+
+import { get } from "../../request";
+import { ArticleType } from "../../components/article/Article";
+import { useAppSelector } from "../../hooks";
+import { currentUser as _currentUser } from "../../reducers/current-user";
+
+import {
+  ArticleOutlinedIcon,
+  ChapterOutlinedIcon,
+  ParagraphOutlinedIcon,
+} from "../../assets/icon";
+
+export interface IRecentRequest {
+  type: ArticleType;
+  article_id: string;
+  param?: string;
+}
+interface IParam {
+  book?: string;
+  para?: string;
+  channel?: string;
+  mode?: string;
+}
+interface IRecentData {
+  id: string;
+  title: string;
+  type: ArticleType;
+  article_id: string;
+  param: string | null;
+  updated_at: string;
+}
+
+export interface IRecentResponse {
+  ok: boolean;
+  message: string;
+  data: IRecentData;
+}
+interface IRecentListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IRecentData[];
+    count: number;
+  };
+}
+
+export interface IRecent {
+  id: string;
+  title: string;
+  type: ArticleType;
+  articleId: string;
+  updatedAt: string;
+  param?: IParam;
+}
+
+interface IWidget {
+  onSelect?: Function;
+}
+const RecentWidget = ({ onSelect }: IWidget) => {
+  const intl = useIntl();
+  const user = useAppSelector(_currentUser);
+  const ref = useRef<ActionType>();
+
+  useEffect(() => {
+    ref.current?.reload();
+  }, [user]);
+  return (
+    <>
+      <ProTable<IRecent>
+        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) => {
+              let icon = <></>;
+              switch (row.type) {
+                case "article":
+                  icon = <ArticleOutlinedIcon />;
+                  break;
+                case "chapter":
+                  icon = <ChapterOutlinedIcon />;
+                  break;
+                case "para":
+                  icon = <ParagraphOutlinedIcon />;
+                  break;
+                default:
+                  break;
+              }
+              return (
+                <Space>
+                  {icon}
+                  <Typography.Link
+                    key={index}
+                    onClick={(event) => {
+                      if (typeof onSelect !== "undefined") {
+                        onSelect(event, row);
+                      }
+                    }}
+                  >
+                    {row.title}
+                  </Typography.Link>
+                </Space>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.type.label",
+            }),
+            dataIndex: "type",
+            key: "type",
+            width: 100,
+            search: false,
+            filters: true,
+            onFilter: true,
+            valueEnum: {
+              all: { text: "全部", status: "Default" },
+              chapter: { text: "章节", status: "Success" },
+              article: { text: "文章", status: "Success" },
+              para: { text: "段落", status: "Success" },
+              sent: { text: "句子", status: "Success" },
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.updated-at.label",
+            }),
+            key: "updated-at",
+            width: 100,
+            search: false,
+            dataIndex: "updatedAt",
+            valueType: "date",
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => [
+              <Dropdown.Button
+                type="link"
+                key={index}
+                trigger={["click", "contextMenu"]}
+                menu={{
+                  items: [
+                    {
+                      key: "open",
+                      label: "在藏经阁中打开",
+                      icon: <SearchOutlined />,
+                    },
+                    {
+                      key: "share",
+                      label: "分享",
+                      icon: <SearchOutlined />,
+                    },
+                    {
+                      key: "delete",
+                      label: "删除",
+                      icon: <SearchOutlined />,
+                    },
+                  ],
+                  onClick: (e) => {
+                    switch (e.key) {
+                      case "share":
+                        break;
+                      case "delete":
+                        break;
+                      default:
+                        break;
+                    }
+                  },
+                }}
+              >
+                {intl.formatMessage({ id: "buttons.edit" })}
+              </Dropdown.Button>,
+            ],
+          },
+        ]}
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          if (typeof user === "undefined") {
+            return {
+              total: 0,
+              succcess: false,
+              data: [],
+            };
+          }
+          let url = `/v2/recent?view=user&id=${user?.id}`;
+          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 : "";
+          console.log("url", url);
+          const res = await get<IRecentListResponse>(url);
+          console.log("article list", res);
+          const items: IRecent[] = res.data.rows.map((item, id) => {
+            return {
+              sn: id + 1,
+              id: item.id,
+              title: item.title,
+              type: item.type,
+              articleId: item.article_id,
+              param: item.param ? JSON.parse(item.param) : undefined,
+              updatedAt: item.updated_at,
+            };
+          });
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: items,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+      />
+    </>
+  );
+};
+
+export default RecentWidget;

+ 73 - 0
dashboard/src/components/recent/RecentModal.tsx

@@ -0,0 +1,73 @@
+import React, { useEffect, useState } from "react";
+import { Modal } from "antd";
+import RecentList, { IRecent } from "./RecentList";
+
+interface IWidget {
+  trigger?: React.ReactNode;
+  open?: boolean;
+  onSelect?: Function;
+  onOpen?: Function;
+}
+const RecentModalWidget = ({
+  trigger,
+  open = false,
+  onSelect,
+  onOpen,
+}: IWidget) => {
+  const [isModalOpen, setIsModalOpen] = useState(open);
+
+  useEffect(() => {
+    setIsModalOpen(open);
+    if (typeof onOpen !== "undefined") {
+      onOpen(open);
+    }
+  }, [open]);
+  const showModal = () => {
+    setIsModalOpen(true);
+    if (typeof onOpen !== "undefined") {
+      onOpen(true);
+    }
+  };
+
+  const handleOk = () => {
+    setIsModalOpen(false);
+    if (typeof onOpen !== "undefined") {
+      onOpen(false);
+    }
+  };
+
+  const handleCancel = () => {
+    setIsModalOpen(false);
+    if (typeof onOpen !== "undefined") {
+      onOpen(false);
+    }
+  };
+
+  return (
+    <>
+      <span onClick={showModal}>{trigger}</span>
+      <Modal
+        width={"80%"}
+        title="选择版本风格"
+        footer={false}
+        open={isModalOpen}
+        onOk={handleOk}
+        onCancel={handleCancel}
+        destroyOnClose
+      >
+        <RecentList
+          onSelect={(
+            event: React.MouseEvent<HTMLElement, MouseEvent>,
+            param: IRecent
+          ) => {
+            if (typeof onSelect !== "undefined") {
+              onSelect(event, param);
+            }
+          }}
+        />
+      </Modal>
+    </>
+  );
+};
+
+export default RecentModalWidget;

+ 3 - 3
dashboard/src/components/template/Nissaya.tsx

@@ -9,16 +9,16 @@ interface IWidgetNissayaCtl {
 }
 const NissayaCtl = ({ pali, meaning, children }: IWidgetNissayaCtl) => {
   return (
-    <Space style={{ marginRight: 10 }}>
+    <span style={{ marginRight: 10 }}>
       <PaliText
         lookup={true}
         text={pali}
         code="my"
         termToLocal={false}
         style={{ fontWeight: 700 }}
-      />
+      />{" "}
       <NissayaMeaning text={meaning} />
-    </Space>
+    </span>
   );
 };
 

+ 3 - 3
dashboard/src/components/template/Nissaya/NissayaMeaning.tsx

@@ -52,18 +52,18 @@ const NissayaMeaningWidget = ({ text, code = "my" }: IWidget) => {
     return <></>;
   }
   return (
-    <Space>
+    <span>
       {words?.map((item, id) => {
         return (
           <span key={id}>
             <Lookup search={item.base}>{item.base}</Lookup>
             {item.ending?.map((item, id) => {
               return <NissayaCardPop text={item} key={id} trigger={item} />;
-            })}
+            })}{" "}
           </span>
         );
       })}
-    </Space>
+    </span>
   );
 };
 

+ 36 - 1
dashboard/src/pages/library/article/show.tsx

@@ -3,6 +3,8 @@ import { Header } from "antd/lib/layout/layout";
 import { Key } from "antd/lib/table/interface";
 import { useEffect, useState } from "react";
 import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+import { FieldTimeOutlined } from "@ant-design/icons";
+
 import { ColumnOutlinedIcon } from "../../../assets/icon";
 import { IArticleDataResponse } from "../../../components/api/Article";
 import { IApiResponseDictList } from "../../../components/api/Dict";
@@ -27,11 +29,13 @@ import ToolButtonToc from "../../../components/article/ToolButtonToc";
 import Avatar from "../../../components/auth/Avatar";
 import { IChannel } from "../../../components/channel/Channel";
 import NetStatus from "../../../components/general/NetStatus";
+import RecentModal from "../../../components/recent/RecentModal";
 import { useAppSelector } from "../../../hooks";
 import { add } from "../../../reducers/inline-dict";
 import { paraParam } from "../../../reducers/para-change";
 import { get } from "../../../request";
 import store from "../../../store";
+import { IRecent } from "../../../components/recent/RecentList";
 
 /**
  * type:
@@ -51,6 +55,8 @@ const Widget = () => {
   const [rightPanel, setRightPanel] = useState<TPanelName>("close");
   const [searchParams, setSearchParams] = useSearchParams();
   const [anchorNavOpen, setAnchorNavOpen] = useState(false);
+  const [recentModalOpen, setRecentModalOpen] = useState(false);
+
   const paraChange = useAppSelector(paraParam);
 
   useEffect(() => {
@@ -180,6 +186,35 @@ const Widget = () => {
           >
             <div>
               <Space direction="vertical">
+                <RecentModal
+                  trigger={<Button icon={<FieldTimeOutlined />} />}
+                  open={recentModalOpen}
+                  onOpen={(isOpen: boolean) => setRecentModalOpen(isOpen)}
+                  onSelect={(
+                    event: React.MouseEvent<HTMLElement, MouseEvent>,
+                    param: IRecent
+                  ) => {
+                    setRecentModalOpen(false);
+                    let url = `/article/${param.type}/${param.articleId}?mode=`;
+                    url += param.param?.mode ? param.param?.mode : "read";
+                    url += param.param?.channel
+                      ? `&channel=${param.param?.channel}`
+                      : "";
+                    url += param.param?.book
+                      ? `&book=${param.param?.book}`
+                      : "";
+                    url += param.param?.para ? `&par=${param.param?.para}` : "";
+                    if (event.ctrlKey || event.metaKey) {
+                      const fullUrl =
+                        process.env.REACT_APP_WEB_HOST +
+                        process.env.PUBLIC_URL +
+                        url;
+                      window.open(fullUrl, "_blank");
+                    } else {
+                      navigate(url);
+                    }
+                  }}
+                />
                 <ToolButtonToc
                   type={type as ArticleType}
                   articleId={id}
@@ -209,7 +244,7 @@ const Widget = () => {
         >
           <div
             key="Article"
-            style={{ marginLeft: "auto", marginRight: "auto", width: 1200 }}
+            style={{ marginLeft: "auto", marginRight: "auto", width: 1100 }}
           >
             <Article
               active={true}

+ 28 - 210
dashboard/src/pages/studio/recent/list.tsx

@@ -1,19 +1,11 @@
-import { useIntl } from "react-intl";
 import { useEffect, useRef, useState } from "react";
-import { Dropdown, Space, Typography } from "antd";
-import { SearchOutlined } from "@ant-design/icons";
-import { ActionType, ProTable } from "@ant-design/pro-components";
+import { ActionType } from "@ant-design/pro-components";
 
-import { get } from "../../../request";
 import { ArticleMode, ArticleType } from "../../../components/article/Article";
 import { useAppSelector } from "../../../hooks";
 import { currentUser as _currentUser } from "../../../reducers/current-user";
 import ArticleDrawer from "../../../components/article/ArticleDrawer";
-import {
-  ArticleOutlinedIcon,
-  ChapterOutlinedIcon,
-  ParagraphOutlinedIcon,
-} from "../../../assets/icon";
+import RecentList from "../../../components/recent/RecentList";
 
 export interface IRecentRequest {
   type: ArticleType;
@@ -40,14 +32,6 @@ export interface IRecentResponse {
   message: string;
   data: IRecentData;
 }
-interface IRecentListResponse {
-  ok: boolean;
-  message: string;
-  data: {
-    rows: IRecentData[];
-    count: number;
-  };
-}
 
 interface IRecent {
   id: string;
@@ -66,7 +50,6 @@ interface IArticleParam {
   para?: string;
 }
 const Widget = () => {
-  const intl = useIntl();
   const user = useAppSelector(_currentUser);
   const ref = useRef<ActionType>();
   const [articleOpen, setArticleOpen] = useState(false);
@@ -77,198 +60,33 @@ const Widget = () => {
   }, [user]);
   return (
     <>
-      <ProTable<IRecent>
-        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) => {
-              let icon = <></>;
-              switch (row.type) {
-                case "article":
-                  icon = <ArticleOutlinedIcon />;
-                  break;
-                case "chapter":
-                  icon = <ChapterOutlinedIcon />;
-                  break;
-                case "para":
-                  icon = <ParagraphOutlinedIcon />;
-                  break;
-                default:
-                  break;
-              }
-              return (
-                <Space>
-                  {icon}
-                  <Typography.Link
-                    key={index}
-                    onClick={(event) => {
-                      if (event.ctrlKey || event.metaKey) {
-                        let url = `/article/${row.type}/${row.articleId}?mode=`;
-                        url += row.param?.mode ? row.param?.mode : "read";
-                        url += row.param?.channel
-                          ? `&channel=${row.param?.channel}`
-                          : "";
-                        url += row.param?.book
-                          ? `&book=${row.param?.book}`
-                          : "";
-                        url += row.param?.para ? `&par=${row.param?.para}` : "";
-                        const fullUrl =
-                          process.env.REACT_APP_WEB_HOST +
-                          process.env.PUBLIC_URL +
-                          url;
-                        window.open(fullUrl, "_blank");
-                      } else {
-                        setParam({
-                          type: row.type,
-                          articleId: row.articleId,
-                          mode: row.param?.mode as ArticleMode,
-                          channelId: row.param?.channel,
-                          book: row.param?.book,
-                          para: row.param?.para,
-                        });
-                        setArticleOpen(true);
-                      }
-                    }}
-                  >
-                    {row.title}
-                  </Typography.Link>
-                </Space>
-              );
-            },
-          },
-          {
-            title: intl.formatMessage({
-              id: "forms.fields.type.label",
-            }),
-            dataIndex: "type",
-            key: "type",
-            width: 100,
-            search: false,
-            filters: true,
-            onFilter: true,
-            valueEnum: {
-              all: { text: "全部", status: "Default" },
-              chapter: { text: "章节", status: "Success" },
-              article: { text: "文章", status: "Success" },
-              para: { text: "段落", status: "Success" },
-              sent: { text: "句子", status: "Success" },
-            },
-          },
-          {
-            title: intl.formatMessage({
-              id: "forms.fields.updated-at.label",
-            }),
-            key: "updated-at",
-            width: 100,
-            search: false,
-            dataIndex: "updatedAt",
-            valueType: "date",
-          },
-          {
-            title: intl.formatMessage({ id: "buttons.option" }),
-            key: "option",
-            width: 120,
-            valueType: "option",
-            render: (text, row, index, action) => [
-              <Dropdown.Button
-                type="link"
-                key={index}
-                trigger={["click", "contextMenu"]}
-                menu={{
-                  items: [
-                    {
-                      key: "open",
-                      label: "在藏经阁中打开",
-                      icon: <SearchOutlined />,
-                    },
-                    {
-                      key: "share",
-                      label: "分享",
-                      icon: <SearchOutlined />,
-                    },
-                    {
-                      key: "delete",
-                      label: "删除",
-                      icon: <SearchOutlined />,
-                    },
-                  ],
-                  onClick: (e) => {
-                    switch (e.key) {
-                      case "share":
-                        break;
-                      case "delete":
-                        break;
-                      default:
-                        break;
-                    }
-                  },
-                }}
-              >
-                {intl.formatMessage({ id: "buttons.edit" })}
-              </Dropdown.Button>,
-            ],
-          },
-        ]}
-        request={async (params = {}, sorter, filter) => {
-          console.log(params, sorter, filter);
-          if (typeof user === "undefined") {
-            return {
-              total: 0,
-              succcess: false,
-              data: [],
-            };
+      <RecentList
+        onSelect={(
+          event: React.MouseEvent<HTMLElement, MouseEvent>,
+          param: IRecent
+        ) => {
+          if (event.ctrlKey || event.metaKey) {
+            let url = `/article/${param.type}/${param.articleId}?mode=`;
+            url += param.param?.mode ? param.param?.mode : "read";
+            url += param.param?.channel
+              ? `&channel=${param.param?.channel}`
+              : "";
+            url += param.param?.book ? `&book=${param.param?.book}` : "";
+            url += param.param?.para ? `&par=${param.param?.para}` : "";
+            const fullUrl =
+              process.env.REACT_APP_WEB_HOST + process.env.PUBLIC_URL + url;
+            window.open(fullUrl, "_blank");
+          } else {
+            setParam({
+              type: param.type,
+              articleId: param.articleId,
+              mode: param.param?.mode as ArticleMode,
+              channelId: param.param?.channel,
+              book: param.param?.book,
+              para: param.param?.para,
+            });
+            setArticleOpen(true);
           }
-          let url = `/v2/recent?view=user&id=${user?.id}`;
-          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 : "";
-          console.log("url", url);
-          const res = await get<IRecentListResponse>(url);
-          console.log("article list", res);
-          const items: IRecent[] = res.data.rows.map((item, id) => {
-            return {
-              sn: id + 1,
-              id: item.id,
-              title: item.title,
-              type: item.type,
-              articleId: item.article_id,
-              param: item.param ? JSON.parse(item.param) : undefined,
-              updatedAt: item.updated_at,
-            };
-          });
-          return {
-            total: res.data.count,
-            succcess: true,
-            data: items,
-          };
-        }}
-        rowKey="id"
-        bordered
-        pagination={{
-          showQuickJumper: true,
-          showSizeChanger: true,
-        }}
-        search={false}
-        options={{
-          search: true,
         }}
       />
       <ArticleDrawer