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

@a-drowned-fish/rox-v

v1.0.91

Published

vue3 组件库,提供常用的 ui 组件,如按钮、输入框、下拉选择框、选项卡等

Readme

Rox V

一个基于 Vue 3 的组件库,支持按需引入和自动导入。 支持 SSR

安装

npm install @a-drowned-fish/rox-v
# 或
yarn add @a-drowned-fish/rox-v
# 或
pnpm add @a-drowned-fish/rox-v

使用方式

1. 完整引入

import { createApp } from "vue";
import RoxV from "@a-drowned-fish/rox-v";
import "@a-drowned-fish/rox-v/dist/style.css"; // 如果需要样式 (SSR OR SSG 建议引入, 避免页面样式闪烁)

const app = createApp(App);
app.use(RoxV);

2. 按需引入(推荐)

  • 优势: 无需手动引入样式文件 且 按需加载
<template>
    <Button>点击我</Button>
    <InputOtp />
</template>
<script setup lang="ts">
import { Button, InputOtp } from "@a-drowned-fish/rox-v";
</script>

可用组件

组件列表

InputOtp - OTP 输入组件

一个用于输入一次性密码(OTP)的组件,支持自定义长度、样式和间距。

基础用法

<template>
    <InputOtp v-model="code" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const code = ref("");
</script>

自定义长度

<template>
    <!-- 4位验证码 -->
    <InputOtp v-model="code" :length="4" />

    <!-- 8位验证码 -->
    <InputOtp v-model="code" :length="8" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const code = ref("");
</script>

自定义样式

<template>
    <InputOtp v-model="code" item-class="custom-item" active-item-class="custom-active" gap="15px" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const code = ref("");
</script>

<style scoped>
.custom-item {
    width: 50px;
    height: 60px;
    border: 2px solid #ddd;
    border-radius: 12px;
    font-size: 24px;
}

.custom-active {
    border-color: #67c23a;
    box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.2);
}
</style>

监听完成事件

<template>
    <InputOtp v-model="code" @complete="handleComplete" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const code = ref("");

const handleComplete = (value: string) => {
    console.log("验证码输入完成:", value);
    // 在这里执行验证逻辑
};
</script>

Props

| 属性 | 说明 | 类型 | 默认值 | | ------------------ | -------------------------------------------- | -------- | ---------- | | length | 输入框数量 | number | 6 | | itemClass | 每个输入项的自定义类名 | string | '' | | activeItemClass | 激活状态输入项的自定义类名 | string | '' | | gap | 输入项之间的间距 | string | '10px' | | hasFilledItemClass | 具有内容的输入项的类名[active前面选项的类名] | string | 'active' |

Events

| 事件名 | 说明 | 回调参数 | | -------- | ------------------------------ | ----------------- | | complete | 输入完成时触发(达到指定长度) | (value: string) |

Popup - 弹出层组件

一个灵活的弹出层组件,支持从不同方向滑入,带有遮罩层和过渡动画效果。

基础用法

<template>
    <Button @click="show = true">打开弹窗</Button>

    <Popup v-model="show">
        <div class="popup-content">
            <h3>标题</h3>
            <p>这是弹窗内容</p>
            <Button @click="show = false">关闭</Button>
        </div>
    </Popup>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Button, Popup } from "@a-drowned-fish/rox-v";

const show = ref(false);
</script>

<style scoped>
.popup-content {
    padding: 20px;
    background: white;
    border-radius: 12px 12px 0 0;
}
</style>

不同位置

<template>
    <!-- 底部弹出(默认) -->
    <Popup v-model="showBottom" position="bottom">
        <div class="content">底部弹窗</div>
    </Popup>

    <!-- 顶部弹出 -->
    <Popup v-model="showTop" position="top">
        <div class="content">顶部弹窗</div>
    </Popup>

    <!-- 左侧弹出 -->
    <Popup v-model="showLeft" position="left">
        <div class="content">左侧弹窗</div>
    </Popup>

    <!-- 右侧弹出 -->
    <Popup v-model="showRight" position="right">
        <div class="content">右侧弹窗</div>
    </Popup>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Popup } from "@a-drowned-fish/rox-v";

const showBottom = ref(false);
const showTop = ref(false);
const showLeft = ref(false);
const showRight = ref(false);
</script>

自定义遮罩层

<template>
    <Popup v-model="show" :bg="'rgba(0, 0, 0, 0.7)'" :duration="500" :mask-closable="false">
        <div class="content">
            <p>点击遮罩层不会关闭</p>
            <Button @click="show = false">手动关闭</Button>
        </div>
    </Popup>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Popup, Button } from "@a-drowned-fish/rox-v";

const show = ref(false);
</script>

自定义挂载节点

<template>
    <div id="custom-container">
        <Popup v-model="show" to="#custom-container">
            <div class="content">挂载到指定容器</div>
        </Popup>
    </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Popup } from "@a-drowned-fish/rox-v";

const show = ref(false);
</script>

Props

| 属性 | 说明 | 类型 | 默认值 | | ------------ | ---------------------------- | ---------------------------------------- | --------------------- | | position | 弹出位置 | 'top' \| 'bottom' \| 'left' \| 'right' | 'bottom' | | bg | 遮罩层背景色 | string | 'rgba(0, 0, 0, .5)' | | duration | 动画持续时间(毫秒) | number | 300 | | maskClosable | 点击遮罩层是否关闭 | boolean | true | | to | teleport 的目标节点 | string | 'body' | | top | 弹窗顶部距离父元素顶部的距离 | string | '0px' | | left | 弹窗左侧距离父元素左侧的距离 | string | '0px' | | right | 弹窗右侧距离父元素右侧的距离 | string | '0px' | | bottom | 弹窗底部距离父元素底部的距离 | string | '0px' | | zIndex | 弹窗的 z-index 值 | number | 50 |

Tab - 标签页组件

一个支持左右滚动的标签页组件,带有底部激活线动画效果,支持自定义样式和插槽。

基础用法

<template>
    <Tab v-model="activeIndex" :items="tabs">
        <template v-slot="{ item, active }">
            <span :class="{ active }">{{ item.label }}</span>
        </template>
        <template v-slot:panel="{ item }">
            <div>{{ item.content }}</div>
        </template>
    </Tab>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Tab } from "@a-drowned-fish/rox-v";

const activeIndex = ref(0);
const tabs = [
    { label: "标签1", content: "内容1" },
    { label: "标签2", content: "内容2" },
    { label: "标签3", content: "内容3" },
];
</script>

<style scoped>
.active {
    color: #409eff;
    font-weight: bold;
}
</style>

自定义间距

<template>
    <Tab v-model="activeIndex" :items="tabs" gap="20px">
        <template #default="{ item }">{{ item.label }}</template>
        <template #panel="{ item }">{{ item.content }}</template>
    </Tab>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Tab } from "@a-drowned-fish/rox-v";

const activeIndex = ref(0);
const tabs = [
    { label: "标签1", content: "内容1" },
    { label: "标签2", content: "内容2" },
];
</script>

自定义样式

<template>
    <Tab
        v-model="activeIndex"
        :items="tabs"
        tab-container-class="custom-tab-container"
        tab-item-class="custom-tab-item"
        panel-container-class="custom-panel-container"
        panel-item-class="custom-panel-item"
        active-line-class="custom-active-line"
    >
        <template #default="{ item, active }">
            <span>{{ item.label }}</span>
        </template>
        <template #panel="{ item }">
            <div>{{ item.content }}</div>
        </template>
    </Tab>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Tab } from "@a-drowned-fish/rox-v";

const activeIndex = ref(0);
const tabs = [
    { label: "标签1", content: "内容1" },
    { label: "标签2", content: "内容2" },
];
</script>

<style scoped>
.custom-tab-item {
    padding: 10px 15px;
    border-radius: 8px 8px 0 0;
}

.custom-tab-item:hover {
    background-color: #f5f7fa;
}

.custom-active-line {
    background-color: #409eff;
    height: 3px;
}

.custom-panel-container {
    border: 1px solid #ebeef5;
    border-top: none;
    padding: 20px;
}
</style>

Props

| 属性 | 说明 | 类型 | 默认值 | | ---------------------- | ------------------------------- | -------------------- | ------------------------------------------------------ | | v-model | 当前激活的标签索引 | number | 0 | | items | 标签数据数组 | T[] | [] | | gap | 标签之间的间距 | string | '10px' | | tabContainerClass | 标签容器的自定义类名 | string | '' | | tabItemClass | 标签项的自定义类名 | string | '' | | topPanelContainerClass | tab上方容器的自定义类名 | string | '' | | topPanelItemClass | tab上方容器内每一项的自定义类名 | string | '' | | panelContainerClass | 面板容器的自定义类名 | string | '' | | panelItemClass | 面板项的自定义类名 | string | '' | | activeLineClass | 激活底线的自定义类名 | string | '' | | trigger | 触发方式 | 'click' \| 'hover' | 'click' | | dir | 布局方向 | 'ltr' \| 'rtl' | undefined. 布局方向;ltr 为从左到右,rtl 为从右到左 | | animate | 是否启用动画 | boolean | true slide 效果 |

Slots

| 插槽名 | 说明 | 参数 | | ------- | ----------------------------------------- | ------------------------- | | default | 标签项的自定义内容 | { item, index, active } | | panel | 面板内容的自定义模板 | { item, index, active } | | middle | 介于 default 和 panel中间的内容自定义模板 | |

CSS 变量

| 变量名 | 说明 | 默认值 | | --------------------- | ---------------- | ------- | | --transition-duration | 底线动画持续时间 | 0.15s |

类型定义

interface RoxVTabProps<T extends Record<string, any>> {
    items: T[]; // 标签数据数组
    gap?: string; // 标签间距
    tabContainerClass?: string; // 标签容器类名
    tabItemClass?: string; // 标签项类名
    panelContainerClass?: string; // 面板容器类名
    panelItemClass?: string; // 面板项类名
    activeLineClass?: string; // 激活底线类名
}

Menu - 下拉菜单组件

一个支持多级嵌套的下拉菜单组件,支持点击和悬停触发方式,带有过渡动画效果。

基础用法

<template>
    <Menu :items="menuItems" v-model="selectedValue">
        <span>点击打开菜单</span>
    </Menu>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Menu } from "@a-drowned-fish/rox-v";

const selectedValue = ref<(string | number)[]>([]);

const menuItems = [
    { label: "选项1", value: "option1" },
    { label: "选项2", value: "option2" },
    { label: "选项3", value: "option3" },
];
</script>

多级菜单

<template>
    <Menu :items="menuItems" v-model="selectedValue">
        <span>多级菜单</span>
    </Menu>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Menu } from "@a-drowned-fish/rox-v";

const selectedValue = ref<(string | number)[]>([]);

const menuItems = [
    {
        label: "文件",
        value: "file",
        children: [
            { label: "新建", value: "new" },
            { label: "打开", value: "open" },
            { label: "保存", value: "save" },
        ],
    },
    {
        label: "编辑",
        value: "edit",
        children: [
            { label: "撤销", value: "undo" },
            { label: "重做", value: "redo" },
            {
                label: "查找",
                value: "find",
                children: [
                    { label: "查找文本", value: "find-text" },
                    { label: "替换", value: "replace" },
                ],
            },
        ],
    },
];
</script>

自定义图标

<template>
    <Menu
        :items="menuItems"
        suffix-icon="/icons/arrow-right.svg"
        checked-icon="/icons/check.svg"
        v-model="selectedValue"
    >
        <span>带图标的菜单</span>
    </Menu>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Menu } from "@a-drowned-fish/rox-v";

const selectedValue = ref<(string | number)[]>([]);

const menuItems = [
    { label: "选项1", value: "option1" },
    { label: "选项2", value: "option2" },
    {
        label: "子菜单",
        value: "submenu",
        children: [{ label: "子选项", value: "sub-option" }],
    },
];
</script>

监听事件

<template>
    <Menu :items="menuItems" v-model="selectedValue" @open="handleOpen" @close="handleClose">
        <span>监听事件</span>
    </Menu>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Menu } from "@a-drowned-fish/rox-v";

const selectedValue = ref<(string | number)[]>([]);

const menuItems = [
    { label: "选项1", value: "option1" },
    { label: "选项2", value: "option2" },
];

const handleOpen = () => {
    console.log("菜单已打开");
};

const handleClose = () => {
    console.log("菜单已关闭");
};
</script>

自定义样式

<template>
    <Menu
        :items="menuItems"
        v-model="selectedValue"
        item-gap="8px"
        active-item-class="custom-active"
        list-container-class="custom-list"
        default-container-class="custom-trigger"
    >
        <span>自定义样式</span>
    </Menu>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Menu } from "@a-drowned-fish/rox-v";

const selectedValue = ref<(string | number)[]>([]);

const menuItems = [
    { label: "选项1", value: "option1" },
    { label: "选项2", value: "option2" },
];
</script>

<style scoped>
.custom-trigger {
    background-color: #409eff;
    color: white;
    border-radius: 4px;
}

.custom-list {
    border-radius: 8px;
}

.custom-active {
    background-color: #ecf5ff;
    color: #409eff;
}
</style>

Props

| 属性 | 说明 | 类型 | 默认值 | | --------------------- | ------------------------ | ----------------------- | ------- | | items | 菜单项数据 | RoxVMenuOptionProps[] | [] | | duration | 过渡动画持续时间(毫秒) | number | 100 | | suffixIcon | 子菜单后缀图标 URL | string | '' | | suffixIconClass | 后缀图标的自定义类名 | string | '' | | checkedIcon | 选中项的图标 URL | string | '' | | checkedIconClass | 选中标记图标的自定义类名 | string | '' | | listContainerClass | 菜单列表容器的自定义类名 | string | '' | | subMenuContainerClass | 子菜单容器的自定义类名 | string | '' | | defaultContainerClass | 触发区域容器的自定义类名 | string | '' | | itemContainerClass | 菜单项的自定义类名 | string | '' | | itemGap | 菜单项之间的间距 | string | '4px' | | activeItemClass | 激活菜单项的自定义类名 | string | '' | | v-model | 当前选中的菜单项路径 | (string \| number)[] | [] |

Events

| 事件名 | 说明 | 回调参数 | | ------ | ---------- | -------- | | open | 菜单打开时 | 无 | | close | 菜单关闭时 | 无 |

Slots

| 插槽名 | 说明 | | ------- | -------------------- | | default | 触发区域的自定义内容 |

类型定义

interface RoxVMenuOptionProps {
    label: string; // 菜单项显示文本
    value: string | number; // 菜单项唯一标识
    children?: RoxVMenuOptionProps[]; // 子菜单项(可选)
}

Expose

| 方法名 | 说明 | 返回值类型 | | --------------- | ---------------------------- | --------------------------------------------------------- | | getSelectedItem | 获取当前选中项的完整数据对象 | { label: string, value: string \| number } \| undefined |

Toast - 消息提示组件

一个轻量级的消息提示组件,支持多种类型的提示消息,可自定义显示时长和样式。

基础用法

<template>
    <Button @click="showToast">显示提示</Button>
    <Toaster />
</template>

<script setup lang="ts">
import { useToast } from "@a-drowned-fish/rox-v";
import { Toaster } from "@a-drowned-fish/rox-v";

const { toast } = useToast();

const showToast = () => {
    toast({
        message: "这是一条提示消息",
        type: "info",
        duration: 3000,
    });
};
</script>

不同类型的提示

<template>
    <Button @click="showSuccess">成功</Button>
    <Button @click="showError">错误</Button>
    <Button @click="showInfo">信息</Button>
    <Button @click="showWarning">警告</Button>
    <Toaster />
</template>

<script setup lang="ts">
import { useToast } from "@a-drowned-fish/rox-v";
import { Toaster } from "@a-drowned-fish/rox-v";

const { success, error, info, warning } = useToast();

const showSuccess = () => success("操作成功");
const showError = () => error("操作失败");
const showInfo = () => info("这是一条信息");
const showWarning = () => warning("这是一条警告");
</script>

自定义显示时长

<template>
    <Button @click="showToast">显示5秒</Button>
    <Toaster />
</template>

<script setup lang="ts">
import { useToast } from "@a-drowned-fish/rox-v";
import { Toaster } from "@a-drowned-fish/rox-v";

const { success } = useToast();

const showToast = () => {
    success("这条消息将显示5秒", 5000);
};
</script>

手动关闭提示

<template>
    <Button @click="showToast">显示提示</Button>
    <Button @click="dismissAll">关闭所有</Button>
    <Toaster />
</template>

<script setup lang="ts">
import { useToast } from "@a-drowned-fish/rox-v";
import { Toaster } from "@a-drowned-fish/rox-v";

const { toast, dismiss } = useToast();
let toastId: string;

const showToast = () => {
    toastId = toast({
        message: "这条消息需要手动关闭",
        type: "info",
        duration: 10000, // 设置较长的显示时间
    });
};

const dismissAll = () => {
    dismiss(); // 关闭所有提示
    // 或 dismiss(toastId) 关闭指定提示
};
</script>

自定义图标

<template>
    <Button @click="showToast">显示提示</Button>
    <Toaster>
        <template #success-icon>
            <svg class="icon" viewBox="0 0 24 24">
                <path
                    fill="#67c23a"
                    d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
                />
            </svg>
        </template>
        <template #error-icon>
            <svg class="icon" viewBox="0 0 24 24">
                <path
                    fill="#f56c6c"
                    d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"
                />
            </svg>
        </template>
        <template #info-icon>
            <svg class="icon" viewBox="0 0 24 24">
                <path
                    fill="#909399"
                    d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"
                />
            </svg>
        </template>
        <template #warning-icon>
            <svg class="icon" viewBox="0 0 24 24">
                <path fill="#e6a23c" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" />
            </svg>
        </template>
    </Toaster>
</template>

<script setup lang="ts">
import { useToast } from "@a-drowned-fish/rox-v";
import { Toaster } from "@a-drowned-fish/rox-v";

const { success, error, info, warning } = useToast();

const showToast = () => {
    success("成功提示");
    error("错误提示");
    info("信息提示");
    warning("警告提示");
};
</script>

<style scoped>
.icon {
    width: 20px;
    height: 20px;
    margin-right: 8px;
}
</style>

自定义样式

<template>
    <Button @click="showToast">显示提示</Button>
    <Toaster title-class="custom-title" message-class="custom-message" />
</template>

<script setup lang="ts">
import { useToast } from "@a-drowned-fish/rox-v";
import { Toaster } from "@a-drowned-fish/rox-v";

const { success } = useToast();

const showToast = () => {
    success("自定义样式的提示消息");
};
</script>

<style scoped>
:deep(.custom-title) {
    justify-content: flex-start;
    padding: 12px 16px;
}

:deep(.custom-message) {
    font-size: 14px;
    line-height: 1.5;
}
</style>

自定义挂载节点

<template>
    <div id="toast-container">
        <Button @click="showToast">显示提示</Button>
        <Toaster to="#toast-container" />
    </div>
</template>

<script setup lang="ts">
import { useToast } from "@a-drowned-fish/rox-v";
import { Toaster } from "@a-drowned-fish/rox-v";

const { success } = useToast();

const showToast = () => {
    success("提示消息将显示在指定容器中");
};
</script>

Toaster Props

| 属性 | 说明 | 类型 | 默认值 | | ------------ | -------------------- | -------- | ----------- | | to | teleport 的目标节点 | string | 'body' | | bg | 提示背景颜色 | string | '#303133' | | titleClass | 标题容器的自定义类名 | string | '' | | messageClass | 消息内容的自定义类名 | string | '' | | toastClass | 提示容器的自定义类名 | string | '' |

useToast 方法

| 方法名 | 说明 | 参数类型 | 返回值 | | ------- | -------------------- | --------------- | -------- | | toast | 创建一个自定义提示 | ToastOptions | string | | success | 创建成功类型提示 | (msg, dur?) | string | | error | 创建错误类型提示 | (msg, dur?) | string | | info | 创建信息类型提示 | (msg, dur?) | string | | warning | 创建警告类型提示 | (msg, dur?) | string | | dismiss | 关闭提示(可指定ID) | (id?: string) | void |

ToastOptions 类型定义

interface ToastOptions {
    message?: string; // 提示消息内容
    duration?: number; // 显示时长(毫秒),默认 3000
    type?: ToastType; // 提示类型:'success' | 'error' | 'info' | 'warning'
}

Toaster Slots

| 插槽名 | 说明 | | ------------ | -------------------- | | success-icon | 成功提示的自定义图标 | | error-icon | 错误提示的自定义图标 | | info-icon | 信息提示的自定义图标 | | warning-icon | 警告提示的自定义图标 |

SliderCaptcha - 滑块验证码组件

一个用于验证用户身份的滑块验证码组件,支持自定义背景图和滑块图,提供实时位置追踪功能。

基础用法

<template>
    <SliderCaptcha
        :background="bgImage"
        :block="blockImage"
        :width="300"
        :block-top="80"
        @success="onSuccess"
        @fail="onFail"
        @change="onChange"
    />
</template>

<script setup lang="ts">
import { ref } from "vue";
import { SliderCaptcha } from "@a-drowned-fish/rox-v";

const bgImage = ref("https://example.com/bg.jpg");
const blockImage = ref("https://example.com/block.png");

function onSuccess() {
    console.log("验证成功");
}

function onFail() {
    console.log("验证失败");
}

function onChange(tracks) {
    console.log("滑块位置变化:", tracks);
}
</script>

带验证函数的用法

<template>
    <SliderCaptcha
        :background="bgImage"
        :block="blockImage"
        :width="400"
        :verify="verifyCaptcha"
        @success="onSuccess"
        @fail="onFail"
    />
</template>

<script setup lang="ts">
import { ref } from "vue";
import { SliderCaptcha } from "@a-drowned-fish/rox-v";

const bgImage = ref("https://example.com/bg.jpg");
const blockImage = ref("https://example.com/block.png");

async function verifyCaptcha(position) {
    // 发送验证请求到后端
    const response = await fetch("/api/verify-captcha", {
        method: "POST",
        body: JSON.stringify(position),
    });
    return response.ok;
}

function onSuccess() {
    console.log("验证成功");
}

function onFail() {
    console.log("验证失败");
}
</script>

SliderCaptcha Props

| 属性 | 说明 | 类型 | 默认值 | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | background | 背景图地址 | string | - | | block | 滑块缺口图地址 | string | - | | width | 容器宽度(像素) | number | - | | blockTop | 滑块图距离顶部的距离(原图像素) | number | 0 | | verify | 验证函数,返回 boolean 或者 undefined 或者 null 或者 void 或者 Promise 或者 Promise 或者 Promise 或者 Promise | (option: RoxVSliderCaptchaTrackItem) => boolean \| undefined \| null \| void \| Promise<boolean> \| Promise<undefined> \| Promise<null> \| Promise<void> | - | | trackBlockBg | 滑块轨道背景颜色 | string | '#f5f5f5' | | trackBg | 滑块轨道进度条背景颜色 | string | '#c5c5c5' |

  • 注:
    • 验证函数返回true时,代表验证成功,返回false时,代表验证失败。 会触发对应的 success 、 fail 事件
    • 验证函数返回undefined、null、void时,不会触发事件

SliderCaptcha Events

| 事件名 | 说明 | 参数类型 | | ------- | ------------------ | ------------------------------ | | success | 验证成功时触发 | - | | fail | 验证失败时触发 | - | | change | 滑块移动时实时触发 | RoxVSliderCaptchaTrackItem[] |

RoxVSliderCaptchaTrackItem 类型定义

interface RoxVSliderCaptchaTrackItem {
    x: number; // 缺口图在原图上的X坐标
    t: number; // 时间戳
    xPercent: number; // X坐标占原图宽度的百分比
}

SliderCaptcha Slots

| 插槽名 | 说明 | | -------------- | ------------------ | | default | 自定义滑块内容 | | verify-success | 自定义验证成功提示 | | verify-fail | 自定义验证失败提示 |

SliderCaptcha Expose

| 属性/方法 | 说明 | 类型 | | --------- | ---------------------- | ------------------------------ | | reset | 重置滑块位置、验证结果 | () => void | | tracks | 滑块移动轨迹数组 | RoxVSliderCaptchaTrackItem[] |

CountDown - 倒计时组件

一个基于剩余秒数的倒计时组件,支持自定义显示格式和初始时间。

Props

| 属性 | 说明 | 类型 | 默认值 | | ------- | ------------ | --------------------- | ----------- | | v-model | 当前剩余秒数 | number \| undefined | undefined |

注: 当v-model为number时,倒计时会从该值开始倒计时,倒计时结束时,v-model会自动设置为0。 但是过程中,v-model绑定的数据不会变化

示例:OTP 倒计时

<template>
    <div>
        <CountDown v-model="remain">
            <template #initial>发送验证码</template>
            <template #default="{ seconds }">
                <span>{{ seconds > 0 ? `剩余${seconds}秒` : "重新发送" }}</span>
            </template>
        </CountDown>
        <Button @click="remain = 9">开始倒计时</Button>
    </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { CountDown, Button } from "@a-drowned-fish/rox-v";

const remain = ref<undefined | number>(undefined);
</script>

Select - 下拉选择组件

一个支持多级嵌套的下拉选择组件,支持悬停展开子菜单,带有过渡效果。

基础用法

<template>
    <Select :items="selectItems" v-model="selectedValues">
        <template #default="{ selected }">
            <span>{{ selected?.label || "请选择" }}</span>
        </template>
        <template #item="{ item, active }">
            <span :class="{ active }">{{ item.label }}</span>
        </template>
    </Select>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Select } from "@a-drowned-fish/rox-v";

const selectedValues = ref<(string | number)[]>([]);

const selectItems = [
    { label: "选项1", value: "option1" },
    { label: "选项2", value: "option2" },
    { label: "选项3", value: "option3" },
];
</script>

<style scoped>
.active {
    color: #409eff;
    font-weight: bold;
}
</style>

多级选择

<template>
    <Select :items="selectItems" v-model="selectedValues">
        <template #default="{ selected }">
            <span>{{ selected?.label || "请选择" }}</span>
        </template>
        <template #item="{ item, active }">
            <span :class="{ active }">{{ item.label }}</span>
            <span v-if="item.children" class="arrow">›</span>
        </template>
    </Select>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Select } from "@a-drowned-fish/rox-v";

const selectedValues = ref<(string | number)[]>([]);

const selectItems = [
    {
        label: "菜单1",
        value: "menu1",
        children: [
            { label: "子选项1-1", value: "sub1-1" },
            { label: "子选项1-2", value: "sub1-2" },
        ],
    },
    {
        label: "菜单2",
        value: "menu2",
        children: [
            {
                label: "子菜单2-1",
                value: "sub2-1",
                children: [
                    { label: "孙选项2-1-1", value: "grand2-1-1" },
                    { label: "孙选项2-1-2", value: "grand2-1-2" },
                ],
            },
            { label: "子选项2-2", value: "sub2-2" },
        ],
    },
];
</script>

<style scoped>
.arrow {
    float: right;
    color: #999;
}
</style>

自定义样式

<template>
    <Select :items="selectItems" v-model="selectedValues" item-gap="8px" list-container-class-name="custom-list">
        <template #default="{ selected }">
            <span class="custom-trigger">{{ selected?.label || "请选择" }}</span>
        </template>
        <template #item="{ item, active }">
            <span :class="{ active }">{{ item.label }}</span>
        </template>
    </Select>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Select } from "@a-drowned-fish/rox-v";

const selectedValues = ref<(string | number)[]>([]);

const selectItems = [
    { label: "选项1", value: "option1" },
    { label: "选项2", value: "option2" },
];
</script>

<style scoped>
.custom-trigger {
    padding: 8px 16px;
    background: #f5f7fa;
    border-radius: 4px;
}

.custom-list {
    border-radius: 8px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>

Props

| 属性 | 说明 | 类型 | 默认值 | | ---------------------- | -------------------- | ------------------------- | ------- | | items | 选择项数据数组 | RoxVSelectOptionProps[] | [] | | itemGap | 菜单项之间的间距 | string | '4px' | | listContainerClassName | 列表容器的自定义类名 | string | '' | | v-model | 当前选中的菜单项路径 | (string \| number)[] | [] |

Events

| 事件名 | 说明 | 回调参数 | | ------ | ---------- | -------- | | open | 菜单打开时 | 无 | | close | 菜单关闭时 | 无 |

Slots

| 插槽名 | 说明 | 参数 | | ------- | -------------------- | ---------------------------------------------------- | | default | 触发区域的自定义内容 | { selected } - 选中项数据,包含 labelvalue | | item | 选项项的自定义内容 | { item, index, active } |

Expose

| 方法名 | 说明 | 返回值类型 | | --------------- | ---------------------------- | --------------------------------------------------------- | | getSelectedItem | 获取当前选中项的完整数据对象 | { label: string, value: string \| number } \| undefined |

手动获取选中项

<template>
    <Select ref="selectRef" :items="selectItems" v-model="selectedValues">
        <template #default="{ selected }">
            <span>{{ selected?.label || "请选择" }}</span>
        </template>
        <template #item="{ item, active }">
            <span :class="{ active }">{{ item.label }}</span>
        </template>
    </Select>
    <Button @click="handleGetSelected">获取选中项</Button>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Select, Button } from "@a-drowned-fish/rox-v";

const selectRef = ref();
const selectedValues = ref<(string | number)[]>([]);

const selectItems = [
    { label: "选项1", value: "option1" },
    { label: "选项2", value: "option2" },
];

const handleGetSelected = () => {
    const item = selectRef.value?.getSelectedItem();
    if (item) {
        console.log("选中项:", item.label, item.value);
    } else {
        console.log("未选中任何项");
    }
};
</script>

类型定义

interface RoxVSelectOptionProps {
    label: string; // 显示文本
    value: string | number; // 唯一标识
    children?: RoxVSelectOptionProps[]; // 子选项(可选)
    [key: string]: any; // 其他自定义属性
}

Panel - 面板切换组件

一个支持横向滑动切换的面板组件,支持 RTL 布局方向,常用于步骤指示器等场景。

基础用法

<template>
    <Panel :items="panels" v-model="activeIndex">
        <template #default="{ item, index, active }">
            <div :class="['panel-content', { active }]">
                <h3>{{ item.title }}</h3>
                <p>{{ item.description }}</p>
            </div>
        </template>
    </Panel>
    <Button @click="activeIndex++" :disabled="activeIndex >= panels.length - 1"> 下一页 </Button>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Panel, Button } from "@a-drowned-fish/rox-v";

const activeIndex = ref(0);

const panels = [
    { title: "步骤1", description: "第一步的内容描述" },
    { title: "步骤2", description: "第二步的内容描述" },
    { title: "步骤3", description: "第三步的内容描述" },
];
</script>

<style scoped>
.panel-content {
    padding: 20px;
    min-height: 200px;
}
.panel-content.active {
    background: #f5f7fa;
}
</style>

RTL 布局

<template>
    <Panel :items="panels" v-model="activeIndex" dir="rtl">
        <template #default="{ item }">
            <div>{{ item.title }}</div>
        </template>
    </Panel>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Panel } from "@a-drowned-fish/rox-v";

const activeIndex = ref(0);

const panels = [{ title: "第一步" }, { title: "第二步" }, { title: "第三步" }];
</script>

Props

| 属性 | 说明 | 类型 | 默认值 | | ------- | ------------ | -------------------------- | ----------- | | items | 面板数据数组 | T[] | [] | | dir | 布局方向 | 'ltr' \| 'rtl' \| 'auto' | undefined | | v-model | 当前索引 | number | 0 |

Slots

| 插槽名 | 说明 | 参数 | | ------- | ------------------ | ------------------------- | | default | 面板内容自定义模板 | { item, index, active } |

License

MIT