npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ohkit/draggable-box

v0.0.4

Published

可拖拽容器

Readme

@ohkit/draggable-box

🚀 功能强大的可拖拽容器组件

一个灵活的拖拽容器组件,支持方向锁定、边界限制、多种定位模式和 Transform 缩放支持,适用于悬浮窗口、工具栏、侧边栏等各种需要拖拽交互的场景。

✨ 特性亮点

  • 🎯 灵活拖拽: 支持窗口范围内的自由拖拽
  • 🔒 方向锁定: 可锁定水平或垂直方向拖动
  • 🎚️ 边界控制: 智能边界限制,支持相对和绝对边界
  • 📐 多种定位: 支持四角定位,适应不同布局需求
  • 🔄 定位模式: 支持 fixed 和 absolute 两种定位模式
  • 📏 Transform 支持: 自动适应父容器的 transform 缩放
  • 可访问性: 支持禁用状态,提升用户体验
  • 🎨 样式自定义: 支持自定义类名和样式覆盖
  • 📱 响应式: 自适应窗口大小变化

📦 安装

npm install @ohkit/draggable-box

🚀 快速开始

import React from 'react';
import { DraggableBox } from '@ohkit/draggable-box';
import '@ohkit/draggable-box/dist/index.css';

function App() {
  return (
    <div style={{ height: '100vh', position: 'relative' }}>
      {/* 基本用法 - 自由拖拽 */}
      <DraggableBox>
        <div style={{ padding: '20px', background: '#f0f0f0', borderRadius: '8px' }}>
          我可以自由拖拽!
        </div>
      </DraggableBox>
    </div>
  );
}

📖 详细使用

方向锁定

function App() {
  return (
    <div style={{ height: '100vh', position: 'relative' }}>
      {/* 锁定水平拖拽 */}
      <DraggableBox lockAxis="x" placement="top-left">
        <div style={{ padding: '16px', background: '#e3f2fd', borderRadius: '6px' }}>
          我只能水平拖拽
        </div>
      </DraggableBox>
      
      {/* 锁定垂直拖拽 */}
      <DraggableBox lockAxis="y" placement="top-right">
        <div style={{ padding: '16px', background: '#f3e5f5', borderRadius: '6px' }}>
          我只能垂直拖拽
        </div>
      </DraggableBox>
    </div>
  );
}

边界限制

边界限制基于 placement 属性进行相对定位,智能计算有效拖动范围:

function App() {
  return (
    <div style={{ height: '100vh', position: 'relative' }}>
      {/* 左上角相对边界 */}
      <DraggableBox 
        placement="top-left" 
        offsetX={50} 
        offsetY={50}
        boundsX={[20, 300]}  // 左边最小20px,最大300px
        boundsY={[20, 200]}  // 顶边最小20px,最大200px
      >
        <div style={{ padding: '12px', background: '#fff3e0' }}>
          左上角边界限制
        </div>
      </DraggableBox>

      {/* 右下角相对边界 */}
      <DraggableBox 
        placement="bottom-right" 
        offsetX={50} 
        offsetY={50}
        boundsX={[100, 500]}  // 右边最小100px,最大500px
        boundsY={[50, 300]}   // 底边最小50px,最大300px
      >
        <div style={{ padding: '12px', background: '#e8f5e8' }}>
          右下角边界限制
        </div>
      </DraggableBox>
    </div>
  );
}

定位模式

组件支持两种定位模式,适应不同的使用场景:

Fixed 模式(默认)

适用于大多数场景,自动适应父容器的 transform 属性:

function FixedModeExample() {
  return (
    <div style={{ height: '100vh', transform: 'scale(0.8)' }}>
      {/* 在有 transform 的父容器中正常工作 */}
      <DraggableBox positionMode="fixed">
        <div style={{ padding: '16px', background: '#e3f2fd' }}>
          Fixed 模式 - 自动适应 transform
        </div>
      </DraggableBox>
    </div>
  );
}

Absolute 模式

适用于需要在定位容器内拖拽的场景:

function AbsoluteModeExample() {
  return (
    <div style={{ position: 'relative', width: '600px', height: '400px', border: '2px solid #52c41a' }}>
      {/* 在定位容器内拖拽 */}
      <DraggableBox positionMode="absolute" placement="top-left">
        <div style={{ padding: '16px', background: '#f6ffed' }}>
          Absolute 模式 - 在定位容器内
        </div>
      </DraggableBox>
    </div>
  );
}

拖拽区域可视化

启用 showDragArea 可以在拖拽时显示可拖拽范围:

function DragAreaExample() {
  return (
    <DraggableBox 
      placement="bottom-right"
      boundsX={[50, 300]}
      boundsY={[50, 200]}
      showDragArea={true}
    >
      <div style={{ padding: '16px', background: '#fff7e6' }}>
        拖拽时显示可拖拽区域
      </div>
    </DraggableBox>
  );
}

组合功能

function App() {
  return (
    <div style={{ height: '100vh', position: 'relative' }}>
      {/* 水平锁定 + 边界限制 */}
      <DraggableBox 
        lockAxis="x"
        placement="top-left"
        boundsX={[50, 400]}
        zIndex={10000}
      >
        <div style={{ 
          padding: '15px', 
          background: 'linear-gradient(45deg, #ff6b6b, #feca57)',
          color: 'white',
          borderRadius: '8px'
        }}>
          水平锁定 + 边界限制
        </div>
      </DraggableBox>

      {/* 禁用状态 */}
      <DraggableBox 
        placement="bottom-right"
        disabled={true}
      >
        <div style={{ padding: '12px', background: '#bdbdbd', color: '#666' }}>
          禁用的拖拽框
        </div>
      </DraggableBox>
    </div>
  );
}

高级用法 - 浮动工具栏

function FloatingToolbar() {
  const [visible, setVisible] = useState(true);

  return (
    <DraggableBox 
      placement="top-right"
      offsetX={20}
      offsetY={100}
      boundsX={[10, 500]}
      boundsY={[50, window.innerHeight - 200]}
      zIndex={9999}
    >
      <div style={{ 
        padding: '10px 15px', 
        background: 'white',
        borderRadius: '25px',
        boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
        border: '1px solid #e0e0e0'
      }}>
        <div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
          <button onClick={() => setVisible(!visible)}>
            {visible ? '隐藏' : '显示'}
          </button>
          <span>浮动工具栏</span>
        </div>
      </div>
    </DraggableBox>
  );
}

Transform 缩放支持

组件自动适应父容器的 transform 缩放,无需额外配置:

function TransformExample() {
  return (
    <div style={{ 
      width: '600px', 
      height: '400px', 
      border: '2px solid #1890ff',
      transform: 'scale(0.8)',
      transformOrigin: 'top left',
      padding: '20px',
      background: '#f0f0f0'
    }}>
      <p style={{ marginBottom: '20px', color: '#666' }}>
        父容器有 transform: scale(0.8),组件自动适应
      </p>
      <DraggableBox 
        placement="bottom-right"
        offsetX={50}
        offsetY={50}
        boundsX={[20, 300]}
        boundsY={[20, 200]}
        showDragArea={true}
      >
        <div style={{ padding: '16px', background: '#e6f7ff' }}>
          在 transform 父元素中正常拖拽
        </div>
      </DraggableBox>
    </div>
  );
}

📋 API 参考

DraggableBoxProps

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | className | string | '' | 自定义 CSS 类名 | | children | ReactNode | - | 拖拽框内容 | | zIndex | number | 9999 | z-index 层级 | | offsetX | number | 20 | 初始位置横向偏移量(px) | | offsetY | number | 20 | 初始位置纵向偏移量(px) | | disabled | boolean | false | 是否禁用拖拽功能 | | placement | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'bottom-right' | 初始定位位置 | | lockAxis | 'none' | 'x' | 'y' | 'none' | 拖拽方向锁定 | | boundsX | [number?, number?] | - | X轴边界范围 [min, max] | | boundsY | [number?, number?] | - | Y轴边界范围 [min, max] | | positionMode | 'fixed' | 'absolute' | 'fixed' | 定位模式 | | showDragArea | boolean | false | 是否显示拖拽区域可视化 | | showDragAreaOverMoveDistanse | number | 5 | 超过多少像素移动距离才显示拖拽区域 | | onDragStart | (positionChange: IPositionChange) => void | - | 拖拽开始回调函数 | | onDrag | (positionChange: IPositionChange) => void | - | 拖拽中回调函数 | | onDragEnd | (positionChange: IPositionChange) => void | - | 拖拽结束回调函数 |

💡 回调函数说明

DraggableBox 提供了三个拖拽生命周期回调函数,方便实时监控拖拽状态:

onDragStart(positionChange: IPositionChange)

  • 在拖拽开始时触发
  • positionChange 包含:
    • left: 起始位置的左侧坐标
    • top: 起始位置的顶部坐标
    • right: 起始位置的右侧坐标
    • bottom: 起始位置的底部坐标
    • diffX: X 轴偏移量(始终为 0)
    • diffY: Y 轴偏移量(始终为 0)

onDrag(positionChange: IPositionChange)

  • 在拖拽过程中实时触发
  • positionChange 包含:
    • left: 当前实时位置的左侧坐标
    • top: 当前实时位置的顶部坐标
    • right: 当前实时位置的右侧坐标
    • bottom: 当前实时位置的底部坐标
    • diffX: 相对于起始位置的 X 轴偏移量
    • diffY: 相对于起始位置的 Y 轴偏移量

onDragEnd(positionChange: IPositionChange)

  • 在拖拽结束时触发
  • positionChange 包含:
    • left: 最终位置的左侧坐标
    • top: 最终位置的顶部坐标
    • right: 最终位置的右侧坐标
    • bottom: 最终位置的底部坐标
    • diffX: 相对于起始位置的 X 轴总偏移量
    • diffY: 相对于起始位置的 Y 轴总偏移量

IPositionChange 接口

interface IPositionChange {
  left: number;   // 左侧坐标(相对于容器左侧)
  top: number;    // 顶部坐标(相对于容器顶部)
  right: number;  // 右侧坐标(相对于容器右侧)
  bottom: number; // 底部坐标(相对于容器底部)
  diffX: number;  // X 轴偏移量(相对于起始位置)
  diffY: number;  // Y 轴偏移量(相对于起始位置)
}

💡 定位模式说明

positionMode="fixed"(默认)

  • 使用 position: fixed 定位
  • 自动查找影响 fixed 定位的父元素(transform/filter/perspective)
  • 适用于大多数场景,特别是父容器有 transform 时
  • 组件会自动适应父容器的缩放

positionMode="absolute"

  • 使用 position: absolute 定位
  • 基于最近的非 static 定位父元素
  • 适用于需要在特定定位容器内拖拽的场景
  • 父容器需要有 position: relativeabsolutefixed

💡 边界说明

边界值基于 placement 属性进行智能计算:

单边边界约束

除了完整的边界范围,DraggableBox 还支持单边边界约束,可以只设置最小边界或最大边界:

// 只有最小边界约束
<DraggableBox 
  placement="bottom-right"
  boundsX={[100, undefined]}  // 右边最小距离100px,无最大限制
  boundsY={[50, undefined]}   // 底边最小距离50px,无最大限制
>
  单边最小边界约束
</DraggableBox>

// 只有最大边界约束
<DraggableBox 
  placement="top-left"
  boundsX={[undefined, 200]}  // 左边最大距离200px,无最小限制
  boundsY={[undefined, 150]}  // 顶边最大距离150px,无最小限制
>
  单边最大边界约束
</DraggableBox>

// 混合单边约束
<DraggableBox 
  placement="bottom-right"
  boundsX={[100, undefined]}  // X轴只有最小边界
  boundsY={[undefined, 30]}   // Y轴只有最大边界
>
  混合单边约束
</DraggableBox>

placement="top-left" 时:

  • boundsX=[min, max]: 左边最小距离 - 左边最大距离
  • boundsY=[min, max]: 顶边最小距离 - 顶边最大距离

placement="bottom-right" 时:

  • boundsX=[min, max]: 右边最小距离 - 右边最大距离
  • boundsY=[min, max]: 底边最小距离 - 底边最大距离

placement="top-right" 时:

  • boundsX=[min, max]: 右边最小距离 - 右边最大距离
  • boundsY=[min, max]: 顶边最小距离 - 顶边最大距离

placement="bottom-left" 时:

  • boundsX=[min, max]: 左边最小距离 - 左边最大距离
  • boundsY=[min, max]: 底边最小距离 - 底边最大距离

💡 最佳实践

1. 边界配置建议

// ✅ 推荐:合理设置边界,确保用户体验
<DraggableBox 
  placement="top-right"
  boundsX={[20, 400]}  // 右边距离20px-400px
  boundsY={[50, 300]}  // 顶边距离50px-300px
>
  优化边界配置
</DraggableBox>

// ❌ 避免:边界设置不合理
<DraggableBox boundsX={[0, 10]}>边界太窄</DraggableBox>

2. 性能优化

// ✅ 推荐:合理的初始位置
<DraggableBox placement="bottom-right" offsetX={20} offsetY={20}>
  合适的初始位置
</DraggableBox>

// ✅ 推荐:单边边界约束使用场景
<DraggableBox boundsX={[100, undefined]}>
  {/* 确保组件不贴边,但又给予最大的移动自由 */}
</DraggableBox>

// ✅ 推荐:在需要时启用拖拽
<DraggableBox disabled={!isEditing}>
  仅在编辑模式下可拖拽
</DraggableBox>

3. 定位模式选择

// ✅ 推荐:大多数场景使用 fixed 模式
<DraggableBox positionMode="fixed">
  {/* 自动适应父容器的 transform */}
</DraggableBox>

// ✅ 推荐:在定位容器内使用 absolute 模式
<div style={{ position: 'relative' }}>
  <DraggableBox positionMode="absolute">
    {/* 在定位容器内拖拽 */}
  </DraggableBox>
</div>

4. Transform 缩放场景

// ✅ 推荐:在有 transform 的父容器中使用 fixed 模式
<div style={{ transform: 'scale(0.8)' }}>
  <DraggableBox positionMode="fixed">
    {/* 组件会自动适应缩放 */}
  </DraggableBox>
</div>

// ✅ 推荐:启用拖拽区域可视化以查看可拖拽范围
<DraggableBox 
  positionMode="fixed"
  boundsX={[50, 300]}
  boundsY={[50, 200]}
  showDragArea={true}
>
  {/* 拖拽时显示可拖拽区域 */}
</DraggableBox>

// ✅ 推荐:调整拖拽区域显示灵敏度
<DraggableBox 
  showDragArea={true}
  showDragAreaOverMoveDistanse={10}  // 移动超过10px才显示区域,避免抖动误触
>
  {/* 提高拖拽区域显示的灵敏度 */}
</DraggableBox>

5. 拖拽回调函数使用

function DraggableWithCallbacks() {
  const [position, setPosition] = useState({ left: 0, top: 0, diffX: 0, diffY: 0 });
  const [draggingState, setDraggingState] = useState('idle');

  return (
    <div>
      {/* 状态显示 */}
      <div style={{ 
        padding: '10px', 
        background: '#f5f5f5', 
        marginBottom: '10px',
        borderRadius: '4px'
      }}>
        状态: {draggingState} | 左: {position.left}px | 顶: {position.top}px
      </div>

      <DraggableBox
        placement="bottom-right"
        boundsX={[50, 400]}
        boundsY={[50, 300]}
        onDragStart={(change) => {
          setDraggingState('dragging');
          console.log('开始拖拽', change);
        }}
        onDrag={(change) => {
          setPosition(change);
          console.log('拖拽中', change);
        }}
        onDragEnd={(change) => {
          setDraggingState('idle');
          setPosition(change);
          console.log('拖拽结束', change);
        }}
      >
        <div style={{ padding: '16px', background: '#e6f7ff' }}>
          拖拽我并查看回调函数输出
        </div>
      </DraggableBox>
    </div>
  );
}

实时位置跟踪示例

function PositionTracker() {
  const [positions, setPositions] = useState<IPositionChange[]>([]);

  return (
    <div>
      <DraggableBox
        onDrag={(positionChange) => {
          setPositions(prev => [...prev.slice(-9), positionChange]); // 保留最近10个位置
        }}
        onDragEnd={(finalPosition) => {
          console.log('最终位置:', finalPosition);
        }}
      >
        <div style={{ padding: '12px', background: '#fff7e6' }}>
          实时位置追踪
        </div>
      </DraggableBox>
      
      {/* 显示实时位置数据 */}
      <div style={{ marginTop: '10px', fontSize: '12px' }}>
        {positions.map((pos, i) => (
          <div key={i}>
            左: {pos.left.toFixed(1)}px, 顶: {pos.top.toFixed(1)}px, dX: {pos.diffX.toFixed(1)}px, dY: {pos.diffY.toFixed(1)}px
          </div>
        ))}
      </div>
    </div>
  );
}

边界条件检测示例

function BoundaryDetector() {
  const [boundaryHit, setBoundaryHit] = useState('none');

  return (
    <div>
      <div style={{ 
        padding: '10px', 
        background: boundaryHit === 'x' ? '#ffccc7' : 
                    boundaryHit === 'y' ? '#d6e4ff' : '#f6ffed',
        marginBottom: '10px',
        borderRadius: '4px'
      }}>
        边界碰撞: {boundaryHit === 'x' ? 'X轴' : boundaryHit === 'y' ? 'Y轴' : '无'}
      </div>

      <DraggableBox
        boundsX={[50, 300]}
        boundsY={[50, 200]}
        onDrag={(change) => {
          // 检测是否碰撞边界
          const isXBoundary = Math.abs(change.diffX) < 1 && 
                            (change.left <= 50 || change.right <= 50);
          const isYBoundary = Math.abs(change.diffY) < 1 && 
                            (change.top <= 50 || change.bottom <= 50);
          
          if (isXBoundary) setBoundaryHit('x');
          else if (isYBoundary) setBoundaryHit('y');
          else setBoundaryHit('none');
        }}
      >
        <div style={{ padding: '12px', background: '#f0f0f0' }}>
          边界碰撞检测
        </div>
      </DraggableBox>
    </div>
  );
}

6. 响应式设计

function ResponsiveDraggable() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <DraggableBox 
      boundsX={[20, windowSize.width - 200]}
      boundsY={[20, windowSize.height - 150]}
    >
      响应式拖拽框
    </DraggableBox>
  );
}

🌟 使用场景

浮动控制面板

function ControlPanel() {
  return (
    <DraggableBox 
      placement="bottom-right"
      offsetX={50}
      offsetY={50}
      boundsX={[50, 600]}
      boundsY={[100, 500]}
      zIndex={10000}
    >
      <div style={{ 
        padding: '20px', 
        background: 'white', 
        borderRadius: '12px',
        boxShadow: '0 8px 32px rgba(0,0,0,0.12)'
      }}>
        <h3>控制面板</h3>
        {/* 控制项 */}
      </div>
    </DraggableBox>
  );
}

可拖拽侧边工具栏

function DraggableToolbar() {
  return (
    <DraggableBox 
      lockAxis="y"
      placement="top-right"
      offsetX={0}
      offsetY={100}
      boundsX={[0, 0]}  // 锁定在右侧
      boundsY={[50, 600]}
    >
      <div style={{ 
        padding: '10px', 
        background: '#f8f9fa',
        borderLeft: '1px solid #dee2e6',
        width: '200px',
        height: '300px'
      }}>
        <h4>侧边工具栏</h4>
        {/* 工具栏按钮 */}
      </div>
    </DraggableBox>
  );
}

Transform 缩放场景

function ScaledContainer() {
  return (
    <div style={{ 
      width: '800px', 
      height: '600px',
      transform: 'scale(0.8)',
      transformOrigin: 'top left',
      border: '2px solid #1890ff',
      padding: '20px',
      background: '#f5f5f5'
    }}>
      <h3 style={{ marginBottom: '20px' }}>
        缩放容器 (scale: 0.8)
      </h3>
      <DraggableBox 
        placement="bottom-right"
        offsetX={50}
        offsetY={50}
        boundsX={[50, 500]}
        boundsY={[50, 400]}
        showDragArea={true}
      >
        <div style={{ 
          padding: '16px', 
          background: 'white',
          borderRadius: '8px',
          boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
        }}>
          在缩放容器中正常拖拽
        </div>
      </DraggableBox>
    </div>
  );
}

🎨 自定义样式

组件支持通过 className 属性传入自定义样式类名,实现样式覆盖:

基础样式自定义

function CustomStyledDraggable() {
  return (
    <DraggableBox className="my-custom-draggable">
      <div>自定义样式的拖拽框</div>
    </DraggableBox>
  );
}
/* 在您的 CSS 文件中 */
.my-custom-draggable {
  /* 覆盖容器样式 */
  cursor: grab;
  border-radius: 12px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
  border: 2px solid #e0e0e0;
  transition: box-shadow 0.2s ease;
}

.my-custom-draggable:hover {
  cursor: grabbing;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
}

.my-custom-draggable:active {
  cursor: grabbing;
}

拖拽状态样式

function DraggableWithState() {
  const [isDragging, setIsDragging] = useState(false);

  return (
    <DraggableBox 
      className={`custom-draggable ${isDragging ? 'dragging' : ''}`}
      onMouseDown={() => setIsDragging(true)}
      onMouseUp={() => setIsDragging(false)}
    >
      <div>{isDragging ? '拖拽中...' : '可以拖拽'}</div>
    </DraggableBox>
  );
}
.custom-draggable {
  background: white;
  padding: 16px;
  border-radius: 8px;
  transition: all 0.2s ease;
}

.custom-draggable.dragging {
  background: #f0f8ff;
  box-shadow: 0 6px 20px rgba(0, 123, 255, 0.2);
  transform: scale(1.02);
}

响应式样式

function ResponsiveDraggable() {
  return (
    <DraggableBox className="responsive-draggable">
      <div>响应式拖拽框</div>
    </DraggableBox>
  );
}
.responsive-draggable {
  max-width: 300px;
  min-width: 200px;
}

/* 移动端适配 */
@media (max-width: 768px) {
  .responsive-draggable {
    max-width: 250px;
    font-size: 14px;
  }
}

主题化样式

function ThemedDraggable({ theme = 'light' }) {
  return (
    <DraggableBox className={`draggable-theme-${theme}`}>
      <div>主题化拖拽框 - {theme}</div>
    </DraggableBox>
  );
}
.draggable-theme-light {
  background: white;
  color: #333;
  border: 1px solid #e0e0e0;
}

.draggable-theme-dark {
  background: #1a1a1a;
  color: white;
  border: 1px solid #444;
}

.draggable-theme-accent {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border: none;
}

📊 浏览器兼容性

  • ✅ Chrome 60+
  • ✅ Firefox 55+
  • ✅ Safari 12+
  • ✅ Edge 79+

🔗 相关链接

📄 许可证

ISC License