Sfoglia il codice sorgente

Merge pull request #1851 from visuddhinanda/agile

支持 notification
visuddhinanda 2 anni fa
parent
commit
5667a18243

+ 6 - 1
dashboard/src/Router.tsx

@@ -75,6 +75,9 @@ import LibrarySearchKey from "./pages/library/search/search";
 import LibraryDownload from "./pages/library/download";
 import LibraryDownloadPage from "./pages/library/download/Download";
 
+import LibraryNotifications from "./pages/library/notifications";
+import LibraryNotificationsList from "./pages/library/notifications/list";
+
 import Studio from "./pages/studio";
 import StudioHome from "./pages/studio/home";
 
@@ -186,7 +189,9 @@ const Widget = () => {
         <Route path="community" element={<LibraryCommunity />}>
           <Route path="list" element={<LibraryCommunityList />} />
         </Route>
-
+        <Route path="notifications" element={<LibraryNotifications />}>
+          <Route path="list" element={<LibraryNotificationsList />} />
+        </Route>
         <Route path="palicanon" element={<LibraryPalicanon />}>
           <Route path="list" element={<LibraryPalicanonByPath />} />
           <Route path="list/:root" element={<LibraryPalicanonByPath />} />

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

@@ -574,6 +574,22 @@ const DocOutLined = () => (
   </svg>
 );
 
+const NotificationOutlined = () => (
+  <svg
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="5268"
+    width="1em"
+    height="1em"
+  >
+    <path
+      d="M861.575529 783.058824a30.117647 30.117647 0 0 1-30.117647-30.117648V378.096941C831.457882 202.541176 689.152 60.235294 513.626353 60.235294 338.070588 60.235294 195.764706 202.541176 195.764706 378.096941V752.941176a30.117647 30.117647 0 0 1-30.117647 30.117648H105.411765v60.235294h813.17647v-60.235294h-57.012706zM918.588235 722.823529a60.235294 60.235294 0 0 1 60.235294 60.235295v60.235294a60.235294 60.235294 0 0 1-60.235294 60.235294H105.411765a60.235294 60.235294 0 0 1-60.235294-60.235294v-60.235294a60.235294 60.235294 0 0 1 60.235294-60.235295h30.117647V378.096941C135.529412 169.261176 304.790588 0 513.626353 0c208.805647 0 378.066824 169.261176 378.066823 378.096941V722.823529H918.588235z m-481.882353 210.82353h180.705883a90.352941 90.352941 0 0 1-180.705883 0z"
+      fill="currentColor"
+      p-id="5269"
+    ></path>
+  </svg>
+);
 export const DictIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={DictSvg} {...props} />
 );
@@ -683,3 +699,7 @@ export const HtmlIcon = (props: Partial<CustomIconComponentProps>) => (
 export const DocIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={DocOutLined} {...props} />
 );
+
+export const NotificationIcon = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={NotificationOutlined} {...props} />
+);

+ 39 - 0
dashboard/src/components/api/notification.ts

@@ -0,0 +1,39 @@
+import { IUser } from "../auth/User";
+
+export interface INotificationPutResponse {
+  ok: boolean;
+  data: {
+    unread: number;
+  };
+  message: string;
+}
+
+export interface INotificationListResponse {
+  ok: boolean;
+  data: INotificationListData;
+  message: string;
+}
+
+export interface INotificationListData {
+  rows: INotificationData[];
+  count: number;
+  unread: number;
+}
+
+interface INotificationData {
+  id: string;
+  from: IUser;
+  to: IUser;
+  url?: string;
+  content: string;
+  content_type: string;
+  res_type: string;
+  res_id: string;
+  status: string;
+  deleted_at?: string;
+  created_at: string;
+  updated_at: string;
+}
+export interface INotificationRequest {
+  status: string;
+}

+ 3 - 3
dashboard/src/components/corpus/SentHistory.tsx

@@ -53,10 +53,10 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
         if (typeof params.keyword !== "undefined") {
           url += "&search=" + (params.keyword ? params.keyword : "");
         }
+        console.info("url", url);
         const res = await get<ISentHistoryListResponse>(url);
         if (res.ok) {
-          console.log(res.data);
-
+          console.debug(res.data);
           const items: ISentHistory[] = res.data.rows.map((item, id) => {
             return {
               content: item.content,
@@ -64,7 +64,7 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
               createdAt: item.created_at,
             };
           });
-          console.log(items);
+          console.debug(items);
           return {
             total: res.data.count,
             succcess: true,

+ 3 - 1
dashboard/src/components/general/Marked.tsx

@@ -5,12 +5,14 @@ const { Text } = Typography;
 
 interface IWidget {
   text?: string;
+  style?: React.CSSProperties;
   className?: string;
 }
-const MarkedWidget = ({ text, className }: IWidget) => {
+const MarkedWidget = ({ text, style, className }: IWidget) => {
   return (
     <Text className={className}>
       <div
+        style={style}
         className={className}
         dangerouslySetInnerHTML={{
           __html: marked.parse(text ? text : ""),

+ 2 - 0
dashboard/src/components/library/HeadBar.tsx

@@ -11,6 +11,7 @@ import ToStudio from "../auth/ToStudio";
 import ThemeSelect from "../general/ThemeSelect";
 import SearchButton from "../general/SearchButton";
 import { dashboardBasePath } from "../../utils";
+import NotificationIcon from "../notification/NotificationIcon";
 
 const { Header } = Layout;
 
@@ -176,6 +177,7 @@ const HeadBarWidget = ({ selectedKeys = "" }: IWidgetHeadBar) => {
           <SearchButton />
           <ToStudio />
           <SignInAvatar />
+          <NotificationIcon />
           <UiLangSelect />
           <ThemeSelect />
         </Space>

+ 68 - 0
dashboard/src/components/notification/NotificationIcon.tsx

@@ -0,0 +1,68 @@
+import { useEffect, useState } from "react";
+import { NotificationIcon } from "../../assets/icon";
+import { Badge, Popover } from "antd";
+import { get } from "../../request";
+import { INotificationListResponse } from "../api/notification";
+import NotificationList from "./NotificationList";
+
+const NotificationIconWidget = () => {
+  const [count, setCount] = useState<number>();
+  useEffect(() => {
+    let timer = setInterval(() => {
+      const url = `/v2/notification?view=to&status=unread&limit=1`;
+      get<INotificationListResponse>(url).then((json) => {
+        if (json.ok) {
+          setCount(json.data.count);
+          if (json.data.count > 0) {
+            const newMessageTime = json.data.rows[0].created_at;
+            const lastTime = localStorage.getItem("notification/new");
+            if (lastTime === null || lastTime !== newMessageTime) {
+              localStorage.setItem("notification/new", newMessageTime);
+              if (window.Notification && Notification.permission !== "denied") {
+                Notification.requestPermission(function (status) {
+                  const notification = new Notification("通知标题", {
+                    body: json.data.rows[0].content,
+                    icon:
+                      process.env.REACT_APP_API_HOST +
+                      "/assets/images/wikipali_logo.png",
+                  });
+                  notification.onclick = (event) => {
+                    event.preventDefault(); // 阻止浏览器聚焦于 Notification 的标签页
+                    window.open(json.data.rows[0].url, "_blank");
+                  };
+                });
+              }
+            }
+          }
+        }
+      });
+    }, 1000 * 60);
+    return () => {
+      clearInterval(timer);
+    };
+  }, []);
+
+  return (
+    <>
+      <Popover
+        placement="bottomLeft"
+        arrowPointAtCenter
+        destroyTooltipOnHide
+        content={
+          <div style={{ width: 600 }}>
+            <NotificationList onChange={(unread: number) => setCount(unread)} />
+          </div>
+        }
+        trigger="click"
+      >
+        <Badge count={count} size="small">
+          <span style={{ color: "white", cursor: "pointer" }}>
+            <NotificationIcon />
+          </span>
+        </Badge>{" "}
+      </Popover>
+    </>
+  );
+};
+
+export default NotificationIconWidget;

+ 226 - 0
dashboard/src/components/notification/NotificationList.tsx

@@ -0,0 +1,226 @@
+import { ActionType, ProList } from "@ant-design/pro-components";
+import { Avatar, Button, Space, Switch, Tag, Typography } from "antd";
+import { ReloadOutlined } from "@ant-design/icons";
+import { get, put } from "../../request";
+import {
+  INotificationListResponse,
+  INotificationPutResponse,
+  INotificationRequest,
+} from "../api/notification";
+import { IUser } from "../auth/User";
+import TimeShow from "../general/TimeShow";
+import { useRef, useState } from "react";
+import Marked from "../general/Marked";
+
+const { Text } = Typography;
+
+interface INotification {
+  id: string;
+  from: IUser;
+  to: IUser;
+  url?: string;
+  content?: string;
+  content_type: string;
+  res_type: string;
+  res_id: string;
+  status: string;
+  deleted_at?: string;
+  created_at: string;
+  updated_at: string;
+}
+interface IWidget {
+  onChange?: Function;
+}
+
+const NotificationListWidget = ({ onChange }: IWidget) => {
+  const ref = useRef<ActionType>();
+  const [activeKey, setActiveKey] = useState<React.Key | undefined>("inbox");
+
+  const putStatus = (id: string, status: string) => {
+    const url = `/v2/notification/${id}`;
+    put<INotificationRequest, INotificationPutResponse>(url, {
+      status: status,
+    }).then((json) => {
+      if (json.ok) {
+        ref.current?.reload();
+        if (typeof onChange !== "undefined") {
+          onChange(json.data.unread);
+        }
+      }
+    });
+  };
+
+  return (
+    <ProList<INotification>
+      rowKey="id"
+      actionRef={ref}
+      onItem={(record: INotification, index: number) => {
+        return {
+          onClick: (event) => {
+            // 点击行
+            if (record.status === "unread") {
+              putStatus(record.id, "read");
+            }
+          },
+        };
+      }}
+      toolBarRender={() => {
+        return [
+          <>
+            {"免打扰"}
+            <Switch size="small" />
+          </>,
+          <Button
+            key="4"
+            type="link"
+            icon={<ReloadOutlined />}
+            onClick={() => {
+              ref.current?.reload();
+            }}
+          />,
+        ];
+      }}
+      search={{
+        filterType: "light",
+      }}
+      request={async (params = {}, sorter, filter) => {
+        console.log(params, sorter, filter);
+        let queryStatus = activeKey;
+        if (activeKey === "inbox") {
+          queryStatus = "read,unread";
+        }
+        let url = `/v2/notification?view=to&status=${queryStatus}`;
+        const offset =
+          ((params.current ? params.current : 1) - 1) *
+          (params.pageSize ? params.pageSize : 5);
+        url += `&limit=${params.pageSize}&offset=${offset}`;
+        const res = await get<INotificationListResponse>(url);
+        let items: INotification[] = [];
+        if (res.ok) {
+          items = res.data.rows.map((item, id) => {
+            return {
+              id: item.id,
+              from: item.from,
+              to: item.to,
+              url: item.url,
+              content: item.content,
+              content_type: item.content_type,
+              res_type: item.res_type,
+              res_id: item.res_id,
+              status: item.status,
+              deleted_at: item.deleted_at,
+              created_at: item.created_at,
+              updated_at: item.updated_at,
+            };
+          });
+          if (typeof onChange !== "undefined") {
+            onChange(res.data.unread);
+          }
+        }
+
+        console.debug(items);
+        return {
+          total: res.data.count,
+          succcess: true,
+          data: items,
+        };
+      }}
+      pagination={{
+        pageSize: 5,
+      }}
+      showActions="hover"
+      metas={{
+        title: {
+          dataIndex: "user",
+          search: false,
+          render: (_, row) => {
+            return (
+              <Text strong={row.status === "unread"}>{row.from.nickName}</Text>
+            );
+          },
+        },
+        avatar: {
+          dataIndex: "avatar",
+          search: false,
+          render: (_, row) => {
+            return (
+              <Avatar size={"small"}>{row.from.nickName.slice(0, 1)}</Avatar>
+            );
+          },
+        },
+        description: {
+          dataIndex: "title",
+          search: false,
+          render: (_, row) => {
+            return (
+              <Text
+                style={{ cursor: "pointer" }}
+                onClick={() => {
+                  window.open(row.url, "_blank");
+                }}
+              >
+                <Marked
+                  style={{ opacity: row.status === "unread" ? 1 : 0.7 }}
+                  text={row.content}
+                />
+              </Text>
+            );
+          },
+        },
+        subTitle: {
+          dataIndex: "labels",
+          render: (_, row) => {
+            return (
+              <Space>
+                <TimeShow createdAt={row.created_at} />
+                <Tag color="blue">{row.res_type}</Tag>
+              </Space>
+            );
+          },
+          search: false,
+        },
+        status: {
+          // 自己扩展的字段,主要用于筛选,不在列表中显示
+          title: "类型筛选",
+          valueType: "select",
+          valueEnum: {
+            all: { text: "全部", status: "Default" },
+            pr: {
+              text: "修改建议",
+              status: "Error",
+            },
+            discussion: {
+              text: "讨论",
+              status: "Success",
+            },
+          },
+        },
+      }}
+      toolbar={{
+        menu: {
+          activeKey,
+          items: [
+            {
+              key: "inbox",
+              label: "Inbox",
+            },
+            {
+              key: "unread",
+              label: "Unread",
+            },
+            {
+              key: "archived",
+              label: "Archived",
+            },
+          ],
+          onChange(key) {
+            setActiveKey(key);
+            ref.current?.reload();
+          },
+        },
+      }}
+    />
+  );
+};
+
+export default NotificationListWidget;

+ 17 - 0
dashboard/src/pages/library/notifications/index.tsx

@@ -0,0 +1,17 @@
+import { Outlet } from "react-router-dom";
+import { Layout } from "antd";
+
+import FooterBar from "../../../components/library/FooterBar";
+import HeadBar from "../../../components/library/HeadBar";
+
+const Widget = () => {
+  return (
+    <Layout>
+      <HeadBar />
+      <Outlet />
+      <FooterBar />
+    </Layout>
+  );
+};
+
+export default Widget;

+ 17 - 0
dashboard/src/pages/library/notifications/list.tsx

@@ -0,0 +1,17 @@
+import { Col, Row } from "antd";
+import NotificationList from "../../../components/notification/NotificationList";
+
+const Widget = () => {
+  // TODO i18n
+  return (
+    <Row>
+      <Col flex="auto"></Col>
+      <Col flex="960px">
+        <NotificationList />
+      </Col>
+      <Col flex="auto"></Col>
+    </Row>
+  );
+};
+
+export default Widget;