mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-15 04:40:34 +00:00
lint: napcat.webui
This commit is contained in:
parent
2889a9f8db
commit
d3c3721d8b
@ -1,33 +1,33 @@
|
|||||||
import { Suspense, lazy, useEffect } from 'react'
|
import { Suspense, lazy, useEffect } from 'react';
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux';
|
||||||
import { Route, Routes, useNavigate } from 'react-router-dom'
|
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import PageBackground from '@/components/page_background'
|
import PageBackground from '@/components/page_background';
|
||||||
import PageLoading from '@/components/page_loading'
|
import PageLoading from '@/components/page_loading';
|
||||||
import Toaster from '@/components/toaster'
|
import Toaster from '@/components/toaster';
|
||||||
|
|
||||||
import DialogProvider from '@/contexts/dialog'
|
import DialogProvider from '@/contexts/dialog';
|
||||||
import AudioProvider from '@/contexts/songs'
|
import AudioProvider from '@/contexts/songs';
|
||||||
|
|
||||||
import useAuth from '@/hooks/auth'
|
import useAuth from '@/hooks/auth';
|
||||||
|
|
||||||
import store from '@/store'
|
import store from '@/store';
|
||||||
|
|
||||||
const WebLoginPage = lazy(() => import('@/pages/web_login'))
|
const WebLoginPage = lazy(() => import('@/pages/web_login'));
|
||||||
const IndexPage = lazy(() => import('@/pages/index'))
|
const IndexPage = lazy(() => import('@/pages/index'));
|
||||||
const QQLoginPage = lazy(() => import('@/pages/qq_login'))
|
const QQLoginPage = lazy(() => import('@/pages/qq_login'));
|
||||||
const DashboardIndexPage = lazy(() => import('@/pages/dashboard'))
|
const DashboardIndexPage = lazy(() => import('@/pages/dashboard'));
|
||||||
const AboutPage = lazy(() => import('@/pages/dashboard/about'))
|
const AboutPage = lazy(() => import('@/pages/dashboard/about'));
|
||||||
const ConfigPage = lazy(() => import('@/pages/dashboard/config'))
|
const ConfigPage = lazy(() => import('@/pages/dashboard/config'));
|
||||||
const DebugPage = lazy(() => import('@/pages/dashboard/debug'))
|
const DebugPage = lazy(() => import('@/pages/dashboard/debug'));
|
||||||
const HttpDebug = lazy(() => import('@/pages/dashboard/debug/http'))
|
const HttpDebug = lazy(() => import('@/pages/dashboard/debug/http'));
|
||||||
const WSDebug = lazy(() => import('@/pages/dashboard/debug/websocket'))
|
const WSDebug = lazy(() => import('@/pages/dashboard/debug/websocket'));
|
||||||
const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'))
|
const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'));
|
||||||
const LogsPage = lazy(() => import('@/pages/dashboard/logs'))
|
const LogsPage = lazy(() => import('@/pages/dashboard/logs'));
|
||||||
const NetworkPage = lazy(() => import('@/pages/dashboard/network'))
|
const NetworkPage = lazy(() => import('@/pages/dashboard/network'));
|
||||||
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'))
|
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'));
|
||||||
|
|
||||||
function App() {
|
function App () {
|
||||||
return (
|
return (
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
@ -42,49 +42,49 @@ function App() {
|
|||||||
</AudioProvider>
|
</AudioProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthChecker({ children }: { children: React.ReactNode }) {
|
function AuthChecker ({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuth } = useAuth()
|
const { isAuth } = useAuth();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuth) {
|
if (!isAuth) {
|
||||||
const search = new URLSearchParams(window.location.search)
|
const search = new URLSearchParams(window.location.search);
|
||||||
const token = search.get('token')
|
const token = search.get('token');
|
||||||
let url = '/web_login'
|
let url = '/web_login';
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
url += `?token=${token}`
|
url += `?token=${token}`;
|
||||||
}
|
}
|
||||||
navigate(url, { replace: true })
|
navigate(url, { replace: true });
|
||||||
}
|
}
|
||||||
}, [isAuth, navigate])
|
}, [isAuth, navigate]);
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes () {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<IndexPage />}>
|
<Route path='/' element={<IndexPage />}>
|
||||||
<Route index element={<DashboardIndexPage />} />
|
<Route index element={<DashboardIndexPage />} />
|
||||||
<Route path="network" element={<NetworkPage />} />
|
<Route path='network' element={<NetworkPage />} />
|
||||||
<Route path="config" element={<ConfigPage />} />
|
<Route path='config' element={<ConfigPage />} />
|
||||||
<Route path="logs" element={<LogsPage />} />
|
<Route path='logs' element={<LogsPage />} />
|
||||||
<Route path="debug" element={<DebugPage />}>
|
<Route path='debug' element={<DebugPage />}>
|
||||||
<Route path="ws" element={<WSDebug />} />
|
<Route path='ws' element={<WSDebug />} />
|
||||||
<Route path="http" element={<HttpDebug />} />
|
<Route path='http' element={<HttpDebug />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="file_manager" element={<FileManagerPage />} />
|
<Route path='file_manager' element={<FileManagerPage />} />
|
||||||
<Route path="terminal" element={<TerminalPage />} />
|
<Route path='terminal' element={<TerminalPage />} />
|
||||||
<Route path="about" element={<AboutPage />} />
|
<Route path='about' element={<AboutPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/qq_login" element={<QQLoginPage />} />
|
<Route path='/qq_login' element={<QQLoginPage />} />
|
||||||
<Route path="/web_login" element={<WebLoginPage />} />
|
<Route path='/web_login' element={<WebLoginPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { ColorResult, SketchPicker } from 'react-color'
|
import { ColorResult, SketchPicker } from 'react-color';
|
||||||
|
|
||||||
// 假定 heroui 提供的 Popover组件
|
// 假定 heroui 提供的 Popover组件
|
||||||
|
|
||||||
@ -11,14 +11,14 @@ interface ColorPickerProps {
|
|||||||
|
|
||||||
const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
|
const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
|
||||||
const handleChange = (colorResult: ColorResult) => {
|
const handleChange = (colorResult: ColorResult) => {
|
||||||
onChange(colorResult)
|
onChange(colorResult);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover triggerScaleOnOpen={false}>
|
<Popover triggerScaleOnOpen={false}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<div
|
<div
|
||||||
className="w-36 h-8 rounded-md cursor-pointer border border-content4"
|
className='w-36 h-8 rounded-md cursor-pointer border border-content4'
|
||||||
style={{ background: color }}
|
style={{ background: color }}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@ -26,11 +26,11 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
|
|||||||
<SketchPicker
|
<SketchPicker
|
||||||
color={color}
|
color={color}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="!bg-transparent !shadow-none"
|
className='!bg-transparent !shadow-none'
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ColorPicker
|
export default ColorPicker;
|
||||||
|
|||||||
@ -1,30 +1,30 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Card, CardBody, CardHeader } from '@heroui/card'
|
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Slider } from '@heroui/slider'
|
import { Slider } from '@heroui/slider';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BiSolidSkipNextCircle,
|
BiSolidSkipNextCircle,
|
||||||
BiSolidSkipPreviousCircle
|
BiSolidSkipPreviousCircle,
|
||||||
} from 'react-icons/bi'
|
} from 'react-icons/bi';
|
||||||
import {
|
import {
|
||||||
FaPause,
|
FaPause,
|
||||||
FaPlay,
|
FaPlay,
|
||||||
FaRegHandPointRight,
|
FaRegHandPointRight,
|
||||||
FaRepeat,
|
FaRepeat,
|
||||||
FaShuffle
|
FaShuffle,
|
||||||
} from 'react-icons/fa6'
|
} from 'react-icons/fa6';
|
||||||
import { TbRepeatOnce } from 'react-icons/tb'
|
import { TbRepeatOnce } from 'react-icons/tb';
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive';
|
||||||
|
|
||||||
import { PlayMode } from '@/const/enum'
|
import { PlayMode } from '@/const/enum';
|
||||||
import key from '@/const/key'
|
import key from '@/const/key';
|
||||||
|
|
||||||
import { VolumeHighIcon, VolumeLowIcon } from './icons'
|
import { VolumeHighIcon, VolumeLowIcon } from './icons';
|
||||||
|
|
||||||
export interface AudioPlayerProps
|
export interface AudioPlayerProps
|
||||||
extends React.AudioHTMLAttributes<HTMLAudioElement> {
|
extends React.AudioHTMLAttributes<HTMLAudioElement> {
|
||||||
@ -39,7 +39,7 @@ export interface AudioPlayerProps
|
|||||||
mode?: PlayMode
|
mode?: PlayMode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AudioPlayer(props: AudioPlayerProps) {
|
export default function AudioPlayer (props: AudioPlayerProps) {
|
||||||
const {
|
const {
|
||||||
src,
|
src,
|
||||||
pressNext,
|
pressNext,
|
||||||
@ -56,116 +56,116 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
autoPlay,
|
autoPlay,
|
||||||
mode = PlayMode.Loop,
|
mode = PlayMode.Loop,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props;
|
||||||
|
|
||||||
const [currentTime, setCurrentTime] = useState(0)
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [duration, setDuration] = useState(0)
|
const [duration, setDuration] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [volume, setVolume] = useState(100)
|
const [volume, setVolume] = useState(100);
|
||||||
const [isCollapsed, setIsCollapsed] = useLocalStorage(
|
const [isCollapsed, setIsCollapsed] = useLocalStorage(
|
||||||
key.isCollapsedMusicPlayer,
|
key.isCollapsedMusicPlayer,
|
||||||
false
|
false
|
||||||
)
|
);
|
||||||
const audioRef = useRef<HTMLAudioElement>(null)
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const cardRef = useRef<HTMLDivElement>(null)
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
const startY = useRef(0)
|
const startY = useRef(0);
|
||||||
const startX = useRef(0)
|
const startX = useRef(0);
|
||||||
const [translateY, setTranslateY] = useState(0)
|
const [translateY, setTranslateY] = useState(0);
|
||||||
const [translateX, setTranslateX] = useState(0)
|
const [translateX, setTranslateX] = useState(0);
|
||||||
const isSmallScreen = useMediaQuery({ maxWidth: 767 })
|
const isSmallScreen = useMediaQuery({ maxWidth: 767 });
|
||||||
const isMediumUp = useMediaQuery({ minWidth: 768 })
|
const isMediumUp = useMediaQuery({ minWidth: 768 });
|
||||||
const shouldAdd = useRef(false)
|
const shouldAdd = useRef(false);
|
||||||
const currentProgress = (currentTime / duration) * 100
|
const currentProgress = (currentTime / duration) * 100;
|
||||||
const [storageAutoPlay, setStorageAutoPlay] = useLocalStorage(
|
const [storageAutoPlay, setStorageAutoPlay] = useLocalStorage(
|
||||||
key.autoPlay,
|
key.autoPlay,
|
||||||
true
|
true
|
||||||
)
|
);
|
||||||
|
|
||||||
const handleTimeUpdate = (event: React.SyntheticEvent<HTMLAudioElement>) => {
|
const handleTimeUpdate = (event: React.SyntheticEvent<HTMLAudioElement>) => {
|
||||||
const audio = event.target as HTMLAudioElement
|
const audio = event.target as HTMLAudioElement;
|
||||||
setCurrentTime(audio.currentTime)
|
setCurrentTime(audio.currentTime);
|
||||||
onTimeUpdate?.(event)
|
onTimeUpdate?.(event);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleLoadedData = (event: React.SyntheticEvent<HTMLAudioElement>) => {
|
const handleLoadedData = (event: React.SyntheticEvent<HTMLAudioElement>) => {
|
||||||
const audio = event.target as HTMLAudioElement
|
const audio = event.target as HTMLAudioElement;
|
||||||
setDuration(audio.duration)
|
setDuration(audio.duration);
|
||||||
onLoadedData?.(event)
|
onLoadedData?.(event);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlePlay = (e: React.SyntheticEvent<HTMLAudioElement>) => {
|
const handlePlay = (e: React.SyntheticEvent<HTMLAudioElement>) => {
|
||||||
setIsPlaying(true)
|
setIsPlaying(true);
|
||||||
setStorageAutoPlay(true)
|
setStorageAutoPlay(true);
|
||||||
onPlay?.(e)
|
onPlay?.(e);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlePause = (e: React.SyntheticEvent<HTMLAudioElement>) => {
|
const handlePause = (e: React.SyntheticEvent<HTMLAudioElement>) => {
|
||||||
setIsPlaying(false)
|
setIsPlaying(false);
|
||||||
onPause?.(e)
|
onPause?.(e);
|
||||||
}
|
};
|
||||||
|
|
||||||
const changeMode = () => {
|
const changeMode = () => {
|
||||||
const modes = [PlayMode.Loop, PlayMode.Random, PlayMode.Single]
|
const modes = [PlayMode.Loop, PlayMode.Random, PlayMode.Single];
|
||||||
const currentIndex = modes.findIndex((_mode) => _mode === mode)
|
const currentIndex = modes.findIndex((_mode) => _mode === mode);
|
||||||
const nextIndex = currentIndex + 1
|
const nextIndex = currentIndex + 1;
|
||||||
const nextMode = modes[nextIndex] || modes[0]
|
const nextMode = modes[nextIndex] || modes[0];
|
||||||
onChangeMode?.(nextMode)
|
onChangeMode?.(nextMode);
|
||||||
}
|
};
|
||||||
|
|
||||||
const volumeChange = (value: number) => {
|
const volumeChange = (value: number) => {
|
||||||
setVolume(value)
|
setVolume(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = audioRef.current
|
const audio = audioRef.current;
|
||||||
if (audio) {
|
if (audio) {
|
||||||
audio.volume = volume / 100
|
audio.volume = volume / 100;
|
||||||
}
|
}
|
||||||
}, [volume])
|
}, [volume]);
|
||||||
|
|
||||||
const handleTouchStart = (e: React.TouchEvent) => {
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
startY.current = e.touches[0].clientY
|
startY.current = e.touches[0].clientY;
|
||||||
startX.current = e.touches[0].clientX
|
startX.current = e.touches[0].clientX;
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleTouchMove = (e: React.TouchEvent) => {
|
const handleTouchMove = (e: React.TouchEvent) => {
|
||||||
const deltaY = e.touches[0].clientY - startY.current
|
const deltaY = e.touches[0].clientY - startY.current;
|
||||||
const deltaX = e.touches[0].clientX - startX.current
|
const deltaX = e.touches[0].clientX - startX.current;
|
||||||
const container = cardRef.current
|
const container = cardRef.current;
|
||||||
const header = cardRef.current?.querySelector('[data-header]')
|
const header = cardRef.current?.querySelector('[data-header]');
|
||||||
const headerHeight = header?.clientHeight || 20
|
const headerHeight = header?.clientHeight || 20;
|
||||||
const addHeight = (container?.clientHeight || headerHeight) - headerHeight
|
const addHeight = (container?.clientHeight || headerHeight) - headerHeight;
|
||||||
const _shouldAdd = isCollapsed && deltaY < 0
|
const _shouldAdd = isCollapsed && deltaY < 0;
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
shouldAdd.current = _shouldAdd
|
shouldAdd.current = _shouldAdd;
|
||||||
setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY)
|
setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY);
|
||||||
} else {
|
} else {
|
||||||
setTranslateX(deltaX)
|
setTranslateX(deltaX);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
const handleTouchEnd = () => {
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
const container = cardRef.current
|
const container = cardRef.current;
|
||||||
const header = cardRef.current?.querySelector('[data-header]')
|
const header = cardRef.current?.querySelector('[data-header]');
|
||||||
const headerHeight = header?.clientHeight || 20
|
const headerHeight = header?.clientHeight || 20;
|
||||||
const addHeight = (container?.clientHeight || headerHeight) - headerHeight
|
const addHeight = (container?.clientHeight || headerHeight) - headerHeight;
|
||||||
const _translateY = translateY - (shouldAdd.current ? addHeight : 0)
|
const _translateY = translateY - (shouldAdd.current ? addHeight : 0);
|
||||||
if (_translateY > 100) {
|
if (_translateY > 100) {
|
||||||
setIsCollapsed(true)
|
setIsCollapsed(true);
|
||||||
} else if (_translateY < -100) {
|
} else if (_translateY < -100) {
|
||||||
setIsCollapsed(false)
|
setIsCollapsed(false);
|
||||||
}
|
}
|
||||||
setTranslateY(0)
|
setTranslateY(0);
|
||||||
} else {
|
} else {
|
||||||
if (translateX > 100) {
|
if (translateX > 100) {
|
||||||
setIsCollapsed(true)
|
setIsCollapsed(true);
|
||||||
} else if (translateX < -100) {
|
} else if (translateX < -100) {
|
||||||
setIsCollapsed(false)
|
setIsCollapsed(false);
|
||||||
}
|
}
|
||||||
setTranslateX(0)
|
setTranslateX(0);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const dragTranslate = isSmallScreen
|
const dragTranslate = isSmallScreen
|
||||||
? translateY
|
? translateY
|
||||||
@ -173,16 +173,16 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
: ''
|
: ''
|
||||||
: translateX
|
: translateX
|
||||||
? `translateX(${translateX}px)`
|
? `translateX(${translateX}px)`
|
||||||
: ''
|
: '';
|
||||||
const collapsedTranslate = isCollapsed
|
const collapsedTranslate = isCollapsed
|
||||||
? isSmallScreen
|
? isSmallScreen
|
||||||
? 'translateY(90%)'
|
? 'translateY(90%)'
|
||||||
: 'translateX(96%)'
|
: 'translateX(96%)'
|
||||||
: ''
|
: '';
|
||||||
|
|
||||||
const translateStyle = dragTranslate || collapsedTranslate
|
const translateStyle = dragTranslate || collapsedTranslate;
|
||||||
|
|
||||||
if (!src) return null
|
if (!src) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -192,7 +192,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
isCollapsed && 'md:hover:!translate-x-80'
|
isCollapsed && 'md:hover:!translate-x-80'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
transform: translateStyle
|
transform: translateStyle,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<audio
|
<audio
|
||||||
@ -216,10 +216,10 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
isSmallScreen ? 'rounded-t-3xl' : 'md:rounded-l-xl'
|
isSmallScreen ? 'rounded-t-3xl' : 'md:rounded-l-xl'
|
||||||
)}
|
)}
|
||||||
classNames={{
|
classNames={{
|
||||||
body: 'p-0'
|
body: 'p-0',
|
||||||
}}
|
}}
|
||||||
shadow="sm"
|
shadow='sm'
|
||||||
radius="none"
|
radius='none'
|
||||||
>
|
>
|
||||||
{isMediumUp && (
|
{isMediumUp && (
|
||||||
<Button
|
<Button
|
||||||
@ -230,9 +230,9 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
? 'top-0 left-0 w-full h-full rounded-xl bg-opacity-0 hover:bg-opacity-30'
|
? 'top-0 left-0 w-full h-full rounded-xl bg-opacity-0 hover:bg-opacity-30'
|
||||||
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
|
: 'top-3 -left-8 rounded-l-full bg-opacity-50 backdrop-blur-md'
|
||||||
)}
|
)}
|
||||||
variant="solid"
|
variant='solid'
|
||||||
color="primary"
|
color='primary'
|
||||||
size="sm"
|
size='sm'
|
||||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||||
>
|
>
|
||||||
<FaRegHandPointRight />
|
<FaRegHandPointRight />
|
||||||
@ -241,65 +241,65 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
{isSmallScreen && (
|
{isSmallScreen && (
|
||||||
<CardHeader
|
<CardHeader
|
||||||
data-header
|
data-header
|
||||||
className="flex-row justify-center pt-4"
|
className='flex-row justify-center pt-4'
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={handleTouchStart}
|
||||||
onTouchMove={handleTouchMove}
|
onTouchMove={handleTouchMove}
|
||||||
onTouchEnd={handleTouchEnd}
|
onTouchEnd={handleTouchEnd}
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
>
|
>
|
||||||
<div className="w-24 h-2 rounded-full bg-content2-foreground shadow-sm"></div>
|
<div className='w-24 h-2 rounded-full bg-content2-foreground shadow-sm' />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
)}
|
)}
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div className="grid grid-cols-6 md:grid-cols-12 gap-6 md:gap-4 items-center justify-center overflow-hidden p-6 md:p-2 m-0">
|
<div className='grid grid-cols-6 md:grid-cols-12 gap-6 md:gap-4 items-center justify-center overflow-hidden p-6 md:p-2 m-0'>
|
||||||
<div className="relative col-span-6 md:col-span-4 flex justify-center">
|
<div className='relative col-span-6 md:col-span-4 flex justify-center'>
|
||||||
<Image
|
<Image
|
||||||
alt="Album cover"
|
alt='Album cover'
|
||||||
className="object-cover"
|
className='object-cover'
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'w-36 aspect-square md:w-24 flex',
|
wrapper: 'w-36 aspect-square md:w-24 flex',
|
||||||
img: 'block w-full h-full'
|
img: 'block w-full h-full',
|
||||||
}}
|
}}
|
||||||
shadow="md"
|
shadow='md'
|
||||||
src={cover}
|
src={cover}
|
||||||
width="100%"
|
width='100%'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col col-span-6 md:col-span-8">
|
<div className='flex flex-col col-span-6 md:col-span-8'>
|
||||||
<div className="flex flex-col gap-0">
|
<div className='flex flex-col gap-0'>
|
||||||
<h1 className="font-medium truncate">{title}</h1>
|
<h1 className='font-medium truncate'>{title}</h1>
|
||||||
<p className="text-xs text-foreground/80 truncate">{artist}</p>
|
<p className='text-xs text-foreground/80 truncate'>{artist}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<Slider
|
<Slider
|
||||||
aria-label="Music progress"
|
aria-label='Music progress'
|
||||||
classNames={{
|
classNames={{
|
||||||
track: 'bg-default-500/30 border-none',
|
track: 'bg-default-500/30 border-none',
|
||||||
thumb: 'w-2 h-2 after:w-1.5 after:h-1.5',
|
thumb: 'w-2 h-2 after:w-1.5 after:h-1.5',
|
||||||
filler: 'rounded-full'
|
filler: 'rounded-full',
|
||||||
}}
|
}}
|
||||||
color="foreground"
|
color='foreground'
|
||||||
value={currentProgress || 0}
|
value={currentProgress || 0}
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
size="sm"
|
size='sm'
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
value = Array.isArray(value) ? value[0] : value
|
value = Array.isArray(value) ? value[0] : value;
|
||||||
const audio = audioRef.current
|
const audio = audioRef.current;
|
||||||
if (audio) {
|
if (audio) {
|
||||||
audio.currentTime = (value / 100) * duration
|
audio.currentTime = (value / 100) * duration;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between h-3">
|
<div className='flex justify-between h-3'>
|
||||||
<p className="text-xs">
|
<p className='text-xs'>
|
||||||
{Math.floor(currentTime / 60)}:
|
{Math.floor(currentTime / 60)}:
|
||||||
{Math.floor(currentTime % 60)
|
{Math.floor(currentTime % 60)
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}
|
.padStart(2, '0')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-foreground/50">
|
<p className='text-xs text-foreground/50'>
|
||||||
{Math.floor(duration / 60)}:
|
{Math.floor(duration / 60)}:
|
||||||
{Math.floor(duration % 60)
|
{Math.floor(duration % 60)
|
||||||
.toString()
|
.toString()
|
||||||
@ -308,7 +308,7 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-center">
|
<div className='flex w-full items-center justify-center'>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
mode === PlayMode.Loop
|
mode === PlayMode.Loop
|
||||||
@ -320,30 +320,30 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="data-[hover]:bg-foreground/10 text-lg md:text-medium"
|
className='data-[hover]:bg-foreground/10 text-lg md:text-medium'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="light"
|
variant='light'
|
||||||
size="md"
|
size='md'
|
||||||
onPress={changeMode}
|
onPress={changeMode}
|
||||||
>
|
>
|
||||||
{mode === PlayMode.Loop && (
|
{mode === PlayMode.Loop && (
|
||||||
<FaRepeat className="text-foreground/80" />
|
<FaRepeat className='text-foreground/80' />
|
||||||
)}
|
)}
|
||||||
{mode === PlayMode.Random && (
|
{mode === PlayMode.Random && (
|
||||||
<FaShuffle className="text-foreground/80" />
|
<FaShuffle className='text-foreground/80' />
|
||||||
)}
|
)}
|
||||||
{mode === PlayMode.Single && (
|
{mode === PlayMode.Single && (
|
||||||
<TbRepeatOnce className="text-foreground/80 text-xl" />
|
<TbRepeatOnce className='text-foreground/80 text-xl' />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="上一首">
|
<Tooltip content='上一首'>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="data-[hover]:bg-foreground/10 text-2xl md:text-xl"
|
className='data-[hover]:bg-foreground/10 text-2xl md:text-xl'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="light"
|
variant='light'
|
||||||
size="md"
|
size='md'
|
||||||
onPress={pressPrevious}
|
onPress={pressPrevious}
|
||||||
>
|
>
|
||||||
<BiSolidSkipPreviousCircle />
|
<BiSolidSkipPreviousCircle />
|
||||||
@ -352,66 +352,66 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
<Tooltip content={isPlaying ? '暂停' : '播放'}>
|
<Tooltip content={isPlaying ? '暂停' : '播放'}>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="data-[hover]:bg-foreground/10 text-3xl md:text-3xl"
|
className='data-[hover]:bg-foreground/10 text-3xl md:text-3xl'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="light"
|
variant='light'
|
||||||
size="lg"
|
size='lg'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
audioRef.current?.pause()
|
audioRef.current?.pause();
|
||||||
setStorageAutoPlay(false)
|
setStorageAutoPlay(false);
|
||||||
} else {
|
} else {
|
||||||
audioRef.current?.play()
|
audioRef.current?.play();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isPlaying ? <FaPause /> : <FaPlay className="ml-1" />}
|
{isPlaying ? <FaPause /> : <FaPlay className='ml-1' />}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="下一首">
|
<Tooltip content='下一首'>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="data-[hover]:bg-foreground/10 text-2xl md:text-xl"
|
className='data-[hover]:bg-foreground/10 text-2xl md:text-xl'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="light"
|
variant='light'
|
||||||
size="md"
|
size='md'
|
||||||
onPress={pressNext}
|
onPress={pressNext}
|
||||||
>
|
>
|
||||||
<BiSolidSkipNextCircle />
|
<BiSolidSkipNextCircle />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover
|
<Popover
|
||||||
placement="top"
|
placement='top'
|
||||||
classNames={{
|
classNames={{
|
||||||
content: 'bg-opacity-30 backdrop-blur-md'
|
content: 'bg-opacity-30 backdrop-blur-md',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="data-[hover]:bg-foreground/10 text-xl md:text-xl"
|
className='data-[hover]:bg-foreground/10 text-xl md:text-xl'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="light"
|
variant='light'
|
||||||
size="md"
|
size='md'
|
||||||
>
|
>
|
||||||
<VolumeHighIcon />
|
<VolumeHighIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<Slider
|
<Slider
|
||||||
orientation="vertical"
|
orientation='vertical'
|
||||||
showTooltip
|
showTooltip
|
||||||
aria-label="Volume"
|
aria-label='Volume'
|
||||||
className="h-40"
|
className='h-40'
|
||||||
color="primary"
|
color='primary'
|
||||||
defaultValue={volume}
|
defaultValue={volume}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
value = Array.isArray(value) ? value[0] : value
|
value = Array.isArray(value) ? value[0] : value;
|
||||||
volumeChange(value)
|
volumeChange(value);
|
||||||
}}
|
}}
|
||||||
startContent={<VolumeHighIcon className="text-2xl" />}
|
startContent={<VolumeHighIcon className='text-2xl' />}
|
||||||
size="sm"
|
size='sm'
|
||||||
endContent={<VolumeLowIcon className="text-2xl" />}
|
endContent={<VolumeLowIcon className='text-2xl' />}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -421,5 +421,5 @@ export default function AudioPlayer(props: AudioPlayerProps) {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +1,87 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import {
|
import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownTrigger
|
DropdownTrigger,
|
||||||
} from '@heroui/dropdown'
|
} from '@heroui/dropdown';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { FaRegCircleQuestion } from 'react-icons/fa6'
|
import { FaRegCircleQuestion } from 'react-icons/fa6';
|
||||||
import { IoAddCircleOutline } from 'react-icons/io5'
|
import { IoAddCircleOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HTTPClientIcon,
|
HTTPClientIcon,
|
||||||
HTTPServerIcon,
|
HTTPServerIcon,
|
||||||
PCIcon,
|
PCIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
WebsocketIcon
|
WebsocketIcon,
|
||||||
} from '../icons'
|
} from '../icons';
|
||||||
|
|
||||||
export interface AddButtonProps {
|
export interface AddButtonProps {
|
||||||
onOpen: (key: keyof OneBotConfig['network']) => void
|
onOpen: (key: keyof OneBotConfig['network']) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddButton: React.FC<AddButtonProps> = (props) => {
|
const AddButton: React.FC<AddButtonProps> = (props) => {
|
||||||
const { onOpen } = props
|
const { onOpen } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
classNames={{
|
classNames={{
|
||||||
content: 'bg-opacity-30 backdrop-blur-md'
|
content: 'bg-opacity-30 backdrop-blur-md',
|
||||||
}}
|
}}
|
||||||
placement="right"
|
placement='right'
|
||||||
>
|
>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
startContent={<IoAddCircleOutline className="text-2xl" />}
|
startContent={<IoAddCircleOutline className='text-2xl' />}
|
||||||
>
|
>
|
||||||
新建
|
新建
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
aria-label="Create Network Config"
|
aria-label='Create Network Config'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onAction={(key) => {
|
onAction={(key) => {
|
||||||
onOpen(key as keyof OneBotConfig['network'])
|
onOpen(key as keyof OneBotConfig['network']);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="title"
|
key='title'
|
||||||
isReadOnly
|
isReadOnly
|
||||||
className="cursor-default hover:!bg-transparent"
|
className='cursor-default hover:!bg-transparent'
|
||||||
textValue="title"
|
textValue='title'
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className='flex items-center gap-2 justify-center'>
|
||||||
<div className="w-5 h-5 -ml-3">
|
<div className='w-5 h-5 -ml-3'>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-primary-400">新建网络配置</div>
|
<div className='text-primary-400'>新建网络配置</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="httpServers"
|
key='httpServers'
|
||||||
textValue="httpServers"
|
textValue='httpServers'
|
||||||
startContent={
|
startContent={
|
||||||
<div className="w-6 h-6">
|
<div className='w-6 h-6'>
|
||||||
<HTTPServerIcon />
|
<HTTPServerIcon />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1 items-center">
|
<div className='flex gap-1 items-center'>
|
||||||
HTTP服务器
|
HTTP服务器
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="「由NapCat建立」一个HTTP服务器,你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址,你或者你的框架应该连接到这个地址。"
|
content='「由NapCat建立」一个HTTP服务器,你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址,你或者你的框架应该连接到这个地址。'
|
||||||
showArrow
|
showArrow
|
||||||
className="max-w-64"
|
className='max-w-64'
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
size="sm"
|
size='sm'
|
||||||
variant="light"
|
variant='light'
|
||||||
className="w-4 h-4 min-w-0"
|
className='w-4 h-4 min-w-0'
|
||||||
>
|
>
|
||||||
<FaRegCircleQuestion />
|
<FaRegCircleQuestion />
|
||||||
</Button>
|
</Button>
|
||||||
@ -89,27 +89,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="httpSseServers"
|
key='httpSseServers'
|
||||||
textValue="httpSseServers"
|
textValue='httpSseServers'
|
||||||
startContent={
|
startContent={
|
||||||
<div className="w-6 h-6">
|
<div className='w-6 h-6'>
|
||||||
<HTTPServerIcon />
|
<HTTPServerIcon />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1 items-center">
|
<div className='flex gap-1 items-center'>
|
||||||
HTTP SSE服务器
|
HTTP SSE服务器
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="「由NapCat建立」一个HTTP SSE服务器,你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址,你或者你的框架应该连接到这个地址。"
|
content='「由NapCat建立」一个HTTP SSE服务器,你可以「使用框架连接」此服务器或者「自己构造请求发送」至此服务器。NapCat会根据你配置的IP和端口等建立一个地址,你或者你的框架应该连接到这个地址。'
|
||||||
showArrow
|
showArrow
|
||||||
className="max-w-64"
|
className='max-w-64'
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
size="sm"
|
size='sm'
|
||||||
variant="light"
|
variant='light'
|
||||||
className="w-4 h-4 min-w-0"
|
className='w-4 h-4 min-w-0'
|
||||||
>
|
>
|
||||||
<FaRegCircleQuestion />
|
<FaRegCircleQuestion />
|
||||||
</Button>
|
</Button>
|
||||||
@ -117,27 +117,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="httpClients"
|
key='httpClients'
|
||||||
textValue="httpClients"
|
textValue='httpClients'
|
||||||
startContent={
|
startContent={
|
||||||
<div className="w-6 h-6">
|
<div className='w-6 h-6'>
|
||||||
<HTTPClientIcon />
|
<HTTPClientIcon />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1 items-center">
|
<div className='flex gap-1 items-center'>
|
||||||
HTTP客户端
|
HTTP客户端
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="「由框架或者你自己建立」的一个用于「接收」NapCat向你发送请求的客户端,通常框架会提供一个HTTP地址。这个地址是你使用的框架提供的,NapCat会主动连接它。"
|
content='「由框架或者你自己建立」的一个用于「接收」NapCat向你发送请求的客户端,通常框架会提供一个HTTP地址。这个地址是你使用的框架提供的,NapCat会主动连接它。'
|
||||||
showArrow
|
showArrow
|
||||||
className="max-w-64"
|
className='max-w-64'
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
size="sm"
|
size='sm'
|
||||||
variant="light"
|
variant='light'
|
||||||
className="w-4 h-4 min-w-0"
|
className='w-4 h-4 min-w-0'
|
||||||
>
|
>
|
||||||
<FaRegCircleQuestion />
|
<FaRegCircleQuestion />
|
||||||
</Button>
|
</Button>
|
||||||
@ -145,27 +145,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="websocketServers"
|
key='websocketServers'
|
||||||
textValue="websocketServers"
|
textValue='websocketServers'
|
||||||
startContent={
|
startContent={
|
||||||
<div className="w-6 h-6">
|
<div className='w-6 h-6'>
|
||||||
<WebsocketIcon />
|
<WebsocketIcon />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1 items-center">
|
<div className='flex gap-1 items-center'>
|
||||||
Websocket服务器
|
Websocket服务器
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="「由NapCat建立」一个WebSocket服务器,你的框架应该连接到此服务器。NapCat会根据你配置的IP和端口等建立一个WebSocket地址,你或者你的框架应该连接到这个地址。"
|
content='「由NapCat建立」一个WebSocket服务器,你的框架应该连接到此服务器。NapCat会根据你配置的IP和端口等建立一个WebSocket地址,你或者你的框架应该连接到这个地址。'
|
||||||
showArrow
|
showArrow
|
||||||
className="max-w-64"
|
className='max-w-64'
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
size="sm"
|
size='sm'
|
||||||
variant="light"
|
variant='light'
|
||||||
className="w-4 h-4 min-w-0"
|
className='w-4 h-4 min-w-0'
|
||||||
>
|
>
|
||||||
<FaRegCircleQuestion />
|
<FaRegCircleQuestion />
|
||||||
</Button>
|
</Button>
|
||||||
@ -173,27 +173,27 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="websocketClients"
|
key='websocketClients'
|
||||||
textValue="websocketClients"
|
textValue='websocketClients'
|
||||||
startContent={
|
startContent={
|
||||||
<div className="w-6 h-6">
|
<div className='w-6 h-6'>
|
||||||
<PCIcon />
|
<PCIcon />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1 items-center">
|
<div className='flex gap-1 items-center'>
|
||||||
Websocket客户端
|
Websocket客户端
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="「由框架或者你自己建立」的WebSocket,通常框架会「提供」一个ws地址,NapCat会主动连接它。"
|
content='「由框架或者你自己建立」的WebSocket,通常框架会「提供」一个ws地址,NapCat会主动连接它。'
|
||||||
showArrow
|
showArrow
|
||||||
className="max-w-64"
|
className='max-w-64'
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
size="sm"
|
size='sm'
|
||||||
variant="light"
|
variant='light'
|
||||||
className="w-4 h-4 min-w-0"
|
className='w-4 h-4 min-w-0'
|
||||||
>
|
>
|
||||||
<FaRegCircleQuestion />
|
<FaRegCircleQuestion />
|
||||||
</Button>
|
</Button>
|
||||||
@ -202,7 +202,7 @@ const AddButton: React.FC<AddButtonProps> = (props) => {
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AddButton
|
export default AddButton;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io';
|
||||||
|
|
||||||
export interface SaveButtonsProps {
|
export interface SaveButtonsProps {
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
@ -16,7 +16,7 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
|||||||
reset,
|
reset,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
refresh,
|
refresh,
|
||||||
className
|
className,
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -24,18 +24,18 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2 mt-5">
|
<div className='flex items-center justify-center gap-2 mt-5'>
|
||||||
<Button
|
<Button
|
||||||
color="default"
|
color='default'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
reset()
|
reset();
|
||||||
toast.success('重置成功')
|
toast.success('重置成功');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
取消更改
|
取消更改
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
onPress={() => onSubmit()}
|
onPress={() => onSubmit()}
|
||||||
>
|
>
|
||||||
@ -44,9 +44,9 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
|||||||
{refresh && (
|
{refresh && (
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="secondary"
|
color='secondary'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => refresh()}
|
onPress={() => refresh()}
|
||||||
>
|
>
|
||||||
<IoMdRefresh size={24} />
|
<IoMdRefresh size={24} />
|
||||||
@ -54,6 +54,6 @@ const SaveButtons: React.FC<SaveButtonsProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default SaveButtons
|
export default SaveButtons;
|
||||||
|
|||||||
@ -1,170 +1,170 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { FaMicrophone } from 'react-icons/fa6'
|
import { FaMicrophone } from 'react-icons/fa6';
|
||||||
import { IoMic } from 'react-icons/io5'
|
import { IoMic } from 'react-icons/io5';
|
||||||
import { MdEdit, MdUpload } from 'react-icons/md'
|
import { MdEdit, MdUpload } from 'react-icons/md';
|
||||||
|
|
||||||
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
|
||||||
|
|
||||||
import { isURI } from '@/utils/url'
|
import { isURI } from '@/utils/url';
|
||||||
|
|
||||||
import type { OB11Segment } from '@/types/onebot'
|
import type { OB11Segment } from '@/types/onebot';
|
||||||
|
|
||||||
const AudioInsert = () => {
|
const AudioInsert = () => {
|
||||||
const [audioUrl, setAudioUrl] = useState<string>('')
|
const [audioUrl, setAudioUrl] = useState<string>('');
|
||||||
const audioInputRef = useRef<HTMLInputElement>(null)
|
const audioInputRef = useRef<HTMLInputElement>(null);
|
||||||
const showStructuredMessage = useShowStructuredMessage()
|
const showStructuredMessage = useShowStructuredMessage();
|
||||||
const showAudioSegment = (file: string) => {
|
const showAudioSegment = (file: string) => {
|
||||||
const messages: OB11Segment[] = [
|
const messages: OB11Segment[] = [
|
||||||
{
|
{
|
||||||
type: 'record',
|
type: 'record',
|
||||||
data: {
|
data: {
|
||||||
file: file
|
file,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
showStructuredMessage(messages)
|
showStructuredMessage(messages);
|
||||||
}
|
};
|
||||||
|
|
||||||
const [isRecording, setIsRecording] = useState(false)
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||||
const audioChunksRef = useRef<Blob[]>([])
|
const audioChunksRef = useRef<Blob[]>([]);
|
||||||
const [audioPreview, setAudioPreview] = useState<string | null>(null)
|
const [audioPreview, setAudioPreview] = useState<string | null>(null);
|
||||||
const [showPreview, setShowPreview] = useState(false)
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
const streamRef = useRef<MediaStream | null>(null)
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
const [recordingTime, setRecordingTime] = useState(0)
|
const [recordingTime, setRecordingTime] = useState(0);
|
||||||
const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
|
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
|
||||||
streamRef.current = stream
|
streamRef.current = stream;
|
||||||
const recorder = new MediaRecorder(stream)
|
const recorder = new MediaRecorder(stream);
|
||||||
mediaRecorderRef.current = recorder
|
mediaRecorderRef.current = recorder;
|
||||||
recorder.start()
|
recorder.start();
|
||||||
recorder.ondataavailable = (event) => {
|
recorder.ondataavailable = (event) => {
|
||||||
if (event.data.size > 0) {
|
if (event.data.size > 0) {
|
||||||
audioChunksRef.current.push(event.data)
|
audioChunksRef.current.push(event.data);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
recorder.onstop = () => {
|
recorder.onstop = () => {
|
||||||
if (audioChunksRef.current.length > 0) {
|
if (audioChunksRef.current.length > 0) {
|
||||||
const audioBlob = new Blob(audioChunksRef.current, {
|
const audioBlob = new Blob(audioChunksRef.current, {
|
||||||
type: 'audio/wav'
|
type: 'audio/wav',
|
||||||
})
|
});
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(audioBlob)
|
reader.readAsDataURL(audioBlob);
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
const base64Audio = reader.result as string
|
const base64Audio = reader.result as string;
|
||||||
setAudioPreview(base64Audio)
|
setAudioPreview(base64Audio);
|
||||||
setShowPreview(true)
|
setShowPreview(true);
|
||||||
}
|
};
|
||||||
audioChunksRef.current = []
|
audioChunksRef.current = [];
|
||||||
}
|
}
|
||||||
stream.getTracks().forEach((track) => track.stop())
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
recordingIntervalRef.current = setInterval(() => {
|
recordingIntervalRef.current = setInterval(() => {
|
||||||
setRecordingTime((prevTime) => prevTime + 1)
|
setRecordingTime((prevTime) => prevTime + 1);
|
||||||
}, 1000)
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
mediaRecorderRef.current?.stop()
|
mediaRecorderRef.current?.stop();
|
||||||
if (recordingIntervalRef.current) {
|
if (recordingIntervalRef.current) {
|
||||||
clearInterval(recordingIntervalRef.current)
|
clearInterval(recordingIntervalRef.current);
|
||||||
recordingIntervalRef.current = null
|
recordingIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isRecording])
|
}, [isRecording]);
|
||||||
|
|
||||||
const startRecording = () => {
|
const startRecording = () => {
|
||||||
setAudioPreview(null)
|
setAudioPreview(null);
|
||||||
setShowPreview(false)
|
setShowPreview(false);
|
||||||
setRecordingTime(0)
|
setRecordingTime(0);
|
||||||
setIsRecording(true)
|
setIsRecording(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const stopRecording = () => {
|
const stopRecording = () => {
|
||||||
setIsRecording(false)
|
setIsRecording(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleShowPreview = () => {
|
const handleShowPreview = () => {
|
||||||
if (audioPreview) {
|
if (audioPreview) {
|
||||||
showAudioSegment(audioPreview)
|
showAudioSegment(audioPreview);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const formatTime = (time: number) => {
|
const formatTime = (time: number) => {
|
||||||
const minutes = Math.floor(time / 60)
|
const minutes = Math.floor(time / 60);
|
||||||
const seconds = time % 60
|
const seconds = time % 60;
|
||||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="发送音频">
|
<Tooltip content='发送音频'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
<Button color='primary' variant='flat' isIconOnly radius='full'>
|
||||||
<IoMic className="text-xl" />
|
<IoMic className='text-xl' />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-2 p-4">
|
<PopoverContent className='flex-row gap-2 p-4'>
|
||||||
<Tooltip content="上传音频">
|
<Tooltip content='上传音频'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
audioInputRef?.current?.click()
|
audioInputRef?.current?.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MdUpload />
|
<MdUpload />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="输入音频地址">
|
<Tooltip content='输入音频地址'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger tooltip="输入音频地址">
|
<PopoverTrigger tooltip='输入音频地址'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
<MdEdit />
|
<MdEdit />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-1 p-2">
|
<PopoverContent className='flex-row gap-1 p-2'>
|
||||||
<Input
|
<Input
|
||||||
value={audioUrl}
|
value={audioUrl}
|
||||||
onChange={(e) => setAudioUrl(e.target.value)}
|
onChange={(e) => setAudioUrl(e.target.value)}
|
||||||
placeholder="请输入音频地址"
|
placeholder='请输入音频地址'
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!isURI(audioUrl)) {
|
if (!isURI(audioUrl)) {
|
||||||
toast.error('请输入正确的音频地址')
|
toast.error('请输入正确的音频地址');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
showAudioSegment(audioUrl)
|
showAudioSegment(audioUrl);
|
||||||
setAudioUrl('')
|
setAudioUrl('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaMicrophone />
|
<FaMicrophone />
|
||||||
@ -172,34 +172,34 @@ const AudioInsert = () => {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="录制音频">
|
<Tooltip content='录制音频'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
<IoMic />
|
<IoMic />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-col gap-2 p-4">
|
<PopoverContent className='flex-col gap-2 p-4'>
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
color={isRecording ? 'primary' : 'primary'}
|
color={isRecording ? 'primary' : 'primary'}
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={isRecording ? stopRecording : startRecording}
|
onPress={isRecording ? stopRecording : startRecording}
|
||||||
>
|
>
|
||||||
{isRecording ? '停止录制' : '开始录制'}
|
{isRecording ? '停止录制' : '开始录制'}
|
||||||
</Button>
|
</Button>
|
||||||
{showPreview && audioPreview && (
|
{showPreview && audioPreview && (
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={handleShowPreview}
|
onPress={handleShowPreview}
|
||||||
>
|
>
|
||||||
查看消息
|
查看消息
|
||||||
@ -207,7 +207,7 @@ const AudioInsert = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(isRecording || audioPreview) && (
|
{(isRecording || audioPreview) && (
|
||||||
<div className="flex gap-1 items-center">
|
<div className='flex gap-1 items-center'>
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4 rounded-full',
|
'w-4 h-4 rounded-full',
|
||||||
@ -215,7 +215,7 @@ const AudioInsert = () => {
|
|||||||
? 'animate-pulse bg-primary-400'
|
? 'animate-pulse bg-primary-400'
|
||||||
: 'bg-success-400'
|
: 'bg-success-400'
|
||||||
)}
|
)}
|
||||||
></span>
|
/>
|
||||||
<span>录制时长: {formatTime(recordingTime)}</span>
|
<span>录制时长: {formatTime(recordingTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -228,27 +228,27 @@ const AudioInsert = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type='file'
|
||||||
ref={audioInputRef}
|
ref={audioInputRef}
|
||||||
hidden
|
hidden
|
||||||
accept="audio/*"
|
accept='audio/*'
|
||||||
className="hidden"
|
className='hidden'
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const dataURL = event.target?.result
|
const dataURL = event.target?.result;
|
||||||
showAudioSegment(dataURL as string)
|
showAudioSegment(dataURL as string);
|
||||||
e.target.value = ''
|
e.target.value = '';
|
||||||
}
|
};
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AudioInsert
|
export default AudioInsert;
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { BsDice3Fill } from 'react-icons/bs'
|
import { BsDice3Fill } from 'react-icons/bs';
|
||||||
|
|
||||||
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
|
||||||
|
|
||||||
const DiceInsert = () => {
|
const DiceInsert = () => {
|
||||||
const showStructuredMessage = useShowStructuredMessage()
|
const showStructuredMessage = useShowStructuredMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="发送骰子">
|
<Tooltip content='发送骰子'>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
showStructuredMessage([
|
showStructuredMessage([
|
||||||
{
|
{
|
||||||
type: 'dice'
|
type: 'dice',
|
||||||
}
|
},
|
||||||
])
|
]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BsDice3Fill className="text-lg" />
|
<BsDice3Fill className='text-lg' />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default DiceInsert
|
export default DiceInsert;
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { data, getUrl } from 'qface'
|
import { data, getUrl } from 'qface';
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { MdEmojiEmotions } from 'react-icons/md'
|
import { MdEmojiEmotions } from 'react-icons/md';
|
||||||
|
|
||||||
import { EmojiValue } from '../formats/emoji_blot'
|
import { EmojiValue } from '../formats/emoji_blot';
|
||||||
|
|
||||||
const emojis = data.map((item) => {
|
const emojis = data.map((item) => {
|
||||||
return {
|
return {
|
||||||
alt: item.QDes,
|
alt: item.QDes,
|
||||||
src: getUrl(item.QSid),
|
src: getUrl(item.QSid),
|
||||||
id: item.QSid
|
id: item.QSid,
|
||||||
} as EmojiValue
|
} as EmojiValue;
|
||||||
})
|
});
|
||||||
|
|
||||||
export interface EmojiPickerProps {
|
export interface EmojiPickerProps {
|
||||||
onInsertEmoji: (emoji: EmojiValue) => void
|
onInsertEmoji: (emoji: EmojiValue) => void
|
||||||
@ -22,62 +22,62 @@ export interface EmojiPickerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
const EmojiPicker = ({ onInsertEmoji, onOpenChange }: EmojiPickerProps) => {
|
||||||
const [visibleEmojis, setVisibleEmojis] = useState<EmojiValue[]>([])
|
const [visibleEmojis, setVisibleEmojis] = useState<EmojiValue[]>([]);
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)
|
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPopoverOpen) {
|
if (isPopoverOpen) {
|
||||||
setVisibleEmojis([]) // Reset visible emojis
|
setVisibleEmojis([]); // Reset visible emojis
|
||||||
requestAnimationFrame(() => loadEmojis()) // Start loading emojis
|
requestAnimationFrame(() => loadEmojis()); // Start loading emojis
|
||||||
}
|
}
|
||||||
}, [isPopoverOpen])
|
}, [isPopoverOpen]);
|
||||||
|
|
||||||
const loadEmojis = (index = 0, batchSize = 10) => {
|
const loadEmojis = (index = 0, batchSize = 10) => {
|
||||||
if (index < emojis.length) {
|
if (index < emojis.length) {
|
||||||
setVisibleEmojis((prev) => [
|
setVisibleEmojis((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
...emojis.slice(index, index + batchSize)
|
...emojis.slice(index, index + batchSize),
|
||||||
])
|
]);
|
||||||
requestAnimationFrame(() => loadEmojis(index + batchSize, batchSize))
|
requestAnimationFrame(() => loadEmojis(index + batchSize, batchSize));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
<Popover
|
<Popover
|
||||||
portalContainer={containerRef.current!}
|
portalContainer={containerRef.current!}
|
||||||
shouldCloseOnScroll={false}
|
shouldCloseOnScroll={false}
|
||||||
placement="right-start"
|
placement='right-start'
|
||||||
onOpenChange={(v) => {
|
onOpenChange={(v) => {
|
||||||
onOpenChange(v)
|
onOpenChange(v);
|
||||||
setIsPopoverOpen(v)
|
setIsPopoverOpen(v);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip content="插入表情">
|
<Tooltip content='插入表情'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
<Button color='primary' variant='flat' isIconOnly radius='full'>
|
||||||
<MdEmojiEmotions className="text-xl" />
|
<MdEmojiEmotions className='text-xl' />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="grid grid-cols-8 gap-1 flex-wrap justify-start items-start overflow-y-auto max-w-full max-h-96 p-2">
|
<PopoverContent className='grid grid-cols-8 gap-1 flex-wrap justify-start items-start overflow-y-auto max-w-full max-h-96 p-2'>
|
||||||
{visibleEmojis.map((emoji) => (
|
{visibleEmojis.map((emoji) => (
|
||||||
<Button
|
<Button
|
||||||
key={emoji.id}
|
key={emoji.id}
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => onInsertEmoji(emoji)}
|
onPress={() => onInsertEmoji(emoji)}
|
||||||
>
|
>
|
||||||
<Image src={emoji.src} alt={emoji.alt} className="w-6 h-6" />
|
<Image src={emoji.src} alt={emoji.alt} className='w-6 h-6' />
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default EmojiPicker
|
export default EmojiPicker;
|
||||||
|
|||||||
@ -1,95 +1,95 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { FaFolder } from 'react-icons/fa6'
|
import { FaFolder } from 'react-icons/fa6';
|
||||||
import { LuFilePlus2 } from 'react-icons/lu'
|
import { LuFilePlus2 } from 'react-icons/lu';
|
||||||
import { MdEdit, MdUpload } from 'react-icons/md'
|
import { MdEdit, MdUpload } from 'react-icons/md';
|
||||||
|
|
||||||
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
|
||||||
|
|
||||||
import { isURI } from '@/utils/url'
|
import { isURI } from '@/utils/url';
|
||||||
|
|
||||||
import type { OB11Segment } from '@/types/onebot'
|
import type { OB11Segment } from '@/types/onebot';
|
||||||
|
|
||||||
const FileInsert = () => {
|
const FileInsert = () => {
|
||||||
const [fileUrl, setFileUrl] = useState<string>('')
|
const [fileUrl, setFileUrl] = useState<string>('');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const showStructuredMessage = useShowStructuredMessage()
|
const showStructuredMessage = useShowStructuredMessage();
|
||||||
const showFileSegment = (file: string) => {
|
const showFileSegment = (file: string) => {
|
||||||
const messages: OB11Segment[] = [
|
const messages: OB11Segment[] = [
|
||||||
{
|
{
|
||||||
type: 'file',
|
type: 'file',
|
||||||
data: {
|
data: {
|
||||||
file: file
|
file,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
showStructuredMessage(messages)
|
showStructuredMessage(messages);
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="发送文件">
|
<Tooltip content='发送文件'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
<Button color='primary' variant='flat' isIconOnly radius='full'>
|
||||||
<FaFolder className="text-lg" />
|
<FaFolder className='text-lg' />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-2 p-4">
|
<PopoverContent className='flex-row gap-2 p-4'>
|
||||||
<Tooltip content="上传文件">
|
<Tooltip content='上传文件'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
fileInputRef?.current?.click()
|
fileInputRef?.current?.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MdUpload />
|
<MdUpload />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="输入文件地址">
|
<Tooltip content='输入文件地址'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger tooltip="输入文件地址">
|
<PopoverTrigger tooltip='输入文件地址'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
<MdEdit />
|
<MdEdit />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-1 p-2">
|
<PopoverContent className='flex-row gap-1 p-2'>
|
||||||
<Input
|
<Input
|
||||||
value={fileUrl}
|
value={fileUrl}
|
||||||
onChange={(e) => setFileUrl(e.target.value)}
|
onChange={(e) => setFileUrl(e.target.value)}
|
||||||
placeholder="请输入文件地址"
|
placeholder='请输入文件地址'
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!isURI(fileUrl)) {
|
if (!isURI(fileUrl)) {
|
||||||
toast.error('请输入正确的文件地址')
|
toast.error('请输入正确的文件地址');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
showFileSegment(fileUrl)
|
showFileSegment(fileUrl);
|
||||||
setFileUrl('')
|
setFileUrl('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LuFilePlus2 />
|
<LuFilePlus2 />
|
||||||
@ -100,26 +100,26 @@ const FileInsert = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type='file'
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
hidden
|
hidden
|
||||||
className="hidden"
|
className='hidden'
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const dataURL = event.target?.result
|
const dataURL = event.target?.result;
|
||||||
showFileSegment(dataURL as string)
|
showFileSegment(dataURL as string);
|
||||||
e.target.value = ''
|
e.target.value = '';
|
||||||
}
|
};
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FileInsert
|
export default FileInsert;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { MdAddPhotoAlternate, MdEdit, MdImage, MdUpload } from 'react-icons/md'
|
import { MdAddPhotoAlternate, MdEdit, MdImage, MdUpload } from 'react-icons/md';
|
||||||
|
|
||||||
import { isURI } from '@/utils/url'
|
import { isURI } from '@/utils/url';
|
||||||
|
|
||||||
export interface ImageInsertProps {
|
export interface ImageInsertProps {
|
||||||
insertImage: (url: string) => void
|
insertImage: (url: string) => void
|
||||||
@ -14,70 +14,70 @@ export interface ImageInsertProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
||||||
const [imgUrl, setImgUrl] = useState<string>('')
|
const [imgUrl, setImgUrl] = useState<string>('');
|
||||||
const imageInputRef = useRef<HTMLInputElement>(null)
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover onOpenChange={onOpenChange}>
|
<Popover onOpenChange={onOpenChange}>
|
||||||
<Tooltip content="插入图片">
|
<Tooltip content='插入图片'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
<Button color='primary' variant='flat' isIconOnly radius='full'>
|
||||||
<MdImage className="text-xl" />
|
<MdImage className='text-xl' />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-2 p-4">
|
<PopoverContent className='flex-row gap-2 p-4'>
|
||||||
<Tooltip content="上传图片">
|
<Tooltip content='上传图片'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
imageInputRef?.current?.click()
|
imageInputRef?.current?.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MdUpload />
|
<MdUpload />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="输入图片地址">
|
<Tooltip content='输入图片地址'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger tooltip="输入图片地址">
|
<PopoverTrigger tooltip='输入图片地址'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
<MdEdit />
|
<MdEdit />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-1 p-2">
|
<PopoverContent className='flex-row gap-1 p-2'>
|
||||||
<Input
|
<Input
|
||||||
value={imgUrl}
|
value={imgUrl}
|
||||||
onChange={(e) => setImgUrl(e.target.value)}
|
onChange={(e) => setImgUrl(e.target.value)}
|
||||||
placeholder="请输入图片地址"
|
placeholder='请输入图片地址'
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!isURI(imgUrl)) {
|
if (!isURI(imgUrl)) {
|
||||||
toast.error('请输入正确的图片地址')
|
toast.error('请输入正确的图片地址');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
insertImage(imgUrl)
|
insertImage(imgUrl);
|
||||||
setImgUrl('')
|
setImgUrl('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MdAddPhotoAlternate />
|
<MdAddPhotoAlternate />
|
||||||
@ -88,27 +88,27 @@ const ImageInsert = ({ insertImage, onOpenChange }: ImageInsertProps) => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type='file'
|
||||||
ref={imageInputRef}
|
ref={imageInputRef}
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept='image/*'
|
||||||
className="hidden"
|
className='hidden'
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const dataURL = event.target?.result
|
const dataURL = event.target?.result;
|
||||||
insertImage(dataURL as string)
|
insertImage(dataURL as string);
|
||||||
e.target.value = ''
|
e.target.value = '';
|
||||||
}
|
};
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ImageInsert
|
export default ImageInsert;
|
||||||
|
|||||||
@ -1,35 +1,35 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Form } from '@heroui/form'
|
import { Form } from '@heroui/form';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Select, SelectItem } from '@heroui/select'
|
import { Select, SelectItem } from '@heroui/select';
|
||||||
import type { SharedSelection } from '@heroui/system'
|
import type { SharedSelection } from '@heroui/system';
|
||||||
import { Tab, Tabs } from '@heroui/tabs'
|
import { Tab, Tabs } from '@heroui/tabs';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import type { Key } from '@react-types/shared'
|
import type { Key } from '@react-types/shared';
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form'
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { IoMusicalNotes } from 'react-icons/io5'
|
import { IoMusicalNotes } from 'react-icons/io5';
|
||||||
import { TbMusicPlus } from 'react-icons/tb'
|
import { TbMusicPlus } from 'react-icons/tb';
|
||||||
|
|
||||||
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
|
||||||
|
|
||||||
import { isURI } from '@/utils/url'
|
import { isURI } from '@/utils/url';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CustomMusicSegment,
|
CustomMusicSegment,
|
||||||
MusicSegment,
|
MusicSegment,
|
||||||
OB11Segment
|
OB11Segment,
|
||||||
} from '@/types/onebot'
|
} from '@/types/onebot';
|
||||||
|
|
||||||
type MusicData = CustomMusicSegment['data'] | MusicSegment['data']
|
type MusicData = CustomMusicSegment['data'] | MusicSegment['data'];
|
||||||
|
|
||||||
const MusicInsert = () => {
|
const MusicInsert = () => {
|
||||||
const [musicId, setMusicId] = useState<string>('')
|
const [musicId, setMusicId] = useState<string>('');
|
||||||
const [musicType, setMusicType] = useState<SharedSelection>(new Set(['163']))
|
const [musicType, setMusicType] = useState<SharedSelection>(new Set(['163']));
|
||||||
const [mode, setMode] = useState<Key>('default')
|
const [mode, setMode] = useState<Key>('default');
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const { control, handleSubmit, reset } = useForm<
|
const { control, handleSubmit, reset } = useForm<
|
||||||
Omit<CustomMusicSegment['data'], 'type'>
|
Omit<CustomMusicSegment['data'], 'type'>
|
||||||
>({
|
>({
|
||||||
@ -38,84 +38,84 @@ const MusicInsert = () => {
|
|||||||
audio: '',
|
audio: '',
|
||||||
title: '',
|
title: '',
|
||||||
image: '',
|
image: '',
|
||||||
content: ''
|
content: '',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
const showStructuredMessage = useShowStructuredMessage()
|
const showStructuredMessage = useShowStructuredMessage();
|
||||||
|
|
||||||
const showMusicSegment = (data: MusicData) => {
|
const showMusicSegment = (data: MusicData) => {
|
||||||
const messages: OB11Segment[] = []
|
const messages: OB11Segment[] = [];
|
||||||
if (data.type === 'custom') {
|
if (data.type === 'custom') {
|
||||||
messages.push({
|
messages.push({
|
||||||
type: 'music',
|
type: 'music',
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
type: 'custom'
|
type: 'custom',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
messages.push({
|
messages.push({
|
||||||
type: 'music',
|
type: 'music',
|
||||||
data
|
data,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
showStructuredMessage(messages)
|
showStructuredMessage(messages);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onSubmit = (data: Omit<CustomMusicSegment['data'], 'type'>) => {
|
const onSubmit = (data: Omit<CustomMusicSegment['data'], 'type'>) => {
|
||||||
showMusicSegment({
|
showMusicSegment({
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
...data
|
...data,
|
||||||
})
|
});
|
||||||
reset()
|
reset();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="overflow-visible">
|
<div ref={containerRef} className='overflow-visible'>
|
||||||
<Popover
|
<Popover
|
||||||
placement="right-start"
|
placement='right-start'
|
||||||
shouldCloseOnScroll={false}
|
shouldCloseOnScroll={false}
|
||||||
portalContainer={containerRef.current!}
|
portalContainer={containerRef.current!}
|
||||||
>
|
>
|
||||||
<Tooltip content="发送音乐">
|
<Tooltip content='发送音乐'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
<Button color='primary' variant='flat' isIconOnly radius='full'>
|
||||||
<IoMusicalNotes className="text-xl" />
|
<IoMusicalNotes className='text-xl' />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="gap-2 p-4">
|
<PopoverContent className='gap-2 p-4'>
|
||||||
<Tabs
|
<Tabs
|
||||||
placement="top"
|
placement='top'
|
||||||
className="w-96"
|
className='w-96'
|
||||||
fullWidth
|
fullWidth
|
||||||
selectedKey={mode}
|
selectedKey={mode}
|
||||||
onSelectionChange={(key) => {
|
onSelectionChange={(key) => {
|
||||||
if (key !== null) setMode(key)
|
if (key !== null) setMode(key);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
|
<Tab title='主流平台' key='default' className='flex flex-col gap-2'>
|
||||||
<Select
|
<Select
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label="音乐平台"
|
aria-label='音乐平台'
|
||||||
selectedKeys={musicType}
|
selectedKeys={musicType}
|
||||||
label="音乐平台"
|
label='音乐平台'
|
||||||
placeholder="请选择音乐平台"
|
placeholder='请选择音乐平台'
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
name: 'QQ音乐',
|
name: 'QQ音乐',
|
||||||
id: 'qq'
|
id: 'qq',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '网易云音乐',
|
name: '网易云音乐',
|
||||||
id: '163'
|
id: '163',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '虾米音乐',
|
name: '虾米音乐',
|
||||||
id: 'xm'
|
id: 'xm',
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
onSelectionChange={setMusicType}
|
onSelectionChange={setMusicType}
|
||||||
>
|
>
|
||||||
@ -128,27 +128,27 @@ const MusicInsert = () => {
|
|||||||
<Input
|
<Input
|
||||||
value={musicId}
|
value={musicId}
|
||||||
onChange={(e) => setMusicId(e.target.value)}
|
onChange={(e) => setMusicId(e.target.value)}
|
||||||
placeholder="请输入音乐ID"
|
placeholder='请输入音乐ID'
|
||||||
label="音乐ID"
|
label='音乐ID'
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
size="lg"
|
size='lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!musicId) {
|
if (!musicId) {
|
||||||
toast.error('请输入音乐ID')
|
toast.error('请输入音乐ID');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
showMusicSegment({
|
showMusicSegment({
|
||||||
type: Array.from(
|
type: Array.from(
|
||||||
musicType
|
musicType
|
||||||
)[0] as MusicSegment['data']['type'],
|
)[0] as MusicSegment['data']['type'],
|
||||||
id: musicId
|
id: musicId,
|
||||||
})
|
});
|
||||||
setMusicId('')
|
setMusicId('');
|
||||||
}}
|
}}
|
||||||
startContent={<TbMusicPlus />}
|
startContent={<TbMusicPlus />}
|
||||||
>
|
>
|
||||||
@ -156,92 +156,92 @@ const MusicInsert = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
title="自定义音乐"
|
title='自定义音乐'
|
||||||
key="custom"
|
key='custom'
|
||||||
className="flex flex-col gap-2"
|
className='flex flex-col gap-2'
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex flex-col gap-2"
|
className='flex flex-col gap-2'
|
||||||
validationBehavior="native"
|
validationBehavior='native'
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="url"
|
name='url'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
isRequired
|
isRequired
|
||||||
validate={(v) => {
|
validate={(v) => {
|
||||||
return !isURI(v) ? '请输入正确的音乐URL' : null
|
return !isURI(v) ? '请输入正确的音乐URL' : null;
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size='sm'
|
||||||
placeholder="请输入音乐URL"
|
placeholder='请输入音乐URL'
|
||||||
label="音乐URL"
|
label='音乐URL'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="audio"
|
name='audio'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
isRequired
|
isRequired
|
||||||
validate={(v) => {
|
validate={(v) => {
|
||||||
return !isURI(v) ? '请输入正确的音频URL' : null
|
return !isURI(v) ? '请输入正确的音频URL' : null;
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size='sm'
|
||||||
placeholder="请输入音频URL"
|
placeholder='请输入音频URL'
|
||||||
label="音频URL"
|
label='音频URL'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="title"
|
name='title'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
isRequired
|
isRequired
|
||||||
size="sm"
|
size='sm'
|
||||||
errorMessage="请输入音乐标题"
|
errorMessage='请输入音乐标题'
|
||||||
placeholder="请输入音乐标题"
|
placeholder='请输入音乐标题'
|
||||||
label="音乐标题"
|
label='音乐标题'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="image"
|
name='image'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
size="sm"
|
size='sm'
|
||||||
placeholder="请输入封面图片URL"
|
placeholder='请输入封面图片URL'
|
||||||
label="封面图片URL"
|
label='封面图片URL'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="content"
|
name='content'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
size="sm"
|
size='sm'
|
||||||
placeholder="请输入音乐描述"
|
placeholder='请输入音乐描述'
|
||||||
label="音乐描述"
|
label='音乐描述'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
size="lg"
|
size='lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
type="submit"
|
type='submit'
|
||||||
startContent={<TbMusicPlus />}
|
startContent={<TbMusicPlus />}
|
||||||
>
|
>
|
||||||
创建自定义音乐
|
创建自定义音乐
|
||||||
@ -252,7 +252,7 @@ const MusicInsert = () => {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MusicInsert
|
export default MusicInsert;
|
||||||
|
|||||||
@ -1,50 +1,50 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import { BsChatQuoteFill } from 'react-icons/bs'
|
import { BsChatQuoteFill } from 'react-icons/bs';
|
||||||
import { MdAdd } from 'react-icons/md'
|
import { MdAdd } from 'react-icons/md';
|
||||||
|
|
||||||
export interface ReplyInsertProps {
|
export interface ReplyInsertProps {
|
||||||
insertReply: (messageId: string) => void
|
insertReply: (messageId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
||||||
const [replyId, setReplyId] = useState<string>('')
|
const [replyId, setReplyId] = useState<string>('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="回复消息">
|
<Tooltip content='回复消息'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
<Button color='primary' variant='flat' isIconOnly radius='full'>
|
||||||
<BsChatQuoteFill className="text-lg" />
|
<BsChatQuoteFill className='text-lg' />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-2 p-4">
|
<PopoverContent className='flex-row gap-2 p-4'>
|
||||||
<Input
|
<Input
|
||||||
placeholder="输入消息 ID"
|
placeholder='输入消息 ID'
|
||||||
value={replyId}
|
value={replyId}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value
|
const value = e.target.value;
|
||||||
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/
|
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/;
|
||||||
if (isNumberReg.test(value)) {
|
if (isNumberReg.test(value)) {
|
||||||
setReplyId(value)
|
setReplyId(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
insertReply(replyId)
|
insertReply(replyId);
|
||||||
setReplyId('')
|
setReplyId('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MdAdd />
|
<MdAdd />
|
||||||
@ -52,7 +52,7 @@ const ReplyInsert = ({ insertReply }: ReplyInsertProps) => {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ReplyInsert
|
export default ReplyInsert;
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { LiaHandScissors } from 'react-icons/lia'
|
import { LiaHandScissors } from 'react-icons/lia';
|
||||||
|
|
||||||
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
|
||||||
|
|
||||||
const RPSInsert = () => {
|
const RPSInsert = () => {
|
||||||
const showStructuredMessage = useShowStructuredMessage()
|
const showStructuredMessage = useShowStructuredMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="发送猜拳">
|
<Tooltip content='发送猜拳'>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
showStructuredMessage([
|
showStructuredMessage([
|
||||||
{
|
{
|
||||||
type: 'rps'
|
type: 'rps',
|
||||||
}
|
},
|
||||||
])
|
]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LiaHandScissors className="text-2xl" />
|
<LiaHandScissors className='text-2xl' />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default RPSInsert
|
export default RPSInsert;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Snippet } from '@heroui/snippet'
|
import { Snippet } from '@heroui/snippet';
|
||||||
|
|
||||||
import { OB11Segment } from '@/types/onebot'
|
import { OB11Segment } from '@/types/onebot';
|
||||||
|
|
||||||
export interface ShowStructedMessageProps {
|
export interface ShowStructedMessageProps {
|
||||||
messages: OB11Segment[]
|
messages: OB11Segment[]
|
||||||
@ -11,22 +11,22 @@ const ShowStructedMessage = ({ messages }: ShowStructedMessageProps) => {
|
|||||||
<Snippet
|
<Snippet
|
||||||
hideSymbol
|
hideSymbol
|
||||||
tooltipProps={{
|
tooltipProps={{
|
||||||
content: '点击复制'
|
content: '点击复制',
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
copyButton: 'self-start sticky top-0 right-0'
|
copyButton: 'self-start sticky top-0 right-0',
|
||||||
}}
|
}}
|
||||||
className="bg-content1 h-96 overflow-y-scroll items-start"
|
className='bg-content1 h-96 overflow-y-scroll items-start'
|
||||||
>
|
>
|
||||||
{JSON.stringify(messages, null, 2)
|
{JSON.stringify(messages, null, 2)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line, i) => (
|
.map((line, i) => (
|
||||||
<span key={i} className="whitespace-pre-wrap break-all">
|
<span key={i} className='whitespace-pre-wrap break-all'>
|
||||||
{line}
|
{line}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</Snippet>
|
</Snippet>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ShowStructedMessage
|
export default ShowStructedMessage;
|
||||||
|
|||||||
@ -1,95 +1,95 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { IoVideocam } from 'react-icons/io5'
|
import { IoVideocam } from 'react-icons/io5';
|
||||||
import { MdEdit, MdUpload } from 'react-icons/md'
|
import { MdEdit, MdUpload } from 'react-icons/md';
|
||||||
import { TbVideoPlus } from 'react-icons/tb'
|
import { TbVideoPlus } from 'react-icons/tb';
|
||||||
|
|
||||||
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
|
||||||
|
|
||||||
import { isURI } from '@/utils/url'
|
import { isURI } from '@/utils/url';
|
||||||
|
|
||||||
import type { OB11Segment } from '@/types/onebot'
|
import type { OB11Segment } from '@/types/onebot';
|
||||||
|
|
||||||
const VideoInsert = () => {
|
const VideoInsert = () => {
|
||||||
const [videoUrl, setVideoUrl] = useState<string>('')
|
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||||
const videoInputRef = useRef<HTMLInputElement>(null)
|
const videoInputRef = useRef<HTMLInputElement>(null);
|
||||||
const showStructuredMessage = useShowStructuredMessage()
|
const showStructuredMessage = useShowStructuredMessage();
|
||||||
const showVideoSegment = (file: string) => {
|
const showVideoSegment = (file: string) => {
|
||||||
const messages: OB11Segment[] = [
|
const messages: OB11Segment[] = [
|
||||||
{
|
{
|
||||||
type: 'video',
|
type: 'video',
|
||||||
data: {
|
data: {
|
||||||
file: file
|
file,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
showStructuredMessage(messages)
|
showStructuredMessage(messages);
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="发送视频">
|
<Tooltip content='发送视频'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button color="primary" variant="flat" isIconOnly radius="full">
|
<Button color='primary' variant='flat' isIconOnly radius='full'>
|
||||||
<IoVideocam className="text-xl" />
|
<IoVideocam className='text-xl' />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-2 p-4">
|
<PopoverContent className='flex-row gap-2 p-4'>
|
||||||
<Tooltip content="上传视频">
|
<Tooltip content='上传视频'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
videoInputRef?.current?.click()
|
videoInputRef?.current?.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MdUpload />
|
<MdUpload />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover>
|
<Popover>
|
||||||
<Tooltip content="输入视频地址">
|
<Tooltip content='输入视频地址'>
|
||||||
<div className="max-w-fit">
|
<div className='max-w-fit'>
|
||||||
<PopoverTrigger tooltip="输入视频地址">
|
<PopoverTrigger tooltip='输入视频地址'>
|
||||||
<Button
|
<Button
|
||||||
className="text-lg"
|
className='text-lg'
|
||||||
color="primary"
|
color='primary'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
<MdEdit />
|
<MdEdit />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PopoverContent className="flex-row gap-1 p-2">
|
<PopoverContent className='flex-row gap-1 p-2'>
|
||||||
<Input
|
<Input
|
||||||
value={videoUrl}
|
value={videoUrl}
|
||||||
onChange={(e) => setVideoUrl(e.target.value)}
|
onChange={(e) => setVideoUrl(e.target.value)}
|
||||||
placeholder="请输入视频地址"
|
placeholder='请输入视频地址'
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!isURI(videoUrl)) {
|
if (!isURI(videoUrl)) {
|
||||||
toast.error('请输入正确的视频地址')
|
toast.error('请输入正确的视频地址');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
showVideoSegment(videoUrl)
|
showVideoSegment(videoUrl);
|
||||||
setVideoUrl('')
|
setVideoUrl('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TbVideoPlus />
|
<TbVideoPlus />
|
||||||
@ -100,27 +100,27 @@ const VideoInsert = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type='file'
|
||||||
ref={videoInputRef}
|
ref={videoInputRef}
|
||||||
hidden
|
hidden
|
||||||
accept="video/*"
|
accept='video/*'
|
||||||
className="hidden"
|
className='hidden'
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const dataURL = event.target?.result
|
const dataURL = event.target?.result;
|
||||||
showVideoSegment(dataURL as string)
|
showVideoSegment(dataURL as string);
|
||||||
e.target.value = ''
|
e.target.value = '';
|
||||||
}
|
};
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default VideoInsert
|
export default VideoInsert;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Quill from 'quill'
|
import Quill from 'quill';
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const Embed = Quill.import('blots/embed') as any
|
const Embed = Quill.import('blots/embed') as any
|
||||||
@ -8,34 +8,34 @@ export interface EmojiValue {
|
|||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
class EmojiBlot extends Embed {
|
class EmojiBlot extends Embed {
|
||||||
static blotName: string = 'emoji'
|
static blotName: string = 'emoji';
|
||||||
static tagName: string = 'img'
|
static tagName: string = 'img';
|
||||||
static classNames: string[] = ['w-6', 'h-6']
|
static classNames: string[] = ['w-6', 'h-6'];
|
||||||
|
|
||||||
static create(value: HTMLImageElement) {
|
static create (value: HTMLImageElement) {
|
||||||
const node = super.create(value)
|
const node = super.create(value);
|
||||||
node.setAttribute('alt', value.alt)
|
node.setAttribute('alt', value.alt);
|
||||||
node.setAttribute('src', value.src)
|
node.setAttribute('src', value.src);
|
||||||
node.setAttribute('data-id', value.id)
|
node.setAttribute('data-id', value.id);
|
||||||
node.classList.add(...EmojiBlot.classNames)
|
node.classList.add(...EmojiBlot.classNames);
|
||||||
return node
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
static formats(node: HTMLImageElement): EmojiValue {
|
static formats (node: HTMLImageElement): EmojiValue {
|
||||||
return {
|
return {
|
||||||
alt: node.getAttribute('alt') ?? '',
|
alt: node.getAttribute('alt') ?? '',
|
||||||
src: node.getAttribute('src') ?? '',
|
src: node.getAttribute('src') ?? '',
|
||||||
id: node.getAttribute('data-id') ?? ''
|
id: node.getAttribute('data-id') ?? '',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static value(node: HTMLImageElement): EmojiValue {
|
static value (node: HTMLImageElement): EmojiValue {
|
||||||
return {
|
return {
|
||||||
alt: node.getAttribute('alt') ?? '',
|
alt: node.getAttribute('alt') ?? '',
|
||||||
src: node.getAttribute('src') ?? '',
|
src: node.getAttribute('src') ?? '',
|
||||||
id: node.getAttribute('data-id') ?? ''
|
id: node.getAttribute('data-id') ?? '',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmojiBlot
|
export default EmojiBlot;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Quill from 'quill'
|
import Quill from 'quill';
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const Embed = Quill.import('blots/embed') as any
|
const Embed = Quill.import('blots/embed') as any
|
||||||
@ -7,24 +7,24 @@ export interface ImageValue {
|
|||||||
src: string
|
src: string
|
||||||
}
|
}
|
||||||
class ImageBlot extends Embed {
|
class ImageBlot extends Embed {
|
||||||
static blotName = 'image'
|
static blotName = 'image';
|
||||||
static tagName = 'img'
|
static tagName = 'img';
|
||||||
static classNames: string[] = ['max-w-48', 'max-h-48', 'align-bottom']
|
static classNames: string[] = ['max-w-48', 'max-h-48', 'align-bottom'];
|
||||||
|
|
||||||
static create(value: ImageValue) {
|
static create (value: ImageValue) {
|
||||||
let node = super.create()
|
const node = super.create();
|
||||||
node.setAttribute('alt', value.alt)
|
node.setAttribute('alt', value.alt);
|
||||||
node.setAttribute('src', value.src)
|
node.setAttribute('src', value.src);
|
||||||
node.classList.add(...ImageBlot.classNames)
|
node.classList.add(...ImageBlot.classNames);
|
||||||
return node
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
static value(node: HTMLImageElement): ImageValue {
|
static value (node: HTMLImageElement): ImageValue {
|
||||||
return {
|
return {
|
||||||
alt: node.getAttribute('alt') ?? '',
|
alt: node.getAttribute('alt') ?? '',
|
||||||
src: node.getAttribute('src') ?? ''
|
src: node.getAttribute('src') ?? '',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImageBlot
|
export default ImageBlot;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Quill from 'quill'
|
import Quill from 'quill';
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const BlockEmbed = Quill.import('blots/block/embed') as any
|
const BlockEmbed = Quill.import('blots/block/embed') as any
|
||||||
@ -6,38 +6,38 @@ export interface ReplyBlockValue {
|
|||||||
messageId: string
|
messageId: string
|
||||||
}
|
}
|
||||||
class ReplyBlock extends BlockEmbed {
|
class ReplyBlock extends BlockEmbed {
|
||||||
static blotName = 'reply'
|
static blotName = 'reply';
|
||||||
static tagName = 'div'
|
static tagName = 'div';
|
||||||
static classNames = [
|
static classNames = [
|
||||||
'p-2',
|
'p-2',
|
||||||
'select-none',
|
'select-none',
|
||||||
'bg-default-100',
|
'bg-default-100',
|
||||||
'rounded-md',
|
'rounded-md',
|
||||||
'pointer-events-none'
|
'pointer-events-none',
|
||||||
]
|
];
|
||||||
|
|
||||||
static create(value: ReplyBlockValue) {
|
static create (value: ReplyBlockValue) {
|
||||||
const node = super.create()
|
const node = super.create();
|
||||||
node.setAttribute('data-message-id', value.messageId)
|
node.setAttribute('data-message-id', value.messageId);
|
||||||
node.setAttribute('contenteditable', 'false')
|
node.setAttribute('contenteditable', 'false');
|
||||||
node.classList.add(...ReplyBlock.classNames)
|
node.classList.add(...ReplyBlock.classNames);
|
||||||
const innerDom = document.createElement('div')
|
const innerDom = document.createElement('div');
|
||||||
innerDom.classList.add('text-sm', 'text-default-500', 'relative')
|
innerDom.classList.add('text-sm', 'text-default-500', 'relative');
|
||||||
const svgContainer = document.createElement('div')
|
const svgContainer = document.createElement('div');
|
||||||
svgContainer.classList.add('w-3', 'h-3', 'absolute', 'top-0', 'right-0')
|
svgContainer.classList.add('w-3', 'h-3', 'absolute', 'top-0', 'right-0');
|
||||||
const svg = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.9082 12.3714H20.5982C20.5182 17.0414 19.5982 17.8114 16.7282 19.5114C16.3982 19.7114 16.2882 20.1314 16.4882 20.4714C16.6882 20.8014 17.1082 20.9114 17.4482 20.7114C20.8282 18.7114 22.0082 17.4914 22.0082 11.6714V6.28141C22.0082 4.57141 20.6182 3.19141 18.9182 3.19141H15.9182C14.1582 3.19141 12.8282 4.52141 12.8282 6.28141V9.28141C12.8182 11.0414 14.1482 12.3714 15.9082 12.3714Z" fill="#292D32"></path> <path d="M5.09 12.3714H9.78C9.7 17.0414 8.78 17.8114 5.91 19.5114C5.58 19.7114 5.47 20.1314 5.67 20.4714C5.87 20.8014 6.29 20.9114 6.63 20.7114C10.01 18.7114 11.19 17.4914 11.19 11.6714V6.28141C11.19 4.57141 9.8 3.19141 8.1 3.19141H5.1C3.33 3.19141 2 4.52141 2 6.28141V9.28141C2 11.0414 3.33 12.3714 5.09 12.3714Z" fill="#292D32"></path> </g></svg>`
|
const svg = '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.9082 12.3714H20.5982C20.5182 17.0414 19.5982 17.8114 16.7282 19.5114C16.3982 19.7114 16.2882 20.1314 16.4882 20.4714C16.6882 20.8014 17.1082 20.9114 17.4482 20.7114C20.8282 18.7114 22.0082 17.4914 22.0082 11.6714V6.28141C22.0082 4.57141 20.6182 3.19141 18.9182 3.19141H15.9182C14.1582 3.19141 12.8282 4.52141 12.8282 6.28141V9.28141C12.8182 11.0414 14.1482 12.3714 15.9082 12.3714Z" fill="#292D32"></path> <path d="M5.09 12.3714H9.78C9.7 17.0414 8.78 17.8114 5.91 19.5114C5.58 19.7114 5.47 20.1314 5.67 20.4714C5.87 20.8014 6.29 20.9114 6.63 20.7114C10.01 18.7114 11.19 17.4914 11.19 11.6714V6.28141C11.19 4.57141 9.8 3.19141 8.1 3.19141H5.1C3.33 3.19141 2 4.52141 2 6.28141V9.28141C2 11.0414 3.33 12.3714 5.09 12.3714Z" fill="#292D32"></path> </g></svg>';
|
||||||
svgContainer.innerHTML = svg
|
svgContainer.innerHTML = svg;
|
||||||
innerDom.innerHTML = `消息ID:${value.messageId}`
|
innerDom.innerHTML = `消息ID:${value.messageId}`;
|
||||||
innerDom.appendChild(svgContainer)
|
innerDom.appendChild(svgContainer);
|
||||||
node.appendChild(innerDom)
|
node.appendChild(innerDom);
|
||||||
return node
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
static value(node: HTMLElement): ReplyBlockValue {
|
static value (node: HTMLElement): ReplyBlockValue {
|
||||||
return {
|
return {
|
||||||
messageId: node.getAttribute('data-message-id') || ''
|
messageId: node.getAttribute('data-message-id') || '',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReplyBlock
|
export default ReplyBlock;
|
||||||
|
|||||||
@ -1,55 +1,55 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import type { Range } from 'quill'
|
import type { Range } from 'quill';
|
||||||
import 'quill/dist/quill.core.css'
|
import 'quill/dist/quill.core.css';
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import { useCustomQuill } from '@/hooks/use_custom_quill'
|
import { useCustomQuill } from '@/hooks/use_custom_quill';
|
||||||
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message';
|
||||||
|
|
||||||
import { quillToMessage } from '@/utils/onebot'
|
import { quillToMessage } from '@/utils/onebot';
|
||||||
|
|
||||||
import type { OB11Segment } from '@/types/onebot'
|
import type { OB11Segment } from '@/types/onebot';
|
||||||
|
|
||||||
import AudioInsert from './components/audio_insert'
|
import AudioInsert from './components/audio_insert';
|
||||||
import DiceInsert from './components/dice_insert'
|
import DiceInsert from './components/dice_insert';
|
||||||
import EmojiPicker from './components/emoji_picker'
|
import EmojiPicker from './components/emoji_picker';
|
||||||
import FileInsert from './components/file_insert'
|
import FileInsert from './components/file_insert';
|
||||||
import ImageInsert from './components/image_insert'
|
import ImageInsert from './components/image_insert';
|
||||||
import MusicInsert from './components/music_insert'
|
import MusicInsert from './components/music_insert';
|
||||||
import ReplyInsert from './components/reply_insert'
|
import ReplyInsert from './components/reply_insert';
|
||||||
import RPSInsert from './components/rps_insert'
|
import RPSInsert from './components/rps_insert';
|
||||||
import VideoInsert from './components/video_insert'
|
import VideoInsert from './components/video_insert';
|
||||||
import EmojiBlot from './formats/emoji_blot'
|
import EmojiBlot from './formats/emoji_blot';
|
||||||
import type { EmojiValue } from './formats/emoji_blot'
|
import type { EmojiValue } from './formats/emoji_blot';
|
||||||
import ImageBlot from './formats/image_blot'
|
import ImageBlot from './formats/image_blot';
|
||||||
import ReplyBlock from './formats/reply_blot'
|
import ReplyBlock from './formats/reply_blot';
|
||||||
|
|
||||||
const ChatInput = () => {
|
const ChatInput = () => {
|
||||||
const memorizedRange = useRef<Range | null>(null)
|
const memorizedRange = useRef<Range | null>(null);
|
||||||
|
|
||||||
const showStructuredMessage = useShowStructuredMessage()
|
const showStructuredMessage = useShowStructuredMessage();
|
||||||
const formats: string[] = ['image', 'emoji', 'reply']
|
const formats: string[] = ['image', 'emoji', 'reply'];
|
||||||
const modules = {
|
const modules = {
|
||||||
toolbar: '#toolbar'
|
toolbar: '#toolbar',
|
||||||
}
|
};
|
||||||
const { quillRef, quill, Quill } = useCustomQuill({
|
const { quillRef, quill, Quill } = useCustomQuill({
|
||||||
modules,
|
modules,
|
||||||
formats,
|
formats,
|
||||||
placeholder: '请输入消息'
|
placeholder: '请输入消息',
|
||||||
})
|
});
|
||||||
|
|
||||||
if (Quill && !quill) {
|
if (Quill && !quill) {
|
||||||
Quill.register('formats/emoji', EmojiBlot)
|
Quill.register('formats/emoji', EmojiBlot);
|
||||||
Quill.register('formats/image', ImageBlot, true)
|
Quill.register('formats/image', ImageBlot, true);
|
||||||
Quill.register('formats/reply', ReplyBlock)
|
Quill.register('formats/reply', ReplyBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quill) {
|
if (quill) {
|
||||||
quill.on('selection-change', (range) => {
|
quill.on('selection-change', (range) => {
|
||||||
if (range) {
|
if (range) {
|
||||||
const editorContent = quill.getContents()
|
const editorContent = quill.getContents();
|
||||||
const firstOp = editorContent.ops[0]
|
const firstOp = editorContent.ops[0];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof firstOp?.insert !== 'string' &&
|
typeof firstOp?.insert !== 'string' &&
|
||||||
@ -57,126 +57,126 @@ const ChatInput = () => {
|
|||||||
range.index === 0 &&
|
range.index === 0 &&
|
||||||
range.length !== quill.getLength()
|
range.length !== quill.getLength()
|
||||||
) {
|
) {
|
||||||
quill.setSelection(1, Quill.sources.SILENT)
|
quill.setSelection(1, Quill.sources.SILENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
quill.on('text-change', () => {
|
quill.on('text-change', () => {
|
||||||
const editorContent = quill.getContents()
|
const editorContent = quill.getContents();
|
||||||
const firstOp = editorContent.ops[0]
|
const firstOp = editorContent.ops[0];
|
||||||
if (
|
if (
|
||||||
firstOp &&
|
firstOp &&
|
||||||
typeof firstOp.insert !== 'string' &&
|
typeof firstOp.insert !== 'string' &&
|
||||||
firstOp.insert?.reply &&
|
firstOp.insert?.reply &&
|
||||||
quill.getLength() === 1
|
quill.getLength() === 1
|
||||||
) {
|
) {
|
||||||
quill.insertText(1, '\n', Quill.sources.SILENT)
|
quill.insertText(1, '\n', Quill.sources.SILENT);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
quill.on('editor-change', (eventName: string) => {
|
quill.on('editor-change', (eventName: string) => {
|
||||||
if (eventName === 'text-change') {
|
if (eventName === 'text-change') {
|
||||||
const editorContent = quill.getContents()
|
const editorContent = quill.getContents();
|
||||||
const firstOp = editorContent.ops[0]
|
const firstOp = editorContent.ops[0];
|
||||||
if (
|
if (
|
||||||
firstOp &&
|
firstOp &&
|
||||||
typeof firstOp.insert !== 'string' &&
|
typeof firstOp.insert !== 'string' &&
|
||||||
firstOp.insert?.reply &&
|
firstOp.insert?.reply &&
|
||||||
quill.getLength() === 1
|
quill.getLength() === 1
|
||||||
) {
|
) {
|
||||||
quill.insertText(1, '\n', Quill.sources.SILENT)
|
quill.insertText(1, '\n', Quill.sources.SILENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
quill.root.addEventListener('compositionstart', () => {
|
quill.root.addEventListener('compositionstart', () => {
|
||||||
const editorContent = quill.getContents()
|
const editorContent = quill.getContents();
|
||||||
const firstOp = editorContent.ops[0]
|
const firstOp = editorContent.ops[0];
|
||||||
if (
|
if (
|
||||||
firstOp &&
|
firstOp &&
|
||||||
typeof firstOp.insert !== 'string' &&
|
typeof firstOp.insert !== 'string' &&
|
||||||
firstOp.insert?.reply &&
|
firstOp.insert?.reply &&
|
||||||
quill.getLength() === 1
|
quill.getLength() === 1
|
||||||
) {
|
) {
|
||||||
quill.insertText(1, '\n', Quill.sources.SILENT)
|
quill.insertText(1, '\n', Quill.sources.SILENT);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOpenChange = (open: boolean) => {
|
const onOpenChange = (open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const selection = quill?.getSelection()
|
const selection = quill?.getSelection();
|
||||||
if (selection) memorizedRange.current = selection
|
if (selection) memorizedRange.current = selection;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const insertImage = (url: string) => {
|
const insertImage = (url: string) => {
|
||||||
const selection = memorizedRange.current || quill?.getSelection()
|
const selection = memorizedRange.current || quill?.getSelection();
|
||||||
quill?.deleteText(selection?.index || 0, selection?.length || 0)
|
quill?.deleteText(selection?.index || 0, selection?.length || 0);
|
||||||
quill?.insertEmbed(selection?.index || 0, 'image', {
|
quill?.insertEmbed(selection?.index || 0, 'image', {
|
||||||
src: url,
|
src: url,
|
||||||
alt: '图片'
|
alt: '图片',
|
||||||
})
|
});
|
||||||
quill?.setSelection((selection?.index || 0) + 1, 0)
|
quill?.setSelection((selection?.index || 0) + 1, 0);
|
||||||
}
|
};
|
||||||
function insertReplyBlock(messageId: string) {
|
function insertReplyBlock (messageId: string) {
|
||||||
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/
|
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/;
|
||||||
if (!isNumberReg.test(messageId)) {
|
if (!isNumberReg.test(messageId)) {
|
||||||
toast.error('请输入正确的消息ID')
|
toast.error('请输入正确的消息ID');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const editorContent = quill?.getContents()
|
const editorContent = quill?.getContents();
|
||||||
const firstOp = editorContent?.ops[0]
|
const firstOp = editorContent?.ops[0];
|
||||||
const currentSelection = quill?.getSelection()
|
const currentSelection = quill?.getSelection();
|
||||||
if (
|
if (
|
||||||
firstOp &&
|
firstOp &&
|
||||||
typeof firstOp.insert !== 'string' &&
|
typeof firstOp.insert !== 'string' &&
|
||||||
firstOp.insert?.reply
|
firstOp.insert?.reply
|
||||||
) {
|
) {
|
||||||
const delta = quill?.getContents()
|
const delta = quill?.getContents();
|
||||||
if (delta) {
|
if (delta) {
|
||||||
delta.ops[0] = {
|
delta.ops[0] = {
|
||||||
insert: { reply: { messageId } }
|
insert: { reply: { messageId } },
|
||||||
}
|
};
|
||||||
quill?.setContents(delta, Quill.sources.USER)
|
quill?.setContents(delta, Quill.sources.USER);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
quill?.insertEmbed(0, 'reply', { messageId }, Quill.sources.USER)
|
quill?.insertEmbed(0, 'reply', { messageId }, Quill.sources.USER);
|
||||||
}
|
}
|
||||||
quill?.setSelection((currentSelection?.index || 0) + 1, 0)
|
quill?.setSelection((currentSelection?.index || 0) + 1, 0);
|
||||||
quill?.blur()
|
quill?.blur();
|
||||||
}
|
}
|
||||||
const onInsertEmoji = (emoji: EmojiValue) => {
|
const onInsertEmoji = (emoji: EmojiValue) => {
|
||||||
const selection = memorizedRange.current || quill?.getSelection()
|
const selection = memorizedRange.current || quill?.getSelection();
|
||||||
quill?.deleteText(selection?.index || 0, selection?.length || 0)
|
quill?.deleteText(selection?.index || 0, selection?.length || 0);
|
||||||
quill?.insertEmbed(selection?.index || 0, 'emoji', {
|
quill?.insertEmbed(selection?.index || 0, 'emoji', {
|
||||||
alt: emoji.alt,
|
alt: emoji.alt,
|
||||||
src: emoji.src,
|
src: emoji.src,
|
||||||
id: emoji.id
|
id: emoji.id,
|
||||||
})
|
});
|
||||||
quill?.setSelection((selection?.index || 0) + 1, 0)
|
quill?.setSelection((selection?.index || 0) + 1, 0);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getChatMessage = () => {
|
const getChatMessage = () => {
|
||||||
const delta = quill?.getContents()
|
const delta = quill?.getContents();
|
||||||
const ops =
|
const ops =
|
||||||
delta?.ops?.filter((op) => {
|
delta?.ops?.filter((op) => {
|
||||||
return op.insert !== '\n'
|
return op.insert !== '\n';
|
||||||
}) ?? []
|
}) ?? [];
|
||||||
const messages: OB11Segment[] = ops.map((op) => {
|
const messages: OB11Segment[] = ops.map((op) => {
|
||||||
return quillToMessage(op)
|
return quillToMessage(op);
|
||||||
})
|
});
|
||||||
return messages
|
return messages;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
ref={quillRef}
|
ref={quillRef}
|
||||||
className="border border-default-200 rounded-md !mb-2 !text-base !h-64"
|
className='border border-default-200 rounded-md !mb-2 !text-base !h-64'
|
||||||
/>
|
/>
|
||||||
<div id="toolbar" className="!border-none flex gap-2">
|
<div id='toolbar' className='!border-none flex gap-2'>
|
||||||
<ImageInsert insertImage={insertImage} onOpenChange={onOpenChange} />
|
<ImageInsert insertImage={insertImage} onOpenChange={onOpenChange} />
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
onInsertEmoji={onInsertEmoji}
|
onInsertEmoji={onInsertEmoji}
|
||||||
@ -190,18 +190,18 @@ const ChatInput = () => {
|
|||||||
<DiceInsert />
|
<DiceInsert />
|
||||||
<RPSInsert />
|
<RPSInsert />
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const messages = getChatMessage()
|
const messages = getChatMessage();
|
||||||
showStructuredMessage(messages)
|
showStructuredMessage(messages);
|
||||||
}}
|
}}
|
||||||
className="ml-auto"
|
className='ml-auto'
|
||||||
>
|
>
|
||||||
获取JSON格式
|
获取JSON格式
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ChatInput
|
export default ChatInput;
|
||||||
|
|||||||
@ -1,42 +1,42 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
useDisclosure
|
useDisclosure,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
|
|
||||||
import ChatInput from '.'
|
import ChatInput from '.';
|
||||||
|
|
||||||
export default function ChatInputModal() {
|
export default function ChatInputModal () {
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
<Button onPress={onOpen} color='primary' radius='full' variant='flat'>
|
||||||
构造聊天消息
|
构造聊天消息
|
||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Modal
|
||||||
size="4xl"
|
size='4xl'
|
||||||
scrollBehavior="inside"
|
scrollBehavior='inside'
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className='flex flex-col gap-1'>
|
||||||
构造消息
|
构造消息
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody className="overflow-y-auto">
|
<ModalBody className='overflow-y-auto'>
|
||||||
<div className="overflow-y-auto">
|
<div className='overflow-y-auto'>
|
||||||
<ChatInput />
|
<ChatInput />
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" onPress={onClose} variant="flat">
|
<Button color='primary' onPress={onClose} variant='flat'>
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@ -45,5 +45,5 @@ export default function ChatInputModal() {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,46 +1,46 @@
|
|||||||
import Editor, { OnMount } from '@monaco-editor/react'
|
import Editor, { OnMount, loader } from '@monaco-editor/react';
|
||||||
import { loader } from '@monaco-editor/react'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import React from 'react';
|
||||||
|
|
||||||
import monaco from '@/monaco'
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
|
import monaco from '@/monaco';
|
||||||
|
|
||||||
loader.config({
|
loader.config({
|
||||||
monaco,
|
monaco,
|
||||||
paths: {
|
paths: {
|
||||||
vs: '/webui/monaco-editor/min/vs'
|
vs: '/webui/monaco-editor/min/vs',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
loader.config({
|
loader.config({
|
||||||
'vs/nls': {
|
'vs/nls': {
|
||||||
availableLanguages: { '*': 'zh-cn' }
|
availableLanguages: { '*': 'zh-cn' },
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export interface CodeEditorProps extends React.ComponentProps<typeof Editor> {
|
export interface CodeEditorProps extends React.ComponentProps<typeof Editor> {
|
||||||
test?: string
|
test?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor
|
export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>(
|
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme();
|
||||||
|
|
||||||
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
if (typeof ref === 'function') {
|
if (typeof ref === 'function') {
|
||||||
ref(editor)
|
ref(editor);
|
||||||
} else {
|
} else {
|
||||||
;(ref as React.RefObject<CodeEditorRef>).current = editor
|
(ref as React.RefObject<CodeEditorRef>).current = editor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (props.onMount) {
|
if (props.onMount) {
|
||||||
props.onMount(editor, monaco)
|
props.onMount(editor, monaco);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Editor
|
<Editor
|
||||||
@ -48,8 +48,8 @@ const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>(
|
|||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
theme={isDark ? 'vs-dark' : 'light'}
|
theme={isDark ? 'vs-dark' : 'light'}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
export default CodeEditor
|
export default CodeEditor;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Button, ButtonGroup } from '@heroui/button'
|
import { Button, ButtonGroup } from '@heroui/button';
|
||||||
import { Switch } from '@heroui/switch'
|
import { Switch } from '@heroui/switch';
|
||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import { CgDebug } from 'react-icons/cg'
|
import { CgDebug } from 'react-icons/cg';
|
||||||
import { FiEdit3 } from 'react-icons/fi'
|
import { FiEdit3 } from 'react-icons/fi';
|
||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md';
|
||||||
|
|
||||||
import DisplayCardContainer from './container'
|
import DisplayCardContainer from './container';
|
||||||
|
|
||||||
type NetworkType = OneBotConfig['network']
|
type NetworkType = OneBotConfig['network'];
|
||||||
|
|
||||||
export type NetworkDisplayCardFields<T extends keyof NetworkType> = Array<{
|
export type NetworkDisplayCardFields<T extends keyof NetworkType> = Array<{
|
||||||
label: string
|
label: string
|
||||||
@ -15,7 +15,7 @@ export type NetworkDisplayCardFields<T extends keyof NetworkType> = Array<{
|
|||||||
render?: (
|
render?: (
|
||||||
value: NetworkType[T][0][keyof NetworkType[T][0]]
|
value: NetworkType[T][0][keyof NetworkType[T][0]]
|
||||||
) => React.ReactNode
|
) => React.ReactNode
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export interface NetworkDisplayCardProps<T extends keyof NetworkType> {
|
export interface NetworkDisplayCardProps<T extends keyof NetworkType> {
|
||||||
data: NetworkType[T][0]
|
data: NetworkType[T][0]
|
||||||
@ -36,25 +36,25 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onEnable,
|
onEnable,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEnableDebug
|
onEnableDebug,
|
||||||
}: NetworkDisplayCardProps<T>) => {
|
}: NetworkDisplayCardProps<T>) => {
|
||||||
const { name, enable, debug } = data
|
const { name, enable, debug } = data;
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
const handleEnable = () => {
|
const handleEnable = () => {
|
||||||
setEditing(true)
|
setEditing(true);
|
||||||
onEnable().finally(() => setEditing(false))
|
onEnable().finally(() => setEditing(false));
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
setEditing(true)
|
setEditing(true);
|
||||||
onDelete().finally(() => setEditing(false))
|
onDelete().finally(() => setEditing(false));
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleEnableDebug = () => {
|
const handleEnableDebug = () => {
|
||||||
setEditing(true)
|
setEditing(true);
|
||||||
onEnableDebug().finally(() => setEditing(false))
|
onEnableDebug().finally(() => setEditing(false));
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisplayCardContainer
|
<DisplayCardContainer
|
||||||
@ -62,12 +62,12 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
|||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
fullWidth
|
fullWidth
|
||||||
isDisabled={editing}
|
isDisabled={editing}
|
||||||
radius="sm"
|
radius='sm'
|
||||||
size="sm"
|
size='sm'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color='warning'
|
||||||
startContent={<FiEdit3 size={16} />}
|
startContent={<FiEdit3 size={16} />}
|
||||||
onPress={onEdit}
|
onPress={onEdit}
|
||||||
>
|
>
|
||||||
@ -76,14 +76,14 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
color={debug ? 'secondary' : 'success'}
|
color={debug ? 'secondary' : 'success'}
|
||||||
variant="flat"
|
variant='flat'
|
||||||
startContent={
|
startContent={
|
||||||
<CgDebug
|
<CgDebug
|
||||||
style={{
|
style={{
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '16px',
|
height: '16px',
|
||||||
minWidth: '16px',
|
minWidth: '16px',
|
||||||
minHeight: '16px'
|
minHeight: '16px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -92,8 +92,8 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
|||||||
{debug ? '关闭调试' : '开启调试'}
|
{debug ? '关闭调试' : '开启调试'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="bg-danger/20 text-danger hover:bg-danger/30 transition-colors"
|
className='bg-danger/20 text-danger hover:bg-danger/30 transition-colors'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
startContent={<MdDeleteForever size={16} />}
|
startContent={<MdDeleteForever size={16} />}
|
||||||
onPress={handleDelete}
|
onPress={handleDelete}
|
||||||
>
|
>
|
||||||
@ -111,7 +111,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
|||||||
tag={showType && typeLabel}
|
tag={showType && typeLabel}
|
||||||
title={name}
|
title={name}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className='grid grid-cols-2 gap-1'>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@ -119,17 +119,19 @@ const NetworkDisplayCard = <T extends keyof NetworkType>({
|
|||||||
field.label === 'URL' ? 'col-span-2' : ''
|
field.label === 'URL' ? 'col-span-2' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-default-400">{field.label}</span>
|
<span className='text-default-400'>{field.label}</span>
|
||||||
{field.render ? (
|
{field.render
|
||||||
field.render(field.value)
|
? (
|
||||||
) : (
|
field.render(field.value)
|
||||||
<span>{field.value}</span>
|
)
|
||||||
)}
|
: (
|
||||||
|
<span>{field.value}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</DisplayCardContainer>
|
</DisplayCardContainer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default NetworkDisplayCard
|
export default NetworkDisplayCard;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card'
|
import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { title } from '../primitives'
|
import { title } from '../primitives';
|
||||||
|
|
||||||
export interface ContainerProps {
|
export interface ContainerProps {
|
||||||
title: string
|
title: string
|
||||||
@ -24,13 +24,13 @@ const DisplayCardContainer: React.FC<ContainerProps> = ({
|
|||||||
action,
|
action,
|
||||||
tag,
|
tag,
|
||||||
enableSwitch,
|
enableSwitch,
|
||||||
children
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-opacity-50 backdrop-blur-sm">
|
<Card className='bg-opacity-50 backdrop-blur-sm'>
|
||||||
<CardHeader className={'pb-0 flex items-center'}>
|
<CardHeader className='pb-0 flex items-center'>
|
||||||
{tag && (
|
{tag && (
|
||||||
<div className="text-center text-default-400 mb-1 absolute top-0 left-1/2 -translate-x-1/2 text-sm pointer-events-none bg-warning-100 dark:bg-warning-50 px-2 rounded-b">
|
<div className='text-center text-default-400 mb-1 absolute top-0 left-1/2 -translate-x-1/2 text-sm pointer-events-none bg-warning-100 dark:bg-warning-50 px-2 rounded-b'>
|
||||||
{tag}
|
{tag}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -39,19 +39,19 @@ const DisplayCardContainer: React.FC<ContainerProps> = ({
|
|||||||
title({
|
title({
|
||||||
color: 'foreground',
|
color: 'foreground',
|
||||||
size: 'xs',
|
size: 'xs',
|
||||||
shadow: true
|
shadow: true,
|
||||||
}),
|
}),
|
||||||
'truncate'
|
'truncate'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{_title}
|
{_title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="ml-auto">{enableSwitch}</div>
|
<div className='ml-auto'>{enableSwitch}</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="text-sm">{children}</CardBody>
|
<CardBody className='text-sm'>{children}</CardBody>
|
||||||
<CardFooter>{action}</CardFooter>
|
<CardFooter>{action}</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default DisplayCardContainer
|
export default DisplayCardContainer;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import NetworkDisplayCard from './common_card'
|
import NetworkDisplayCard from './common_card';
|
||||||
import type { NetworkDisplayCardFields } from './common_card'
|
import type { NetworkDisplayCardFields } from './common_card';
|
||||||
|
|
||||||
interface HTTPClientDisplayCardProps {
|
interface HTTPClientDisplayCardProps {
|
||||||
data: OneBotConfig['network']['httpClients'][0]
|
data: OneBotConfig['network']['httpClients'][0]
|
||||||
@ -13,8 +13,8 @@ interface HTTPClientDisplayCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HTTPClientDisplayCard: React.FC<HTTPClientDisplayCardProps> = (props) => {
|
const HTTPClientDisplayCard: React.FC<HTTPClientDisplayCardProps> = (props) => {
|
||||||
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
|
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
|
||||||
const { url, reportSelfMessage, messagePostFormat } = data
|
const { url, reportSelfMessage, messagePostFormat } = data;
|
||||||
|
|
||||||
const fields: NetworkDisplayCardFields<'httpClients'> = [
|
const fields: NetworkDisplayCardFields<'httpClients'> = [
|
||||||
{ label: 'URL', value: url },
|
{ label: 'URL', value: url },
|
||||||
@ -23,25 +23,25 @@ const HTTPClientDisplayCard: React.FC<HTTPClientDisplayCardProps> = (props) => {
|
|||||||
label: '上报自身消息',
|
label: '上报自身消息',
|
||||||
value: reportSelfMessage,
|
value: reportSelfMessage,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '是' : '否'}
|
{value ? '是' : '否'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkDisplayCard
|
<NetworkDisplayCard
|
||||||
data={data}
|
data={data}
|
||||||
showType={showType}
|
showType={showType}
|
||||||
typeLabel="HTTP客户端"
|
typeLabel='HTTP客户端'
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onEnable={onEnable}
|
onEnable={onEnable}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onEnableDebug={onEnableDebug}
|
onEnableDebug={onEnableDebug}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HTTPClientDisplayCard
|
export default HTTPClientDisplayCard;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import NetworkDisplayCard from './common_card'
|
import NetworkDisplayCard from './common_card';
|
||||||
import type { NetworkDisplayCardFields } from './common_card'
|
import type { NetworkDisplayCardFields } from './common_card';
|
||||||
|
|
||||||
interface HTTPServerDisplayCardProps {
|
interface HTTPServerDisplayCardProps {
|
||||||
data: OneBotConfig['network']['httpServers'][0]
|
data: OneBotConfig['network']['httpServers'][0]
|
||||||
@ -13,8 +13,8 @@ interface HTTPServerDisplayCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HTTPServerDisplayCard: React.FC<HTTPServerDisplayCardProps> = (props) => {
|
const HTTPServerDisplayCard: React.FC<HTTPServerDisplayCardProps> = (props) => {
|
||||||
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
|
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
|
||||||
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data
|
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data;
|
||||||
|
|
||||||
const fields: NetworkDisplayCardFields<'httpServers'> = [
|
const fields: NetworkDisplayCardFields<'httpServers'> = [
|
||||||
{ label: '主机', value: host },
|
{ label: '主机', value: host },
|
||||||
@ -24,34 +24,34 @@ const HTTPServerDisplayCard: React.FC<HTTPServerDisplayCardProps> = (props) => {
|
|||||||
label: 'CORS',
|
label: 'CORS',
|
||||||
value: enableCors,
|
value: enableCors,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '已启用' : '未启用'}
|
{value ? '已启用' : '未启用'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'WS',
|
label: 'WS',
|
||||||
value: enableWebsocket,
|
value: enableWebsocket,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '已启用' : '未启用'}
|
{value ? '已启用' : '未启用'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkDisplayCard
|
<NetworkDisplayCard
|
||||||
data={data}
|
data={data}
|
||||||
showType={showType}
|
showType={showType}
|
||||||
typeLabel="HTTP服务器"
|
typeLabel='HTTP服务器'
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onEnable={onEnable}
|
onEnable={onEnable}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onEnableDebug={onEnableDebug}
|
onEnableDebug={onEnableDebug}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HTTPServerDisplayCard
|
export default HTTPServerDisplayCard;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import NetworkDisplayCard from './common_card'
|
import NetworkDisplayCard from './common_card';
|
||||||
import type { NetworkDisplayCardFields } from './common_card'
|
import type { NetworkDisplayCardFields } from './common_card';
|
||||||
|
|
||||||
interface HTTPSSEServerDisplayCardProps {
|
interface HTTPSSEServerDisplayCardProps {
|
||||||
data: OneBotConfig['network']['httpSseServers'][0]
|
data: OneBotConfig['network']['httpSseServers'][0]
|
||||||
@ -15,8 +15,8 @@ interface HTTPSSEServerDisplayCardProps {
|
|||||||
const HTTPSSEServerDisplayCard: React.FC<HTTPSSEServerDisplayCardProps> = (
|
const HTTPSSEServerDisplayCard: React.FC<HTTPSSEServerDisplayCardProps> = (
|
||||||
props
|
props
|
||||||
) => {
|
) => {
|
||||||
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
|
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
|
||||||
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data
|
const { host, port, enableCors, enableWebsocket, messagePostFormat } = data;
|
||||||
|
|
||||||
const fields: NetworkDisplayCardFields<'httpServers'> = [
|
const fields: NetworkDisplayCardFields<'httpServers'> = [
|
||||||
{ label: '主机', value: host },
|
{ label: '主机', value: host },
|
||||||
@ -26,34 +26,34 @@ const HTTPSSEServerDisplayCard: React.FC<HTTPSSEServerDisplayCardProps> = (
|
|||||||
label: 'CORS',
|
label: 'CORS',
|
||||||
value: enableCors,
|
value: enableCors,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '已启用' : '未启用'}
|
{value ? '已启用' : '未启用'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'WS',
|
label: 'WS',
|
||||||
value: enableWebsocket,
|
value: enableWebsocket,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '已启用' : '未启用'}
|
{value ? '已启用' : '未启用'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkDisplayCard
|
<NetworkDisplayCard
|
||||||
data={data}
|
data={data}
|
||||||
showType={showType}
|
showType={showType}
|
||||||
typeLabel="HTTP服务器"
|
typeLabel='HTTP服务器'
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onEnable={onEnable}
|
onEnable={onEnable}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onEnableDebug={onEnableDebug}
|
onEnableDebug={onEnableDebug}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HTTPSSEServerDisplayCard
|
export default HTTPSSEServerDisplayCard;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import NetworkDisplayCard from './common_card'
|
import NetworkDisplayCard from './common_card';
|
||||||
import type { NetworkDisplayCardFields } from './common_card'
|
import type { NetworkDisplayCardFields } from './common_card';
|
||||||
|
|
||||||
interface WebsocketClientDisplayCardProps {
|
interface WebsocketClientDisplayCardProps {
|
||||||
data: OneBotConfig['network']['websocketClients'][0]
|
data: OneBotConfig['network']['websocketClients'][0]
|
||||||
@ -15,14 +15,14 @@ interface WebsocketClientDisplayCardProps {
|
|||||||
const WebsocketClientDisplayCard: React.FC<WebsocketClientDisplayCardProps> = (
|
const WebsocketClientDisplayCard: React.FC<WebsocketClientDisplayCardProps> = (
|
||||||
props
|
props
|
||||||
) => {
|
) => {
|
||||||
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
|
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
heartInterval,
|
heartInterval,
|
||||||
reconnectInterval,
|
reconnectInterval,
|
||||||
messagePostFormat,
|
messagePostFormat,
|
||||||
reportSelfMessage
|
reportSelfMessage,
|
||||||
} = data
|
} = data;
|
||||||
|
|
||||||
const fields: NetworkDisplayCardFields<'websocketClients'> = [
|
const fields: NetworkDisplayCardFields<'websocketClients'> = [
|
||||||
{ label: 'URL', value: url },
|
{ label: 'URL', value: url },
|
||||||
@ -33,25 +33,25 @@ const WebsocketClientDisplayCard: React.FC<WebsocketClientDisplayCardProps> = (
|
|||||||
label: '上报自身消息',
|
label: '上报自身消息',
|
||||||
value: reportSelfMessage,
|
value: reportSelfMessage,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '是' : '否'}
|
{value ? '是' : '否'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkDisplayCard
|
<NetworkDisplayCard
|
||||||
data={data}
|
data={data}
|
||||||
showType={showType}
|
showType={showType}
|
||||||
typeLabel="Websocket客户端"
|
typeLabel='Websocket客户端'
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onEnable={onEnable}
|
onEnable={onEnable}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onEnableDebug={onEnableDebug}
|
onEnableDebug={onEnableDebug}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default WebsocketClientDisplayCard
|
export default WebsocketClientDisplayCard;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import NetworkDisplayCard from './common_card'
|
import NetworkDisplayCard from './common_card';
|
||||||
import type { NetworkDisplayCardFields } from './common_card'
|
import type { NetworkDisplayCardFields } from './common_card';
|
||||||
|
|
||||||
interface WebsocketServerDisplayCardProps {
|
interface WebsocketServerDisplayCardProps {
|
||||||
data: OneBotConfig['network']['websocketServers'][0]
|
data: OneBotConfig['network']['websocketServers'][0]
|
||||||
@ -15,15 +15,15 @@ interface WebsocketServerDisplayCardProps {
|
|||||||
const WebsocketServerDisplayCard: React.FC<WebsocketServerDisplayCardProps> = (
|
const WebsocketServerDisplayCard: React.FC<WebsocketServerDisplayCardProps> = (
|
||||||
props
|
props
|
||||||
) => {
|
) => {
|
||||||
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props
|
const { data, showType, onEdit, onEnable, onDelete, onEnableDebug } = props;
|
||||||
const {
|
const {
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
heartInterval,
|
heartInterval,
|
||||||
messagePostFormat,
|
messagePostFormat,
|
||||||
reportSelfMessage,
|
reportSelfMessage,
|
||||||
enableForcePushEvent
|
enableForcePushEvent,
|
||||||
} = data
|
} = data;
|
||||||
|
|
||||||
const fields: NetworkDisplayCardFields<'websocketServers'> = [
|
const fields: NetworkDisplayCardFields<'websocketServers'> = [
|
||||||
{ label: '主机', value: host },
|
{ label: '主机', value: host },
|
||||||
@ -34,34 +34,34 @@ const WebsocketServerDisplayCard: React.FC<WebsocketServerDisplayCardProps> = (
|
|||||||
label: '上报自身消息',
|
label: '上报自身消息',
|
||||||
value: reportSelfMessage,
|
value: reportSelfMessage,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '是' : '否'}
|
{value ? '是' : '否'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '强制推送事件',
|
label: '强制推送事件',
|
||||||
value: enableForcePushEvent,
|
value: enableForcePushEvent,
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Chip color={value ? 'success' : 'default'} size="sm" variant="flat">
|
<Chip color={value ? 'success' : 'default'} size='sm' variant='flat'>
|
||||||
{value ? '是' : '否'}
|
{value ? '是' : '否'}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkDisplayCard
|
<NetworkDisplayCard
|
||||||
data={data}
|
data={data}
|
||||||
showType={showType}
|
showType={showType}
|
||||||
typeLabel="Websocket服务器"
|
typeLabel='Websocket服务器'
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onEnable={onEnable}
|
onEnable={onEnable}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onEnableDebug={onEnableDebug}
|
onEnableDebug={onEnableDebug}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default WebsocketServerDisplayCard
|
export default WebsocketServerDisplayCard;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Card, CardBody } from '@heroui/card'
|
import { Card, CardBody } from '@heroui/card';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { title } from '@/components/primitives'
|
import { title } from '@/components/primitives';
|
||||||
|
|
||||||
export interface NetworkItemDisplayProps {
|
export interface NetworkItemDisplayProps {
|
||||||
count: number
|
count: number
|
||||||
@ -12,7 +12,7 @@ export interface NetworkItemDisplayProps {
|
|||||||
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||||
count,
|
count,
|
||||||
label,
|
label,
|
||||||
size = 'md'
|
size = 'md',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -22,16 +22,16 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
|||||||
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
|
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
|
||||||
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
||||||
)}
|
)}
|
||||||
shadow="sm"
|
shadow='sm'
|
||||||
>
|
>
|
||||||
<CardBody className="items-center md:gap-1 p-1 md:p-2">
|
<CardBody className='items-center md:gap-1 p-1 md:p-2'>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1',
|
'flex-1',
|
||||||
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
||||||
title({
|
title({
|
||||||
color: size === 'md' ? 'pink' : 'yellow',
|
color: size === 'md' ? 'pink' : 'yellow',
|
||||||
size
|
size,
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -44,7 +44,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
|||||||
title({
|
title({
|
||||||
color: size === 'md' ? 'pink' : 'yellow',
|
color: size === 'md' ? 'pink' : 'yellow',
|
||||||
shadow: true,
|
shadow: true,
|
||||||
size: 'xxs'
|
size: 'xxs',
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -52,7 +52,7 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default NetworkItemDisplay
|
export default NetworkItemDisplay;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Card, CardProps } from '@heroui/card'
|
import { Card, CardProps } from '@heroui/card';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
|
|
||||||
export interface HoverEffectCardProps extends CardProps {
|
export interface HoverEffectCardProps extends CardProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -18,15 +18,15 @@ const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
|
|||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
lightClassName,
|
lightClassName,
|
||||||
lightStyle
|
lightStyle,
|
||||||
} = props
|
} = props;
|
||||||
const cardRef = React.useRef<HTMLDivElement | null>(null)
|
const cardRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const lightRef = React.useRef<HTMLDivElement | null>(null)
|
const lightRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const [isShowLight, setIsShowLight] = React.useState(false)
|
const [isShowLight, setIsShowLight] = React.useState(false);
|
||||||
const [pos, setPos] = React.useState({
|
const [pos, setPos] = React.useState({
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0
|
top: 0,
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -40,53 +40,53 @@ const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
|
|||||||
willChange: 'transform',
|
willChange: 'transform',
|
||||||
transform:
|
transform:
|
||||||
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)',
|
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)',
|
||||||
...style
|
...style,
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (cardRef.current) {
|
if (cardRef.current) {
|
||||||
cardRef.current.style.transition = 'transform 0.3s ease-out'
|
cardRef.current.style.transition = 'transform 0.3s ease-out';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setIsShowLight(false)
|
setIsShowLight(false);
|
||||||
if (cardRef.current) {
|
if (cardRef.current) {
|
||||||
cardRef.current.style.transition = 'transform 0.5s'
|
cardRef.current.style.transition = 'transform 0.5s';
|
||||||
cardRef.current.style.transform =
|
cardRef.current.style.transform =
|
||||||
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
|
'perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
|
onMouseMove={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (cardRef.current) {
|
if (cardRef.current) {
|
||||||
setIsShowLight(true)
|
setIsShowLight(true);
|
||||||
|
|
||||||
const { x, y } = cardRef.current.getBoundingClientRect()
|
const { x, y } = cardRef.current.getBoundingClientRect();
|
||||||
const { clientX, clientY } = e
|
const { clientX, clientY } = e;
|
||||||
|
|
||||||
const offsetX = clientX - x
|
const offsetX = clientX - x;
|
||||||
const offsetY = clientY - y
|
const offsetY = clientY - y;
|
||||||
|
|
||||||
const lightWidth = lightStyle?.width?.toString() || '100'
|
const lightWidth = lightStyle?.width?.toString() || '100';
|
||||||
const lightHeight = lightStyle?.height?.toString() || '100'
|
const lightHeight = lightStyle?.height?.toString() || '100';
|
||||||
const lightWidthNum = parseInt(lightWidth)
|
const lightWidthNum = parseInt(lightWidth);
|
||||||
const lightHeightNum = parseInt(lightHeight)
|
const lightHeightNum = parseInt(lightHeight);
|
||||||
|
|
||||||
const left = offsetX - lightWidthNum / 2
|
const left = offsetX - lightWidthNum / 2;
|
||||||
const top = offsetY - lightHeightNum / 2
|
const top = offsetY - lightHeightNum / 2;
|
||||||
|
|
||||||
setPos({
|
setPos({
|
||||||
left,
|
left,
|
||||||
top
|
top,
|
||||||
})
|
});
|
||||||
|
|
||||||
cardRef.current.style.transition = 'transform 0.1s'
|
cardRef.current.style.transition = 'transform 0.1s';
|
||||||
|
|
||||||
const rangeX = 400 / 2
|
const rangeX = 400 / 2;
|
||||||
const rangeY = 400 / 2
|
const rangeY = 400 / 2;
|
||||||
|
|
||||||
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation
|
const rotateX = ((offsetY - rangeY) / rangeY) * maxXRotation;
|
||||||
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation
|
const rotateY = -1 * ((offsetX - rangeX) / rangeX) * maxYRotation;
|
||||||
|
|
||||||
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
|
cardRef.current.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -98,12 +98,12 @@ const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
|
|||||||
lightClassName
|
lightClassName
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
...pos
|
...pos,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HoverEffectCard
|
export default HoverEffectCard;
|
||||||
|
|||||||
@ -1,30 +1,30 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Code } from '@heroui/code'
|
import { Code } from '@heroui/code';
|
||||||
import { MdError } from 'react-icons/md'
|
import { MdError } from 'react-icons/md';
|
||||||
|
|
||||||
export interface ErrorFallbackProps {
|
export interface ErrorFallbackProps {
|
||||||
error: Error
|
error: Error
|
||||||
resetErrorBoundary: () => void
|
resetErrorBoundary: () => void
|
||||||
}
|
}
|
||||||
function errorFallbackRender({
|
function errorFallbackRender ({
|
||||||
error,
|
error,
|
||||||
resetErrorBoundary
|
resetErrorBoundary,
|
||||||
}: ErrorFallbackProps) {
|
}: ErrorFallbackProps) {
|
||||||
return (
|
return (
|
||||||
<div className="pt-32 flex flex-col justify-center items-center">
|
<div className='pt-32 flex flex-col justify-center items-center'>
|
||||||
<div className="flex items-center">
|
<div className='flex items-center'>
|
||||||
<MdError className="mr-2" color="red" size={30} />
|
<MdError className='mr-2' color='red' size={30} />
|
||||||
<h1 className="text-2xl">出错了</h1>
|
<h1 className='text-2xl'>出错了</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-6 flex flex-col justify-center items-center">
|
<div className='my-6 flex flex-col justify-center items-center'>
|
||||||
<p className="mb-2">错误信息</p>
|
<p className='mb-2'>错误信息</p>
|
||||||
<Code>{error.message}</Code>
|
<Code>{error.message}</Code>
|
||||||
</div>
|
</div>
|
||||||
<Button color="primary" size="md" onPress={resetErrorBoundary}>
|
<Button color='primary' size='md' onPress={resetErrorBoundary}>
|
||||||
重试
|
重试
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default errorFallbackRender
|
export default errorFallbackRender;
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import {
|
|||||||
FaFileVideo,
|
FaFileVideo,
|
||||||
FaFileWord,
|
FaFileWord,
|
||||||
FaFileZipper,
|
FaFileZipper,
|
||||||
FaFolderClosed
|
FaFolderClosed,
|
||||||
} from 'react-icons/fa6'
|
} from 'react-icons/fa6';
|
||||||
|
|
||||||
export interface FileIconProps {
|
export interface FileIconProps {
|
||||||
name?: string
|
name?: string
|
||||||
@ -20,12 +20,12 @@ export interface FileIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FileIcon = (props: FileIconProps) => {
|
const FileIcon = (props: FileIconProps) => {
|
||||||
const { name, isDirectory = false } = props
|
const { name, isDirectory = false } = props;
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
return <FaFolderClosed className="text-yellow-500" />
|
return <FaFolderClosed className='text-yellow-500' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = name?.split('.').pop() || ''
|
const ext = name?.split('.').pop() || '';
|
||||||
if (ext) {
|
if (ext) {
|
||||||
switch (ext.toLowerCase()) {
|
switch (ext.toLowerCase()) {
|
||||||
case 'jpg':
|
case 'jpg':
|
||||||
@ -50,20 +50,20 @@ const FileIcon = (props: FileIconProps) => {
|
|||||||
case 'fig':
|
case 'fig':
|
||||||
case 'xd':
|
case 'xd':
|
||||||
case 'svgz':
|
case 'svgz':
|
||||||
return <FaFileImage className="text-green-500" />
|
return <FaFileImage className='text-green-500' />;
|
||||||
case 'pdf':
|
case 'pdf':
|
||||||
return <FaFilePdf className="text-red-500" />
|
return <FaFilePdf className='text-red-500' />;
|
||||||
case 'doc':
|
case 'doc':
|
||||||
case 'docx':
|
case 'docx':
|
||||||
return <FaFileWord className="text-blue-500" />
|
return <FaFileWord className='text-blue-500' />;
|
||||||
case 'xls':
|
case 'xls':
|
||||||
case 'xlsx':
|
case 'xlsx':
|
||||||
return <FaFileExcel className="text-green-500" />
|
return <FaFileExcel className='text-green-500' />;
|
||||||
case 'csv':
|
case 'csv':
|
||||||
return <FaFileCsv className="text-green-500" />
|
return <FaFileCsv className='text-green-500' />;
|
||||||
case 'ppt':
|
case 'ppt':
|
||||||
case 'pptx':
|
case 'pptx':
|
||||||
return <FaFilePowerpoint className="text-red-500" />
|
return <FaFilePowerpoint className='text-red-500' />;
|
||||||
case 'zip':
|
case 'zip':
|
||||||
case 'rar':
|
case 'rar':
|
||||||
case '7z':
|
case '7z':
|
||||||
@ -79,18 +79,18 @@ const FileIcon = (props: FileIconProps) => {
|
|||||||
case 'taz':
|
case 'taz':
|
||||||
case 'tz':
|
case 'tz':
|
||||||
case 'tzo':
|
case 'tzo':
|
||||||
return <FaFileZipper className="text-green-500" />
|
return <FaFileZipper className='text-green-500' />;
|
||||||
case 'txt':
|
case 'txt':
|
||||||
return <FaFileLines className="text-gray-500" />
|
return <FaFileLines className='text-gray-500' />;
|
||||||
case 'mp3':
|
case 'mp3':
|
||||||
case 'wav':
|
case 'wav':
|
||||||
case 'flac':
|
case 'flac':
|
||||||
return <FaFileAudio className="text-green-500" />
|
return <FaFileAudio className='text-green-500' />;
|
||||||
case 'mp4':
|
case 'mp4':
|
||||||
case 'avi':
|
case 'avi':
|
||||||
case 'mov':
|
case 'mov':
|
||||||
case 'wmv':
|
case 'wmv':
|
||||||
return <FaFileVideo className="text-red-500" />
|
return <FaFileVideo className='text-red-500' />;
|
||||||
case 'html':
|
case 'html':
|
||||||
case 'css':
|
case 'css':
|
||||||
case 'js':
|
case 'js':
|
||||||
@ -154,13 +154,13 @@ const FileIcon = (props: FileIconProps) => {
|
|||||||
case 'userosscache':
|
case 'userosscache':
|
||||||
case 'sln.docstates':
|
case 'sln.docstates':
|
||||||
case 'dll':
|
case 'dll':
|
||||||
return <FaFileCode className="text-blue-500" />
|
return <FaFileCode className='text-blue-500' />;
|
||||||
default:
|
default:
|
||||||
return <FaFile className="text-gray-500" />
|
return <FaFile className='text-gray-500' />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FaFile className="text-gray-500" />
|
return <FaFile className='text-gray-500' />;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FileIcon
|
export default FileIcon;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Button, ButtonGroup } from '@heroui/button'
|
import { Button, ButtonGroup } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader
|
ModalHeader,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
|
|
||||||
interface CreateFileModalProps {
|
interface CreateFileModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@ -18,22 +18,22 @@ interface CreateFileModalProps {
|
|||||||
onCreate: () => void
|
onCreate: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateFileModal({
|
export default function CreateFileModal ({
|
||||||
isOpen,
|
isOpen,
|
||||||
fileType,
|
fileType,
|
||||||
newFileName,
|
newFileName,
|
||||||
onTypeChange,
|
onTypeChange,
|
||||||
onNameChange,
|
onNameChange,
|
||||||
onClose,
|
onClose,
|
||||||
onCreate
|
onCreate,
|
||||||
}: CreateFileModalProps) {
|
}: CreateFileModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>新建</ModalHeader>
|
<ModalHeader>新建</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="flex flex-col gap-4">
|
<div className='flex flex-col gap-4'>
|
||||||
<ButtonGroup color="primary">
|
<ButtonGroup color='primary'>
|
||||||
<Button
|
<Button
|
||||||
variant={fileType === 'file' ? 'solid' : 'flat'}
|
variant={fileType === 'file' ? 'solid' : 'flat'}
|
||||||
onPress={() => onTypeChange('file')}
|
onPress={() => onTypeChange('file')}
|
||||||
@ -47,18 +47,18 @@ export default function CreateFileModal({
|
|||||||
目录
|
目录
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Input label="名称" value={newFileName} onChange={onNameChange} />
|
<Input label='名称' value={newFileName} onChange={onNameChange} />
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" variant="flat" onPress={onClose}>
|
<Button color='primary' variant='flat' onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" onPress={onCreate}>
|
<Button color='primary' onPress={onCreate}>
|
||||||
创建
|
创建
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Code } from '@heroui/code'
|
import { Code } from '@heroui/code';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader
|
ModalHeader,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
|
|
||||||
import CodeEditor from '@/components/code_editor'
|
import CodeEditor from '@/components/code_editor';
|
||||||
|
|
||||||
interface FileEditModalProps {
|
interface FileEditModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@ -18,61 +18,61 @@ interface FileEditModalProps {
|
|||||||
onContentChange: (newContent?: string) => void
|
onContentChange: (newContent?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FileEditModal({
|
export default function FileEditModal ({
|
||||||
isOpen,
|
isOpen,
|
||||||
file,
|
file,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
onContentChange
|
onContentChange,
|
||||||
}: FileEditModalProps) {
|
}: FileEditModalProps) {
|
||||||
// 根据文件后缀返回对应语言
|
// 根据文件后缀返回对应语言
|
||||||
const getLanguage = (filePath: string) => {
|
const getLanguage = (filePath: string) => {
|
||||||
if (filePath.endsWith('.js')) return 'javascript'
|
if (filePath.endsWith('.js')) return 'javascript';
|
||||||
if (filePath.endsWith('.ts')) return 'typescript'
|
if (filePath.endsWith('.ts')) return 'typescript';
|
||||||
if (filePath.endsWith('.tsx')) return 'tsx'
|
if (filePath.endsWith('.tsx')) return 'tsx';
|
||||||
if (filePath.endsWith('.jsx')) return 'jsx'
|
if (filePath.endsWith('.jsx')) return 'jsx';
|
||||||
if (filePath.endsWith('.vue')) return 'vue'
|
if (filePath.endsWith('.vue')) return 'vue';
|
||||||
if (filePath.endsWith('.svelte')) return 'svelte'
|
if (filePath.endsWith('.svelte')) return 'svelte';
|
||||||
if (filePath.endsWith('.json')) return 'json'
|
if (filePath.endsWith('.json')) return 'json';
|
||||||
if (filePath.endsWith('.html')) return 'html'
|
if (filePath.endsWith('.html')) return 'html';
|
||||||
if (filePath.endsWith('.css')) return 'css'
|
if (filePath.endsWith('.css')) return 'css';
|
||||||
if (filePath.endsWith('.scss')) return 'scss'
|
if (filePath.endsWith('.scss')) return 'scss';
|
||||||
if (filePath.endsWith('.less')) return 'less'
|
if (filePath.endsWith('.less')) return 'less';
|
||||||
if (filePath.endsWith('.md')) return 'markdown'
|
if (filePath.endsWith('.md')) return 'markdown';
|
||||||
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) return 'yaml'
|
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) return 'yaml';
|
||||||
if (filePath.endsWith('.xml')) return 'xml'
|
if (filePath.endsWith('.xml')) return 'xml';
|
||||||
if (filePath.endsWith('.sql')) return 'sql'
|
if (filePath.endsWith('.sql')) return 'sql';
|
||||||
if (filePath.endsWith('.sh')) return 'shell'
|
if (filePath.endsWith('.sh')) return 'shell';
|
||||||
if (filePath.endsWith('.bat')) return 'bat'
|
if (filePath.endsWith('.bat')) return 'bat';
|
||||||
if (filePath.endsWith('.php')) return 'php'
|
if (filePath.endsWith('.php')) return 'php';
|
||||||
if (filePath.endsWith('.java')) return 'java'
|
if (filePath.endsWith('.java')) return 'java';
|
||||||
if (filePath.endsWith('.c')) return 'c'
|
if (filePath.endsWith('.c')) return 'c';
|
||||||
if (filePath.endsWith('.cpp')) return 'cpp'
|
if (filePath.endsWith('.cpp')) return 'cpp';
|
||||||
if (filePath.endsWith('.h')) return 'h'
|
if (filePath.endsWith('.h')) return 'h';
|
||||||
if (filePath.endsWith('.hpp')) return 'hpp'
|
if (filePath.endsWith('.hpp')) return 'hpp';
|
||||||
if (filePath.endsWith('.go')) return 'go'
|
if (filePath.endsWith('.go')) return 'go';
|
||||||
if (filePath.endsWith('.py')) return 'python'
|
if (filePath.endsWith('.py')) return 'python';
|
||||||
if (filePath.endsWith('.rb')) return 'ruby'
|
if (filePath.endsWith('.rb')) return 'ruby';
|
||||||
if (filePath.endsWith('.cs')) return 'csharp'
|
if (filePath.endsWith('.cs')) return 'csharp';
|
||||||
if (filePath.endsWith('.swift')) return 'swift'
|
if (filePath.endsWith('.swift')) return 'swift';
|
||||||
if (filePath.endsWith('.vb')) return 'vb'
|
if (filePath.endsWith('.vb')) return 'vb';
|
||||||
if (filePath.endsWith('.lua')) return 'lua'
|
if (filePath.endsWith('.lua')) return 'lua';
|
||||||
if (filePath.endsWith('.pl')) return 'perl'
|
if (filePath.endsWith('.pl')) return 'perl';
|
||||||
if (filePath.endsWith('.r')) return 'r'
|
if (filePath.endsWith('.r')) return 'r';
|
||||||
return 'plaintext'
|
return 'plaintext';
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal size="full" isOpen={isOpen} onClose={onClose}>
|
<Modal size='full' isOpen={isOpen} onClose={onClose}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex items-center gap-2 bg-content2 bg-opacity-50">
|
<ModalHeader className='flex items-center gap-2 bg-content2 bg-opacity-50'>
|
||||||
<span>编辑文件</span>
|
<span>编辑文件</span>
|
||||||
<Code className="text-xs">{file?.path}</Code>
|
<Code className='text-xs'>{file?.path}</Code>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody className="p-0">
|
<ModalBody className='p-0'>
|
||||||
<div className="h-full">
|
<div className='h-full'>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
height="100%"
|
height='100%'
|
||||||
value={file?.content || ''}
|
value={file?.content || ''}
|
||||||
onChange={onContentChange}
|
onChange={onContentChange}
|
||||||
options={{ wordWrap: 'on' }}
|
options={{ wordWrap: 'on' }}
|
||||||
@ -81,14 +81,14 @@ export default function FileEditModal({
|
|||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" variant="flat" onPress={onClose}>
|
<Button color='primary' variant='flat' onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" onPress={onSave}>
|
<Button color='primary' onPress={onSave}>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader
|
ModalHeader,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import { useRequest } from 'ahooks'
|
import { useRequest } from 'ahooks';
|
||||||
import path from 'path-browserify'
|
import path from 'path-browserify';
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import FileManager from '@/controllers/file_manager'
|
import FileManager from '@/controllers/file_manager';
|
||||||
|
|
||||||
interface FilePreviewModalProps {
|
interface FilePreviewModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@ -19,74 +19,74 @@ interface FilePreviewModalProps {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const videoExts = ['.mp4', '.webm']
|
export const videoExts = ['.mp4', '.webm'];
|
||||||
export const audioExts = ['.mp3', '.wav']
|
export const audioExts = ['.mp3', '.wav'];
|
||||||
|
|
||||||
export const supportedPreviewExts = [...videoExts, ...audioExts]
|
export const supportedPreviewExts = [...videoExts, ...audioExts];
|
||||||
|
|
||||||
export default function FilePreviewModal({
|
export default function FilePreviewModal ({
|
||||||
isOpen,
|
isOpen,
|
||||||
filePath,
|
filePath,
|
||||||
onClose
|
onClose,
|
||||||
}: FilePreviewModalProps) {
|
}: FilePreviewModalProps) {
|
||||||
const ext = path.extname(filePath).toLowerCase()
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
const { data, loading, error, run } = useRequest(
|
const { data, loading, error, run } = useRequest(
|
||||||
async () => FileManager.downloadToURL(filePath),
|
async () => FileManager.downloadToURL(filePath),
|
||||||
{
|
{
|
||||||
refreshDeps: [filePath],
|
refreshDeps: [filePath],
|
||||||
manual: true,
|
manual: true,
|
||||||
refreshDepsAction: () => {
|
refreshDepsAction: () => {
|
||||||
const ext = path.extname(filePath).toLowerCase()
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
if (!filePath || !supportedPreviewExts.includes(ext)) {
|
if (!filePath || !supportedPreviewExts.includes(ext)) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
run()
|
run();
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
run()
|
run();
|
||||||
}
|
}
|
||||||
}, [filePath])
|
}, [filePath]);
|
||||||
|
|
||||||
let contentElement = null
|
let contentElement = null;
|
||||||
if (!supportedPreviewExts.includes(ext)) {
|
if (!supportedPreviewExts.includes(ext)) {
|
||||||
contentElement = <div>暂不支持预览此文件类型</div>
|
contentElement = <div>暂不支持预览此文件类型</div>;
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
contentElement = <div>读取文件失败</div>
|
contentElement = <div>读取文件失败</div>;
|
||||||
} else if (loading || !data) {
|
} else if (loading || !data) {
|
||||||
contentElement = (
|
contentElement = (
|
||||||
<div className="flex justify-center items-center h-full">
|
<div className='flex justify-center items-center h-full'>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
} else if (videoExts.includes(ext)) {
|
} else if (videoExts.includes(ext)) {
|
||||||
contentElement = <video src={data} controls className="max-w-full" />
|
contentElement = <video src={data} controls className='max-w-full' />;
|
||||||
} else if (audioExts.includes(ext)) {
|
} else if (audioExts.includes(ext)) {
|
||||||
contentElement = <audio src={data} controls className="w-full" />
|
contentElement = <audio src={data} controls className='w-full' />;
|
||||||
} else {
|
} else {
|
||||||
contentElement = (
|
contentElement = (
|
||||||
<div className="flex justify-center items-center h-full">
|
<div className='flex justify-center items-center h-full'>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior="inside" size="3xl">
|
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior='inside' size='3xl'>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>文件预览</ModalHeader>
|
<ModalHeader>文件预览</ModalHeader>
|
||||||
<ModalBody className="flex justify-center items-center">
|
<ModalBody className='flex justify-center items-center'>
|
||||||
{contentElement}
|
{contentElement}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" variant="flat" onPress={onClose}>
|
<Button color='primary' variant='flat' onPress={onClose}>
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Button, ButtonGroup } from '@heroui/button'
|
import { Button, ButtonGroup } from '@heroui/button';
|
||||||
import { Pagination } from '@heroui/pagination'
|
import { Pagination } from '@heroui/pagination';
|
||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import {
|
import {
|
||||||
type Selection,
|
type Selection,
|
||||||
type SortDescriptor,
|
type SortDescriptor,
|
||||||
@ -9,20 +9,20 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow
|
TableRow,
|
||||||
} from '@heroui/table'
|
} from '@heroui/table';
|
||||||
import path from 'path-browserify'
|
import path from 'path-browserify';
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi';
|
||||||
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi'
|
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi';
|
||||||
import { PhotoSlider } from 'react-photo-view'
|
import { PhotoSlider } from 'react-photo-view';
|
||||||
|
|
||||||
import FileIcon from '@/components/file_icon'
|
import FileIcon from '@/components/file_icon';
|
||||||
|
|
||||||
import type { FileInfo } from '@/controllers/file_manager'
|
import type { FileInfo } from '@/controllers/file_manager';
|
||||||
|
|
||||||
import { supportedPreviewExts } from './file_preview_modal'
|
import { supportedPreviewExts } from './file_preview_modal';
|
||||||
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button'
|
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button';
|
||||||
|
|
||||||
export interface FileTableProps {
|
export interface FileTableProps {
|
||||||
files: FileInfo[]
|
files: FileInfo[]
|
||||||
@ -42,9 +42,9 @@ export interface FileTableProps {
|
|||||||
onDownload: (filePath: string) => void
|
onDownload: (filePath: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
export default function FileTable({
|
export default function FileTable ({
|
||||||
files,
|
files,
|
||||||
currentPath,
|
currentPath,
|
||||||
loading,
|
loading,
|
||||||
@ -59,40 +59,40 @@ export default function FileTable({
|
|||||||
onMoveRequest,
|
onMoveRequest,
|
||||||
onCopyPath,
|
onCopyPath,
|
||||||
onDelete,
|
onDelete,
|
||||||
onDownload
|
onDownload,
|
||||||
}: FileTableProps) {
|
}: FileTableProps) {
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1);
|
||||||
const pages = Math.ceil(files.length / PAGE_SIZE) || 1
|
const pages = Math.ceil(files.length / PAGE_SIZE) || 1;
|
||||||
const start = (page - 1) * PAGE_SIZE
|
const start = (page - 1) * PAGE_SIZE;
|
||||||
const end = start + PAGE_SIZE
|
const end = start + PAGE_SIZE;
|
||||||
const displayFiles = files.slice(start, end)
|
const displayFiles = files.slice(start, end);
|
||||||
const [showImage, setShowImage] = useState(false)
|
const [showImage, setShowImage] = useState(false);
|
||||||
const [previewIndex, setPreviewIndex] = useState(0)
|
const [previewIndex, setPreviewIndex] = useState(0);
|
||||||
const [previewImages, setPreviewImages] = useState<PreviewImage[]>([])
|
const [previewImages, setPreviewImages] = useState<PreviewImage[]>([]);
|
||||||
|
|
||||||
const addPreviewImage = useCallback((image: PreviewImage) => {
|
const addPreviewImage = useCallback((image: PreviewImage) => {
|
||||||
setPreviewImages((prev) => {
|
setPreviewImages((prev) => {
|
||||||
const exists = prev.some((p) => p.key === image.key)
|
const exists = prev.some((p) => p.key === image.key);
|
||||||
if (exists) return prev
|
if (exists) return prev;
|
||||||
return [...prev, image]
|
return [...prev, image];
|
||||||
})
|
});
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreviewImages([])
|
setPreviewImages([]);
|
||||||
setPreviewIndex(0)
|
setPreviewIndex(0);
|
||||||
setShowImage(false)
|
setShowImage(false);
|
||||||
setPage(1)
|
setPage(1);
|
||||||
}, [currentPath])
|
}, [currentPath]);
|
||||||
|
|
||||||
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
||||||
const index = images.findIndex((image) => image.key === name)
|
const index = images.findIndex((image) => image.key === name);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
setPreviewIndex(index)
|
setPreviewIndex(index);
|
||||||
setShowImage(true)
|
setShowImage(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -104,20 +104,20 @@ export default function FileTable({
|
|||||||
onIndexChange={setPreviewIndex}
|
onIndexChange={setPreviewIndex}
|
||||||
/>
|
/>
|
||||||
<Table
|
<Table
|
||||||
aria-label="文件列表"
|
aria-label='文件列表'
|
||||||
sortDescriptor={sortDescriptor}
|
sortDescriptor={sortDescriptor}
|
||||||
onSortChange={onSortChange}
|
onSortChange={onSortChange}
|
||||||
onSelectionChange={onSelectionChange}
|
onSelectionChange={onSelectionChange}
|
||||||
defaultSelectedKeys={[]}
|
defaultSelectedKeys={[]}
|
||||||
selectedKeys={selectedFiles}
|
selectedKeys={selectedFiles}
|
||||||
selectionMode="multiple"
|
selectionMode='multiple'
|
||||||
bottomContent={
|
bottomContent={
|
||||||
<div className="flex w-full justify-center">
|
<div className='flex w-full justify-center'>
|
||||||
<Pagination
|
<Pagination
|
||||||
isCompact
|
isCompact
|
||||||
showControls
|
showControls
|
||||||
showShadow
|
showShadow
|
||||||
color="primary"
|
color='primary'
|
||||||
page={page}
|
page={page}
|
||||||
total={pages}
|
total={pages}
|
||||||
onChange={(page) => setPage(page)}
|
onChange={(page) => setPage(page)}
|
||||||
@ -126,64 +126,65 @@ export default function FileTable({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableColumn key="name" allowsSorting>
|
<TableColumn key='name' allowsSorting>
|
||||||
名称
|
名称
|
||||||
</TableColumn>
|
</TableColumn>
|
||||||
<TableColumn key="type" allowsSorting>
|
<TableColumn key='type' allowsSorting>
|
||||||
类型
|
类型
|
||||||
</TableColumn>
|
</TableColumn>
|
||||||
<TableColumn key="size" allowsSorting>
|
<TableColumn key='size' allowsSorting>
|
||||||
大小
|
大小
|
||||||
</TableColumn>
|
</TableColumn>
|
||||||
<TableColumn key="mtime" allowsSorting>
|
<TableColumn key='mtime' allowsSorting>
|
||||||
修改时间
|
修改时间
|
||||||
</TableColumn>
|
</TableColumn>
|
||||||
<TableColumn key="actions">操作</TableColumn>
|
<TableColumn key='actions'>操作</TableColumn>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody
|
<TableBody
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
loadingContent={
|
loadingContent={
|
||||||
<div className="flex justify-center items-center h-full">
|
<div className='flex justify-center items-center h-full'>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{displayFiles.map((file: FileInfo) => {
|
{displayFiles.map((file: FileInfo) => {
|
||||||
const filePath = path.join(currentPath, file.name)
|
const filePath = path.join(currentPath, file.name);
|
||||||
const ext = path.extname(file.name).toLowerCase()
|
const ext = path.extname(file.name).toLowerCase();
|
||||||
const previewable = supportedPreviewExts.includes(ext)
|
const previewable = supportedPreviewExts.includes(ext);
|
||||||
const images = previewImages
|
const images = previewImages;
|
||||||
return (
|
return (
|
||||||
<TableRow key={file.name}>
|
<TableRow key={file.name}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{imageExts.includes(ext) ? (
|
{imageExts.includes(ext)
|
||||||
<ImageNameButton
|
? (
|
||||||
name={file.name}
|
<ImageNameButton
|
||||||
filePath={filePath}
|
name={file.name}
|
||||||
onPreview={() => onPreviewImage(file.name, images)}
|
filePath={filePath}
|
||||||
onAddPreview={addPreviewImage}
|
onPreview={() => onPreviewImage(file.name, images)}
|
||||||
/>
|
onAddPreview={addPreviewImage}
|
||||||
) : (
|
/>
|
||||||
<Button
|
)
|
||||||
variant="light"
|
: (
|
||||||
onPress={() =>
|
<Button
|
||||||
file.isDirectory
|
variant='light'
|
||||||
? onDirectoryClick(file.name)
|
onPress={() =>
|
||||||
: previewable
|
file.isDirectory
|
||||||
? onPreview(filePath)
|
? onDirectoryClick(file.name)
|
||||||
: onEdit(filePath)
|
: previewable
|
||||||
|
? onPreview(filePath)
|
||||||
|
: onEdit(filePath)}
|
||||||
|
className='text-left justify-start'
|
||||||
|
startContent={
|
||||||
|
<FileIcon
|
||||||
|
name={file.name}
|
||||||
|
isDirectory={file.isDirectory}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
className="text-left justify-start"
|
>
|
||||||
startContent={
|
{file.name}
|
||||||
<FileIcon
|
</Button>
|
||||||
name={file.name}
|
)}
|
||||||
isDirectory={file.isDirectory}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{file.name}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
|
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -193,43 +194,43 @@ export default function FileTable({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<ButtonGroup size="sm">
|
<ButtonGroup size='sm'>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => onRenameRequest(file.name)}
|
onPress={() => onRenameRequest(file.name)}
|
||||||
>
|
>
|
||||||
<BiRename />
|
<BiRename />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => onMoveRequest(file.name)}
|
onPress={() => onMoveRequest(file.name)}
|
||||||
>
|
>
|
||||||
<FiMove />
|
<FiMove />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => onCopyPath(file.name)}
|
onPress={() => onCopyPath(file.name)}
|
||||||
>
|
>
|
||||||
<FiCopy />
|
<FiCopy />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => onDownload(filePath)}
|
onPress={() => onDownload(filePath)}
|
||||||
>
|
>
|
||||||
<FiDownload />
|
<FiDownload />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => onDelete(filePath)}
|
onPress={() => onDelete(filePath)}
|
||||||
>
|
>
|
||||||
<FiTrash2 />
|
<FiTrash2 />
|
||||||
@ -237,10 +238,10 @@ export default function FileTable({
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import { useRequest } from 'ahooks'
|
import { useRequest } from 'ahooks';
|
||||||
import path from 'path-browserify'
|
import path from 'path-browserify';
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import FileManager from '@/controllers/file_manager'
|
import FileManager from '@/controllers/file_manager';
|
||||||
|
|
||||||
import FileIcon from '../file_icon'
|
import FileIcon from '../file_icon';
|
||||||
|
|
||||||
export interface PreviewImage {
|
export interface PreviewImage {
|
||||||
key: string
|
key: string
|
||||||
src: string
|
src: string
|
||||||
alt: string
|
alt: string
|
||||||
}
|
}
|
||||||
export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
|
export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'];
|
||||||
|
|
||||||
export interface ImageNameButtonProps {
|
export interface ImageNameButtonProps {
|
||||||
name: string
|
name: string
|
||||||
@ -23,11 +23,11 @@ export interface ImageNameButtonProps {
|
|||||||
onAddPreview: (image: PreviewImage) => void
|
onAddPreview: (image: PreviewImage) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ImageNameButton({
|
export default function ImageNameButton ({
|
||||||
name,
|
name,
|
||||||
filePath,
|
filePath,
|
||||||
onPreview,
|
onPreview,
|
||||||
onAddPreview
|
onAddPreview,
|
||||||
}: ImageNameButtonProps) {
|
}: ImageNameButtonProps) {
|
||||||
const { data, loading, error, run } = useRequest(
|
const { data, loading, error, run } = useRequest(
|
||||||
async () => FileManager.downloadToURL(filePath),
|
async () => FileManager.downloadToURL(filePath),
|
||||||
@ -35,54 +35,58 @@ export default function ImageNameButton({
|
|||||||
refreshDeps: [filePath],
|
refreshDeps: [filePath],
|
||||||
manual: true,
|
manual: true,
|
||||||
refreshDepsAction: () => {
|
refreshDepsAction: () => {
|
||||||
const ext = path.extname(filePath).toLowerCase()
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
if (!filePath || !imageExts.includes(ext)) {
|
if (!filePath || !imageExts.includes(ext)) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
run()
|
run();
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
onAddPreview({
|
onAddPreview({
|
||||||
key: name,
|
key: name,
|
||||||
src: data,
|
src: data,
|
||||||
alt: name
|
alt: name,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [data, name, onAddPreview])
|
}, [data, name, onAddPreview]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
run()
|
run();
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant='light'
|
||||||
className="text-left justify-start"
|
className='text-left justify-start'
|
||||||
onPress={onPreview}
|
onPress={onPreview}
|
||||||
startContent={
|
startContent={
|
||||||
error ? (
|
error
|
||||||
<FileIcon name={name} isDirectory={false} />
|
? (
|
||||||
) : loading || !data ? (
|
<FileIcon name={name} isDirectory={false} />
|
||||||
<Spinner size="sm" />
|
)
|
||||||
) : (
|
: loading || !data
|
||||||
<Image
|
? (
|
||||||
src={data}
|
<Spinner size='sm' />
|
||||||
alt={name}
|
)
|
||||||
className="w-8 h-8 flex-shrink-0"
|
: (
|
||||||
classNames={{
|
<Image
|
||||||
wrapper: 'w-8 h-8 flex-shrink-0'
|
src={data}
|
||||||
}}
|
alt={name}
|
||||||
radius="sm"
|
className='w-8 h-8 flex-shrink-0'
|
||||||
/>
|
classNames={{
|
||||||
)
|
wrapper: 'w-8 h-8 flex-shrink-0',
|
||||||
|
}}
|
||||||
|
radius='sm'
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader
|
ModalHeader,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import path from 'path-browserify'
|
import path from 'path-browserify';
|
||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import { IoAdd, IoRemove } from 'react-icons/io5'
|
import { IoAdd, IoRemove } from 'react-icons/io5';
|
||||||
|
|
||||||
import FileManager from '@/controllers/file_manager'
|
import FileManager from '@/controllers/file_manager';
|
||||||
|
|
||||||
interface MoveModalProps {
|
interface MoveModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@ -25,68 +25,68 @@ interface MoveModalProps {
|
|||||||
|
|
||||||
// 将 DirectoryTree 改为递归组件
|
// 将 DirectoryTree 改为递归组件
|
||||||
// 新增 selectedPath 属性,用于标识当前选中的目录
|
// 新增 selectedPath 属性,用于标识当前选中的目录
|
||||||
function DirectoryTree({
|
function DirectoryTree ({
|
||||||
basePath,
|
basePath,
|
||||||
onSelect,
|
onSelect,
|
||||||
selectedPath
|
selectedPath,
|
||||||
}: {
|
}: {
|
||||||
basePath: string
|
basePath: string
|
||||||
onSelect: (dir: string) => void
|
onSelect: (dir: string) => void
|
||||||
selectedPath?: string
|
selectedPath?: string
|
||||||
}) {
|
}) {
|
||||||
const [dirs, setDirs] = useState<string[]>([])
|
const [dirs, setDirs] = useState<string[]>([]);
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false);
|
||||||
// 新增loading状态
|
// 新增loading状态
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const fetchDirectories = async () => {
|
const fetchDirectories = async () => {
|
||||||
try {
|
try {
|
||||||
// 直接使用 basePath 调用接口,移除 process.platform 判断
|
// 直接使用 basePath 调用接口,移除 process.platform 判断
|
||||||
const list = await FileManager.listDirectories(basePath)
|
const list = await FileManager.listDirectories(basePath);
|
||||||
setDirs(list.map((item) => item.name))
|
setDirs(list.map((item) => item.name));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ...error handling...
|
// ...error handling...
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleToggle = async () => {
|
const handleToggle = async () => {
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
setExpanded(true)
|
setExpanded(true);
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
await fetchDirectories()
|
await fetchDirectories();
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
setExpanded(false)
|
setExpanded(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
onSelect(basePath)
|
onSelect(basePath);
|
||||||
handleToggle()
|
handleToggle();
|
||||||
}
|
};
|
||||||
|
|
||||||
// 计算显示的名称
|
// 计算显示的名称
|
||||||
const getDisplayName = () => {
|
const getDisplayName = () => {
|
||||||
if (basePath === '/') return '/'
|
if (basePath === '/') return '/';
|
||||||
if (/^[A-Z]:$/i.test(basePath)) return basePath
|
if (/^[A-Z]:$/i.test(basePath)) return basePath;
|
||||||
return path.basename(basePath)
|
return path.basename(basePath);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 更新 Button 的 variant 逻辑
|
// 更新 Button 的 variant 逻辑
|
||||||
const isSeleted = selectedPath === basePath
|
const isSeleted = selectedPath === basePath;
|
||||||
const variant = isSeleted
|
const variant = isSeleted
|
||||||
? 'solid'
|
? 'solid'
|
||||||
: selectedPath && path.dirname(selectedPath) === basePath
|
: selectedPath && path.dirname(selectedPath) === basePath
|
||||||
? 'flat'
|
? 'flat'
|
||||||
: 'light'
|
: 'light';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ml-4">
|
<div className='ml-4'>
|
||||||
<Button
|
<Button
|
||||||
onPress={handleClick}
|
onPress={handleClick}
|
||||||
className="py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md"
|
className='py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md'
|
||||||
size="sm"
|
size='sm'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant={variant}
|
variant={variant}
|
||||||
startContent={
|
startContent={
|
||||||
<div
|
<div
|
||||||
@ -103,66 +103,68 @@ function DirectoryTree({
|
|||||||
</Button>
|
</Button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div>
|
<div>
|
||||||
{loading ? (
|
{loading
|
||||||
<div className="flex py-1 px-8">
|
? (
|
||||||
<Spinner size="sm" color="primary" />
|
<div className='flex py-1 px-8'>
|
||||||
</div>
|
<Spinner size='sm' color='primary' />
|
||||||
) : (
|
</div>
|
||||||
dirs.map((dirName) => {
|
)
|
||||||
const childPath =
|
: (
|
||||||
|
dirs.map((dirName) => {
|
||||||
|
const childPath =
|
||||||
basePath === '/' && /^[A-Z]:$/i.test(dirName)
|
basePath === '/' && /^[A-Z]:$/i.test(dirName)
|
||||||
? dirName
|
? dirName
|
||||||
: path.join(basePath, dirName)
|
: path.join(basePath, dirName);
|
||||||
return (
|
return (
|
||||||
<DirectoryTree
|
<DirectoryTree
|
||||||
key={childPath}
|
key={childPath}
|
||||||
basePath={childPath}
|
basePath={childPath}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
selectedPath={selectedPath}
|
selectedPath={selectedPath}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MoveModal({
|
export default function MoveModal ({
|
||||||
isOpen,
|
isOpen,
|
||||||
moveTargetPath,
|
moveTargetPath,
|
||||||
selectionInfo,
|
selectionInfo,
|
||||||
onClose,
|
onClose,
|
||||||
onMove,
|
onMove,
|
||||||
onSelect
|
onSelect,
|
||||||
}: MoveModalProps) {
|
}: MoveModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>选择目标目录</ModalHeader>
|
<ModalHeader>选择目标目录</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="rounded-md p-2 border border-default-300 overflow-auto max-h-60">
|
<div className='rounded-md p-2 border border-default-300 overflow-auto max-h-60'>
|
||||||
<DirectoryTree
|
<DirectoryTree
|
||||||
basePath="/"
|
basePath='/'
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
selectedPath={moveTargetPath}
|
selectedPath={moveTargetPath}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-default-500 mt-2">
|
<p className='text-sm text-default-500 mt-2'>
|
||||||
当前选择:{moveTargetPath || '未选择'}
|
当前选择:{moveTargetPath || '未选择'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-default-500">移动项:{selectionInfo}</p>
|
<p className='text-sm text-default-500'>移动项:{selectionInfo}</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" variant="flat" onPress={onClose}>
|
<Button color='primary' variant='flat' onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" onPress={onMove}>
|
<Button color='primary' onPress={onMove}>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader
|
ModalHeader,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
|
|
||||||
interface RenameModalProps {
|
interface RenameModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@ -16,29 +16,29 @@ interface RenameModalProps {
|
|||||||
onRename: () => void
|
onRename: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RenameModal({
|
export default function RenameModal ({
|
||||||
isOpen,
|
isOpen,
|
||||||
newFileName,
|
newFileName,
|
||||||
onNameChange,
|
onNameChange,
|
||||||
onClose,
|
onClose,
|
||||||
onRename
|
onRename,
|
||||||
}: RenameModalProps) {
|
}: RenameModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>重命名</ModalHeader>
|
<ModalHeader>重命名</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Input label="新名称" value={newFileName} onChange={onNameChange} />
|
<Input label='新名称' value={newFileName} onChange={onNameChange} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" variant="flat" onPress={onClose}>
|
<Button color='primary' variant='flat' onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" onPress={onRename}>
|
<Button color='primary' onPress={onRename}>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export interface IconWrapperProps {
|
export interface IconWrapperProps {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@ -14,6 +14,6 @@ const IconWrapper = ({ children, className }: IconWrapperProps) => (
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default IconWrapper
|
export default IconWrapper;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { ChevronRightIcon } from '../icons'
|
import { ChevronRightIcon } from '../icons';
|
||||||
|
|
||||||
const ItemCounter = ({ number }: { number: number }) => (
|
const ItemCounter = ({ number }: { number: number }) => (
|
||||||
<div className="flex items-center gap-1 text-default-400">
|
<div className='flex items-center gap-1 text-default-400'>
|
||||||
<span className="text-small">{number}</span>
|
<span className='text-small'>{number}</span>
|
||||||
<ChevronRightIcon className="text-xl" />
|
<ChevronRightIcon className='text-xl' />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default ItemCounter
|
export default ItemCounter;
|
||||||
|
|||||||
@ -1,40 +1,40 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { getReleaseTime } from '@/utils/time'
|
import { getReleaseTime } from '@/utils/time';
|
||||||
|
|
||||||
import type { GithubRelease as GithubReleaseType } from '@/types/github'
|
import type { GithubRelease as GithubReleaseType } from '@/types/github';
|
||||||
|
|
||||||
export interface GithubReleaseProps {
|
export interface GithubReleaseProps {
|
||||||
releaseData: GithubReleaseType
|
releaseData: GithubReleaseType
|
||||||
}
|
}
|
||||||
const GithubRelease: React.FC<GithubReleaseProps> = (props) => {
|
const GithubRelease: React.FC<GithubReleaseProps> = (props) => {
|
||||||
const { releaseData } = props
|
const { releaseData } = props;
|
||||||
const [releaseTime, setReleaseTime] = useState<string | null>(null)
|
const [releaseTime, setReleaseTime] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (releaseData) {
|
if (releaseData) {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
const time = getReleaseTime(releaseData.published_at)
|
const time = getReleaseTime(releaseData.published_at);
|
||||||
|
|
||||||
setReleaseTime(time)
|
setReleaseTime(time);
|
||||||
}, 1000)
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer);
|
||||||
}
|
}
|
||||||
}, [releaseData])
|
}, [releaseData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className='flex flex-col gap-1'>
|
||||||
<span>Releases</span>
|
<span>Releases</span>
|
||||||
<div className="px-2 py-1 rounded-small bg-default-100 bg-opacity-50 backdrop-blur-sm group-data-[hover=true]:bg-default-200">
|
<div className='px-2 py-1 rounded-small bg-default-100 bg-opacity-50 backdrop-blur-sm group-data-[hover=true]:bg-default-200'>
|
||||||
<span className="text-tiny text-default-600">{releaseData.name}</span>
|
<span className='text-tiny text-default-600'>{releaseData.name}</span>
|
||||||
<div className="flex gap-2 text-tiny">
|
<div className='flex gap-2 text-tiny'>
|
||||||
<span className="text-default-500">{releaseTime}</span>
|
<span className='text-default-500'>{releaseTime}</span>
|
||||||
<span className="text-success">Latest</span>
|
<span className='text-success'>Latest</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default GithubRelease
|
export default GithubRelease;
|
||||||
|
|||||||
@ -1,76 +1,78 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useRequest } from 'ahooks'
|
import { useRequest } from 'ahooks';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { IoCopy, IoRefresh } from 'react-icons/io5'
|
import { IoCopy, IoRefresh } from 'react-icons/io5';
|
||||||
|
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request';
|
||||||
|
|
||||||
import PageLoading from './page_loading'
|
import PageLoading from './page_loading';
|
||||||
|
|
||||||
export default function Hitokoto() {
|
export default function Hitokoto () {
|
||||||
const {
|
const {
|
||||||
data: dataOri,
|
data: dataOri,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
run
|
run,
|
||||||
} = useRequest(() => request.get<IHitokoto>('https://hitokoto.152710.xyz/'), {
|
} = useRequest(() => request.get<IHitokoto>('https://hitokoto.152710.xyz/'), {
|
||||||
pollingInterval: 10000,
|
pollingInterval: 10000,
|
||||||
throttleWait: 1000
|
throttleWait: 1000,
|
||||||
})
|
});
|
||||||
const data = dataOri?.data
|
const data = dataOri?.data;
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
try {
|
try {
|
||||||
const text = `${data?.hitokoto} —— ${data?.from} ${data?.from_who}`
|
const text = `${data?.hitokoto} —— ${data?.from} ${data?.from_who}`;
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text);
|
||||||
toast.success('复制成功')
|
toast.success('复制成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('复制失败, 请手动复制')
|
toast.error('复制失败, 请手动复制');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="relative">
|
<div className='relative'>
|
||||||
{loading && <PageLoading />}
|
{loading && <PageLoading />}
|
||||||
{error ? (
|
{error
|
||||||
<div className="text-primary-400">一言加载失败:{error.message}</div>
|
? (
|
||||||
) : (
|
<div className='text-primary-400'>一言加载失败:{error.message}</div>
|
||||||
<>
|
)
|
||||||
<div>{data?.hitokoto}</div>
|
: (
|
||||||
<div className="text-right">
|
<>
|
||||||
—— <span className="text-default-400">{data?.from}</span>{' '}
|
<div>{data?.hitokoto}</div>
|
||||||
{data?.from_who}
|
<div className='text-right'>
|
||||||
</div>
|
—— <span className='text-default-400'>{data?.from}</span>{' '}
|
||||||
</>
|
{data?.from_who}
|
||||||
)}
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<Tooltip content="刷新" placement="top">
|
<Tooltip content='刷新' placement='top'>
|
||||||
<Button
|
<Button
|
||||||
onPress={run}
|
onPress={run}
|
||||||
size="sm"
|
size='sm'
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
>
|
>
|
||||||
<IoRefresh />
|
<IoRefresh />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="复制" placement="top">
|
<Tooltip content='复制' placement='top'>
|
||||||
<Button
|
<Button
|
||||||
onPress={onCopy}
|
onPress={onCopy}
|
||||||
size="sm"
|
size='sm'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
color="success"
|
color='success'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
>
|
>
|
||||||
<IoCopy />
|
<IoCopy />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { motion, useMotionValue, useSpring } from 'motion/react'
|
import { motion, useMotionValue, useSpring } from 'motion/react';
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
const springValues = {
|
const springValues = {
|
||||||
damping: 30,
|
damping: 30,
|
||||||
stiffness: 100,
|
stiffness: 100,
|
||||||
mass: 2
|
mass: 2,
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface HoverTiltedCardProps {
|
export interface HoverTiltedCardProps {
|
||||||
imageSrc: string
|
imageSrc: string
|
||||||
@ -22,7 +22,7 @@ export interface HoverTiltedCardProps {
|
|||||||
displayOverlayContent?: boolean
|
displayOverlayContent?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HoverTiltedCard({
|
export default function HoverTiltedCard ({
|
||||||
imageSrc,
|
imageSrc,
|
||||||
altText = 'NapCat',
|
altText = 'NapCat',
|
||||||
captionText = 'NapCat',
|
captionText = 'NapCat',
|
||||||
@ -34,95 +34,95 @@ export default function HoverTiltedCard({
|
|||||||
rotateAmplitude = 14,
|
rotateAmplitude = 14,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
overlayContent = (
|
overlayContent = (
|
||||||
<div className="text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-primary-600 text-default-100 bg-opacity-80">
|
<div className='text-center mt-6 px-4 py-0.5 shadow-lg rounded-full bg-primary-600 text-default-100 bg-opacity-80'>
|
||||||
NapCat
|
NapCat
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
displayOverlayContent = true
|
displayOverlayContent = true,
|
||||||
}: HoverTiltedCardProps) {
|
}: HoverTiltedCardProps) {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const x = useMotionValue(0)
|
const x = useMotionValue(0);
|
||||||
const y = useMotionValue(0)
|
const y = useMotionValue(0);
|
||||||
const rotateX = useSpring(useMotionValue(0), springValues)
|
const rotateX = useSpring(useMotionValue(0), springValues);
|
||||||
const rotateY = useSpring(useMotionValue(0), springValues)
|
const rotateY = useSpring(useMotionValue(0), springValues);
|
||||||
const scale = useSpring(1, springValues)
|
const scale = useSpring(1, springValues);
|
||||||
const opacity = useSpring(0)
|
const opacity = useSpring(0);
|
||||||
const rotateFigcaption = useSpring(0, {
|
const rotateFigcaption = useSpring(0, {
|
||||||
stiffness: 350,
|
stiffness: 350,
|
||||||
damping: 30,
|
damping: 30,
|
||||||
mass: 1
|
mass: 1,
|
||||||
})
|
});
|
||||||
|
|
||||||
const [lastY, setLastY] = useState(0)
|
const [lastY, setLastY] = useState(0);
|
||||||
|
|
||||||
function handleMouse(e: React.MouseEvent) {
|
function handleMouse (e: React.MouseEvent) {
|
||||||
if (!ref.current) return
|
if (!ref.current) return;
|
||||||
|
|
||||||
const rect = ref.current.getBoundingClientRect()
|
const rect = ref.current.getBoundingClientRect();
|
||||||
const offsetX = e.clientX - rect.left - rect.width / 2
|
const offsetX = e.clientX - rect.left - rect.width / 2;
|
||||||
const offsetY = e.clientY - rect.top - rect.height / 2
|
const offsetY = e.clientY - rect.top - rect.height / 2;
|
||||||
|
|
||||||
const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude
|
const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude;
|
||||||
const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude
|
const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude;
|
||||||
|
|
||||||
rotateX.set(rotationX)
|
rotateX.set(rotationX);
|
||||||
rotateY.set(rotationY)
|
rotateY.set(rotationY);
|
||||||
|
|
||||||
x.set(e.clientX - rect.left)
|
x.set(e.clientX - rect.left);
|
||||||
y.set(e.clientY - rect.top)
|
y.set(e.clientY - rect.top);
|
||||||
|
|
||||||
const velocityY = offsetY - lastY
|
const velocityY = offsetY - lastY;
|
||||||
rotateFigcaption.set(-velocityY * 0.6)
|
rotateFigcaption.set(-velocityY * 0.6);
|
||||||
setLastY(offsetY)
|
setLastY(offsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseEnter() {
|
function handleMouseEnter () {
|
||||||
scale.set(scaleOnHover)
|
scale.set(scaleOnHover);
|
||||||
opacity.set(1)
|
opacity.set(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseLeave() {
|
function handleMouseLeave () {
|
||||||
opacity.set(0)
|
opacity.set(0);
|
||||||
scale.set(1)
|
scale.set(1);
|
||||||
rotateX.set(0)
|
rotateX.set(0);
|
||||||
rotateY.set(0)
|
rotateY.set(0);
|
||||||
rotateFigcaption.set(0)
|
rotateFigcaption.set(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<figure
|
<figure
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="relative w-full h-full [perspective:800px] flex flex-col items-center justify-center"
|
className='relative w-full h-full [perspective:800px] flex flex-col items-center justify-center'
|
||||||
style={{
|
style={{
|
||||||
height: containerHeight,
|
height: containerHeight,
|
||||||
width: containerWidth
|
width: containerWidth,
|
||||||
}}
|
}}
|
||||||
onMouseMove={handleMouse}
|
onMouseMove={handleMouse}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative [transform-style:preserve-3d]"
|
className='relative [transform-style:preserve-3d]'
|
||||||
style={{
|
style={{
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
height: imageHeight,
|
height: imageHeight,
|
||||||
rotateX,
|
rotateX,
|
||||||
rotateY,
|
rotateY,
|
||||||
scale
|
scale,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.img
|
<motion.img
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={altText}
|
alt={altText}
|
||||||
className="absolute top-0 left-0 object-cover rounded-md will-change-transform [transform:translateZ(0)] pointer-events-none select-none"
|
className='absolute top-0 left-0 object-cover rounded-md will-change-transform [transform:translateZ(0)] pointer-events-none select-none'
|
||||||
style={{
|
style={{
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
height: imageHeight
|
height: imageHeight,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{displayOverlayContent && overlayContent && (
|
{displayOverlayContent && overlayContent && (
|
||||||
<motion.div className="absolute top-0 left-0 right-0 z-10 flex justify-center will-change-transform [transform:translateZ(30px)]">
|
<motion.div className='absolute top-0 left-0 right-0 z-10 flex justify-center will-change-transform [transform:translateZ(30px)]'>
|
||||||
{overlayContent}
|
{overlayContent}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@ -130,17 +130,17 @@ export default function HoverTiltedCard({
|
|||||||
|
|
||||||
{showTooltip && (
|
{showTooltip && (
|
||||||
<motion.figcaption
|
<motion.figcaption
|
||||||
className="pointer-events-none absolute left-0 top-0 rounded-md bg-white px-2 py-1 text-sm text-default-900 opacity-0 z-10 hidden sm:block"
|
className='pointer-events-none absolute left-0 top-0 rounded-md bg-white px-2 py-1 text-sm text-default-900 opacity-0 z-10 hidden sm:block'
|
||||||
style={{
|
style={{
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
opacity,
|
opacity,
|
||||||
rotate: rotateFigcaption
|
rotate: rotateFigcaption,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{captionText}
|
{captionText}
|
||||||
</motion.figcaption>
|
</motion.figcaption>
|
||||||
)}
|
)}
|
||||||
</figure>
|
</figure>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
export interface FileInputProps {
|
export interface FileInputProps {
|
||||||
onChange: (file: File) => Promise<void> | void
|
onChange: (file: File) => Promise<void> | void
|
||||||
@ -13,32 +13,32 @@ const FileInput: React.FC<FileInputProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
label,
|
label,
|
||||||
accept
|
accept,
|
||||||
}) => {
|
}) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div className="flex items-end gap-2">
|
<div className='flex items-end gap-2'>
|
||||||
<div className="flex-grow">
|
<div className='flex-grow'>
|
||||||
<Input
|
<Input
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
label={label}
|
label={label}
|
||||||
type="file"
|
type='file'
|
||||||
placeholder="选择文件"
|
placeholder='选择文件'
|
||||||
accept={accept}
|
accept={accept}
|
||||||
onChange={async (e) => {
|
onChange={async (e) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
await onChange(file)
|
await onChange(file);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
if (inputRef.current) inputRef.current.value = ''
|
if (inputRef.current) inputRef.current.value = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -47,23 +47,23 @@ const FileInput: React.FC<FileInputProps> = ({
|
|||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
if (onDelete) await onDelete()
|
if (onDelete) await onDelete();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
if (inputRef.current) inputRef.current.value = ''
|
if (inputRef.current) inputRef.current.value = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
size="sm"
|
size='sm'
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FileInput
|
export default FileInput;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react';
|
||||||
|
|
||||||
export interface ImageInputProps {
|
export interface ImageInputProps {
|
||||||
onChange: (base64: string) => void
|
onChange: (base64: string) => void
|
||||||
@ -10,47 +10,47 @@ export interface ImageInputProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => {
|
const ImageInput: React.FC<ImageInputProps> = ({ onChange, value, label }) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
return (
|
return (
|
||||||
<div className="flex items-end gap-2">
|
<div className='flex items-end gap-2'>
|
||||||
<div className="w-5 h-5 flex-shrink-0">
|
<div className='w-5 h-5 flex-shrink-0'>
|
||||||
<Image
|
<Image
|
||||||
src={value}
|
src={value}
|
||||||
alt={label}
|
alt={label}
|
||||||
className="w-5 h-5 flex-shrink-0 rounded-none"
|
className='w-5 h-5 flex-shrink-0 rounded-none'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
label={label}
|
label={label}
|
||||||
type="file"
|
type='file'
|
||||||
placeholder="选择图片"
|
placeholder='选择图片'
|
||||||
accept="image/*"
|
accept='image/*'
|
||||||
onChange={async (e) => {
|
onChange={async (e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
const base64 = reader.result as string
|
const base64 = reader.result as string;
|
||||||
onChange(base64)
|
onChange(base64);
|
||||||
}
|
};
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onChange('')
|
onChange('');
|
||||||
if (inputRef.current) inputRef.current.value = ''
|
if (inputRef.current) inputRef.current.value = '';
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
size="sm"
|
size='sm'
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ImageInput
|
export default ImageInput;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Card, CardBody, CardHeader } from '@heroui/card'
|
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||||
import { Select, SelectItem } from '@heroui/select'
|
import { Select, SelectItem } from '@heroui/select';
|
||||||
import type { Selection } from '@react-types/shared'
|
import type { Selection } from '@react-types/shared';
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { colorizeLogLevel } from '@/utils/terminal'
|
import { colorizeLogLevel } from '@/utils/terminal';
|
||||||
|
|
||||||
import PageLoading from '../page_loading'
|
import PageLoading from '../page_loading';
|
||||||
import XTerm from '../xterm'
|
import XTerm from '../xterm';
|
||||||
import type { XTermRef } from '../xterm'
|
import type { XTermRef } from '../xterm';
|
||||||
import LogLevelSelect from './log_level_select'
|
import LogLevelSelect from './log_level_select';
|
||||||
|
|
||||||
export interface HistoryLogsProps {
|
export interface HistoryLogsProps {
|
||||||
list: string[]
|
list: string[]
|
||||||
@ -32,80 +32,80 @@ const HistoryLogs: React.FC<HistoryLogsProps> = (props) => {
|
|||||||
listLoading,
|
listLoading,
|
||||||
logContent,
|
logContent,
|
||||||
listError,
|
listError,
|
||||||
logLoading
|
logLoading,
|
||||||
} = props
|
} = props;
|
||||||
const Xterm = useRef<XTermRef>(null)
|
const Xterm = useRef<XTermRef>(null);
|
||||||
|
|
||||||
const [logLevel, setLogLevel] = useState<Selection>(
|
const [logLevel, setLogLevel] = useState<Selection>(
|
||||||
new Set(['info', 'warn', 'error'])
|
new Set(['info', 'warn', 'error'])
|
||||||
)
|
);
|
||||||
|
|
||||||
const logToColored = (log: string) => {
|
const logToColored = (log: string) => {
|
||||||
const logs = log
|
const logs = log
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
const colored = colorizeLogLevel(line)
|
const colored = colorizeLogLevel(line);
|
||||||
return colored
|
return colored;
|
||||||
})
|
})
|
||||||
.filter((log) => {
|
.filter((log) => {
|
||||||
if (logLevel === 'all') {
|
if (logLevel === 'all') {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
return logLevel.has(log.level)
|
return logLevel.has(log.level);
|
||||||
})
|
})
|
||||||
.map((log) => log.content)
|
.map((log) => log.content)
|
||||||
.join('\r\n')
|
.join('\r\n');
|
||||||
return logs
|
return logs;
|
||||||
}
|
};
|
||||||
|
|
||||||
const onDownloadLog = () => {
|
const onDownloadLog = () => {
|
||||||
if (!logContent) {
|
if (!logContent) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const blob = new Blob([logContent], { type: 'text/plain' })
|
const blob = new Blob([logContent], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a');
|
||||||
a.href = url
|
a.href = url;
|
||||||
a.download = `${selectedLog}.log`
|
a.download = `${selectedLog}.log`;
|
||||||
a.click()
|
a.click();
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Xterm.current || !logContent) {
|
if (!Xterm.current || !logContent) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
Xterm.current.clear()
|
Xterm.current.clear();
|
||||||
const _logContent = logToColored(logContent)
|
const _logContent = logToColored(logContent);
|
||||||
Xterm.current.write(_logContent + '\r\nnapcat@webui:~$ ')
|
Xterm.current.write(_logContent + '\r\nnapcat@webui:~$ ');
|
||||||
}, [logContent, logLevel])
|
}, [logContent, logLevel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>历史日志 - NapCat WebUI</title>
|
<title>历史日志 - NapCat WebUI</title>
|
||||||
<Card className="max-w-full h-full bg-opacity-50 backdrop-blur-sm">
|
<Card className='max-w-full h-full bg-opacity-50 backdrop-blur-sm'>
|
||||||
<CardHeader className="flex-row justify-start gap-3">
|
<CardHeader className='flex-row justify-start gap-3'>
|
||||||
<Select
|
<Select
|
||||||
label="选择日志"
|
label='选择日志'
|
||||||
size="sm"
|
size='sm'
|
||||||
isLoading={listLoading}
|
isLoading={listLoading}
|
||||||
errorMessage={listError?.message}
|
errorMessage={listError?.message}
|
||||||
classNames={{
|
classNames={{
|
||||||
trigger:
|
trigger:
|
||||||
'hover:!bg-content3 bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60'
|
'hover:!bg-content3 bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60',
|
||||||
}}
|
}}
|
||||||
placeholder="选择日志"
|
placeholder='选择日志'
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value
|
const value = e.target.value;
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
onSelect(value)
|
onSelect(value);
|
||||||
}}
|
}}
|
||||||
selectedKeys={[selectedLog || '']}
|
selectedKeys={[selectedLog || '']}
|
||||||
items={list.map((name) => ({
|
items={list.map((name) => ({
|
||||||
value: name,
|
value: name,
|
||||||
label: name
|
label: name,
|
||||||
}))}
|
}))}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
@ -118,19 +118,19 @@ const HistoryLogs: React.FC<HistoryLogsProps> = (props) => {
|
|||||||
selectedKeys={logLevel}
|
selectedKeys={logLevel}
|
||||||
onSelectionChange={setLogLevel}
|
onSelectionChange={setLogLevel}
|
||||||
/>
|
/>
|
||||||
<Button className="flex-shrink-0" onPress={onDownloadLog}>
|
<Button className='flex-shrink-0' onPress={onDownloadLog}>
|
||||||
下载日志
|
下载日志
|
||||||
</Button>
|
</Button>
|
||||||
<Button onPress={refreshList}>刷新列表</Button>
|
<Button onPress={refreshList}>刷新列表</Button>
|
||||||
<Button onPress={refreshLog}>刷新日志</Button>
|
<Button onPress={refreshLog}>刷新日志</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="relative">
|
<CardBody className='relative'>
|
||||||
<PageLoading loading={logLoading} />
|
<PageLoading loading={logLoading} />
|
||||||
<XTerm className="w-full h-full" ref={Xterm} />
|
<XTerm className='w-full h-full' ref={Xterm} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HistoryLogs
|
export default HistoryLogs;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
import { Select, SelectItem } from '@heroui/select'
|
import { Select, SelectItem } from '@heroui/select';
|
||||||
import { SharedSelection } from '@heroui/system'
|
import { SharedSelection } from '@heroui/system';
|
||||||
import type { Selection } from '@react-types/shared'
|
import type { Selection } from '@react-types/shared';
|
||||||
|
|
||||||
import { LogLevel } from '@/const/enum'
|
import { LogLevel } from '@/const/enum';
|
||||||
|
|
||||||
export interface LogLevelSelectProps {
|
export interface LogLevelSelectProps {
|
||||||
selectedKeys: Selection
|
selectedKeys: Selection
|
||||||
@ -22,57 +22,57 @@ const logLevelColor: {
|
|||||||
[LogLevel.INFO]: 'primary',
|
[LogLevel.INFO]: 'primary',
|
||||||
[LogLevel.WARN]: 'warning',
|
[LogLevel.WARN]: 'warning',
|
||||||
[LogLevel.ERROR]: 'primary',
|
[LogLevel.ERROR]: 'primary',
|
||||||
[LogLevel.FATAL]: 'primary'
|
[LogLevel.FATAL]: 'primary',
|
||||||
}
|
};
|
||||||
const LogLevelSelect = (props: LogLevelSelectProps) => {
|
const LogLevelSelect = (props: LogLevelSelectProps) => {
|
||||||
const { selectedKeys, onSelectionChange } = props
|
const { selectedKeys, onSelectionChange } = props;
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedKeys}
|
||||||
onSelectionChange={(selectedKeys) => {
|
onSelectionChange={(selectedKeys) => {
|
||||||
if (selectedKeys !== 'all' && selectedKeys?.size === 0) {
|
if (selectedKeys !== 'all' && selectedKeys?.size === 0) {
|
||||||
selectedKeys = 'all'
|
selectedKeys = 'all';
|
||||||
}
|
}
|
||||||
onSelectionChange(selectedKeys)
|
onSelectionChange(selectedKeys);
|
||||||
}}
|
}}
|
||||||
label="日志级别"
|
label='日志级别'
|
||||||
selectionMode="multiple"
|
selectionMode='multiple'
|
||||||
aria-label="Log Level"
|
aria-label='Log Level'
|
||||||
classNames={{
|
classNames={{
|
||||||
label: 'mb-2',
|
label: 'mb-2',
|
||||||
trigger: 'bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60',
|
trigger: 'bg-opacity-50 backdrop-blur-sm hover:!bg-opacity-60',
|
||||||
popoverContent: 'bg-opacity-50 backdrop-blur-sm'
|
popoverContent: 'bg-opacity-50 backdrop-blur-sm',
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size='sm'
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Debug', value: LogLevel.DEBUG },
|
{ label: 'Debug', value: LogLevel.DEBUG },
|
||||||
{ label: 'Info', value: LogLevel.INFO },
|
{ label: 'Info', value: LogLevel.INFO },
|
||||||
{ label: 'Warn', value: LogLevel.WARN },
|
{ label: 'Warn', value: LogLevel.WARN },
|
||||||
{ label: 'Error', value: LogLevel.ERROR },
|
{ label: 'Error', value: LogLevel.ERROR },
|
||||||
{ label: 'Fatal', value: LogLevel.FATAL }
|
{ label: 'Fatal', value: LogLevel.FATAL },
|
||||||
]}
|
]}
|
||||||
renderValue={(value) => {
|
renderValue={(value) => {
|
||||||
if (value.length === 5) {
|
if (value.length === 5) {
|
||||||
return (
|
return (
|
||||||
<Chip size="sm" color="primary" variant="flat">
|
<Chip size='sm' color='primary' variant='flat'>
|
||||||
全部
|
全部
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
{value.map((v) => (
|
{value.map((v) => (
|
||||||
<Chip
|
<Chip
|
||||||
size="sm"
|
size='sm'
|
||||||
key={v.key}
|
key={v.key}
|
||||||
color={logLevelColor[v.data?.value as LogLevel]}
|
color={logLevelColor[v.data?.value as LogLevel]}
|
||||||
variant="flat"
|
variant='flat'
|
||||||
>
|
>
|
||||||
{v.data?.label}
|
{v.data?.label}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
@ -81,7 +81,7 @@ const LogLevelSelect = (props: LogLevelSelectProps) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LogLevelSelect
|
export default LogLevelSelect;
|
||||||
|
|||||||
@ -1,114 +1,114 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import type { Selection } from '@react-types/shared'
|
import type { Selection } from '@react-types/shared';
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { IoDownloadOutline } from 'react-icons/io5'
|
import { IoDownloadOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
import { colorizeLogLevelWithTag } from '@/utils/terminal'
|
import { colorizeLogLevelWithTag } from '@/utils/terminal';
|
||||||
|
|
||||||
import WebUIManager, { Log } from '@/controllers/webui_manager'
|
import WebUIManager, { Log } from '@/controllers/webui_manager';
|
||||||
|
|
||||||
import type { XTermRef } from '../xterm'
|
import type { XTermRef } from '../xterm';
|
||||||
import XTerm from '../xterm'
|
import XTerm from '../xterm';
|
||||||
import LogLevelSelect from './log_level_select'
|
import LogLevelSelect from './log_level_select';
|
||||||
|
|
||||||
const RealTimeLogs = () => {
|
const RealTimeLogs = () => {
|
||||||
const Xterm = useRef<XTermRef>(null)
|
const Xterm = useRef<XTermRef>(null);
|
||||||
const [logLevel, setLogLevel] = useState<Selection>(
|
const [logLevel, setLogLevel] = useState<Selection>(
|
||||||
new Set(['info', 'warn', 'error'])
|
new Set(['info', 'warn', 'error'])
|
||||||
)
|
);
|
||||||
const [dataArr, setDataArr] = useState<Log[]>([])
|
const [dataArr, setDataArr] = useState<Log[]>([]);
|
||||||
|
|
||||||
const onDownloadLog = () => {
|
const onDownloadLog = () => {
|
||||||
const logContent = dataArr
|
const logContent = dataArr
|
||||||
.filter((log) => {
|
.filter((log) => {
|
||||||
if (logLevel === 'all') {
|
if (logLevel === 'all') {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
return logLevel.has(log.level)
|
return logLevel.has(log.level);
|
||||||
})
|
})
|
||||||
.map((log) => colorizeLogLevelWithTag(log.message, log.level))
|
.map((log) => colorizeLogLevelWithTag(log.message, log.level))
|
||||||
.join('\r\n')
|
.join('\r\n');
|
||||||
const blob = new Blob([logContent], { type: 'text/plain' })
|
const blob = new Blob([logContent], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a');
|
||||||
a.href = url
|
a.href = url;
|
||||||
a.download = 'napcat.log'
|
a.download = 'napcat.log';
|
||||||
a.click()
|
a.click();
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url);
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeStream = () => {
|
const writeStream = () => {
|
||||||
try {
|
try {
|
||||||
const _data = dataArr
|
const _data = dataArr
|
||||||
.filter((log) => {
|
.filter((log) => {
|
||||||
if (logLevel === 'all') {
|
if (logLevel === 'all') {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
return logLevel.has(log.level)
|
return logLevel.has(log.level);
|
||||||
})
|
})
|
||||||
.map((log) => colorizeLogLevelWithTag(log.message, log.level))
|
.map((log) => colorizeLogLevelWithTag(log.message, log.level))
|
||||||
.join('\r\n')
|
.join('\r\n');
|
||||||
Xterm.current?.clear()
|
Xterm.current?.clear();
|
||||||
Xterm.current?.write(_data)
|
Xterm.current?.write(_data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
toast.error('获取实时日志失败')
|
toast.error('获取实时日志失败');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
writeStream()
|
writeStream();
|
||||||
}, [logLevel, dataArr])
|
}, [logLevel, dataArr]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscribeLogs = () => {
|
const subscribeLogs = () => {
|
||||||
try {
|
try {
|
||||||
const source = WebUIManager.getRealTimeLogs((data) => {
|
const source = WebUIManager.getRealTimeLogs((data) => {
|
||||||
setDataArr((prev) => {
|
setDataArr((prev) => {
|
||||||
const newData = [...prev, ...data]
|
const newData = [...prev, ...data];
|
||||||
if (newData.length > 1000) {
|
if (newData.length > 1000) {
|
||||||
newData.splice(0, newData.length - 1000)
|
newData.splice(0, newData.length - 1000);
|
||||||
}
|
}
|
||||||
return newData
|
return newData;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
return () => {
|
return () => {
|
||||||
source.close()
|
source.close();
|
||||||
}
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('获取实时日志失败')
|
toast.error('获取实时日志失败');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const close = subscribeLogs()
|
const close = subscribeLogs();
|
||||||
return () => {
|
return () => {
|
||||||
console.log('close')
|
console.log('close');
|
||||||
close?.()
|
close?.();
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>实时日志 - NapCat WebUI</title>
|
<title>实时日志 - NapCat WebUI</title>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<LogLevelSelect
|
<LogLevelSelect
|
||||||
selectedKeys={logLevel}
|
selectedKeys={logLevel}
|
||||||
onSelectionChange={setLogLevel}
|
onSelectionChange={setLogLevel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="flex-shrink-0"
|
className='flex-shrink-0'
|
||||||
onPress={onDownloadLog}
|
onPress={onDownloadLog}
|
||||||
startContent={<IoDownloadOutline className="text-lg" />}
|
startContent={<IoDownloadOutline className='text-lg' />}
|
||||||
>
|
>
|
||||||
下载日志
|
下载日志
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 h-full overflow-hidden">
|
<div className='flex-1 h-full overflow-hidden'>
|
||||||
<XTerm ref={Xterm} />
|
<XTerm ref={Xterm} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default RealTimeLogs
|
export default RealTimeLogs;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import {
|
import {
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
Modal as NextUIModal,
|
Modal as NextUIModal,
|
||||||
useDisclosure
|
useDisclosure,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
content: React.ReactNode
|
content: React.ReactNode
|
||||||
@ -37,8 +37,8 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props;
|
||||||
const { onClose: onNativeClose } = useDisclosure()
|
const { onClose: onNativeClose } = useDisclosure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextUIModal
|
<NextUIModal
|
||||||
@ -46,12 +46,12 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
backdrop={backdrop}
|
backdrop={backdrop}
|
||||||
isDismissable={dismissible}
|
isDismissable={dismissible}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onClose?.()
|
onClose?.();
|
||||||
onNativeClose()
|
onNativeClose();
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
backdrop: 'z-[99]',
|
backdrop: 'z-[99]',
|
||||||
wrapper: 'z-[99]'
|
wrapper: 'z-[99]',
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
@ -59,27 +59,27 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
{(nativeClose) => (
|
{(nativeClose) => (
|
||||||
<>
|
<>
|
||||||
{title && (
|
{title && (
|
||||||
<ModalHeader className="flex flex-col gap-1">{title}</ModalHeader>
|
<ModalHeader className='flex flex-col gap-1'>{title}</ModalHeader>
|
||||||
)}
|
)}
|
||||||
<ModalBody className="break-all">{content}</ModalBody>
|
<ModalBody className='break-all'>{content}</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{showCancel && (
|
{showCancel && (
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="light"
|
variant='light'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onCancel?.()
|
onCancel?.();
|
||||||
nativeClose()
|
nativeClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onConfirm?.()
|
onConfirm?.();
|
||||||
nativeClose()
|
nativeClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{confirmText}
|
{confirmText}
|
||||||
@ -89,9 +89,9 @@ const Modal: React.FC<ModalProps> = React.memo((props) => {
|
|||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</NextUIModal>
|
</NextUIModal>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
Modal.displayName = 'Modal'
|
Modal.displayName = 'Modal';
|
||||||
|
|
||||||
export default Modal
|
export default Modal;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Listbox, ListboxItem } from '@heroui/listbox'
|
import { Listbox, ListboxItem } from '@heroui/listbox';
|
||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import { useRequest } from 'ahooks'
|
import { useRequest } from 'ahooks';
|
||||||
import { MdError } from 'react-icons/md'
|
import { MdError } from 'react-icons/md';
|
||||||
|
|
||||||
import IconWrapper from '@/components/github_info/icon_wrapper'
|
import IconWrapper from '@/components/github_info/icon_wrapper';
|
||||||
import ItemCounter from '@/components/github_info/item_counter'
|
import ItemCounter from '@/components/github_info/item_counter';
|
||||||
import GithubRelease from '@/components/github_info/release'
|
import GithubRelease from '@/components/github_info/release';
|
||||||
import {
|
import {
|
||||||
BookIcon,
|
BookIcon,
|
||||||
BugIcon,
|
BugIcon,
|
||||||
@ -13,193 +13,197 @@ import {
|
|||||||
StarIcon,
|
StarIcon,
|
||||||
TagIcon,
|
TagIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
WatchersIcon
|
WatchersIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons';
|
||||||
|
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request';
|
||||||
import { openUrl } from '@/utils/url'
|
import { openUrl } from '@/utils/url';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GirhubRepo,
|
GirhubRepo,
|
||||||
GithubContributor,
|
GithubContributor,
|
||||||
GithubPullRequest,
|
GithubPullRequest,
|
||||||
GithubRelease as GithubReleaseType
|
GithubRelease as GithubReleaseType,
|
||||||
} from '@/types/github'
|
} from '@/types/github';
|
||||||
|
|
||||||
function displayData(data: number, loading: boolean, error?: Error) {
|
function displayData (data: number, loading: boolean, error?: Error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return <MdError className="text-primary-400" />
|
return <MdError className='text-primary-400' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Spinner size="sm" />
|
return <Spinner size='sm' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ItemCounter number={data} />
|
return <ItemCounter number={data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NapCatRepoInfo() {
|
export default function NapCatRepoInfo () {
|
||||||
// repo info
|
// repo info
|
||||||
const {
|
const {
|
||||||
data: repoOriData,
|
data: repoOriData,
|
||||||
error: repoError,
|
error: repoError,
|
||||||
loading: repoLoading
|
loading: repoLoading,
|
||||||
} = useRequest(() =>
|
} = useRequest(() =>
|
||||||
request.get<GirhubRepo>('https://api.github.com/repos/NapNeko/NapCatQQ')
|
request.get<GirhubRepo>('https://api.github.com/repos/NapNeko/NapCatQQ')
|
||||||
)
|
);
|
||||||
|
|
||||||
// release info
|
// release info
|
||||||
const {
|
const {
|
||||||
data: releaseOriData,
|
data: releaseOriData,
|
||||||
error: releaseError,
|
error: releaseError,
|
||||||
loading: releaseLoading
|
loading: releaseLoading,
|
||||||
} = useRequest(() =>
|
} = useRequest(() =>
|
||||||
request.get<GithubReleaseType[]>(
|
request.get<GithubReleaseType[]>(
|
||||||
'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
|
'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
// pr info
|
// pr info
|
||||||
const {
|
const {
|
||||||
data: prData,
|
data: prData,
|
||||||
error: prError,
|
error: prError,
|
||||||
loading: prLoading
|
loading: prLoading,
|
||||||
} = useRequest(() =>
|
} = useRequest(() =>
|
||||||
request.get<GithubPullRequest[]>(
|
request.get<GithubPullRequest[]>(
|
||||||
'https://api.github.com/repos/NapNeko/NapCatQQ/pulls'
|
'https://api.github.com/repos/NapNeko/NapCatQQ/pulls'
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
// contributors info
|
// contributors info
|
||||||
const {
|
const {
|
||||||
data: contributorsData,
|
data: contributorsData,
|
||||||
error: contributorsError,
|
error: contributorsError,
|
||||||
loading: contributorsLoading
|
loading: contributorsLoading,
|
||||||
} = useRequest(() =>
|
} = useRequest(() =>
|
||||||
request.get<GithubContributor[]>(
|
request.get<GithubContributor[]>(
|
||||||
'https://api.github.com/repos/NapNeko/NapCatQQ/contributors'
|
'https://api.github.com/repos/NapNeko/NapCatQQ/contributors'
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
const repoData = repoOriData?.data
|
const repoData = repoOriData?.data;
|
||||||
const releaseData = releaseOriData?.data?.[0]
|
const releaseData = releaseOriData?.data?.[0];
|
||||||
const prCount = prData?.data?.length || 0
|
const prCount = prData?.data?.length || 0;
|
||||||
const contributorsCount = contributorsData?.data?.length || 0
|
const contributorsCount = contributorsData?.data?.length || 0;
|
||||||
|
|
||||||
const releaseCount = releaseOriData?.data?.length || 0
|
const releaseCount = releaseOriData?.data?.length || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Listbox
|
<Listbox
|
||||||
aria-label="NapCat Repo Info"
|
aria-label='NapCat Repo Info'
|
||||||
className="p-0 gap-0 divide-y divide-default-300/50 dark:divide-default-100/80 bg-content1 max-w-[300px] overflow-visible shadow-small rounded-medium bg-opacity-50 backdrop-blur-sm"
|
className='p-0 gap-0 divide-y divide-default-300/50 dark:divide-default-100/80 bg-content1 max-w-[300px] overflow-visible shadow-small rounded-medium bg-opacity-50 backdrop-blur-sm'
|
||||||
itemClasses={{
|
itemClasses={{
|
||||||
base: 'px-3 first:rounded-t-medium last:rounded-b-medium rounded-none gap-3 h-12 data-[hover=true]:bg-default-100/80'
|
base: 'px-3 first:rounded-t-medium last:rounded-b-medium rounded-none gap-3 h-12 data-[hover=true]:bg-default-100/80',
|
||||||
}}
|
}}
|
||||||
onAction={(key: React.Key) => {
|
onAction={(key: React.Key) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'releases':
|
case 'releases':
|
||||||
openUrl('https://github.com/NapNeko/NapCatQQ/releases', true)
|
openUrl('https://github.com/NapNeko/NapCatQQ/releases', true);
|
||||||
break
|
break;
|
||||||
case 'contributors':
|
case 'contributors':
|
||||||
openUrl(
|
openUrl(
|
||||||
'https://github.com/NapNeko/NapCatQQ/graphs/contributors',
|
'https://github.com/NapNeko/NapCatQQ/graphs/contributors',
|
||||||
true
|
true
|
||||||
)
|
);
|
||||||
break
|
break;
|
||||||
case 'license':
|
case 'license':
|
||||||
openUrl(
|
openUrl(
|
||||||
'https://github.com/NapNeko/NapCatQQ/blob/main/LICENSE',
|
'https://github.com/NapNeko/NapCatQQ/blob/main/LICENSE',
|
||||||
true
|
true
|
||||||
)
|
);
|
||||||
break
|
break;
|
||||||
case 'watchers':
|
case 'watchers':
|
||||||
openUrl('https://github.com/NapNeko/NapCatQQ/watchers', true)
|
openUrl('https://github.com/NapNeko/NapCatQQ/watchers', true);
|
||||||
break
|
break;
|
||||||
case 'star':
|
case 'star':
|
||||||
openUrl('https://github.com/NapNeko/NapCatQQ/stargazers', true)
|
openUrl('https://github.com/NapNeko/NapCatQQ/stargazers', true);
|
||||||
break
|
break;
|
||||||
case 'issues':
|
case 'issues':
|
||||||
openUrl('https://github.com/NapNeko/NapCatQQ/issues', true)
|
openUrl('https://github.com/NapNeko/NapCatQQ/issues', true);
|
||||||
break
|
break;
|
||||||
case 'pull_requests':
|
case 'pull_requests':
|
||||||
openUrl('https://github.com/NapNeko/NapCatQQ/pulls', true)
|
openUrl('https://github.com/NapNeko/NapCatQQ/pulls', true);
|
||||||
break
|
break;
|
||||||
default:
|
default:
|
||||||
openUrl('https://github.com/NapNeko/NapCatQQ', true)
|
openUrl('https://github.com/NapNeko/NapCatQQ', true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key="star"
|
key='star'
|
||||||
endContent={displayData(
|
endContent={displayData(
|
||||||
repoData?.stargazers_count ?? 0,
|
repoData?.stargazers_count ?? 0,
|
||||||
false,
|
false,
|
||||||
repoError
|
repoError
|
||||||
)}
|
)}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-success/10 text-success">
|
<IconWrapper className='bg-success/10 text-success'>
|
||||||
<StarIcon className="text-lg" />
|
<StarIcon className='text-lg' />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Star
|
Star
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key="issues"
|
key='issues'
|
||||||
endContent={displayData(
|
endContent={displayData(
|
||||||
repoData?.open_issues_count ?? 0,
|
repoData?.open_issues_count ?? 0,
|
||||||
false,
|
false,
|
||||||
repoError
|
repoError
|
||||||
)}
|
)}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-success/10 text-success">
|
<IconWrapper className='bg-success/10 text-success'>
|
||||||
<BugIcon className="text-lg" />
|
<BugIcon className='text-lg' />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Issues
|
Issues
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key="pull_requests"
|
key='pull_requests'
|
||||||
endContent={displayData(prCount, prLoading, prError)}
|
endContent={displayData(prCount, prLoading, prError)}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-primary/10 text-primary">
|
<IconWrapper className='bg-primary/10 text-primary'>
|
||||||
<PullRequestIcon className="text-lg" />
|
<PullRequestIcon className='text-lg' />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Pull Requests
|
Pull Requests
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key="releases"
|
key='releases'
|
||||||
className="group h-auto py-3"
|
className='group h-auto py-3'
|
||||||
endContent={
|
endContent={
|
||||||
releaseError ? (
|
releaseError
|
||||||
<MdError className="text-primary-400" />
|
? (
|
||||||
) : releaseLoading ? (
|
<MdError className='text-primary-400' />
|
||||||
<Spinner size="sm" />
|
)
|
||||||
) : (
|
: releaseLoading
|
||||||
<ItemCounter number={releaseCount} />
|
? (
|
||||||
)
|
<Spinner size='sm' />
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ItemCounter number={releaseCount} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-primary/10 text-primary">
|
<IconWrapper className='bg-primary/10 text-primary'>
|
||||||
<TagIcon className="text-lg" />
|
<TagIcon className='text-lg' />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
textValue="Releases"
|
textValue='Releases'
|
||||||
>
|
>
|
||||||
{releaseData && <GithubRelease releaseData={releaseData} />}
|
{releaseData && <GithubRelease releaseData={releaseData} />}
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key="contributors"
|
key='contributors'
|
||||||
endContent={displayData(
|
endContent={displayData(
|
||||||
contributorsCount,
|
contributorsCount,
|
||||||
contributorsLoading,
|
contributorsLoading,
|
||||||
contributorsError
|
contributorsError
|
||||||
)}
|
)}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-warning/10 text-warning">
|
<IconWrapper className='bg-warning/10 text-warning'>
|
||||||
<UsersIcon />
|
<UsersIcon />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
@ -207,14 +211,14 @@ export default function NapCatRepoInfo() {
|
|||||||
Contributors
|
Contributors
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key="watchers"
|
key='watchers'
|
||||||
endContent={displayData(
|
endContent={displayData(
|
||||||
repoData?.watchers_count ?? 0,
|
repoData?.watchers_count ?? 0,
|
||||||
repoLoading,
|
repoLoading,
|
||||||
repoError
|
repoError
|
||||||
)}
|
)}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-default/50 text-foreground">
|
<IconWrapper className='bg-default/50 text-foreground'>
|
||||||
<WatchersIcon />
|
<WatchersIcon />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
@ -222,14 +226,14 @@ export default function NapCatRepoInfo() {
|
|||||||
Watchers
|
Watchers
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key="license"
|
key='license'
|
||||||
endContent={
|
endContent={
|
||||||
<span className="text-small text-default-400">
|
<span className='text-small text-default-400'>
|
||||||
{repoData?.license?.name ?? 'unknown'}
|
{repoData?.license?.name ?? 'unknown'}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
startContent={
|
startContent={
|
||||||
<IconWrapper className="bg-primary/10 text-primary dark:text-primary-500">
|
<IconWrapper className='bg-primary/10 text-primary dark:text-primary-500'>
|
||||||
<BookIcon />
|
<BookIcon />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
}
|
}
|
||||||
@ -237,5 +241,5 @@ export default function NapCatRepoInfo() {
|
|||||||
License
|
License
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +1,87 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { ModalBody, ModalFooter } from '@heroui/modal'
|
import { ModalBody, ModalFooter } from '@heroui/modal';
|
||||||
import { Select, SelectItem } from '@heroui/select'
|
import { Select, SelectItem } from '@heroui/select';
|
||||||
import { ReactElement, useEffect } from 'react'
|
import { ReactElement, useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form'
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import type {
|
import type {
|
||||||
DefaultValues,
|
DefaultValues,
|
||||||
Path,
|
Path,
|
||||||
PathValue,
|
PathValue,
|
||||||
SubmitHandler
|
SubmitHandler,
|
||||||
} from 'react-hook-form'
|
} from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import SwitchCard from '../switch_card'
|
import SwitchCard from '../switch_card';
|
||||||
|
|
||||||
export type FieldTypes = 'input' | 'select' | 'switch'
|
export type FieldTypes = 'input' | 'select' | 'switch';
|
||||||
|
|
||||||
type NetworkConfigType = OneBotConfig['network']
|
type NetworkConfigType = OneBotConfig['network'];
|
||||||
|
|
||||||
export interface Field<T extends keyof OneBotConfig['network']> {
|
export interface Field<T extends keyof OneBotConfig['network']> {
|
||||||
name: keyof NetworkConfigType[T][0]
|
name: keyof NetworkConfigType[T][0];
|
||||||
label: string
|
label: string;
|
||||||
type: FieldTypes
|
type: FieldTypes;
|
||||||
options?: Array<{ key: string; value: string }>
|
options?: Array<{ key: string; value: string; }>;
|
||||||
placeholder?: string
|
placeholder?: string;
|
||||||
isRequired?: boolean
|
isRequired?: boolean;
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean;
|
||||||
description?: string
|
description?: string;
|
||||||
colSpan?: 1 | 2
|
colSpan?: 1 | 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericFormProps<T extends keyof NetworkConfigType> {
|
export interface GenericFormProps<T extends keyof NetworkConfigType> {
|
||||||
data?: NetworkConfigType[T][0]
|
data?: NetworkConfigType[T][0];
|
||||||
defaultValues: DefaultValues<NetworkConfigType[T][0]>
|
defaultValues: DefaultValues<NetworkConfigType[T][0]>;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
onSubmit: (data: NetworkConfigType[T][0]) => Promise<void>
|
onSubmit: (data: NetworkConfigType[T][0]) => Promise<void>;
|
||||||
fields: Array<Field<T>>
|
fields: Array<Field<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GenericForm = <T extends keyof NetworkConfigType>({
|
const GenericForm = <T extends keyof NetworkConfigType> ({
|
||||||
data,
|
data,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
fields
|
fields,
|
||||||
}: GenericFormProps<T>): ReactElement => {
|
}: GenericFormProps<T>): ReactElement => {
|
||||||
const { control, handleSubmit, formState, setValue, reset } = useForm<
|
const { control, handleSubmit, formState, setValue, reset } = useForm<
|
||||||
NetworkConfigType[T][0]
|
NetworkConfigType[T][0]
|
||||||
>({
|
>({
|
||||||
defaultValues
|
defaultValues,
|
||||||
})
|
});
|
||||||
|
|
||||||
const submitAction: SubmitHandler<NetworkConfigType[T][0]> = async (data) => {
|
const submitAction: SubmitHandler<NetworkConfigType[T][0]> = async (data) => {
|
||||||
await onSubmit(data)
|
await onSubmit(data);
|
||||||
onClose()
|
onClose();
|
||||||
}
|
};
|
||||||
|
|
||||||
const _onSubmit = handleSubmit(submitAction, (e) => {
|
const _onSubmit = handleSubmit(submitAction, (e) => {
|
||||||
for (const error in e) {
|
const errors = Object.values(e);
|
||||||
toast.error(e[error]?.message as string)
|
if (errors.length > 0) {
|
||||||
return
|
toast.error(errors[0]?.message as string);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const keys = Object.keys(data) as Path<NetworkConfig[T][0]>[]
|
const keys = Object.keys(data) as Path<NetworkConfig[T][0]>[];
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = data[key] as PathValue<
|
const value = data[key] as PathValue<
|
||||||
NetworkConfig[T][0],
|
NetworkConfig[T][0],
|
||||||
Path<NetworkConfig[T][0]>
|
Path<NetworkConfig[T][0]>
|
||||||
>
|
>;
|
||||||
setValue(key, value)
|
setValue(key, value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reset()
|
reset();
|
||||||
}
|
}
|
||||||
}, [data, reset, setValue])
|
}, [data, reset, setValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="grid grid-cols-2 gap-y-4 gap-x-2 w-full">
|
<div className='grid grid-cols-2 gap-y-4 gap-x-2 w-full'>
|
||||||
{fields.map((field) => (
|
{fields.map((field) => (
|
||||||
<div
|
<div
|
||||||
key={field.name as string}
|
key={field.name as string}
|
||||||
@ -93,9 +93,9 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
rules={
|
rules={
|
||||||
field.isRequired
|
field.isRequired
|
||||||
? {
|
? {
|
||||||
required: `请填写${field.label}`
|
required: `请填写${field.label}`,
|
||||||
}
|
}
|
||||||
: void 0
|
: undefined
|
||||||
}
|
}
|
||||||
render={({ field: controllerField }) => {
|
render={({ field: controllerField }) => {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
@ -103,15 +103,14 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
value={controllerField.value as string}
|
value={controllerField.value as string}
|
||||||
onChange={controllerField.onChange}
|
onValueChange={(value) => controllerField.onChange(value)}
|
||||||
onBlur={controllerField.onBlur}
|
|
||||||
ref={controllerField.ref}
|
ref={controllerField.ref}
|
||||||
isRequired={field.isRequired}
|
isRequired={field.isRequired}
|
||||||
isDisabled={field.isDisabled}
|
isDisabled={field.isDisabled}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@ -129,7 +128,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
)) || <></>}
|
)) || <></>}
|
||||||
</Select>
|
</Select>
|
||||||
)
|
);
|
||||||
case 'switch':
|
case 'switch':
|
||||||
return (
|
return (
|
||||||
<SwitchCard
|
<SwitchCard
|
||||||
@ -138,9 +137,9 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
description={field.description}
|
description={field.description}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
default:
|
default:
|
||||||
return <></>
|
return <></>;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -150,15 +149,15 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
isDisabled={formState.isSubmitting}
|
isDisabled={formState.isSubmitting}
|
||||||
variant="light"
|
variant='light'
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
>
|
>
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
isLoading={formState.isSubmitting}
|
isLoading={formState.isSubmitting}
|
||||||
onPress={() => _onSubmit()}
|
onPress={() => _onSubmit()}
|
||||||
>
|
>
|
||||||
@ -166,16 +165,16 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default GenericForm
|
export default GenericForm;
|
||||||
export function random_token(length: number) {
|
export function random_token (length: number) {
|
||||||
const chars =
|
const chars =
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?'
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?';
|
||||||
let result = ''
|
let result = '';
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import GenericForm, { random_token } from './generic_form'
|
import GenericForm, { random_token } from './generic_form';
|
||||||
import type { Field } from './generic_form'
|
import type { Field } from './generic_form';
|
||||||
|
|
||||||
export interface HTTPClientFormProps {
|
export interface HTTPClientFormProps {
|
||||||
data?: OneBotConfig['network']['httpClients'][0]
|
data?: OneBotConfig['network']['httpClients'][0]
|
||||||
@ -7,12 +7,12 @@ export interface HTTPClientFormProps {
|
|||||||
onSubmit: (data: OneBotConfig['network']['httpClients'][0]) => Promise<void>
|
onSubmit: (data: OneBotConfig['network']['httpClients'][0]) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPClientFormType = OneBotConfig['network']['httpClients']
|
type HTTPClientFormType = OneBotConfig['network']['httpClients'];
|
||||||
|
|
||||||
const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
|
const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
|
||||||
data,
|
data,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValues: HTTPClientFormType[0] = {
|
const defaultValues: HTTPClientFormType[0] = {
|
||||||
enable: false,
|
enable: false,
|
||||||
@ -21,8 +21,8 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
|
|||||||
reportSelfMessage: false,
|
reportSelfMessage: false,
|
||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
token: random_token(16),
|
token: random_token(16),
|
||||||
debug: false
|
debug: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
const fields: Field<'httpClients'>[] = [
|
const fields: Field<'httpClients'>[] = [
|
||||||
{
|
{
|
||||||
@ -30,14 +30,14 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
|
|||||||
label: '启用',
|
label: '启用',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '保存后启用此配置',
|
description: '保存后启用此配置',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'debug',
|
name: 'debug',
|
||||||
label: '开启Debug',
|
label: '开启Debug',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否开启调试模式',
|
description: '是否开启调试模式',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@ -45,21 +45,21 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入名称',
|
placeholder: '请输入名称',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isDisabled: !!data
|
isDisabled: !!data,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'url',
|
name: 'url',
|
||||||
label: 'URL',
|
label: 'URL',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入URL',
|
placeholder: '请输入URL',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reportSelfMessage',
|
name: 'reportSelfMessage',
|
||||||
label: '上报自身消息',
|
label: '上报自身消息',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否上报自身消息',
|
description: '是否上报自身消息',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'messagePostFormat',
|
name: 'messagePostFormat',
|
||||||
@ -69,17 +69,17 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
|
|||||||
isRequired: true,
|
isRequired: true,
|
||||||
options: [
|
options: [
|
||||||
{ key: 'array', value: 'Array' },
|
{ key: 'array', value: 'Array' },
|
||||||
{ key: 'string', value: 'String' }
|
{ key: 'string', value: 'String' },
|
||||||
],
|
],
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'token',
|
name: 'token',
|
||||||
label: 'Token',
|
label: 'Token',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入Token'
|
placeholder: '请输入Token',
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericForm
|
<GenericForm
|
||||||
@ -89,7 +89,7 @@ const HTTPClientForm: React.FC<HTTPClientFormProps> = ({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HTTPClientForm
|
export default HTTPClientForm;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import GenericForm, { random_token } from './generic_form'
|
import GenericForm, { random_token } from './generic_form';
|
||||||
import type { Field } from './generic_form'
|
import type { Field } from './generic_form';
|
||||||
|
|
||||||
export interface HTTPServerFormProps {
|
export interface HTTPServerFormProps {
|
||||||
data?: OneBotConfig['network']['httpServers'][0]
|
data?: OneBotConfig['network']['httpServers'][0]
|
||||||
@ -7,12 +7,12 @@ export interface HTTPServerFormProps {
|
|||||||
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>
|
onSubmit: (data: OneBotConfig['network']['httpServers'][0]) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPServerFormType = OneBotConfig['network']['httpServers']
|
type HTTPServerFormType = OneBotConfig['network']['httpServers'];
|
||||||
|
|
||||||
const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
||||||
data,
|
data,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValues: HTTPServerFormType[0] = {
|
const defaultValues: HTTPServerFormType[0] = {
|
||||||
enable: false,
|
enable: false,
|
||||||
@ -23,8 +23,8 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
|||||||
enableWebsocket: true,
|
enableWebsocket: true,
|
||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
token: random_token(16),
|
token: random_token(16),
|
||||||
debug: false
|
debug: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
const fields: Field<'httpServers'>[] = [
|
const fields: Field<'httpServers'>[] = [
|
||||||
{
|
{
|
||||||
@ -32,14 +32,14 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
|||||||
label: '启用',
|
label: '启用',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '保存后启用此配置',
|
description: '保存后启用此配置',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'debug',
|
name: 'debug',
|
||||||
label: '开启Debug',
|
label: '开启Debug',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否开启调试模式',
|
description: '是否开启调试模式',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@ -47,35 +47,35 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入名称',
|
placeholder: '请输入名称',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isDisabled: !!data
|
isDisabled: !!data,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'host',
|
name: 'host',
|
||||||
label: 'Host',
|
label: 'Host',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入主机地址',
|
placeholder: '请输入主机地址',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'port',
|
name: 'port',
|
||||||
label: 'Port',
|
label: 'Port',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入端口',
|
placeholder: '请输入端口',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableCors',
|
name: 'enableCors',
|
||||||
label: '启用CORS',
|
label: '启用CORS',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否启用CORS跨域',
|
description: '是否启用CORS跨域',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableWebsocket',
|
name: 'enableWebsocket',
|
||||||
label: '启用Websocket',
|
label: '启用Websocket',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否启用Websocket',
|
description: '是否启用Websocket',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'messagePostFormat',
|
name: 'messagePostFormat',
|
||||||
@ -85,16 +85,16 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
|||||||
isRequired: true,
|
isRequired: true,
|
||||||
options: [
|
options: [
|
||||||
{ key: 'array', value: 'Array' },
|
{ key: 'array', value: 'Array' },
|
||||||
{ key: 'string', value: 'String' }
|
{ key: 'string', value: 'String' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'token',
|
name: 'token',
|
||||||
label: 'Token',
|
label: 'Token',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入Token'
|
placeholder: '请输入Token',
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericForm
|
<GenericForm
|
||||||
@ -104,7 +104,7 @@ const HTTPServerForm: React.FC<HTTPServerFormProps> = ({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HTTPServerForm
|
export default HTTPServerForm;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import GenericForm, { random_token } from './generic_form'
|
import GenericForm, { random_token } from './generic_form';
|
||||||
import type { Field } from './generic_form'
|
import type { Field } from './generic_form';
|
||||||
|
|
||||||
export interface HTTPServerSSEFormProps {
|
export interface HTTPServerSSEFormProps {
|
||||||
data?: OneBotConfig['network']['httpSseServers'][0]
|
data?: OneBotConfig['network']['httpSseServers'][0]
|
||||||
@ -9,12 +9,12 @@ export interface HTTPServerSSEFormProps {
|
|||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers']
|
type HTTPServerSSEFormType = OneBotConfig['network']['httpSseServers'];
|
||||||
|
|
||||||
const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
||||||
data,
|
data,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValues: HTTPServerSSEFormType[0] = {
|
const defaultValues: HTTPServerSSEFormType[0] = {
|
||||||
enable: false,
|
enable: false,
|
||||||
@ -26,8 +26,8 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
|||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
token: random_token(16),
|
token: random_token(16),
|
||||||
debug: false,
|
debug: false,
|
||||||
reportSelfMessage: false
|
reportSelfMessage: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
const fields: Field<'httpSseServers'>[] = [
|
const fields: Field<'httpSseServers'>[] = [
|
||||||
{
|
{
|
||||||
@ -35,14 +35,14 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
|||||||
label: '启用',
|
label: '启用',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '保存后启用此配置',
|
description: '保存后启用此配置',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'debug',
|
name: 'debug',
|
||||||
label: '开启Debug',
|
label: '开启Debug',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否开启调试模式',
|
description: '是否开启调试模式',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@ -50,35 +50,35 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入名称',
|
placeholder: '请输入名称',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isDisabled: !!data
|
isDisabled: !!data,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'host',
|
name: 'host',
|
||||||
label: 'Host',
|
label: 'Host',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入主机地址',
|
placeholder: '请输入主机地址',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'port',
|
name: 'port',
|
||||||
label: 'Port',
|
label: 'Port',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入端口',
|
placeholder: '请输入端口',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableCors',
|
name: 'enableCors',
|
||||||
label: '启用CORS',
|
label: '启用CORS',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否启用CORS跨域',
|
description: '是否启用CORS跨域',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableWebsocket',
|
name: 'enableWebsocket',
|
||||||
label: '启用Websocket',
|
label: '启用Websocket',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否启用Websocket',
|
description: '是否启用Websocket',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'messagePostFormat',
|
name: 'messagePostFormat',
|
||||||
@ -88,23 +88,23 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
|||||||
isRequired: true,
|
isRequired: true,
|
||||||
options: [
|
options: [
|
||||||
{ key: 'array', value: 'Array' },
|
{ key: 'array', value: 'Array' },
|
||||||
{ key: 'string', value: 'String' }
|
{ key: 'string', value: 'String' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'token',
|
name: 'token',
|
||||||
label: 'Token',
|
label: 'Token',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入Token'
|
placeholder: '请输入Token',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reportSelfMessage',
|
name: 'reportSelfMessage',
|
||||||
label: '上报自身消息',
|
label: '上报自身消息',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否上报自身消息',
|
description: '是否上报自身消息',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericForm
|
<GenericForm
|
||||||
@ -114,7 +114,7 @@ const HTTPServerSSEForm: React.FC<HTTPServerSSEFormProps> = ({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HTTPServerSSEForm
|
export default HTTPServerSSEForm;
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import { Modal, ModalContent, ModalHeader } from '@heroui/modal'
|
import { Modal, ModalContent, ModalHeader } from '@heroui/modal';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import useConfig from '@/hooks/use-config'
|
import useConfig from '@/hooks/use-config';
|
||||||
|
|
||||||
import HTTPClientForm from './http_client'
|
import HTTPClientForm from './http_client';
|
||||||
import HTTPServerForm from './http_server'
|
import HTTPServerForm from './http_server';
|
||||||
import HTTPServerSSEForm from './http_sse'
|
import HTTPServerSSEForm from './http_sse';
|
||||||
import WebsocketClientForm from './ws_client'
|
import WebsocketClientForm from './ws_client';
|
||||||
import WebsocketServerForm from './ws_server'
|
import WebsocketServerForm from './ws_server';
|
||||||
|
|
||||||
const modalTitle = {
|
const modalTitle = {
|
||||||
httpServers: 'HTTP Server',
|
httpServers: 'HTTP Server',
|
||||||
httpClients: 'HTTP Client',
|
httpClients: 'HTTP Client',
|
||||||
httpSseServers: 'HTTP SSE Server',
|
httpSseServers: 'HTTP SSE Server',
|
||||||
websocketServers: 'Websocket Server',
|
websocketServers: 'Websocket Server',
|
||||||
websocketClients: 'Websocket Client'
|
websocketClients: 'Websocket Client',
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface NetworkFormModalProps<
|
export interface NetworkFormModalProps<
|
||||||
T extends keyof OneBotConfig['network']
|
T extends keyof OneBotConfig['network']
|
||||||
@ -29,26 +29,26 @@ export interface NetworkFormModalProps<
|
|||||||
const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
||||||
props: NetworkFormModalProps<T>
|
props: NetworkFormModalProps<T>
|
||||||
) => {
|
) => {
|
||||||
const { isOpen, onOpenChange, field, data } = props
|
const { isOpen, onOpenChange, field, data } = props;
|
||||||
const { createNetworkConfig, updateNetworkConfig } = useConfig()
|
const { createNetworkConfig, updateNetworkConfig } = useConfig();
|
||||||
const isCreate = !data
|
const isCreate = !data;
|
||||||
|
|
||||||
const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => {
|
const onSubmit = async (data: OneBotConfig['network'][typeof field][0]) => {
|
||||||
try {
|
try {
|
||||||
if (isCreate) {
|
if (isCreate) {
|
||||||
await createNetworkConfig(field, data)
|
await createNetworkConfig(field, data);
|
||||||
} else {
|
} else {
|
||||||
await updateNetworkConfig(field, data)
|
await updateNetworkConfig(field, data);
|
||||||
}
|
}
|
||||||
toast.success('保存配置成功')
|
toast.success('保存配置成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = (error as Error).message
|
const msg = (error as Error).message;
|
||||||
|
|
||||||
toast.error(`保存配置失败: ${msg}`)
|
toast.error(`保存配置失败: ${msg}`);
|
||||||
|
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderFormComponent = (onClose: () => void) => {
|
const renderFormComponent = (onClose: () => void) => {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
@ -59,7 +59,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'httpClients':
|
case 'httpClients':
|
||||||
return (
|
return (
|
||||||
<HTTPClientForm
|
<HTTPClientForm
|
||||||
@ -67,7 +67,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'websocketServers':
|
case 'websocketServers':
|
||||||
return (
|
return (
|
||||||
<WebsocketServerForm
|
<WebsocketServerForm
|
||||||
@ -75,7 +75,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'websocketClients':
|
case 'websocketClients':
|
||||||
return (
|
return (
|
||||||
<WebsocketClientForm
|
<WebsocketClientForm
|
||||||
@ -83,7 +83,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'httpSseServers':
|
case 'httpSseServers':
|
||||||
return (
|
return (
|
||||||
<HTTPServerSSEForm
|
<HTTPServerSSEForm
|
||||||
@ -91,25 +91,25 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
default:
|
default:
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
backdrop="blur"
|
backdrop='blur'
|
||||||
isDismissable={false}
|
isDismissable={false}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size="lg"
|
size='lg'
|
||||||
scrollBehavior="outside"
|
scrollBehavior='outside'
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className='flex flex-col gap-1'>
|
||||||
{modalTitle[field]}
|
{modalTitle[field]}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{renderFormComponent(onClose)}
|
{renderFormComponent(onClose)}
|
||||||
@ -117,7 +117,7 @@ const NetworkFormModal = <T extends keyof OneBotConfig['network']>(
|
|||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default NetworkFormModal
|
export default NetworkFormModal;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import GenericForm, { random_token } from './generic_form'
|
import GenericForm, { random_token } from './generic_form';
|
||||||
import type { Field } from './generic_form'
|
import type { Field } from './generic_form';
|
||||||
|
|
||||||
export interface WebsocketClientFormProps {
|
export interface WebsocketClientFormProps {
|
||||||
data?: OneBotConfig['network']['websocketClients'][0]
|
data?: OneBotConfig['network']['websocketClients'][0]
|
||||||
@ -9,12 +9,12 @@ export interface WebsocketClientFormProps {
|
|||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsocketClientFormType = OneBotConfig['network']['websocketClients']
|
type WebsocketClientFormType = OneBotConfig['network']['websocketClients'];
|
||||||
|
|
||||||
const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
||||||
data,
|
data,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValues: WebsocketClientFormType[0] = {
|
const defaultValues: WebsocketClientFormType[0] = {
|
||||||
enable: false,
|
enable: false,
|
||||||
@ -25,8 +25,8 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
|||||||
token: random_token(16),
|
token: random_token(16),
|
||||||
debug: false,
|
debug: false,
|
||||||
heartInterval: 30000,
|
heartInterval: 30000,
|
||||||
reconnectInterval: 30000
|
reconnectInterval: 30000,
|
||||||
}
|
};
|
||||||
|
|
||||||
const fields: Field<'websocketClients'>[] = [
|
const fields: Field<'websocketClients'>[] = [
|
||||||
{
|
{
|
||||||
@ -34,14 +34,14 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
|||||||
label: '启用',
|
label: '启用',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '保存后启用此配置',
|
description: '保存后启用此配置',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'debug',
|
name: 'debug',
|
||||||
label: '开启Debug',
|
label: '开启Debug',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否开启调试模式',
|
description: '是否开启调试模式',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@ -49,21 +49,21 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入名称',
|
placeholder: '请输入名称',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isDisabled: !!data
|
isDisabled: !!data,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'url',
|
name: 'url',
|
||||||
label: 'URL',
|
label: 'URL',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入URL',
|
placeholder: '请输入URL',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reportSelfMessage',
|
name: 'reportSelfMessage',
|
||||||
label: '上报自身消息',
|
label: '上报自身消息',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否上报自身消息',
|
description: '是否上报自身消息',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'messagePostFormat',
|
name: 'messagePostFormat',
|
||||||
@ -73,15 +73,15 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
|||||||
isRequired: true,
|
isRequired: true,
|
||||||
options: [
|
options: [
|
||||||
{ key: 'array', value: 'Array' },
|
{ key: 'array', value: 'Array' },
|
||||||
{ key: 'string', value: 'String' }
|
{ key: 'string', value: 'String' },
|
||||||
],
|
],
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'token',
|
name: 'token',
|
||||||
label: 'Token',
|
label: 'Token',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入Token'
|
placeholder: '请输入Token',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'heartInterval',
|
name: 'heartInterval',
|
||||||
@ -89,7 +89,7 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入心跳间隔',
|
placeholder: '请输入心跳间隔',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reconnectInterval',
|
name: 'reconnectInterval',
|
||||||
@ -97,9 +97,9 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入重连间隔',
|
placeholder: '请输入重连间隔',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericForm
|
<GenericForm
|
||||||
@ -109,7 +109,7 @@ const WebsocketClientForm: React.FC<WebsocketClientFormProps> = ({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default WebsocketClientForm
|
export default WebsocketClientForm;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import GenericForm, { random_token } from './generic_form'
|
import GenericForm, { random_token } from './generic_form';
|
||||||
import type { Field } from './generic_form'
|
import type { Field } from './generic_form';
|
||||||
|
|
||||||
export interface WebsocketServerFormProps {
|
export interface WebsocketServerFormProps {
|
||||||
data?: OneBotConfig['network']['websocketServers'][0]
|
data?: OneBotConfig['network']['websocketServers'][0]
|
||||||
@ -9,12 +9,12 @@ export interface WebsocketServerFormProps {
|
|||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsocketServerFormType = OneBotConfig['network']['websocketServers']
|
type WebsocketServerFormType = OneBotConfig['network']['websocketServers'];
|
||||||
|
|
||||||
const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
||||||
data,
|
data,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValues: WebsocketServerFormType[0] = {
|
const defaultValues: WebsocketServerFormType[0] = {
|
||||||
enable: false,
|
enable: false,
|
||||||
@ -26,8 +26,8 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
|||||||
messagePostFormat: 'array',
|
messagePostFormat: 'array',
|
||||||
token: random_token(16),
|
token: random_token(16),
|
||||||
debug: false,
|
debug: false,
|
||||||
heartInterval: 30000
|
heartInterval: 30000,
|
||||||
}
|
};
|
||||||
|
|
||||||
const fields: Field<'websocketServers'>[] = [
|
const fields: Field<'websocketServers'>[] = [
|
||||||
{
|
{
|
||||||
@ -35,14 +35,14 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
|||||||
label: '启用',
|
label: '启用',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '保存后启用此配置',
|
description: '保存后启用此配置',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'debug',
|
name: 'debug',
|
||||||
label: '开启Debug',
|
label: '开启Debug',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否开启调试模式',
|
description: '是否开启调试模式',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@ -50,14 +50,14 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入名称',
|
placeholder: '请输入名称',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isDisabled: !!data
|
isDisabled: !!data,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'host',
|
name: 'host',
|
||||||
label: 'Host',
|
label: 'Host',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入主机地址',
|
placeholder: '请输入主机地址',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'port',
|
name: 'port',
|
||||||
@ -65,7 +65,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入端口',
|
placeholder: '请输入端口',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'messagePostFormat',
|
name: 'messagePostFormat',
|
||||||
@ -75,38 +75,38 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
|||||||
isRequired: true,
|
isRequired: true,
|
||||||
options: [
|
options: [
|
||||||
{ key: 'array', value: 'Array' },
|
{ key: 'array', value: 'Array' },
|
||||||
{ key: 'string', value: 'String' }
|
{ key: 'string', value: 'String' },
|
||||||
],
|
],
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reportSelfMessage',
|
name: 'reportSelfMessage',
|
||||||
label: '上报自身消息',
|
label: '上报自身消息',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否上报自身消息',
|
description: '是否上报自身消息',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableForcePushEvent',
|
name: 'enableForcePushEvent',
|
||||||
label: '强制推送事件',
|
label: '强制推送事件',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
description: '是否强制推送事件',
|
description: '是否强制推送事件',
|
||||||
colSpan: 1
|
colSpan: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'token',
|
name: 'token',
|
||||||
label: 'Token',
|
label: 'Token',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入Token'
|
placeholder: '请输入Token',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'heartInterval',
|
name: 'heartInterval',
|
||||||
label: '心跳间隔',
|
label: '心跳间隔',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
placeholder: '请输入心跳间隔',
|
placeholder: '请输入心跳间隔',
|
||||||
isRequired: true
|
isRequired: true,
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericForm
|
<GenericForm
|
||||||
@ -116,7 +116,7 @@ const WebsocketServerForm: React.FC<WebsocketServerFormProps> = ({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default WebsocketServerForm
|
export default WebsocketServerForm;
|
||||||
|
|||||||
@ -1,26 +1,26 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Card, CardBody, CardHeader } from '@heroui/card'
|
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import { Snippet } from '@heroui/snippet'
|
import { Snippet } from '@heroui/snippet';
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react';
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { IoLink, IoSend } from 'react-icons/io5'
|
import { IoLink, IoSend } from 'react-icons/io5';
|
||||||
import { PiCatDuotone } from 'react-icons/pi'
|
import { PiCatDuotone } from 'react-icons/pi';
|
||||||
|
|
||||||
import key from '@/const/key'
|
import key from '@/const/key';
|
||||||
import { OneBotHttpApiContent, OneBotHttpApiPath } from '@/const/ob_api'
|
import { OneBotHttpApiContent, OneBotHttpApiPath } from '@/const/ob_api';
|
||||||
|
|
||||||
import ChatInputModal from '@/components/chat_input/modal'
|
import ChatInputModal from '@/components/chat_input/modal';
|
||||||
import CodeEditor from '@/components/code_editor'
|
import CodeEditor from '@/components/code_editor';
|
||||||
import PageLoading from '@/components/page_loading'
|
import PageLoading from '@/components/page_loading';
|
||||||
|
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request';
|
||||||
import { parseAxiosResponse } from '@/utils/url'
|
import { parseAxiosResponse } from '@/utils/url';
|
||||||
import { generateDefaultJson, parse } from '@/utils/zod'
|
import { generateDefaultJson, parse } from '@/utils/zod';
|
||||||
|
|
||||||
import DisplayStruct from './display_struct'
|
import DisplayStruct from './display_struct';
|
||||||
|
|
||||||
export interface OneBotApiDebugProps {
|
export interface OneBotApiDebugProps {
|
||||||
path: OneBotHttpApiPath
|
path: OneBotHttpApiPath
|
||||||
@ -28,106 +28,104 @@ export interface OneBotApiDebugProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||||
const { path, data } = props
|
const { path, data } = props;
|
||||||
const currentURL = new URL(window.location.origin)
|
const currentURL = new URL(window.location.origin);
|
||||||
currentURL.port = '3000'
|
currentURL.port = '3000';
|
||||||
const defaultHttpUrl = currentURL.href
|
const defaultHttpUrl = currentURL.href;
|
||||||
const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, {
|
const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, {
|
||||||
url: defaultHttpUrl,
|
url: defaultHttpUrl,
|
||||||
token: ''
|
token: '',
|
||||||
})
|
});
|
||||||
const [requestBody, setRequestBody] = useState('{}')
|
const [requestBody, setRequestBody] = useState('{}');
|
||||||
const [responseContent, setResponseContent] = useState('')
|
const [responseContent, setResponseContent] = useState('');
|
||||||
const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false)
|
const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false);
|
||||||
const [isResponseOpen, setIsResponseOpen] = useState(false)
|
const [isResponseOpen, setIsResponseOpen] = useState(false);
|
||||||
const [isFetching, setIsFetching] = useState(false)
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
const responseRef = useRef<HTMLDivElement>(null)
|
const responseRef = useRef<HTMLDivElement>(null);
|
||||||
const parsedRequest = parse(data.request)
|
const parsedRequest = parse(data.request);
|
||||||
const parsedResponse = parse(data.response)
|
const parsedResponse = parse(data.response);
|
||||||
|
|
||||||
const sendRequest = async () => {
|
const sendRequest = async () => {
|
||||||
if (isFetching) return
|
if (isFetching) return;
|
||||||
setIsFetching(true)
|
setIsFetching(true);
|
||||||
const r = toast.loading('正在发送请求...')
|
const r = toast.loading('正在发送请求...');
|
||||||
try {
|
try {
|
||||||
const parsedRequestBody = JSON.parse(requestBody)
|
const parsedRequestBody = JSON.parse(requestBody);
|
||||||
const requestURL = new URL(httpConfig.url)
|
const requestURL = new URL(httpConfig.url);
|
||||||
requestURL.pathname = path
|
requestURL.pathname = path;
|
||||||
request
|
request
|
||||||
.post(requestURL.href, parsedRequestBody, {
|
.post(requestURL.href, parsedRequestBody, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${httpConfig.token}`
|
Authorization: `Bearer ${httpConfig.token}`,
|
||||||
},
|
},
|
||||||
responseType: 'text'
|
responseType: 'text',
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setResponseContent(parseAxiosResponse(res))
|
setResponseContent(parseAxiosResponse(res));
|
||||||
toast.success('请求发送完成,请查看响应')
|
toast.success('请求发送完成,请查看响应');
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error('请求发送失败:' + err.message)
|
toast.error('请求发送失败:' + err.message);
|
||||||
setResponseContent(parseAxiosResponse(err.response))
|
setResponseContent(parseAxiosResponse(err.response));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsFetching(false)
|
setIsFetching(false);
|
||||||
setIsResponseOpen(true)
|
setIsResponseOpen(true);
|
||||||
responseRef.current?.scrollIntoView({
|
responseRef.current?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'start'
|
block: 'start',
|
||||||
})
|
});
|
||||||
toast.dismiss(r)
|
toast.dismiss(r);
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('请求体 JSON 格式错误')
|
toast.error('请求体 JSON 格式错误');
|
||||||
setIsFetching(false)
|
setIsFetching(false);
|
||||||
toast.dismiss(r)
|
toast.dismiss(r);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRequestBody(generateDefaultJson(data.request))
|
setRequestBody(generateDefaultJson(data.request));
|
||||||
setResponseContent('')
|
setResponseContent('');
|
||||||
}, [path])
|
}, [path]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="p-4 pt-14 rounded-lg shadow-md">
|
<section className='p-4 pt-14 rounded-lg shadow-md'>
|
||||||
<h1 className="text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400">
|
<h1 className='text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400'>
|
||||||
<PiCatDuotone />
|
<PiCatDuotone />
|
||||||
{data.description}
|
{data.description}
|
||||||
</h1>
|
</h1>
|
||||||
<h1 className="text-lg font-bold mb-4">
|
<h1 className='text-lg font-bold mb-4'>
|
||||||
<Snippet
|
<Snippet
|
||||||
className="bg-default-50 bg-opacity-50 backdrop-blur-md"
|
className='bg-default-50 bg-opacity-50 backdrop-blur-md'
|
||||||
symbol={<IoLink size={18} className="inline-block mr-1" />}
|
symbol={<IoLink size={18} className='inline-block mr-1' />}
|
||||||
tooltipProps={{
|
tooltipProps={{
|
||||||
content: '点击复制地址'
|
content: '点击复制地址',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
</Snippet>
|
</Snippet>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex gap-2 items-center">
|
<div className='flex gap-2 items-center'>
|
||||||
<Input
|
<Input
|
||||||
label="HTTP URL"
|
label='HTTP URL'
|
||||||
placeholder="输入 HTTP URL"
|
placeholder='输入 HTTP URL'
|
||||||
value={httpConfig.url}
|
value={httpConfig.url}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setHttpConfig({ ...httpConfig, url: e.target.value })
|
setHttpConfig({ ...httpConfig, url: e.target.value })}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Token"
|
label='Token'
|
||||||
placeholder="输入 Token"
|
placeholder='输入 Token'
|
||||||
value={httpConfig.token}
|
value={httpConfig.token}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setHttpConfig({ ...httpConfig, token: e.target.value })
|
setHttpConfig({ ...httpConfig, token: e.target.value })}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onPress={sendRequest}
|
onPress={sendRequest}
|
||||||
color="primary"
|
color='primary'
|
||||||
size="lg"
|
size='lg'
|
||||||
radius="full"
|
radius='full'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
isDisabled={isFetching}
|
isDisabled={isFetching}
|
||||||
>
|
>
|
||||||
@ -135,17 +133,17 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Card
|
<Card
|
||||||
shadow="sm"
|
shadow='sm'
|
||||||
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible"
|
className='my-4 bg-opacity-50 backdrop-blur-md overflow-visible'
|
||||||
>
|
>
|
||||||
<CardHeader className="font-bold text-lg gap-1 pb-0">
|
<CardHeader className='font-bold text-lg gap-1 pb-0'>
|
||||||
<span className="mr-2">请求体</span>
|
<span className='mr-2'>请求体</span>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color='warning'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => setIsCodeEditorOpen(!isCodeEditorOpen)}
|
onPress={() => setIsCodeEditorOpen(!isCodeEditorOpen)}
|
||||||
size="sm"
|
size='sm'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
{isCodeEditorOpen ? '收起' : '展开'}
|
{isCodeEditorOpen ? '收起' : '展开'}
|
||||||
</Button>
|
</Button>
|
||||||
@ -156,24 +154,23 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: isCodeEditorOpen ? 1 : 0,
|
opacity: isCodeEditorOpen ? 1 : 0,
|
||||||
height: isCodeEditorOpen ? 'auto' : 0
|
height: isCodeEditorOpen ? 'auto' : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={requestBody}
|
value={requestBody}
|
||||||
onChange={(value) => setRequestBody(value ?? '')}
|
onChange={(value) => setRequestBody(value ?? '')}
|
||||||
language="json"
|
language='json'
|
||||||
height="400px"
|
height='400px'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-1">
|
<div className='flex justify-end gap-1'>
|
||||||
<ChatInputModal />
|
<ChatInputModal />
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
setRequestBody(generateDefaultJson(data.request))
|
setRequestBody(generateDefaultJson(data.request))}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
填充示例请求体
|
填充示例请求体
|
||||||
</Button>
|
</Button>
|
||||||
@ -182,61 +179,61 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
shadow="sm"
|
shadow='sm'
|
||||||
className="my-4 relative bg-opacity-50 backdrop-blur-md"
|
className='my-4 relative bg-opacity-50 backdrop-blur-md'
|
||||||
>
|
>
|
||||||
<PageLoading loading={isFetching} />
|
<PageLoading loading={isFetching} />
|
||||||
<CardHeader className="font-bold text-lg gap-1 pb-0">
|
<CardHeader className='font-bold text-lg gap-1 pb-0'>
|
||||||
<span className="mr-2">响应</span>
|
<span className='mr-2'>响应</span>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color='warning'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => setIsResponseOpen(!isResponseOpen)}
|
onPress={() => setIsResponseOpen(!isResponseOpen)}
|
||||||
size="sm"
|
size='sm'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
{isResponseOpen ? '收起' : '展开'}
|
{isResponseOpen ? '收起' : '展开'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="success"
|
color='success'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigator.clipboard.writeText(responseContent)
|
navigator.clipboard.writeText(responseContent);
|
||||||
toast.success('响应内容已复制到剪贴板')
|
toast.success('响应内容已复制到剪贴板');
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size='sm'
|
||||||
radius="full"
|
radius='full'
|
||||||
>
|
>
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="overflow-y-auto text-sm"
|
className='overflow-y-auto text-sm'
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: isResponseOpen ? 1 : 0,
|
opacity: isResponseOpen ? 1 : 0,
|
||||||
height: isResponseOpen ? 300 : 0
|
height: isResponseOpen ? 300 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre>
|
<pre>
|
||||||
<code>
|
<code>
|
||||||
{responseContent || (
|
{responseContent || (
|
||||||
<div className="text-gray-400">暂无响应</div>
|
<div className='text-gray-400'>暂无响应</div>
|
||||||
)}
|
)}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="p-2 md:p-4 border border-default-50 dark:border-default-200 rounded-lg backdrop-blur-sm">
|
<div className='p-2 md:p-4 border border-default-50 dark:border-default-200 rounded-lg backdrop-blur-sm'>
|
||||||
<h2 className="text-xl font-semibold mb-2">请求体结构</h2>
|
<h2 className='text-xl font-semibold mb-2'>请求体结构</h2>
|
||||||
<DisplayStruct schema={parsedRequest} />
|
<DisplayStruct schema={parsedRequest} />
|
||||||
<h2 className="text-xl font-semibold mt-4 mb-2">响应体结构</h2>
|
<h2 className='text-xl font-semibold mt-4 mb-2'>响应体结构</h2>
|
||||||
<DisplayStruct schema={parsedResponse} />
|
<DisplayStruct schema={parsedResponse} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotApiDebug
|
export default OneBotApiDebug;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react';
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
import { TbSquareRoundedChevronRightFilled } from 'react-icons/tb'
|
import { TbSquareRoundedChevronRightFilled } from 'react-icons/tb';
|
||||||
|
|
||||||
import type { LiteralValue, ParsedSchema } from '@/utils/zod'
|
import type { LiteralValue, ParsedSchema } from '@/utils/zod';
|
||||||
|
|
||||||
interface DisplayStructProps {
|
interface DisplayStructProps {
|
||||||
schema: ParsedSchema | ParsedSchema[]
|
schema: ParsedSchema | ParsedSchema[]
|
||||||
@ -13,84 +13,86 @@ interface DisplayStructProps {
|
|||||||
|
|
||||||
const SchemaType = ({
|
const SchemaType = ({
|
||||||
type,
|
type,
|
||||||
value
|
value,
|
||||||
}: {
|
}: {
|
||||||
type: string
|
type: string
|
||||||
value?: LiteralValue
|
value?: LiteralValue
|
||||||
}) => {
|
}) => {
|
||||||
let name = type
|
let name = type;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'union':
|
case 'union':
|
||||||
name = '联合类型'
|
name = '联合类型';
|
||||||
break
|
break;
|
||||||
case 'value':
|
case 'value':
|
||||||
name = '固定值'
|
name = '固定值';
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
|
let chipColor: 'primary' | 'success' | 'primary' | 'warning' | 'secondary' =
|
||||||
'primary'
|
'primary';
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'enum':
|
case 'enum':
|
||||||
chipColor = 'warning'
|
chipColor = 'warning';
|
||||||
break
|
break;
|
||||||
case 'union':
|
case 'union':
|
||||||
chipColor = 'secondary'
|
chipColor = 'secondary';
|
||||||
break
|
break;
|
||||||
case 'array':
|
case 'array':
|
||||||
chipColor = 'primary'
|
chipColor = 'primary';
|
||||||
break
|
break;
|
||||||
case 'object':
|
case 'object':
|
||||||
chipColor = 'success'
|
chipColor = 'success';
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip size="sm" color={chipColor} variant="flat">
|
<Chip size='sm' color={chipColor} variant='flat'>
|
||||||
{name}
|
{name}
|
||||||
{type === 'value' && (
|
{type === 'value' && (
|
||||||
<span className="px-1 rounded-full bg-primary-400 text-white ml-1">
|
<span className='px-1 rounded-full bg-primary-400 text-white ml-1'>
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Chip>
|
</Chip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const SchemaLabel: React.FC<{
|
const SchemaLabel: React.FC<{
|
||||||
schema: ParsedSchema
|
schema: ParsedSchema
|
||||||
}> = ({ schema }) => (
|
}> = ({ schema }) => (
|
||||||
<>
|
<>
|
||||||
{Array.isArray(schema.type) ? (
|
{Array.isArray(schema.type)
|
||||||
schema.type.map((type) => (
|
? (
|
||||||
<SchemaType key={type} type={type} value={schema?.value} />
|
schema.type.map((type) => (
|
||||||
))
|
<SchemaType key={type} type={type} value={schema?.value} />
|
||||||
) : (
|
))
|
||||||
<SchemaType type={schema.type} value={schema?.value} />
|
)
|
||||||
)}
|
: (
|
||||||
|
<SchemaType type={schema.type} value={schema?.value} />
|
||||||
|
)}
|
||||||
{schema.optional && (
|
{schema.optional && (
|
||||||
<Chip size="sm" color="default" variant="flat">
|
<Chip size='sm' color='default' variant='flat'>
|
||||||
可选
|
可选
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
{schema.description && (
|
{schema.description && (
|
||||||
<span className="text-xs text-default-400">{schema.description}</span>
|
<span className='text-xs text-default-400'>{schema.description}</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
|
|
||||||
const SchemaContainer: React.FC<{
|
const SchemaContainer: React.FC<{
|
||||||
schema: ParsedSchema
|
schema: ParsedSchema
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}> = ({ schema, children }) => {
|
}> = ({ schema, children }) => {
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const toggleExpand = () => setExpanded(!expanded)
|
const toggleExpand = () => setExpanded(!expanded);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className='mb-2'>
|
||||||
<div
|
<div
|
||||||
onClick={toggleExpand}
|
onClick={toggleExpand}
|
||||||
className="md:cursor-pointer flex items-center gap-1"
|
className='md:cursor-pointer flex items-center gap-1'
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ rotate: 0 }}
|
initial={{ rotate: 0 }}
|
||||||
@ -98,13 +100,13 @@ const SchemaContainer: React.FC<{
|
|||||||
>
|
>
|
||||||
<TbSquareRoundedChevronRightFilled />
|
<TbSquareRoundedChevronRightFilled />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<Tooltip content="点击复制" placement="top" showArrow>
|
<Tooltip content='点击复制' placement='top' showArrow>
|
||||||
<span
|
<span
|
||||||
className="border-b border-transparent border-dashed hover:border-primary-400"
|
className='border-b border-transparent border-dashed hover:border-primary-400'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
navigator.clipboard.writeText(schema.name || '')
|
navigator.clipboard.writeText(schema.name || '');
|
||||||
toast.success('已复制')
|
toast.success('已复制');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{schema.name}
|
{schema.name}
|
||||||
@ -113,30 +115,32 @@ const SchemaContainer: React.FC<{
|
|||||||
<SchemaLabel schema={schema} />
|
<SchemaLabel schema={schema} />
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="ml-5 overflow-hidden"
|
className='ml-5 overflow-hidden'
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: expanded ? 1 : 0, height: expanded ? 'auto' : 0 }}
|
animate={{ opacity: expanded ? 1 : 0, height: expanded ? 'auto' : 0 }}
|
||||||
>
|
>
|
||||||
<div className="h-2"></div>
|
<div className='h-2' />
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
|
const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
|
||||||
if (schema.type === 'object') {
|
if (schema.type === 'object') {
|
||||||
return (
|
return (
|
||||||
<SchemaContainer schema={schema}>
|
<SchemaContainer schema={schema}>
|
||||||
{schema.children && schema.children.length > 0 ? (
|
{schema.children && schema.children.length > 0
|
||||||
schema.children.map((child, i) => (
|
? (
|
||||||
<RenderSchema key={child.name || i} schema={child} />
|
schema.children.map((child, i) => (
|
||||||
))
|
<RenderSchema key={child.name || i} schema={child} />
|
||||||
) : (
|
))
|
||||||
<div>{`{}`}</div>
|
)
|
||||||
)}
|
: (
|
||||||
|
<div>{'{}'}</div>
|
||||||
|
)}
|
||||||
</SchemaContainer>
|
</SchemaContainer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schema.type === 'array' || schema.type === 'union') {
|
if (schema.type === 'array' || schema.type === 'union') {
|
||||||
@ -146,37 +150,37 @@ const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
|
|||||||
<RenderSchema key={child.name || i} schema={child} />
|
<RenderSchema key={child.name || i} schema={child} />
|
||||||
))}
|
))}
|
||||||
</SchemaContainer>
|
</SchemaContainer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schema.type === 'enum' && Array.isArray(schema.enum)) {
|
if (schema.type === 'enum' && Array.isArray(schema.enum)) {
|
||||||
return (
|
return (
|
||||||
<SchemaContainer schema={schema}>
|
<SchemaContainer schema={schema}>
|
||||||
<div className="flex gap-1 items-center">
|
<div className='flex gap-1 items-center'>
|
||||||
{schema.enum?.map((value, i) => (
|
{schema.enum?.map((value, i) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={value?.toString() || i}
|
key={value?.toString() || i}
|
||||||
size="sm"
|
size='sm'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
color="success"
|
color='success'
|
||||||
>
|
>
|
||||||
{value?.toString()}
|
{value?.toString()}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</SchemaContainer>
|
</SchemaContainer>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 flex items-center gap-1 pl-5">
|
<div className='mb-2 flex items-center gap-1 pl-5'>
|
||||||
<Tooltip content="点击复制" placement="top" showArrow>
|
<Tooltip content='点击复制' placement='top' showArrow>
|
||||||
<span
|
<span
|
||||||
className="border-b border-transparent border-dashed hover:border-primary-400 md:cursor-pointer"
|
className='border-b border-transparent border-dashed hover:border-primary-400 md:cursor-pointer'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
navigator.clipboard.writeText(schema.name || '')
|
navigator.clipboard.writeText(schema.name || '');
|
||||||
toast.success('已复制')
|
toast.success('已复制');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{schema.name}
|
{schema.name}
|
||||||
@ -184,19 +188,21 @@ const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<SchemaLabel schema={schema} />
|
<SchemaLabel schema={schema} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const DisplayStruct: React.FC<DisplayStructProps> = ({ schema }) => {
|
const DisplayStruct: React.FC<DisplayStructProps> = ({ schema }) => {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-content2 rounded-lg bg-opacity-50">
|
<div className='p-4 bg-content2 rounded-lg bg-opacity-50'>
|
||||||
{Array.isArray(schema) ? (
|
{Array.isArray(schema)
|
||||||
schema.map((s, i) => <RenderSchema key={s.name || i} schema={s} />)
|
? (
|
||||||
) : (
|
schema.map((s, i) => <RenderSchema key={s.name || i} schema={s} />)
|
||||||
<RenderSchema schema={schema} />
|
)
|
||||||
)}
|
: (
|
||||||
|
<RenderSchema schema={schema} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default DisplayStruct
|
export default DisplayStruct;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Card, CardBody } from '@heroui/card'
|
import { Card, CardBody } from '@heroui/card';
|
||||||
import { Input } from '@heroui/input'
|
import { Input } from '@heroui/input';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react';
|
||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api'
|
import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api';
|
||||||
|
|
||||||
export interface OneBotApiNavListProps {
|
export interface OneBotApiNavListProps {
|
||||||
data: OneBotHttpApi
|
data: OneBotHttpApi
|
||||||
@ -14,8 +14,8 @@ export interface OneBotApiNavListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||||
const { data, selectedApi, onSelect, openSideBar } = props
|
const { data, selectedApi, onSelect, openSideBar } = props;
|
||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('');
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -26,21 +26,21 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
transition={{
|
transition={{
|
||||||
type: openSideBar ? 'spring' : 'tween',
|
type: openSideBar ? 'spring' : 'tween',
|
||||||
stiffness: 150,
|
stiffness: 150,
|
||||||
damping: 15
|
damping: 15,
|
||||||
}}
|
}}
|
||||||
animate={{ width: openSideBar ? '16rem' : '0rem' }}
|
animate={{ width: openSideBar ? '16rem' : '0rem' }}
|
||||||
style={{ overflowY: openSideBar ? 'auto' : 'hidden' }}
|
style={{ overflowY: openSideBar ? 'auto' : 'hidden' }}
|
||||||
>
|
>
|
||||||
<div className="w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0">
|
<div className='w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0'>
|
||||||
<Input
|
<Input
|
||||||
className="sticky top-0 z-10 text-primary-600"
|
className='sticky top-0 z-10 text-primary-600'
|
||||||
classNames={{
|
classNames={{
|
||||||
inputWrapper:
|
inputWrapper:
|
||||||
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
||||||
input: 'bg-transparent !text-primary-400 !placeholder-primary-400'
|
input: 'bg-transparent !text-primary-400 !placeholder-primary-400',
|
||||||
}}
|
}}
|
||||||
radius="full"
|
radius='full'
|
||||||
placeholder="搜索 API"
|
placeholder='搜索 API'
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
isClearable
|
isClearable
|
||||||
@ -49,28 +49,28 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
{Object.entries(data).map(([apiName, api]) => (
|
{Object.entries(data).map(([apiName, api]) => (
|
||||||
<Card
|
<Card
|
||||||
key={apiName}
|
key={apiName}
|
||||||
shadow="none"
|
shadow='none'
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400',
|
'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400',
|
||||||
{
|
{
|
||||||
hidden: !(
|
hidden: !(
|
||||||
apiName.includes(searchValue) ||
|
apiName.includes(searchValue) ||
|
||||||
api.description?.includes(searchValue)
|
api.description?.includes(searchValue)
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600':
|
'!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600':
|
||||||
apiName === selectedApi
|
apiName === selectedApi,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
isPressable
|
isPressable
|
||||||
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
|
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
|
||||||
>
|
>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<h2 className="font-bold">{api.description}</h2>
|
<h2 className='font-bold'>{api.description}</h2>
|
||||||
<div
|
<div
|
||||||
className={clsx('text-sm text-primary-200', {
|
className={clsx('text-sm text-primary-200', {
|
||||||
'!text-primary-400': apiName === selectedApi
|
'!text-primary-400': apiName === selectedApi,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{apiName}
|
{apiName}
|
||||||
@ -80,7 +80,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotApiNavList
|
export default OneBotApiNavList;
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { Avatar } from '@heroui/avatar'
|
import { Avatar } from '@heroui/avatar';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { isOB11GroupMessage } from '@/utils/onebot'
|
import { isOB11GroupMessage } from '@/utils/onebot';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
OB11GroupMessage,
|
OB11GroupMessage,
|
||||||
OB11Message,
|
OB11Message,
|
||||||
OB11PrivateMessage
|
OB11PrivateMessage,
|
||||||
} from '@/types/onebot'
|
} from '@/types/onebot';
|
||||||
|
|
||||||
import { renderMessageContent } from '../render_message'
|
import { renderMessageContent } from '../render_message';
|
||||||
|
|
||||||
export interface OneBotMessageProps {
|
export interface OneBotMessageProps {
|
||||||
data: OB11Message
|
data: OB11Message
|
||||||
@ -26,11 +26,11 @@ export interface OneBotMessagePrivateProps {
|
|||||||
|
|
||||||
const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
|
const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col overflow-hidden flex-1">
|
<div className='h-full flex flex-col overflow-hidden flex-1'>
|
||||||
<div className="flex gap-2 items-center flex-shrink-0">
|
<div className='flex gap-2 items-center flex-shrink-0'>
|
||||||
<div className="font-bold">
|
<div className='font-bold'>
|
||||||
{isOB11GroupMessage(data) && data.sender.card && (
|
{isOB11GroupMessage(data) && data.sender.card && (
|
||||||
<span className="mr-1">{data.sender.card}</span>
|
<span className='mr-1'>{data.sender.card}</span>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -43,12 +43,12 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>({data.sender.user_id})</div>
|
<div>({data.sender.user_id})</div>
|
||||||
<div className="text-sm">消息ID: {data.message_id}</div>
|
<div className='text-sm'>消息ID: {data.message_id}</div>
|
||||||
</div>
|
</div>
|
||||||
<Popover showArrow triggerScaleOnOpen={false}>
|
<Popover showArrow triggerScaleOnOpen={false}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<div className="flex-1 break-all overflow-hidden whitespace-pre-wrap border border-default-100 p-2 rounded-md hover:bg-content2 md:cursor-pointer transition-background relative group">
|
<div className='flex-1 break-all overflow-hidden whitespace-pre-wrap border border-default-100 p-2 rounded-md hover:bg-content2 md:cursor-pointer transition-background relative group'>
|
||||||
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 text-default-300">
|
<div className='absolute right-2 top-2 opacity-0 group-hover:opacity-100 text-default-300'>
|
||||||
点击查看完整消息
|
点击查看完整消息
|
||||||
</div>
|
</div>
|
||||||
{Array.isArray(data.message)
|
{Array.isArray(data.message)
|
||||||
@ -57,7 +57,7 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
|
|||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
<div className="p-2">
|
<div className='p-2'>
|
||||||
{Array.isArray(data.message)
|
{Array.isArray(data.message)
|
||||||
? renderMessageContent(data.message)
|
? renderMessageContent(data.message)
|
||||||
: data.raw_message}
|
: data.raw_message}
|
||||||
@ -65,58 +65,58 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const OneBotMessageGroup: React.FC<OneBotMessageGroupProps> = ({ data }) => {
|
const OneBotMessageGroup: React.FC<OneBotMessageGroupProps> = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-hidden flex flex-col w-full">
|
<div className='h-full overflow-hidden flex flex-col w-full'>
|
||||||
<div className="flex items-center p-1 flex-shrink-0">
|
<div className='flex items-center p-1 flex-shrink-0'>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={`https://p.qlogo.cn/gh/${data.group_id}/${data.group_id}/640/`}
|
src={`https://p.qlogo.cn/gh/${data.group_id}/${data.group_id}/640/`}
|
||||||
alt="群头像"
|
alt='群头像'
|
||||||
size="sm"
|
size='sm'
|
||||||
className="flex-shrink-0 mr-2"
|
className='flex-shrink-0 mr-2'
|
||||||
/>
|
/>
|
||||||
<div>群 {data.group_id}</div>
|
<div>群 {data.group_id}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start p-1 rounded-md h-full flex-1 border border-default-100">
|
<div className='flex items-start p-1 rounded-md h-full flex-1 border border-default-100'>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
|
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
|
||||||
alt="用户头像"
|
alt='用户头像'
|
||||||
size="md"
|
size='md'
|
||||||
className="flex-shrink-0 mr-2"
|
className='flex-shrink-0 mr-2'
|
||||||
/>
|
/>
|
||||||
<MessageContent data={data} />
|
<MessageContent data={data} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const OneBotMessagePrivate: React.FC<OneBotMessagePrivateProps> = ({
|
const OneBotMessagePrivate: React.FC<OneBotMessagePrivateProps> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start p-2 rounded-md h-full flex-1">
|
<div className='flex items-start p-2 rounded-md h-full flex-1'>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
|
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
|
||||||
alt="用户头像"
|
alt='用户头像'
|
||||||
size="md"
|
size='md'
|
||||||
className="flex-shrink-0 mr-2"
|
className='flex-shrink-0 mr-2'
|
||||||
/>
|
/>
|
||||||
<MessageContent data={data} />
|
<MessageContent data={data} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const OneBotMessage: React.FC<OneBotMessageProps> = ({ data }) => {
|
const OneBotMessage: React.FC<OneBotMessageProps> = ({ data }) => {
|
||||||
if (data.message_type === 'group') {
|
if (data.message_type === 'group') {
|
||||||
return <OneBotMessageGroup data={data} />
|
return <OneBotMessageGroup data={data} />;
|
||||||
} else if (data.message_type === 'private') {
|
} else if (data.message_type === 'private') {
|
||||||
return <OneBotMessagePrivate data={data} />
|
return <OneBotMessagePrivate data={data} />;
|
||||||
} else {
|
} else {
|
||||||
return <div>未知消息类型</div>
|
return <div>未知消息类型</div>;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotMessage
|
export default OneBotMessage;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import { getLifecycleColor, getLifecycleName } from '@/utils/onebot'
|
import { getLifecycleColor, getLifecycleName } from '@/utils/onebot';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
OB11Meta,
|
OB11Meta,
|
||||||
OneBot11Heartbeat,
|
OneBot11Heartbeat,
|
||||||
OneBot11Lifecycle
|
OneBot11Lifecycle,
|
||||||
} from '@/types/onebot'
|
} from '@/types/onebot';
|
||||||
|
|
||||||
export interface OneBotDisplayMetaProps {
|
export interface OneBotDisplayMetaProps {
|
||||||
data: OB11Meta
|
data: OB11Meta
|
||||||
@ -21,32 +21,32 @@ export interface OneBotDisplayMetaLifecycleProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OneBotDisplayMetaHeartbeat: React.FC<OneBotDisplayMetaHeartbeatProps> = ({
|
const OneBotDisplayMetaHeartbeat: React.FC<OneBotDisplayMetaHeartbeatProps> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<Chip>心跳</Chip>
|
<Chip>心跳</Chip>
|
||||||
<Chip>间隔 {data.status.interval}ms</Chip>
|
<Chip>间隔 {data.status.interval}ms</Chip>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const OneBotDisplayMetaLifecycle: React.FC<OneBotDisplayMetaLifecycleProps> = ({
|
const OneBotDisplayMetaLifecycle: React.FC<OneBotDisplayMetaLifecycleProps> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className='flex gap-2'>
|
||||||
<Chip>生命周期</Chip>
|
<Chip>生命周期</Chip>
|
||||||
<Chip color={getLifecycleColor(data.sub_type)}>
|
<Chip color={getLifecycleColor(data.sub_type)}>
|
||||||
{getLifecycleName(data.sub_type)}
|
{getLifecycleName(data.sub_type)}
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const OneBotDisplayMeta: React.FC<OneBotDisplayMetaProps> = ({ data }) => {
|
const OneBotDisplayMeta: React.FC<OneBotDisplayMetaProps> = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center">
|
<div className='h-full flex items-center'>
|
||||||
{data.meta_event_type === 'lifecycle' && (
|
{data.meta_event_type === 'lifecycle' && (
|
||||||
<OneBotDisplayMetaLifecycle data={data} />
|
<OneBotDisplayMetaLifecycle data={data} />
|
||||||
)}
|
)}
|
||||||
@ -54,7 +54,7 @@ const OneBotDisplayMeta: React.FC<OneBotDisplayMetaProps> = ({ data }) => {
|
|||||||
<OneBotDisplayMetaHeartbeat data={data} />
|
<OneBotDisplayMetaHeartbeat data={data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotDisplayMeta
|
export default OneBotDisplayMeta;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
|
|
||||||
import { getNoticeTypeName } from '@/utils/onebot'
|
import { getNoticeTypeName } from '@/utils/onebot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OB11Notice,
|
OB11Notice,
|
||||||
@ -18,8 +18,8 @@ import {
|
|||||||
OneBot11GroupUpload,
|
OneBot11GroupUpload,
|
||||||
OneBot11Honor,
|
OneBot11Honor,
|
||||||
OneBot11LuckyKing,
|
OneBot11LuckyKing,
|
||||||
OneBot11Poke
|
OneBot11Poke,
|
||||||
} from '@/types/onebot'
|
} from '@/types/onebot';
|
||||||
|
|
||||||
export interface OneBotNoticeProps {
|
export interface OneBotNoticeProps {
|
||||||
data: OB11Notice
|
data: OB11Notice
|
||||||
@ -30,9 +30,9 @@ export interface NoticeProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GroupUploadNotice: React.FC<NoticeProps<OneBot11GroupUpload>> = ({
|
const GroupUploadNotice: React.FC<NoticeProps<OneBot11GroupUpload>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, user_id, file } = data
|
const { group_id, user_id, file } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -40,26 +40,26 @@ const GroupUploadNotice: React.FC<NoticeProps<OneBot11GroupUpload>> = ({
|
|||||||
<div>文件名: {file.name}</div>
|
<div>文件名: {file.name}</div>
|
||||||
<div>文件大小: {file.size} 字节</div>
|
<div>文件大小: {file.size} 字节</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupAdminNotice: React.FC<NoticeProps<OneBot11GroupAdmin>> = ({
|
const GroupAdminNotice: React.FC<NoticeProps<OneBot11GroupAdmin>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, user_id, sub_type } = data
|
const { group_id, user_id, sub_type } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
<div>变动类型: {sub_type === 'set' ? '设置管理员' : '取消管理员'}</div>
|
<div>变动类型: {sub_type === 'set' ? '设置管理员' : '取消管理员'}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupDecreaseNotice: React.FC<NoticeProps<OneBot11GroupDecrease>> = ({
|
const GroupDecreaseNotice: React.FC<NoticeProps<OneBot11GroupDecrease>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, operator_id, user_id, sub_type } = data
|
const { group_id, operator_id, user_id, sub_type } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -67,13 +67,13 @@ const GroupDecreaseNotice: React.FC<NoticeProps<OneBot11GroupDecrease>> = ({
|
|||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
<div>原因: {sub_type}</div>
|
<div>原因: {sub_type}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupIncreaseNotice: React.FC<NoticeProps<OneBot11GroupIncrease>> = ({
|
const GroupIncreaseNotice: React.FC<NoticeProps<OneBot11GroupIncrease>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, operator_id, user_id, sub_type } = data
|
const { group_id, operator_id, user_id, sub_type } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -81,11 +81,11 @@ const GroupIncreaseNotice: React.FC<NoticeProps<OneBot11GroupIncrease>> = ({
|
|||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
<div>增加类型: {sub_type}</div>
|
<div>增加类型: {sub_type}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupBanNotice: React.FC<NoticeProps<OneBot11GroupBan>> = ({ data }) => {
|
const GroupBanNotice: React.FC<NoticeProps<OneBot11GroupBan>> = ({ data }) => {
|
||||||
const { group_id, operator_id, user_id, sub_type, duration } = data
|
const { group_id, operator_id, user_id, sub_type, duration } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -94,24 +94,24 @@ const GroupBanNotice: React.FC<NoticeProps<OneBot11GroupBan>> = ({ data }) => {
|
|||||||
<div>禁言类型: {sub_type}</div>
|
<div>禁言类型: {sub_type}</div>
|
||||||
<div>禁言时长: {duration} 秒</div>
|
<div>禁言时长: {duration} 秒</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const FriendAddNotice: React.FC<NoticeProps<OneBot11FriendAdd>> = ({
|
const FriendAddNotice: React.FC<NoticeProps<OneBot11FriendAdd>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { user_id } = data
|
const { user_id } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupRecallNotice: React.FC<NoticeProps<OneBot11GroupRecall>> = ({
|
const GroupRecallNotice: React.FC<NoticeProps<OneBot11GroupRecall>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, user_id, operator_id, message_id } = data
|
const { group_id, user_id, operator_id, message_id } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -119,60 +119,60 @@ const GroupRecallNotice: React.FC<NoticeProps<OneBot11GroupRecall>> = ({
|
|||||||
<div>操作者ID: {operator_id}</div>
|
<div>操作者ID: {operator_id}</div>
|
||||||
<div>消息ID: {message_id}</div>
|
<div>消息ID: {message_id}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const FriendRecallNotice: React.FC<NoticeProps<OneBot11FriendRecall>> = ({
|
const FriendRecallNotice: React.FC<NoticeProps<OneBot11FriendRecall>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { user_id, message_id } = data
|
const { user_id, message_id } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
<div>消息ID: {message_id}</div>
|
<div>消息ID: {message_id}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const PokeNotice: React.FC<NoticeProps<OneBot11Poke>> = ({ data }) => {
|
const PokeNotice: React.FC<NoticeProps<OneBot11Poke>> = ({ data }) => {
|
||||||
const { group_id, user_id, target_id } = data
|
const { group_id, user_id, target_id } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
<div>目标ID: {target_id}</div>
|
<div>目标ID: {target_id}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const LuckyKingNotice: React.FC<NoticeProps<OneBot11LuckyKing>> = ({
|
const LuckyKingNotice: React.FC<NoticeProps<OneBot11LuckyKing>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, user_id, target_id } = data
|
const { group_id, user_id, target_id } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
<div>目标ID: {target_id}</div>
|
<div>目标ID: {target_id}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const HonorNotice: React.FC<NoticeProps<OneBot11Honor>> = ({ data }) => {
|
const HonorNotice: React.FC<NoticeProps<OneBot11Honor>> = ({ data }) => {
|
||||||
const { group_id, user_id, honor_type } = data
|
const { group_id, user_id, honor_type } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
<div>用户ID: {user_id}</div>
|
<div>用户ID: {user_id}</div>
|
||||||
<div>荣誉类型: {honor_type}</div>
|
<div>荣誉类型: {honor_type}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupMessageReactionNotice: React.FC<
|
const GroupMessageReactionNotice: React.FC<
|
||||||
NoticeProps<OneBot11GroupMessageReaction>
|
NoticeProps<OneBot11GroupMessageReaction>
|
||||||
> = ({ data }) => {
|
> = ({ data }) => {
|
||||||
const { group_id, user_id, message_id, likes } = data
|
const { group_id, user_id, message_id, likes } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -185,13 +185,13 @@ const GroupMessageReactionNotice: React.FC<
|
|||||||
.join(', ')}
|
.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupEssenceNotice: React.FC<NoticeProps<OneBot11GroupEssence>> = ({
|
const GroupEssenceNotice: React.FC<NoticeProps<OneBot11GroupEssence>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, message_id, sender_id, operator_id, sub_type } = data
|
const { group_id, message_id, sender_id, operator_id, sub_type } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -200,13 +200,13 @@ const GroupEssenceNotice: React.FC<NoticeProps<OneBot11GroupEssence>> = ({
|
|||||||
<div>操作者ID: {operator_id}</div>
|
<div>操作者ID: {operator_id}</div>
|
||||||
<div>操作类型: {sub_type}</div>
|
<div>操作类型: {sub_type}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const GroupCardNotice: React.FC<NoticeProps<OneBot11GroupCard>> = ({
|
const GroupCardNotice: React.FC<NoticeProps<OneBot11GroupCard>> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id, user_id, card_new, card_old } = data
|
const { group_id, user_id, card_new, card_old } = data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>群号: {group_id}</div>
|
<div>群号: {group_id}</div>
|
||||||
@ -214,79 +214,79 @@ const GroupCardNotice: React.FC<NoticeProps<OneBot11GroupCard>> = ({
|
|||||||
<div>新名片: {card_new}</div>
|
<div>新名片: {card_new}</div>
|
||||||
<div>旧名片: {card_old}</div>
|
<div>旧名片: {card_old}</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const OneBotNotice: React.FC<OneBotNoticeProps> = ({ data }) => {
|
const OneBotNotice: React.FC<OneBotNoticeProps> = ({ data }) => {
|
||||||
let NoticeComponent: React.ReactNode
|
let NoticeComponent: React.ReactNode;
|
||||||
switch (data.notice_type) {
|
switch (data.notice_type) {
|
||||||
case OB11NoticeType.GroupUpload:
|
case OB11NoticeType.GroupUpload:
|
||||||
NoticeComponent = <GroupUploadNotice data={data} />
|
NoticeComponent = <GroupUploadNotice data={data} />;
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupAdmin:
|
case OB11NoticeType.GroupAdmin:
|
||||||
NoticeComponent = <GroupAdminNotice data={data} />
|
NoticeComponent = <GroupAdminNotice data={data} />;
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupDecrease:
|
case OB11NoticeType.GroupDecrease:
|
||||||
NoticeComponent = <GroupDecreaseNotice data={data} />
|
NoticeComponent = <GroupDecreaseNotice data={data} />;
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupIncrease:
|
case OB11NoticeType.GroupIncrease:
|
||||||
NoticeComponent = (
|
NoticeComponent = (
|
||||||
<GroupIncreaseNotice data={data as OneBot11GroupIncrease} />
|
<GroupIncreaseNotice data={data as OneBot11GroupIncrease} />
|
||||||
)
|
);
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupBan:
|
case OB11NoticeType.GroupBan:
|
||||||
NoticeComponent = <GroupBanNotice data={data} />
|
NoticeComponent = <GroupBanNotice data={data} />;
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.FriendAdd:
|
case OB11NoticeType.FriendAdd:
|
||||||
NoticeComponent = <FriendAddNotice data={data as OneBot11FriendAdd} />
|
NoticeComponent = <FriendAddNotice data={data as OneBot11FriendAdd} />;
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupRecall:
|
case OB11NoticeType.GroupRecall:
|
||||||
NoticeComponent = <GroupRecallNotice data={data as OneBot11GroupRecall} />
|
NoticeComponent = <GroupRecallNotice data={data as OneBot11GroupRecall} />;
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.FriendRecall:
|
case OB11NoticeType.FriendRecall:
|
||||||
NoticeComponent = (
|
NoticeComponent = (
|
||||||
<FriendRecallNotice data={data as OneBot11FriendRecall} />
|
<FriendRecallNotice data={data as OneBot11FriendRecall} />
|
||||||
)
|
);
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.Notify:
|
case OB11NoticeType.Notify:
|
||||||
switch (data.sub_type) {
|
switch (data.sub_type) {
|
||||||
case 'poke':
|
case 'poke':
|
||||||
NoticeComponent = <PokeNotice data={data as OneBot11Poke} />
|
NoticeComponent = <PokeNotice data={data as OneBot11Poke} />;
|
||||||
break
|
break;
|
||||||
case 'lucky_king':
|
case 'lucky_king':
|
||||||
NoticeComponent = <LuckyKingNotice data={data as OneBot11LuckyKing} />
|
NoticeComponent = <LuckyKingNotice data={data as OneBot11LuckyKing} />;
|
||||||
break
|
break;
|
||||||
case 'honor':
|
case 'honor':
|
||||||
NoticeComponent = <HonorNotice data={data as OneBot11Honor} />
|
NoticeComponent = <HonorNotice data={data as OneBot11Honor} />;
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupMsgEmojiLike:
|
case OB11NoticeType.GroupMsgEmojiLike:
|
||||||
NoticeComponent = (
|
NoticeComponent = (
|
||||||
<GroupMessageReactionNotice
|
<GroupMessageReactionNotice
|
||||||
data={data as OneBot11GroupMessageReaction}
|
data={data as OneBot11GroupMessageReaction}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupEssence:
|
case OB11NoticeType.GroupEssence:
|
||||||
NoticeComponent = (
|
NoticeComponent = (
|
||||||
<GroupEssenceNotice data={data as OneBot11GroupEssence} />
|
<GroupEssenceNotice data={data as OneBot11GroupEssence} />
|
||||||
)
|
);
|
||||||
break
|
break;
|
||||||
case OB11NoticeType.GroupCard:
|
case OB11NoticeType.GroupCard:
|
||||||
NoticeComponent = <GroupCardNotice data={data as OneBot11GroupCard} />
|
NoticeComponent = <GroupCardNotice data={data as OneBot11GroupCard} />;
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center">
|
<div className='flex gap-2 items-center'>
|
||||||
<Chip color="warning" variant="flat">
|
<Chip color='warning' variant='flat'>
|
||||||
通知
|
通知
|
||||||
</Chip>
|
</Chip>
|
||||||
<Chip>{getNoticeTypeName(data.notice_type)}</Chip>
|
<Chip>{getNoticeTypeName(data.notice_type)}</Chip>
|
||||||
{NoticeComponent}
|
{NoticeComponent}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotNotice
|
export default OneBotNotice;
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Card, CardBody, CardHeader } from '@heroui/card'
|
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Snippet } from '@heroui/snippet'
|
import { Snippet } from '@heroui/snippet';
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react';
|
||||||
import { IoCode } from 'react-icons/io5'
|
import { IoCode } from 'react-icons/io5';
|
||||||
|
|
||||||
import OneBotDisplayMeta from '@/components/onebot/display_card/meta'
|
import OneBotDisplayMeta from '@/components/onebot/display_card/meta';
|
||||||
|
|
||||||
import { getEventName, isOB11Event } from '@/utils/onebot'
|
import { getEventName, isOB11Event } from '@/utils/onebot';
|
||||||
import { timestampToDateString } from '@/utils/time'
|
import { timestampToDateString } from '@/utils/time';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AllOB11WsResponse,
|
AllOB11WsResponse,
|
||||||
OB11AllEvent,
|
OB11AllEvent,
|
||||||
OB11Request
|
OB11Request,
|
||||||
} from '@/types/onebot'
|
} from '@/types/onebot';
|
||||||
|
|
||||||
import OneBotMessage from './message'
|
import OneBotMessage from './message';
|
||||||
import OneBotNotice from './notice'
|
import OneBotNotice from './notice';
|
||||||
import OneBotDisplayResponse from './response'
|
import OneBotDisplayResponse from './response';
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, scale: 0.8, y: 50 },
|
hidden: { opacity: 0, scale: 0.8, y: 50 },
|
||||||
@ -26,12 +26,12 @@ const itemVariants = {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
transition: { type: 'spring' as const, stiffness: 300, damping: 20 }
|
transition: { type: 'spring' as const, stiffness: 300, damping: 20 },
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
function RequestComponent({ data: _ }: { data: OB11Request }) {
|
function RequestComponent ({ data: _ }: { data: OB11Request }) {
|
||||||
return <div>Request消息,暂未适配</div>
|
return <div>Request消息,暂未适配</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OneBotItemRenderProps {
|
export interface OneBotItemRenderProps {
|
||||||
@ -42,78 +42,78 @@ export interface OneBotItemRenderProps {
|
|||||||
|
|
||||||
export const getItemSize = (event: OB11AllEvent['post_type']) => {
|
export const getItemSize = (event: OB11AllEvent['post_type']) => {
|
||||||
if (event === 'meta_event') {
|
if (event === 'meta_event') {
|
||||||
return 100
|
return 100;
|
||||||
}
|
}
|
||||||
if (event === 'message') {
|
if (event === 'message') {
|
||||||
return 180
|
return 180;
|
||||||
}
|
}
|
||||||
if (event === 'request') {
|
if (event === 'request') {
|
||||||
return 100
|
return 100;
|
||||||
}
|
}
|
||||||
if (event === 'notice') {
|
if (event === 'notice') {
|
||||||
return 100
|
return 100;
|
||||||
}
|
}
|
||||||
if (event === 'message_sent') {
|
if (event === 'message_sent') {
|
||||||
return 250
|
return 250;
|
||||||
}
|
}
|
||||||
return 100
|
return 100;
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderDetail = (data: AllOB11WsResponse) => {
|
const renderDetail = (data: AllOB11WsResponse) => {
|
||||||
if (isOB11Event(data)) {
|
if (isOB11Event(data)) {
|
||||||
switch (data.post_type) {
|
switch (data.post_type) {
|
||||||
case 'meta_event':
|
case 'meta_event':
|
||||||
return <OneBotDisplayMeta data={data} />
|
return <OneBotDisplayMeta data={data} />;
|
||||||
case 'message':
|
case 'message':
|
||||||
return <OneBotMessage data={data} />
|
return <OneBotMessage data={data} />;
|
||||||
case 'request':
|
case 'request':
|
||||||
return <RequestComponent data={data} />
|
return <RequestComponent data={data} />;
|
||||||
case 'notice':
|
case 'notice':
|
||||||
return <OneBotNotice data={data} />
|
return <OneBotNotice data={data} />;
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
return <OneBotMessage data={data} />
|
return <OneBotMessage data={data} />;
|
||||||
default:
|
default:
|
||||||
return <div>未知类型的消息</div>
|
return <div>未知类型的消息</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <OneBotDisplayResponse data={data} />
|
return <OneBotDisplayResponse data={data} />;
|
||||||
}
|
};
|
||||||
|
|
||||||
const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
||||||
const msg = data[index]
|
const msg = data[index];
|
||||||
const isEvent = isOB11Event(msg)
|
const isEvent = isOB11Event(msg);
|
||||||
return (
|
return (
|
||||||
<div style={style} className="p-1 overflow-visible w-full h-full">
|
<div style={style} className='p-1 overflow-visible w-full h-full'>
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
initial="hidden"
|
initial='hidden'
|
||||||
animate="visible"
|
animate='visible'
|
||||||
className="h-full px-2"
|
className='h-full px-2'
|
||||||
>
|
>
|
||||||
<Card className="w-full h-full py-2 bg-opacity-50 backdrop-blur-sm">
|
<Card className='w-full h-full py-2 bg-opacity-50 backdrop-blur-sm'>
|
||||||
<CardHeader className="py-0 text-default-500 flex-row gap-2">
|
<CardHeader className='py-0 text-default-500 flex-row gap-2'>
|
||||||
<div className="font-bold">
|
<div className='font-bold'>
|
||||||
{isEvent ? getEventName(msg.post_type) : '请求响应'}
|
{isEvent ? getEventName(msg.post_type) : '请求响应'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className='text-sm'>
|
||||||
{isEvent && timestampToDateString(msg.time)}
|
{isEvent && timestampToDateString(msg.time)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className='ml-auto'>
|
||||||
<Popover
|
<Popover
|
||||||
placement="left"
|
placement='left'
|
||||||
showArrow
|
showArrow
|
||||||
classNames={{
|
classNames={{
|
||||||
content: 'max-h-96 max-w-96 overflow-hidden p-0'
|
content: 'max-h-96 max-w-96 overflow-hidden p-0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size='sm'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="text-medium"
|
className='text-medium'
|
||||||
>
|
>
|
||||||
<IoCode />
|
<IoCode />
|
||||||
</Button>
|
</Button>
|
||||||
@ -122,17 +122,17 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
|||||||
<Snippet
|
<Snippet
|
||||||
hideSymbol
|
hideSymbol
|
||||||
tooltipProps={{
|
tooltipProps={{
|
||||||
content: '点击复制'
|
content: '点击复制',
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
copyButton: 'self-start sticky top-0 right-0'
|
copyButton: 'self-start sticky top-0 right-0',
|
||||||
}}
|
}}
|
||||||
className="bg-content1 h-full overflow-y-scroll items-start"
|
className='bg-content1 h-full overflow-y-scroll items-start'
|
||||||
>
|
>
|
||||||
{JSON.stringify(msg, null, 2)
|
{JSON.stringify(msg, null, 2)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line, i) => (
|
.map((line, i) => (
|
||||||
<span key={i} className="whitespace-pre-wrap break-all">
|
<span key={i} className='whitespace-pre-wrap break-all'>
|
||||||
{line}
|
{line}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -141,11 +141,11 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="py-0">{renderDetail(msg)}</CardBody>
|
<CardBody className='py-0'>{renderDetail(msg)}</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotItemRender
|
export default OneBotItemRender;
|
||||||
|
|||||||
@ -1,39 +1,39 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Snippet } from '@heroui/snippet'
|
import { Snippet } from '@heroui/snippet';
|
||||||
|
|
||||||
import { getResponseStatusColor, getResponseStatusText } from '@/utils/onebot'
|
import { getResponseStatusColor, getResponseStatusText } from '@/utils/onebot';
|
||||||
|
|
||||||
import { RequestResponse } from '@/types/onebot'
|
import { RequestResponse } from '@/types/onebot';
|
||||||
|
|
||||||
export interface OneBotDisplayResponseProps {
|
export interface OneBotDisplayResponseProps {
|
||||||
data: RequestResponse
|
data: RequestResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
||||||
data
|
data,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center">
|
<div className='flex gap-2 items-center'>
|
||||||
<Chip color={getResponseStatusColor(data.status)} variant="flat">
|
<Chip color={getResponseStatusColor(data.status)} variant='flat'>
|
||||||
{getResponseStatusText(data.status)}
|
{getResponseStatusText(data.status)}
|
||||||
</Chip>
|
</Chip>
|
||||||
{data.data && (
|
{data.data && (
|
||||||
<Popover
|
<Popover
|
||||||
placement="right"
|
placement='right'
|
||||||
showArrow
|
showArrow
|
||||||
classNames={{
|
classNames={{
|
||||||
content: 'max-h-96 max-w-96 overflow-hidden p-0'
|
content: 'max-h-96 max-w-96 overflow-hidden p-0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size='sm'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="flat"
|
variant='flat'
|
||||||
radius="full"
|
radius='full'
|
||||||
className="text-medium"
|
className='text-medium'
|
||||||
>
|
>
|
||||||
查看数据
|
查看数据
|
||||||
</Button>
|
</Button>
|
||||||
@ -42,17 +42,17 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
|||||||
<Snippet
|
<Snippet
|
||||||
hideSymbol
|
hideSymbol
|
||||||
tooltipProps={{
|
tooltipProps={{
|
||||||
content: '点击复制'
|
content: '点击复制',
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
copyButton: 'self-start sticky top-0 right-0'
|
copyButton: 'self-start sticky top-0 right-0',
|
||||||
}}
|
}}
|
||||||
className="bg-content1 h-full overflow-y-scroll items-start"
|
className='bg-content1 h-full overflow-y-scroll items-start'
|
||||||
>
|
>
|
||||||
{JSON.stringify(data.data, null, 2)
|
{JSON.stringify(data.data, null, 2)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line, i) => (
|
.map((line, i) => (
|
||||||
<span key={i} className="whitespace-pre-wrap break-all">
|
<span key={i} className='whitespace-pre-wrap break-all'>
|
||||||
{line}
|
{line}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -61,15 +61,15 @@ const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
|
|||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
{data.message && (
|
{data.message && (
|
||||||
<Chip className="pl-0.5" variant="flat">
|
<Chip className='pl-0.5' variant='flat'>
|
||||||
<Chip color="warning" size="sm" className="-ml-2 mr-1" variant="flat">
|
<Chip color='warning' size='sm' className='-ml-2 mr-1' variant='flat'>
|
||||||
返回消息
|
返回消息
|
||||||
</Chip>
|
</Chip>
|
||||||
{data.message}
|
{data.message}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotDisplayResponse
|
export default OneBotDisplayResponse;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Select, SelectItem } from '@heroui/select'
|
import { Select, SelectItem } from '@heroui/select';
|
||||||
import { SharedSelection } from '@heroui/system'
|
import { SharedSelection } from '@heroui/system';
|
||||||
import type { Selection } from '@react-types/shared'
|
import type { Selection } from '@react-types/shared';
|
||||||
|
|
||||||
export interface FilterMessageTypeProps {
|
export interface FilterMessageTypeProps {
|
||||||
filterTypes: Selection
|
filterTypes: Selection
|
||||||
@ -11,27 +11,27 @@ const items = [
|
|||||||
{ label: '消息', value: 'message' },
|
{ label: '消息', value: 'message' },
|
||||||
{ label: '请求', value: 'request' },
|
{ label: '请求', value: 'request' },
|
||||||
{ label: '通知', value: 'notice' },
|
{ label: '通知', value: 'notice' },
|
||||||
{ label: '消息发送', value: 'message_sent' }
|
{ label: '消息发送', value: 'message_sent' },
|
||||||
]
|
];
|
||||||
const FilterMessageType: React.FC<FilterMessageTypeProps> = (props) => {
|
const FilterMessageType: React.FC<FilterMessageTypeProps> = (props) => {
|
||||||
const { filterTypes, onSelectionChange } = props
|
const { filterTypes, onSelectionChange } = props;
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
selectedKeys={filterTypes}
|
selectedKeys={filterTypes}
|
||||||
onSelectionChange={(selectedKeys) => {
|
onSelectionChange={(selectedKeys) => {
|
||||||
if (selectedKeys !== 'all' && selectedKeys?.size === 0) {
|
if (selectedKeys !== 'all' && selectedKeys?.size === 0) {
|
||||||
selectedKeys = 'all'
|
selectedKeys = 'all';
|
||||||
}
|
}
|
||||||
onSelectionChange(selectedKeys)
|
onSelectionChange(selectedKeys);
|
||||||
}}
|
}}
|
||||||
label="筛选消息类型"
|
label='筛选消息类型'
|
||||||
selectionMode="multiple"
|
selectionMode='multiple'
|
||||||
items={items}
|
items={items}
|
||||||
renderValue={(value) => {
|
renderValue={(value) => {
|
||||||
if (value.length === items.length) {
|
if (value.length === items.length) {
|
||||||
return '全部'
|
return '全部';
|
||||||
}
|
}
|
||||||
return value.map((v) => v.data?.label).join(',')
|
return value.map((v) => v.data?.label).join(',');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
@ -40,8 +40,8 @@ const FilterMessageType: React.FC<FilterMessageTypeProps> = (props) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const renderFilterMessageType = (
|
export const renderFilterMessageType = (
|
||||||
filterTypes: Selection,
|
filterTypes: Selection,
|
||||||
@ -52,7 +52,7 @@ export const renderFilterMessageType = (
|
|||||||
filterTypes={filterTypes}
|
filterTypes={filterTypes}
|
||||||
onSelectionChange={onSelectionChange}
|
onSelectionChange={onSelectionChange}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FilterMessageType
|
export default FilterMessageType;
|
||||||
|
|||||||
@ -1,62 +1,62 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { VariableSizeList } from 'react-window'
|
import { VariableSizeList } from 'react-window';
|
||||||
|
|
||||||
import OneBotItemRender, {
|
import OneBotItemRender, {
|
||||||
getItemSize
|
getItemSize,
|
||||||
} from '@/components/onebot/display_card/render'
|
} from '@/components/onebot/display_card/render';
|
||||||
|
|
||||||
import { isOB11Event } from '@/utils/onebot'
|
import { isOB11Event } from '@/utils/onebot';
|
||||||
|
|
||||||
import type { AllOB11WsResponse } from '@/types/onebot'
|
import type { AllOB11WsResponse } from '@/types/onebot';
|
||||||
|
|
||||||
export interface OneBotMessageListProps {
|
export interface OneBotMessageListProps {
|
||||||
messages: AllOB11WsResponse[]
|
messages: AllOB11WsResponse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const OneBotMessageList: React.FC<OneBotMessageListProps> = (props) => {
|
const OneBotMessageList: React.FC<OneBotMessageListProps> = (props) => {
|
||||||
const { messages } = props
|
const { messages } = props;
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const listRef = useRef<VariableSizeList>(null)
|
const listRef = useRef<VariableSizeList>(null);
|
||||||
const [containerHeight, setContainerHeight] = useState(400)
|
const [containerHeight, setContainerHeight] = useState(400);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
setContainerHeight(containerRef.current.offsetHeight)
|
setContainerHeight(containerRef.current.offsetHeight);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
resizeObserver.observe(containerRef.current)
|
resizeObserver.observe(containerRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect();
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (listRef.current) {
|
if (listRef.current) {
|
||||||
listRef.current.resetAfterIndex(0, true)
|
listRef.current.resetAfterIndex(0, true);
|
||||||
}
|
}
|
||||||
}, [messages])
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full overflow-hidden" ref={containerRef}>
|
<div className='w-full h-full overflow-hidden' ref={containerRef}>
|
||||||
<VariableSizeList
|
<VariableSizeList
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
itemCount={messages.length}
|
itemCount={messages.length}
|
||||||
width="100%"
|
width='100%'
|
||||||
style={{
|
style={{
|
||||||
overflowX: 'hidden'
|
overflowX: 'hidden',
|
||||||
}}
|
}}
|
||||||
itemSize={(idx) => {
|
itemSize={(idx) => {
|
||||||
const msg = messages[idx]
|
const msg = messages[idx];
|
||||||
if (isOB11Event(msg)) {
|
if (isOB11Event(msg)) {
|
||||||
const size = getItemSize(msg.post_type)
|
const size = getItemSize(msg.post_type);
|
||||||
return size
|
return size;
|
||||||
} else {
|
} else {
|
||||||
return 100
|
return 100;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
height={containerHeight}
|
height={containerHeight}
|
||||||
@ -66,7 +66,7 @@ const OneBotMessageList: React.FC<OneBotMessageListProps> = (props) => {
|
|||||||
{OneBotItemRender}
|
{OneBotItemRender}
|
||||||
</VariableSizeList>
|
</VariableSizeList>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneBotMessageList
|
export default OneBotMessageList;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import qface from 'qface'
|
import qface from 'qface';
|
||||||
import { FaReply } from 'react-icons/fa6'
|
import { FaReply } from 'react-icons/fa6';
|
||||||
|
|
||||||
import { OB11Segment } from '@/types/onebot'
|
import { OB11Segment } from '@/types/onebot';
|
||||||
|
|
||||||
export const renderMessageContent = (
|
export const renderMessageContent = (
|
||||||
segments: OB11Segment[],
|
segments: OB11Segment[],
|
||||||
@ -11,27 +11,27 @@ export const renderMessageContent = (
|
|||||||
return segments.map((segment, index) => {
|
return segments.map((segment, index) => {
|
||||||
switch (segment.type) {
|
switch (segment.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
return <span key={index}>{segment.data.text}</span>
|
return <span key={index}>{segment.data.text}</span>;
|
||||||
case 'face':
|
case 'face':
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
removeWrapper
|
removeWrapper
|
||||||
classNames={{
|
classNames={{
|
||||||
img: 'w-6 h-6 inline !text-[0px] m-0 -mt-1.5 !p-0'
|
img: 'w-6 h-6 inline !text-[0px] m-0 -mt-1.5 !p-0',
|
||||||
}}
|
}}
|
||||||
key={index}
|
key={index}
|
||||||
src={qface.getUrl(segment.data.id)}
|
src={qface.getUrl(segment.data.id)}
|
||||||
alt={`face-${segment.data.id}`}
|
alt={`face-${segment.data.id}`}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'image':
|
case 'image':
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'block !text-[0px] !m-0 !p-0',
|
wrapper: 'block !text-[0px] !m-0 !p-0',
|
||||||
img: 'block'
|
img: 'block',
|
||||||
}}
|
}}
|
||||||
radius="sm"
|
radius='sm'
|
||||||
className={
|
className={
|
||||||
small
|
small
|
||||||
? 'max-h-16 object-cover'
|
? 'max-h-16 object-cover'
|
||||||
@ -39,10 +39,10 @@ export const renderMessageContent = (
|
|||||||
}
|
}
|
||||||
key={index}
|
key={index}
|
||||||
src={segment.data.url || segment.data.file}
|
src={segment.data.url || segment.data.file}
|
||||||
alt="image"
|
alt='image'
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy='no-referrer'
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'record':
|
case 'record':
|
||||||
return (
|
return (
|
||||||
<audio
|
<audio
|
||||||
@ -50,7 +50,7 @@ export const renderMessageContent = (
|
|||||||
controls
|
controls
|
||||||
src={segment.data.url || segment.data.file}
|
src={segment.data.url || segment.data.file}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
@ -58,107 +58,109 @@ export const renderMessageContent = (
|
|||||||
controls
|
controls
|
||||||
src={segment.data.url || segment.data.file}
|
src={segment.data.url || segment.data.file}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
case 'at':
|
case 'at':
|
||||||
return (
|
return (
|
||||||
<span key={index} className="text-blue-500">
|
<span key={index} className='text-blue-500'>
|
||||||
@
|
@
|
||||||
{segment.data.qq === 'all' ? (
|
{segment.data.qq === 'all'
|
||||||
'所有人'
|
? (
|
||||||
) : (
|
'所有人'
|
||||||
<span>
|
)
|
||||||
{segment.data.name}({segment.data.qq})
|
: (
|
||||||
</span>
|
<span>
|
||||||
)}
|
{segment.data.name}({segment.data.qq})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
case 'rps':
|
case 'rps':
|
||||||
return <span key={index}>[猜拳]</span>
|
return <span key={index}>[猜拳]</span>;
|
||||||
case 'dice':
|
case 'dice':
|
||||||
return <span key={index}>[掷骰子]</span>
|
return <span key={index}>[掷骰子]</span>;
|
||||||
case 'shake':
|
case 'shake':
|
||||||
return <span key={index}>[窗口抖动]</span>
|
return <span key={index}>[窗口抖动]</span>;
|
||||||
case 'poke':
|
case 'poke':
|
||||||
return (
|
return (
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
[戳一戳: {segment.data.name || segment.data.id}]
|
[戳一戳: {segment.data.name || segment.data.id}]
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
case 'anonymous':
|
case 'anonymous':
|
||||||
return <span key={index}>[匿名消息]</span>
|
return <span key={index}>[匿名消息]</span>;
|
||||||
case 'share':
|
case 'share':
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={index}
|
key={index}
|
||||||
href={segment.data.url}
|
href={segment.data.url}
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
{segment.data.title}
|
{segment.data.title}
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
case 'contact':
|
case 'contact':
|
||||||
return (
|
return (
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
[推荐{segment.data.type === 'qq' ? '好友' : '群'}: {segment.data.id}
|
[推荐{segment.data.type === 'qq' ? '好友' : '群'}: {segment.data.id}
|
||||||
]
|
]
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
case 'location':
|
case 'location':
|
||||||
return <span key={index}>[位置: {segment.data.title || '未知'}]</span>
|
return <span key={index}>[位置: {segment.data.title || '未知'}]</span>;
|
||||||
case 'music':
|
case 'music':
|
||||||
if (segment.data.type === 'custom') {
|
if (segment.data.type === 'custom') {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={index}
|
key={index}
|
||||||
href={segment.data.url}
|
href={segment.data.url}
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
{segment.data.title}
|
{segment.data.title}
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
[音乐: {segment.data.type} - {segment.data.id}]
|
[音乐: {segment.data.type} - {segment.data.id}]
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
case 'reply':
|
case 'reply':
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-content3 py-1 px-2 rounded-md flex items-center gap-1"
|
className='bg-content3 py-1 px-2 rounded-md flex items-center gap-1'
|
||||||
>
|
>
|
||||||
<FaReply className="text-default-500" />
|
<FaReply className='text-default-500' />
|
||||||
回复消息ID: {segment.data.id}
|
回复消息ID: {segment.data.id}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
case 'forward':
|
case 'forward':
|
||||||
return <span key={index}>[合并转发: {segment.data.id}]</span>
|
return <span key={index}>[合并转发: {segment.data.id}]</span>;
|
||||||
case 'node':
|
case 'node':
|
||||||
return <span key={index}>[消息节点]</span>
|
return <span key={index}>[消息节点]</span>;
|
||||||
case 'xml':
|
case 'xml':
|
||||||
return <pre key={index}>{segment.data.data}</pre>
|
return <pre key={index}>{segment.data.data}</pre>;
|
||||||
case 'json':
|
case 'json':
|
||||||
return (
|
return (
|
||||||
<pre key={index} className="break-all whitespace-break-spaces">
|
<pre key={index} className='break-all whitespace-break-spaces'>
|
||||||
{segment.data.data}
|
{segment.data.data}
|
||||||
</pre>
|
</pre>
|
||||||
)
|
);
|
||||||
case 'file':
|
case 'file':
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={index}
|
key={index}
|
||||||
href={segment.data.file}
|
href={segment.data.file}
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
[文件]
|
[文件]
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
default:
|
default:
|
||||||
return <span key={index}>[不支持的消息类型]</span>
|
return <span key={index}>[不支持的消息类型]</span>;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,70 +1,70 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
useDisclosure
|
useDisclosure,
|
||||||
} from '@heroui/modal'
|
} from '@heroui/modal';
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef } from 'react';
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import ChatInputModal from '@/components/chat_input/modal'
|
import ChatInputModal from '@/components/chat_input/modal';
|
||||||
import CodeEditor from '@/components/code_editor'
|
import CodeEditor from '@/components/code_editor';
|
||||||
import type { CodeEditorRef } from '@/components/code_editor'
|
import type { CodeEditorRef } from '@/components/code_editor';
|
||||||
|
|
||||||
export interface OneBotSendModalProps {
|
export interface OneBotSendModalProps {
|
||||||
sendMessage: (msg: string) => void
|
sendMessage: (msg: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||||
const { sendMessage } = props
|
const { sendMessage } = props;
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
const editorRef = useRef<CodeEditorRef | null>(null)
|
const editorRef = useRef<CodeEditorRef | null>(null);
|
||||||
|
|
||||||
const handleSendMessage = useCallback(
|
const handleSendMessage = useCallback(
|
||||||
(onClose: () => void) => {
|
(onClose: () => void) => {
|
||||||
const msg = editorRef.current?.getValue()
|
const msg = editorRef.current?.getValue();
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
toast.error('消息不能为空')
|
toast.error('消息不能为空');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
sendMessage(msg)
|
sendMessage(msg);
|
||||||
toast.success('消息发送成功')
|
toast.success('消息发送成功');
|
||||||
onClose()
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('消息发送失败')
|
toast.error('消息发送失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sendMessage]
|
[sendMessage]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onPress={onOpen} color="primary" radius="full" variant="flat">
|
<Button onPress={onOpen} color='primary' radius='full' variant='flat'>
|
||||||
构造请求
|
构造请求
|
||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
placement="top-center"
|
placement='top-center'
|
||||||
size="5xl"
|
size='5xl'
|
||||||
scrollBehavior="outside"
|
scrollBehavior='outside'
|
||||||
isDismissable={false}
|
isDismissable={false}
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className='flex flex-col gap-1'>
|
||||||
构造请求
|
构造请求
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="h-96 dark:bg-[rgb(30,30,30)] p-2 rounded-md border border-default-100">
|
<div className='h-96 dark:bg-[rgb(30,30,30)] p-2 rounded-md border border-default-100'>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
height="100%"
|
height='100%'
|
||||||
defaultLanguage="json"
|
defaultLanguage='json'
|
||||||
defaultValue={`{
|
defaultValue={`{
|
||||||
"action": "get_group_list"
|
"action": "get_group_list"
|
||||||
}`}
|
}`}
|
||||||
@ -75,11 +75,11 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<ChatInputModal />
|
<ChatInputModal />
|
||||||
|
|
||||||
<Button color="primary" variant="flat" onPress={onClose}>
|
<Button color='primary' variant='flat' onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color='primary'
|
||||||
onPress={() => handleSendMessage(onClose)}
|
onPress={() => handleSendMessage(onClose)}
|
||||||
>
|
>
|
||||||
发送
|
发送
|
||||||
@ -90,6 +90,6 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default OneBotSendModal
|
export default OneBotSendModal;
|
||||||
|
|||||||
@ -1,39 +1,39 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { ReadyState } from 'react-use-websocket'
|
import { ReadyState } from 'react-use-websocket';
|
||||||
|
|
||||||
export interface WSStatusProps {
|
export interface WSStatusProps {
|
||||||
state: ReadyState
|
state: ReadyState
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusTag({
|
function StatusTag ({
|
||||||
title,
|
title,
|
||||||
color
|
color,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
color: 'success' | 'primary' | 'warning'
|
color: 'success' | 'primary' | 'warning'
|
||||||
}) {
|
}) {
|
||||||
const textClassName = `text-${color} text-sm`
|
const textClassName = `text-${color} text-sm`;
|
||||||
const bgClassName = `bg-${color}`
|
const bgClassName = `bg-${color}`;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center gap-1 rounded-md px-2 col-span-2 md:col-span-1">
|
<div className='flex flex-col justify-center items-center gap-1 rounded-md px-2 col-span-2 md:col-span-1'>
|
||||||
<div className={clsx('w-4 h-4 rounded-full', bgClassName)}></div>
|
<div className={clsx('w-4 h-4 rounded-full', bgClassName)} />
|
||||||
<div className={textClassName}>{title}</div>
|
<div className={textClassName}>{title}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WSStatus({ state }: WSStatusProps) {
|
export default function WSStatus ({ state }: WSStatusProps) {
|
||||||
if (state === ReadyState.OPEN) {
|
if (state === ReadyState.OPEN) {
|
||||||
return <StatusTag title="已连接" color="success" />
|
return <StatusTag title='已连接' color='success' />;
|
||||||
}
|
}
|
||||||
if (state === ReadyState.CLOSED) {
|
if (state === ReadyState.CLOSED) {
|
||||||
return <StatusTag title="已关闭" color="primary" />
|
return <StatusTag title='已关闭' color='primary' />;
|
||||||
}
|
}
|
||||||
if (state === ReadyState.CONNECTING) {
|
if (state === ReadyState.CONNECTING) {
|
||||||
return <StatusTag title="连接中" color="warning" />
|
return <StatusTag title='连接中' color='warning' />;
|
||||||
}
|
}
|
||||||
if (state === ReadyState.CLOSING) {
|
if (state === ReadyState.CLOSING) {
|
||||||
return <StatusTag title="关闭中" color="warning" />
|
return <StatusTag title='关闭中' color='warning' />;
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
|
|
||||||
import bkg_color from '@/assets/images/bkg-color.png'
|
import bkg_color from '@/assets/images/bkg-color.png';
|
||||||
|
|
||||||
const PageBackground = () => {
|
const PageBackground = () => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<div className="fixed w-full h-full -z-[0] flex justify-end opacity-80">
|
<div className='fixed w-full h-full -z-[0] flex justify-end opacity-80'>
|
||||||
<Image
|
<Image
|
||||||
className="overflow-hidden object-contain -top-42 h-[160%] -right-[30%] -rotate-45 pointer-events-none select-none -z-10 relative"
|
className='overflow-hidden object-contain -top-42 h-[160%] -right-[30%] -rotate-45 pointer-events-none select-none -z-10 relative'
|
||||||
src={bkg_color}
|
src={bkg_color}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="fixed w-full h-full overflow-hidden -z-[0] hue-rotate-90 flex justify-start opacity-80">
|
<div className='fixed w-full h-full overflow-hidden -z-[0] hue-rotate-90 flex justify-start opacity-80'>
|
||||||
<Image
|
<Image
|
||||||
className="relative -top-92 h-[180%] object-contain pointer-events-none rotate-90 select-none -z-10 top-44"
|
className='relative -top-92 h-[180%] object-contain pointer-events-none rotate-90 select-none -z-10 top-44'
|
||||||
src={bkg_color}
|
src={bkg_color}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PageBackground
|
export default PageBackground;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export interface PageLoadingProps {
|
export interface PageLoadingProps {
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
@ -10,13 +10,13 @@ const PageLoading: React.FC<PageLoadingProps> = ({ loading }) => {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute top-0 left-0 w-full h-full bg-zinc-500 bg-opacity-10 z-50 flex justify-center items-center backdrop-blur',
|
'absolute top-0 left-0 w-full h-full bg-zinc-500 bg-opacity-10 z-50 flex justify-center items-center backdrop-blur',
|
||||||
{
|
{
|
||||||
hidden: !loading
|
hidden: !loading,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Spinner size="lg" />
|
<Spinner size='lg' />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PageLoading
|
export default PageLoading;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { tv } from 'tailwind-variants'
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
export const title = tv({
|
export const title = tv({
|
||||||
base: 'tracking-tight inline font-semibold',
|
base: 'tracking-tight inline font-semibold',
|
||||||
@ -10,24 +10,24 @@ export const title = tv({
|
|||||||
cyan: 'from-[#00b7fa] to-[#01cfea]',
|
cyan: 'from-[#00b7fa] to-[#01cfea]',
|
||||||
green: 'from-[#6FEE8D] to-[#17c964]',
|
green: 'from-[#6FEE8D] to-[#17c964]',
|
||||||
pink: 'from-[#FF72E1] to-[#F54C7A]',
|
pink: 'from-[#FF72E1] to-[#F54C7A]',
|
||||||
foreground: 'from-[#FFFFFF] to-[#4B4B4B]'
|
foreground: 'from-[#FFFFFF] to-[#4B4B4B]',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
xxs: 'text-medium lg:text-medium',
|
xxs: 'text-medium lg:text-medium',
|
||||||
xs: 'text-xl lg:text-xl',
|
xs: 'text-xl lg:text-xl',
|
||||||
sm: 'text-3xl lg:text-4xl',
|
sm: 'text-3xl lg:text-4xl',
|
||||||
md: 'text-[2.3rem] lg:text-5xl leading-9',
|
md: 'text-[2.3rem] lg:text-5xl leading-9',
|
||||||
lg: 'text-4xl lg:text-6xl'
|
lg: 'text-4xl lg:text-6xl',
|
||||||
},
|
},
|
||||||
fullWidth: {
|
fullWidth: {
|
||||||
true: 'w-full block'
|
true: 'w-full block',
|
||||||
},
|
},
|
||||||
shadow: {
|
shadow: {
|
||||||
true: 'drop-shadow-md'
|
true: 'drop-shadow-md',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: 'md'
|
size: 'md',
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
{
|
{
|
||||||
@ -38,21 +38,21 @@ export const title = tv({
|
|||||||
'cyan',
|
'cyan',
|
||||||
'green',
|
'green',
|
||||||
'pink',
|
'pink',
|
||||||
'foreground'
|
'foreground',
|
||||||
],
|
],
|
||||||
class: 'bg-clip-text text-transparent bg-gradient-to-b'
|
class: 'bg-clip-text text-transparent bg-gradient-to-b',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
export const subtitle = tv({
|
export const subtitle = tv({
|
||||||
base: 'w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full',
|
base: 'w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full',
|
||||||
variants: {
|
variants: {
|
||||||
fullWidth: {
|
fullWidth: {
|
||||||
true: '!w-full'
|
true: '!w-full',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
fullWidth: true
|
fullWidth: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Card, CardBody } from '@heroui/card'
|
import { Card, CardBody } from '@heroui/card';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { BsTencentQq } from 'react-icons/bs'
|
import { BsTencentQq } from 'react-icons/bs';
|
||||||
|
|
||||||
import { SelfInfo } from '@/types/user'
|
import { SelfInfo } from '@/types/user';
|
||||||
|
|
||||||
import PageLoading from './page_loading'
|
import PageLoading from './page_loading';
|
||||||
|
|
||||||
export interface QQInfoCardProps {
|
export interface QQInfoCardProps {
|
||||||
data?: SelfInfo
|
data?: SelfInfo
|
||||||
@ -16,46 +16,48 @@ export interface QQInfoCardProps {
|
|||||||
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="relative bg-primary-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-primary-300 dark:shadow-primary-50"
|
className='relative bg-primary-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-primary-300 dark:shadow-primary-50'
|
||||||
shadow="none"
|
shadow='none'
|
||||||
radius="lg"
|
radius='lg'
|
||||||
>
|
>
|
||||||
<PageLoading loading={loading} />
|
<PageLoading loading={loading} />
|
||||||
{error ? (
|
{error
|
||||||
<CardBody className="items-center gap-1 justify-center">
|
? (
|
||||||
<div className="flex-1 text-content1-foreground">Error</div>
|
<CardBody className='items-center gap-1 justify-center'>
|
||||||
<div className="whitespace-nowrap text-nowrap flex-shrink-0">
|
<div className='flex-1 text-content1-foreground'>Error</div>
|
||||||
{error.message}
|
<div className='whitespace-nowrap text-nowrap flex-shrink-0'>
|
||||||
</div>
|
{error.message}
|
||||||
</CardBody>
|
</div>
|
||||||
) : (
|
</CardBody>
|
||||||
<CardBody className="flex-row items-center gap-2 overflow-hidden relative">
|
)
|
||||||
<div className="absolute right-0 bottom-0 text-5xl text-primary-400">
|
: (
|
||||||
<BsTencentQq />
|
<CardBody className='flex-row items-center gap-2 overflow-hidden relative'>
|
||||||
</div>
|
<div className='absolute right-0 bottom-0 text-5xl text-primary-400'>
|
||||||
<div className="relative flex-shrink-0 z-10">
|
<BsTencentQq />
|
||||||
<Image
|
</div>
|
||||||
src={
|
<div className='relative flex-shrink-0 z-10'>
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
data?.avatarUrl ??
|
data?.avatarUrl ??
|
||||||
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1`
|
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1`
|
||||||
}
|
}
|
||||||
className="shadow-md rounded-full w-12 aspect-square"
|
className='shadow-md rounded-full w-12 aspect-square'
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
|
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
|
||||||
data?.online ? 'bg-green-500' : 'bg-gray-500'
|
data?.online ? 'bg-green-500' : 'bg-gray-500'
|
||||||
)}
|
)}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-col justify-center">
|
<div className='flex-col justify-center'>
|
||||||
<div className="text-lg truncate">{data?.nick}</div>
|
<div className='text-lg truncate'>{data?.nick}</div>
|
||||||
<div className="text-primary-500 text-sm">{data?.uin}</div>
|
<div className='text-primary-500 text-sm'>{data?.uin}</div>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default QQInfoCard
|
export default QQInfoCard;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import { QRCodeSVG } from 'qrcode.react'
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
|
||||||
interface QrCodeLoginProps {
|
interface QrCodeLoginProps {
|
||||||
qrcode: string
|
qrcode: string
|
||||||
@ -7,18 +7,18 @@ interface QrCodeLoginProps {
|
|||||||
|
|
||||||
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
|
const QrCodeLogin: React.FC<QrCodeLoginProps> = ({ qrcode }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className='flex flex-col items-center'>
|
||||||
<div className="bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden">
|
<div className='bg-white p-2 rounded-md w-fit mx-auto relative overflow-hidden'>
|
||||||
{!qrcode && (
|
{!qrcode && (
|
||||||
<div className="absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center">
|
<div className='absolute left-2 top-2 right-2 bottom-2 bg-white bg-opacity-50 backdrop-blur flex items-center justify-center'>
|
||||||
<Spinner color="primary" />
|
<Spinner color='primary' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<QRCodeSVG size={180} value={qrcode} />
|
<QRCodeSVG size={180} value={qrcode} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 text-center">请使用QQ或者TIM扫描上方二维码</div>
|
<div className='mt-5 text-center'>请使用QQ或者TIM扫描上方二维码</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default QrCodeLogin
|
export default QrCodeLogin;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Avatar } from '@heroui/avatar'
|
import { Avatar } from '@heroui/avatar';
|
||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import { Select, SelectItem } from '@heroui/select'
|
import { Select, SelectItem } from '@heroui/select';
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io';
|
||||||
|
|
||||||
import { isQQQuickNewItem } from '@/utils/qq'
|
import { isQQQuickNewItem } from '@/utils/qq';
|
||||||
|
|
||||||
export interface QQItem {
|
export interface QQItem {
|
||||||
uin: string
|
uin: string
|
||||||
@ -27,67 +27,67 @@ const QuickLogin: React.FC<QuickLoginProps> = ({
|
|||||||
selectedQQ,
|
selectedQQ,
|
||||||
onUpdateQQList,
|
onUpdateQQList,
|
||||||
handleSelectionChange,
|
handleSelectionChange,
|
||||||
onSubmit
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className='flex flex-col gap-8'>
|
||||||
<div className="flex justify-center">
|
<div className='flex justify-center'>
|
||||||
<Image
|
<Image
|
||||||
className="shadow-lg"
|
className='shadow-lg'
|
||||||
height={100}
|
height={100}
|
||||||
radius="full"
|
radius='full'
|
||||||
src={`https://q1.qlogo.cn/g?b=qq&nk=${selectedQQ || '0'}&s=100`}
|
src={`https://q1.qlogo.cn/g?b=qq&nk=${selectedQQ || '0'}&s=100`}
|
||||||
width={100}
|
width={100}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Select
|
<Select
|
||||||
classNames={{
|
classNames={{
|
||||||
popoverContent: 'bg-opacity-50 backdrop-blur'
|
popoverContent: 'bg-opacity-50 backdrop-blur',
|
||||||
}}
|
}}
|
||||||
aria-label="QQ Login"
|
aria-label='QQ Login'
|
||||||
isDisabled={refresh}
|
isDisabled={refresh}
|
||||||
items={qqList}
|
items={qqList}
|
||||||
placeholder="请选择QQ"
|
placeholder='请选择QQ'
|
||||||
renderValue={(items) => {
|
renderValue={(items) => {
|
||||||
return items.map((item) => (
|
return items.map((item) => (
|
||||||
<div key={item.key} className="flex items-center gap-2">
|
<div key={item.key} className='flex items-center gap-2'>
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={item.key?.toString()}
|
alt={item.key?.toString()}
|
||||||
className="flex-shrink-0"
|
className='flex-shrink-0'
|
||||||
size="sm"
|
size='sm'
|
||||||
src={
|
src={
|
||||||
isQQQuickNewItem(item.data)
|
isQQQuickNewItem(item.data)
|
||||||
? item.data?.faceUrl
|
? item.data?.faceUrl
|
||||||
: `https://q1.qlogo.cn/g?b=qq&nk=${item.key}&s=1`
|
: `https://q1.qlogo.cn/g?b=qq&nk=${item.key}&s=1`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
{isQQQuickNewItem(item.data)
|
{isQQQuickNewItem(item.data)
|
||||||
? `${item.data.nickName}(${item.key?.toString()})`
|
? `${item.data.nickName}(${item.key?.toString()})`
|
||||||
: item.key?.toString()}
|
: item.key?.toString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
}}
|
}}
|
||||||
selectedKeys={[selectedQQ]}
|
selectedKeys={[selectedQQ]}
|
||||||
size="lg"
|
size='lg'
|
||||||
onChange={handleSelectionChange}
|
onChange={handleSelectionChange}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<SelectItem key={item.uin} textValue={item.uin}>
|
<SelectItem key={item.uin} textValue={item.uin}>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={item.uin}
|
alt={item.uin}
|
||||||
className="flex-shrink-0"
|
className='flex-shrink-0'
|
||||||
size="sm"
|
size='sm'
|
||||||
src={
|
src={
|
||||||
isQQQuickNewItem(item)
|
isQQQuickNewItem(item)
|
||||||
? item.faceUrl
|
? item.faceUrl
|
||||||
: `https://q1.qlogo.cn/g?b=qq&nk=${item.uin}&s=1`
|
: `https://q1.qlogo.cn/g?b=qq&nk=${item.uin}&s=1`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
{isQQQuickNewItem(item)
|
{isQQQuickNewItem(item)
|
||||||
? `${item.nickName}(${item.uin})`
|
? `${item.nickName}(${item.uin})`
|
||||||
: item.uin}
|
: item.uin}
|
||||||
@ -98,32 +98,32 @@ const QuickLogin: React.FC<QuickLoginProps> = ({
|
|||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="flex-grow-0 flex-shrink-0"
|
className='flex-grow-0 flex-shrink-0'
|
||||||
color="secondary"
|
color='secondary'
|
||||||
isLoading={refresh}
|
isLoading={refresh}
|
||||||
radius="full"
|
radius='full'
|
||||||
size="lg"
|
size='lg'
|
||||||
variant="light"
|
variant='light'
|
||||||
onPress={onUpdateQQList}
|
onPress={onUpdateQQList}
|
||||||
>
|
>
|
||||||
<IoMdRefresh size={24} />
|
<IoMdRefresh size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center mt-5">
|
<div className='flex justify-center mt-5'>
|
||||||
<Button
|
<Button
|
||||||
className="w-64 max-w-full"
|
className='w-64 max-w-full'
|
||||||
color="primary"
|
color='primary'
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
radius="full"
|
radius='full'
|
||||||
size="lg"
|
size='lg'
|
||||||
variant="shadow"
|
variant='shadow'
|
||||||
onPress={onSubmit}
|
onPress={onSubmit}
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default QuickLogin
|
export default QuickLogin;
|
||||||
|
|||||||
@ -3,19 +3,19 @@ import {
|
|||||||
HTMLMotionProps,
|
HTMLMotionProps,
|
||||||
TargetAndTransition,
|
TargetAndTransition,
|
||||||
Transition,
|
Transition,
|
||||||
motion
|
motion,
|
||||||
} from 'motion/react'
|
} from 'motion/react';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState
|
useState,
|
||||||
} from 'react'
|
} from 'react';
|
||||||
|
|
||||||
function cn(...classes: (string | undefined | null | boolean)[]): string {
|
function cn (...classes: (string | undefined | null | boolean)[]): string {
|
||||||
return classes.filter(Boolean).join(' ')
|
return classes.filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RotatingTextRef {
|
export interface RotatingTextRef {
|
||||||
@ -73,65 +73,65 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [currentTextIndex, setCurrentTextIndex] = useState<number>(0)
|
const [currentTextIndex, setCurrentTextIndex] = useState<number>(0);
|
||||||
|
|
||||||
const splitIntoCharacters = (text: string): string[] => {
|
const splitIntoCharacters = (text: string): string[] => {
|
||||||
return Array.from(text)
|
return Array.from(text);
|
||||||
}
|
};
|
||||||
|
|
||||||
const elements = useMemo(() => {
|
const elements = useMemo(() => {
|
||||||
const currentText: string = texts[currentTextIndex]
|
const currentText: string = texts[currentTextIndex];
|
||||||
if (splitBy === 'characters') {
|
if (splitBy === 'characters') {
|
||||||
const words = currentText.split(' ')
|
const words = currentText.split(' ');
|
||||||
return words.map((word, i) => ({
|
return words.map((word, i) => ({
|
||||||
characters: splitIntoCharacters(word),
|
characters: splitIntoCharacters(word),
|
||||||
needsSpace: i !== words.length - 1
|
needsSpace: i !== words.length - 1,
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
if (splitBy === 'words') {
|
if (splitBy === 'words') {
|
||||||
return currentText.split(' ').map((word, i, arr) => ({
|
return currentText.split(' ').map((word, i, arr) => ({
|
||||||
characters: [word],
|
characters: [word],
|
||||||
needsSpace: i !== arr.length - 1
|
needsSpace: i !== arr.length - 1,
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
if (splitBy === 'lines') {
|
if (splitBy === 'lines') {
|
||||||
return currentText.split('\n').map((line, i, arr) => ({
|
return currentText.split('\n').map((line, i, arr) => ({
|
||||||
characters: [line],
|
characters: [line],
|
||||||
needsSpace: i !== arr.length - 1
|
needsSpace: i !== arr.length - 1,
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentText.split(splitBy).map((part, i, arr) => ({
|
return currentText.split(splitBy).map((part, i, arr) => ({
|
||||||
characters: [part],
|
characters: [part],
|
||||||
needsSpace: i !== arr.length - 1
|
needsSpace: i !== arr.length - 1,
|
||||||
}))
|
}));
|
||||||
}, [texts, currentTextIndex, splitBy])
|
}, [texts, currentTextIndex, splitBy]);
|
||||||
|
|
||||||
const getStaggerDelay = useCallback(
|
const getStaggerDelay = useCallback(
|
||||||
(index: number, totalChars: number): number => {
|
(index: number, totalChars: number): number => {
|
||||||
const total = totalChars
|
const total = totalChars;
|
||||||
if (staggerFrom === 'first') return index * staggerDuration
|
if (staggerFrom === 'first') return index * staggerDuration;
|
||||||
if (staggerFrom === 'last') return (total - 1 - index) * staggerDuration
|
if (staggerFrom === 'last') return (total - 1 - index) * staggerDuration;
|
||||||
if (staggerFrom === 'center') {
|
if (staggerFrom === 'center') {
|
||||||
const center = Math.floor(total / 2)
|
const center = Math.floor(total / 2);
|
||||||
return Math.abs(center - index) * staggerDuration
|
return Math.abs(center - index) * staggerDuration;
|
||||||
}
|
}
|
||||||
if (staggerFrom === 'random') {
|
if (staggerFrom === 'random') {
|
||||||
const randomIndex = Math.floor(Math.random() * total)
|
const randomIndex = Math.floor(Math.random() * total);
|
||||||
return Math.abs(randomIndex - index) * staggerDuration
|
return Math.abs(randomIndex - index) * staggerDuration;
|
||||||
}
|
}
|
||||||
return Math.abs((staggerFrom as number) - index) * staggerDuration
|
return Math.abs((staggerFrom as number) - index) * staggerDuration;
|
||||||
},
|
},
|
||||||
[staggerFrom, staggerDuration]
|
[staggerFrom, staggerDuration]
|
||||||
)
|
);
|
||||||
|
|
||||||
const handleIndexChange = useCallback(
|
const handleIndexChange = useCallback(
|
||||||
(newIndex: number) => {
|
(newIndex: number) => {
|
||||||
setCurrentTextIndex(newIndex)
|
setCurrentTextIndex(newIndex);
|
||||||
if (onNext) onNext(newIndex)
|
if (onNext) onNext(newIndex);
|
||||||
},
|
},
|
||||||
[onNext]
|
[onNext]
|
||||||
)
|
);
|
||||||
|
|
||||||
const next = useCallback(() => {
|
const next = useCallback(() => {
|
||||||
const nextIndex =
|
const nextIndex =
|
||||||
@ -139,11 +139,11 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
? loop
|
? loop
|
||||||
? 0
|
? 0
|
||||||
: currentTextIndex
|
: currentTextIndex
|
||||||
: currentTextIndex + 1
|
: currentTextIndex + 1;
|
||||||
if (nextIndex !== currentTextIndex) {
|
if (nextIndex !== currentTextIndex) {
|
||||||
handleIndexChange(nextIndex)
|
handleIndexChange(nextIndex);
|
||||||
}
|
}
|
||||||
}, [currentTextIndex, texts.length, loop, handleIndexChange])
|
}, [currentTextIndex, texts.length, loop, handleIndexChange]);
|
||||||
|
|
||||||
const previous = useCallback(() => {
|
const previous = useCallback(() => {
|
||||||
const prevIndex =
|
const prevIndex =
|
||||||
@ -151,27 +151,27 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
? loop
|
? loop
|
||||||
? texts.length - 1
|
? texts.length - 1
|
||||||
: currentTextIndex
|
: currentTextIndex
|
||||||
: currentTextIndex - 1
|
: currentTextIndex - 1;
|
||||||
if (prevIndex !== currentTextIndex) {
|
if (prevIndex !== currentTextIndex) {
|
||||||
handleIndexChange(prevIndex)
|
handleIndexChange(prevIndex);
|
||||||
}
|
}
|
||||||
}, [currentTextIndex, texts.length, loop, handleIndexChange])
|
}, [currentTextIndex, texts.length, loop, handleIndexChange]);
|
||||||
|
|
||||||
const jumpTo = useCallback(
|
const jumpTo = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const validIndex = Math.max(0, Math.min(index, texts.length - 1))
|
const validIndex = Math.max(0, Math.min(index, texts.length - 1));
|
||||||
if (validIndex !== currentTextIndex) {
|
if (validIndex !== currentTextIndex) {
|
||||||
handleIndexChange(validIndex)
|
handleIndexChange(validIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[texts.length, currentTextIndex, handleIndexChange]
|
[texts.length, currentTextIndex, handleIndexChange]
|
||||||
)
|
);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
if (currentTextIndex !== 0) {
|
if (currentTextIndex !== 0) {
|
||||||
handleIndexChange(0)
|
handleIndexChange(0);
|
||||||
}
|
}
|
||||||
}, [currentTextIndex, handleIndexChange])
|
}, [currentTextIndex, handleIndexChange]);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
@ -179,16 +179,16 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
next,
|
next,
|
||||||
previous,
|
previous,
|
||||||
jumpTo,
|
jumpTo,
|
||||||
reset
|
reset,
|
||||||
}),
|
}),
|
||||||
[next, previous, jumpTo, reset]
|
[next, previous, jumpTo, reset]
|
||||||
)
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!auto) return
|
if (!auto) return;
|
||||||
const intervalId = setInterval(next, rotationInterval)
|
const intervalId = setInterval(next, rotationInterval);
|
||||||
return () => clearInterval(intervalId)
|
return () => clearInterval(intervalId);
|
||||||
}, [next, rotationInterval, auto])
|
}, [next, rotationInterval, auto]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.span
|
<motion.span
|
||||||
@ -200,7 +200,7 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
layout
|
layout
|
||||||
transition={transition}
|
transition={transition}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{texts[currentTextIndex]}</span>
|
<span className='sr-only'>{texts[currentTextIndex]}</span>
|
||||||
<AnimatePresence
|
<AnimatePresence
|
||||||
mode={animatePresenceMode}
|
mode={animatePresenceMode}
|
||||||
initial={animatePresenceInitial}
|
initial={animatePresenceInitial}
|
||||||
@ -213,7 +213,7 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
: 'flex flex-wrap whitespace-pre-wrap relative'
|
: 'flex flex-wrap whitespace-pre-wrap relative'
|
||||||
)}
|
)}
|
||||||
layout
|
layout
|
||||||
aria-hidden="true"
|
aria-hidden='true'
|
||||||
initial={initial as HTMLMotionProps<'div'>['initial']}
|
initial={initial as HTMLMotionProps<'div'>['initial']}
|
||||||
animate={animate as HTMLMotionProps<'div'>['animate']}
|
animate={animate as HTMLMotionProps<'div'>['animate']}
|
||||||
exit={exit as HTMLMotionProps<'div'>['exit']}
|
exit={exit as HTMLMotionProps<'div'>['exit']}
|
||||||
@ -221,7 +221,7 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
{elements.map((wordObj, wordIndex, array) => {
|
{elements.map((wordObj, wordIndex, array) => {
|
||||||
const previousCharsCount = array
|
const previousCharsCount = array
|
||||||
.slice(0, wordIndex)
|
.slice(0, wordIndex)
|
||||||
.reduce((sum, word) => sum + word.characters.length, 0)
|
.reduce((sum, word) => sum + word.characters.length, 0);
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={wordIndex}
|
key={wordIndex}
|
||||||
@ -241,7 +241,7 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
(sum, word) => sum + word.characters.length,
|
(sum, word) => sum + word.characters.length,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
}}
|
}}
|
||||||
className={cn('inline-block', elementLevelClassName)}
|
className={cn('inline-block', elementLevelClassName)}
|
||||||
>
|
>
|
||||||
@ -249,17 +249,17 @@ const RotatingText = forwardRef<RotatingTextRef, RotatingTextProps>(
|
|||||||
</motion.span>
|
</motion.span>
|
||||||
))}
|
))}
|
||||||
{wordObj.needsSpace && (
|
{wordObj.needsSpace && (
|
||||||
<span className="whitespace-pre"> </span>
|
<span className='whitespace-pre'> </span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
RotatingText.displayName = 'RotatingText'
|
RotatingText.displayName = 'RotatingText';
|
||||||
export default RotatingText
|
export default RotatingText;
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { IoMdLogOut } from 'react-icons/io'
|
import { IoMdLogOut } from 'react-icons/io';
|
||||||
import { MdDarkMode, MdLightMode } from 'react-icons/md'
|
import { MdDarkMode, MdLightMode } from 'react-icons/md';
|
||||||
|
|
||||||
import useAuth from '@/hooks/auth'
|
import useAuth from '@/hooks/auth';
|
||||||
import useDialog from '@/hooks/use-dialog'
|
import useDialog from '@/hooks/use-dialog';
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
import logo from '@/assets/images/logo.png'
|
import logo from '@/assets/images/logo.png';
|
||||||
import type { MenuItem } from '@/config/site'
|
import type { MenuItem } from '@/config/site';
|
||||||
|
|
||||||
import Menus from './menus'
|
import Menus from './menus';
|
||||||
|
|
||||||
interface SideBarProps {
|
interface SideBarProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@ -21,17 +21,17 @@ interface SideBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SideBar: React.FC<SideBarProps> = (props) => {
|
const SideBar: React.FC<SideBarProps> = (props) => {
|
||||||
const { open, items } = props
|
const { open, items } = props;
|
||||||
const { toggleTheme, isDark } = useTheme()
|
const { toggleTheme, isDark } = useTheme();
|
||||||
const { revokeAuth } = useAuth()
|
const { revokeAuth } = useAuth();
|
||||||
const dialog = useDialog()
|
const dialog = useDialog();
|
||||||
const onRevokeAuth = () => {
|
const onRevokeAuth = () => {
|
||||||
dialog.confirm({
|
dialog.confirm({
|
||||||
title: '退出登录',
|
title: '退出登录',
|
||||||
content: '确定要退出登录吗?',
|
content: '确定要退出登录吗?',
|
||||||
onConfirm: revokeAuth
|
onConfirm: revokeAuth,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -42,13 +42,13 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
transition={{
|
transition={{
|
||||||
type: open ? 'spring' : 'tween',
|
type: open ? 'spring' : 'tween',
|
||||||
stiffness: 150,
|
stiffness: 150,
|
||||||
damping: open ? 15 : 10
|
damping: open ? 15 : 10,
|
||||||
}}
|
}}
|
||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<motion.div className="w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right">
|
<motion.div className='w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right'>
|
||||||
<div className="flex justify-center items-center my-2 gap-2">
|
<div className='flex justify-center items-center my-2 gap-2'>
|
||||||
<Image radius="none" height={40} src={logo} className="mb-2" />
|
<Image radius='none' height={40} src={logo} className='mb-2' />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center font-bold',
|
'flex items-center font-bold',
|
||||||
@ -58,14 +58,14 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
NapCat
|
NapCat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto flex flex-col flex-1 px-4">
|
<div className='overflow-y-auto flex flex-col flex-1 px-4'>
|
||||||
<Menus items={items} />
|
<Menus items={items} />
|
||||||
<div className="mt-auto mb-10 md:mb-0">
|
<div className='mt-auto mb-10 md:mb-0'>
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className='w-full'
|
||||||
color="primary"
|
color='primary'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="light"
|
variant='light'
|
||||||
onPress={toggleTheme}
|
onPress={toggleTheme}
|
||||||
startContent={
|
startContent={
|
||||||
!isDark ? <MdLightMode size={16} /> : <MdDarkMode size={16} />
|
!isDark ? <MdLightMode size={16} /> : <MdDarkMode size={16} />
|
||||||
@ -74,10 +74,10 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
切换主题
|
切换主题
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-full mb-2"
|
className='w-full mb-2'
|
||||||
color="primary"
|
color='primary'
|
||||||
radius="full"
|
radius='full'
|
||||||
variant="light"
|
variant='light'
|
||||||
onPress={onRevokeAuth}
|
onPress={onRevokeAuth}
|
||||||
startContent={<IoMdLogOut size={16} />}
|
startContent={<IoMdLogOut size={16} />}
|
||||||
>
|
>
|
||||||
@ -87,7 +87,7 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SideBar
|
export default SideBar;
|
||||||
|
|||||||
@ -1,50 +1,50 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks'
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom'
|
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import key from '@/const/key'
|
import key from '@/const/key';
|
||||||
|
|
||||||
import type { MenuItem } from '@/config/site'
|
import type { MenuItem } from '@/config/site';
|
||||||
|
|
||||||
const renderItems = (items: MenuItem[], children = false) => {
|
const renderItems = (items: MenuItem[], children = false) => {
|
||||||
return items?.map((item) => {
|
return items?.map((item) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const locate = useLocation()
|
const locate = useLocation();
|
||||||
const [open, setOpen] = React.useState(!!item.autoOpen)
|
const [open, setOpen] = React.useState(!!item.autoOpen);
|
||||||
const canOpen = React.useMemo(
|
const canOpen = React.useMemo(
|
||||||
() => item.items && item.items.length > 0,
|
() => item.items && item.items.length > 0,
|
||||||
[item.items]
|
[item.items]
|
||||||
)
|
);
|
||||||
const [b64img] = useLocalStorage(key.backgroundImage, '')
|
const [b64img] = useLocalStorage(key.backgroundImage, '');
|
||||||
const [customIcons] = useLocalStorage<Record<string, string>>(
|
const [customIcons] = useLocalStorage<Record<string, string>>(
|
||||||
key.customIcons,
|
key.customIcons,
|
||||||
{}
|
{}
|
||||||
)
|
);
|
||||||
const isActive = React.useMemo(() => {
|
const isActive = React.useMemo(() => {
|
||||||
if (item.href) {
|
if (item.href) {
|
||||||
return !!matchPath(item.href, locate.pathname)
|
return !!matchPath(item.href, locate.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
}, [item.href, locate.pathname])
|
}, [item.href, locate.pathname]);
|
||||||
|
|
||||||
const goTo = (href: string) => {
|
const goTo = (href: string) => {
|
||||||
navigate(href)
|
navigate(href);
|
||||||
}
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
const shouldOpen = item.items.some(
|
const shouldOpen = item.items.some(
|
||||||
(item) => item?.href && !!matchPath(item.href, locate.pathname)
|
(item) => item?.href && !!matchPath(item.href, locate.pathname)
|
||||||
)
|
);
|
||||||
|
|
||||||
if (shouldOpen) setOpen(true)
|
if (shouldOpen) setOpen(true);
|
||||||
}
|
}
|
||||||
}, [item.items, locate.pathname])
|
}, [item.items, locate.pathname]);
|
||||||
const panelRef = React.useRef<HTMLDivElement>(null)
|
const panelRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.href + item.label}>
|
<div key={item.href + item.label}>
|
||||||
@ -55,74 +55,78 @@ const renderItems = (items: MenuItem[], children = false) => {
|
|||||||
isActive && 'bg-opacity-60',
|
isActive && 'bg-opacity-60',
|
||||||
b64img && 'backdrop-blur-md text-white'
|
b64img && 'backdrop-blur-md text-white'
|
||||||
)}
|
)}
|
||||||
color="primary"
|
color='primary'
|
||||||
endContent={
|
endContent={
|
||||||
canOpen ? (
|
canOpen
|
||||||
<div
|
? (
|
||||||
className={clsx(
|
<div
|
||||||
'ml-auto relative w-3 h-3 transition-transform',
|
className={clsx(
|
||||||
open && 'transform rotate-180',
|
'ml-auto relative w-3 h-3 transition-transform',
|
||||||
isActive
|
open && 'transform rotate-180',
|
||||||
? 'text-primary-500'
|
isActive
|
||||||
: 'text-primary-200 dark:text-white',
|
? 'text-primary-500'
|
||||||
'before:rounded-full',
|
: 'text-primary-200 dark:text-white',
|
||||||
'before:content-[""]',
|
'before:rounded-full',
|
||||||
'before:block',
|
'before:content-[""]',
|
||||||
'before:absolute',
|
'before:block',
|
||||||
'before:w-3',
|
'before:absolute',
|
||||||
'before:h-[4.5px]',
|
'before:w-3',
|
||||||
'before:bg-current',
|
'before:h-[4.5px]',
|
||||||
'before:top-1/2',
|
'before:bg-current',
|
||||||
'before:-left-[3px]',
|
'before:top-1/2',
|
||||||
'before:transform',
|
'before:-left-[3px]',
|
||||||
'before:-translate-y-1/2',
|
'before:transform',
|
||||||
'before:rotate-45',
|
'before:-translate-y-1/2',
|
||||||
'after:rounded-full',
|
'before:rotate-45',
|
||||||
'after:content-[""]',
|
'after:rounded-full',
|
||||||
'after:block',
|
'after:content-[""]',
|
||||||
'after:absolute',
|
'after:block',
|
||||||
'after:w-3',
|
'after:absolute',
|
||||||
'after:h-[4.5px]',
|
'after:w-3',
|
||||||
'after:bg-current',
|
'after:h-[4.5px]',
|
||||||
'after:top-1/2',
|
'after:bg-current',
|
||||||
'after:left-[3px]',
|
'after:top-1/2',
|
||||||
'after:transform',
|
'after:left-[3px]',
|
||||||
'after:-translate-y-1/2',
|
'after:transform',
|
||||||
'after:-rotate-45'
|
'after:-translate-y-1/2',
|
||||||
)}
|
'after:-rotate-45'
|
||||||
/>
|
)}
|
||||||
) : (
|
/>
|
||||||
<div
|
)
|
||||||
className={clsx(
|
: (
|
||||||
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
|
<div
|
||||||
isActive
|
className={clsx(
|
||||||
? 'bg-primary-500 animate-spinner-ease-spin'
|
'w-3 h-1.5 rounded-full ml-auto shadow-lg',
|
||||||
: 'bg-primary-200 dark:bg-white'
|
isActive
|
||||||
)}
|
? 'bg-primary-500 animate-spinner-ease-spin'
|
||||||
/>
|
: 'bg-primary-200 dark:bg-white'
|
||||||
)
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
radius="full"
|
radius='full'
|
||||||
startContent={
|
startContent={
|
||||||
customIcons[item.label] ? (
|
customIcons[item.label]
|
||||||
<Image
|
? (
|
||||||
radius="none"
|
<Image
|
||||||
src={customIcons[item.label]}
|
radius='none'
|
||||||
alt={item.label}
|
src={customIcons[item.label]}
|
||||||
className="w-5 h-5"
|
alt={item.label}
|
||||||
/>
|
className='w-5 h-5'
|
||||||
) : (
|
/>
|
||||||
item.icon
|
)
|
||||||
)
|
: (
|
||||||
|
item.icon
|
||||||
|
)
|
||||||
}
|
}
|
||||||
variant={isActive ? (children ? 'solid' : 'shadow') : 'light'}
|
variant={isActive ? (children ? 'solid' : 'shadow') : 'light'}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (item.href) {
|
if (item.href) {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
goTo(item.href)
|
goTo(item.href);
|
||||||
}
|
}
|
||||||
} else if (canOpen) {
|
} else if (canOpen) {
|
||||||
setOpen(!open)
|
setOpen(!open);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -130,29 +134,29 @@ const renderItems = (items: MenuItem[], children = false) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
className="ml-4 overflow-hidden transition-all duration-300"
|
className='ml-4 overflow-hidden transition-all duration-300'
|
||||||
style={{
|
style={{
|
||||||
height: open ? panelRef.current?.scrollHeight : 0
|
height: open ? panelRef.current?.scrollHeight : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.items && renderItems(item.items, true)}
|
{item.items && renderItems(item.items, true)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
interface MenusProps {
|
interface MenusProps {
|
||||||
items: MenuItem[]
|
items: MenuItem[]
|
||||||
}
|
}
|
||||||
const Menus: React.FC<MenusProps> = (props) => {
|
const Menus: React.FC<MenusProps> = (props) => {
|
||||||
const { items } = props
|
const { items } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-content-center flex-1 gap-2">
|
<div className='flex flex-col justify-content-center flex-1 gap-2'>
|
||||||
{renderItems(items)}
|
{renderItems(items)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Menus
|
export default Menus;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Switch } from '@heroui/switch'
|
import { Switch } from '@heroui/switch';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import React, { forwardRef } from 'react'
|
import React, { forwardRef } from 'react';
|
||||||
|
|
||||||
export interface SwitchCardProps {
|
export interface SwitchCardProps {
|
||||||
label?: string
|
label?: string
|
||||||
@ -15,8 +15,8 @@ export interface SwitchCardProps {
|
|||||||
|
|
||||||
const SwitchCard = forwardRef<HTMLInputElement, SwitchCardProps>(
|
const SwitchCard = forwardRef<HTMLInputElement, SwitchCardProps>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const { label, description, value, onValueChange, disabled } = props
|
const { label, description, value, onValueChange, disabled } = props;
|
||||||
const selectString = value ? 'true' : 'false'
|
const selectString = value ? 'true' : 'false';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
@ -25,7 +25,7 @@ const SwitchCard = forwardRef<HTMLInputElement, SwitchCardProps>(
|
|||||||
'inline-flex flex-row-reverse w-full max-w-md bg-content1 hover:bg-content2 items-center',
|
'inline-flex flex-row-reverse w-full max-w-md bg-content1 hover:bg-content2 items-center',
|
||||||
'justify-between cursor-pointer rounded-lg gap-2 p-3 border-2 border-transparent',
|
'justify-between cursor-pointer rounded-lg gap-2 p-3 border-2 border-transparent',
|
||||||
'data-[selected=true]:border-primary bg-opacity-50 backdrop-blur-sm'
|
'data-[selected=true]:border-primary bg-opacity-50 backdrop-blur-sm'
|
||||||
)
|
),
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -34,15 +34,15 @@ const SwitchCard = forwardRef<HTMLInputElement, SwitchCardProps>(
|
|||||||
value={selectString}
|
value={selectString}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className='flex flex-col gap-1'>
|
||||||
<p className="text-medium">{label}</p>
|
<p className='text-medium'>{label}</p>
|
||||||
<p className="text-tiny text-default-400">{description}</p>
|
<p className='text-tiny text-default-400'>{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
SwitchCard.displayName = 'SwitchCard'
|
SwitchCard.displayName = 'SwitchCard';
|
||||||
|
|
||||||
export default SwitchCard
|
export default SwitchCard;
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
import { Button } from '@heroui/button'
|
import { Button } from '@heroui/button';
|
||||||
import { Card, CardBody, CardHeader } from '@heroui/card'
|
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||||
import { Chip } from '@heroui/chip'
|
import { Chip } from '@heroui/chip';
|
||||||
import { Spinner } from '@heroui/spinner'
|
import { Spinner } from '@heroui/spinner';
|
||||||
import { Tooltip } from '@heroui/tooltip'
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useRequest } from 'ahooks'
|
import { useRequest } from 'ahooks';
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react';
|
||||||
import { BsStars } from 'react-icons/bs'
|
import { BsStars } from 'react-icons/bs';
|
||||||
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'
|
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6';
|
||||||
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'
|
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io';
|
||||||
import { RiMacFill } from 'react-icons/ri'
|
import { RiMacFill } from 'react-icons/ri';
|
||||||
|
|
||||||
import useDialog from '@/hooks/use-dialog'
|
import useDialog from '@/hooks/use-dialog';
|
||||||
|
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request';
|
||||||
import { compareVersion } from '@/utils/version'
|
import { compareVersion } from '@/utils/version';
|
||||||
|
|
||||||
import WebUIManager from '@/controllers/webui_manager'
|
import WebUIManager from '@/controllers/webui_manager';
|
||||||
import { GithubRelease } from '@/types/github'
|
import { GithubRelease } from '@/types/github';
|
||||||
|
|
||||||
import TailwindMarkdown from './tailwind_markdown'
|
import TailwindMarkdown from './tailwind_markdown';
|
||||||
|
|
||||||
export interface SystemInfoItemProps {
|
export interface SystemInfoItemProps {
|
||||||
title: string
|
title: string
|
||||||
@ -31,71 +31,71 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
|
|||||||
title,
|
title,
|
||||||
value = '--',
|
value = '--',
|
||||||
icon,
|
icon,
|
||||||
endContent
|
endContent,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-100 dark:shadow-primary-100 rounded text-primary-400">
|
<div className='flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-100 dark:shadow-primary-100 rounded text-primary-400'>
|
||||||
{icon}
|
{icon}
|
||||||
<div className="w-24">{title}</div>
|
<div className='w-24'>{title}</div>
|
||||||
<div className="text-primary-200">{value}</div>
|
<div className='text-primary-200'>{value}</div>
|
||||||
<div className="ml-auto">{endContent}</div>
|
<div className='ml-auto'>{endContent}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface NewVersionTipProps {
|
export interface NewVersionTipProps {
|
||||||
currentVersion?: string
|
currentVersion?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewVersionTip = (props: NewVersionTipProps) => {
|
const NewVersionTip = (props: NewVersionTipProps) => {
|
||||||
const { currentVersion } = props
|
const { currentVersion } = props;
|
||||||
const dialog = useDialog()
|
const dialog = useDialog();
|
||||||
const { data: releaseData, error } = useRequest(() =>
|
const { data: releaseData, error } = useRequest(() =>
|
||||||
request.get<GithubRelease[]>(
|
request.get<GithubRelease[]>(
|
||||||
'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
|
'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Tooltip content="检查新版本失败">
|
<Tooltip content='检查新版本失败'>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="shadow"
|
variant='shadow'
|
||||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
className='!w-5 !h-5 !min-w-0 text-small shadow-md'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
dialog.alert({
|
dialog.alert({
|
||||||
title: '检查新版本失败',
|
title: '检查新版本失败',
|
||||||
content: error.message
|
content: error.message,
|
||||||
})
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaInfo />
|
<FaInfo />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestVersion = releaseData?.data?.[0]?.tag_name
|
const latestVersion = releaseData?.data?.[0]?.tag_name;
|
||||||
|
|
||||||
if (!latestVersion || !currentVersion) {
|
if (!latestVersion || !currentVersion) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compareVersion(latestVersion, currentVersion) <= 0) {
|
if (compareVersion(latestVersion, currentVersion) <= 0) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const middleVersions: GithubRelease[] = []
|
const middleVersions: GithubRelease[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < releaseData.data.length; i++) {
|
for (let i = 0; i < releaseData.data.length; i++) {
|
||||||
const versionInfo = releaseData.data[i]
|
const versionInfo = releaseData.data[i];
|
||||||
if (compareVersion(versionInfo.tag_name, currentVersion) > 0) {
|
if (compareVersion(versionInfo.tag_name, currentVersion) > 0) {
|
||||||
middleVersions.push(versionInfo)
|
middleVersions.push(versionInfo);
|
||||||
} else {
|
} else {
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,72 +104,72 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
|||||||
data: aiSummaryData,
|
data: aiSummaryData,
|
||||||
loading: aiSummaryLoading,
|
loading: aiSummaryLoading,
|
||||||
error: aiSummaryError,
|
error: aiSummaryError,
|
||||||
run: runAiSummary
|
run: runAiSummary,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
(version) =>
|
(version) =>
|
||||||
request.get<ServerResponse<string | null>>(
|
request.get<ServerResponse<string | null>>(
|
||||||
`https://release.nc.152710.xyz/?version=${version}`,
|
`https://release.nc.152710.xyz/?version=${version}`,
|
||||||
{
|
{
|
||||||
timeout: 30000
|
timeout: 30000,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
manual: true
|
manual: true,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAiSummary(currentVersion)
|
runAiSummary(currentVersion);
|
||||||
}, [currentVersion, runAiSummary])
|
}, [currentVersion, runAiSummary]);
|
||||||
|
|
||||||
if (aiSummaryLoading) {
|
if (aiSummaryLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center py-1">
|
<div className='flex justify-center py-1'>
|
||||||
<Spinner size="sm" />
|
<Spinner size='sm' />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
if (aiSummaryError) {
|
if (aiSummaryError) {
|
||||||
return <div className="text-center text-primary-500">AI 摘要获取失败</div>
|
return <div className='text-center text-primary-500'>AI 摘要获取失败</div>;
|
||||||
}
|
}
|
||||||
return <span className="text-default-700">{aiSummaryData?.data.data}</span>
|
return <span className='text-default-700'>{aiSummaryData?.data.data}</span>;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="有新版本可用">
|
<Tooltip content='有新版本可用'>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius="full"
|
radius='full'
|
||||||
color="primary"
|
color='primary'
|
||||||
variant="shadow"
|
variant='shadow'
|
||||||
className="!w-5 !h-5 !min-w-0 text-small shadow-md"
|
className='!w-5 !h-5 !min-w-0 text-small shadow-md'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
dialog.confirm({
|
dialog.confirm({
|
||||||
title: '有新版本可用',
|
title: '有新版本可用',
|
||||||
content: (
|
content: (
|
||||||
<div className="space-y-2">
|
<div className='space-y-2'>
|
||||||
<div className="text-sm space-x-2">
|
<div className='text-sm space-x-2'>
|
||||||
<span>当前版本</span>
|
<span>当前版本</span>
|
||||||
<Chip color="primary" variant="flat">
|
<Chip color='primary' variant='flat'>
|
||||||
v{currentVersion}
|
v{currentVersion}
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm space-x-2">
|
<div className='text-sm space-x-2'>
|
||||||
<span>最新版本</span>
|
<span>最新版本</span>
|
||||||
<Chip color="primary">{latestVersion}</Chip>
|
<Chip color='primary'>{latestVersion}</Chip>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded-md bg-content2 text-sm">
|
<div className='p-2 rounded-md bg-content2 text-sm'>
|
||||||
<div className="text-primary-400 font-bold flex items-center gap-1 mb-1">
|
<div className='text-primary-400 font-bold flex items-center gap-1 mb-1'>
|
||||||
<BsStars />
|
<BsStars />
|
||||||
<span>AI总结</span>
|
<span>AI总结</span>
|
||||||
</div>
|
</div>
|
||||||
{<AISummaryComponent />}
|
<AISummaryComponent />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm space-y-2 !mt-4">
|
<div className='text-sm space-y-2 !mt-4'>
|
||||||
{middleVersions.map((versionInfo) => (
|
{middleVersions.map((versionInfo) => (
|
||||||
<div
|
<div
|
||||||
key={versionInfo.tag_name}
|
key={versionInfo.tag_name}
|
||||||
className="p-4 bg-content1 rounded-md shadow-small"
|
className='p-4 bg-content1 rounded-md shadow-small'
|
||||||
>
|
>
|
||||||
<TailwindMarkdown content={versionInfo.body} />
|
<TailwindMarkdown content={versionInfo.body} />
|
||||||
</div>
|
</div>
|
||||||
@ -180,95 +180,103 @@ const NewVersionTip = (props: NewVersionTipProps) => {
|
|||||||
scrollBehavior: 'inside',
|
scrollBehavior: 'inside',
|
||||||
size: '3xl',
|
size: '3xl',
|
||||||
confirmText: '前往下载',
|
confirmText: '前往下载',
|
||||||
onConfirm() {
|
onConfirm () {
|
||||||
window.open(
|
window.open(
|
||||||
'https://github.com/NapNeko/NapCatQQ/releases',
|
'https://github.com/NapNeko/NapCatQQ/releases',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener'
|
'noopener'
|
||||||
)
|
);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaInfo />
|
<FaInfo />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const NapCatVersion = () => {
|
const NapCatVersion = () => {
|
||||||
const {
|
const {
|
||||||
data: packageData,
|
data: packageData,
|
||||||
loading: packageLoading,
|
loading: packageLoading,
|
||||||
error: packageError
|
error: packageError,
|
||||||
} = useRequest(WebUIManager.getPackageInfo)
|
} = useRequest(WebUIManager.getPackageInfo);
|
||||||
|
|
||||||
const currentVersion = packageData?.version
|
const currentVersion = packageData?.version;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SystemInfoItem
|
<SystemInfoItem
|
||||||
title="NapCat 版本"
|
title='NapCat 版本'
|
||||||
icon={<IoLogoOctocat className="text-xl" />}
|
icon={<IoLogoOctocat className='text-xl' />}
|
||||||
value={
|
value={
|
||||||
packageError ? (
|
packageError
|
||||||
|
? (
|
||||||
`错误:${packageError.message}`
|
`错误:${packageError.message}`
|
||||||
) : packageLoading ? (
|
)
|
||||||
<Spinner size="sm" />
|
: packageLoading
|
||||||
) : (
|
? (
|
||||||
currentVersion
|
<Spinner size='sm' />
|
||||||
)
|
)
|
||||||
|
: (
|
||||||
|
currentVersion
|
||||||
|
)
|
||||||
}
|
}
|
||||||
endContent={<NewVersionTip currentVersion={currentVersion} />}
|
endContent={<NewVersionTip currentVersion={currentVersion} />}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface SystemInfoProps {
|
export interface SystemInfoProps {
|
||||||
archInfo?: string
|
archInfo?: string
|
||||||
}
|
}
|
||||||
const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
||||||
const { archInfo } = props
|
const { archInfo } = props;
|
||||||
const {
|
const {
|
||||||
data: qqVersionData,
|
data: qqVersionData,
|
||||||
loading: qqVersionLoading,
|
loading: qqVersionLoading,
|
||||||
error: qqVersionError
|
error: qqVersionError,
|
||||||
} = useRequest(WebUIManager.getQQVersion)
|
} = useRequest(WebUIManager.getQQVersion);
|
||||||
return (
|
return (
|
||||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1">
|
<Card className='bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1'>
|
||||||
<CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
|
<CardHeader className='pb-0 items-center gap-1 text-primary-500 font-extrabold'>
|
||||||
<FaCircleInfo className="text-lg" />
|
<FaCircleInfo className='text-lg' />
|
||||||
<span>系统信息</span>
|
<span>系统信息</span>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="flex-1">
|
<CardBody className='flex-1'>
|
||||||
<div className="flex flex-col justify-between h-full">
|
<div className='flex flex-col justify-between h-full'>
|
||||||
<NapCatVersion />
|
<NapCatVersion />
|
||||||
<SystemInfoItem
|
<SystemInfoItem
|
||||||
title="QQ 版本"
|
title='QQ 版本'
|
||||||
icon={<FaQq className="text-lg" />}
|
icon={<FaQq className='text-lg' />}
|
||||||
value={
|
value={
|
||||||
qqVersionError ? (
|
qqVersionError
|
||||||
|
? (
|
||||||
`错误:${qqVersionError.message}`
|
`错误:${qqVersionError.message}`
|
||||||
) : qqVersionLoading ? (
|
)
|
||||||
<Spinner size="sm" />
|
: qqVersionLoading
|
||||||
) : (
|
? (
|
||||||
qqVersionData
|
<Spinner size='sm' />
|
||||||
)
|
)
|
||||||
|
: (
|
||||||
|
qqVersionData
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SystemInfoItem
|
<SystemInfoItem
|
||||||
title="WebUI 版本"
|
title='WebUI 版本'
|
||||||
icon={<IoLogoChrome className="text-xl" />}
|
icon={<IoLogoChrome className='text-xl' />}
|
||||||
value="Next"
|
value='Next'
|
||||||
/>
|
/>
|
||||||
<SystemInfoItem
|
<SystemInfoItem
|
||||||
title="系统版本"
|
title='系统版本'
|
||||||
icon={<RiMacFill className="text-xl" />}
|
icon={<RiMacFill className='text-xl' />}
|
||||||
value={archInfo}
|
value={archInfo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SystemInfo
|
export default SystemInfo;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Card, CardBody } from '@heroui/card'
|
import { Card, CardBody } from '@heroui/card';
|
||||||
import { Image } from '@heroui/image'
|
import { Image } from '@heroui/image';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { BiSolidMemoryCard } from 'react-icons/bi'
|
import { BiSolidMemoryCard } from 'react-icons/bi';
|
||||||
import { GiCpu } from 'react-icons/gi'
|
import { GiCpu } from 'react-icons/gi';
|
||||||
|
|
||||||
import bkg from '@/assets/images/bg/1AD934174C0107F14BAD8776D29C5F90.png'
|
import bkg from '@/assets/images/bg/1AD934174C0107F14BAD8776D29C5F90.png';
|
||||||
|
|
||||||
import UsagePie from './usage_pie'
|
import UsagePie from './usage_pie';
|
||||||
|
|
||||||
export interface SystemStatusItemProps {
|
export interface SystemStatusItemProps {
|
||||||
title: string
|
title: string
|
||||||
@ -19,7 +19,7 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
|||||||
title,
|
title,
|
||||||
value = '-',
|
value = '-',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
unit
|
unit,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -28,14 +28,14 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
|||||||
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
|
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="w-24">{title}</div>
|
<div className='w-24'>{title}</div>
|
||||||
<div className="text-default-400">
|
<div className='text-default-400'>
|
||||||
{value}
|
{value}
|
||||||
{unit}
|
{unit}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface SystemStatusDisplayProps {
|
export interface SystemStatusDisplayProps {
|
||||||
data?: SystemStatus
|
data?: SystemStatus
|
||||||
@ -44,87 +44,87 @@ export interface SystemStatusDisplayProps {
|
|||||||
const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||||
const memoryUsage = {
|
const memoryUsage = {
|
||||||
system: 0,
|
system: 0,
|
||||||
qq: 0
|
qq: 0,
|
||||||
}
|
};
|
||||||
if (data) {
|
if (data) {
|
||||||
const system = Number(data.memory.total) || 1
|
const system = Number(data.memory.total) || 1;
|
||||||
const systemUsage = Number(data.memory.usage.system)
|
const systemUsage = Number(data.memory.usage.system);
|
||||||
const qqUsage = Number(data.memory.usage.qq)
|
const qqUsage = Number(data.memory.usage.qq);
|
||||||
memoryUsage.system = (systemUsage / system) * 100
|
memoryUsage.system = (systemUsage / system) * 100;
|
||||||
memoryUsage.qq = (qqUsage / system) * 100
|
memoryUsage.qq = (qqUsage / system) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-opacity-60 shadow-sm shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden">
|
<Card className='bg-opacity-60 shadow-sm shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden'>
|
||||||
<div className="absolute h-full right-0 top-0">
|
<div className='absolute h-full right-0 top-0'>
|
||||||
<Image
|
<Image
|
||||||
src={bkg}
|
src={bkg}
|
||||||
alt="background"
|
alt='background'
|
||||||
className="select-none pointer-events-none !opacity-30 w-full h-full"
|
className='select-none pointer-events-none !opacity-30 w-full h-full'
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'w-full h-full',
|
wrapper: 'w-full h-full',
|
||||||
img: 'object-contain w-full h-full'
|
img: 'object-contain w-full h-full',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardBody className="overflow-visible md:flex-row gap-4 items-center justify-stretch z-10">
|
<CardBody className='overflow-visible md:flex-row gap-4 items-center justify-stretch z-10'>
|
||||||
<div className="flex-1 w-full md:max-w-96">
|
<div className='flex-1 w-full md:max-w-96'>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400">
|
<h2 className='text-lg font-semibold flex items-center gap-1 text-primary-400'>
|
||||||
<GiCpu className="text-xl" />
|
<GiCpu className='text-xl' />
|
||||||
<span>CPU</span>
|
<span>CPU</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
<SystemStatusItem title="型号" value={data?.cpu.model} size="lg" />
|
<SystemStatusItem title='型号' value={data?.cpu.model} size='lg' />
|
||||||
<SystemStatusItem title="内核数" value={data?.cpu.core} />
|
<SystemStatusItem title='内核数' value={data?.cpu.core} />
|
||||||
<SystemStatusItem title="主频" value={data?.cpu.speed} unit="GHz" />
|
<SystemStatusItem title='主频' value={data?.cpu.speed} unit='GHz' />
|
||||||
<SystemStatusItem
|
<SystemStatusItem
|
||||||
title="使用率"
|
title='使用率'
|
||||||
value={data?.cpu.usage.system}
|
value={data?.cpu.usage.system}
|
||||||
unit="%"
|
unit='%'
|
||||||
/>
|
/>
|
||||||
<SystemStatusItem
|
<SystemStatusItem
|
||||||
title="QQ主线程"
|
title='QQ主线程'
|
||||||
value={data?.cpu.usage.qq}
|
value={data?.cpu.usage.qq}
|
||||||
unit="%"
|
unit='%'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2">
|
<h2 className='text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2'>
|
||||||
<BiSolidMemoryCard className="text-xl" />
|
<BiSolidMemoryCard className='text-xl' />
|
||||||
<span>内存</span>
|
<span>内存</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
<SystemStatusItem
|
<SystemStatusItem
|
||||||
title="总量"
|
title='总量'
|
||||||
value={data?.memory.total}
|
value={data?.memory.total}
|
||||||
size="lg"
|
size='lg'
|
||||||
unit="MB"
|
unit='MB'
|
||||||
/>
|
/>
|
||||||
<SystemStatusItem
|
<SystemStatusItem
|
||||||
title="使用量"
|
title='使用量'
|
||||||
value={data?.memory.usage.system}
|
value={data?.memory.usage.system}
|
||||||
unit="MB"
|
unit='MB'
|
||||||
/>
|
/>
|
||||||
<SystemStatusItem
|
<SystemStatusItem
|
||||||
title="QQ主线程"
|
title='QQ主线程'
|
||||||
value={data?.memory.usage.qq}
|
value={data?.memory.usage.qq}
|
||||||
unit="MB"
|
unit='MB'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row md:flex-col gap-2 flex-shrink-0 w-full justify-center md:w-40 min-h-40 mt-4 md:mt-0 md:mx-auto">
|
<div className='flex flex-row md:flex-col gap-2 flex-shrink-0 w-full justify-center md:w-40 min-h-40 mt-4 md:mt-0 md:mx-auto'>
|
||||||
<UsagePie
|
<UsagePie
|
||||||
systemUsage={Number(data?.cpu.usage.system) || 0}
|
systemUsage={Number(data?.cpu.usage.system) || 0}
|
||||||
processUsage={Number(data?.cpu.usage.qq) || 0}
|
processUsage={Number(data?.cpu.usage.qq) || 0}
|
||||||
title="CPU占用"
|
title='CPU占用'
|
||||||
/>
|
/>
|
||||||
<UsagePie
|
<UsagePie
|
||||||
systemUsage={memoryUsage.system}
|
systemUsage={memoryUsage.system}
|
||||||
processUsage={memoryUsage.qq}
|
processUsage={memoryUsage.qq}
|
||||||
title="内存占用"
|
title='内存占用'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default SystemStatusDisplay
|
export default SystemStatusDisplay;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { type ReactNode, createContext, forwardRef, useContext } from 'react'
|
import { type ReactNode, createContext, forwardRef, useContext } from 'react';
|
||||||
|
|
||||||
export interface TabsContextValue {
|
export interface TabsContextValue {
|
||||||
activeKey: string
|
activeKey: string
|
||||||
@ -8,8 +8,8 @@ export interface TabsContextValue {
|
|||||||
|
|
||||||
const TabsContext = createContext<TabsContextValue>({
|
const TabsContext = createContext<TabsContextValue>({
|
||||||
activeKey: '',
|
activeKey: '',
|
||||||
onChange: () => {}
|
onChange: () => {},
|
||||||
})
|
});
|
||||||
|
|
||||||
export interface TabsProps {
|
export interface TabsProps {
|
||||||
activeKey: string
|
activeKey: string
|
||||||
@ -18,12 +18,12 @@ export interface TabsProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tabs({ activeKey, onChange, children, className }: TabsProps) {
|
export function Tabs ({ activeKey, onChange, children, className }: TabsProps) {
|
||||||
return (
|
return (
|
||||||
<TabsContext.Provider value={{ activeKey, onChange }}>
|
<TabsContext.Provider value={{ activeKey, onChange }}>
|
||||||
<div className={clsx('flex flex-col gap-2', className)}>{children}</div>
|
<div className={clsx('flex flex-col gap-2', className)}>{children}</div>
|
||||||
</TabsContext.Provider>
|
</TabsContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabListProps {
|
export interface TabListProps {
|
||||||
@ -31,10 +31,10 @@ export interface TabListProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabList({ children, className }: TabListProps) {
|
export function TabList ({ children, className }: TabListProps) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex items-center gap-1', className)}>{children}</div>
|
<div className={clsx('flex items-center gap-1', className)}>{children}</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabProps extends React.ButtonHTMLAttributes<HTMLDivElement> {
|
export interface TabProps extends React.ButtonHTMLAttributes<HTMLDivElement> {
|
||||||
@ -46,17 +46,17 @@ export interface TabProps extends React.ButtonHTMLAttributes<HTMLDivElement> {
|
|||||||
|
|
||||||
export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||||
({ className, isSelected, value, ...props }, ref) => {
|
({ className, isSelected, value, ...props }, ref) => {
|
||||||
const { onChange } = useContext(TabsContext)
|
const { onChange } = useContext(TabsContext);
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
onChange(value)
|
onChange(value);
|
||||||
props.onClick?.(e)
|
props.onClick?.(e);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
role="tab"
|
role='tab'
|
||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -68,11 +68,11 @@ export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
Tab.displayName = 'Tab'
|
Tab.displayName = 'Tab';
|
||||||
|
|
||||||
export interface TabPanelProps {
|
export interface TabPanelProps {
|
||||||
value: string
|
value: string
|
||||||
@ -80,10 +80,10 @@ export interface TabPanelProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabPanel({ value, children, className }: TabPanelProps) {
|
export function TabPanel ({ value, children, className }: TabPanelProps) {
|
||||||
const { activeKey } = useContext(TabsContext)
|
const { activeKey } = useContext(TabsContext);
|
||||||
|
|
||||||
if (value !== activeKey) return null
|
if (value !== activeKey) return null;
|
||||||
|
|
||||||
return <div className={clsx('flex-1', className)}>{children}</div>
|
return <div className={clsx('flex-1', className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,30 @@
|
|||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
|
||||||
import { Tab } from '@/components/tabs'
|
import { Tab } from '@/components/tabs';
|
||||||
import type { TabProps } from '@/components/tabs'
|
import type { TabProps } from '@/components/tabs';
|
||||||
|
|
||||||
interface SortableTabProps extends TabProps {
|
interface SortableTabProps extends TabProps {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SortableTab({ id, ...props }: SortableTabProps) {
|
export function SortableTab ({ id, ...props }: SortableTabProps) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
setNodeRef,
|
setNodeRef,
|
||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
isDragging
|
isDragging,
|
||||||
} = useSortable({ id })
|
} = useSortable({ id });
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? 1 : 0,
|
zIndex: isDragging ? 1 : 0,
|
||||||
position: 'relative' as const,
|
position: 'relative' as const,
|
||||||
touchAction: 'none'
|
touchAction: 'none',
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
@ -34,5 +34,5 @@ export function SortableTab({ id, ...props }: SortableTabProps) {
|
|||||||
{...listeners}
|
{...listeners}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,49 @@
|
|||||||
import Markdown from 'react-markdown'
|
import Markdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
|
const TailwindMarkdown: React.FC<{ content: string }> = ({ content }) => {
|
||||||
return (
|
return (
|
||||||
<Markdown
|
<Markdown
|
||||||
className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl"
|
className='prose prose-sm sm:prose lg:prose-lg xl:prose-xl'
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
h1: ({ node, ...props }) => (
|
h1: ({ node, ...props }) => (
|
||||||
<h1 className="text-2xl font-bold" {...props} />
|
<h1 className='text-2xl font-bold' {...props} />
|
||||||
),
|
),
|
||||||
h2: ({ node, ...props }) => (
|
h2: ({ node, ...props }) => (
|
||||||
<h2 className="text-xl font-bold" {...props} />
|
<h2 className='text-xl font-bold' {...props} />
|
||||||
),
|
),
|
||||||
h3: ({ node, ...props }) => (
|
h3: ({ node, ...props }) => (
|
||||||
<h3 className="text-lg font-bold" {...props} />
|
<h3 className='text-lg font-bold' {...props} />
|
||||||
),
|
),
|
||||||
p: ({ node, ...props }) => <p className="m-0" {...props} />,
|
p: ({ node, ...props }) => <p className='m-0' {...props} />,
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
className="text-primary-500 inline-block hover:underline"
|
className='text-primary-500 inline-block hover:underline'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
ul: ({ node, ...props }) => (
|
ul: ({ node, ...props }) => (
|
||||||
<ul className="list-disc list-inside" {...props} />
|
<ul className='list-disc list-inside' {...props} />
|
||||||
),
|
),
|
||||||
ol: ({ node, ...props }) => (
|
ol: ({ node, ...props }) => (
|
||||||
<ol className="list-decimal list-inside" {...props} />
|
<ol className='list-decimal list-inside' {...props} />
|
||||||
),
|
),
|
||||||
blockquote: ({ node, ...props }) => (
|
blockquote: ({ node, ...props }) => (
|
||||||
<blockquote
|
<blockquote
|
||||||
className="border-l-4 border-default-300 pl-4 italic"
|
className='border-l-4 border-default-300 pl-4 italic'
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
code: ({ node, ...props }) => (
|
code: ({ node, ...props }) => (
|
||||||
<code className="bg-default-100 p-1 rounded text-xs" {...props} />
|
<code className='bg-default-100 p-1 rounded text-xs' {...props} />
|
||||||
)
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TailwindMarkdown
|
export default TailwindMarkdown;
|
||||||
|
|||||||
@ -1,56 +1,56 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import TerminalManager from '@/controllers/terminal_manager'
|
import TerminalManager from '@/controllers/terminal_manager';
|
||||||
|
|
||||||
import XTerm, { XTermRef } from '../xterm'
|
import XTerm, { XTermRef } from '../xterm';
|
||||||
|
|
||||||
interface TerminalInstanceProps {
|
interface TerminalInstanceProps {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
export function TerminalInstance ({ id }: TerminalInstanceProps) {
|
||||||
const termRef = useRef<XTermRef>(null)
|
const termRef = useRef<XTermRef>(null);
|
||||||
const connected = useRef(false)
|
const connected = useRef(false);
|
||||||
|
|
||||||
const handleData = (data: string) => {
|
const handleData = (data: string) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data)
|
const parsed = JSON.parse(data);
|
||||||
if (parsed.data) {
|
if (parsed.data) {
|
||||||
termRef.current?.write(parsed.data)
|
termRef.current?.write(parsed.data);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
termRef.current?.write(data)
|
termRef.current?.write(data);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (connected.current) {
|
if (connected.current) {
|
||||||
TerminalManager.disconnectTerminal(id, handleData)
|
TerminalManager.disconnectTerminal(id, handleData);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [id])
|
}, [id]);
|
||||||
|
|
||||||
const handleInput = (data: string) => {
|
const handleInput = (data: string) => {
|
||||||
TerminalManager.sendInput(id, data)
|
TerminalManager.sendInput(id, data);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleResize = (cols: number, rows: number) => {
|
const handleResize = (cols: number, rows: number) => {
|
||||||
if (!connected.current) {
|
if (!connected.current) {
|
||||||
connected.current = true
|
connected.current = true;
|
||||||
console.log('instance', rows, cols)
|
console.log('instance', rows, cols);
|
||||||
TerminalManager.connectTerminal(id, handleData, { rows, cols })
|
TerminalManager.connectTerminal(id, handleData, { rows, cols });
|
||||||
} else {
|
} else {
|
||||||
TerminalManager.sendResize(id, cols, rows)
|
TerminalManager.sendResize(id, cols, rows);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<XTerm
|
<XTerm
|
||||||
ref={termRef}
|
ref={termRef}
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onResize={handleResize} // 使用 fitAddon 改变后触发的 resize 回调
|
onResize={handleResize} // 使用 fitAddon 改变后触发的 resize 回调
|
||||||
className="w-full h-full"
|
className='w-full h-full'
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { SwitchProps, useSwitch } from '@heroui/switch'
|
import { SwitchProps, useSwitch } from '@heroui/switch';
|
||||||
import { VisuallyHidden } from '@react-aria/visually-hidden'
|
import { VisuallyHidden } from '@react-aria/visually-hidden';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { MoonFilledIcon, SunFilledIcon } from '@/components/icons'
|
import { MoonFilledIcon, SunFilledIcon } from '@/components/icons';
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
export interface ThemeSwitchProps {
|
export interface ThemeSwitchProps {
|
||||||
className?: string
|
className?: string
|
||||||
@ -14,13 +14,13 @@ export interface ThemeSwitchProps {
|
|||||||
|
|
||||||
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
||||||
className,
|
className,
|
||||||
classNames
|
classNames,
|
||||||
}) => {
|
}) => {
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
const { theme, toggleTheme } = useTheme()
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
const onChange = toggleTheme
|
const onChange = toggleTheme;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Component,
|
Component,
|
||||||
@ -28,18 +28,18 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
|||||||
isSelected,
|
isSelected,
|
||||||
getBaseProps,
|
getBaseProps,
|
||||||
getInputProps,
|
getInputProps,
|
||||||
getWrapperProps
|
getWrapperProps,
|
||||||
} = useSwitch({
|
} = useSwitch({
|
||||||
isSelected: theme === 'light',
|
isSelected: theme === 'light',
|
||||||
onChange
|
onChange,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true)
|
setIsMounted(true);
|
||||||
}, [isMounted])
|
}, [isMounted]);
|
||||||
|
|
||||||
// Prevent Hydration Mismatch
|
// Prevent Hydration Mismatch
|
||||||
if (!isMounted) return <div className="w-6 h-6" />
|
if (!isMounted) return <div className='w-6 h-6' />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
@ -49,7 +49,7 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
|||||||
'px-px transition-opacity hover:opacity-80 cursor-pointer',
|
'px-px transition-opacity hover:opacity-80 cursor-pointer',
|
||||||
className,
|
className,
|
||||||
classNames?.base
|
classNames?.base
|
||||||
)
|
),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<VisuallyHidden>
|
<VisuallyHidden>
|
||||||
@ -68,18 +68,20 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
|||||||
'!text-default-500',
|
'!text-default-500',
|
||||||
'pt-px',
|
'pt-px',
|
||||||
'px-0',
|
'px-0',
|
||||||
'mx-0'
|
'mx-0',
|
||||||
],
|
],
|
||||||
classNames?.wrapper
|
classNames?.wrapper
|
||||||
)
|
),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isSelected ? (
|
{isSelected
|
||||||
<MoonFilledIcon size={22} />
|
? (
|
||||||
) : (
|
<MoonFilledIcon size={22} />
|
||||||
<SunFilledIcon size={22} />
|
)
|
||||||
)}
|
: (
|
||||||
|
<SunFilledIcon size={22} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Component>
|
</Component>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Toaster as HotToaster } from 'react-hot-toast'
|
import { Toaster as HotToaster } from 'react-hot-toast';
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
export const Toaster = () => {
|
export const Toaster = () => {
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotToaster
|
<HotToaster
|
||||||
@ -11,11 +11,11 @@ export const Toaster = () => {
|
|||||||
style: {
|
style: {
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
background: isDark ? '#333' : '#fff',
|
background: isDark ? '#333' : '#fff',
|
||||||
color: isDark ? '#fff' : '#333'
|
color: isDark ? '#fff' : '#333',
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Toaster
|
export default Toaster;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
export default function UnderConstruction() {
|
export default function UnderConstruction () {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full pt-4">
|
<div className='flex flex-col items-center justify-center h-full pt-4'>
|
||||||
<div className="flex flex-col items-center justify-center space-y-4">
|
<div className='flex flex-col items-center justify-center space-y-4'>
|
||||||
<div className="text-6xl font-bold text-gray-500">🚧</div>
|
<div className='text-6xl font-bold text-gray-500'>🚧</div>
|
||||||
<div className="text-2xl font-bold text-gray-500">
|
<div className='text-2xl font-bold text-gray-500'>
|
||||||
Under Construction
|
Under Construction
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts';
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
interface UsagePieProps {
|
interface UsagePieProps {
|
||||||
systemUsage: number
|
systemUsage: number
|
||||||
@ -14,7 +14,7 @@ const defaultOption: echarts.EChartsOption = {
|
|||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '<center>{b}<br/><b>{d}%</b></center>',
|
formatter: '<center>{b}<br/><b>{d}%</b></center>',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
extraCssText: 'backdrop-filter: blur(10px);'
|
extraCssText: 'backdrop-filter: blur(10px);',
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@ -26,49 +26,49 @@ const defaultOption: echarts.EChartsOption = {
|
|||||||
show: true,
|
show: true,
|
||||||
position: 'center',
|
position: 'center',
|
||||||
formatter: '系统占用',
|
formatter: '系统占用',
|
||||||
fontSize: 14
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 10
|
borderRadius: 10,
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
value: 100,
|
value: 100,
|
||||||
name: '系统总量'
|
name: '系统总量',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
const UsagePie: React.FC<UsagePieProps> = ({
|
const UsagePie: React.FC<UsagePieProps> = ({
|
||||||
systemUsage,
|
systemUsage,
|
||||||
processUsage,
|
processUsage,
|
||||||
title
|
title,
|
||||||
}) => {
|
}) => {
|
||||||
const chartRef = useRef<HTMLDivElement>(null)
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
const chartInstance = useRef<echarts.ECharts | null>(null)
|
const chartInstance = useRef<echarts.ECharts | null>(null);
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chartRef.current) {
|
if (chartRef.current) {
|
||||||
chartInstance.current = echarts.init(chartRef.current)
|
chartInstance.current = echarts.init(chartRef.current);
|
||||||
const option = defaultOption
|
const option = defaultOption;
|
||||||
chartInstance.current.setOption(option)
|
chartInstance.current.setOption(option);
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
chartInstance.current?.resize()
|
chartInstance.current?.resize();
|
||||||
})
|
});
|
||||||
observer.observe(chartRef.current)
|
observer.observe(chartRef.current);
|
||||||
return () => {
|
return () => {
|
||||||
chartInstance.current?.dispose()
|
chartInstance.current?.dispose();
|
||||||
observer.disconnect()
|
observer.disconnect();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chartInstance.current) {
|
if (chartInstance.current) {
|
||||||
@ -76,13 +76,13 @@ const UsagePie: React.FC<UsagePieProps> = ({
|
|||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
label: {
|
label: {
|
||||||
formatter: title
|
formatter: title,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [title])
|
}, [title]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chartInstance.current) {
|
if (chartInstance.current) {
|
||||||
@ -94,8 +94,8 @@ const UsagePie: React.FC<UsagePieProps> = ({
|
|||||||
? 'rgba(0, 0, 0, 0.8)'
|
? 'rgba(0, 0, 0, 0.8)'
|
||||||
: 'rgba(255, 255, 255, 0.8)',
|
: 'rgba(255, 255, 255, 0.8)',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: theme === 'dark' ? '#fff' : '#333'
|
color: theme === 'dark' ? '#fff' : '#333',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
color:
|
color:
|
||||||
theme === 'dark'
|
theme === 'dark'
|
||||||
@ -104,13 +104,13 @@ const UsagePie: React.FC<UsagePieProps> = ({
|
|||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: theme === 'dark' ? '#333' : '#F0A9A7'
|
borderColor: theme === 'dark' ? '#333' : '#F0A9A7',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [theme])
|
}, [theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chartInstance.current) {
|
if (chartInstance.current) {
|
||||||
@ -120,24 +120,24 @@ const UsagePie: React.FC<UsagePieProps> = ({
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
value: processUsage,
|
value: processUsage,
|
||||||
name: 'QQ占用'
|
name: 'QQ占用',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: systemUsage - processUsage,
|
value: systemUsage - processUsage,
|
||||||
name: '其他进程占用'
|
name: '其他进程占用',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 100 - systemUsage,
|
value: 100 - systemUsage,
|
||||||
name: '剩余系统总量'
|
name: '剩余系统总量',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [systemUsage, processUsage])
|
}, [systemUsage, processUsage]);
|
||||||
|
|
||||||
return <div ref={chartRef} className="w-36 h-36 flex-shrink-0" />
|
return <div ref={chartRef} className='w-36 h-36 flex-shrink-0' />;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default UsagePie
|
export default UsagePie;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { CanvasAddon } from '@xterm/addon-canvas'
|
import { CanvasAddon } from '@xterm/addon-canvas';
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||||
// import { WebglAddon } from '@xterm/addon-webgl'
|
// import { WebglAddon } from '@xterm/addon-webgl'
|
||||||
import { Terminal } from '@xterm/xterm'
|
import { Terminal } from '@xterm/xterm';
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx';
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/use-theme'
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
export type XTermRef = {
|
export type XTermRef = {
|
||||||
write: (
|
write: (
|
||||||
@ -20,7 +20,7 @@ export type XTermRef = {
|
|||||||
writelnAsync: (data: Parameters<Terminal['writeln']>[0]) => Promise<void>
|
writelnAsync: (data: Parameters<Terminal['writeln']>[0]) => Promise<void>
|
||||||
clear: () => void
|
clear: () => void
|
||||||
terminalRef: React.RefObject<Terminal | null>
|
terminalRef: React.RefObject<Terminal | null>
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface XTermProps
|
export interface XTermProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput' | 'onResize'> {
|
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput' | 'onResize'> {
|
||||||
@ -30,10 +30,10 @@ export interface XTermProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||||
const domRef = useRef<HTMLDivElement>(null)
|
const domRef = useRef<HTMLDivElement>(null);
|
||||||
const terminalRef = useRef<Terminal | null>(null)
|
const terminalRef = useRef<Terminal | null>(null);
|
||||||
const { className, onInput, onKey, onResize, ...rest } = props
|
const { className, onInput, onKey, onResize, ...rest } = props;
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const terminal = new Terminal({
|
const terminal = new Terminal({
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
@ -42,57 +42,57 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
cursorInactiveStyle: 'outline',
|
cursorInactiveStyle: 'outline',
|
||||||
drawBoldTextInBrightColors: false,
|
drawBoldTextInBrightColors: false,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 1.2
|
lineHeight: 1.2,
|
||||||
})
|
});
|
||||||
terminalRef.current = terminal
|
terminalRef.current = terminal;
|
||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon();
|
||||||
terminal.loadAddon(
|
terminal.loadAddon(
|
||||||
new WebLinksAddon((event, uri) => {
|
new WebLinksAddon((event, uri) => {
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
window.open(uri, '_blank')
|
window.open(uri, '_blank');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
terminal.loadAddon(fitAddon)
|
terminal.loadAddon(fitAddon);
|
||||||
terminal.open(domRef.current!)
|
terminal.open(domRef.current!);
|
||||||
|
|
||||||
terminal.loadAddon(new CanvasAddon())
|
terminal.loadAddon(new CanvasAddon());
|
||||||
terminal.onData((data) => {
|
terminal.onData((data) => {
|
||||||
if (onInput) {
|
if (onInput) {
|
||||||
onInput(data)
|
onInput(data);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
terminal.onKey((event) => {
|
terminal.onKey((event) => {
|
||||||
if (onKey) {
|
if (onKey) {
|
||||||
onKey(event.key, event.domEvent)
|
onKey(event.key, event.domEvent);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
fitAddon.fit()
|
fitAddon.fit();
|
||||||
// 获取当前终端尺寸
|
// 获取当前终端尺寸
|
||||||
const cols = terminal.cols
|
const cols = terminal.cols;
|
||||||
const rows = terminal.rows
|
const rows = terminal.rows;
|
||||||
if (onResize) {
|
if (onResize) {
|
||||||
onResize(cols, rows)
|
onResize(cols, rows);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 字体加载完成后重新调整终端大小
|
// 字体加载完成后重新调整终端大小
|
||||||
document.fonts.ready.then(() => {
|
document.fonts.ready.then(() => {
|
||||||
fitAddon.fit()
|
fitAddon.fit();
|
||||||
|
|
||||||
resizeObserver.observe(domRef.current!)
|
resizeObserver.observe(domRef.current!);
|
||||||
})
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
terminal.dispose()
|
terminal.dispose();
|
||||||
}, 0)
|
}, 0);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminalRef.current) {
|
if (terminalRef.current) {
|
||||||
@ -115,8 +115,8 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
brightWhite: '#e5e5e5',
|
brightWhite: '#e5e5e5',
|
||||||
foreground: '#cccccc',
|
foreground: '#cccccc',
|
||||||
selectionBackground: '#3a3d41',
|
selectionBackground: '#3a3d41',
|
||||||
cursor: '#ffffff'
|
cursor: '#ffffff',
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
terminalRef.current.options.theme = {
|
terminalRef.current.options.theme = {
|
||||||
background: '#ffffff00',
|
background: '#ffffff00',
|
||||||
@ -136,38 +136,38 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
brightWhite: '#b0b0b0',
|
brightWhite: '#b0b0b0',
|
||||||
foreground: '#000000',
|
foreground: '#000000',
|
||||||
selectionBackground: '#bfdbfe',
|
selectionBackground: '#bfdbfe',
|
||||||
cursor: '#007acc'
|
cursor: '#007acc',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme])
|
}, [theme]);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
() => ({
|
() => ({
|
||||||
write: (...args) => {
|
write: (...args) => {
|
||||||
return terminalRef.current?.write(...args)
|
return terminalRef.current?.write(...args);
|
||||||
},
|
},
|
||||||
writeAsync: async (data) => {
|
writeAsync: async (data) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
terminalRef.current?.write(data, resolve)
|
terminalRef.current?.write(data, resolve);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
writeln: (...args) => {
|
writeln: (...args) => {
|
||||||
return terminalRef.current?.writeln(...args)
|
return terminalRef.current?.writeln(...args);
|
||||||
},
|
},
|
||||||
writelnAsync: async (data) => {
|
writelnAsync: async (data) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
terminalRef.current?.writeln(data, resolve)
|
terminalRef.current?.writeln(data, resolve);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
terminalRef.current?.clear()
|
terminalRef.current?.clear();
|
||||||
},
|
},
|
||||||
terminalRef: terminalRef
|
terminalRef,
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -181,12 +181,12 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%'
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
ref={domRef}
|
ref={domRef}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
export default XTerm
|
export default XTerm;
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import {
|
|||||||
RouteIcon,
|
RouteIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
SignalTowerIcon,
|
SignalTowerIcon,
|
||||||
TerminalIcon
|
TerminalIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons';
|
||||||
|
|
||||||
export type SiteConfig = typeof siteConfig
|
export type SiteConfig = typeof siteConfig;
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
label: string
|
label: string
|
||||||
icon?: React.ReactNode
|
icon?: React.ReactNode
|
||||||
@ -26,87 +26,87 @@ export const siteConfig = {
|
|||||||
{
|
{
|
||||||
label: '基础信息',
|
label: '基础信息',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<RouteIcon />
|
<RouteIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/'
|
href: '/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '网络配置',
|
label: '网络配置',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<SignalTowerIcon />
|
<SignalTowerIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/network'
|
href: '/network',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '其他配置',
|
label: '其他配置',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/config'
|
href: '/config',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '猫猫日志',
|
label: '猫猫日志',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<LogIcon />
|
<LogIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/logs'
|
href: '/logs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '接口调试',
|
label: '接口调试',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<BugIcon2 />
|
<BugIcon2 />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'HTTP',
|
label: 'HTTP',
|
||||||
href: '/debug/http'
|
href: '/debug/http',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Websocket',
|
label: 'Websocket',
|
||||||
href: '/debug/ws'
|
href: '/debug/ws',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '文件管理',
|
label: '文件管理',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<FileIcon />
|
<FileIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/file_manager'
|
href: '/file_manager',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '系统终端',
|
label: '系统终端',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<TerminalIcon />
|
<TerminalIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/terminal'
|
href: '/terminal',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关于我们',
|
label: '关于我们',
|
||||||
icon: (
|
icon: (
|
||||||
<div className="w-5 h-5">
|
<div className='w-5 h-5'>
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
href: '/about'
|
href: '/about',
|
||||||
}
|
},
|
||||||
] as MenuItem[],
|
] as MenuItem[],
|
||||||
links: {
|
links: {
|
||||||
github: 'https://github.com/NapNeko/NapCatQQ',
|
github: 'https://github.com/NapNeko/NapCatQQ',
|
||||||
docs: 'https://napcat.napneko.icu/'
|
docs: 'https://napcat.napneko.icu/',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@ -3,11 +3,11 @@ export enum LogLevel {
|
|||||||
INFO = 'info',
|
INFO = 'info',
|
||||||
WARN = 'warn',
|
WARN = 'warn',
|
||||||
ERROR = 'error',
|
ERROR = 'error',
|
||||||
FATAL = 'fatal'
|
FATAL = 'fatal',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PlayMode {
|
export enum PlayMode {
|
||||||
Loop = 'loop',
|
Loop = 'loop',
|
||||||
Random = 'random',
|
Random = 'random',
|
||||||
Single = 'single'
|
Single = 'single',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ enum key {
|
|||||||
isCollapsedMusicPlayer = 'is-collapsed-music-player',
|
isCollapsedMusicPlayer = 'is-collapsed-music-player',
|
||||||
sideBarOpen = 'side-bar-open',
|
sideBarOpen = 'side-bar-open',
|
||||||
httpDebugConfig = 'http-debug-config',
|
httpDebugConfig = 'http-debug-config',
|
||||||
wsDebugConfig = 'ws-debug-config'
|
wsDebugConfig = 'ws-debug-config',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default key
|
export default key;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod';
|
||||||
|
|
||||||
import messageNodeSchema from './message/node'
|
import messageNodeSchema from './message/node';
|
||||||
import { baseResponseSchema, commonResponseDataSchema } from './response'
|
import { baseResponseSchema, commonResponseDataSchema } from './response';
|
||||||
|
|
||||||
const oneBotHttpApiGroup = {
|
const oneBotHttpApiGroup = {
|
||||||
'/set_group_kick': {
|
'/set_group_kick': {
|
||||||
@ -9,18 +9,18 @@ const oneBotHttpApiGroup = {
|
|||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
||||||
reject_add_request: z.boolean().describe('拒绝此人的加群请求')
|
reject_add_request: z.boolean().describe('拒绝此人的加群请求'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/set_group_ban': {
|
'/set_group_ban': {
|
||||||
description: '群禁言',
|
description: '群禁言',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
||||||
duration: z.number()
|
duration: z.number(),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/get_group_system_msg': {
|
'/get_group_system_msg': {
|
||||||
description: '获取群系统消息',
|
description: '获取群系统消息',
|
||||||
@ -38,7 +38,7 @@ const oneBotHttpApiGroup = {
|
|||||||
message: z.string().describe('入群回答'),
|
message: z.string().describe('入群回答'),
|
||||||
group_name: z.string().describe('群名称'),
|
group_name: z.string().describe('群名称'),
|
||||||
checked: z.boolean().describe('是否已处理'),
|
checked: z.boolean().describe('是否已处理'),
|
||||||
actor: z.string().describe('处理人 QQ 号')
|
actor: z.string().describe('处理人 QQ 号'),
|
||||||
})
|
})
|
||||||
.describe('邀请入群请求')
|
.describe('邀请入群请求')
|
||||||
)
|
)
|
||||||
@ -52,16 +52,16 @@ const oneBotHttpApiGroup = {
|
|||||||
message: z.string().describe('入群回答'),
|
message: z.string().describe('入群回答'),
|
||||||
group_name: z.string().describe('群名称'),
|
group_name: z.string().describe('群名称'),
|
||||||
checked: z.boolean().describe('是否已处理'),
|
checked: z.boolean().describe('是否已处理'),
|
||||||
actor: z.string().describe('处理人 QQ 号')
|
actor: z.string().describe('处理人 QQ 号'),
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_essence_msg_list': {
|
'/get_essence_msg_list': {
|
||||||
description: '获取精华消息',
|
description: '获取精华消息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z
|
data: z
|
||||||
@ -76,44 +76,44 @@ const oneBotHttpApiGroup = {
|
|||||||
operator_nick: z.string().describe('操作人昵称'),
|
operator_nick: z.string().describe('操作人昵称'),
|
||||||
message_id: z.string().describe('消息 ID'),
|
message_id: z.string().describe('消息 ID'),
|
||||||
operator_time: z.string().describe('操作时间'),
|
operator_time: z.string().describe('操作时间'),
|
||||||
content: z.array(messageNodeSchema)
|
content: z.array(messageNodeSchema),
|
||||||
})
|
})
|
||||||
.describe('精华消息')
|
.describe('精华消息')
|
||||||
)
|
)
|
||||||
.describe('精华消息列表')
|
.describe('精华消息列表'),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_group_whole_ban': {
|
'/set_group_whole_ban': {
|
||||||
description: '全员禁言',
|
description: '全员禁言',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
enable: z.boolean().describe('是否开启')
|
enable: z.boolean().describe('是否开启'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/set_group_portrait': {
|
'/set_group_portrait': {
|
||||||
description: '设置群头像',
|
description: '设置群头像',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
file: z.string().describe('图片文件路径,服务器本地路径或远程 URL')
|
file: z.string().describe('图片文件路径,服务器本地路径或远程 URL'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: commonResponseDataSchema
|
data: commonResponseDataSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_group_admin': {
|
'/set_group_admin': {
|
||||||
description: '设置群管理',
|
description: '设置群管理',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
||||||
enable: z.boolean().describe('是否设置为管理员')
|
enable: z.boolean().describe('是否设置为管理员'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/set_essence_msg': {
|
'/set_essence_msg': {
|
||||||
description: '设置群精华消息',
|
description: '设置群精华消息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
message_id: z.union([z.string(), z.number()]).describe('消息 ID')
|
message_id: z.union([z.string(), z.number()]).describe('消息 ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@ -139,28 +139,28 @@ const oneBotHttpApiGroup = {
|
|||||||
addDigestTime: z.number().describe('添加精华消息时间'),
|
addDigestTime: z.number().describe('添加精华消息时间'),
|
||||||
startTime: z.number().describe('开始时间'),
|
startTime: z.number().describe('开始时间'),
|
||||||
latestMsgSeq: z.number().describe('最新消息序号'),
|
latestMsgSeq: z.number().describe('最新消息序号'),
|
||||||
opType: z.number().describe('操作类型')
|
opType: z.number().describe('操作类型'),
|
||||||
})
|
})
|
||||||
.describe('消息内容'),
|
.describe('消息内容'),
|
||||||
errorCode: z.number().describe('错误码')
|
errorCode: z.number().describe('错误码'),
|
||||||
})
|
})
|
||||||
.describe('结果')
|
.describe('结果'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_group_card': {
|
'/set_group_card': {
|
||||||
description: '设置群成员名片',
|
description: '设置群成员名片',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
||||||
card: z.string().describe('名片')
|
card: z.string().describe('名片'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/delete_essence_msg': {
|
'/delete_essence_msg': {
|
||||||
description: '删除群精华消息',
|
description: '删除群精华消息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
message_id: z.union([z.string(), z.number()]).describe('消息 ID')
|
message_id: z.union([z.string(), z.number()]).describe('消息 ID'),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
@ -186,42 +186,42 @@ const oneBotHttpApiGroup = {
|
|||||||
addDigestTime: z.number().describe('添加精华消息时间'),
|
addDigestTime: z.number().describe('添加精华消息时间'),
|
||||||
startTime: z.number().describe('开始时间'),
|
startTime: z.number().describe('开始时间'),
|
||||||
latestMsgSeq: z.number().describe('最新消息序号'),
|
latestMsgSeq: z.number().describe('最新消息序号'),
|
||||||
opType: z.number().describe('操作类型')
|
opType: z.number().describe('操作类型'),
|
||||||
}),
|
}),
|
||||||
errorCode: z.number().describe('错误码')
|
errorCode: z.number().describe('错误码'),
|
||||||
})
|
})
|
||||||
.describe('结果')
|
.describe('结果'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_group_name': {
|
'/set_group_name': {
|
||||||
description: '设置群名称',
|
description: '设置群名称',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
group_name: z.string().describe('群名称')
|
group_name: z.string().describe('群名称'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/set_group_leave': {
|
'/set_group_leave': {
|
||||||
description: '退出群聊',
|
description: '退出群聊',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/_send_group_notice': {
|
'/_send_group_notice': {
|
||||||
description: '发送群公告',
|
description: '发送群公告',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
content: z.string().describe('公告内容'),
|
content: z.string().describe('公告内容'),
|
||||||
image: z.string().optional().describe('图片地址')
|
image: z.string().optional().describe('图片地址'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/_get_group_notice': {
|
'/_get_group_notice': {
|
||||||
description: '获取群公告',
|
description: '获取群公告',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
@ -237,24 +237,24 @@ const oneBotHttpApiGroup = {
|
|||||||
.object({
|
.object({
|
||||||
id: z.string().describe('图片 ID'),
|
id: z.string().describe('图片 ID'),
|
||||||
height: z.string().describe('高度'),
|
height: z.string().describe('高度'),
|
||||||
width: z.string().describe('宽度')
|
width: z.string().describe('宽度'),
|
||||||
})
|
})
|
||||||
.describe('图片信息')
|
.describe('图片信息')
|
||||||
)
|
)
|
||||||
.describe('图片内容列表')
|
.describe('图片内容列表'),
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_group_special_title': {
|
'/set_group_special_title': {
|
||||||
description: '设置群成员专属头衔',
|
description: '设置群成员专属头衔',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
||||||
special_title: z.string().describe('专属头衔内容')
|
special_title: z.string().describe('专属头衔内容'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/upload_group_file': {
|
'/upload_group_file': {
|
||||||
description: '上传群文件',
|
description: '上传群文件',
|
||||||
@ -262,34 +262,34 @@ const oneBotHttpApiGroup = {
|
|||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
file: z.string().describe('文件路径'),
|
file: z.string().describe('文件路径'),
|
||||||
name: z.string().describe('文件名'),
|
name: z.string().describe('文件名'),
|
||||||
folder_id: z.string().describe('文件夹 ID')
|
folder_id: z.string().describe('文件夹 ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: commonResponseDataSchema
|
data: commonResponseDataSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_group_add_request': {
|
'/set_group_add_request': {
|
||||||
description: '处理加群请求',
|
description: '处理加群请求',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
flag: z.string().describe('请求ID'),
|
flag: z.string().describe('请求ID'),
|
||||||
approve: z.boolean().describe('是否同意'),
|
approve: z.boolean().describe('是否同意'),
|
||||||
reason: z.string().optional().describe('拒绝理由')
|
reason: z.string().optional().describe('拒绝理由'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/get_group_info': {
|
'/get_group_info': {
|
||||||
description: '获取群信息',
|
description: '获取群信息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({})
|
data: z.object({}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_info_ex': {
|
'/get_group_info_ex': {
|
||||||
description: '获取群信息扩展',
|
description: '获取群信息扩展',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z
|
data: z
|
||||||
@ -315,7 +315,7 @@ const oneBotHttpApiGroup = {
|
|||||||
.object({
|
.object({
|
||||||
memberUin: z.string().describe('群主QQ号'),
|
memberUin: z.string().describe('群主QQ号'),
|
||||||
memberUid: z.string().describe('群主ID'),
|
memberUid: z.string().describe('群主ID'),
|
||||||
memberQid: z.string().describe('群主QID')
|
memberQid: z.string().describe('群主QID'),
|
||||||
})
|
})
|
||||||
.describe('群主信息'),
|
.describe('群主信息'),
|
||||||
essentialMsgPrivilege: z.number().describe('精华消息权限'),
|
essentialMsgPrivilege: z.number().describe('精华消息权限'),
|
||||||
@ -327,7 +327,7 @@ const oneBotHttpApiGroup = {
|
|||||||
groupFlagPro1: z.string()?.describe('群标识1'),
|
groupFlagPro1: z.string()?.describe('群标识1'),
|
||||||
groupBindGuildIds: z
|
groupBindGuildIds: z
|
||||||
.object({
|
.object({
|
||||||
guildIds: z.array(z.string())
|
guildIds: z.array(z.string()),
|
||||||
})
|
})
|
||||||
.describe('绑定频道ID列表?'),
|
.describe('绑定频道ID列表?'),
|
||||||
viewedMsgDisappearTime: z.string().describe('消息消失时间'),
|
viewedMsgDisappearTime: z.string().describe('消息消失时间'),
|
||||||
@ -337,13 +337,13 @@ const oneBotHttpApiGroup = {
|
|||||||
dayNums: z.array(z.number()).describe('天数列表'),
|
dayNums: z.array(z.number()).describe('天数列表'),
|
||||||
version: z.number().describe('版本号'),
|
version: z.number().describe('版本号'),
|
||||||
updateTime: z.string().describe('更新时间'),
|
updateTime: z.string().describe('更新时间'),
|
||||||
isDisplayDayNum: z.boolean().describe('是否显示天数')
|
isDisplayDayNum: z.boolean().describe('是否显示天数'),
|
||||||
}),
|
}),
|
||||||
groupBindGuildSwitch: z.number().describe('绑定频道开关'),
|
groupBindGuildSwitch: z.number().describe('绑定频道开关'),
|
||||||
groupAioBindGuildId: z.string().describe('AIO绑定频道ID'),
|
groupAioBindGuildId: z.string().describe('AIO绑定频道ID'),
|
||||||
groupExcludeGuildIds: z
|
groupExcludeGuildIds: z
|
||||||
.object({
|
.object({
|
||||||
guildIds: z.array(z.string()).describe('排除频道ID')
|
guildIds: z.array(z.string()).describe('排除频道ID'),
|
||||||
})
|
})
|
||||||
.describe('排除频道ID列表?'),
|
.describe('排除频道ID列表?'),
|
||||||
fullGroupExpansionSwitch: z.number().describe('全员群扩容开关'),
|
fullGroupExpansionSwitch: z.number().describe('全员群扩容开关'),
|
||||||
@ -354,18 +354,18 @@ const oneBotHttpApiGroup = {
|
|||||||
inviteRobotMemberExamine: z
|
inviteRobotMemberExamine: z
|
||||||
.number()
|
.number()
|
||||||
.describe('邀请机器人成员审核'),
|
.describe('邀请机器人成员审核'),
|
||||||
groupSquareSwitch: z.number().describe('群广场开关')
|
groupSquareSwitch: z.number().describe('群广场开关'),
|
||||||
})
|
})
|
||||||
.describe('扩展信息')
|
.describe('扩展信息'),
|
||||||
})
|
})
|
||||||
.describe('结果')
|
.describe('结果'),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/create_group_file_folder': {
|
'/create_group_file_folder': {
|
||||||
description: '创建群文件夹',
|
description: '创建群文件夹',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
folder_name: z.string().describe('文件夹名称')
|
folder_name: z.string().describe('文件夹名称'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z
|
data: z
|
||||||
@ -374,7 +374,7 @@ const oneBotHttpApiGroup = {
|
|||||||
.object({
|
.object({
|
||||||
retCode: z.number().describe('结果码'),
|
retCode: z.number().describe('结果码'),
|
||||||
retMsg: z.string().describe('结果信息'),
|
retMsg: z.string().describe('结果信息'),
|
||||||
clientWording: z.string().describe('客户端提示')
|
clientWording: z.string().describe('客户端提示'),
|
||||||
})
|
})
|
||||||
.describe('结果'),
|
.describe('结果'),
|
||||||
groupItem: z
|
groupItem: z
|
||||||
@ -393,20 +393,20 @@ const oneBotHttpApiGroup = {
|
|||||||
totalFileCount: z.string().describe('文件总数'),
|
totalFileCount: z.string().describe('文件总数'),
|
||||||
modifyUin: z.string().describe('修改人 QQ 号'),
|
modifyUin: z.string().describe('修改人 QQ 号'),
|
||||||
modifyName: z.string().describe('修改人昵称'),
|
modifyName: z.string().describe('修改人昵称'),
|
||||||
usedSpace: z.string().describe('已使用空间')
|
usedSpace: z.string().describe('已使用空间'),
|
||||||
})
|
})
|
||||||
.describe('文件夹信息')
|
.describe('文件夹信息'),
|
||||||
})
|
})
|
||||||
.describe('群文件夹信息')
|
.describe('群文件夹信息'),
|
||||||
})
|
})
|
||||||
.describe('数据')
|
.describe('数据'),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/delete_group_file': {
|
'/delete_group_file': {
|
||||||
description: '删除群文件',
|
description: '删除群文件',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
file_id: z.string().describe('文件 ID')
|
file_id: z.string().describe('文件 ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z
|
data: z
|
||||||
@ -419,51 +419,51 @@ const oneBotHttpApiGroup = {
|
|||||||
.object({
|
.object({
|
||||||
retCode: z.number().describe('结果码'),
|
retCode: z.number().describe('结果码'),
|
||||||
retMsg: z.string().describe('结果信息'),
|
retMsg: z.string().describe('结果信息'),
|
||||||
clientWording: z.string().describe('客户端提示')
|
clientWording: z.string().describe('客户端提示'),
|
||||||
})
|
})
|
||||||
.describe('结果'),
|
.describe('结果'),
|
||||||
successFileIdList: z
|
successFileIdList: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.describe('成功文件 ID 列表'),
|
.describe('成功文件 ID 列表'),
|
||||||
failFileIdList: z.array(z.string()).describe('失败文件 ID 列表')
|
failFileIdList: z.array(z.string()).describe('失败文件 ID 列表'),
|
||||||
})
|
})
|
||||||
.describe('删除群文件结果')
|
.describe('删除群文件结果'),
|
||||||
})
|
})
|
||||||
.describe('结果')
|
.describe('结果'),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/delete_group_folder': {
|
'/delete_group_folder': {
|
||||||
description: '删除群文件夹',
|
description: '删除群文件夹',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
folder_id: z.string().describe('文件夹 ID')
|
folder_id: z.string().describe('文件夹 ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
retCode: z.number().describe('结果码'),
|
retCode: z.number().describe('结果码'),
|
||||||
retMsg: z.string().describe('结果信息'),
|
retMsg: z.string().describe('结果信息'),
|
||||||
clientWording: z.string().describe('客户端提示')
|
clientWording: z.string().describe('客户端提示'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_file_system_info': {
|
'/get_group_file_system_info': {
|
||||||
description: '获取群文件系统信息',
|
description: '获取群文件系统信息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
file_count: z.number().describe('文件总数'),
|
file_count: z.number().describe('文件总数'),
|
||||||
limit_count: z.number().describe('文件总数限制'),
|
limit_count: z.number().describe('文件总数限制'),
|
||||||
used_space: z.number().describe('已使用空间'),
|
used_space: z.number().describe('已使用空间'),
|
||||||
total_space: z.number().describe('总空间')
|
total_space: z.number().describe('总空间'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_root_files': {
|
'/get_group_root_files': {
|
||||||
description: '获取群根目录文件列表',
|
description: '获取群根目录文件列表',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
@ -482,7 +482,7 @@ const oneBotHttpApiGroup = {
|
|||||||
modify_time: z.number().describe('修改时间'),
|
modify_time: z.number().describe('修改时间'),
|
||||||
download_times: z.number().describe('下载次数'),
|
download_times: z.number().describe('下载次数'),
|
||||||
uploader: z.number().describe('上传人 QQ 号'),
|
uploader: z.number().describe('上传人 QQ 号'),
|
||||||
uploader_name: z.string().describe('上传人昵称')
|
uploader_name: z.string().describe('上传人昵称'),
|
||||||
})
|
})
|
||||||
.describe('文件信息')
|
.describe('文件信息')
|
||||||
)
|
)
|
||||||
@ -498,21 +498,21 @@ const oneBotHttpApiGroup = {
|
|||||||
create_time: z.string().describe('创建时间'),
|
create_time: z.string().describe('创建时间'),
|
||||||
creator: z.string().describe('创建人 QQ 号'),
|
creator: z.string().describe('创建人 QQ 号'),
|
||||||
creator_name: z.string().describe('创建人昵称'),
|
creator_name: z.string().describe('创建人昵称'),
|
||||||
total_file_count: z.string().describe('文件总数')
|
total_file_count: z.string().describe('文件总数'),
|
||||||
})
|
})
|
||||||
.describe('文件夹信息')
|
.describe('文件夹信息')
|
||||||
)
|
)
|
||||||
.describe('文件夹列表')
|
.describe('文件夹列表'),
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_files_by_folder': {
|
'/get_group_files_by_folder': {
|
||||||
description: '获取群子目录文件列表',
|
description: '获取群子目录文件列表',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
folder_id: z.string().describe('文件夹 ID'),
|
folder_id: z.string().describe('文件夹 ID'),
|
||||||
file_count: z.number().describe('文件数量')
|
file_count: z.number().describe('文件数量'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@ -530,7 +530,7 @@ const oneBotHttpApiGroup = {
|
|||||||
modify_time: z.number().describe('修改时间'),
|
modify_time: z.number().describe('修改时间'),
|
||||||
download_times: z.number().describe('下载次数'),
|
download_times: z.number().describe('下载次数'),
|
||||||
uploader: z.number().describe('上传人 QQ 号'),
|
uploader: z.number().describe('上传人 QQ 号'),
|
||||||
uploader_name: z.string().describe('上传人昵称')
|
uploader_name: z.string().describe('上传人昵称'),
|
||||||
})
|
})
|
||||||
.describe('文件信息')
|
.describe('文件信息')
|
||||||
)
|
)
|
||||||
@ -546,60 +546,60 @@ const oneBotHttpApiGroup = {
|
|||||||
create_time: z.string().describe('创建时间'),
|
create_time: z.string().describe('创建时间'),
|
||||||
creator: z.string().describe('创建人 QQ 号'),
|
creator: z.string().describe('创建人 QQ 号'),
|
||||||
creator_name: z.string().describe('创建人昵称'),
|
creator_name: z.string().describe('创建人昵称'),
|
||||||
total_file_count: z.string().describe('文件总数')
|
total_file_count: z.string().describe('文件总数'),
|
||||||
})
|
})
|
||||||
.describe('文件夹信息')
|
.describe('文件夹信息')
|
||||||
)
|
)
|
||||||
.describe('文件夹列表')
|
.describe('文件夹列表'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_file_url': {
|
'/get_group_file_url': {
|
||||||
description: '获取群文件下载链接',
|
description: '获取群文件下载链接',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
file_id: z.string().describe('文件 ID')
|
file_id: z.string().describe('文件 ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
url: z.string().describe('下载链接')
|
url: z.string().describe('下载链接'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_list': {
|
'/get_group_list': {
|
||||||
description: '获取群列表',
|
description: '获取群列表',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
next_token: z.string().optional().describe('下一页标识')
|
next_token: z.string().optional().describe('下一页标识'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.array(z.object({}))
|
data: z.array(z.object({})),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_member_info': {
|
'/get_group_member_info': {
|
||||||
description: '获取群成员信息',
|
description: '获取群成员信息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
user_id: z.union([z.string(), z.number()]).describe('QQ 号'),
|
||||||
no_cache: z.boolean().describe('是否不使用缓存')
|
no_cache: z.boolean().describe('是否不使用缓存'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({})
|
data: z.object({}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_member_list': {
|
'/get_group_member_list': {
|
||||||
description: '获取群成员列表',
|
description: '获取群成员列表',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
no_cache: z.boolean().describe('是否不使用缓存')
|
no_cache: z.boolean().describe('是否不使用缓存'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.array(z.object({}))
|
data: z.array(z.object({})),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_honor_info': {
|
'/get_group_honor_info': {
|
||||||
description: '获取群荣誉',
|
description: '获取群荣誉',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z
|
data: z
|
||||||
@ -611,7 +611,7 @@ const oneBotHttpApiGroup = {
|
|||||||
avatar: z.string().describe('头像 URL'),
|
avatar: z.string().describe('头像 URL'),
|
||||||
nickname: z.string().describe('昵称'),
|
nickname: z.string().describe('昵称'),
|
||||||
day_count: z.number().describe('天数'),
|
day_count: z.number().describe('天数'),
|
||||||
description: z.string().describe('描述')
|
description: z.string().describe('描述'),
|
||||||
})
|
})
|
||||||
.describe('当前龙王'),
|
.describe('当前龙王'),
|
||||||
talkative_list: z
|
talkative_list: z
|
||||||
@ -621,7 +621,7 @@ const oneBotHttpApiGroup = {
|
|||||||
avatar: z.string().describe('头像 URL'),
|
avatar: z.string().describe('头像 URL'),
|
||||||
nickname: z.string().describe('昵称'),
|
nickname: z.string().describe('昵称'),
|
||||||
day_count: z.number().describe('天数'),
|
day_count: z.number().describe('天数'),
|
||||||
description: z.string().describe('描述')
|
description: z.string().describe('描述'),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.describe('龙王榜'),
|
.describe('龙王榜'),
|
||||||
@ -631,34 +631,34 @@ const oneBotHttpApiGroup = {
|
|||||||
user_id: z.number().describe('QQ 号'),
|
user_id: z.number().describe('QQ 号'),
|
||||||
avatar: z.string().describe('头像 URL'),
|
avatar: z.string().describe('头像 URL'),
|
||||||
nickname: z.string().describe('昵称'),
|
nickname: z.string().describe('昵称'),
|
||||||
description: z.string().describe('描述')
|
description: z.string().describe('描述'),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.describe('?'),
|
.describe('?'),
|
||||||
legend_list: z.array(z.string()).describe('?'),
|
legend_list: z.array(z.string()).describe('?'),
|
||||||
emotion_list: z.array(z.string()).describe('?'),
|
emotion_list: z.array(z.string()).describe('?'),
|
||||||
strong_newbie_list: z.array(z.string()).describe('?')
|
strong_newbie_list: z.array(z.string()).describe('?'),
|
||||||
})
|
})
|
||||||
.describe('群荣誉信息')
|
.describe('群荣誉信息'),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_at_all_remain': {
|
'/get_group_at_all_remain': {
|
||||||
description: '获取群 @全体成员 剩余次数',
|
description: '获取群 @全体成员 剩余次数',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
can_at_all: z.boolean().describe('是否可以 @全体成员'),
|
can_at_all: z.boolean().describe('是否可以 @全体成员'),
|
||||||
remain_at_all_count_for_group: z.number().describe('剩余次数(group?)'),
|
remain_at_all_count_for_group: z.number().describe('剩余次数(group?)'),
|
||||||
remain_at_all_count_for_uin: z.number().describe('剩余次数(qq?)')
|
remain_at_all_count_for_uin: z.number().describe('剩余次数(qq?)'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_ignored_notifies': {
|
'/get_group_ignored_notifies': {
|
||||||
description: '获取群过滤系统消息',
|
description: '获取群过滤系统消息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@ -671,32 +671,32 @@ const oneBotHttpApiGroup = {
|
|||||||
group_id: z.string().describe('群号'),
|
group_id: z.string().describe('群号'),
|
||||||
group_name: z.string().describe('群名称'),
|
group_name: z.string().describe('群名称'),
|
||||||
checked: z.boolean().describe('是否已处理'),
|
checked: z.boolean().describe('是否已处理'),
|
||||||
actor: z.string().describe('处理人 QQ 号')
|
actor: z.string().describe('处理人 QQ 号'),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.describe('入群请求列表')
|
.describe('入群请求列表'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_group_sign': {
|
'/set_group_sign': {
|
||||||
description: '设置群打卡',
|
description: '设置群打卡',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/send_group_sign': {
|
'/send_group_sign': {
|
||||||
description: '发送群打卡',
|
description: '发送群打卡',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/get_ai_characters': {
|
'/get_ai_characters': {
|
||||||
description: '获取AI语音人物',
|
description: '获取AI语音人物',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
chat_type: z.union([z.string(), z.number()]).describe('聊天类型')
|
chat_type: z.union([z.string(), z.number()]).describe('聊天类型'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
@ -707,38 +707,38 @@ const oneBotHttpApiGroup = {
|
|||||||
.object({
|
.object({
|
||||||
character_id: z.string().describe('人物 ID'),
|
character_id: z.string().describe('人物 ID'),
|
||||||
character_name: z.string().describe('人物名称'),
|
character_name: z.string().describe('人物名称'),
|
||||||
preview_url: z.string().describe('预览音频地址')
|
preview_url: z.string().describe('预览音频地址'),
|
||||||
})
|
})
|
||||||
.describe('人物信息')
|
.describe('人物信息')
|
||||||
)
|
),
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/send_group_ai_record': {
|
'/send_group_ai_record': {
|
||||||
description: '发送群AI语音',
|
description: '发送群AI语音',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
character: z.string().describe('人物ID'),
|
character: z.string().describe('人物ID'),
|
||||||
text: z.string().describe('文本内容')
|
text: z.string().describe('文本内容'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
message_id: z.string().describe('消息 ID')
|
message_id: z.string().describe('消息 ID'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_ai_record': {
|
'/get_ai_record': {
|
||||||
description: '获取AI语音',
|
description: '获取AI语音',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.string().describe('群号'),
|
group_id: z.string().describe('群号'),
|
||||||
character: z.string().describe('人物ID'),
|
character: z.string().describe('人物ID'),
|
||||||
text: z.string().describe('文本内容')
|
text: z.string().describe('文本内容'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.string()
|
data: z.string(),
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export default oneBotHttpApiGroup
|
export default oneBotHttpApiGroup;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { ZodSchema } from 'zod'
|
import { ZodSchema } from 'zod';
|
||||||
|
|
||||||
import oneBotHttpApiGroup from './group'
|
import oneBotHttpApiGroup from './group';
|
||||||
import oneBotHttpApiMessage from './message'
|
import oneBotHttpApiMessage from './message';
|
||||||
import oneBotHttpApiSystem from './system'
|
import oneBotHttpApiSystem from './system';
|
||||||
import oneBotHttpApiUser from './user'
|
import oneBotHttpApiUser from './user';
|
||||||
|
|
||||||
type AllKey =
|
type AllKey =
|
||||||
| keyof typeof oneBotHttpApiUser
|
| keyof typeof oneBotHttpApiUser
|
||||||
| keyof typeof oneBotHttpApiMessage
|
| keyof typeof oneBotHttpApiMessage
|
||||||
| keyof typeof oneBotHttpApiGroup
|
| keyof typeof oneBotHttpApiGroup
|
||||||
| keyof typeof oneBotHttpApiSystem
|
| keyof typeof oneBotHttpApiSystem;
|
||||||
|
|
||||||
export type OneBotHttpApi = Record<
|
export type OneBotHttpApi = Record<
|
||||||
AllKey,
|
AllKey,
|
||||||
@ -18,17 +18,17 @@ export type OneBotHttpApi = Record<
|
|||||||
request: ZodSchema
|
request: ZodSchema
|
||||||
response: ZodSchema
|
response: ZodSchema
|
||||||
}
|
}
|
||||||
>
|
>;
|
||||||
|
|
||||||
const oneBotHttpApi: OneBotHttpApi = {
|
const oneBotHttpApi: OneBotHttpApi = {
|
||||||
...oneBotHttpApiUser,
|
...oneBotHttpApiUser,
|
||||||
...oneBotHttpApiMessage,
|
...oneBotHttpApiMessage,
|
||||||
...oneBotHttpApiGroup,
|
...oneBotHttpApiGroup,
|
||||||
...oneBotHttpApiSystem
|
...oneBotHttpApiSystem,
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type OneBotHttpApiPath = keyof OneBotHttpApi
|
export type OneBotHttpApiPath = keyof OneBotHttpApi;
|
||||||
|
|
||||||
export type OneBotHttpApiContent = OneBotHttpApi[OneBotHttpApiPath]
|
export type OneBotHttpApiContent = OneBotHttpApi[OneBotHttpApiPath];
|
||||||
|
|
||||||
export default oneBotHttpApi
|
export default oneBotHttpApi;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod';
|
||||||
import type { ZodSchema } from 'zod'
|
import type { ZodSchema } from 'zod';
|
||||||
|
|
||||||
import { baseResponseSchema, commonResponseDataSchema } from '../response'
|
import { baseResponseSchema, commonResponseDataSchema } from '../response';
|
||||||
import messageNodeSchema, { nodeMessage } from './node'
|
import messageNodeSchema, { nodeMessage } from './node';
|
||||||
|
|
||||||
const oneBotHttpApiMessageGroup: Record<
|
const oneBotHttpApiMessageGroup: Record<
|
||||||
string,
|
string,
|
||||||
@ -20,26 +20,26 @@ const oneBotHttpApiMessageGroup: Record<
|
|||||||
.union([z.string(), z.number()])
|
.union([z.string(), z.number()])
|
||||||
.describe('群号')
|
.describe('群号')
|
||||||
.describe('群号'),
|
.describe('群号'),
|
||||||
message: z.array(messageNodeSchema).describe('消息内容')
|
message: z.array(messageNodeSchema).describe('消息内容'),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
const hasReply = data.message.some((item) => item.type === 'reply')
|
const hasReply = data.message.some((item) => item.type === 'reply');
|
||||||
|
|
||||||
if (hasReply) {
|
if (hasReply) {
|
||||||
return data.message[0].type === 'reply'
|
return data.message[0].type === 'reply';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
'如果 message 包含 reply 类型的消息,那么只能包含一个,而且排在最前面'
|
'如果 message 包含 reply 类型的消息,那么只能包含一个,而且排在最前面',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: commonResponseDataSchema
|
data: commonResponseDataSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/send_group_forward_msg': {
|
'/send_group_forward_msg': {
|
||||||
description: '发送群合并转发消息',
|
description: '发送群合并转发消息',
|
||||||
@ -49,36 +49,36 @@ const oneBotHttpApiMessageGroup: Record<
|
|||||||
news: z
|
news: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
text: z.string()
|
text: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.describe('?'),
|
.describe('?'),
|
||||||
prompt: z.string().describe('外显'),
|
prompt: z.string().describe('外显'),
|
||||||
summary: z.string().describe('底下文本'),
|
summary: z.string().describe('底下文本'),
|
||||||
source: z.string().describe('内容')
|
source: z.string().describe('内容'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: commonResponseDataSchema
|
data: commonResponseDataSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/forward_group_single_msg': {
|
'/forward_group_single_msg': {
|
||||||
description: '消息转发到群',
|
description: '消息转发到群',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
message_id: z.union([z.string(), z.number()]).describe('消息 ID')
|
message_id: z.union([z.string(), z.number()]).describe('消息 ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: commonResponseDataSchema
|
data: commonResponseDataSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/group_poke': {
|
'/group_poke': {
|
||||||
description: '发送戳一戳',
|
description: '发送戳一戳',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
user_id: z.union([z.string(), z.number()]).describe('对方QQ号')
|
user_id: z.union([z.string(), z.number()]).describe('对方QQ号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default oneBotHttpApiMessageGroup
|
export default oneBotHttpApiMessageGroup;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { baseResponseSchema, commonResponseDataSchema } from '../response'
|
import { baseResponseSchema, commonResponseDataSchema } from '../response';
|
||||||
import oneBotHttpApiMessageGroup from './group'
|
import oneBotHttpApiMessageGroup from './group';
|
||||||
import messageNodeSchema from './node'
|
import messageNodeSchema from './node';
|
||||||
import oneBotHttpApiMessagePrivate from './private'
|
import oneBotHttpApiMessagePrivate from './private';
|
||||||
|
|
||||||
const fileSchema = z
|
const fileSchema = z
|
||||||
.object({
|
.object({
|
||||||
@ -11,9 +11,9 @@ const fileSchema = z
|
|||||||
url: z.string().describe('路径或链接'),
|
url: z.string().describe('路径或链接'),
|
||||||
file_size: z.string().describe('文件大小'),
|
file_size: z.string().describe('文件大小'),
|
||||||
file_name: z.string().describe('文件名'),
|
file_name: z.string().describe('文件名'),
|
||||||
base64: z.string().describe('文件base64编码')
|
base64: z.string().describe('文件base64编码'),
|
||||||
})
|
})
|
||||||
.describe('文件')
|
.describe('文件');
|
||||||
const messageSchema = z
|
const messageSchema = z
|
||||||
.object({
|
.object({
|
||||||
self_id: z.number().describe('自己QQ号'),
|
self_id: z.number().describe('自己QQ号'),
|
||||||
@ -30,7 +30,7 @@ const messageSchema = z
|
|||||||
sex: z.enum(['male', 'female', 'unknown']).describe('性别'),
|
sex: z.enum(['male', 'female', 'unknown']).describe('性别'),
|
||||||
age: z.number().describe('年龄'),
|
age: z.number().describe('年龄'),
|
||||||
card: z.string().describe('名片'),
|
card: z.string().describe('名片'),
|
||||||
role: z.enum(['owner', 'admin', 'member']).describe('角色')
|
role: z.enum(['owner', 'admin', 'member']).describe('角色'),
|
||||||
})
|
})
|
||||||
.describe('发送人信息'),
|
.describe('发送人信息'),
|
||||||
raw_message: z.string().describe('原始消息'),
|
raw_message: z.string().describe('原始消息'),
|
||||||
@ -40,9 +40,9 @@ const messageSchema = z
|
|||||||
message_format: z.string().describe('消息格式'),
|
message_format: z.string().describe('消息格式'),
|
||||||
post_type: z.string().describe('?'),
|
post_type: z.string().describe('?'),
|
||||||
message_sent_type: z.string().describe('消息发送类型'),
|
message_sent_type: z.string().describe('消息发送类型'),
|
||||||
group_id: z.number().describe('群号')
|
group_id: z.number().describe('群号'),
|
||||||
})
|
})
|
||||||
.describe('消息')
|
.describe('消息');
|
||||||
|
|
||||||
const oneBotHttpApiMessage = {
|
const oneBotHttpApiMessage = {
|
||||||
...oneBotHttpApiMessagePrivate,
|
...oneBotHttpApiMessagePrivate,
|
||||||
@ -58,61 +58,61 @@ const oneBotHttpApiMessage = {
|
|||||||
user_id: z
|
user_id: z
|
||||||
.union([z.string(), z.number()])
|
.union([z.string(), z.number()])
|
||||||
.optional()
|
.optional()
|
||||||
.describe('用户QQ号,与 group_id 二选一')
|
.describe('用户QQ号,与 group_id 二选一'),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) =>
|
(data) =>
|
||||||
(data.group_id && !data.user_id) || (!data.group_id && data.user_id),
|
(data.group_id && !data.user_id) || (!data.group_id && data.user_id),
|
||||||
{
|
{
|
||||||
message: 'group_id 和 user_id 必须二选一,且不能同时存在或同时为空',
|
message: 'group_id 和 user_id 必须二选一,且不能同时存在或同时为空',
|
||||||
path: ['group_id', 'user_id']
|
path: ['group_id', 'user_id'],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/mark_group_msg_as_read': {
|
'/mark_group_msg_as_read': {
|
||||||
description: '标记群消息已读',
|
description: '标记群消息已读',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
group_id: z.union([z.string(), z.number()]).describe('群号')
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/mark_private_msg_as_read': {
|
'/mark_private_msg_as_read': {
|
||||||
description: '标记私聊消息已读',
|
description: '标记私聊消息已读',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
user_id: z.union([z.string(), z.number()]).describe('用户QQ号')
|
user_id: z.union([z.string(), z.number()]).describe('用户QQ号'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/_mark_all_as_read': {
|
'/_mark_all_as_read': {
|
||||||
description: '标记所有消息已读',
|
description: '标记所有消息已读',
|
||||||
request: z.object({}),
|
request: z.object({}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/delete_msg': {
|
'/delete_msg': {
|
||||||
description: '撤回消息',
|
description: '撤回消息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
message_id: z.union([z.string(), z.number()]).describe('消息ID')
|
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema
|
response: baseResponseSchema,
|
||||||
},
|
},
|
||||||
'/get_msg': {
|
'/get_msg': {
|
||||||
description: '获取消息',
|
description: '获取消息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
message_id: z.union([z.string(), z.number()]).describe('消息ID')
|
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({})
|
data: z.object({}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_image': {
|
'/get_image': {
|
||||||
description: '获取图片',
|
description: '获取图片',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
file_id: z.string().describe('文件ID')
|
file_id: z.string().describe('文件ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: fileSchema
|
data: fileSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_record': {
|
'/get_record': {
|
||||||
description: '获取语音',
|
description: '获取语音',
|
||||||
@ -120,20 +120,20 @@ const oneBotHttpApiMessage = {
|
|||||||
file_id: z.string().describe('文件ID'),
|
file_id: z.string().describe('文件ID'),
|
||||||
out_format: z
|
out_format: z
|
||||||
.enum(['mp3', 'amr', 'wma', 'm4a', 'spx', 'ogg', 'wav', 'flac'])
|
.enum(['mp3', 'amr', 'wma', 'm4a', 'spx', 'ogg', 'wav', 'flac'])
|
||||||
.describe('输出格式')
|
.describe('输出格式'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: fileSchema
|
data: fileSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_file': {
|
'/get_file': {
|
||||||
description: '获取文件',
|
description: '获取文件',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
file_id: z.string().describe('文件ID')
|
file_id: z.string().describe('文件ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: fileSchema
|
data: fileSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_group_msg_history': {
|
'/get_group_msg_history': {
|
||||||
description: '获取群消息历史',
|
description: '获取群消息历史',
|
||||||
@ -141,24 +141,24 @@ const oneBotHttpApiMessage = {
|
|||||||
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
group_id: z.union([z.string(), z.number()]).describe('群号'),
|
||||||
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
|
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
|
||||||
count: z.number().int().positive().describe('获取数量'),
|
count: z.number().int().positive().describe('获取数量'),
|
||||||
reverseOrder: z.boolean().describe('是否倒序')
|
reverseOrder: z.boolean().describe('是否倒序'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
messages: z.array(messageSchema).describe('消息列表')
|
messages: z.array(messageSchema).describe('消息列表'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/set_msg_emoji_like': {
|
'/set_msg_emoji_like': {
|
||||||
description: '贴表情',
|
description: '贴表情',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
|
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
|
||||||
emoji_id: z.number().describe('表情ID'),
|
emoji_id: z.number().describe('表情ID'),
|
||||||
set: z.boolean().describe('?')
|
set: z.boolean().describe('?'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: commonResponseDataSchema
|
data: commonResponseDataSchema,
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_friend_msg_history': {
|
'/get_friend_msg_history': {
|
||||||
description: '获取好友消息历史',
|
description: '获取好友消息历史',
|
||||||
@ -166,18 +166,18 @@ const oneBotHttpApiMessage = {
|
|||||||
user_id: z.union([z.string(), z.number()]).describe('用户QQ号'),
|
user_id: z.union([z.string(), z.number()]).describe('用户QQ号'),
|
||||||
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
|
message_seq: z.union([z.string(), z.number()]).describe('消息序号'),
|
||||||
count: z.number().int().positive().describe('获取数量'),
|
count: z.number().int().positive().describe('获取数量'),
|
||||||
reverseOrder: z.boolean().describe('是否倒序')
|
reverseOrder: z.boolean().describe('是否倒序'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
messages: z.array(messageSchema)
|
messages: z.array(messageSchema),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_recent_contact': {
|
'/get_recent_contact': {
|
||||||
description: '最近消息列表',
|
description: '最近消息列表',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
count: z.number().int().positive().describe('获取数量')
|
count: z.number().int().positive().describe('获取数量'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
@ -190,10 +190,10 @@ const oneBotHttpApiMessage = {
|
|||||||
msgId: z.string().describe('消息ID'),
|
msgId: z.string().describe('消息ID'),
|
||||||
sendNickName: z.string().describe('发送人昵称'),
|
sendNickName: z.string().describe('发送人昵称'),
|
||||||
sendMemberName: z.string().describe('发送人?昵称'),
|
sendMemberName: z.string().describe('发送人?昵称'),
|
||||||
peerName: z.string().describe('对方昵称')
|
peerName: z.string().describe('对方昵称'),
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/fetch_emoji_like': {
|
'/fetch_emoji_like': {
|
||||||
description: '获取贴表情详情',
|
description: '获取贴表情详情',
|
||||||
@ -206,7 +206,7 @@ const oneBotHttpApiMessage = {
|
|||||||
.union([z.string(), z.number()])
|
.union([z.string(), z.number()])
|
||||||
.optional()
|
.optional()
|
||||||
.describe('用户QQ号'),
|
.describe('用户QQ号'),
|
||||||
count: z.number().int().positive().optional().describe('获取数量')
|
count: z.number().int().positive().optional().describe('获取数量'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@ -218,25 +218,25 @@ const oneBotHttpApiMessage = {
|
|||||||
.object({
|
.object({
|
||||||
tinyId: z.string().describe('表情ID'),
|
tinyId: z.string().describe('表情ID'),
|
||||||
nickName: z.string().describe('昵称?'),
|
nickName: z.string().describe('昵称?'),
|
||||||
headUrl: z.string().describe('头像?')
|
headUrl: z.string().describe('头像?'),
|
||||||
})
|
})
|
||||||
.describe('表情点赞列表')
|
.describe('表情点赞列表')
|
||||||
)
|
)
|
||||||
.describe('表情点赞列表'),
|
.describe('表情点赞列表'),
|
||||||
cookie: z.string().describe('cookie'),
|
cookie: z.string().describe('cookie'),
|
||||||
isLastPage: z.boolean().describe('是否最后一页'),
|
isLastPage: z.boolean().describe('是否最后一页'),
|
||||||
isFirstPage: z.boolean().describe('是否第一页')
|
isFirstPage: z.boolean().describe('是否第一页'),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/get_forward_msg': {
|
'/get_forward_msg': {
|
||||||
description: '获取合并转发消息',
|
description: '获取合并转发消息',
|
||||||
request: z.object({
|
request: z.object({
|
||||||
message_id: z.union([z.string(), z.number()]).describe('消息ID')
|
message_id: z.union([z.string(), z.number()]).describe('消息ID'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: z.object({})
|
data: z.object({}),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
'/send_forward_msg': {
|
'/send_forward_msg': {
|
||||||
description: '发送合并转发消息',
|
description: '发送合并转发消息',
|
||||||
@ -250,20 +250,20 @@ const oneBotHttpApiMessage = {
|
|||||||
news: z
|
news: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
text: z.string()
|
text: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.describe('?'),
|
.describe('?'),
|
||||||
prompt: z.string().describe('外显'),
|
prompt: z.string().describe('外显'),
|
||||||
summary: z.string().describe('底下文字'),
|
summary: z.string().describe('底下文字'),
|
||||||
source: z.string().describe('内容')
|
source: z.string().describe('内容'),
|
||||||
}),
|
}),
|
||||||
response: baseResponseSchema.extend({
|
response: baseResponseSchema.extend({
|
||||||
data: commonResponseDataSchema.extend({
|
data: commonResponseDataSchema.extend({
|
||||||
data: z.object({})
|
data: z.object({}),
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export default oneBotHttpApiMessage
|
export default oneBotHttpApiMessage;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user