feat: 新版webui

This commit is contained in:
bietiaop
2025-01-24 21:13:44 +08:00
parent 1d0d25eea2
commit ee1291e42c
201 changed files with 18454 additions and 3422 deletions

View File

@@ -0,0 +1,101 @@
import { Chip } from '@heroui/chip'
import { Image } from '@heroui/image'
import { Link } from '@heroui/link'
import { Spinner } from '@heroui/spinner'
import { Tooltip } from '@heroui/tooltip'
import { useRequest } from 'ahooks'
import clsx from 'clsx'
import { BietiaopIcon, GithubIcon, WebUIIcon } from '@/components/icons'
import NapCatRepoInfo from '@/components/napcat_repo_info'
import { title } from '@/components/primitives'
import logo from '@/assets/images/logo.png'
import WebUIManager from '@/controllers/webui_manager'
import packageJson from '../../../package.json'
function VersionInfo() {
const { data, loading, error } = useRequest(WebUIManager.getPackageInfo)
return (
<div className="flex items-center gap-2 mb-5">
<Chip
startContent={
<Chip color="danger" size="sm" className="-ml-0.5 select-none">
WebUI
</Chip>
}
>
{packageJson.version}
</Chip>
<Chip
startContent={
<Chip color="warning" size="sm" className="-ml-0.5 select-none">
NapCat
</Chip>
}
>
{error ? (
error.message
) : loading ? (
<Spinner size="sm" />
) : (
data?.version
)}
</Chip>
<Tooltip content="查看WebUI源码" placement="bottom" showArrow>
<Link isExternal href="https://github.com/bietiaop/NextNapCatWebUI">
<GithubIcon className="text-default-900 hover:text-default-600 w-8 h-8 hover:drop-shadow-lg transition-all" />
</Link>
</Tooltip>
</div>
)
}
export default function AboutPage() {
return (
<>
<title> NapCat WebUI</title>
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="max-w-full w-[1000px] px-5 flex flex-col items-center">
<div className="flex flex-col md:flex-row items-center">
<Image
alt="logo"
className="flex-shrink-0 w-52 md:w-48 mr-2"
src={logo}
/>
<div className="flex -mt-9 md:mt-0">
<WebUIIcon />
</div>
</div>
<div className="flex opacity-60 items-center gap-2 mb-2 font-ubuntu">
Created By
<div className="flex scale-80 -ml-5 -mr-5">
<BietiaopIcon />
</div>
</div>
<VersionInfo />
<div className="mb-6 flex flex-col items-center gap-4">
<p
className={clsx(
title({
color: 'cyan',
shadow: true
}),
'!text-3xl'
)}
>
NapCat Contributors
</p>
<Image
className="w-[600px] max-w-full pointer-events-none select-none"
src="https://contrib.rocks/image?repo=bietiaop/NapCatQQ"
alt="Contributors"
/>
</div>
<NapCatRepoInfo />
</div>
</section>
</>
)
}

View File

@@ -0,0 +1,135 @@
import { Tab, Tabs } from '@heroui/tabs'
import { useLocalStorage } from '@uidotdev/usehooks'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import { useMediaQuery } from 'react-responsive'
import key from '@/const/key'
import useConfig from '@/hooks/use-config'
import useMusic from '@/hooks/use-music'
import OneBotConfigCard from './onebot'
import WebUIConfigCard from './webui'
export default function ConfigPage() {
const { config, saveConfigWithoutNetwork, refreshConfig } = useConfig()
const {
control: onebotControl,
handleSubmit: handleOnebotSubmit,
formState: { isSubmitting: isOnebotSubmitting },
setValue: setOnebotValue
} = useForm<IConfig['onebot']>({
defaultValues: {
musicSignUrl: '',
enableLocalFile2Url: false,
parseMultMsg: false
}
})
const {
control: webuiControl,
handleSubmit: handleWebuiSubmit,
formState: { isSubmitting: isWebuiSubmitting },
setValue: setWebuiValue
} = useForm<IConfig['webui']>({
defaultValues: {
background: '',
musicListID: '',
customIcons: {}
}
})
const isMediumUp = useMediaQuery({ minWidth: 768 })
const [b64img, setB64img] = useLocalStorage(key.backgroundImage, '')
const [customIcons, setCustomIcons] = useLocalStorage<Record<string, string>>(
key.customIcons,
{}
)
const { setListId, listId } = useMusic()
const resetOneBot = () => {
setOnebotValue('musicSignUrl', config.musicSignUrl)
setOnebotValue('enableLocalFile2Url', config.enableLocalFile2Url)
setOnebotValue('parseMultMsg', config.parseMultMsg)
}
const resetWebUI = () => {
setWebuiValue('musicListID', listId)
setWebuiValue('customIcons', customIcons)
setWebuiValue('background', b64img)
}
const onOneBotSubmit = handleOnebotSubmit((data) => {
try {
saveConfigWithoutNetwork(data)
toast.success('保存成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`保存失败: ${msg}`)
}
})
const onWebuiSubmit = handleWebuiSubmit((data) => {
try {
setListId(data.musicListID)
setCustomIcons(data.customIcons)
setB64img(data.background)
toast.success('保存成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`保存失败: ${msg}`)
}
})
const onRefresh = async () => {
try {
await refreshConfig()
toast.success('刷新成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`刷新失败: ${msg}`)
}
}
useEffect(() => {
resetOneBot()
resetWebUI()
}, [config])
return (
<section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10">
<Tabs
aria-label="config tab"
fullWidth
className="w-full"
isVertical={isMediumUp}
classNames={{
tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm',
panel: 'w-full',
base: 'md:!w-auto flex-grow-0 flex-shrink-0 mr-0',
cursor: 'bg-opacity-60 backdrop-blur-sm'
}}
>
<Tab title="OneBot配置" key="onebot">
<OneBotConfigCard
isSubmitting={isOnebotSubmitting}
onRefresh={onRefresh}
onSubmit={onOneBotSubmit}
control={onebotControl}
reset={resetOneBot}
/>
</Tab>
<Tab title="WebUI配置" key="webui">
<WebUIConfigCard
isSubmitting={isWebuiSubmitting}
onRefresh={onRefresh}
onSubmit={onWebuiSubmit}
control={webuiControl}
reset={resetWebUI}
/>
</Tab>
</Tabs>
</section>
)
}

View File

@@ -0,0 +1,70 @@
import { Card, CardBody } from '@heroui/card'
import { Input } from '@heroui/input'
import { Controller } from 'react-hook-form'
import type { Control } from 'react-hook-form'
import SaveButtons from '@/components/button/save_buttons'
import SwitchCard from '@/components/switch_card'
export interface OneBotConfigCardProps {
control: Control<IConfig['onebot']>
onSubmit: () => void
reset: () => void
isSubmitting: boolean
onRefresh: () => void
}
const OneBotConfigCard: React.FC<OneBotConfigCardProps> = (props) => {
const { control, onSubmit, reset, isSubmitting, onRefresh } = props
return (
<>
<title>OneBot配置 - NapCat WebUI</title>
<Card className="bg-opacity-50 backdrop-blur-sm">
<CardBody className="items-center py-5">
<div className="w-96 max-w-full flex flex-col gap-2">
<Controller
control={control}
name="musicSignUrl"
render={({ field }) => (
<Input
{...field}
label="音乐签名地址"
placeholder="请输入音乐签名地址"
/>
)}
/>
<Controller
control={control}
name="enableLocalFile2Url"
render={({ field }) => (
<SwitchCard
{...field}
description="启用本地文件到URL"
label="启用本地文件到URL"
/>
)}
/>
<Controller
control={control}
name="parseMultMsg"
render={({ field }) => (
<SwitchCard
{...field}
description="启用上报解析合并消息"
label="启用上报解析合并消息"
/>
)}
/>
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting}
refresh={onRefresh}
/>
</div>
</CardBody>
</Card>
</>
)
}
export default OneBotConfigCard

View File

@@ -0,0 +1,71 @@
import { Card, CardBody } from '@heroui/card'
import { Input } from '@heroui/input'
import { Controller } from 'react-hook-form'
import type { Control } from 'react-hook-form'
import SaveButtons from '@/components/button/save_buttons'
import ImageInput from '@/components/input/image_input'
import { siteConfig } from '@/config/site'
export interface WebUIConfigCardProps {
control: Control<IConfig['webui']>
onSubmit: () => void
reset: () => void
isSubmitting: boolean
onRefresh: () => void
}
const WebUIConfigCard: React.FC<WebUIConfigCardProps> = (props) => {
const { control, onSubmit, reset, isSubmitting, onRefresh } = props
return (
<>
<title>WebUI配置 - NapCat WebUI</title>
<Card className="bg-opacity-50 backdrop-blur-sm">
<CardBody className="items-center py-5">
<div className="w-96 max-w-full flex flex-col gap-2">
<Controller
control={control}
name="musicListID"
render={({ field }) => (
<Input
{...field}
label="网易云音乐歌单ID网页内音乐播放器"
placeholder="请输入歌单ID"
/>
)}
/>
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full"></div>
<Controller
control={control}
name="background"
render={({ field }) => <ImageInput {...field} />}
/>
</div>
<div className="flex flex-col gap-2">
<div></div>
{siteConfig.navItems.map((item) => (
<Controller
key={item.label}
control={control}
name={`customIcons.${item.label}`}
render={({ field }) => (
<ImageInput {...field} label={item.label} />
)}
/>
))}
</div>
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting}
refresh={onRefresh}
/>
</div>
</CardBody>
</Card>
</>
)
}
export default WebUIConfigCard

View File

@@ -0,0 +1,68 @@
import { Button } from '@heroui/button'
import clsx from 'clsx'
import { motion } from 'motion/react'
import { useEffect, useRef, useState } from 'react'
import { TbSquareRoundedChevronLeftFilled } from 'react-icons/tb'
import oneBotHttpApi from '@/const/ob_api'
import type { OneBotHttpApi } from '@/const/ob_api'
import OneBotApiDebug from '@/components/onebot/api/debug'
import OneBotApiNavList from '@/components/onebot/api/nav_list'
export default function HttpDebug() {
const [selectedApi, setSelectedApi] =
useState<keyof OneBotHttpApi>('/set_qq_profile')
const data = oneBotHttpApi[selectedApi]
const contentRef = useRef<HTMLDivElement>(null)
const [openSideBar, setOpenSideBar] = useState(true)
useEffect(() => {
contentRef?.current?.scrollTo?.({
top: 0,
behavior: 'smooth'
})
}, [selectedApi])
return (
<>
<title>HTTP调试 - NapCat WebUI</title>
<div className="w-full h-[calc(100%-3.6rem)] flex items-stretch">
<OneBotApiNavList
data={oneBotHttpApi}
selectedApi={selectedApi}
onSelect={setSelectedApi}
openSideBar={openSideBar}
/>
<div
ref={contentRef}
className="flex-1 h-full overflow-x-hidden relative"
>
<motion.div
className="sticky top-0 z-20 md:!ml-4"
animate={{ marginLeft: openSideBar ? '16rem' : '1rem' }}
transition={{ type: 'spring', stiffness: 150, damping: 15 }}
>
<Button
isIconOnly
color="danger"
radius="md"
variant="shadow"
size="sm"
onPress={() => setOpenSideBar(!openSideBar)}
>
<TbSquareRoundedChevronLeftFilled
size={24}
className={clsx(
'transition-transform',
openSideBar ? '' : 'transform rotate-180'
)}
/>
</Button>
</motion.div>
<OneBotApiDebug path={selectedApi} data={data} />
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,5 @@
import { Outlet } from 'react-router-dom'
export default function DebugPage() {
return <Outlet />
}

View File

@@ -0,0 +1,92 @@
import { Button } from '@heroui/button'
import { Card, CardBody } from '@heroui/card'
import { Input } from '@heroui/input'
import { useCallback, useState } from 'react'
import toast from 'react-hot-toast'
import ChatInputModal from '@/components/chat_input/modal'
import OneBotMessageList from '@/components/onebot/message_list'
import OneBotSendModal from '@/components/onebot/send_modal'
import WSStatus from '@/components/onebot/ws_status'
import { useWebSocketDebug } from '@/hooks/use-websocket-debug'
export default function WSDebug() {
const url = new URL(window.location.origin).href
const defaultWsUrl = url.replace('http', 'ws').replace(':6099', ':3000')
const [socketConfig, setSocketConfig] = useState({
url: defaultWsUrl,
token: ''
})
const [inputUrl, setInputUrl] = useState(socketConfig.url)
const [inputToken, setInputToken] = useState(socketConfig.token)
const { sendMessage, readyState, FilterMessagesType, filteredMessages } =
useWebSocketDebug(socketConfig.url, socketConfig.token)
const handleConnect = useCallback(() => {
if (!inputUrl.startsWith('ws://') && !inputUrl.startsWith('wss://')) {
toast.error('WebSocket URL 不合法')
return
}
setSocketConfig({
url: inputUrl,
token: inputToken
})
}, [inputUrl, inputToken])
return (
<>
<title>Websocket调试 - NapCat WebUI</title>
<div className="h-[calc(100vh-4rem)] overflow-hidden flex flex-col">
<Card className="mx-2 mt-2 flex-shrink-0 bg-opacity-50 backdrop-blur-sm">
<CardBody className="gap-2">
<div className="grid gap-2 items-center md:grid-cols-5">
<Input
className="col-span-2"
label="WebSocket URL"
type="text"
value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)}
placeholder="输入 WebSocket URL"
/>
<Input
className="col-span-2"
label="Token"
type="text"
value={inputToken}
onChange={(e) => setInputToken(e.target.value)}
placeholder="输入 Token"
/>
<div className="flex-shrink-0 flex gap-2 col-span-2 md:col-span-1">
<Button
color="danger"
onPress={handleConnect}
size="lg"
radius="full"
className="w-full md:w-auto"
>
</Button>
</div>
</div>
<div className="p-2 border border-default-100 bg-content1 bg-opacity-50 rounded-md dark:bg-[rgb(30,30,30)]">
<div className="grid gap-2 md:grid-cols-5 items-center md:w-fit">
<WSStatus state={readyState} />
<div className="md:w-64 max-w-full col-span-2">
{FilterMessagesType}
</div>
<OneBotSendModal sendMessage={sendMessage} />
<ChatInputModal />
</div>
</div>
</CardBody>
</Card>
<div className="flex-1 overflow-hidden">
<OneBotMessageList messages={filteredMessages} />
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,118 @@
import { Card, CardBody } from '@heroui/card'
import { useRequest } from 'ahooks'
import { useCallback, useEffect, useState } from 'react'
import { useRef } from 'react'
import toast from 'react-hot-toast'
import NetworkItemDisplay from '@/components/display_network_item'
import Hitokoto from '@/components/hitokoto'
import QQInfoCard from '@/components/qq_info_card'
import SystemInfo from '@/components/system_info'
import SystemStatusDisplay from '@/components/system_status_display'
import useConfig from '@/hooks/use-config'
import QQManager from '@/controllers/qq_manager'
import WebUIManager from '@/controllers/webui_manager'
const Networks: React.FC = () => {
const { config, refreshConfig } = useConfig()
const allNetWorkConfigLength =
config.network.httpClients.length +
config.network.websocketClients.length +
config.network.websocketServers.length +
config.network.httpServers.length
useEffect(() => {
refreshConfig()
}, [])
return (
<div className="grid grid-cols-8 md:grid-cols-3 lg:grid-cols-6 gap-y-2 gap-x-1 md:gap-y-4 md:gap-x-4 py-5">
<NetworkItemDisplay count={allNetWorkConfigLength} label="网络配置" />
<NetworkItemDisplay
count={config.network.httpServers.length}
label="HTTP服务器"
size="sm"
/>
<NetworkItemDisplay
count={config.network.httpClients.length}
label="HTTP客户端"
size="sm"
/>
<NetworkItemDisplay
count={config.network.websocketServers.length}
label="WS服务器"
size="sm"
/>
<NetworkItemDisplay
count={config.network.websocketClients.length}
label="WS客户端"
size="sm"
/>
</div>
)
}
const QQInfo: React.FC = () => {
const { data, loading, error } = useRequest(QQManager.getQQLoginInfo)
return <QQInfoCard data={data} error={error} loading={loading} />
}
export interface SystemStatusCardProps {
setArchInfo: (arch: string | undefined) => void
}
const SystemStatusCard: React.FC<SystemStatusCardProps> = ({ setArchInfo }) => {
const [systemStatus, setSystemStatus] = useState<SystemStatus>()
const isSetted = useRef(false)
const getStatus = useCallback(() => {
try {
const event = WebUIManager.getSystemStatus(setSystemStatus)
return event
} catch (error) {
toast.error('获取系统状态失败')
}
}, [])
useEffect(() => {
const close = getStatus()
return () => {
close?.close()
}
}, [getStatus])
useEffect(() => {
if (systemStatus?.arch && !isSetted.current) {
setArchInfo(systemStatus.arch)
isSetted.current = true
}
}, [systemStatus, setArchInfo])
return <SystemStatusDisplay data={systemStatus} />
}
const DashboardIndexPage: React.FC = () => {
const [archInfo, setArchInfo] = useState<string>()
return (
<>
<title> - NapCat WebUI</title>
<section className="w-full p-2 md:p-4 md:max-w-[1000px] mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 items-stretch">
<div className="flex flex-col gap-2">
<QQInfo />
<SystemInfo archInfo={archInfo} />
</div>
<SystemStatusCard setArchInfo={setArchInfo} />
</div>
<Networks />
<Card className="bg-opacity-60 shadow-sm shadow-danger-50">
<CardBody>
<Hitokoto />
</CardBody>
</Card>
</section>
</>
)
}
export default DashboardIndexPage

View File

@@ -0,0 +1,79 @@
import { Tab, Tabs } from '@heroui/tabs'
import { useRequest } from 'ahooks'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import HistoryLogs from '@/components/log_com/history'
import RealTimeLogs from '@/components/log_com/realtime'
import WebUIManager from '@/controllers/webui_manager'
export default function LogsPage() {
const {
data: logList,
loading: logListLoading,
error: logListError,
refresh: refreshLogList
} = useRequest(WebUIManager.getLogList)
const [selectedLog, setSelectedLog] = useState<string | null>(null)
const [logContent, setLogContent] = useState<string | null>(null)
const [logLoading, setLogLoading] = useState<boolean>(false)
const onLogSelect = (name: string) => {
setSelectedLog(name)
}
const onLoadLog = async () => {
if (!selectedLog) {
return
}
setLogLoading(true)
try {
const result = await WebUIManager.getLogContent(selectedLog)
setLogContent(result)
} catch (error) {
const msg = (error as Error).message
toast.error(`加载日志失败: ${msg}`)
} finally {
setLogLoading(false)
}
}
useEffect(() => {
if (logList && logList.length > 0) {
setSelectedLog(logList[0])
}
}, [logList])
useEffect(() => {
if (selectedLog) {
onLoadLog()
}
}, [selectedLog])
return (
<div className="h-[calc(100vh_-_8rem)] flex flex-col gap-4 items-center pt-4 px-2">
<Tabs
aria-label="Logs"
classNames={{
panel: 'w-full flex-1 h-full py-0 flex flex-col gap-4',
base: 'flex-shrink-0 !h-fit',
tabList: 'bg-opacity-50 backdrop-blur-sm',
cursor: 'bg-opacity-60 backdrop-blur-sm'
}}
>
<Tab title="实时日志">
<RealTimeLogs />
</Tab>
<Tab title="历史日志">
<HistoryLogs
list={logList || []}
onSelect={onLogSelect}
selectedLog={selectedLog || undefined}
refreshList={refreshLogList}
refreshLog={onLoadLog}
listLoading={logListLoading}
logLoading={logLoading}
listError={logListError}
logContent={logContent || undefined}
/>
</Tab>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,367 @@
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 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, 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 '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
]
.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 (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: '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>
</>
)
}