NapCatQQ/packages/napcat-webui-frontend/src/pages/dashboard/network.tsx
手瓜一十雪 4360775eff
refactor: 整体重构 (#1381)
* feat: pnpm new

* Refactor build and release workflows, update dependencies

Switch build scripts and workflows from npm to pnpm, update build and artifact paths, and simplify release workflow by removing version detection and changelog steps. Add new dependencies (silk-wasm, express, ws, node-pty-prebuilt-multiarch), update exports in package.json files, and add vite config for napcat-framework. Also, rename manifest.json for framework package and fix static asset copying in shell build config.
2025-11-13 15:39:42 +08:00

408 lines
11 KiB
TypeScript

import { Button } from '@heroui/button';
import { useDisclosure } from '@heroui/modal';
import { Tab, Tabs } from '@heroui/tabs';
import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { IoMdRefresh } from 'react-icons/io';
import AddButton from '@/components/button/add_button';
import HTTPClientDisplayCard from '@/components/display_card/http_client';
import HTTPServerDisplayCard from '@/components/display_card/http_server';
import HTTPSSEServerDisplayCard from '@/components/display_card/http_sse_server';
import WebsocketClientDisplayCard from '@/components/display_card/ws_client';
import WebsocketServerDisplayCard from '@/components/display_card/ws_server';
import NetworkFormModal from '@/components/network_edit/modal';
import PageLoading from '@/components/page_loading';
import useConfig from '@/hooks/use-config';
import useDialog from '@/hooks/use-dialog';
export interface SectionProps {
title: string;
color?:
| 'violet'
| 'yellow'
| 'blue'
| 'cyan'
| 'green'
| 'pink'
| 'foreground';
icon: React.ReactNode;
children: React.ReactNode;
}
export interface EmptySectionProps {
isEmpty: boolean;
}
const EmptySection: React.FC<EmptySectionProps> = ({ isEmpty }) => {
return (
<div
className={clsx('text-default-400', {
hidden: !isEmpty,
})}
>
</div>
);
};
export default function NetworkPage () {
const {
config,
refreshConfig,
deleteNetworkConfig,
enableNetworkConfig,
enableDebugNetworkConfig,
} = useConfig();
const [activeField, setActiveField] =
useState<keyof OneBotConfig['network']>('httpServers');
const [activeName, setActiveName] = useState<string>('');
const {
network: {
httpServers,
httpClients,
httpSseServers,
websocketServers,
websocketClients,
},
} = config;
const [loading, setLoading] = useState(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const dialog = useDialog();
const activeData = useMemo(() => {
const findData = config.network[activeField].find(
(item) => item.name === activeName
);
return findData;
}, [activeField, activeName, config]);
const refresh = async () => {
setLoading(true);
try {
await refreshConfig();
setLoading(false);
} catch (error) {
const msg = (error as Error).message;
toast.error(`获取配置失败: ${msg}`);
} finally {
setLoading(false);
}
};
const handleClickCreate = (key: keyof OneBotConfig['network']) => {
setActiveField(key);
setActiveName('');
onOpen();
};
const onDelete = async (
field: keyof OneBotConfig['network'],
name: string
) => {
return new Promise<void>((resolve, reject) => {
dialog.confirm({
title: '删除配置',
content: `确定要删除配置「${name}」吗?`,
onConfirm: async () => {
try {
await deleteNetworkConfig(field, name);
toast.success('删除配置成功');
resolve();
} catch (error) {
const msg = (error as Error).message;
toast.error(`删除配置失败: ${msg}`);
reject(error);
}
},
onCancel: () => {
resolve();
},
});
});
};
const onEnable = async (
field: keyof OneBotConfig['network'],
name: string
) => {
try {
await enableNetworkConfig(field, name);
toast.success('更新配置成功');
} catch (error) {
const msg = (error as Error).message;
toast.error(`更新配置失败: ${msg}`);
throw error;
}
};
const onEnableDebug = async (
field: keyof OneBotConfig['network'],
name: string
) => {
try {
await enableDebugNetworkConfig(field, name);
toast.success('更新配置成功');
} catch (error) {
const msg = (error as Error).message;
toast.error(`更新配置失败: ${msg}`);
throw error;
}
};
const onEdit = (field: keyof OneBotConfig['network'], name: string) => {
setActiveField(field);
setActiveName(name);
onOpen();
};
const renderCard = <T extends keyof OneBotConfig['network']> (
type: T,
item: OneBotConfig['network'][T][0],
showType = false
) => {
switch (type) {
case 'httpServers':
return (
<HTTPServerDisplayCard
key={item.name}
showType={showType}
data={item as OneBotConfig['network']['httpServers'][0]}
onDelete={async () => {
await onDelete('httpServers', item.name);
}}
onEdit={() => {
onEdit('httpServers', item.name);
}}
onEnable={async () => {
await onEnable('httpServers', item.name);
}}
onEnableDebug={async () => {
await onEnableDebug('httpServers', item.name);
}}
/>
);
case 'httpClients':
return (
<HTTPClientDisplayCard
key={item.name}
showType={showType}
data={item as OneBotConfig['network']['httpClients'][0]}
onDelete={async () => {
await onDelete('httpClients', item.name);
}}
onEdit={() => {
onEdit('httpClients', item.name);
}}
onEnable={async () => {
await onEnable('httpClients', item.name);
}}
onEnableDebug={async () => {
await onEnableDebug('httpClients', item.name);
}}
/>
);
case 'httpSseServers':
return (
<HTTPSSEServerDisplayCard
key={item.name}
showType={showType}
data={item as OneBotConfig['network']['httpSseServers'][0]}
onDelete={async () => {
await onDelete('httpSseServers', item.name);
}}
onEdit={() => {
onEdit('httpSseServers', item.name);
}}
onEnable={async () => {
await onEnable('httpSseServers', item.name);
}}
onEnableDebug={async () => {
await onEnableDebug('httpSseServers', item.name);
}}
/>
);
case 'websocketServers':
return (
<WebsocketServerDisplayCard
key={item.name}
showType={showType}
data={item as OneBotConfig['network']['websocketServers'][0]}
onDelete={async () => {
await onDelete('websocketServers', item.name);
}}
onEdit={() => {
onEdit('websocketServers', item.name);
}}
onEnable={async () => {
await onEnable('websocketServers', item.name);
}}
onEnableDebug={async () => {
await onEnableDebug('websocketServers', item.name);
}}
/>
);
case 'websocketClients':
return (
<WebsocketClientDisplayCard
key={item.name}
showType={showType}
data={item as OneBotConfig['network']['websocketClients'][0]}
onDelete={async () => {
await onDelete('websocketClients', item.name);
}}
onEdit={() => {
onEdit('websocketClients', item.name);
}}
onEnable={async () => {
await onEnable('websocketClients', item.name);
}}
onEnableDebug={async () => {
await onEnableDebug('websocketClients', item.name);
}}
/>
);
}
};
const tabs = [
{
key: 'all',
title: '全部',
items: [
...httpServers,
...httpClients,
...websocketServers,
...websocketClients,
...httpSseServers,
]
.sort((a, b) => a.name.localeCompare(b.name))
.map((item) => {
if (httpServers.find((i) => i.name === item.name)) {
return renderCard(
'httpServers',
item as OneBotConfig['network']['httpServers'][0],
true
);
}
if (httpSseServers.find((i) => i.name === item.name)) {
return renderCard(
'httpSseServers',
item as OneBotConfig['network']['httpSseServers'][0],
true
);
}
if (httpClients.find((i) => i.name === item.name)) {
return renderCard(
'httpClients',
item as OneBotConfig['network']['httpClients'][0],
true
);
}
if (websocketServers.find((i) => i.name === item.name)) {
return renderCard(
'websocketServers',
item as OneBotConfig['network']['websocketServers'][0],
true
);
}
if (websocketClients.find((i) => i.name === item.name)) {
return renderCard(
'websocketClients',
item as OneBotConfig['network']['websocketClients'][0],
true
);
}
return null;
}),
},
{
key: 'httpServers',
title: 'HTTP服务器',
items: httpServers.map((item) => renderCard('httpServers', item)),
},
{
key: 'httpClients',
title: 'HTTP客户端',
items: httpClients.map((item) => renderCard('httpClients', item)),
},
{
key: 'httpSseServers',
title: 'HTTP SSE服务器',
items: httpSseServers.map((item) => renderCard('httpSseServers', item)),
},
{
key: 'websocketServers',
title: 'Websocket服务器',
items: websocketServers.map((item) =>
renderCard('websocketServers', item)
),
},
{
key: 'websocketClients',
title: 'Websocket客户端',
items: websocketClients.map((item) =>
renderCard('websocketClients', item)
),
},
];
useEffect(() => {
refresh();
}, []);
return (
<>
<title> - NapCat WebUI</title>
<div className='p-2 md:p-4 relative'>
<NetworkFormModal
data={activeData}
field={activeField}
isOpen={isOpen}
onOpenChange={onOpenChange}
/>
<PageLoading loading={loading} />
<div className='flex mb-6 items-center gap-4'>
<AddButton onOpen={handleClickCreate} />
<Button
isIconOnly
color='primary'
radius='full'
variant='flat'
onPress={refresh}
>
<IoMdRefresh size={24} />
</Button>
</div>
<Tabs
aria-label='Network Configs'
className='max-w-full'
items={tabs}
classNames={{
tabList: 'bg-opacity-50 backdrop-blur-sm',
cursor: 'bg-opacity-60 backdrop-blur-sm',
}}
>
{(item) => (
<Tab key={item.key} title={item.title}>
<EmptySection isEmpty={!item.items.length} />
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-start items-stretch gap-x-2 gap-y-4'>
{item.items}
</div>
</Tab>
)}
</Tabs>
</div>
</>
);
}