banxia 1 kuukausi sitten
vanhempi
commit
3b7e98094b

+ 2 - 0
.gitignore

@@ -22,3 +22,5 @@ dist-ssr
 *.njsproj
 *.sln
 *.sw?
+
+dist-electron

+ 2 - 0
electron/main/ipcMain.ts

@@ -5,6 +5,7 @@ import {
   minimizeMainWin,
   unmaximizeMainWin,
   maximizeMainWin,
+  middleMainWin,
 } from "../win/mainWin";
 
 export const initIpcMain = () => {
@@ -31,4 +32,5 @@ export const initIpcMain = () => {
     }
     return obj;
   });
+  handle("middle", () => middleMainWin());
 };

+ 1 - 0
electron/preload/electronAPI.ts

@@ -3,6 +3,7 @@ import { contextBridge, ipcRenderer } from "electron";
 contextBridge.exposeInMainWorld("electronAPI", {
   max: (...res: any) => ipcRenderer.invoke("max", ...res),
   min: (...res: any) => ipcRenderer.invoke("min", ...res),
+  middle: (...res: any) => ipcRenderer.invoke("middle", ...res),
   close: (...res: any) => ipcRenderer.invoke("close", ...res),
   send: (channel, data) => ipcRenderer.send(channel, data),
   on: (channel, callback) =>

+ 20 - 2
electron/win/mainWin.ts

@@ -1,9 +1,10 @@
-import { app, BrowserWindow, screen, session } from "electron";
+import { BrowserWindow, screen } from "electron";
 import { join } from "node:path";
 import { ICON, DIST, preload, url } from "../main/utils";
 import log from "electron-log/main";
 
 const NODE_ENV = process.env.NODE_ENV;
+
 const indexHtml = join(DIST, "./index.html");
 let mainWindow: BrowserWindow | null;
 const extra = {
@@ -13,7 +14,7 @@ const extra = {
 // 主窗口
 const createWindow = (): BrowserWindow => {
   //动态适应宽高
-  const { width, height } = screen.getPrimaryDisplay().workAreaSize;
+  const { width } = screen.getPrimaryDisplay().workAreaSize;
   // 设置窗口的宽度和高度
   const winWidth = 420;
   const winHeight = 700;
@@ -112,6 +113,22 @@ function maximizeMainWin() {
   mainWindow!.maximize();
 }
 
+function middleMainWin() {
+  const { width } = screen.getPrimaryDisplay().workAreaSize;
+  const currentWidth = mainWindow?.getBounds().width;
+  if (currentWidth === 840) {
+    mainWindow?.setBounds({
+      width: 420,
+      x: width - 420,
+    });
+    return;
+  }
+  mainWindow?.setBounds({
+    width: 840,
+    x: width - 840,
+  });
+}
+
 // 取消最大化窗口
 function unmaximizeMainWin() {
   mainWindow!.unmaximize();
@@ -143,4 +160,5 @@ export {
   isMaximized,
   maximizeMainWin,
   unmaximizeMainWin,
+  middleMainWin,
 };

+ 4 - 1
package.json

@@ -16,18 +16,21 @@
     "axios": "^1.7.7",
     "cookie-parser": "^1.4.7",
     "cors": "^2.8.5",
+    "dayjs": "^1.11.13",
     "electron-log": "^5.2.2",
     "electron-store": "^8.2.0",
     "express": "^4.21.1",
     "express-session": "^1.18.1",
     "jingmiao": "file:",
+    "lucide-react": "^0.486.0",
     "nprogress": "^0.2.0",
     "qs": "^6.13.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-router-dom": "^6.28.0",
     "sass": "1.69.5",
-    "sky": "file:"
+    "sky": "file:",
+    "zustand": "^5.0.3"
   },
   "devDependencies": {
     "@eslint/js": "^9.14.0",

+ 148 - 0
src/components/MessageModal/MessageModal.tsx

@@ -0,0 +1,148 @@
+import React, { useState } from "react";
+import { Modal, Button, Tabs } from "antd";
+import { CheckCircleOutlined, EyeOutlined } from "@ant-design/icons";
+import type { TabsProps } from "antd";
+import { MESSAGE_TAB } from "@/constants";
+import { useNotificationStore } from "@/store/NotificationStore";
+import { Eraser, FileText } from "lucide-react";
+import dayjs from "dayjs";
+
+interface MessageModalProps {
+  visible: boolean;
+  onClose: () => void;
+  socket: any;
+}
+
+const MessageModal: React.FC<MessageModalProps> = ({
+  visible,
+  onClose,
+  socket,
+}) => {
+  const [activeKey, setActiveKey] = useState(MESSAGE_TAB.UNREAD);
+  const { notifications } = useNotificationStore();
+  console.log("notifications", notifications);
+
+  const tabItems: TabsProps["items"] = [
+    {
+      key: MESSAGE_TAB.ALL,
+      label: (
+        <span className="flex">
+          全部
+          <span className="text-xs text-white bg-red-500 rounded-full w-5 h-5 flex justify-center items-center">
+            {notifications.length > 9 ? "9+" : notifications.length}
+          </span>
+        </span>
+      ),
+    },
+    {
+      key: MESSAGE_TAB.UNREAD,
+      label: (
+        <span className="flex">
+          未读{" "}
+          <span className="text-xs text-white bg-red-500 rounded-full w-5 h-5 flex justify-center items-center">
+            {notifications.filter(
+              (notification) => notification.status === MESSAGE_TAB.UNREAD
+            ).length > 9
+              ? "9+"
+              : notifications.filter(
+                  (notification) => notification.status === MESSAGE_TAB.UNREAD
+                ).length}
+          </span>
+        </span>
+      ),
+    },
+    {
+      key: MESSAGE_TAB.HAS_READ,
+      label: (
+        <span className="flex">
+          已读
+          <span className="text-xs text-white bg-red-500 rounded-full w-5 h-5 flex justify-center items-center">
+            {notifications.filter(
+              (notification) => notification.status === MESSAGE_TAB.HAS_READ
+            ).length > 9
+              ? "9+"
+              : notifications.filter(
+                  (notification) => notification.status === MESSAGE_TAB.HAS_READ
+                ).length}
+          </span>
+        </span>
+      ),
+    },
+  ];
+
+  return (
+    <Modal
+      title={
+        <div className="flex items-center">
+          <span>消息中心 (1)</span>
+          <div className="cursor-pointer hover:bg-gray-100 rounded-md px-2 py-1 ml-2 text-xs flex justify-center items-center gap-1">
+            <Eraser size={12} />
+            清除消息
+          </div>
+        </div>
+      }
+      open={visible}
+      onCancel={onClose}
+      footer={null}
+      width={600}
+    >
+      <div className="flex flex-col">
+        <Tabs
+          activeKey={activeKey}
+          items={tabItems}
+          className="mb-4"
+          onChange={(key) => {
+            setActiveKey(key);
+          }}
+        />
+        <div className="space-y-4 max-h-[60vh] overflow-auto">
+          {notifications
+            .filter(
+              (notification) =>
+                activeKey === MESSAGE_TAB.ALL ||
+                notification.status === activeKey
+            )
+            .map((message, index) => (
+              <div
+                key={index}
+                className="p-4 bg-gray-50 rounded-lg shadow-sm flex flex-col gap-4"
+              >
+                <div className="flex justify-between items-center">
+                  <div className="flex gap-1 items-center">
+                    <div className="p-1 bg-blue-500 rounded-full w-6 h-6 text-white flex justify-center items-center">
+                      <FileText size={14} />
+                    </div>
+                    <div className="font-bold">病例时效提醒</div>
+                  </div>
+                  <div className="text-gray-500 text-xs">
+                    {dayjs(message.timestamp).format("YYYY-MM-DD HH:mm")}
+                  </div>
+                </div>
+                <div className="flex items-start space-x-4">
+                  <div className="flex-1">
+                    <div className="flex">
+                      <h4 className="text-sm font-semibold">{message.title}</h4>
+                    </div>
+                    <p className="text-gray-700 mt-1 indent-[1.75rem]">
+                      {message.message}
+                    </p>
+                    <p className="text-gray-700 mt-1 indent-[1.75rem]">
+                      {message.footer || "祝您工作顺利!"}
+                    </p>
+                    <div className="mt-2 flex space-x-2 justify-end">
+                      <Button size="middle">知道了</Button>
+                      <Button type="primary" size="middle">
+                        去查看
+                      </Button>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            ))}
+        </div>
+      </div>
+    </Modal>
+  );
+};
+
+export default MessageModal;

+ 5 - 0
src/constants/index.ts

@@ -0,0 +1,5 @@
+export const MESSAGE_TAB = {
+  ALL: "1",
+  HAS_READ: "2",
+  UNREAD: "3",
+};

+ 55 - 0
src/hooks/useWebSocket.ts

@@ -0,0 +1,55 @@
+import { useNotificationStore } from "@/store/NotificationStore";
+import { useState, useEffect } from "react";
+
+interface Notification {
+  type: string;
+  isRead: boolean;
+  [key: string]: any;
+}
+
+interface UseWebSocketReturn {
+  socket: WebSocket | null;
+}
+
+const useWebSocket = (url: string): UseWebSocketReturn => {
+  const [socket, setSocket] = useState<WebSocket | null>(null);
+  const { notifications, changeNotification } = useNotificationStore();
+  useEffect(() => {
+    const ws = new WebSocket(url);
+
+    ws.onopen = () => {
+      console.log("已连接到 WebSocket 服务器");
+    };
+
+    ws.onmessage = (event) => {
+      const data = JSON.parse(event.data);
+      console.log("收到消息:", data);
+
+      if (data.type === "notification") {
+        changeNotification("add", data);
+      } else if (data.type === "update") {
+        changeNotification("update", { ...data, isRead: false });
+      } else if (data.type === "clear") {
+        changeNotification("clear");
+      }
+    };
+
+    ws.onclose = () => {
+      console.log("WebSocket 连接已关闭");
+    };
+
+    ws.onerror = (error) => {
+      console.error("WebSocket 错误:", error);
+    };
+
+    setSocket(ws);
+
+    return () => {
+      ws.close();
+    };
+  }, [url]);
+
+  return { socket };
+};
+
+export default useWebSocket;

+ 45 - 12
src/layouts/Header/index.tsx

@@ -1,5 +1,9 @@
 import { useState } from "react";
+import { Bell, Expand, Minus, Square, X } from "lucide-react";
 import "./index.scss";
+import MessageModal from "@/components/MessageModal/MessageModal";
+import { useNotificationStore } from "@/store/NotificationStore";
+import useWebSocket from "@/hooks/useWebSocket";
 
 interface optType {
   maxStatus?: string; // 最大化
@@ -7,7 +11,10 @@ interface optType {
 
 // 顶部栏
 const LayoutHeader = (props) => {
+  const { socket } = useWebSocket("ws://localhost:8080");
   const [opt, setOpt] = useState<optType>({});
+  const [messageModalVisible, setMessageModalVisible] = useState(false);
+  const { notifications } = useNotificationStore();
 
   // 操作点击
   const handleTitleClick = async (event, type: string) => {
@@ -30,23 +37,49 @@ const LayoutHeader = (props) => {
     <header className="header bg-[#ffffff]">
       <div className="content flex">
         <div className="header-menu flex-1 h-full"></div>
-        <div className="opt flex items-center">
-          <i
-            className="iconfont icon-jianhao icon"
+        <div className="opt flex items-center py-2 px-6">
+          <div
+            onClick={() => setMessageModalVisible(true)}
+            className="relative cursor-pointer p-2 hover:bg-gray-100 rounded-lg"
+          >
+            <Bell className="text-gray-600 hover:bg-gray-5002" size={16} />
+            {!!notifications.length && (
+              <span className="absolute top-0 right-0 w-4 h-4 flex justify-center items-center text-xs bg-red-500 text-white rounded-full">
+                {notifications.length}
+              </span>
+            )}
+          </div>
+          <div
+            className="cursor-pointer p-2 hover:bg-gray-100 rounded-lg"
+            onClick={(event) => handleTitleClick(event, "middle")}
+          >
+            <Expand className="text-gray-600 hover:bg-gray-5002" size={16} />
+          </div>
+          <div
+            className="cursor-pointer p-2 hover:bg-gray-100 rounded-lg"
             onClick={(event) => handleTitleClick(event, "min")}
-          ></i>
-          <i
-            className={`iconfont ${
-              opt.maxStatus == "max" ? "icon-suoxiao" : "icon-24gl-square"
-            } icon-24gl-square icon`}
+          >
+            <Minus className="cursor-pointer text-gray-600" size={16} />
+          </div>
+          <div
+            className="cursor-pointer p-2 hover:bg-gray-100 rounded-lg"
             onClick={(event) => handleTitleClick(event, "max")}
-          ></i>
-          <i
-            className="iconfont icon-chacha icon"
+          >
+            <Square className="cursor-pointer text-gray-600" size={16} />
+          </div>
+          <div
+            className="cursor-pointer p-2 hover:bg-gray-100 rounded-lg"
             onClick={(event) => handleTitleClick(event, "close")}
-          ></i>
+          >
+            <X className="cursor-pointer text-gray-600 " size={16} />
+          </div>
         </div>
       </div>
+      <MessageModal
+        visible={messageModalVisible}
+        onClose={() => setMessageModalVisible(false)}
+        socket={socket}
+      />
     </header>
   );
 };

+ 1 - 1
src/layouts/index.tsx

@@ -8,7 +8,7 @@ const { Content } = Layout;
 const MainPage = () => {
   return (
     <Layout className="ant-layout">
-      {/* <LayoutHeader /> */}
+      <LayoutHeader />
       <Content className="layout-content">
         <Outlet></Outlet>
       </Content>

+ 26 - 0
src/store/NotificationStore.ts

@@ -0,0 +1,26 @@
+import { create } from "zustand";
+interface NotificationStore {
+  notifications: any[];
+}
+interface NotificationAction {
+  changeNotification: (type: string, notification?: any) => void;
+}
+
+const initialState = {
+  notifications: [],
+};
+
+export const useNotificationStore = create<
+  NotificationStore & NotificationAction
+>((set, get) => ({
+  ...initialState,
+  changeNotification: (type, notification) => {
+    let new_notification: any = [];
+    if (type === "add") {
+      new_notification = [...get().notifications, notification];
+    } else if (type === "clear") {
+      new_notification = [];
+    }
+    set({ notifications: new_notification });
+  },
+}));