visuddhinanda %!s(int64=2) %!d(string=hai) anos
pai
achega
6b7dbf5930

+ 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;
+}

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

@@ -0,0 +1,65 @@
+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,
+                  });
+                  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;

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

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

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

@@ -0,0 +1,8 @@
+import NotificationList from "../../../components/notification/NotificationList";
+
+const Widget = () => {
+  // TODO i18n
+  return <NotificationList />;
+};
+
+export default Widget;