feat: wrap message by fluent ui toast
This commit is contained in:
		@@ -7,6 +7,7 @@ export interface Resp<T> {
 | 
			
		||||
    data: T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 类型保护函数
 | 
			
		||||
function isResp<T>(obj: any): obj is Resp<T> {
 | 
			
		||||
    return (
 | 
			
		||||
@@ -18,23 +19,32 @@ function isResp<T>(obj: any): obj is Resp<T> {
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const invoke = async <T>(path: string, req: any): Promise<Resp<T>> => {
 | 
			
		||||
export async function Dial<T>(path: string, req: any = null): Promise<Resp<T>> {
 | 
			
		||||
    const bs = JSON.stringify(req)
 | 
			
		||||
    console.log(`[DEBUG] invoke req: path = ${path}, req =`, req)
 | 
			
		||||
    const res = await Invoke(path, bs)
 | 
			
		||||
    console.log(`[DEBUG] invoke res: path = ${path}, res =`, res)
 | 
			
		||||
 | 
			
		||||
    let result: Resp<T>;
 | 
			
		||||
    let ok = false;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const res = await Invoke(path, bs)
 | 
			
		||||
        const parsed = JSON.parse(res);
 | 
			
		||||
        if (isResp<T>(parsed)) {
 | 
			
		||||
            return parsed;
 | 
			
		||||
            result = parsed;
 | 
			
		||||
            ok = true
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error('[ERROR] invoke: resp not valid =', res)
 | 
			
		||||
            throw new Error('Parsed response does not match Resp<T> structure');
 | 
			
		||||
            result = {status: 500, msg: "发生错误(0)", err: res} as Resp<T>;
 | 
			
		||||
        }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error(`[ERROR] invoke: resp parse err, err = ${error}, res =`, res);
 | 
			
		||||
        throw new Error('Invalid response format');
 | 
			
		||||
        result = {status: 500, msg: "发生错误(-1)", err: "backend method(Invoke) not found in window"} as Resp<T>;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Dial = invoke;
 | 
			
		||||
    if (ok) {
 | 
			
		||||
        console.log(`[DEBUG] invoke res: path = ${path}, res =`, result)
 | 
			
		||||
    } else {
 | 
			
		||||
        console.error(`[ERROR] invoke res: path = ${path}, res =`, result)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								frontend/src/interfaces/connection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/interfaces/connection.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
export interface Connection {
 | 
			
		||||
    id: number;
 | 
			
		||||
    created_at: number;
 | 
			
		||||
    updated_at: number;
 | 
			
		||||
    deleted_at: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    endpoint: string;
 | 
			
		||||
    active: boolean;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +1,25 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import {createRoot} from 'react-dom/client'
 | 
			
		||||
import './style.css'
 | 
			
		||||
import { FluentProvider, webLightTheme } from '@fluentui/react-components';
 | 
			
		||||
import {createBrowserRouter,RouterProvider} from "react-router-dom";
 | 
			
		||||
import {FluentProvider, webLightTheme} from '@fluentui/react-components';
 | 
			
		||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
 | 
			
		||||
import Home from "./page/home/home";
 | 
			
		||||
import Connection from "./page/connection/connection";
 | 
			
		||||
import {ToastProvider} from "./message";
 | 
			
		||||
 | 
			
		||||
const container = document.getElementById('root')
 | 
			
		||||
 | 
			
		||||
const root = createRoot(container!)
 | 
			
		||||
 | 
			
		||||
const router = createBrowserRouter([
 | 
			
		||||
    {path:'/', element: <Home />},
 | 
			
		||||
    {path:'/connection', element: <Connection />},
 | 
			
		||||
    {path: '/', element: <Home/>},
 | 
			
		||||
    {path: '/connection', element: <Connection/>},
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
root.render(
 | 
			
		||||
    <FluentProvider theme={webLightTheme}>
 | 
			
		||||
        <RouterProvider router={router} />
 | 
			
		||||
        <ToastProvider>
 | 
			
		||||
            <RouterProvider router={router}/>
 | 
			
		||||
        </ToastProvider>
 | 
			
		||||
    </FluentProvider>,
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								frontend/src/message.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/src/message.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import {createContext, FC, ReactNode, useContext} from "react";
 | 
			
		||||
import {Toast, Toaster, ToastTitle, useId, useToastController} from "@fluentui/react-components";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface ToastContextType {
 | 
			
		||||
    dispatchMessage: (content: string, type: "success" | "error" | "warning" | "info") => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
 | 
			
		||||
 | 
			
		||||
export const ToastProvider: FC<{ children: ReactNode }> = ({children}) => {
 | 
			
		||||
 | 
			
		||||
    const toasterId = useId("toaster");
 | 
			
		||||
    const {dispatchToast} = useToastController(toasterId);
 | 
			
		||||
 | 
			
		||||
    const dispatchMessage = (content: string, type: "success" | "error" | "warning" | "info" = "info") => {
 | 
			
		||||
        dispatchToast(
 | 
			
		||||
            <Toast>
 | 
			
		||||
                <ToastTitle>{content}</ToastTitle>
 | 
			
		||||
            </Toast>,
 | 
			
		||||
            {position: "top-end", intent: type}
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return <ToastContext.Provider value={{dispatchMessage}}>
 | 
			
		||||
        {children}
 | 
			
		||||
        <Toaster toasterId={toasterId}/>
 | 
			
		||||
    </ToastContext.Provider>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useToast = () => {
 | 
			
		||||
    const context = useContext(ToastContext);
 | 
			
		||||
    if (!context) {
 | 
			
		||||
        throw new Error("useToast must be used within a ToastProvider");
 | 
			
		||||
    }
 | 
			
		||||
    return context;
 | 
			
		||||
};
 | 
			
		||||
@@ -3,23 +3,28 @@ import {
 | 
			
		||||
    useId,
 | 
			
		||||
    Button,
 | 
			
		||||
    FieldProps,
 | 
			
		||||
    useToastController,
 | 
			
		||||
    Toast,
 | 
			
		||||
    ToastTitle,
 | 
			
		||||
    ToastIntent,
 | 
			
		||||
    Toaster
 | 
			
		||||
    Spinner
 | 
			
		||||
} from "@fluentui/react-components";
 | 
			
		||||
import {Field, Input} from "@fluentui/react-components";
 | 
			
		||||
import {useNavigate} from "react-router-dom";
 | 
			
		||||
import {useState} from "react";
 | 
			
		||||
import {Dial} from "../../api";
 | 
			
		||||
import {useToast} from "../../message";
 | 
			
		||||
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const Connection = (props: Partial<FieldProps>) => {
 | 
			
		||||
 | 
			
		||||
    const toasterId = useId("toaster");
 | 
			
		||||
    const {dispatchToast} = useToastController(toasterId);
 | 
			
		||||
    const { dispatchMessage } = useToast();
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const [testLoading, setTestLoading] = useState<"initial" | "loading" | "success" | "error">("initial");
 | 
			
		||||
    const buttonIcon =
 | 
			
		||||
        testLoading === "loading" ? (
 | 
			
		||||
            <Spinner size="tiny"/>
 | 
			
		||||
        ) : testLoading === "success" ? (
 | 
			
		||||
            <CheckmarkFilled/>
 | 
			
		||||
        ) : testLoading === "error" ? (
 | 
			
		||||
            <DismissRegular/>
 | 
			
		||||
        ) : null;
 | 
			
		||||
    const [value, setValue] = useState<{ name: string, endpoint: string, access: string, key: string }>({
 | 
			
		||||
        name: '',
 | 
			
		||||
        endpoint: '',
 | 
			
		||||
@@ -28,16 +33,24 @@ const Connection = (props: Partial<FieldProps>) => {
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    function test() {
 | 
			
		||||
        const val = JSON.stringify(value);
 | 
			
		||||
        console.log('[DEBUG] connection.test: value =', val)
 | 
			
		||||
        setTestLoading("loading")
 | 
			
		||||
        Dial<string>("/api/connection/test", value).then(res => {
 | 
			
		||||
            let status: "success" | "error" = "error"
 | 
			
		||||
            if (res.status === 200) {
 | 
			
		||||
                dispatchToast(
 | 
			
		||||
                    <Toast>
 | 
			
		||||
                        <ToastTitle>连接成功!</ToastTitle>
 | 
			
		||||
                    </Toast>,
 | 
			
		||||
                    {position: "top-end", intent: "success"}
 | 
			
		||||
                )
 | 
			
		||||
                status = "success"
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setTestLoading(status);
 | 
			
		||||
 | 
			
		||||
            dispatchMessage(res.msg, status)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function create() {
 | 
			
		||||
        Dial<unknown>("/api/connection/create", value).then(res => {
 | 
			
		||||
            dispatchMessage(res.msg, res.status === 200?"success":"error");
 | 
			
		||||
            if (res.status === 200) {
 | 
			
		||||
                navigate("/");
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
@@ -94,17 +107,17 @@ const Connection = (props: Partial<FieldProps>) => {
 | 
			
		||||
                </Field>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className='connection-form-field connection-form-field-actions'>
 | 
			
		||||
                <Button appearance='transparent' onClick={() => test()}>测试连接</Button>
 | 
			
		||||
                <Button appearance='transparent' icon={buttonIcon} onClick={() => test()}>测试连接</Button>
 | 
			
		||||
                <div style={{marginLeft: 'auto'}}>
 | 
			
		||||
                    <Button style={{marginRight: '20px'}} className='connection-form-field-actions-cancel'
 | 
			
		||||
                            onClick={() => {
 | 
			
		||||
                                navigate("/")
 | 
			
		||||
                            }}>取消</Button>
 | 
			
		||||
                    <Button className='connection-form-field-actions-confirm' appearance='primary'>新建</Button>
 | 
			
		||||
                    <Button className='connection-form-field-actions-confirm' appearance='primary'
 | 
			
		||||
                            onClick={() => create()}>新建</Button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Toaster toasterId={toasterId}/>
 | 
			
		||||
    </div>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
import {useEffect, useState} from 'react';
 | 
			
		||||
import './home.css';
 | 
			
		||||
import {
 | 
			
		||||
    Button,
 | 
			
		||||
@@ -6,25 +6,45 @@ import {
 | 
			
		||||
import {
 | 
			
		||||
    CloudAddFilled, DismissRegular
 | 
			
		||||
} from "@fluentui/react-icons";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import {useNavigate} from "react-router-dom";
 | 
			
		||||
import {Dial} from "../../api";
 | 
			
		||||
import {useToast} from "../../message";
 | 
			
		||||
import {Connection} from "../../interfaces/connection";
 | 
			
		||||
 | 
			
		||||
function Home() {
 | 
			
		||||
    const {dispatchMessage} = useToast();
 | 
			
		||||
    const [connectionFilterKeywords, setConnectionFilterKeywords] = useState<string>('');
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const [connectionList, setConnectionList] = useState<Connection[]>([]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        Dial<{ list: Connection[] }>("/api/connection/list").then(res => {
 | 
			
		||||
            dispatchMessage(res.msg, res.status === 200 ? "success" : "error");
 | 
			
		||||
            if (res.status === 200) {
 | 
			
		||||
                setConnectionList(res.data.list)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="container">
 | 
			
		||||
            <div className="header">
 | 
			
		||||
                        <Button appearance="primary" icon={<CloudAddFilled  />} onClick={() => {navigate("/connection")}}>
 | 
			
		||||
                            新建连接
 | 
			
		||||
                        </Button>
 | 
			
		||||
                <Button appearance="primary" icon={<CloudAddFilled/>} onClick={() => {
 | 
			
		||||
                    navigate("/connection")
 | 
			
		||||
                }}>
 | 
			
		||||
                    新建连接
 | 
			
		||||
                </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="body">
 | 
			
		||||
                <div className="body-connections">
 | 
			
		||||
                    <div className="body-connections-search">
 | 
			
		||||
                        <input className="body-connections-search-input" type={"text"} placeholder="搜索连接" value={connectionFilterKeywords} onChange={(e) => setConnectionFilterKeywords(e.target.value)} />
 | 
			
		||||
                        <div className="body-connections-search-dismiss" onClick={() => {setConnectionFilterKeywords('')}}>
 | 
			
		||||
                            <DismissRegular />
 | 
			
		||||
                        <input className="body-connections-search-input" type={"text"} placeholder="搜索连接"
 | 
			
		||||
                               value={connectionFilterKeywords}
 | 
			
		||||
                               onChange={(e) => setConnectionFilterKeywords(e.target.value)}/>
 | 
			
		||||
                        <div className="body-connections-search-dismiss" onClick={() => {
 | 
			
		||||
                            setConnectionFilterKeywords('')
 | 
			
		||||
                        }}>
 | 
			
		||||
                            <DismissRegular/>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="body-connections-list"></div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user