Compare commits

..

6 Commits

Author SHA1 Message Date
手瓜一十雪
872a3e0100 Add @heroui/divider to frontend dependencies
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Added the @heroui/divider package (version ^2.2.21) to the napcat-webui-frontend dependencies in package.json and updated pnpm-lock.yaml accordingly.
2025-12-20 18:10:32 +08:00
手瓜一十雪
4fcbdc4d89 Remove music player and related context/hooks
Deleted the audio player component, songs context, and use-music hook, along with all related code and configuration. Updated affected components and pages to remove music player dependencies and UI. Also improved sidebar, background, and about page UI, and refactored site config icons to use react-icons.
2025-12-20 18:07:16 +08:00
手瓜一十雪
176af14915 Add 42941 version mappings to external JSON files
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
Added new entries for version 42941 to appid.json, napi2native.json, and packet.json, including mappings for x64 and arm64 architectures. This update ensures support for the latest client versions and their corresponding identifiers and packet mappings.
2025-12-05 18:29:10 +08:00
手瓜一十雪
81cf1fd98e Update wording in usage instructions in README
Some checks failed
Build NapCat Artifacts / Build-Framework (push) Has been cancelled
Build NapCat Artifacts / Build-Shell (push) Has been cancelled
Clarified the instructions regarding support for integration, basic, and underlying framework issues to improve user understanding.
2025-12-01 13:28:18 +08:00
手瓜一十雪
5189099146 Add pnpm-lock.yaml and update .gitignore
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Added pnpm-lock.yaml to track dependencies and removed it from .gitignore in the napcat-webui-frontend package to enable version control of the lock file.
2025-11-30 18:08:22 +08:00
手瓜一十雪
7fc17d45ba Add support for 9.9.25-42905 and 6.9.86-42905 versions
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Updated appid.json, napi2native.json, and packet.json to include entries for versions 9.9.25-42905 (x64/Win) and 6.9.86-42905 (arm64/Mac), adding corresponding appid, qua, send, and recv values.
2025-11-30 12:56:24 +08:00
24 changed files with 12596 additions and 1014 deletions

View File

@ -43,7 +43,7 @@ _Modern protocol-side framework implemented based on NTQQ._
**首次使用**请务必查看如下文档看使用教程 **首次使用**请务必查看如下文档看使用教程
> 项目非盈利,对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。 > 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
## Link ## Link

View File

@ -478,5 +478,25 @@
"6.9.86-42744": { "6.9.86-42744": {
"appid": 537328495, "appid": 537328495,
"qua": "V1_MAC_NQ_6.9.85_42744_GW_B" "qua": "V1_MAC_NQ_6.9.85_42744_GW_B"
},
"9.9.25-42905": {
"appid": 537328521,
"qua": "V1_WIN_NQ_9.9.25_42905_GW_B"
},
"6.9.86-42905": {
"appid": 537328546,
"qua": "V1_MAC_NQ_6.9.86_42905_GW_B"
},
"3.2.22-42941": {
"appid": 537328659,
"qua": "V1_LNX_NQ_3.2.22_42941_GW_B"
},
"9.9.25-42941": {
"appid": 537328623,
"qua": "V1_WIN_NQ_9.9.25_42941_GW_B"
},
"6.9.86-42941": {
"appid": 537328648,
"qua": "V1_MAC_NQ_6.9.86_42941_GW_B"
} }
} }

View File

@ -102,5 +102,29 @@
"6.9.85-42744-arm64": { "6.9.85-42744-arm64": {
"send": "23DFEF0", "send": "23DFEF0",
"recv": "095FD80" "recv": "095FD80"
},
"9.9.25-42905-x64": {
"send": "0A12E74",
"recv": "1D450FD"
},
"6.9.86-42905-arm64": {
"send": "2342408",
"recv": "09639B8"
},
"3.2.22-42941-x64": {
"send": "5BC1630",
"recv": "3011E00"
},
"3.2.22-42941-arm64": {
"send": "3DC90AC",
"recv": "1497A70"
},
"9.9.25-42941-x64": {
"send": "0A131D4",
"recv": "1D4547D"
},
"6.9.86-42941-arm64": {
"send": "2346108",
"recv": "09675F0"
} }
} }

View File

@ -614,5 +614,29 @@
"6.9.86-42744-arm64": { "6.9.86-42744-arm64": {
"send": "3DCC840", "send": "3DCC840",
"recv": "3DCF150" "recv": "3DCF150"
},
"9.9.25-42905-x64": {
"send": "2CE46A0",
"recv": "2CE7C20"
},
"6.9.86-42905-arm64": {
"send": "3DD6098",
"recv": "3DD89A8"
},
"3.2.22-42941-x64": {
"send": "A8AD8A0",
"recv": "A8B1320"
},
"9.9.25-42941-x64": {
"send": "2CE4DA0",
"recv": "2CE8320"
},
"3.2.22-42941-arm64": {
"send": "6BC95E8",
"recv": "6BCCF78"
},
"6.9.86-42941-arm64": {
"send": "3DDDAD0",
"recv": "3DE03E0"
} }
} }

View File

@ -26,7 +26,5 @@ dist-ssr
# NPM LOCK files # NPM LOCK files
package-lock.json package-lock.json
yarn.lock yarn.lock
pnpm-lock.yaml
dist.zip dist.zip

View File

@ -22,6 +22,7 @@
"@heroui/checkbox": "2.3.9", "@heroui/checkbox": "2.3.9",
"@heroui/chip": "2.2.7", "@heroui/chip": "2.2.7",
"@heroui/code": "2.2.7", "@heroui/code": "2.2.7",
"@heroui/divider": "^2.2.21",
"@heroui/dropdown": "2.3.10", "@heroui/dropdown": "2.3.10",
"@heroui/form": "2.1.9", "@heroui/form": "2.1.9",
"@heroui/image": "2.2.6", "@heroui/image": "2.2.6",

View File

@ -7,7 +7,6 @@ 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 useAuth from '@/hooks/auth'; import useAuth from '@/hooks/auth';
@ -33,13 +32,11 @@ function App () {
<Provider store={store}> <Provider store={store}>
<PageBackground /> <PageBackground />
<Toaster /> <Toaster />
<AudioProvider> <Suspense fallback={<PageLoading />}>
<Suspense fallback={<PageLoading />}> <AuthChecker>
<AuthChecker> <AppRoutes />
<AppRoutes /> </AuthChecker>
</AuthChecker> </Suspense>
</Suspense>
</AudioProvider>
</Provider> </Provider>
</DialogProvider> </DialogProvider>
); );

View File

@ -1,425 +0,0 @@
import { Button } from '@heroui/button';
import { Card, CardBody, CardHeader } from '@heroui/card';
import { Image } from '@heroui/image';
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
import { Slider } from '@heroui/slider';
import { Tooltip } from '@heroui/tooltip';
import { useLocalStorage } from '@uidotdev/usehooks';
import clsx from 'clsx';
import { useEffect, useRef, useState } from 'react';
import {
BiSolidSkipNextCircle,
BiSolidSkipPreviousCircle,
} from 'react-icons/bi';
import {
FaPause,
FaPlay,
FaRegHandPointRight,
FaRepeat,
FaShuffle,
} from 'react-icons/fa6';
import { TbRepeatOnce } from 'react-icons/tb';
import { useMediaQuery } from 'react-responsive';
import { PlayMode } from '@/const/enum';
import key from '@/const/key';
import { VolumeHighIcon, VolumeLowIcon } from './icons';
export interface AudioPlayerProps
extends React.AudioHTMLAttributes<HTMLAudioElement> {
src: string
title?: string
artist?: string
cover?: string
pressNext?: () => void
pressPrevious?: () => void
onPlayEnd?: () => void
onChangeMode?: (mode: PlayMode) => void
mode?: PlayMode
}
export default function AudioPlayer (props: AudioPlayerProps) {
const {
src,
pressNext,
pressPrevious,
cover = 'https://nextui.org/images/album-cover.png',
title = '未知',
artist = '未知',
onTimeUpdate,
onLoadedData,
onPlay,
onPause,
onPlayEnd,
onChangeMode,
autoPlay,
mode = PlayMode.Loop,
...rest
} = props;
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(100);
const [isCollapsed, setIsCollapsed] = useLocalStorage(
key.isCollapsedMusicPlayer,
false
);
const audioRef = useRef<HTMLAudioElement>(null);
const cardRef = useRef<HTMLDivElement>(null);
const startY = useRef(0);
const startX = useRef(0);
const [translateY, setTranslateY] = useState(0);
const [translateX, setTranslateX] = useState(0);
const isSmallScreen = useMediaQuery({ maxWidth: 767 });
const isMediumUp = useMediaQuery({ minWidth: 768 });
const shouldAdd = useRef(false);
const currentProgress = (currentTime / duration) * 100;
const [storageAutoPlay, setStorageAutoPlay] = useLocalStorage(
key.autoPlay,
true
);
const handleTimeUpdate = (event: React.SyntheticEvent<HTMLAudioElement>) => {
const audio = event.target as HTMLAudioElement;
setCurrentTime(audio.currentTime);
onTimeUpdate?.(event);
};
const handleLoadedData = (event: React.SyntheticEvent<HTMLAudioElement>) => {
const audio = event.target as HTMLAudioElement;
setDuration(audio.duration);
onLoadedData?.(event);
};
const handlePlay = (e: React.SyntheticEvent<HTMLAudioElement>) => {
setIsPlaying(true);
setStorageAutoPlay(true);
onPlay?.(e);
};
const handlePause = (e: React.SyntheticEvent<HTMLAudioElement>) => {
setIsPlaying(false);
onPause?.(e);
};
const changeMode = () => {
const modes = [PlayMode.Loop, PlayMode.Random, PlayMode.Single];
const currentIndex = modes.findIndex((_mode) => _mode === mode);
const nextIndex = currentIndex + 1;
const nextMode = modes[nextIndex] || modes[0];
onChangeMode?.(nextMode);
};
const volumeChange = (value: number) => {
setVolume(value);
};
useEffect(() => {
const audio = audioRef.current;
if (audio) {
audio.volume = volume / 100;
}
}, [volume]);
const handleTouchStart = (e: React.TouchEvent) => {
startY.current = e.touches[0].clientY;
startX.current = e.touches[0].clientX;
};
const handleTouchMove = (e: React.TouchEvent) => {
const deltaY = e.touches[0].clientY - startY.current;
const deltaX = e.touches[0].clientX - startX.current;
const container = cardRef.current;
const header = cardRef.current?.querySelector('[data-header]');
const headerHeight = header?.clientHeight || 20;
const addHeight = (container?.clientHeight || headerHeight) - headerHeight;
const _shouldAdd = isCollapsed && deltaY < 0;
if (isSmallScreen) {
shouldAdd.current = _shouldAdd;
setTranslateY(_shouldAdd ? deltaY + addHeight : deltaY);
} else {
setTranslateX(deltaX);
}
};
const handleTouchEnd = () => {
if (isSmallScreen) {
const container = cardRef.current;
const header = cardRef.current?.querySelector('[data-header]');
const headerHeight = header?.clientHeight || 20;
const addHeight = (container?.clientHeight || headerHeight) - headerHeight;
const _translateY = translateY - (shouldAdd.current ? addHeight : 0);
if (_translateY > 100) {
setIsCollapsed(true);
} else if (_translateY < -100) {
setIsCollapsed(false);
}
setTranslateY(0);
} else {
if (translateX > 100) {
setIsCollapsed(true);
} else if (translateX < -100) {
setIsCollapsed(false);
}
setTranslateX(0);
}
};
const dragTranslate = isSmallScreen
? translateY
? `translateY(${translateY}px)`
: ''
: translateX
? `translateX(${translateX}px)`
: '';
const collapsedTranslate = isCollapsed
? isSmallScreen
? 'translateY(90%)'
: 'translateX(96%)'
: '';
const translateStyle = dragTranslate || collapsedTranslate;
if (!src) return null;
return (
<div
className={clsx(
'fixed right-0 bottom-0 z-[52] w-full md:w-96',
!translateX && !translateY && 'transition-transform',
isCollapsed && 'md:hover:!translate-x-80'
)}
style={{
transform: translateStyle,
}}
>
<audio
src={src}
onLoadedData={handleLoadedData}
onTimeUpdate={handleTimeUpdate}
onPlay={handlePlay}
onPause={handlePause}
onEnded={onPlayEnd}
autoPlay={autoPlay ?? storageAutoPlay}
{...rest}
controls={false}
hidden
ref={audioRef}
/>
<Card
ref={cardRef}
className={clsx(
'border-none bg-background/60 dark:bg-default-300/50 w-full max-w-full transform transition-transform backdrop-blur-md duration-300 overflow-visible',
isSmallScreen ? 'rounded-t-3xl' : 'md:rounded-l-xl'
)}
classNames={{
body: 'p-0',
}}
shadow='sm'
radius='none'
>
{isMediumUp && (
<Button
isIconOnly
className={clsx(
'absolute data-[hover]:bg-foreground/10 text-lg z-50',
isCollapsed
? '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'
)}
variant='solid'
color='primary'
size='sm'
onPress={() => setIsCollapsed(!isCollapsed)}
>
<FaRegHandPointRight />
</Button>
)}
{isSmallScreen && (
<CardHeader
data-header
className='flex-row justify-center pt-4'
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onClick={() => setIsCollapsed(!isCollapsed)}
>
<div className='w-24 h-2 rounded-full bg-content2-foreground shadow-sm' />
</CardHeader>
)}
<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='relative col-span-6 md:col-span-4 flex justify-center'>
<Image
alt='Album cover'
className='object-cover'
classNames={{
wrapper: 'w-36 aspect-square md:w-24 flex',
img: 'block w-full h-full',
}}
shadow='md'
src={cover}
width='100%'
/>
</div>
<div className='flex flex-col col-span-6 md:col-span-8'>
<div className='flex flex-col gap-0'>
<h1 className='font-medium truncate'>{title}</h1>
<p className='text-xs text-foreground/80 truncate'>{artist}</p>
</div>
<div className='flex flex-col'>
<Slider
aria-label='Music progress'
classNames={{
track: 'bg-default-500/30 border-none',
thumb: 'w-2 h-2 after:w-1.5 after:h-1.5',
filler: 'rounded-full',
}}
color='foreground'
value={currentProgress || 0}
defaultValue={0}
size='sm'
onChange={(value) => {
value = Array.isArray(value) ? value[0] : value;
const audio = audioRef.current;
if (audio) {
audio.currentTime = (value / 100) * duration;
}
}}
/>
<div className='flex justify-between h-3'>
<p className='text-xs'>
{Math.floor(currentTime / 60)}:
{Math.floor(currentTime % 60)
.toString()
.padStart(2, '0')}
</p>
<p className='text-xs text-foreground/50'>
{Math.floor(duration / 60)}:
{Math.floor(duration % 60)
.toString()
.padStart(2, '0')}
</p>
</div>
</div>
<div className='flex w-full items-center justify-center'>
<Tooltip
content={
mode === PlayMode.Loop
? '列表循环'
: mode === PlayMode.Random
? '随机播放'
: '单曲循环'
}
>
<Button
isIconOnly
className='data-[hover]:bg-foreground/10 text-lg md:text-medium'
radius='full'
variant='light'
size='md'
onPress={changeMode}
>
{mode === PlayMode.Loop && (
<FaRepeat className='text-foreground/80' />
)}
{mode === PlayMode.Random && (
<FaShuffle className='text-foreground/80' />
)}
{mode === PlayMode.Single && (
<TbRepeatOnce className='text-foreground/80 text-xl' />
)}
</Button>
</Tooltip>
<Tooltip content='上一首'>
<Button
isIconOnly
className='data-[hover]:bg-foreground/10 text-2xl md:text-xl'
radius='full'
variant='light'
size='md'
onPress={pressPrevious}
>
<BiSolidSkipPreviousCircle />
</Button>
</Tooltip>
<Tooltip content={isPlaying ? '暂停' : '播放'}>
<Button
isIconOnly
className='data-[hover]:bg-foreground/10 text-3xl md:text-3xl'
radius='full'
variant='light'
size='lg'
onPress={() => {
if (isPlaying) {
audioRef.current?.pause();
setStorageAutoPlay(false);
} else {
audioRef.current?.play();
}
}}
>
{isPlaying ? <FaPause /> : <FaPlay className='ml-1' />}
</Button>
</Tooltip>
<Tooltip content='下一首'>
<Button
isIconOnly
className='data-[hover]:bg-foreground/10 text-2xl md:text-xl'
radius='full'
variant='light'
size='md'
onPress={pressNext}
>
<BiSolidSkipNextCircle />
</Button>
</Tooltip>
<Popover
placement='top'
classNames={{
content: 'bg-opacity-30 backdrop-blur-md',
}}
>
<PopoverTrigger>
<Button
isIconOnly
className='data-[hover]:bg-foreground/10 text-xl md:text-xl'
radius='full'
variant='light'
size='md'
>
<VolumeHighIcon />
</Button>
</PopoverTrigger>
<PopoverContent>
<Slider
orientation='vertical'
showTooltip
aria-label='Volume'
className='h-40'
color='primary'
defaultValue={volume}
onChange={(value) => {
value = Array.isArray(value) ? value[0] : value;
volumeChange(value);
}}
startContent={<VolumeHighIcon className='text-2xl' />}
size='sm'
endContent={<VolumeLowIcon className='text-2xl' />}
/>
</PopoverContent>
</Popover>
</div>
</div>
</div>
</CardBody>
</Card>
</div>
);
}

View File

@ -94,7 +94,7 @@ const HoverEffectCard: React.FC<HoverEffectCardProps> = (props) => {
ref={lightRef} ref={lightRef}
className={clsx( className={clsx(
isShowLight ? 'opacity-100' : 'opacity-0', isShowLight ? 'opacity-100' : 'opacity-0',
'absolute rounded-full blur-[150px] filter transition-opacity duration-300 dark:bg-[#2850ff] bg-[#ff4132] w-[100px] h-[100px]', 'absolute rounded-full blur-[100px] filter transition-opacity duration-300 bg-gradient-to-r from-primary-400 to-secondary-400 w-[150px] h-[150px]',
lightClassName lightClassName
)} )}
style={{ style={{

View File

@ -1,23 +1,37 @@
import { Image } from '@heroui/image'; import { motion } from 'motion/react';
import bkg_color from '@/assets/images/bkg-color.png';
const PageBackground = () => { const PageBackground = () => {
return ( return (
<> <div className='fixed inset-0 w-full h-full -z-10 overflow-hidden bg-gradient-to-br from-indigo-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900'>
<div className='fixed w-full h-full -z-[0] flex justify-end opacity-80'> {/* 动态呼吸光斑 - ACG风格 */}
<Image <motion.div
className='overflow-hidden object-contain -top-42 h-[160%] -right-[30%] -rotate-45 pointer-events-none select-none -z-10 relative' animate={{
src={bkg_color} scale: [1, 1.2, 1],
/> rotate: [0, 90, 0],
</div> opacity: [0.3, 0.5, 0.3]
<div className='fixed w-full h-full overflow-hidden -z-[0] hue-rotate-90 flex justify-start opacity-80'> }}
<Image transition={{ duration: 15, repeat: Infinity, ease: "easeInOut" }}
className='relative -top-92 h-[180%] object-contain pointer-events-none rotate-90 select-none -z-10 top-44' className='absolute top-[-10%] left-[-10%] w-[500px] h-[500px] rounded-full bg-primary-200/40 blur-[100px]'
src={bkg_color} />
/> <motion.div
</div> animate={{
</> scale: [1, 1.3, 1],
x: [0, 100, 0],
opacity: [0.3, 0.6, 0.3]
}}
transition={{ duration: 18, repeat: Infinity, ease: "easeInOut", delay: 2 }}
className='absolute top-[20%] right-[-10%] w-[400px] h-[400px] rounded-full bg-secondary-200/40 blur-[90px]'
/>
<motion.div
animate={{
scale: [1, 1.1, 1],
y: [0, -50, 0],
opacity: [0.2, 0.4, 0.2]
}}
transition={{ duration: 12, repeat: Infinity, ease: "easeInOut", delay: 5 }}
className='absolute bottom-[-10%] left-[20%] w-[600px] h-[600px] rounded-full bg-pink-200/30 blur-[110px]'
/>
</div>
); );
}; };

View File

@ -1,5 +1,4 @@
import { Button } from '@heroui/button'; import { Button } from '@heroui/button';
import { Image } from '@heroui/image';
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence, motion } from 'motion/react'; import { AnimatePresence, motion } from 'motion/react';
import React from 'react'; import React from 'react';
@ -10,15 +9,13 @@ 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 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;
items: MenuItem[] items: MenuItem[];
onClose?: () => void onClose?: () => void;
} }
const SideBar: React.FC<SideBarProps> = (props) => { const SideBar: React.FC<SideBarProps> = (props) => {
@ -61,40 +58,33 @@ const SideBar: React.FC<SideBarProps> = (props) => {
}} }}
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 p-4'>
<div className='flex justify-center items-center my-2 gap-2'> <div className='flex items-center justify-start gap-3 px-2 my-8 ml-2'>
<Image radius='none' height={40} src={logo} className='mb-2' /> <div className="h-5 w-1 bg-primary rounded-full shadow-sm" />
<div <div className="text-xl font-bold text-default-900 dark:text-white tracking-wide select-none">
className={clsx(
'flex items-center font-bold',
'!text-2xl shiny-text'
)}
>
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-2'>
<Menus items={items} /> <Menus items={items} />
<div className='mt-auto mb-10 md:mb-0'> <div className='mt-auto mb-10 md:mb-0 space-y-3 px-2'>
<Button <Button
className='w-full' className='w-full bg-primary-50/50 hover:bg-primary-100/80 text-primary-600 font-medium shadow-sm hover:shadow-md transition-all duration-300 backdrop-blur-sm'
color='primary'
radius='full' radius='full'
variant='light' variant='flat'
onPress={toggleTheme} onPress={toggleTheme}
startContent={ startContent={
!isDark ? <MdLightMode size={16} /> : <MdDarkMode size={16} /> !isDark ? <MdLightMode size={18} /> : <MdDarkMode size={18} />
} }
> >
</Button> </Button>
<Button <Button
className='w-full mb-2' className='w-full mb-2 bg-danger-50/50 hover:bg-danger-100/80 text-danger-500 font-medium shadow-sm hover:shadow-md transition-all duration-300 backdrop-blur-sm'
color='primary'
radius='full' radius='full'
variant='light' variant='flat'
onPress={onRevokeAuth} onPress={onRevokeAuth}
startContent={<IoMdLogOut size={16} />} startContent={<IoMdLogOut size={18} />}
> >
退 退
</Button> </Button>

View File

@ -50,12 +50,13 @@ const renderItems = (items: MenuItem[], children = false) => {
<div key={item.href + item.label}> <div key={item.href + item.label}>
<Button <Button
className={clsx( className={clsx(
'flex items-center w-full text-left justify-start dark:text-white', 'flex items-center w-full text-left justify-start dark:text-white transition-all duration-300',
// children && 'rounded-l-lg', isActive
isActive && 'bg-opacity-60', ? 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-400 shadow-none font-semibold translate-x-1'
: 'hover:bg-default-100 hover:translate-x-1',
b64img && 'backdrop-blur-md text-white' b64img && 'backdrop-blur-md text-white'
)} )}
color='primary' color={isActive ? 'primary' : 'default'}
endContent={ endContent={
canOpen canOpen
? ( ? (
@ -104,7 +105,6 @@ const renderItems = (items: MenuItem[], children = false) => {
/> />
) )
} }
radius='full'
startContent={ startContent={
customIcons[item.label] customIcons[item.label]
? ( ? (
@ -147,7 +147,7 @@ const renderItems = (items: MenuItem[], children = false) => {
}; };
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;

View File

@ -1,107 +1,72 @@
import { import {
BugIcon2, LuActivity,
FileIcon, LuFileText,
InfoIcon, LuFolderOpen,
LogIcon, LuInfo,
RouteIcon, LuLayoutDashboard,
SettingsIcon, LuSettings,
SignalTowerIcon, LuSignal,
TerminalIcon, LuTerminal,
} from '@/components/icons'; LuZap,
} from 'react-icons/lu';
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;
autoOpen?: boolean autoOpen?: boolean;
href?: string href?: string;
items?: MenuItem[] items?: MenuItem[];
customIcon?: string customIcon?: string;
} }
export const siteConfig = { export const siteConfig = {
name: 'NapCat WebUI', name: 'NapCat',
description: 'NapCat WebUI.', description: 'NapCat WebUI.',
navItems: [ navItems: [
{ {
label: '基础信息', label: '基础信息',
icon: ( icon: <LuLayoutDashboard className='w-5 h-5' />,
<div className='w-5 h-5'>
<RouteIcon />
</div>
),
href: '/', href: '/',
}, },
{ {
label: '网络配置', label: '网络配置',
icon: ( icon: <LuSignal className='w-5 h-5' />,
<div className='w-5 h-5'>
<SignalTowerIcon />
</div>
),
href: '/network', href: '/network',
}, },
{ {
label: '其他配置', label: '其他配置',
icon: ( icon: <LuSettings className='w-5 h-5' />,
<div className='w-5 h-5'>
<SettingsIcon />
</div>
),
href: '/config', href: '/config',
}, },
{ {
label: '猫猫日志', label: '猫猫日志',
icon: ( icon: <LuFileText className='w-5 h-5' />,
<div className='w-5 h-5'>
<LogIcon />
</div>
),
href: '/logs', href: '/logs',
}, },
{ {
label: '接口调试', label: '接口调试',
icon: ( icon: <LuActivity className='w-5 h-5' />,
<div className='w-5 h-5'> href: '/debug/http',
<BugIcon2 /> },
</div> {
), label: '实时调试',
items: [ icon: <LuZap className='w-5 h-5' />,
{ href: '/debug/ws',
label: 'HTTP',
href: '/debug/http',
},
{
label: 'Websocket',
href: '/debug/ws',
},
],
}, },
{ {
label: '文件管理', label: '文件管理',
icon: ( icon: <LuFolderOpen className='w-5 h-5' />,
<div className='w-5 h-5'>
<FileIcon />
</div>
),
href: '/file_manager', href: '/file_manager',
}, },
{ {
label: '系统终端', label: '系统终端',
icon: ( icon: <LuTerminal className='w-5 h-5' />,
<div className='w-5 h-5'>
<TerminalIcon />
</div>
),
href: '/terminal', href: '/terminal',
}, },
{ {
label: '关于我们', label: '关于我们',
icon: ( icon: <LuInfo className='w-5 h-5' />,
<div className='w-5 h-5'>
<InfoIcon />
</div>
),
href: '/about', href: '/about',
}, },
] as MenuItem[], ] as MenuItem[],

View File

@ -1,91 +0,0 @@
// Songs Context
import { useLocalStorage } from '@uidotdev/usehooks';
import { createContext, useEffect, useState } from 'react';
import { PlayMode } from '@/const/enum';
import key from '@/const/key';
import AudioPlayer from '@/components/audio_player';
import { get163MusicListSongs, getNextMusic } from '@/utils/music';
import type { FinalMusic } from '@/types/music';
export interface MusicContextProps {
setListId: (id: string) => void
listId: string
onNext: () => void
onPrevious: () => void
}
export interface MusicProviderProps {
children: React.ReactNode
}
export const AudioContext = createContext<MusicContextProps>({
setListId: () => {},
listId: '5438670983',
onNext: () => {},
onPrevious: () => {},
});
const AudioProvider: React.FC<MusicProviderProps> = ({ children }) => {
const [listId, setListId] = useLocalStorage(key.musicID, '5438670983');
const [musicList, setMusicList] = useState<FinalMusic[]>([]);
const [musicId, setMusicId] = useState<number>(0);
const [playMode, setPlayMode] = useState<PlayMode>(PlayMode.Loop);
const music = musicList.find((music) => music.id === musicId);
const [token] = useLocalStorage(key.token, '');
const onNext = () => {
const nextID = getNextMusic(musicList, musicId, playMode);
setMusicId(nextID);
};
const onPrevious = () => {
const index = musicList.findIndex((music) => music.id === musicId);
if (index === 0) {
setMusicId(musicList[musicList.length - 1].id);
} else {
setMusicId(musicList[index - 1].id);
}
};
const onPlayEnd = () => {
const nextID = getNextMusic(musicList, musicId, playMode);
setMusicId(nextID);
};
const changeMode = (mode: PlayMode) => {
setPlayMode(mode);
};
const fetchMusicList = async (id: string) => {
const res = await get163MusicListSongs(id);
setMusicList(res);
setMusicId(res[0].id);
};
useEffect(() => {
if (listId && token) fetchMusicList(listId);
}, [listId, token]);
return (
<AudioContext.Provider
value={{
setListId,
listId,
onNext,
onPrevious,
}}
>
<AudioPlayer
title={music?.title}
src={music?.url || ''}
artist={music?.artist}
cover={music?.cover}
mode={playMode}
pressNext={onNext}
pressPrevious={onPrevious}
onPlayEnd={onPlayEnd}
onChangeMode={changeMode}
/>
{children}
</AudioContext.Provider>
);
};
export default AudioProvider;

View File

@ -1,11 +0,0 @@
import React from 'react';
import { AudioContext } from '@/contexts/songs';
const useMusic = () => {
const music = React.useContext(AudioContext);
return music;
};
export default useMusic;

View File

@ -79,10 +79,11 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
}, [location.pathname]); }, [location.pathname]);
return ( return (
<div <div
className='h-screen relative flex bg-primary-50 dark:bg-black items-stretch' className='h-screen relative flex items-stretch overflow-hidden'
style={{ style={{
backgroundImage: `url(${b64img})`, backgroundImage: b64img ? `url(${b64img})` : undefined,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center',
}} }}
> >
<SideBar <SideBar
@ -90,14 +91,19 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
open={openSideBar} open={openSideBar}
onClose={() => setOpenSideBar(false)} onClose={() => setOpenSideBar(false)}
/> />
<div <motion.div
layout
ref={contentRef} ref={contentRef}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
className={clsx( className={clsx(
'overflow-y-auto flex-1 rounded-md m-1 bg-content1 pb-10 md:pb-0', 'flex-1 overflow-y-auto',
openSideBar ? 'ml-0' : 'ml-1', 'bg-white/60 dark:bg-black/40 backdrop-blur-xl',
!b64img && 'shadow-inner', 'shadow-[0_8px_32px_0_rgba(31,38,135,0.07)]',
b64img && '!bg-opacity-50 backdrop-blur-none dark:bg-background', 'transition-all duration-300 ease-in-out',
'overflow-x-hidden' openSideBar ? 'm-3 ml-0 rounded-3xl border border-white/40 dark:border-white/10' : 'm-0 rounded-none',
'pb-10 md:pb-0'
)} )}
> >
<div <div
@ -109,15 +115,12 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
'z-30 m-2 mb-0 sticky top-2 left-0' 'z-30 m-2 mb-0 sticky top-2 left-0'
)} )}
> >
<motion.div <div
className={clsx( className={clsx(
'mr-1 ease-in-out ml-0 md:relative z-50 md:z-auto', 'mr-1 ease-in-out ml-0 md:relative z-50 md:z-auto',
openSideBar && 'pl-2 absolute', openSideBar && 'pl-2',
'md:!ml-0 md:pl-0' 'md:!ml-0 md:pl-0'
)} )}
transition={{ type: 'spring', stiffness: 150, damping: 15 }}
initial={{ marginLeft: 0 }}
animate={{ marginLeft: openSideBar ? '15rem' : 0 }}
> >
<Button <Button
isIconOnly isIconOnly
@ -127,7 +130,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
> >
{openSideBar ? <MdMenuOpen size={24} /> : <MdMenu size={24} />} {openSideBar ? <MdMenuOpen size={24} /> : <MdMenu size={24} />}
</Button> </Button>
</motion.div> </div>
<Breadcrumbs isDisabled size='lg'> <Breadcrumbs isDisabled size='lg'>
{title?.map((item, index) => ( {title?.map((item, index) => (
<BreadcrumbItem key={index}> <BreadcrumbItem key={index}>
@ -149,7 +152,7 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
<ErrorBoundary fallbackRender={errorFallbackRender}> <ErrorBoundary fallbackRender={errorFallbackRender}>
{children} {children}
</ErrorBoundary> </ErrorBoundary>
</div> </motion.div>
</div> </div>
); );
}; };

View File

@ -1,19 +1,20 @@
import { Card, CardBody } from '@heroui/card'; import { Card, CardBody, CardHeader } from '@heroui/card';
import { Chip } from '@heroui/chip';
import { Divider } from '@heroui/divider';
import { Image } from '@heroui/image'; import { Image } from '@heroui/image';
import { Link } from '@heroui/link'; import { Link } from '@heroui/link';
import { Skeleton } from '@heroui/skeleton';
import { Spinner } from '@heroui/spinner'; import { Spinner } from '@heroui/spinner';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { useMemo } from 'react'; import {
import { BsTelegram, BsTencentQq } from 'react-icons/bs'; BsCodeSlash,
import { IoDocument } from 'react-icons/io5'; BsCpu,
BsGithub,
import HoverTiltedCard from '@/components/hover_titled_card'; BsGlobe,
import NapCatRepoInfo from '@/components/napcat_repo_info'; BsPlugin,
import RotatingText from '@/components/rotating_text'; BsTelegram,
BsTencentQq
import { usePreloadImages } from '@/hooks/use-preload-images'; } from 'react-icons/bs';
import { useTheme } from '@/hooks/use-theme'; import { IoDocument, IoRocketSharp } from 'react-icons/io5';
import logo from '@/assets/images/logo.png'; import logo from '@/assets/images/logo.png';
import WebUIManager from '@/controllers/webui_manager'; import WebUIManager from '@/controllers/webui_manager';
@ -22,184 +23,168 @@ function VersionInfo () {
const { data, loading, error } = useRequest(WebUIManager.GetNapCatVersion); const { data, loading, error } = useRequest(WebUIManager.GetNapCatVersion);
return ( return (
<div className='flex items-center gap-4'> <div className='flex items-center gap-2'>
<div className='flex items-center gap-2 text-2xl font-bold'> {error ? (
<div className='text-primary-500 drop-shadow-md'>NapCat</div> <Chip color="danger" variant="flat" size="sm">{error.message}</Chip>
{error ) : loading ? (
? ( <Spinner size='sm' color="default" />
error.message ) : (
) <div className="flex items-center gap-2">
: loading <Chip size="sm" color="default" variant="flat" className="text-default-500">WebUI v0.0.6</Chip>
? ( <Chip size="sm" color="primary" variant="flat">Core {data?.version}</Chip>
<Spinner size='sm' /> </div>
) )}
: (
<RotatingText
texts={['WebUI', data?.version ?? '']}
mainClassName='overflow-hidden flex items-center bg-primary-500 px-2 rounded-lg text-default-50 shadow-md'
staggerFrom='last'
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '-120%' }}
staggerDuration={0.025}
splitLevelClassName='overflow-hidden'
transition={{ type: 'spring', damping: 30, stiffness: 400 }}
rotationInterval={2000}
/>
)}
</div>
</div> </div>
); );
} }
export default function AboutPage () { export default function AboutPage () {
const { isDark } = useTheme(); const features = [
{
const imageUrls = useMemo( icon: <IoRocketSharp size={20} />,
() => [ title: '高性能架构',
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=light', desc: 'Node.js + Native 混合架构,资源占用低,响应速度快。',
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=dark', className: 'bg-primary-50 text-primary'
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=light',
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=dark',
],
[]
);
const { loadedUrls, isLoading } = usePreloadImages(imageUrls);
const getImageUrl = useMemo(
() => (baseUrl: string) => {
const theme = isDark ? 'dark' : 'light';
const fullUrl = baseUrl.replace(
/color_scheme=(?:light|dark)/,
`color_scheme=${theme}`
);
return isLoading ? null : loadedUrls[fullUrl] ? fullUrl : null;
}, },
[isDark, isLoading, loadedUrls] {
); icon: <BsGlobe size={20} />,
title: '全平台支持',
const renderImage = useMemo( desc: '适配 Windows、Linux 及 Docker 环境。',
() => (baseUrl: string, alt: string) => { className: 'bg-success-50 text-success'
const imageUrl = getImageUrl(baseUrl);
if (!imageUrl) {
return <Skeleton className='h-16 rounded-lg' />;
}
return (
<Image
className='flex-1 pointer-events-none select-none rounded-none'
src={imageUrl}
alt={alt}
/>
);
}, },
[getImageUrl] {
); icon: <BsCodeSlash size={20} />,
title: 'OneBot 11',
desc: '深度集成标准协议,兼容现有生态。',
className: 'bg-warning-50 text-warning'
},
{
icon: <BsPlugin size={20} />,
title: '极易扩展',
desc: '提供丰富的 API 接口与 WebHook 支持。',
className: 'bg-secondary-50 text-secondary'
}
];
const links = [
{ icon: <BsGithub />, name: 'GitHub', href: 'https://github.com/NapNeko/NapCatQQ' },
{ icon: <BsTelegram />, name: 'Telegram', href: 'https://t.me/napcatqq' },
{ icon: <BsTencentQq />, name: 'QQ 群 1', href: 'https://qm.qq.com/q/F9cgs1N3Mc' },
{ icon: <BsTencentQq />, name: 'QQ 群 2', href: 'https://qm.qq.com/q/hSt0u9PVn' },
{ icon: <IoDocument />, name: '文档', href: 'https://napcat.napneko.icu/' },
];
const cardStyle = "bg-default/40 backdrop-blur-lg border-none shadow-none";
return ( return (
<> <div className='flex flex-col h-full w-full gap-6 p-2 md:p-6'>
<title> NapCat WebUI</title> <title> - NapCat WebUI</title>
<section className='max-w-7xl py-8 md:py-10 px-5 mx-auto space-y-10'>
<div className='w-full flex flex-col md:flex-row gap-4'> {/* 头部标题区 */}
<div className='flex flex-col md:flex-row items-center'> <div className="flex flex-col gap-2">
<HoverTiltedCard imageSrc={logo} overlayContent='' /> <h1 className="text-2xl font-bold flex items-center gap-3 text-default-900">
</div> <Image src={logo} alt="NapCat Logo" width={32} height={32} />
<div className='flex-1 flex flex-col gap-2 py-2'> NapCat
<VersionInfo /> </h1>
<div className='space-y-1'> <div className="flex items-center gap-4 text-small text-default-500">
<p className='font-bold text-primary-400'>NapCat ?</p> <p> QQ </p>
<p className='text-default-800'> <Divider orientation="vertical" className="h-4" />
TypeScript构建的Bot框架,,QQ <VersionInfo />
Node模块提供给客户端的接口,Bot的功能. </div>
</div>
<Divider className="opacity-50" />
{/* 主内容区:双栏布局 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-grow">
{/* 左侧:介绍与特性 */}
<div className="lg:col-span-2 space-y-6">
<Card shadow="sm" className={cardStyle}>
<CardHeader className="pb-0 pt-4 px-4 flex-col items-start">
<h2 className="text-lg font-bold"></h2>
</CardHeader>
<CardBody className="py-4 text-default-600 leading-relaxed space-y-2">
<p>
NapCat () QQ NTQQ
GUI Headless
</p> </p>
<p className='font-bold text-primary-400'></p> <p>
<p className='text-default-800'> NapCat OneBot 11
QQ
便使 OneBot HTTP /
WebSocket
QQ发送接口之类的接口
</p> </p>
</div> </CardBody>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{features.map((item, index) => (
<Card key={index} shadow="sm" className={cardStyle}>
<CardBody className="flex flex-row items-start gap-4 p-4">
<div className={`p-3 rounded-lg ${item.className}`}>
{item.icon}
</div>
<div>
<h3 className="font-semibold text-default-900">{item.title}</h3>
<p className="text-small text-default-500 mt-1">{item.desc}</p>
</div>
</CardBody>
</Card>
))}
</div> </div>
</div> </div>
<div className='flex flex-row gap-2 flex-wrap justify-around'>
<Card {/* 右侧:信息与链接 */}
as={Link} <div className="space-y-6">
shadow='sm' <Card shadow="sm" className={cardStyle}>
isPressable <CardHeader className="pb-0 pt-4 px-4">
isExternal <h2 className="text-lg font-bold"></h2>
href='https://qm.qq.com/q/F9cgs1N3Mc' </CardHeader>
> <CardBody className="py-4">
<CardBody className='flex-row items-center gap-2'> <div className="flex flex-col gap-2">
<span className='p-2 rounded-small bg-primary-50 text-primary-500'> {links.map((link, idx) => (
<BsTencentQq size={16} /> <Link
</span> key={idx}
<span>1</span> isExternal
href={link.href}
className="flex items-center justify-between p-3 rounded-xl hover:bg-default-100/50 transition-colors text-default-600"
>
<span className="flex items-center gap-3">
{link.icon}
{link.name}
</span>
<span className="text-tiny text-default-400"> &rarr;</span>
</Link>
))}
</div>
</CardBody> </CardBody>
</Card> </Card>
<Card <Card shadow="sm" className={cardStyle}>
as={Link} <CardHeader className="pb-0 pt-4 px-4">
shadow='sm' <h2 className="text-lg font-bold flex items-center gap-2">
isPressable <BsCpu />
isExternal </h2>
href='https://qm.qq.com/q/hSt0u9PVn' </CardHeader>
> <CardBody className="py-4">
<CardBody className='flex-row items-center gap-2'> <div className="flex flex-wrap gap-2">
<span className='p-2 rounded-small bg-primary-50 text-primary-500'> {['TypeScript', 'React', 'Vite', 'Node.js', 'Electron', 'HeroUI'].map((tech) => (
<BsTencentQq size={16} /> <Chip key={tech} size="sm" variant="flat" className="bg-default-100/50 text-default-600">
</span> {tech}
<span>2</span> </Chip>
</CardBody> ))}
</Card> </div>
<Card
as={Link}
shadow='sm'
isPressable
isExternal
href='https://t.me/napcatqq'
>
<CardBody className='flex-row items-center gap-2'>
<span className='p-2 rounded-small bg-primary-50 text-primary-500'>
<BsTelegram size={16} />
</span>
<span>Telegram</span>
</CardBody>
</Card>
<Card
as={Link}
shadow='sm'
isPressable
isExternal
href='https://napcat.napneko.icu/'
>
<CardBody className='flex-row items-center gap-2'>
<span className='p-2 rounded-small bg-primary-50 text-primary-500'>
<IoDocument size={16} />
</span>
<span>使</span>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div className='flex flex-col md:flex-row md:items-start gap-4'> </div>
<div className='w-full flex flex-col gap-4'>
{renderImage(
'https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=777721566&limit=30&image_size=auto&color_scheme=light',
'Contributors'
)}
{renderImage(
'https://next.ossinsight.io/widgets/official/compose-activity-trends/thumbnail.png?repo_id=41986369&image_size=auto&color_scheme=light',
'Activity Trends'
)}
</div>
<NapCatRepoInfo /> {/* 底部版权 - 移出 grid 布局 */}
</div> <div className="w-full text-center text-tiny text-default-400 py-4 mt-auto flex flex-col items-center gap-1">
</section> <p className="flex items-center justify-center gap-1">
</> Made with <span className="text-danger"></span> by NapCat Team
</p>
<p>MIT License © {new Date().getFullYear()}</p>
</div>
</div>
); );
} }

View File

@ -1,4 +1,3 @@
import { Input } from '@heroui/input';
import { Button } from '@heroui/button'; import { Button } from '@heroui/button';
import { useLocalStorage } from '@uidotdev/usehooks'; import { useLocalStorage } from '@uidotdev/usehooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -11,8 +10,6 @@ import SaveButtons from '@/components/button/save_buttons';
import FileInput from '@/components/input/file_input'; import FileInput from '@/components/input/file_input';
import ImageInput from '@/components/input/image_input'; import ImageInput from '@/components/input/image_input';
import useMusic from '@/hooks/use-music';
import { siteConfig } from '@/config/site'; import { siteConfig } from '@/config/site';
import FileManager from '@/controllers/file_manager'; import FileManager from '@/controllers/file_manager';
import WebUIManager from '@/controllers/webui_manager'; import WebUIManager from '@/controllers/webui_manager';
@ -43,7 +40,6 @@ const WebUIConfigCard = () => {
} = useForm<IConfig['webui']>({ } = useForm<IConfig['webui']>({
defaultValues: { defaultValues: {
background: '', background: '',
musicListID: '',
customIcons: {}, customIcons: {},
}, },
}); });
@ -53,7 +49,6 @@ const WebUIConfigCard = () => {
key.customIcons, key.customIcons,
{} {}
); );
const { setListId, listId } = useMusic();
const [registrationOptions, setRegistrationOptions] = useState<any>(null); const [registrationOptions, setRegistrationOptions] = useState<any>(null);
const [isLoadingOptions, setIsLoadingOptions] = useState(false); const [isLoadingOptions, setIsLoadingOptions] = useState(false);
@ -75,14 +70,12 @@ const WebUIConfigCard = () => {
}; };
const reset = () => { const reset = () => {
setWebuiValue('musicListID', listId);
setWebuiValue('customIcons', customIcons); setWebuiValue('customIcons', customIcons);
setWebuiValue('background', b64img); setWebuiValue('background', b64img);
}; };
const onSubmit = handleWebuiSubmit((data) => { const onSubmit = handleWebuiSubmit((data) => {
try { try {
setListId(data.musicListID);
setCustomIcons(data.customIcons); setCustomIcons(data.customIcons);
setB64img(data.background); setB64img(data.background);
toast.success('保存成功'); toast.success('保存成功');
@ -94,7 +87,7 @@ const WebUIConfigCard = () => {
useEffect(() => { useEffect(() => {
reset(); reset();
}, [listId, customIcons, b64img]); }, [customIcons, b64img]);
return ( return (
<> <>
@ -130,20 +123,6 @@ const WebUIConfigCard = () => {
/> />
</div> </div>
</div> </div>
<div className='flex flex-col gap-2'>
<div className='flex-shrink-0 w-full'>WebUI音乐播放器</div>
<Controller
control={control}
name='musicListID'
render={({ field }) => (
<Input
{...field}
label='网易云音乐歌单ID网页内音乐播放器'
placeholder='请输入歌单ID'
/>
)}
/>
</div>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<div className='flex-shrink-0 w-full'></div> <div className='flex-shrink-0 w-full'></div>
<Controller <Controller

View File

@ -6,6 +6,8 @@ import { useEffect, useRef, useState } from 'react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import logo from '@/assets/images/logo.png';
import HoverEffectCard from '@/components/effect_card'; import HoverEffectCard from '@/components/effect_card';
import { title } from '@/components/primitives'; import { title } from '@/components/primitives';
import QrCodeLogin from '@/components/qr_code_login'; import QrCodeLogin from '@/components/qr_code_login';
@ -13,9 +15,9 @@ import QuickLogin from '@/components/quick_login';
import type { QQItem } from '@/components/quick_login'; import type { QQItem } from '@/components/quick_login';
import { ThemeSwitch } from '@/components/theme-switch'; import { ThemeSwitch } from '@/components/theme-switch';
import logo from '@/assets/images/logo.png';
import QQManager from '@/controllers/qq_manager'; import QQManager from '@/controllers/qq_manager';
import PureLayout from '@/layouts/pure'; import PureLayout from '@/layouts/pure';
import { motion } from 'motion/react';
export default function QQLoginPage () { export default function QQLoginPage () {
const navigate = useNavigate(); const navigate = useNavigate();
@ -112,7 +114,12 @@ export default function QQLoginPage () {
<> <>
<title>QQ登录 - NapCat WebUI</title> <title>QQ登录 - NapCat WebUI</title>
<PureLayout> <PureLayout>
<div className='w-[608px] max-w-full py-8 px-2 md:px-8 overflow-hidden'> <motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.5, type: 'spring', stiffness: 120, damping: 20 }}
className='w-[608px] max-w-full py-8 px-2 md:px-8 overflow-hidden'
>
<HoverEffectCard <HoverEffectCard
className='items-center gap-4 pt-0 pb-6 bg-default-50' className='items-center gap-4 pt-0 pb-6 bg-default-50'
maxXRotation={3} maxXRotation={3}
@ -169,7 +176,7 @@ export default function QQLoginPage () {
</Button> </Button>
</CardBody> </CardBody>
</HoverEffectCard> </HoverEffectCard>
</div> </motion.div>
</PureLayout> </PureLayout>
</> </>
); );

View File

@ -8,15 +8,17 @@ import { toast } from 'react-hot-toast';
import { IoKeyOutline } from 'react-icons/io5'; import { IoKeyOutline } from 'react-icons/io5';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import logo from '@/assets/images/logo.png';
import key from '@/const/key'; import key from '@/const/key';
import HoverEffectCard from '@/components/effect_card'; import HoverEffectCard from '@/components/effect_card';
import { title } from '@/components/primitives'; import { title } from '@/components/primitives';
import { ThemeSwitch } from '@/components/theme-switch'; import { ThemeSwitch } from '@/components/theme-switch';
import logo from '@/assets/images/logo.png';
import WebUIManager from '@/controllers/webui_manager'; import WebUIManager from '@/controllers/webui_manager';
import PureLayout from '@/layouts/pure'; import PureLayout from '@/layouts/pure';
import { motion } from 'motion/react';
export default function WebLoginPage () { export default function WebLoginPage () {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
@ -150,7 +152,12 @@ export default function WebLoginPage () {
<> <>
<title>WebUI登录 - NapCat WebUI</title> <title>WebUI登录 - NapCat WebUI</title>
<PureLayout> <PureLayout>
<div className='w-[608px] max-w-full py-8 px-2 md:px-8 overflow-hidden'> <motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.5, type: "spring", stiffness: 120, damping: 20 }}
className='w-[608px] max-w-full py-8 px-2 md:px-8 overflow-hidden'
>
<HoverEffectCard <HoverEffectCard
className='items-center gap-4 pt-0 pb-6 bg-default-50' className='items-center gap-4 pt-0 pb-6 bg-default-50'
maxXRotation={3} maxXRotation={3}
@ -257,7 +264,7 @@ export default function WebLoginPage () {
</Button> </Button>
</CardBody> </CardBody>
</HoverEffectCard> </HoverEffectCard>
</div> </motion.div>
</PureLayout> </PureLayout>
</> </>
); );

View File

@ -6,15 +6,45 @@
body { body {
font-family: font-family:
'Aa偷吃可爱长大的', 'Quicksand',
PingFang SC, 'Nunito',
Helvetica Neue, 'Inter',
Microsoft YaHei, -apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
'PingFang SC',
'Microsoft YaHei',
sans-serif !important; sans-serif !important;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
font-smooth: always; font-smooth: always;
letter-spacing: 0.02em;
}
:root {
--heroui-primary: 217.2 91.2% 59.8%; /* 自然的现代蓝 */
--heroui-primary-foreground: 210 40% 98%;
--heroui-radius: 0.75rem;
--text-primary: 222.2 47.4% 11.2%;
--text-secondary: 215.4 16.3% 46.9%;
}
h1, h2, h3, h4, h5, h6 {
color: hsl(var(--text-primary));
letter-spacing: -0.02em;
}
.dark h1, .dark h2, .dark h3, .dark h4, .dark h5, .dark h6 {
color: hsl(210 40% 98%);
}
.dark {
--heroui-primary: 217.2 91.2% 59.8%;
--heroui-primary-foreground: 210 40% 98%;
} }
@layer components { @layer components {
@ -34,23 +64,29 @@ body {
} }
} }
::selection {
background-color: #ffcdba;
color: #fff;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 6px;
height: 8px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background-color: transparent; background-color: transparent;
-webkit-border-radius: 2em; border-radius: 3px;
-moz-border-radius: 2em;
border-radius: 2em;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: rgb(147, 147, 153, 0.5); background-color: rgba(255, 182, 193, 0.4); /* 浅粉色滚动条 */
-webkit-border-radius: 2em; border-radius: 3px;
-moz-border-radius: 2em; transition: all 0.3s;
border-radius: 2em; }
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 127, 172, 0.6);
} }
.monaco-editor { .monaco-editor {

View File

@ -1,122 +0,0 @@
import { PlayMode } from '@/const/enum';
import WebUIManager from '@/controllers/webui_manager';
import type {
FinalMusic,
Music163ListResponse,
Music163URLResponse,
} from '@/types/music';
/**
*
* @param id id
* @returns
*/
export const get163MusicList = async (id: string) => {
const res = await WebUIManager.proxy<Music163ListResponse>(
'https://wavesgame.top/playlist/track/all?id=' + id
);
// const res = await request.get<Music163ListResponse>(
// `https://wavesgame.top/playlist/track/all?id=${id}`
// )
if (res?.data?.code !== 200) {
throw new Error('获取歌曲列表失败');
}
return res.data;
};
/**
*
* @param ids id
* @returns
*/
export const getSongsURL = async (ids: number[]) => {
const _ids = ids.reduce((prev, cur, index) => {
const groupIndex = Math.floor(index / 10);
if (!prev[groupIndex]) {
prev[groupIndex] = [];
}
prev[groupIndex].push(cur);
return prev;
}, [] as number[][]);
const res = await Promise.all(
_ids.map(async (id) => {
const res = await WebUIManager.proxy<Music163URLResponse>(
`https://wavesgame.top/song/url?id=${id.join(',')}`
);
if (res?.data?.code !== 200) {
throw new Error('获取歌曲地址失败');
}
return res.data.data;
})
);
const result = res.reduce((prev, cur) => {
return prev.concat(...cur);
}, []);
return result;
};
/**
*
* @param id id
* @returns
*/
export const get163MusicListSongs = async (id: string) => {
const listRes = await get163MusicList(id);
const songs = listRes.songs.map((song) => song.id);
const songsRes = await getSongsURL(songs);
const finalMusic: FinalMusic[] = [];
for (let i = 0; i < listRes.songs.length; i++) {
const song = listRes.songs[i];
const music = songsRes.find((s) => s.id === song.id);
const songURL = music?.url;
if (songURL) {
finalMusic.push({
id: song.id,
url: songURL.replace(/http:\/\//, '//').replace(/https:\/\//, '//'),
title: song.name,
artist: song.ar.map((p) => p.name).join('/'),
cover: song.al.picUrl,
});
}
}
return finalMusic;
};
/**
*
* @param ids id
* @param currentId id
* @returns id
*/
export const getRandomMusic = (ids: number[], currentId: number): number => {
const randomIndex = Math.floor(Math.random() * ids.length);
const randomId = ids[randomIndex];
if (randomId === currentId) {
return getRandomMusic(ids, currentId);
}
return randomId;
};
/**
* id
* @param ids id
* @param currentId ID
* @param mode
*/
export const getNextMusic = (
musics: FinalMusic[],
currentId: number,
mode: PlayMode
): number => {
const ids = musics.map((music) => music.id);
if (mode === PlayMode.Loop) {
const currentIndex = ids.findIndex((id) => id === currentId);
const nextIndex = currentIndex + 1;
return ids[nextIndex] || ids[0];
}
if (mode === PlayMode.Random) {
return getRandomMusic(ids, currentId);
}
return currentId;
};

View File

@ -25,18 +25,32 @@ export default {
light: { light: {
colors: { colors: {
primary: { primary: {
DEFAULT: '#f31260', DEFAULT: '#FF7FAC', // 樱花粉
foreground: '#fff', foreground: '#fff',
50: '#fee7ef', 50: '#FFF0F5',
100: '#fdd0df', 100: '#FFE4E9',
200: '#faa0bf', 200: '#FFCDD9',
300: '#f871a0', 300: '#FF9EB5',
400: '#f54180', 400: '#FF7FAC',
500: '#f31260', 500: '#F33B7C',
600: '#c20e4d', 600: '#C92462',
700: '#920b3a', 700: '#991B4B',
800: '#610726', 800: '#691233',
900: '#310413', 900: '#380A1B',
},
secondary: {
DEFAULT: '#88C0D0', // 冰霜蓝
foreground: '#fff',
50: '#F0F9FC',
100: '#D7F0F8',
200: '#AEE1F2',
300: '#88C0D0',
400: '#5E9FBF',
500: '#4C8DAE',
600: '#3A708C',
700: '#2A546A',
800: '#1A3748',
900: '#0B1B26',
}, },
danger: { danger: {
DEFAULT: '#DB3694', DEFAULT: '#DB3694',

12167
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff