feat: 新版webui

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

View File

@@ -0,0 +1,122 @@
import { Avatar } from '@heroui/avatar'
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
import clsx from 'clsx'
import { isOB11GroupMessage } from '@/utils/onebot'
import type {
OB11GroupMessage,
OB11Message,
OB11PrivateMessage
} from '@/types/onebot'
import { renderMessageContent } from '../render_message'
export interface OneBotMessageProps {
data: OB11Message
}
export interface OneBotMessageGroupProps {
data: OB11GroupMessage
}
export interface OneBotMessagePrivateProps {
data: OB11PrivateMessage
}
const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
return (
<div className="h-full flex flex-col overflow-hidden flex-1">
<div className="flex gap-2 items-center flex-shrink-0">
<div className="font-bold">
{isOB11GroupMessage(data) && data.sender.card && (
<span className="mr-1">{data.sender.card}</span>
)}
<span
className={clsx(
isOB11GroupMessage(data) &&
data.sender.card &&
'text-default-400 font-normal'
)}
>
{data.sender.nickname}
</span>
</div>
<div>({data.sender.user_id})</div>
<div className="text-sm">ID: {data.message_id}</div>
</div>
<Popover showArrow triggerScaleOnOpen={false}>
<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="absolute right-2 top-2 opacity-0 group-hover:opacity-100 text-default-300">
</div>
{Array.isArray(data.message)
? renderMessageContent(data.message, true)
: data.raw_message}
</div>
</PopoverTrigger>
<PopoverContent>
<div className="p-2">
{Array.isArray(data.message)
? renderMessageContent(data.message)
: data.raw_message}
</div>
</PopoverContent>
</Popover>
</div>
)
}
const OneBotMessageGroup: React.FC<OneBotMessageGroupProps> = ({ data }) => {
return (
<div className="h-full overflow-hidden flex flex-col w-full">
<div className="flex items-center p-1 flex-shrink-0">
<Avatar
src={`https://p.qlogo.cn/gh/${data.group_id}/${data.group_id}/640/`}
alt="群头像"
size="sm"
className="flex-shrink-0 mr-2"
/>
<div> {data.group_id}</div>
</div>
<div className="flex items-start p-1 rounded-md h-full flex-1 border border-default-100">
<Avatar
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
alt="用户头像"
size="md"
className="flex-shrink-0 mr-2"
/>
<MessageContent data={data} />
</div>
</div>
)
}
const OneBotMessagePrivate: React.FC<OneBotMessagePrivateProps> = ({
data
}) => {
return (
<div className="flex items-start p-2 rounded-md h-full flex-1">
<Avatar
src={`https://q1.qlogo.cn/g?b=qq&nk=${data.sender.user_id}&s=100`}
alt="用户头像"
size="md"
className="flex-shrink-0 mr-2"
/>
<MessageContent data={data} />
</div>
)
}
const OneBotMessage: React.FC<OneBotMessageProps> = ({ data }) => {
if (data.message_type === 'group') {
return <OneBotMessageGroup data={data} />
} else if (data.message_type === 'private') {
return <OneBotMessagePrivate data={data} />
} else {
return <div></div>
}
}
export default OneBotMessage

View File

@@ -0,0 +1,60 @@
import { Chip } from '@heroui/chip'
import { getLifecycleColor, getLifecycleName } from '@/utils/onebot'
import type {
OB11Meta,
OneBot11Heartbeat,
OneBot11Lifecycle
} from '@/types/onebot'
export interface OneBotDisplayMetaProps {
data: OB11Meta
}
export interface OneBotDisplayMetaHeartbeatProps {
data: OneBot11Heartbeat
}
export interface OneBotDisplayMetaLifecycleProps {
data: OneBot11Lifecycle
}
const OneBotDisplayMetaHeartbeat: React.FC<OneBotDisplayMetaHeartbeatProps> = ({
data
}) => {
return (
<div className="flex gap-2">
<Chip></Chip>
<Chip> {data.status.interval}ms</Chip>
</div>
)
}
const OneBotDisplayMetaLifecycle: React.FC<OneBotDisplayMetaLifecycleProps> = ({
data
}) => {
return (
<div className="flex gap-2">
<Chip></Chip>
<Chip color={getLifecycleColor(data.sub_type)}>
{getLifecycleName(data.sub_type)}
</Chip>
</div>
)
}
const OneBotDisplayMeta: React.FC<OneBotDisplayMetaProps> = ({ data }) => {
return (
<div className="h-full flex items-center">
{data.meta_event_type === 'lifecycle' && (
<OneBotDisplayMetaLifecycle data={data} />
)}
{data.meta_event_type === 'heartbeat' && (
<OneBotDisplayMetaHeartbeat data={data} />
)}
</div>
)
}
export default OneBotDisplayMeta

View File

@@ -0,0 +1,292 @@
import { Chip } from '@heroui/chip'
import { getNoticeTypeName } from '@/utils/onebot'
import {
OB11Notice,
OB11NoticeType,
OneBot11FriendAdd,
OneBot11FriendRecall,
OneBot11GroupAdmin,
OneBot11GroupBan,
OneBot11GroupCard,
OneBot11GroupDecrease,
OneBot11GroupEssence,
OneBot11GroupIncrease,
OneBot11GroupMessageReaction,
OneBot11GroupRecall,
OneBot11GroupUpload,
OneBot11Honor,
OneBot11LuckyKing,
OneBot11Poke
} from '@/types/onebot'
export interface OneBotNoticeProps {
data: OB11Notice
}
export interface NoticeProps<T> {
data: T
}
const GroupUploadNotice: React.FC<NoticeProps<OneBot11GroupUpload>> = ({
data
}) => {
const { group_id, user_id, file } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>: {file.name}</div>
<div>: {file.size} </div>
</>
)
}
const GroupAdminNotice: React.FC<NoticeProps<OneBot11GroupAdmin>> = ({
data
}) => {
const { group_id, user_id, sub_type } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>: {sub_type === 'set' ? '设置管理员' : '取消管理员'}</div>
</>
)
}
const GroupDecreaseNotice: React.FC<NoticeProps<OneBot11GroupDecrease>> = ({
data
}) => {
const { group_id, operator_id, user_id, sub_type } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {operator_id}</div>
<div>ID: {user_id}</div>
<div>: {sub_type}</div>
</>
)
}
const GroupIncreaseNotice: React.FC<NoticeProps<OneBot11GroupIncrease>> = ({
data
}) => {
const { group_id, operator_id, user_id, sub_type } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {operator_id}</div>
<div>ID: {user_id}</div>
<div>: {sub_type}</div>
</>
)
}
const GroupBanNotice: React.FC<NoticeProps<OneBot11GroupBan>> = ({ data }) => {
const { group_id, operator_id, user_id, sub_type, duration } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {operator_id}</div>
<div>ID: {user_id}</div>
<div>: {sub_type}</div>
<div>: {duration} </div>
</>
)
}
const FriendAddNotice: React.FC<NoticeProps<OneBot11FriendAdd>> = ({
data
}) => {
const { user_id } = data
return (
<>
<div>ID: {user_id}</div>
</>
)
}
const GroupRecallNotice: React.FC<NoticeProps<OneBot11GroupRecall>> = ({
data
}) => {
const { group_id, user_id, operator_id, message_id } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>ID: {operator_id}</div>
<div>ID: {message_id}</div>
</>
)
}
const FriendRecallNotice: React.FC<NoticeProps<OneBot11FriendRecall>> = ({
data
}) => {
const { user_id, message_id } = data
return (
<>
<div>ID: {user_id}</div>
<div>ID: {message_id}</div>
</>
)
}
const PokeNotice: React.FC<NoticeProps<OneBot11Poke>> = ({ data }) => {
const { group_id, user_id, target_id } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>ID: {target_id}</div>
</>
)
}
const LuckyKingNotice: React.FC<NoticeProps<OneBot11LuckyKing>> = ({
data
}) => {
const { group_id, user_id, target_id } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>ID: {target_id}</div>
</>
)
}
const HonorNotice: React.FC<NoticeProps<OneBot11Honor>> = ({ data }) => {
const { group_id, user_id, honor_type } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>: {honor_type}</div>
</>
)
}
const GroupMessageReactionNotice: React.FC<
NoticeProps<OneBot11GroupMessageReaction>
> = ({ data }) => {
const { group_id, user_id, message_id, likes } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>ID: {message_id}</div>
<div>
:
{likes
.map((like) => `表情ID: ${like.emoji_id}, 数量: ${like.count}`)
.join(', ')}
</div>
</>
)
}
const GroupEssenceNotice: React.FC<NoticeProps<OneBot11GroupEssence>> = ({
data
}) => {
const { group_id, message_id, sender_id, operator_id, sub_type } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {message_id}</div>
<div>ID: {sender_id}</div>
<div>ID: {operator_id}</div>
<div>: {sub_type}</div>
</>
)
}
const GroupCardNotice: React.FC<NoticeProps<OneBot11GroupCard>> = ({
data
}) => {
const { group_id, user_id, card_new, card_old } = data
return (
<>
<div>: {group_id}</div>
<div>ID: {user_id}</div>
<div>: {card_new}</div>
<div>: {card_old}</div>
</>
)
}
const OneBotNotice: React.FC<OneBotNoticeProps> = ({ data }) => {
let NoticeComponent: React.ReactNode
switch (data.notice_type) {
case OB11NoticeType.GroupUpload:
NoticeComponent = <GroupUploadNotice data={data} />
break
case OB11NoticeType.GroupAdmin:
NoticeComponent = <GroupAdminNotice data={data} />
break
case OB11NoticeType.GroupDecrease:
NoticeComponent = <GroupDecreaseNotice data={data} />
break
case OB11NoticeType.GroupIncrease:
NoticeComponent = (
<GroupIncreaseNotice data={data as OneBot11GroupIncrease} />
)
break
case OB11NoticeType.GroupBan:
NoticeComponent = <GroupBanNotice data={data} />
break
case OB11NoticeType.FriendAdd:
NoticeComponent = <FriendAddNotice data={data as OneBot11FriendAdd} />
break
case OB11NoticeType.GroupRecall:
NoticeComponent = <GroupRecallNotice data={data as OneBot11GroupRecall} />
break
case OB11NoticeType.FriendRecall:
NoticeComponent = (
<FriendRecallNotice data={data as OneBot11FriendRecall} />
)
break
case OB11NoticeType.Notify:
switch (data.sub_type) {
case 'poke':
NoticeComponent = <PokeNotice data={data as OneBot11Poke} />
break
case 'lucky_king':
NoticeComponent = <LuckyKingNotice data={data as OneBot11LuckyKing} />
break
case 'honor':
NoticeComponent = <HonorNotice data={data as OneBot11Honor} />
break
}
break
case OB11NoticeType.GroupMsgEmojiLike:
NoticeComponent = (
<GroupMessageReactionNotice
data={data as OneBot11GroupMessageReaction}
/>
)
break
case OB11NoticeType.GroupEssence:
NoticeComponent = (
<GroupEssenceNotice data={data as OneBot11GroupEssence} />
)
break
case OB11NoticeType.GroupCard:
NoticeComponent = <GroupCardNotice data={data as OneBot11GroupCard} />
break
}
return (
<div className="flex gap-2 items-center">
<Chip color="warning" variant="flat">
</Chip>
<Chip>{getNoticeTypeName(data.notice_type)}</Chip>
{NoticeComponent}
</div>
)
}
export default OneBotNotice

View File

@@ -0,0 +1,151 @@
import { Button } from '@heroui/button'
import { Card, CardBody, CardHeader } from '@heroui/card'
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
import { Snippet } from '@heroui/snippet'
import { motion } from 'motion/react'
import { IoCode } from 'react-icons/io5'
import OneBotDisplayMeta from '@/components/onebot/display_card/meta'
import { getEventName, isOB11Event } from '@/utils/onebot'
import { timestampToDateString } from '@/utils/time'
import type {
AllOB11WsResponse,
OB11AllEvent,
OB11Request
} from '@/types/onebot'
import OneBotMessage from './message'
import OneBotNotice from './notice'
import OneBotDisplayResponse from './response'
const itemVariants = {
hidden: { opacity: 0, scale: 0.8, y: 50 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring', stiffness: 300, damping: 20 }
}
}
function RequestComponent({ data: _ }: { data: OB11Request }) {
return <div>Request消息</div>
}
export interface OneBotItemRenderProps {
data: AllOB11WsResponse[]
index: number
style: React.CSSProperties
}
export const getItemSize = (event: OB11AllEvent['post_type']) => {
if (event === 'meta_event') {
return 100
}
if (event === 'message') {
return 180
}
if (event === 'request') {
return 100
}
if (event === 'notice') {
return 100
}
if (event === 'message_sent') {
return 250
}
return 100
}
const renderDetail = (data: AllOB11WsResponse) => {
if (isOB11Event(data)) {
switch (data.post_type) {
case 'meta_event':
return <OneBotDisplayMeta data={data} />
case 'message':
return <OneBotMessage data={data} />
case 'request':
return <RequestComponent data={data} />
case 'notice':
return <OneBotNotice data={data} />
case 'message_sent':
return <OneBotMessage data={data} />
default:
return <div></div>
}
}
return <OneBotDisplayResponse data={data} />
}
const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
const msg = data[index]
const isEvent = isOB11Event(msg)
return (
<div style={style} className="p-1 overflow-visible w-full h-full">
<motion.div
variants={itemVariants}
initial="hidden"
animate="visible"
className="h-full px-2"
>
<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">
<div className="font-bold">
{isEvent ? getEventName(msg.post_type) : '请求响应'}
</div>
<div className="text-sm">
{isEvent && timestampToDateString(msg.time)}
</div>
<div className="ml-auto">
<Popover
placement="left"
showArrow
classNames={{
content: 'max-h-96 max-w-96 overflow-hidden p-0'
}}
>
<PopoverTrigger>
<Button
size="sm"
color="danger"
variant="flat"
radius="full"
isIconOnly
className="text-medium"
>
<IoCode />
</Button>
</PopoverTrigger>
<PopoverContent>
<Snippet
hideSymbol
tooltipProps={{
content: '点击复制'
}}
classNames={{
copyButton: 'self-start sticky top-0 right-0'
}}
className="bg-content1 h-full overflow-y-scroll items-start"
>
{JSON.stringify(msg, null, 2)
.split('\n')
.map((line, i) => (
<span key={i} className="whitespace-pre-wrap break-all">
{line}
</span>
))}
</Snippet>
</PopoverContent>
</Popover>
</div>
</CardHeader>
<CardBody className="py-0">{renderDetail(msg)}</CardBody>
</Card>
</motion.div>
</div>
)
}
export default OneBotItemRender

View File

@@ -0,0 +1,75 @@
import { Button } from '@heroui/button'
import { Chip } from '@heroui/chip'
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
import { Snippet } from '@heroui/snippet'
import { getResponseStatusColor, getResponseStatusText } from '@/utils/onebot'
import { RequestResponse } from '@/types/onebot'
export interface OneBotDisplayResponseProps {
data: RequestResponse
}
const OneBotDisplayResponse: React.FC<OneBotDisplayResponseProps> = ({
data
}) => {
return (
<div className="flex gap-2 items-center">
<Chip color={getResponseStatusColor(data.status)} variant="flat">
{getResponseStatusText(data.status)}
</Chip>
{data.data && (
<Popover
placement="right"
showArrow
classNames={{
content: 'max-h-96 max-w-96 overflow-hidden p-0'
}}
>
<PopoverTrigger>
<Button
size="sm"
color="danger"
variant="flat"
radius="full"
className="text-medium"
>
</Button>
</PopoverTrigger>
<PopoverContent>
<Snippet
hideSymbol
tooltipProps={{
content: '点击复制'
}}
classNames={{
copyButton: 'self-start sticky top-0 right-0'
}}
className="bg-content1 h-full overflow-y-scroll items-start"
>
{JSON.stringify(data.data, null, 2)
.split('\n')
.map((line, i) => (
<span key={i} className="whitespace-pre-wrap break-all">
{line}
</span>
))}
</Snippet>
</PopoverContent>
</Popover>
)}
{data.message && (
<Chip className="pl-0.5" variant="flat">
<Chip color="warning" size="sm" className="-ml-2 mr-1" variant="flat">
</Chip>
{data.message}
</Chip>
)}
</div>
)
}
export default OneBotDisplayResponse