zpd-ui
v1.1.0
Published
A React component library with AI-friendly configuration guide
Maintainers
Readme
ZPD UI
一个基于 React + TypeScript 的组件库。
📦 安装
npm install zpd-ui
# 或者
pnpm add zpd-ui
# 或者
yarn add zpd-ui🚀 快速开始
💡 提示:如果你在使用 Cursor 或 GitHub Copilot,可以查看 AI Skills 配置指南,通过简单的提示词即可生成完整的组件配置代码!
1️⃣ 在应用入口引入样式(推荐)
// main.tsx 或 index.tsx
import 'zpd-ui/styles'; // 👈 只需引入一次
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);💡 提示:样式文件已包含移动端优化(iOS 回弹禁用、滚动条隐藏、表单元素重置等),专为 H5 场景设计。
2️⃣ 在组件中使用
// App.tsx 或任何组件
import { Button, MusicPlayer, AudioEffect } from 'zpd-ui';
import { useAudio, audioManager } from 'zpd-ui';
function App() {
const { play } = useAudio();
return (
<div>
<Button variant="primary" size="medium" onClick={() => play('/click.mp3')}>
点击我
</Button>
<MusicPlayer
musicUrl="/music.mp3"
playIconClass="icon-play"
pauseIconClass="icon-pause"
/>
</div>
);
}💡 提示: 样式只需在入口文件引入一次,其他地方直接使用组件即可。详见 样式引入指南
📚 组件列表
Barrage 弹幕
弹幕组件,支持自动播放和手动控制。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| list | BarrageBullet[] | - | 弹幕列表 |
| intervalTime | number | 1300 | 弹幕间隔时间(毫秒) |
| className | string | '' | 容器类名 |
| tabClassName | string | '' | 弹幕项类名 |
| renderItem | (item: BarrageBullet) => React.ReactNode | - | 渲染函数 |
示例
<Barrage
list={barrageList}
intervalTime={1300}
renderItem={(item) => <span>{item.text}</span>}
/>CountDown 倒计时
倒计时组件,支持自定义格式和服务器时间同步。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| targetTime | number \| string \| null \| undefined | - | 目标时间戳(毫秒) |
| serverTime | number | - | 服务器时间戳(毫秒) |
| format | string | 'HH:mm:ss' | 格式化字符串(dd/HH/mm/ss) |
| className | string | - | 容器类名 |
| refreshData | () => void | - | 倒计时结束回调 |
| backgroundClassName | string | '' | 背景类名 |
| partClassName | string | '' | 部分类名 |
示例
<CountDown
className={Images.time_bg_png}
targetTime={newer_end_ts ? newer_end_ts * 1000 : 0}
format="dd'D'HH:mm:ss"
refreshData={() => countDownEnd()}
/>List 列表
列表组件,支持分页加载、无限滚动、游标分页、简单渲染和数据填充等功能。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| url | string | - | 请求地址 |
| params | Record<string, unknown> | {} | 请求参数 |
| method | 'GET' \| 'POST' | 'get' | 请求方法 |
| pageSize | number | 10 | 每页数量 |
| dataKey | string | 'items' | 数据字段名 |
| className | string | - | 容器类名 |
| renderItem | (item: T, index: number) => React.ReactNode | - | 渲染函数 |
| onDataLoaded | (data: unknown, list: T[], myRank: T) => void | - | 数据加载回调 |
| finishRender | () => React.ReactNode | - | 加载完成后的渲染函数 |
| noDataRender | () => React.ReactNode | - | 无数据时的渲染函数 |
| cursorKey | string | - | 游标分页参数名(如 'last_id') |
| hasNextKey | string | 'has_next' | 是否有下一页的字段名 |
| myRankKey | string | 'my_rank' | 我的排名的字段名 |
| children | React.ReactNode | - | 子元素 |
| request | (options: any) => Promise<ApiResponse> | - | 自定义请求函数(优先使用,否则使用 window.ZPD_REQUEST.request) |
| useSimpleRender | boolean | false | 使用简单渲染模式(不使用滚动容器),适用于一次性加载全部数据 |
| enablePadding | boolean | false | 启用数据填充,仅在没有下一页且当前数据不足时填充占位数据 |
| paddingThreshold | number | pageSize | 填充阈值,当数据量 < 此值时才填充 |
| paddingPlaceholder | T | {} | 填充的占位数据 |
渲染模式
容器滚动模式(默认):
- 列表在固定高度容器内滚动
- 支持自动无限加载更多
- 适用于大部分列表场景
简单渲染模式(useSimpleRender={true}):
- 列表直接渲染,自然撑开页面
- 基于页面滚动
- 适用于一次性加载全部数据或手动控制加载的场景
示例
// 基础用法 - 容器内无限滚动
<List
ref={listRef}
url="/api/users"
params={{ status: 'active' }}
request={request}
renderItem={(item: User) => <UserCard data={item} />}
onDataLoaded={(data, list) => {
console.log('加载完成', list.length)
}}
/>
// 简单渲染模式 - 一次性加载全部数据
<List
url="/api/all-ranks"
useSimpleRender={true}
request={request}
renderItem={(item: Rank) => <RankCard data={item} />}
/>
// 数据填充 - 最后一页数据不足时自动填充
<List
url="/api/top10"
pageSize={10}
enablePadding={true}
paddingThreshold={10}
paddingPlaceholder={{ name: '虚位以待', score: 0 }}
request={request}
renderItem={(item: Rank, index) => (
<div>#{index + 1} {item.name}</div>
)}
/>
// 简单渲染 + 数据填充 - 固定展示数量
<List
url="/api/featured"
useSimpleRender={true}
enablePadding={true}
paddingThreshold={5}
paddingPlaceholder={{ name: '暂无' }}
request={request}
renderItem={(item) => <Card data={item} />}
/>
// 游标分页 - 高性能场景
<List
url="/api/messages"
cursorKey="last_id"
dataKey="messages"
request={request}
renderItem={(item) => <MessageItem data={item} />}
/>MusicPlayer 音乐播放器
音乐播放器组件,支持自动播放、循环播放和动态切换音频源。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| musicUrl | string | - | 音乐文件 URL |
| playIconClass | string | - | 播放状态图标类名 |
| pauseIconClass | string | - | 暂停状态图标类名 |
| className | string | - | 额外容器类名 |
| loop | boolean | true | 是否循环播放 |
| autoPlay | boolean | true | 是否自动播放 |
特性
- ✅ 三层自动播放策略(直接播放 → 静音播放 → 等待用户交互)
- ✅ 支持动态切换音频源
- ✅ 适合背景音乐场景
示例
// 基础用法
<MusicPlayer
musicUrl="/music.mp3"
playIconClass="icon-play"
pauseIconClass="icon-pause"
/>
// 根据状态切换音乐
<MusicPlayer
musicUrl={status === 'active' ? '/music1.mp3' : '/music2.mp3'}
playIconClass="icon-play"
pauseIconClass="icon-pause"
loop={true}
/>AudioEffect 音效组件
无 UI 的音效播放组件,适用于状态音效、提示音等短音频场景。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| audioUrl | string | - | 音频文件路径 |
| volume | number | 1 | 音量 (0-1) |
| autoPlay | boolean | true | 是否自动播放 |
| onPlay | () => void | - | 播放开始回调 |
| onEnded | () => void | - | 播放完成回调 |
| onError | (error: Error) => void | - | 播放失败回调 |
特性
- ✅ 基于 AudioManager 实现,支持预加载和缓存
- ✅ 支持并发播放(快速点击不会打断)
- ✅ 自动清理 DOM,避免内存泄漏
- ✅ 支持动态切换音频源
示例
// 根据状态播放不同音效
<AudioEffect
audioUrl={status === 'success' ? '/sounds/success.mp3' : '/sounds/error.mp3'}
volume={0.8}
onEnded={() => console.log('播放完成')}
/>
// 手动控制播放
<AudioEffect
audioUrl="/sounds/notification.mp3"
autoPlay={false}
onPlay={() => console.log('开始播放')}
/>NavBar 导航栏
导航栏组件,支持标题居中/左对齐、滚动变色、自定义左右侧内容等功能。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| title | string | - | 标题 |
| leftIcon | string | '' | 左侧图标类名 |
| rightIcon | string | '' | 右侧图标类名 |
| onRightClick | () => void | - | 右侧点击回调 |
| onLeftClick | () => void | - | 左侧点击回调 |
| type | 'back' \| 'close' | 'close' | 左侧按钮类型 |
| className | string | '' | 容器类名 |
| children | React.ReactNode | - | 自定义内容 |
| enableScrollBg | boolean | false | 是否启用滚动变色 |
| scrollBgClassName | string | 'bg-black/60 backdrop-blur-sm' | 滚动时样式类名 |
| titleAlign | 'center' \| 'left' | 'center' | 标题对齐方式 |
| renderLeft | () => React.ReactNode | - | 自定义左侧内容(优先级高于 leftIcon) |
| renderRight | () => React.ReactNode | - | 自定义右侧内容(优先级高于 rightIcon) |
示例
// 基础用法 - 标题居中
<NavBar
title="页面标题"
leftIcon="i-custom-back"
rightIcon="i-custom-more"
onLeftClick={() => history.back()}
enableScrollBg={true}
/>
// 左对齐模式 - 左侧图标和标题紧挨着
<NavBar
title="消息列表"
leftIcon="i-custom-back"
rightIcon="i-custom-search"
titleAlign="left"
/>
// 自定义右侧内容 - 多个操作按钮
<NavBar
title="购物车"
leftIcon="i-custom-back"
renderRight={() => (
<div className="flex items-center gap-16">
<span className="text-24 text-gray-400" onClick={() => console.log('管理')}>
管理
</span>
<div className="i-custom-search" onClick={() => console.log('搜索')} />
</div>
)}
/>
// 完全自定义左右两侧
<NavBar
title="设置"
titleAlign="left"
renderLeft={() => (
<button className="flex items-center gap-8" onClick={() => history.back()}>
<div className="i-custom-back" />
<span className="text-24">返回</span>
</button>
)}
renderRight={() => (
<div className="flex items-center gap-16">
<button className="px-16 py-8 bg-blue-500 rounded-8 text-white">
保存
</button>
<div className="i-custom-more" />
</div>
)}
/>NoticeBar 通知栏
通知栏组件,支持无限滚动和悬停暂停。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| children | React.ReactNode | - | 子元素内容 |
| text | string[] \| string | - | 文本数组或字符串 |
| textClassName | string | - | 文本类名 |
| pixelsPerSecond | number | 25 | 滚动速度(像素/秒) |
| speed | number | 8 | 动画持续时间(秒) |
| pauseOnHover | boolean | false | 是否悬停暂停 |
| gap | string | - | 滚动内容间距 |
示例
<NoticeBar text="通知内容" pauseOnHover />
<NoticeBar>
<span>自定义内容</span>
</NoticeBar>Progress 进度条
进度条组件,支持拖拽和按钮控制。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| value | number | - | 当前数值 |
| maxCount | number | - | 最大数量 |
| onChange | (value: number) => void | - | 改变回调 |
| disabled | boolean | false | 是否禁用 |
| draggable | boolean | true | 是否允许拖拽 |
| innerImageClassName | string | '' | 内层进度条背景类名 |
| outerImageClassName | string | '' | 外层轨道背景类名 |
| pointClassName | string | '' | 进度条指示点类名 |
| decreaseButtonClassName | string | - | 减少按钮类名 |
| increaseButtonClassName | string | - | 增加按钮类名 |
| step | number | 1 | 步长 |
示例
<Progress
value={count}
maxCount={maxExchangeCount}
innerImageClassName={BackgroundImages.shop_inner_bg_png}
outerImageClassName={BackgroundImages.shop_process_out_bg_png}
decreaseButtonClassName={BackgroundImages.shop_reduce_png}
increaseButtonClassName={BackgroundImages.shop_add_png}
onChange={setCount}
/>RankList 排行榜列表
排行榜列表组件,支持顶部展示、我的排名、数据填充等功能。自动将前 N 名单独渲染在顶部区域。 继承 List 组件的所有功能(分页、简单渲染、数据填充、游标分页等)。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| topRender | (topList: T[]) => React.ReactNode | - | 顶部渲染函数 |
| topCount | number | 3 | 顶部数量(设为 0 则不渲染顶部区域) |
| renderItem | (item: T, index: number) => React.ReactNode | - | 列表项渲染函数 |
| myRankRender | (myRank: T) => React.ReactNode | - | 我的排名渲染函数 |
| className | string | '' | 容器类名 |
| listClassName | string | '' | 列表区域类名 |
| listStyle | CSSProperties | {} | 列表区域样式 |
| classNameStyle | CSSProperties | {} | 容器样式 |
| ...ListProps | - | - | 继承 List 组件的所有属性(url, params, useSimpleRender, enablePadding 等) |
Ref 方法
| 方法 | 参数 | 返回值 | 描述 |
|------|------|--------|------|
| refresh | - | void | 刷新列表 |
| clear | - | void | 清空列表 |
| getList | - | T[] | 获取完整列表 |
| getTopList | - | T[] | 获取 Top 列表 |
| getRestList | - | T[] | 获取剩余列表(去除 Top) |
示例
// 基础用法 - 容器内滚动 + 前 3 名单独展示
<RankList
url="/api/ranks"
dataKey="ranks"
request={request}
topCount={3}
listClassName="h-600"
topRender={topList => (
<div className="mb-20 flex justify-around">
{topList.map((item, index) => (
<div key={index} className="flex flex-col items-center">
<img src={item.avatar} className="h-80 w-80 rounded-full" />
<span className="text-20">Top {index + 1}</span>
<span className="text-24 font-bold">{item.name}</span>
</div>
))}
</div>
)}
renderItem={(item, index) => (
<div className="flex items-center justify-between px-20 py-16">
<span className="text-gray-400">#{index + 1}</span>
<span>{item.name}</span>
<span className="text-24 font-bold">{item.score}</span>
</div>
)}
/>
// 简单渲染模式 - 一次性加载全部排行数据
<RankList
url="/api/all-ranks"
dataKey="ranks"
request={request}
useSimpleRender={true}
topCount={3}
topRender={topList => <TopRankCard list={topList} />}
renderItem={(item, index) => <RankItem data={item} />}
/>
// 数据填充 - 最后一页不足时自动填充
<RankList
url="/api/ranks"
dataKey="ranks"
request={request}
pageSize={10}
enablePadding={true}
paddingThreshold={10}
paddingPlaceholder={{ name: '虚位以待', score: 0 }}
topCount={3}
topRender={topList => <TopRankCard list={topList} />}
renderItem={(item, index) => <RankItem data={item} />}
/>
// 简单渲染 + 数据填充 - 固定展示 10 个排名位
<RankList
url="/api/top10"
dataKey="ranks"
request={request}
useSimpleRender={true}
enablePadding={true}
paddingThreshold={10}
paddingPlaceholder={{ name: '暂无', score: 0 }}
topCount={3}
topRender={topList => <TopThree list={topList} />}
renderItem={(item, index) => (
<div>#{index + 1} {item.name || '暂无'}</div>
)}
/>
// 不渲染 Top 区域
<RankList
url="/api/ranks"
request={request}
topCount={0}
renderItem={(item, index) => <RankItem data={item} />}
/>
// 带我的排名
<RankList
url="/api/ranks"
request={request}
topCount={3}
listClassName="h-600"
topRender={topList => <TopRankCard list={topList} />}
renderItem={(item, index) => <RankItem data={item} />}
myRankRender={myRank => (
<div className="fixed bottom-0 left-0 w-full bg-gradient-to-t from-black/80 px-20 py-16">
<div className="flex items-center justify-between">
<span>我的排名:#{myRank.rank}</span>
<span className="text-24 font-bold">{myRank.score}</span>
</div>
</div>
)}
/>
// 使用 ref 方法
const rankListRef = useRef<RankListRef>(null)
<RankList
ref={rankListRef}
url="/api/ranks"
request={request}
renderItem={(item) => <RankItem data={item} />}
/>
// 刷新列表
<button onClick={() => rankListRef.current?.refresh()}>
刷新
</button>
// 获取列表数据
const topList = rankListRef.current?.getTopList()
const fullList = rankListRef.current?.getList()ShadowText 阴影文字
阴影文字组件,支持自定义阴影颜色和偏移。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| children | React.ReactNode | - | 子元素 |
| className | string | '' | 类名 |
| shadowColor | string | '#FFEE7B' | 阴影颜色 |
| shadowOffset | number | 2 | 阴影偏移 |
| useResponsive | boolean | true | 是否使用响应式 |
示例
<ShadowText shadowColor="#FFEE7B" shadowOffset={2}>
阴影文字
</ShadowText>StrokeText 描边文字
描边文字组件,使用 SVG 实现。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| text | string | - | 文本内容 |
| strokeWidth | number | 4 | 描边宽度 |
| fontSize | number | 30 | 字体大小 |
| stroke | string | '#F57E34' | 描边颜色 |
| className | string | '' | 类名 |
示例
<StrokeText
text="描边文字"
strokeWidth={4}
fontSize={30}
stroke="#F57E34"
/>TabList 标签列表
标签列表组件,支持自定义样式。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| tabs | TabItem[] | - | 标签列表 |
| activeValue | string \| number | - | 激活值 |
| onTabClick | (value: string \| number, index: number) => void | - | 点击回调 |
| tabClassName | string | - | 标签类名 |
| activeTabClassName | string | - | 激活标签类名 |
| renderTab | (tab: TabItem, index: number, isActive: boolean) => React.ReactNode | - | 自定义渲染 |
示例
<TabList
tabs={[
{ value: 1, label: '标签1' },
{ value: 2, label: '标签2' },
]}
activeValue={activeValue}
onTabClick={setActiveValue}
tabClassName="text-24 mx-10"
activeTabClassName="text-red"
/>Tabs 可滚动标签
可滚动标签组件,支持自动滚动到激活标签。
Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| tabs | ScrollableTabsItem[] | - | 标签列表 |
| activeValue | string \| number | - | 激活值 |
| onTabClick | (value: string \| number, index: number) => void | - | 点击回调 |
| scrollDelay | number | 50 | 滚动延迟(毫秒) |
| disableAutoScroll | boolean | false | 禁用自动滚动 |
| tabClassName | string | '' | 标签类名 |
| activeTabClassName | string | '' | 激活标签类名 |
示例
<Tabs
tabs={tabs}
activeValue={activeValue}
onTabClick={setActiveValue}
tabClassName="text-24 mx-10"
activeTabClassName="text-red"
/>🎣 Hooks
useAudio
音频播放 Hook,基于 AudioManager 的 React Hook 封装,适用于复杂的音频控制场景。
参数
interface UseAudioOptions {
volume?: number // 音量 (0-1),默认 1
preloadUrls?: string[] // 预加载的音频列表
clearOnUnmount?: boolean // 是否在组件卸载时清理缓存
}返回值
interface UseAudioReturn {
play: (audioUrl: string, volume?: number, callbacks?: {
onEnded?: () => void
onError?: (err: Error) => void
}) => Promise<void>
preload: (audioUrl: string) => Promise<void>
preloadAll: (audioUrls: string[]) => Promise<void>
isCached: (audioUrl: string) => boolean
stopAll: () => void
clear: () => void
isLoading: boolean
}示例
import { useAudio } from 'zpd-ui';
function GameComponent() {
const { play, preloadAll, isLoading } = useAudio({
volume: 0.8,
preloadUrls: ['/sounds/click.mp3', '/sounds/success.mp3']
});
// 预加载完成后播放
useEffect(() => {
if (!isLoading) {
console.log('音频预加载完成');
}
}, [isLoading]);
return (
<div>
<button onClick={() => play('/sounds/click.mp3')}>
点击播放音效
</button>
<button onClick={() => play('/sounds/success.mp3', 1, {
onEnded: () => console.log('播放完成')
})}>
播放成功音效
</button>
</div>
);
}🛠️ 工具函数
zpd-ui 还提供了一些实用的工具函数:
音频管理
import { audioManager, playSound } from 'zpd-ui';
// 方式1: 使用 audioManager 单例
await audioManager.preloadAll(['/sounds/click.mp3', '/sounds/success.mp3']);
await audioManager.play('/sounds/click.mp3', 0.8);
// 方式2: 使用简化 API
await playSound('/sounds/click.mp3', 0.8, {
onEnded: () => console.log('播放完成'),
onError: (err) => console.error('播放失败', err)
});
// 检查是否已缓存
if (audioManager.isCached('/sounds/click.mp3')) {
console.log('音频已缓存');
}
// 清理所有缓存
audioManager.clear();AudioManager API
| 方法 | 参数 | 描述 |
|------|------|------|
| preload(filename) | filename: string | 预加载单个音频 |
| preloadAll(filenames) | filenames: string[] | 批量预加载音频 |
| play(filename, volume, callbacks) | filename: string, volume?: number, callbacks?: object | 播放音效(支持并发) |
| stopAll() | - | 停止所有音频 |
| clear() | - | 清除所有缓存 |
| isCached(filename) | filename: string | 检查是否已缓存 |
| setAssetOrigin(origin) | origin: string | 设置资源地址前缀 |
其他工具
import { classNames, debounce, throttle } from 'zpd-ui';
// 类名合并
const className = classNames('btn', isActive && 'active');
// 防抖
const handleSearch = debounce((value: string) => {
console.log('搜索:', value);
}, 300);
// 节流
const handleScroll = throttle(() => {
console.log('滚动');
}, 100);更多示例请查看 使用示例文档
🛠️ 开发
安装依赖
pnpm install启动开发服务器
pnpm dev构建
pnpm build这会生成:
dist/zpd-ui.es.js- ES Module 格式dist/zpd-ui.cjs.js- CommonJS 格式dist/zpd-ui.css- 样式文件dist/index.d.ts及相关 - TypeScript 类型声明文件
Lint
pnpm lint📝 发布
- 更新版本号:
npm version patch # 或 minor, major- 发布到 npm:
npm publish注意: 首次发布前,请确保:
- 已登录 npm 账号 (
npm login)- 在
package.json中配置了正确的name、author、repository等字段- 包名在 npm 上是可用的
🎵 音频系统使用指南
zpd-ui 提供了完整的音频管理解决方案,根据不同场景选择合适的方式:
| 场景 | 推荐方案 | 说明 |
|------|----------|------|
| 背景音乐 | <MusicPlayer /> | 有 UI 控制,支持循环和动态切换 |
| 状态音效(声明式) | <AudioEffect /> | 根据状态自动播放,无 UI |
| 交互音效(命令式) | useAudio() Hook | 更灵活的控制,适合复杂交互 |
| 底层控制 | audioManager | 直接操作音频缓存和播放 |
典型组合:
// 页面中同时使用多种音频
function GamePage() {
const { play } = useAudio({
preloadUrls: ['/click.mp3', '/success.mp3', '/error.mp3']
});
return (
<>
{/* 背景音乐 */}
<MusicPlayer musicUrl="/bg-music.mp3" playIconClass="..." pauseIconClass="..." />
{/* 状态音效 */}
<AudioEffect
audioUrl={gameStatus === 'win' ? '/success.mp3' : '/error.mp3'}
volume={0.8}
/>
{/* 交互音效 */}
<button onClick={() => play('/click.mp3')}>点击</button>
</>
);
}📖 文档
- AI Skills 配置指南 - 🤖 专为 AI 助手设计的组件配置规范(Cursor/Copilot 适用)
- 使用示例 - 组件使用示例和最佳实践
✨ 特性
- 🎨 精美的组件设计
- 📦 Tree-shaking 支持,按需加载
- 💪 使用 TypeScript 编写,完整的类型定义
- 🔧 提供实用的工具函数
- 🎵 完整的音频管理体系(播放器、音效、Hook、管理器)
- 📱 响应式设计
- ⚡ 基于 Vite 构建,开发体验极佳
📄 License
MIT
