chore: 升级 shadcn-vue 组件到v4最新版 (#7972)
* fix: useStore is deprecated * chore: update deps * feat: 升级shadcn-ui v4 * fix: workbench style * feat: 升级shadcn-ui v4 step2 * feat: 升级shadcn-ui v4 step3 * chore: 升级shadcn v4 * fix: pagination * fix: dark style * fix: doc import * feat: 增加详情组件,参考 antdv-next * docs: descriptions docs * docs: Browser Support * feat: add table action * feat: icon use vbenIcon * fix: type error * fix: dropdown popConfirm * feat: 使用默认的文字交互 * feat: 优化渲染性能pull/355/head^2
parent
108d7ff335
commit
04fbb7a556
|
|
@ -114,7 +114,7 @@ pnpm build
|
|||
|
||||
## ブラウザサポート
|
||||
|
||||
ローカル開発には `Chrome 80+` ブラウザを推奨します
|
||||
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
|
||||
|
||||
モダンブラウザをサポートし、IEはサポートしません
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.
|
|||
|
||||
## Browser Support
|
||||
|
||||
The `Chrome 80+` browser is recommended for local development
|
||||
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
|
||||
|
||||
Support modern browsers, not IE
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ pnpm build
|
|||
|
||||
## 浏览器支持
|
||||
|
||||
本地开发推荐使用 `Chrome 80+` 浏览器
|
||||
Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+
|
||||
|
||||
支持现代浏览器,不支持 IE
|
||||
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
|
|
@ -252,7 +252,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
|||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
class="lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { computed, ref, useSlots } from 'vue';
|
|||
|
||||
import { VbenTooltip } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { Code } from 'lucide-vue-next';
|
||||
import { Code } from '@lucide/vue';
|
||||
import {
|
||||
TabsContent,
|
||||
TabsIndicator,
|
||||
|
|
|
|||
|
|
@ -198,6 +198,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
|||
link: 'common-ui/vben-ellipsis-text',
|
||||
text: 'EllipsisText',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-descriptions',
|
||||
text: 'Descriptions',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-table-action',
|
||||
text: 'TableAction',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-cropper',
|
||||
text: 'Cropper',
|
||||
|
|
|
|||
|
|
@ -196,6 +196,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
|||
link: 'common-ui/vben-ellipsis-text',
|
||||
text: 'EllipsisText 省略文本',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-descriptions',
|
||||
text: 'Descriptions 描述列表',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-table-action',
|
||||
text: 'TableAction 表格操作',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-cropper',
|
||||
text: 'Cropper 图片裁剪',
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@lucide/vue": "catalog:",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/plugins": "workspace:*",
|
||||
"@vben/styles": "workspace:*",
|
||||
"antdv-next": "catalog:",
|
||||
"lucide-vue-next": "catalog:",
|
||||
"medium-zoom": "catalog:",
|
||||
"reka-ui": "catalog:",
|
||||
"vitepress-plugin-group-icons": "catalog:"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Descriptions 描述列表
|
||||
|
||||
`Descriptions` 用于成组展示只读的字段信息,常用于详情页、信息预览等场景。组件基于 shadcn-ui 构建,API 参考 Ant Design Vue 的 Descriptions,支持响应式列数、跨列、边框、垂直布局等能力。
|
||||
|
||||
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
组件提供两种使用方式:通过 `items` 数据驱动(推荐),或通过子组件 `VbenDescriptionsItem` 声明列表项。两者可按需选择,`items` 优先级更高。:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过 `items` 传入字段数组,每项包含 `label` 与 `content`。默认按断点自适应列数(`xs` 1 列、`sm` 2 列、`md` 及以上 3 列)。
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/basic" />
|
||||
|
||||
## 带边框
|
||||
|
||||
设置 `bordered` 展示边框样式,配合 `title` 标题与 `#extra` 插槽(位于标题右侧的操作区域)。
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/bordered" />
|
||||
|
||||
## 垂直布局
|
||||
|
||||
通过 `layout="vertical"` 让标签位于内容上方。
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/vertical" />
|
||||
|
||||
## 不同尺寸
|
||||
|
||||
通过 `size` 设置 `small`、`middle`、`large` 三种尺寸。
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/size" />
|
||||
|
||||
## 跨列与响应式
|
||||
|
||||
单项通过 `span` 设置跨列数,`'filled'` 表示占满当前行剩余空间;`column` 支持传入按断点配置的对象实现响应式列数。
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/span" />
|
||||
|
||||
## 子组件用法
|
||||
|
||||
不传 `items` 时,可在默认插槽中使用 `VbenDescriptionsItem` 声明列表项,内容支持默认插槽或 `#content` 插槽自定义。
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/custom" />
|
||||
|
||||
## API
|
||||
|
||||
### Descriptions Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| items | 数据驱动的列表项;不传则读取默认插槽 | `DescriptionsItemType[]` | - |
|
||||
| bordered | 是否展示边框 | `boolean` | `false` |
|
||||
| column | 一行的列数,支持按断点配置 | `number \| Partial<Record<Breakpoint, number>>` | `{ xs: 1, sm: 2, md: 3, xxxl: 4 }` |
|
||||
| layout | 布局方式 | `'horizontal' \| 'vertical'` | `'horizontal'` |
|
||||
| size | 尺寸 | `'small' \| 'middle' \| 'large'` | `'middle'` |
|
||||
| colon | 是否显示冒号(仅非边框的水平布局生效) | `boolean` | `true` |
|
||||
| title | 标题 | `string` | - |
|
||||
| extra | 标题右侧的操作区域 | `string` | - |
|
||||
| labelStyle | 统一的标签样式 | `CSSProperties` | - |
|
||||
| contentStyle | 统一的内容样式 | `CSSProperties` | - |
|
||||
| class | 根节点自定义类名 | `string` | - |
|
||||
|
||||
### Descriptions Slots
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| ------- | ---------------------------------- |
|
||||
| title | 自定义标题 |
|
||||
| extra | 自定义标题右侧操作区域 |
|
||||
| default | 放置 `VbenDescriptionsItem` 子组件 |
|
||||
|
||||
### DescriptionsItem
|
||||
|
||||
`items` 数组中的每一项,或子组件 `VbenDescriptionsItem` 的属性。
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| label | 标签 | `string \| number \| (() => VNode) \| Component` | - |
|
||||
| content | 内容 | `string \| number \| (() => VNode) \| Component` | - |
|
||||
| span | 跨列数,`'filled'` 占满当前行剩余 | `number \| 'filled' \| Partial<Record<Breakpoint, number>>` | `1` |
|
||||
| labelStyle | 标签样式 | `CSSProperties` | - |
|
||||
| contentStyle | 内容样式 | `CSSProperties` | - |
|
||||
| key | 唯一标识 | `string \| number` | - |
|
||||
|
||||
### DescriptionsItem Slots
|
||||
|
||||
仅子组件用法可用。
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| ------- | ------------------------ |
|
||||
| default | 内容(等价于 `content`) |
|
||||
| content | 自定义内容 |
|
||||
| label | 自定义标签 |
|
||||
|
||||
::: tip Breakpoint
|
||||
|
||||
响应式断点 `Breakpoint` 取值为 `'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'`,断点像素与 Ant Design 一致(`sm` 576、`md` 768、`lg` 992、`xl` 1200、`xxl` 1600、`xxxl` 2000)。:::
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben TableAction 表格操作
|
||||
|
||||
`TableAction` 用于在表格操作列中渲染一组操作按钮,参考 vben2 的 TableAction 设计。基于 shadcn-ui 构建,支持权限控制、气泡确认、提示、下拉「更多」、分割线等能力,可在表格内外任意场景复用。
|
||||
|
||||
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
组件本身不依赖任何业务逻辑(不直接读取权限 store),权限通过注入 `hasPermission` 实现,从而保持核心层零耦合、可跨框架复用。在 vxe-table 中推荐通过列插槽(`slots: { default: 'action' }`)在页面里渲染,不改变表格原有的渲染机制。:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过 `actions` 传入操作项数组,每项包含 `text`、`onClick` 等;`danger` 标记危险操作,`divider` 显示按钮间分割线。
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/basic" />
|
||||
|
||||
## 提示
|
||||
|
||||
通过 `tooltip` 为操作项添加提示,支持字符串或 `{ content, side }` 配置。
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/tooltip" />
|
||||
|
||||
## 气泡确认
|
||||
|
||||
通过 `popConfirm` 开启点击前的气泡确认,常用于删除等危险操作。
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/popconfirm" />
|
||||
|
||||
## 更多下拉
|
||||
|
||||
通过 `dropdownActions` 将次要操作收纳到「更多」下拉中,`moreText` 可自定义按钮文案。
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/dropdown" />
|
||||
|
||||
## 权限控制
|
||||
|
||||
为操作项设置 `auth` 权限码,并注入 `hasPermission` 判断函数,无权限的操作会被隐藏。
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/permission" />
|
||||
|
||||
## 在 vxe-table 中使用
|
||||
|
||||
不改变 vxe-table 原有渲染方式,推荐在列配置中声明插槽,在页面通过插槽渲染。
|
||||
|
||||
::: tip 推荐:使用适配器封装的版本项目的 `#/adapter/vxe-table` 已对 `VbenTableAction` 做了二次封装,内部统一注入了 `hasPermission`(基于 `useAccess().hasAccessByCodes`)。因此从适配器引入时**无需再传入 `:has-permission`**,只需通过操作项的 `auth` 字段声明权限码即可。:::
|
||||
|
||||
```ts
|
||||
// data.ts —— 列配置声明插槽
|
||||
{
|
||||
align: 'center',
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: $t('system.user.operation'),
|
||||
width: 180,
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- list.vue —— 从适配器引入,权限自动注入,无需传入 has-permission -->
|
||||
<script setup lang="ts">
|
||||
import { VbenTableAction } from '#/adapter/vxe-table';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #action="{ row }">
|
||||
<template #action="{ row }">
|
||||
<VbenTableAction
|
||||
:actions="[
|
||||
{
|
||||
text: $t('common.detail'),
|
||||
icon: 'lucide:eye',
|
||||
onClick: () => onDetail(row),
|
||||
},
|
||||
{
|
||||
text: $t('common.edit'),
|
||||
icon: 'lucide:edit',
|
||||
onClick: () => onEdit(row),
|
||||
},
|
||||
]"
|
||||
:dropdown-actions="[
|
||||
{
|
||||
text: $t('common.delete'),
|
||||
icon: 'lucide:trash-2',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
auth: ['AC_100100'],
|
||||
},
|
||||
]"
|
||||
align="center"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
```
|
||||
|
||||
若直接从 `@vben/common-ui` 引入核心组件(不经过适配器),组件不依赖任何业务逻辑,需自行注入 `hasPermission`:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useAccess } from '@vben/access';
|
||||
import { VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
function hasPermission(auth?: string | string[]) {
|
||||
if (!auth) return true;
|
||||
return hasAccessByCodes(Array.isArray(auth) ? auth : [auth]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenTableAction
|
||||
v-bind="useActions(row, onActionClick)"
|
||||
:has-permission="hasPermission"
|
||||
align="center"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### TableAction Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| actions | 主操作按钮 | `ActionItem[]` | `[]` |
|
||||
| dropdownActions | 「更多」下拉中的操作 | `ActionItem[]` | `[]` |
|
||||
| align | 对齐方式 | `'start' \| 'center' \| 'end'` | `'end'` |
|
||||
| divider | 按钮之间是否显示分割线 | `boolean` | `false` |
|
||||
| moreText | 「更多」按钮文案(提供时显示在图标右侧) | `string` | - |
|
||||
| hasPermission | 权限判断函数,返回 `false` 则隐藏对应 `auth` 的操作(从 `#/adapter/vxe-table` 引入时已自动注入,无需手动传入) | `(auth?: string \| string[]) => boolean` | - |
|
||||
| class | 根节点自定义类名 | `string` | - |
|
||||
|
||||
### ActionItem
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| text | 按钮文本 | `string` | - |
|
||||
| icon | 图标组件 | `string`\| `VbenIcon` | - |
|
||||
| onClick | 点击回调 | `() => void` | - |
|
||||
| auth | 权限码,配合 `hasPermission` 过滤 | `string \| string[]` | - |
|
||||
| ifShow | 是否显示 | `boolean \| (() => boolean)` | `true` |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| loading | 加载状态 | `boolean` | `false` |
|
||||
| danger | 危险操作(红色文字) | `boolean` | `false` |
|
||||
| tooltip | 提示 | `string \| { content: string; side?: 'top' \| 'bottom' \| 'left' \| 'right' }` | - |
|
||||
| popConfirm | 气泡确认 | `TableActionPopConfirm` | - |
|
||||
| variant | 按钮样式变体 | `ButtonVariants['variant']` | `'link'` |
|
||||
| size | 按钮尺寸 | `ButtonVariants['size']` | `'sm'` |
|
||||
| key | 唯一标识 | `string \| number` | - |
|
||||
|
||||
### TableActionPopConfirm
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| title | 提示标题 | `string` | `'Are you sure?'` |
|
||||
| okText | 确认按钮文案 | `string` | `'OK'` |
|
||||
| cancelText | 取消按钮文案 | `string` | `'Cancel'` |
|
||||
| confirm | 确认回调;未提供时回退到 `action.onClick` | `() => void` | - |
|
||||
|
|
@ -3,8 +3,8 @@ import { h } from 'vue';
|
|||
|
||||
import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import { BadgeJapaneseYen } from '@lucide/vue';
|
||||
import { Input, RadioGroup, Select } from 'antdv-next';
|
||||
import { BadgeJapaneseYen } from 'lucide-vue-next';
|
||||
|
||||
function showPrompt() {
|
||||
prompt({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts" setup>
|
||||
import { VbenDescriptions } from '@vben/common-ui';
|
||||
|
||||
const items = [
|
||||
{ content: 'Vben', label: '用户名' },
|
||||
{ content: '13800138000', label: '手机号' },
|
||||
{ content: '中国 · 杭州', label: '居住地' },
|
||||
{ content: '前端工程师', label: '职位' },
|
||||
{
|
||||
content: '这是一段较长的备注信息,用于演示跨列展示。',
|
||||
label: '备注',
|
||||
span: 3,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<VbenDescriptions :items="items" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts" setup>
|
||||
import { VbenDescriptions } from '@vben/common-ui';
|
||||
|
||||
const items = [
|
||||
{ content: 'Vben', label: '用户名' },
|
||||
{ content: '13800138000', label: '手机号' },
|
||||
{ content: '正常', label: '状态' },
|
||||
{ content: '中国 · 杭州', label: '居住地' },
|
||||
{
|
||||
content: '浙江省杭州市西湖区某某街道某某小区 1 幢 2 单元',
|
||||
label: '地址',
|
||||
span: 3,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<VbenDescriptions bordered title="用户信息" :items="items">
|
||||
<template #extra>
|
||||
<span style="color: #1677ff; cursor: pointer">编辑</span>
|
||||
</template>
|
||||
</VbenDescriptions>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import { VbenDescriptions, VbenDescriptionsItem } from '@vben/common-ui';
|
||||
</script>
|
||||
<template>
|
||||
<!-- 通过子组件 VbenDescriptionsItem 声明列表项 -->
|
||||
<VbenDescriptions bordered :column="2">
|
||||
<VbenDescriptionsItem label="用户名">Vben</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="状态">
|
||||
<span style="color: #52c41a">● 正常</span>
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="备注" :span="2">
|
||||
<template #content>
|
||||
<span style="color: #888">通过 #content 插槽自定义内容</span>
|
||||
</template>
|
||||
</VbenDescriptionsItem>
|
||||
</VbenDescriptions>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts" setup>
|
||||
import { VbenDescriptions } from '@vben/common-ui';
|
||||
|
||||
const items = [
|
||||
{ content: 'Vben', label: '用户名' },
|
||||
{ content: '13800138000', label: '手机号' },
|
||||
{ content: '中国 · 杭州', label: '居住地' },
|
||||
{ content: '前端工程师', label: '职位' },
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div style="display: flex; flex-direction: column; gap: 16px">
|
||||
<VbenDescriptions
|
||||
size="small"
|
||||
bordered
|
||||
title="Small"
|
||||
:column="2"
|
||||
:items="items"
|
||||
/>
|
||||
<VbenDescriptions
|
||||
size="middle"
|
||||
bordered
|
||||
title="Middle"
|
||||
:column="2"
|
||||
:items="items"
|
||||
/>
|
||||
<VbenDescriptions
|
||||
size="large"
|
||||
bordered
|
||||
title="Large"
|
||||
:column="2"
|
||||
:items="items"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts" setup>
|
||||
import { VbenDescriptions } from '@vben/common-ui';
|
||||
|
||||
const items = [
|
||||
{ content: '1', label: 'A' },
|
||||
{ content: '2(span: 2)', label: 'B', span: 2 },
|
||||
{ content: '3', label: 'C' },
|
||||
{ content: '占满当前行剩余空间', label: 'D(span: filled)', span: 'filled' },
|
||||
{ content: '5', label: 'E' },
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<!-- 列数随断点变化:xs 1 列、sm 2 列、md 及以上 3 列 -->
|
||||
<VbenDescriptions bordered :column="{ md: 3, sm: 2, xs: 1 }" :items="items" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts" setup>
|
||||
import { VbenDescriptions } from '@vben/common-ui';
|
||||
|
||||
const items = [
|
||||
{ content: 'Vben', label: '用户名' },
|
||||
{ content: '13800138000', label: '手机号' },
|
||||
{ content: '中国 · 杭州', label: '居住地' },
|
||||
{ content: '这是一段较长的备注信息。', label: '备注', span: 3 },
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<VbenDescriptions bordered layout="vertical" :items="items" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ActionItem } from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
const last = ref('无');
|
||||
|
||||
const actions: ActionItem[] = [
|
||||
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
|
||||
{ key: 'detail', onClick: () => (last.value = '详情'), text: '详情' },
|
||||
{
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
onClick: () => (last.value = '删除'),
|
||||
text: '删除',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VbenTableAction :actions="actions" align="start" divider />
|
||||
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
|
||||
最近点击:{{ last }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ActionItem } from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
const last = ref('无');
|
||||
|
||||
const actions: ActionItem[] = [
|
||||
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
|
||||
];
|
||||
|
||||
const dropdownActions: ActionItem[] = [
|
||||
{ key: 'copy', onClick: () => (last.value = '复制'), text: '复制' },
|
||||
{ key: 'export', onClick: () => (last.value = '导出'), text: '导出' },
|
||||
{
|
||||
danger: true,
|
||||
key: 'remove',
|
||||
// 下拉项同样支持气泡确认
|
||||
popConfirm: {
|
||||
cancelText: '取消',
|
||||
confirm: () => (last.value = '已移除'),
|
||||
okText: '确认',
|
||||
title: '确定移除吗?',
|
||||
},
|
||||
text: '移除',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VbenTableAction
|
||||
:actions="actions"
|
||||
:dropdown-actions="dropdownActions"
|
||||
align="start"
|
||||
divider
|
||||
more-text="更多"
|
||||
/>
|
||||
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
|
||||
最近点击:{{ last }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ActionItem } from '@vben/common-ui';
|
||||
|
||||
import { VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
// 模拟当前用户拥有的权限码
|
||||
const allow = new Set(['user:detail', 'user:edit']);
|
||||
|
||||
function hasPermission(auth?: string | string[]) {
|
||||
if (!auth) return true;
|
||||
const codes = Array.isArray(auth) ? auth : [auth];
|
||||
return codes.some((code) => allow.has(code));
|
||||
}
|
||||
|
||||
const actions: ActionItem[] = [
|
||||
{ auth: 'user:edit', key: 'edit', text: '编辑' },
|
||||
{ auth: 'user:detail', key: 'detail', text: '详情' },
|
||||
// 无 user:delete 权限,按钮被隐藏
|
||||
{ auth: 'user:delete', danger: true, key: 'delete', text: '删除(无权限)' },
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<VbenTableAction
|
||||
:actions="actions"
|
||||
:has-permission="hasPermission"
|
||||
align="start"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ActionItem } from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
const last = ref('无');
|
||||
|
||||
const actions: ActionItem[] = [
|
||||
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
|
||||
{
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
popConfirm: {
|
||||
cancelText: '取消',
|
||||
confirm: () => (last.value = '已删除'),
|
||||
okText: '确认',
|
||||
title: '确定删除这一行吗?',
|
||||
},
|
||||
text: '删除',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VbenTableAction :actions="actions" align="start" />
|
||||
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
|
||||
最近操作:{{ last }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ActionItem } from '@vben/common-ui';
|
||||
|
||||
import { VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
const actions: ActionItem[] = [
|
||||
{ key: 'edit', text: '编辑', tooltip: '编辑这一行' },
|
||||
{
|
||||
key: 'detail',
|
||||
text: '详情',
|
||||
tooltip: { content: '查看详情', side: 'top' },
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<VbenTableAction :actions="actions" align="start" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Descriptions
|
||||
|
||||
`Descriptions` displays a group of read-only fields, commonly used on detail pages and information previews. It is built on shadcn-ui with an API modeled after Ant Design Vue's Descriptions, supporting responsive columns, column spanning, borders, and vertical layout.
|
||||
|
||||
> If the documentation does not cover the details you need, please refer to the online examples.
|
||||
|
||||
::: info Before you start
|
||||
|
||||
The component supports two usages: data-driven via `items` (recommended), or declaring entries with the `VbenDescriptionsItem` child component. `items` takes precedence when both are provided. :::
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Pass an array of fields via `items`, each with a `label` and `content`. Columns adapt to breakpoints by default (1 column on `xs`, 2 on `sm`, 3 on `md` and above).
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/basic" />
|
||||
|
||||
## Bordered
|
||||
|
||||
Set `bordered` for a bordered style, combined with the `title` prop and the `#extra` slot (an action area on the right of the title).
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/bordered" />
|
||||
|
||||
## Vertical Layout
|
||||
|
||||
Use `layout="vertical"` to place labels above their content.
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/vertical" />
|
||||
|
||||
## Sizes
|
||||
|
||||
Use `size` to switch between `small`, `middle`, and `large`.
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/size" />
|
||||
|
||||
## Span & Responsive
|
||||
|
||||
Set `span` on an item to span multiple columns; `'filled'` fills the remaining space of the current row. `column` accepts a breakpoint-keyed object for responsive columns.
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/span" />
|
||||
|
||||
## Child Component Usage
|
||||
|
||||
When `items` is omitted, declare entries with `VbenDescriptionsItem` in the default slot. Content can be customized via the default slot or the `#content` slot.
|
||||
|
||||
<DemoPreview dir="demos/vben-descriptions/custom" />
|
||||
|
||||
## API
|
||||
|
||||
### Descriptions Props
|
||||
|
||||
| Prop | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| items | Data-driven entries; reads the default slot when omitted | `DescriptionsItemType[]` | - |
|
||||
| bordered | Whether to show borders | `boolean` | `false` |
|
||||
| column | Columns per row, supports breakpoint config | `number \| Partial<Record<Breakpoint, number>>` | `{ xs: 1, sm: 2, md: 3, xxxl: 4 }` |
|
||||
| layout | Layout direction | `'horizontal' \| 'vertical'` | `'horizontal'` |
|
||||
| size | Size | `'small' \| 'middle' \| 'large'` | `'middle'` |
|
||||
| colon | Show colon (only for non-bordered horizontal layout) | `boolean` | `true` |
|
||||
| title | Title | `string` | - |
|
||||
| extra | Action area on the right of the title | `string` | - |
|
||||
| labelStyle | Shared label style | `CSSProperties` | - |
|
||||
| contentStyle | Shared content style | `CSSProperties` | - |
|
||||
| class | Custom class for the root node | `string` | - |
|
||||
|
||||
### Descriptions Slots
|
||||
|
||||
| Slot | Description |
|
||||
| ------- | ------------------------------------- |
|
||||
| title | Custom title |
|
||||
| extra | Custom action area beside the title |
|
||||
| default | Place `VbenDescriptionsItem` children |
|
||||
|
||||
### DescriptionsItem
|
||||
|
||||
Each entry in `items`, or the props of the `VbenDescriptionsItem` child component.
|
||||
|
||||
| Prop | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| label | Label | `string \| number \| (() => VNode) \| Component` | - |
|
||||
| content | Content | `string \| number \| (() => VNode) \| Component` | - |
|
||||
| span | Columns to span, `'filled'` fills the rest of the row | `number \| 'filled' \| Partial<Record<Breakpoint, number>>` | `1` |
|
||||
| labelStyle | Label style | `CSSProperties` | - |
|
||||
| contentStyle | Content style | `CSSProperties` | - |
|
||||
| key | Unique key | `string \| number` | - |
|
||||
|
||||
### DescriptionsItem Slots
|
||||
|
||||
Available only for the child component usage.
|
||||
|
||||
| Slot | Description |
|
||||
| ------- | --------------------------------- |
|
||||
| default | Content (equivalent to `content`) |
|
||||
| content | Custom content |
|
||||
| label | Custom label |
|
||||
|
||||
::: tip Breakpoint
|
||||
|
||||
The responsive `Breakpoint` is one of `'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'`, with pixel values aligned with Ant Design (`sm` 576, `md` 768, `lg` 992, `xl` 1200, `xxl` 1600, `xxxl` 2000). :::
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben TableAction
|
||||
|
||||
`TableAction` renders a group of action buttons for table operation columns, inspired by the TableAction component from vben2. Built on shadcn-ui, it supports permission control, popconfirm, tooltips, a "more" dropdown, and dividers, and can be reused inside or outside tables.
|
||||
|
||||
> If the documentation does not cover the details you need, please refer to the online examples.
|
||||
|
||||
::: info Before you start
|
||||
|
||||
The component carries no business logic (it does not read the permission store directly); permissions are handled by injecting `hasPermission`, keeping the core layer decoupled and reusable across frameworks. Inside vxe-table, the recommended approach is to render it via a column slot (`slots: { default: 'action' }`) on the page, without changing the table's original rendering mechanism. :::
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Pass an array of action items via `actions`, each with `text`, `onClick`, etc. `danger` marks destructive actions, and `divider` shows separators between buttons.
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/basic" />
|
||||
|
||||
## Tooltip
|
||||
|
||||
Add a tooltip to an action via `tooltip`, accepting a string or a `{ content, side }` object.
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/tooltip" />
|
||||
|
||||
## PopConfirm
|
||||
|
||||
Use `popConfirm` to require confirmation before the action runs, commonly used for destructive actions like delete.
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/popconfirm" />
|
||||
|
||||
## More Dropdown
|
||||
|
||||
Use `dropdownActions` to collapse secondary actions into a "more" dropdown. `moreText` customizes the button label.
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/dropdown" />
|
||||
|
||||
## Permission Control
|
||||
|
||||
Set an `auth` code on an action and inject a `hasPermission` resolver; actions without permission are hidden.
|
||||
|
||||
<DemoPreview dir="demos/vben-table-action/permission" />
|
||||
|
||||
## Usage with vxe-table
|
||||
|
||||
Without changing vxe-table's rendering mechanism, declare a slot in the column config and render it on the page.
|
||||
|
||||
::: tip Recommended: use the adapter-wrapped version The project's `#/adapter/vxe-table` re-wraps `VbenTableAction` and injects `hasPermission` internally (based on `useAccess().hasAccessByCodes`). So when you import it from the adapter, **you no longer need to pass `:has-permission`** — just declare permission codes via the `auth` field of each action. :::
|
||||
|
||||
```ts
|
||||
// data.ts — declare a slot in the column config
|
||||
{
|
||||
align: 'center',
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: $t('system.user.operation'),
|
||||
width: 180,
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- list.vue — import from the adapter; permission is auto-injected, no has-permission needed -->
|
||||
<script setup lang="ts">
|
||||
import { VbenTableAction } from '#/adapter/vxe-table';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<template #action="{ row }">
|
||||
<template #action="{ row }">
|
||||
<VbenTableAction
|
||||
:actions="[
|
||||
{
|
||||
text: $t('common.detail'),
|
||||
icon: 'lucide:eye',
|
||||
onClick: () => onDetail(row),
|
||||
},
|
||||
{
|
||||
text: $t('common.edit'),
|
||||
icon: 'lucide:edit',
|
||||
onClick: () => onEdit(row),
|
||||
},
|
||||
]"
|
||||
:dropdown-actions="[
|
||||
{
|
||||
text: $t('common.delete'),
|
||||
icon: 'lucide:trash-2',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
auth: ['AC_100100'],
|
||||
},
|
||||
]"
|
||||
align="center"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
```
|
||||
|
||||
If you import the core component directly from `@vben/common-ui` (without going through the adapter), the component carries no business logic and you need to inject `hasPermission` yourself:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useAccess } from '@vben/access';
|
||||
import { VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
function hasPermission(auth?: string | string[]) {
|
||||
if (!auth) return true;
|
||||
return hasAccessByCodes(Array.isArray(auth) ? auth : [auth]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenTableAction
|
||||
v-bind="useActions(row, onActionClick)"
|
||||
:has-permission="hasPermission"
|
||||
align="center"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### TableAction Props
|
||||
|
||||
| Prop | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| actions | Main action buttons | `ActionItem[]` | `[]` |
|
||||
| dropdownActions | Actions inside the "more" dropdown | `ActionItem[]` | `[]` |
|
||||
| align | Alignment | `'start' \| 'center' \| 'end'` | `'end'` |
|
||||
| divider | Whether to show separators between buttons | `boolean` | `false` |
|
||||
| moreText | Label for the "more" button (shown beside the icon) | `string` | - |
|
||||
| hasPermission | Permission resolver; returning `false` hides the action with that `auth` (auto-injected when imported from `#/adapter/vxe-table`, no need to pass manually) | `(auth?: string \| string[]) => boolean` | - |
|
||||
| class | Custom class for the root node | `string` | - |
|
||||
|
||||
### ActionItem
|
||||
|
||||
| Prop | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| text | Button text | `string` | - |
|
||||
| icon | Icon component | `string` \| `VbenIcon` | - |
|
||||
| onClick | Click callback | `() => void` | - |
|
||||
| auth | Permission code, filtered by `hasPermission` | `string \| string[]` | - |
|
||||
| ifShow | Whether to show | `boolean \| (() => boolean)` | `true` |
|
||||
| disabled | Whether disabled | `boolean` | `false` |
|
||||
| loading | Loading state | `boolean` | `false` |
|
||||
| danger | Destructive action (red text) | `boolean` | `false` |
|
||||
| tooltip | Tooltip | `string \| { content: string; side?: 'top' \| 'bottom' \| 'left' \| 'right' }` | - |
|
||||
| popConfirm | PopConfirm | `TableActionPopConfirm` | - |
|
||||
| variant | Button variant | `ButtonVariants['variant']` | `'link'` |
|
||||
| size | Button size | `ButtonVariants['size']` | `'sm'` |
|
||||
| key | Unique key | `string \| number` | - |
|
||||
|
||||
### TableActionPopConfirm
|
||||
|
||||
| Prop | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| title | Confirm title | `string` | `'Are you sure?'` |
|
||||
| okText | Confirm button text | `string` | `'OK'` |
|
||||
| cancelText | Cancel button text | `string` | `'Cancel'` |
|
||||
| confirm | Confirm callback; falls back to `action.onClick` if omitted | `() => void` | - |
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
## Browser Support
|
||||
|
||||
- **Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
|
||||
- **Local development** is recommended using the **latest version of Chrome**. **Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+**.
|
||||
|
||||
- **Production environment** supports modern browsers, IE is not supported.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
## 浏览器支持
|
||||
|
||||
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
|
||||
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**` Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+。
|
||||
|
||||
- **生产环境**支持现代浏览器,不支持 IE。
|
||||
|
||||
|
|
|
|||
|
|
@ -106,5 +106,5 @@
|
|||
"node": "^22.18.0 || ^24.0.0",
|
||||
"pnpm": ">=11.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.2.2"
|
||||
"packageManager": "pnpm@11.4.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "catalog:",
|
||||
"lucide-vue-next": "catalog:",
|
||||
"@lucide/vue": "catalog:",
|
||||
"vue": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export {
|
|||
EyeOff,
|
||||
FoldHorizontal,
|
||||
Fullscreen,
|
||||
Github,
|
||||
Grid,
|
||||
Grip,
|
||||
GripVertical,
|
||||
|
|
@ -93,4 +92,4 @@ export {
|
|||
Unlink2,
|
||||
UserRoundPen,
|
||||
X,
|
||||
} from 'lucide-vue-next';
|
||||
} from '@lucide/vue';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const messages: Record<Locale, Record<string, string>> = {
|
|||
prompt: 'Prompt',
|
||||
reset: 'Reset',
|
||||
submit: 'Submit',
|
||||
confirmTitle: 'Please Confirm',
|
||||
},
|
||||
'zh-CN': {
|
||||
cancel: '取消',
|
||||
|
|
@ -18,6 +19,7 @@ export const messages: Record<Locale, Record<string, string>> = {
|
|||
prompt: '提示',
|
||||
reset: '重置',
|
||||
submit: '提交',
|
||||
confirmTitle: '请确认',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type {
|
|||
|
||||
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
|
||||
|
||||
import { useStore } from '@vben-core/shared/store';
|
||||
import { useSelector } from '@vben-core/shared/store';
|
||||
|
||||
import { FormApi } from './form-api';
|
||||
import VbenUseForm from './vben-use-form.vue';
|
||||
|
|
@ -19,7 +19,7 @@ export function useVbenForm<
|
|||
const api = new FormApi(options as unknown as VbenFormProps);
|
||||
const extendedApi: ExtendedFormApi = api as never;
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useStore(api.store, selector);
|
||||
return useSelector(api.store, selector);
|
||||
};
|
||||
|
||||
const Form = defineComponent(
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ const components = globalShareState.getComponents();
|
|||
const id = useId();
|
||||
provide('DISMISSABLE_DRAWER_ID', id);
|
||||
|
||||
// const wrapperRef = ref<HTMLElement>();
|
||||
// @ts-expect-error unused
|
||||
const wrapperRef = ref<HTMLElement>();
|
||||
const { $t } = useSimpleLocale();
|
||||
const { isMobile } = useIsMobile();
|
||||
|
||||
|
|
@ -285,8 +286,8 @@ const getForceMount = computed(() => {
|
|||
<SheetDescription />
|
||||
</VisuallyHidden>
|
||||
</template>
|
||||
<!-- 注释掉的部分 <div ref="wrapperRef" -->
|
||||
<div
|
||||
ref="wrapperRef"
|
||||
:class="
|
||||
cn('relative flex-1 overflow-y-auto p-3', contentClass, {
|
||||
'pointer-events-none': showLoading || submitting,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from 'vue';
|
||||
|
||||
import { usePreferences } from '@vben-core/preferences';
|
||||
import { useStore } from '@vben-core/shared/store';
|
||||
import { useSelector } from '@vben-core/shared/store';
|
||||
|
||||
import { DrawerApi } from './drawer-api';
|
||||
import VbenDrawer from './drawer.vue';
|
||||
|
|
@ -109,7 +109,7 @@ export function useVbenDrawer<
|
|||
const extendedApi: ExtendedDrawerApi = api as never;
|
||||
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useStore(api.store, selector);
|
||||
return useSelector(api.store, selector);
|
||||
};
|
||||
|
||||
const Drawer = defineComponent(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'vue';
|
||||
|
||||
import { usePreferences } from '@vben-core/preferences';
|
||||
import { useStore } from '@vben-core/shared/store';
|
||||
import { useSelector } from '@vben-core/shared/store';
|
||||
|
||||
import { ModalApi } from './modal-api';
|
||||
import VbenModal from './modal.vue';
|
||||
|
|
@ -116,7 +116,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||
const extendedApi: ExtendedModalApi = api as never;
|
||||
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useStore(api.store, selector);
|
||||
return useSelector(api.store, selector);
|
||||
};
|
||||
|
||||
const Modal = defineComponent(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@lucide/vue": "catalog:",
|
||||
"@vben-core/composables": "workspace:*",
|
||||
"@vben-core/design": "workspace:*",
|
||||
"@vben-core/icons": "workspace:*",
|
||||
|
|
@ -44,7 +45,6 @@
|
|||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"class-variance-authority": "catalog:",
|
||||
"lucide-vue-next": "catalog:",
|
||||
"reka-ui": "catalog:",
|
||||
"vee-validate": "catalog:",
|
||||
"vue": "catalog:"
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const { handleClick, visible } = useBackTop(props);
|
|||
:style="backTopStyle"
|
||||
class="data z-popup bg-background shadow-float hover:bg-heavy dark:bg-accent dark:hover:bg-heavy fixed bottom-10 size-10 rounded-full duration-500"
|
||||
size="icon"
|
||||
variant="icon"
|
||||
variant="ghost"
|
||||
@click="handleClick"
|
||||
>
|
||||
<ArrowUpToLine class="size-4" />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { AsTag } from 'reka-ui';
|
|||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { ButtonVariants, ButtonVariantSize } from '../../ui';
|
||||
import type { ButtonVariants } from '../../ui';
|
||||
|
||||
export interface VbenButtonProps {
|
||||
/**
|
||||
|
|
@ -19,8 +19,8 @@ export interface VbenButtonProps {
|
|||
class?: any;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
size?: ButtonVariantSize;
|
||||
variant?: ButtonVariants;
|
||||
size?: ButtonVariants['size'];
|
||||
variant?: ButtonVariants['variant'];
|
||||
}
|
||||
|
||||
export type CustomRenderType = (() => Component | string) | string;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ interface Props extends VbenButtonProps {
|
|||
tooltip?: string;
|
||||
tooltipDelayDuration?: number;
|
||||
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
||||
variant?: ButtonVariants;
|
||||
variant?: ButtonVariants['variant'];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
@ -24,7 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
onClick: () => {},
|
||||
tooltipDelayDuration: 200,
|
||||
tooltipSide: 'bottom',
|
||||
variant: 'icon',
|
||||
variant: 'ghost',
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
|
|||
|
||||
import { useNamespace } from '@vben-core/composables';
|
||||
|
||||
import { ChevronsDown } from 'lucide-vue-next';
|
||||
import { ChevronsDown } from '@lucide/vue';
|
||||
import {
|
||||
CollapsibleContent,
|
||||
CollapsibleRoot,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { ClassType } from '@vben-core/typings';
|
|||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ChevronsDown } from 'lucide-vue-next';
|
||||
import { ChevronsDown } from '@lucide/vue';
|
||||
import {
|
||||
CollapsibleContent,
|
||||
CollapsibleRoot,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import type { DescriptionsRenderNode, DescriptionsSize } from './types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { VbenRenderContent } from '../render-content';
|
||||
|
||||
interface Props {
|
||||
/** 是否边框模式 */
|
||||
bordered?: boolean;
|
||||
/** 是否显示冒号(仅非边框模式生效) */
|
||||
colon?: boolean;
|
||||
/** 内容 */
|
||||
content?: DescriptionsRenderNode | null;
|
||||
/** 内容样式 */
|
||||
contentStyle?: CSSProperties;
|
||||
/** 单项自定义类名 */
|
||||
itemClass?: string;
|
||||
/** 标签 */
|
||||
label?: DescriptionsRenderNode | null;
|
||||
/** 标签样式 */
|
||||
labelStyle?: CSSProperties;
|
||||
/** 尺寸 */
|
||||
size?: DescriptionsSize;
|
||||
/** 跨列数 */
|
||||
span?: number;
|
||||
/** 渲染标签 th 还是 td */
|
||||
tag: 'td' | 'th';
|
||||
/** 单元格类型 */
|
||||
type: 'content' | 'item' | 'label';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
bordered: false,
|
||||
colon: true,
|
||||
content: null,
|
||||
contentStyle: undefined,
|
||||
itemClass: undefined,
|
||||
label: null,
|
||||
labelStyle: undefined,
|
||||
size: 'middle',
|
||||
span: 1,
|
||||
});
|
||||
|
||||
const BORDERED_PADDING: Record<DescriptionsSize, string> = {
|
||||
large: 'px-6 py-4',
|
||||
middle: 'px-4 py-2.5',
|
||||
small: 'px-3 py-2',
|
||||
};
|
||||
|
||||
const PLAIN_PADDING: Record<DescriptionsSize, string> = {
|
||||
large: 'pb-6',
|
||||
middle: 'pb-4',
|
||||
small: 'pb-2',
|
||||
};
|
||||
|
||||
// 冒号通过伪元素追加,避免标签为渲染函数时无法拼接
|
||||
const COLON_CLASS = "after:content-[':']";
|
||||
|
||||
const hasLabel = computed(
|
||||
() => props.label !== null && props.label !== undefined,
|
||||
);
|
||||
const hasContent = computed(
|
||||
() => props.content !== null && props.content !== undefined,
|
||||
);
|
||||
|
||||
// 数字 0 会被 VbenRenderContent 当作 falsy 隐藏,这里转为字符串保证展示;
|
||||
// 同时将 null 归一为 undefined,匹配 VbenRenderContent 的 content 类型
|
||||
const displayLabel = computed(() => {
|
||||
if (props.label === null || props.label === undefined) return undefined;
|
||||
return typeof props.label === 'number' ? String(props.label) : props.label;
|
||||
});
|
||||
const displayContent = computed(() => {
|
||||
if (props.content === null || props.content === undefined) return undefined;
|
||||
return typeof props.content === 'number'
|
||||
? String(props.content)
|
||||
: props.content;
|
||||
});
|
||||
|
||||
const cellClass = computed(() => {
|
||||
if (props.bordered) {
|
||||
return cn(
|
||||
'border border-border align-top break-words',
|
||||
BORDERED_PADDING[props.size],
|
||||
props.type === 'label'
|
||||
? 'bg-muted/50 text-start font-normal text-foreground'
|
||||
: 'text-foreground',
|
||||
props.itemClass,
|
||||
);
|
||||
}
|
||||
return cn('align-top', PLAIN_PADDING[props.size], props.itemClass);
|
||||
});
|
||||
|
||||
const labelClass = computed(() =>
|
||||
cn('mr-2 shrink-0 text-muted-foreground', props.colon && COLON_CLASS),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="tag" :class="cellClass" :colspan="span">
|
||||
<!-- 边框模式:每个单元格仅承载 label 或 content -->
|
||||
<template v-if="bordered">
|
||||
<span v-if="hasLabel" :style="labelStyle">
|
||||
<VbenRenderContent :content="displayLabel" />
|
||||
</span>
|
||||
<span v-if="hasContent" :style="contentStyle">
|
||||
<VbenRenderContent :content="displayContent" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 非边框模式:label + content 容器 -->
|
||||
<div v-else class="flex">
|
||||
<span v-if="hasLabel" :class="labelClass" :style="labelStyle">
|
||||
<VbenRenderContent :content="displayLabel" />
|
||||
</span>
|
||||
<span
|
||||
v-if="hasContent"
|
||||
class="break-words text-foreground"
|
||||
:style="contentStyle"
|
||||
>
|
||||
<VbenRenderContent :content="displayContent" />
|
||||
</span>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { DescriptionsItemSpan, DescriptionsRenderNode } from './types';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { DESCRIPTIONS_ITEM_NAME } from './use-descriptions';
|
||||
|
||||
/**
|
||||
* 子节点用法的标记组件,本身不渲染任何内容。
|
||||
* 其 props 与默认插槽会被父级 VbenDescriptions 收集为列表项。
|
||||
*/
|
||||
const VbenDescriptionsItem = defineComponent({
|
||||
name: DESCRIPTIONS_ITEM_NAME,
|
||||
props: {
|
||||
content: {
|
||||
default: undefined,
|
||||
type: [
|
||||
String,
|
||||
Number,
|
||||
Function,
|
||||
Object,
|
||||
] as PropType<DescriptionsRenderNode>,
|
||||
},
|
||||
contentStyle: {
|
||||
default: undefined,
|
||||
type: Object,
|
||||
},
|
||||
label: {
|
||||
default: undefined,
|
||||
type: [
|
||||
String,
|
||||
Number,
|
||||
Function,
|
||||
Object,
|
||||
] as PropType<DescriptionsRenderNode>,
|
||||
},
|
||||
labelStyle: {
|
||||
default: undefined,
|
||||
type: Object,
|
||||
},
|
||||
span: {
|
||||
default: undefined,
|
||||
type: [Number, String, Object] as PropType<DescriptionsItemSpan>,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return () => null;
|
||||
},
|
||||
});
|
||||
|
||||
// 额外标记,便于在 vnode 中稳健识别
|
||||
(VbenDescriptionsItem as Record<string, any>).__isDescriptionsItem = true;
|
||||
|
||||
export default VbenDescriptionsItem;
|
||||
</script>
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import type { DescriptionsSize, InternalDescriptionsItem } from './types';
|
||||
|
||||
import DescriptionsCell from './descriptions-cell.vue';
|
||||
|
||||
interface Props {
|
||||
bordered?: boolean;
|
||||
colon?: boolean;
|
||||
contentStyle?: CSSProperties;
|
||||
labelStyle?: CSSProperties;
|
||||
row: InternalDescriptionsItem[];
|
||||
size?: DescriptionsSize;
|
||||
vertical?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
bordered: false,
|
||||
colon: true,
|
||||
contentStyle: undefined,
|
||||
labelStyle: undefined,
|
||||
size: 'middle',
|
||||
vertical: false,
|
||||
});
|
||||
|
||||
function mergeStyle(
|
||||
base?: CSSProperties,
|
||||
override?: CSSProperties,
|
||||
): CSSProperties | undefined {
|
||||
if (!base && !override) return undefined;
|
||||
return { ...base, ...override };
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 垂直布局:标签独占一行,内容独占一行 -->
|
||||
<template v-if="vertical">
|
||||
<tr>
|
||||
<DescriptionsCell
|
||||
v-for="(item, index) in row"
|
||||
:key="`label-${item.key ?? index}`"
|
||||
tag="th"
|
||||
type="label"
|
||||
:span="item.span ?? 1"
|
||||
:bordered="bordered"
|
||||
:colon="colon"
|
||||
:size="size"
|
||||
:label="item.label ?? null"
|
||||
:item-class="item.class"
|
||||
:label-style="mergeStyle(labelStyle, item.labelStyle)"
|
||||
/>
|
||||
</tr>
|
||||
<tr>
|
||||
<DescriptionsCell
|
||||
v-for="(item, index) in row"
|
||||
:key="`content-${item.key ?? index}`"
|
||||
tag="td"
|
||||
type="content"
|
||||
:span="item.span ?? 1"
|
||||
:bordered="bordered"
|
||||
:size="size"
|
||||
:content="item.content ?? null"
|
||||
:item-class="item.class"
|
||||
:content-style="mergeStyle(contentStyle, item.contentStyle)"
|
||||
/>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- 水平 + 边框:每项拆分为 label(th) 与 content(td) -->
|
||||
<tr v-else-if="bordered">
|
||||
<template v-for="(item, index) in row" :key="item.key ?? index">
|
||||
<DescriptionsCell
|
||||
tag="th"
|
||||
type="label"
|
||||
:span="1"
|
||||
:bordered="true"
|
||||
:size="size"
|
||||
:label="item.label ?? null"
|
||||
:item-class="item.class"
|
||||
:label-style="mergeStyle(labelStyle, item.labelStyle)"
|
||||
/>
|
||||
<DescriptionsCell
|
||||
tag="td"
|
||||
type="content"
|
||||
:span="(item.span ?? 1) * 2 - 1"
|
||||
:bordered="true"
|
||||
:size="size"
|
||||
:content="item.content ?? null"
|
||||
:content-style="mergeStyle(contentStyle, item.contentStyle)"
|
||||
/>
|
||||
</template>
|
||||
</tr>
|
||||
|
||||
<!-- 水平 + 非边框:每项一个单元格,label 与 content 同列 -->
|
||||
<tr v-else>
|
||||
<DescriptionsCell
|
||||
v-for="(item, index) in row"
|
||||
:key="item.key ?? index"
|
||||
tag="td"
|
||||
type="item"
|
||||
:span="item.span ?? 1"
|
||||
:colon="colon"
|
||||
:size="size"
|
||||
:label="item.label ?? null"
|
||||
:content="item.content ?? null"
|
||||
:item-class="item.class"
|
||||
:label-style="mergeStyle(labelStyle, item.labelStyle)"
|
||||
:content-style="mergeStyle(contentStyle, item.contentStyle)"
|
||||
/>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<script setup lang="ts">
|
||||
import type { VNode } from 'vue';
|
||||
|
||||
import type { DescriptionsItemType, DescriptionsProps } from './types';
|
||||
|
||||
import { computed, useSlots } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import DescriptionsRow from './descriptions-row.vue';
|
||||
import {
|
||||
calcRows,
|
||||
normalizeItems,
|
||||
parseItemsFromSlot,
|
||||
resolveColumn,
|
||||
useScreens,
|
||||
} from './use-descriptions';
|
||||
|
||||
defineOptions({ name: 'VbenDescriptions' });
|
||||
|
||||
const props = withDefaults(defineProps<DescriptionsProps>(), {
|
||||
bordered: false,
|
||||
class: undefined,
|
||||
colon: true,
|
||||
column: undefined,
|
||||
contentStyle: undefined,
|
||||
extra: undefined,
|
||||
items: undefined,
|
||||
labelStyle: undefined,
|
||||
layout: 'horizontal',
|
||||
size: 'middle',
|
||||
title: undefined,
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const screens = useScreens();
|
||||
|
||||
// 优先使用 items;否则从默认插槽中解析 VbenDescriptionsItem
|
||||
const resolvedItems = computed<DescriptionsItemType[]>(() => {
|
||||
if (props.items && props.items.length > 0) return props.items;
|
||||
const nodes = (slots.default?.() ?? []) as VNode[];
|
||||
return parseItemsFromSlot(nodes);
|
||||
});
|
||||
|
||||
const mergedColumn = computed(() => resolveColumn(props.column, screens.value));
|
||||
const mergedItems = computed(() =>
|
||||
normalizeItems(resolvedItems.value, screens.value),
|
||||
);
|
||||
const rows = computed(() => calcRows(mergedItems.value, mergedColumn.value));
|
||||
|
||||
const hasHeader = computed(
|
||||
() => !!props.title || !!props.extra || !!slots.title || !!slots.extra,
|
||||
);
|
||||
|
||||
const tableClass = computed(() =>
|
||||
cn(
|
||||
'w-full table-auto border-collapse text-sm',
|
||||
// 非边框模式下,去掉最后一行的底部间距
|
||||
!props.bordered && '[&>tbody>tr:last-child>td]:pb-0',
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('w-full', props.class)">
|
||||
<div v-if="hasHeader" class="mb-5 flex items-center justify-between gap-4">
|
||||
<div class="text-base font-semibold text-foreground">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</div>
|
||||
<div class="text-foreground">
|
||||
<slot name="extra">{{ extra }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table :class="tableClass">
|
||||
<tbody>
|
||||
<DescriptionsRow
|
||||
v-for="(row, index) in rows"
|
||||
:key="index"
|
||||
:row="row"
|
||||
:vertical="layout === 'vertical'"
|
||||
:bordered="bordered"
|
||||
:colon="colon"
|
||||
:size="size"
|
||||
:label-style="labelStyle"
|
||||
:content-style="contentStyle"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export { default as VbenDescriptionsItem } from './descriptions-item.vue';
|
||||
export { default as VbenDescriptions } from './descriptions.vue';
|
||||
|
||||
export * from './types';
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import type { Component, CSSProperties } from 'vue';
|
||||
|
||||
/** 响应式断点,与 antdv-next 保持一致 */
|
||||
export type DescriptionsBreakpoint =
|
||||
| 'lg'
|
||||
| 'md'
|
||||
| 'sm'
|
||||
| 'xl'
|
||||
| 'xs'
|
||||
| 'xxl'
|
||||
| 'xxxl';
|
||||
|
||||
/** 当前命中的断点集合 */
|
||||
export type ScreenMap = Partial<Record<DescriptionsBreakpoint, boolean>>;
|
||||
|
||||
export type DescriptionsLayout = 'horizontal' | 'vertical';
|
||||
|
||||
export type DescriptionsSize = 'large' | 'middle' | 'small';
|
||||
|
||||
/** 列数,可为固定数字或按断点配置 */
|
||||
export type DescriptionsColumn =
|
||||
| number
|
||||
| Partial<Record<DescriptionsBreakpoint, number>>;
|
||||
|
||||
/** 单项跨列,支持固定数字、'filled'(占满当前行剩余)或按断点配置 */
|
||||
export type DescriptionsItemSpan =
|
||||
| 'filled'
|
||||
| number
|
||||
| Partial<Record<DescriptionsBreakpoint, number>>;
|
||||
|
||||
/** 可渲染内容:字符串/数字/渲染函数/组件 */
|
||||
export type DescriptionsRenderNode = (() => any) | Component | number | string;
|
||||
|
||||
export interface DescriptionsItemType {
|
||||
/** 内容 */
|
||||
content?: DescriptionsRenderNode;
|
||||
/** 内容样式 */
|
||||
contentStyle?: CSSProperties;
|
||||
/** 唯一 key */
|
||||
key?: number | string;
|
||||
/** 标签 */
|
||||
label?: DescriptionsRenderNode;
|
||||
/** 标签样式 */
|
||||
labelStyle?: CSSProperties;
|
||||
/** 跨列 */
|
||||
span?: DescriptionsItemSpan;
|
||||
}
|
||||
|
||||
export interface DescriptionsProps {
|
||||
/** 是否展示边框 */
|
||||
bordered?: boolean;
|
||||
class?: any;
|
||||
/** 是否显示冒号(仅非 bordered 的水平布局生效) */
|
||||
colon?: boolean;
|
||||
/** 一行的列数 */
|
||||
column?: DescriptionsColumn;
|
||||
/** 统一的内容样式 */
|
||||
contentStyle?: CSSProperties;
|
||||
/** 操作区域,位于标题右侧 */
|
||||
extra?: string;
|
||||
/** 数据驱动的列表项;不传则读取默认插槽中的 VbenDescriptionsItem */
|
||||
items?: DescriptionsItemType[];
|
||||
/** 统一的标签样式 */
|
||||
labelStyle?: CSSProperties;
|
||||
/** 布局方式 */
|
||||
layout?: DescriptionsLayout;
|
||||
/** 尺寸 */
|
||||
size?: DescriptionsSize;
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface DescriptionsItemProps {
|
||||
content?: DescriptionsRenderNode;
|
||||
contentStyle?: CSSProperties;
|
||||
label?: DescriptionsRenderNode;
|
||||
labelStyle?: CSSProperties;
|
||||
span?: DescriptionsItemSpan;
|
||||
}
|
||||
|
||||
/** 归一化后的内部项,span 已解析为数字 */
|
||||
export interface InternalDescriptionsItem {
|
||||
_index?: number;
|
||||
class?: string;
|
||||
content?: DescriptionsRenderNode;
|
||||
contentStyle?: CSSProperties;
|
||||
filled?: boolean;
|
||||
key?: number | string;
|
||||
label?: DescriptionsRenderNode;
|
||||
labelStyle?: CSSProperties;
|
||||
span?: number;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import type { VNode } from 'vue';
|
||||
|
||||
import type {
|
||||
DescriptionsBreakpoint,
|
||||
DescriptionsColumn,
|
||||
DescriptionsItemType,
|
||||
InternalDescriptionsItem,
|
||||
ScreenMap,
|
||||
} from './types';
|
||||
|
||||
import { Comment, computed, Fragment } from 'vue';
|
||||
|
||||
import { useBreakpoints } from '@vueuse/core';
|
||||
|
||||
/** 默认列数映射 */
|
||||
export const DEFAULT_COLUMN_MAP: Record<DescriptionsBreakpoint, number> = {
|
||||
lg: 3,
|
||||
md: 3,
|
||||
sm: 2,
|
||||
xl: 3,
|
||||
xs: 1,
|
||||
xxl: 3,
|
||||
xxxl: 4,
|
||||
};
|
||||
|
||||
/** 由大到小的断点顺序,matchScreen 按此顺序取第一个命中的值 */
|
||||
const RESPONSIVE_ARRAY: DescriptionsBreakpoint[] = [
|
||||
'xxxl',
|
||||
'xxl',
|
||||
'xl',
|
||||
'lg',
|
||||
'md',
|
||||
'sm',
|
||||
'xs',
|
||||
];
|
||||
|
||||
/** 断点像素值 */
|
||||
const BREAKPOINT_PX = {
|
||||
sm: 576,
|
||||
md: 768,
|
||||
lg: 992,
|
||||
xl: 1200,
|
||||
xxl: 1600,
|
||||
xxxl: 2000,
|
||||
};
|
||||
|
||||
/**
|
||||
* 在给定的断点配置中,按由大到小的顺序取第一个命中的值
|
||||
*/
|
||||
export function matchScreen(
|
||||
screens: ScreenMap,
|
||||
screenSizes?: Partial<Record<DescriptionsBreakpoint, number>>,
|
||||
): number | undefined {
|
||||
if (!screenSizes) return undefined;
|
||||
for (const breakpoint of RESPONSIVE_ARRAY) {
|
||||
if (screens[breakpoint] && screenSizes[breakpoint] !== undefined) {
|
||||
return screenSizes[breakpoint];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听视口宽度,返回当前命中的断点集合
|
||||
*/
|
||||
export function useScreens() {
|
||||
const breakpoints = useBreakpoints(BREAKPOINT_PX);
|
||||
return computed<ScreenMap>(() => ({
|
||||
lg: breakpoints.lg.value,
|
||||
md: breakpoints.md.value,
|
||||
sm: breakpoints.sm.value,
|
||||
xl: breakpoints.xl.value,
|
||||
xs: !breakpoints.sm.value,
|
||||
xxl: breakpoints.xxl.value,
|
||||
xxxl: breakpoints.xxxl.value,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最终列数:固定数字直接返回,否则按断点解析
|
||||
*/
|
||||
export function resolveColumn(
|
||||
column: DescriptionsColumn | undefined,
|
||||
screens: ScreenMap,
|
||||
): number {
|
||||
if (typeof column === 'number') return column;
|
||||
return matchScreen(screens, { ...DEFAULT_COLUMN_MAP, ...column }) ?? 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化列表项:将 span 解析为数字,'filled' 标记为 filled
|
||||
*/
|
||||
export function normalizeItems(
|
||||
items: DescriptionsItemType[],
|
||||
screens: ScreenMap,
|
||||
): InternalDescriptionsItem[] {
|
||||
return items.map((item, index) => {
|
||||
const { span, ...rest } = item;
|
||||
if (span === 'filled') {
|
||||
return { ...rest, _index: index, filled: true };
|
||||
}
|
||||
return {
|
||||
...rest,
|
||||
_index: index,
|
||||
span: typeof span === 'number' ? span : matchScreen(screens, span),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 行装箱算法:根据列数与各项 span 将列表项拆分为多行,
|
||||
* 并补齐每行最后一项以占满列数。移植自 antdv-next useRow。
|
||||
*/
|
||||
export function calcRows(
|
||||
items: InternalDescriptionsItem[],
|
||||
column: number,
|
||||
): InternalDescriptionsItem[][] {
|
||||
let rows: InternalDescriptionsItem[][] = [];
|
||||
let tmpRow: InternalDescriptionsItem[] = [];
|
||||
let count = 0;
|
||||
|
||||
items.filter(Boolean).forEach((item) => {
|
||||
const { filled, ...rest } = item;
|
||||
// filled:占满当前行剩余,并立即换行
|
||||
if (filled) {
|
||||
tmpRow.push(rest);
|
||||
rows.push(tmpRow);
|
||||
tmpRow = [];
|
||||
count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const restSpan = column - count;
|
||||
count += item.span || 1;
|
||||
|
||||
if (count >= column) {
|
||||
// 超出列数时,将当前项 span 收敛为剩余列数,避免溢出
|
||||
tmpRow.push(count > column ? { ...rest, span: restSpan } : rest);
|
||||
rows.push(tmpRow);
|
||||
tmpRow = [];
|
||||
count = 0;
|
||||
} else {
|
||||
tmpRow.push(rest);
|
||||
}
|
||||
});
|
||||
|
||||
if (tmpRow.length > 0) rows.push(tmpRow);
|
||||
|
||||
// 补齐:若一行总 span 不足列数,扩展最后一项
|
||||
rows = rows.map((row) => {
|
||||
const total = row.reduce((acc, item) => acc + (item.span || 1), 0);
|
||||
if (total < column) {
|
||||
const last = row[row.length - 1];
|
||||
if (last) {
|
||||
last.span = column - (total - (last.span || 1));
|
||||
}
|
||||
}
|
||||
return row;
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 标记组件类型为 DescriptionsItem,便于从插槽 vnode 中识别 */
|
||||
export const DESCRIPTIONS_ITEM_NAME = 'VbenDescriptionsItem';
|
||||
|
||||
function isItemVNode(node: VNode): boolean {
|
||||
const type = node.type as any;
|
||||
return (
|
||||
!!type &&
|
||||
(type.__isDescriptionsItem === true || type.name === DESCRIPTIONS_ITEM_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
function flattenVNodes(nodes: VNode[]): VNode[] {
|
||||
const result: VNode[] = [];
|
||||
for (const node of nodes) {
|
||||
if (node.type === Fragment && Array.isArray(node.children)) {
|
||||
result.push(...flattenVNodes(node.children as VNode[]));
|
||||
} else if (node.type !== Comment) {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从默认插槽的 vnode 中解析出列表项,支持
|
||||
* <VbenDescriptionsItem label="..." :span="2">content</VbenDescriptionsItem> 写法
|
||||
*/
|
||||
export function parseItemsFromSlot(nodes: VNode[]): DescriptionsItemType[] {
|
||||
return flattenVNodes(nodes)
|
||||
.filter((node) => isItemVNode(node))
|
||||
.map((node) => {
|
||||
const props = (node.props ?? {}) as Record<string, any>;
|
||||
const children = (node.children ?? {}) as Record<string, any>;
|
||||
const labelSlot =
|
||||
typeof children.label === 'function' ? children.label : undefined;
|
||||
const contentDefaultSlot =
|
||||
typeof children.default === 'function' ? children.default : undefined;
|
||||
const contentSlot =
|
||||
typeof children.content === 'function'
|
||||
? children.content
|
||||
: contentDefaultSlot;
|
||||
return {
|
||||
class: props.class,
|
||||
content: contentSlot ?? props.content,
|
||||
contentStyle: props.contentStyle ?? props['content-style'],
|
||||
key: node.key ?? undefined,
|
||||
label: labelSlot ?? props.label,
|
||||
labelStyle: props.labelStyle ?? props['label-style'],
|
||||
span: props.span,
|
||||
style: props.style,
|
||||
} as DescriptionsItemType;
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export * from './checkbox';
|
|||
export * from './collapsible';
|
||||
export * from './context-menu';
|
||||
export * from './count-to-animator';
|
||||
export * from './descriptions';
|
||||
export * from './dropdown-menu';
|
||||
export * from './expandable-arrow';
|
||||
export * from './full-screen';
|
||||
|
|
@ -21,4 +22,5 @@ export * from './segmented';
|
|||
export * from './select';
|
||||
export * from './spine-text';
|
||||
export * from './spinner';
|
||||
export * from './table-action';
|
||||
export * from './tooltip';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { PinInputProps } from './types';
|
|||
|
||||
import { computed, onBeforeUnmount, ref, useId, watch } from 'vue';
|
||||
|
||||
import { PinInput, PinInputGroup, PinInputInput } from '../../ui';
|
||||
import { PinInput, PinInputGroup, PinInputSlot } from '../../ui';
|
||||
import { VbenButton } from '../button';
|
||||
|
||||
defineOptions({
|
||||
|
|
@ -101,7 +101,7 @@ const pinType = 'text' as const;
|
|||
>
|
||||
<div class="relative flex w-full">
|
||||
<PinInputGroup class="mr-2">
|
||||
<PinInputInput
|
||||
<PinInputSlot
|
||||
v-for="(item, index) in codeLength"
|
||||
:key="item"
|
||||
:index="index"
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ function handleScroll(event: Event) {
|
|||
></div>
|
||||
<ScrollBar
|
||||
v-if="horizontal"
|
||||
:class="scrollBarClass"
|
||||
:class="cn(scrollBarClass)"
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</ScrollArea>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function activeClass(tab: string): string[] {
|
|||
<Tabs v-model="activeTab" :default-value="getDefaultValue">
|
||||
<TabsList
|
||||
:style="tabsStyle"
|
||||
class="bg-accent outline-heavy! relative grid w-full outline! outline-2!"
|
||||
class="bg-accent outline-heavy! relative grid w-full outline-2!"
|
||||
>
|
||||
<TabsIndicator :style="tabsIndicatorStyle" />
|
||||
<template v-for="tab in tabs" :key="tab.value">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
<script setup lang="ts">
|
||||
import type { ActionItem } from './types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useSimpleLocale } from '@vben-core/composables';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '../../ui';
|
||||
import { VbenButton } from '../button';
|
||||
import { VbenIcon } from '../icon';
|
||||
|
||||
const props = defineProps<{ action: ActionItem }>();
|
||||
const emit = defineEmits<{ confirm: [] }>();
|
||||
const { $t } = useSimpleLocale();
|
||||
const open = ref(false);
|
||||
|
||||
const itemClass = computed(() =>
|
||||
cn(
|
||||
'cursor-pointer gap-2',
|
||||
props.action.danger && 'text-destructive focus:text-destructive',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* 阻止 reka-ui 事件的默认行为,用于:
|
||||
* - @select:阻止点击菜单项后自动关闭菜单,以便弹出气泡确认框;
|
||||
* - @open-auto-focus:阻止弹层抢占焦点(避免与菜单的焦点陷阱冲突);
|
||||
* - @focus-outside:阻止因菜单夺回焦点而被误判为「焦点移出」从而关闭弹层。
|
||||
*/
|
||||
function preventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (props.action.disabled) return;
|
||||
props.action.onClick?.();
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
open.value = false;
|
||||
const pc = props.action.popConfirm;
|
||||
if (pc?.confirm) {
|
||||
pc.confirm();
|
||||
} else {
|
||||
props.action.onClick?.();
|
||||
}
|
||||
// 确认后关闭整个下拉菜单
|
||||
emit('confirm');
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
open.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!--
|
||||
气泡确认:菜单项同时作为 Popover 触发器。
|
||||
通过双重 as-child(DropdownMenuItem + PopoverTrigger 均合并到同一个叶子元素),
|
||||
使该元素既是菜单项又是弹层触发器;@select 阻止点击后菜单自动关闭。
|
||||
-->
|
||||
<Popover v-if="action.popConfirm" v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<DropdownMenuItem
|
||||
as-child
|
||||
:class="itemClass"
|
||||
:disabled="action.disabled"
|
||||
@select="preventDefault"
|
||||
>
|
||||
<div>
|
||||
<VbenIcon v-if="action.icon" :icon="action.icon" class="size-4" />
|
||||
{{ action.text }}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
class="z-popup w-60"
|
||||
side="left"
|
||||
@focus-outside="preventDefault"
|
||||
@open-auto-focus="preventDefault"
|
||||
>
|
||||
<div class="text-foreground mb-3 text-sm">
|
||||
{{ action.popConfirm.title ?? $t('confirmTitle') }}
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<VbenButton size="sm" variant="outline" @click="onCancel">
|
||||
{{ action.popConfirm.cancelText ?? $t('cancel') }}
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
:variant="action.danger ? 'destructive' : 'default'"
|
||||
size="sm"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ action.popConfirm.okText ?? $t('confirm') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- 普通下拉项 -->
|
||||
<DropdownMenuItem
|
||||
v-else
|
||||
:class="itemClass"
|
||||
:disabled="action.disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<VbenIcon v-if="action.icon" :icon="action.icon" class="size-4" />
|
||||
{{ action.text }}
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<script setup lang="ts">
|
||||
import type { ActionItem } from './types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../ui';
|
||||
import { VbenButton } from '../button';
|
||||
import { VbenIcon } from '../icon';
|
||||
|
||||
const props = defineProps<{ action: ActionItem }>();
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
const buttonClass = computed(() =>
|
||||
cn(
|
||||
'gap-1',
|
||||
props.action.danger && 'text-destructive hover:text-destructive',
|
||||
props.action.class,
|
||||
),
|
||||
);
|
||||
|
||||
const variant = computed(() => props.action.variant ?? 'link');
|
||||
const size = computed(() => props.action.size ?? 'default');
|
||||
|
||||
function onClick() {
|
||||
if (props.action.disabled || props.action.loading) return;
|
||||
props.action.onClick?.();
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
open.value = false;
|
||||
const pc = props.action.popConfirm;
|
||||
if (pc?.confirm) {
|
||||
pc.confirm();
|
||||
} else {
|
||||
props.action.onClick?.();
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
open.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 气泡确认 -->
|
||||
<Popover v-if="action.popConfirm" v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<VbenButton
|
||||
:class="buttonClass"
|
||||
:disabled="action.disabled"
|
||||
:loading="action.loading"
|
||||
:size="size"
|
||||
class="p-2"
|
||||
:variant="variant"
|
||||
>
|
||||
<VbenIcon :icon="action.icon" v-if="action.icon" class="size-4" />
|
||||
<span v-if="action.text">{{ action.text }}</span>
|
||||
</VbenButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="z-popup w-60" side="top">
|
||||
<div class="text-foreground mb-3 text-sm">
|
||||
{{ action.popConfirm.title ?? 'Are you sure?' }}
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<VbenButton size="default" variant="outline" @click="onCancel">
|
||||
{{ action.popConfirm.cancelText ?? 'Cancel' }}
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
:variant="action.danger ? 'destructive' : 'default'"
|
||||
size="default"
|
||||
class="p-2"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ action.popConfirm.okText ?? 'OK' }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- 普通按钮 -->
|
||||
<VbenButton
|
||||
v-else
|
||||
:class="buttonClass"
|
||||
:disabled="action.disabled"
|
||||
:loading="action.loading"
|
||||
:size="size"
|
||||
class="p-2"
|
||||
:variant="variant"
|
||||
@click="onClick"
|
||||
>
|
||||
<VbenIcon :icon="action.icon" v-if="action.icon" class="size-4" />
|
||||
<span v-if="action.text">{{ action.text }}</span>
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { default as VbenTableAction } from './table-action.vue';
|
||||
|
||||
export type * from './types';
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
<script setup lang="ts">
|
||||
import type { ActionItem, TableActionProps } from './types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Ellipsis } from '@vben-core/icons';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Separator,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '../../ui';
|
||||
import { VbenButton } from '../button';
|
||||
import { VbenIcon } from '../icon';
|
||||
import ActionDropdownItemComp from './action-dropdown-item.vue';
|
||||
import ActionItemComp from './action-item.vue';
|
||||
|
||||
defineOptions({ name: 'VbenTableAction' });
|
||||
|
||||
const props = withDefaults(defineProps<TableActionProps>(), {
|
||||
actions: () => [],
|
||||
align: 'end',
|
||||
class: undefined,
|
||||
divider: false,
|
||||
dropdownActions: () => [],
|
||||
hasPermission: undefined,
|
||||
moreText: undefined,
|
||||
});
|
||||
|
||||
function checkVisible(item: ActionItem): boolean {
|
||||
// 权限
|
||||
if (item.auth && props.hasPermission && !props.hasPermission(item.auth)) {
|
||||
return false;
|
||||
}
|
||||
// ifShow
|
||||
if (typeof item.ifShow === 'boolean') return item.ifShow;
|
||||
if (typeof item.ifShow === 'function') return item.ifShow();
|
||||
return true;
|
||||
}
|
||||
|
||||
const visibleActions = computed(() =>
|
||||
(props.actions ?? []).filter((item) => checkVisible(item)),
|
||||
);
|
||||
const visibleDropdownActions = computed(() =>
|
||||
(props.dropdownActions ?? []).filter((item) => checkVisible(item)),
|
||||
);
|
||||
|
||||
const alignClass = computed(
|
||||
() =>
|
||||
({ center: 'justify-center', end: 'justify-end', start: 'justify-start' })[
|
||||
props.align
|
||||
],
|
||||
);
|
||||
|
||||
// 缓存根节点类名,避免每次渲染都执行 cn()(内部 tailwind-merge 解析开销较大)
|
||||
const wrapperClass = computed(() =>
|
||||
cn('flex items-center gap-1', alignClass.value, props.class),
|
||||
);
|
||||
|
||||
function tooltipSide(action: ActionItem) {
|
||||
return typeof action.tooltip === 'object'
|
||||
? (action.tooltip.side ?? 'top')
|
||||
: 'top';
|
||||
}
|
||||
function tooltipContent(action: ActionItem) {
|
||||
return typeof action.tooltip === 'object'
|
||||
? action.tooltip.content
|
||||
: action.tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预计算每个主操作的渲染视图模型:
|
||||
* - 普通按钮在本组件内直接渲染,不再为每个操作多包一层子组件,
|
||||
* 表格大量行时可显著减少组件实例数;
|
||||
* - 仅 popConfirm 操作仍交由子组件维护独立弹层状态;
|
||||
* - 类名等在此一次性计算并缓存,避免模板每次渲染都执行 cn()。
|
||||
*/
|
||||
const renderedActions = computed(() => {
|
||||
const list = visibleActions.value;
|
||||
return list.map((action, index) => {
|
||||
const hasTooltip = !!action.tooltip && !action.popConfirm;
|
||||
return {
|
||||
action,
|
||||
buttonClass: cn(
|
||||
'gap-1 p-2',
|
||||
action.danger && 'text-destructive hover:text-destructive',
|
||||
action.class,
|
||||
),
|
||||
hasTooltip,
|
||||
isConfirm: !!action.popConfirm,
|
||||
key: action.key ?? index,
|
||||
showDivider: props.divider && index < list.length - 1,
|
||||
size: action.size ?? 'default',
|
||||
tooltipContent: hasTooltip ? tooltipContent(action) : undefined,
|
||||
tooltipSide: hasTooltip ? tooltipSide(action) : 'top',
|
||||
variant: action.variant ?? 'link',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const dropdownOpen = ref(false);
|
||||
|
||||
function onActionClick(action: ActionItem) {
|
||||
if (action.disabled || action.loading) return;
|
||||
action.onClick?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当与气泡确认(Popover)交互时,避免误关闭整个下拉菜单。
|
||||
* Popover 内容被 Portal 渲染到菜单之外,默认会被判定为「点击外部」而关闭菜单。
|
||||
*/
|
||||
function onContentInteractOutside(event: Event) {
|
||||
const target = (event as CustomEvent).detail?.originalEvent?.target as
|
||||
| HTMLElement
|
||||
| null
|
||||
| undefined;
|
||||
if (target?.closest('[data-slot="popover-content"]')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<!-- 所有主操作共享同一个 TooltipProvider,避免每个 tooltip 各建一个 provider -->
|
||||
<TooltipProvider v-if="renderedActions.length > 0" :delay-duration="0">
|
||||
<template v-for="item in renderedActions" :key="item.key">
|
||||
<!-- 气泡确认:需独立弹层状态,交由子组件维护 -->
|
||||
<ActionItemComp v-if="item.isConfirm" :action="item.action" />
|
||||
|
||||
<!-- 带提示的普通按钮 -->
|
||||
<Tooltip v-else-if="item.hasTooltip">
|
||||
<TooltipTrigger as-child tabindex="-1">
|
||||
<VbenButton
|
||||
:class="item.buttonClass"
|
||||
:disabled="item.action.disabled"
|
||||
:loading="item.action.loading"
|
||||
:size="item.size"
|
||||
:variant="item.variant"
|
||||
@click="onActionClick(item.action)"
|
||||
>
|
||||
<VbenIcon
|
||||
v-if="item.action.icon"
|
||||
:icon="item.action.icon"
|
||||
class="size-4"
|
||||
/>
|
||||
<span v-if="item.action.text">{{ item.action.text }}</span>
|
||||
</VbenButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
:side="item.tooltipSide"
|
||||
class="side-content bg-accent text-popover-foreground rounded-md"
|
||||
>
|
||||
{{ item.tooltipContent }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<!-- 普通按钮 -->
|
||||
<VbenButton
|
||||
v-else
|
||||
:class="item.buttonClass"
|
||||
:disabled="item.action.disabled"
|
||||
:loading="item.action.loading"
|
||||
:size="item.size"
|
||||
:variant="item.variant"
|
||||
@click="onActionClick(item.action)"
|
||||
>
|
||||
<VbenIcon
|
||||
v-if="item.action.icon"
|
||||
:icon="item.action.icon"
|
||||
class="size-4"
|
||||
/>
|
||||
<span v-if="item.action.text">{{ item.action.text }}</span>
|
||||
</VbenButton>
|
||||
|
||||
<Separator v-if="item.showDivider" orientation="vertical" class="h-4" />
|
||||
</template>
|
||||
</TooltipProvider>
|
||||
|
||||
<DropdownMenu
|
||||
v-if="visibleDropdownActions.length > 0"
|
||||
v-model:open="dropdownOpen"
|
||||
>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<VbenButton class="gap-1 p-2" variant="link">
|
||||
<Ellipsis class="size-4" />
|
||||
<span v-if="moreText">{{ moreText }}</span>
|
||||
</VbenButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
@interact-outside="onContentInteractOutside"
|
||||
>
|
||||
<template
|
||||
v-for="(item, index) in visibleDropdownActions"
|
||||
:key="item.key ?? index"
|
||||
>
|
||||
<ActionDropdownItemComp
|
||||
:action="item"
|
||||
@confirm="dropdownOpen = false"
|
||||
/>
|
||||
<DropdownMenuSeparator
|
||||
v-if="divider && index < visibleDropdownActions.length - 1"
|
||||
/>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import type { ButtonVariants } from '../../ui';
|
||||
|
||||
import { VbenIcon } from '../icon';
|
||||
|
||||
/** 权限码:单个或多个,配合注入的 hasPermission 判断 */
|
||||
export type TableActionAuth = string | string[];
|
||||
|
||||
/** 操作按钮提示 */
|
||||
export interface TableActionTooltip {
|
||||
content: string;
|
||||
side?: 'bottom' | 'left' | 'right' | 'top';
|
||||
}
|
||||
|
||||
/** 气泡确认框配置 */
|
||||
export interface TableActionPopConfirm {
|
||||
/** 取消按钮文案 */
|
||||
cancelText?: string;
|
||||
/** 确认回调;未提供时回退到 action.onClick */
|
||||
confirm?: () => void;
|
||||
/** 确认按钮文案 */
|
||||
okText?: string;
|
||||
/** 提示标题 */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ActionItem {
|
||||
/** 权限码,配合注入的 hasPermission 过滤 */
|
||||
auth?: TableActionAuth;
|
||||
/** 自定义类名 */
|
||||
class?: any;
|
||||
/** 危险操作(红色文字) */
|
||||
danger?: boolean;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 图标组件 */
|
||||
icon?: typeof VbenIcon.icon;
|
||||
/** 是否显示:布尔或返回布尔的函数 */
|
||||
ifShow?: (() => boolean) | boolean;
|
||||
/** 唯一标识,点击回调可据此区分 */
|
||||
key?: number | string;
|
||||
/** 加载状态 */
|
||||
loading?: boolean;
|
||||
/** 点击回调 */
|
||||
onClick?: () => void;
|
||||
/** 气泡确认框 */
|
||||
popConfirm?: TableActionPopConfirm;
|
||||
/** 尺寸 */
|
||||
size?: ButtonVariants['size'];
|
||||
/** 文本 */
|
||||
text?: string;
|
||||
/** 提示:字符串或配置对象 */
|
||||
tooltip?: string | TableActionTooltip;
|
||||
/** 按钮样式变体 */
|
||||
variant?: ButtonVariants['variant'];
|
||||
}
|
||||
|
||||
export interface TableActionProps {
|
||||
/** 主操作按钮 */
|
||||
actions?: ActionItem[];
|
||||
/** 对齐方式 */
|
||||
align?: 'center' | 'end' | 'start';
|
||||
/** 自定义类名 */
|
||||
class?: any;
|
||||
/** 按钮之间是否显示分割线 */
|
||||
divider?: boolean;
|
||||
/** “更多”下拉中的操作 */
|
||||
dropdownActions?: ActionItem[];
|
||||
/**
|
||||
* 权限判断函数,返回 false 则隐藏对应 auth 的操作。
|
||||
* 核心组件不依赖业务,由使用方注入(如 useAccess().hasAccessByCodes)。
|
||||
*/
|
||||
hasPermission?: (auth?: TableActionAuth) => boolean;
|
||||
/** “更多”按钮文案(提供时显示在图标右侧) */
|
||||
moreText?: string;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { CircleHelp } from 'lucide-vue-next';
|
||||
import { CircleHelp } from '@lucide/vue';
|
||||
|
||||
import Tooltip from './tooltip.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const forwarded = useForwardPropsEmits(props, emits);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-bind="forwarded">
|
||||
<slot></slot>
|
||||
<AccordionRoot v-slot="slotProps" data-slot="accordion" v-bind="forwarded">
|
||||
<slot v-bind="slotProps"></slot>
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { AccordionContentProps } from 'reka-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AccordionContent } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AccordionContentProps & { class?: any }>();
|
||||
const props = defineProps<
|
||||
AccordionContentProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionContent
|
||||
data-slot="accordion-content"
|
||||
v-bind="delegatedProps"
|
||||
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import type { AccordionItemProps } from 'reka-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AccordionItem, useForwardProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AccordionItemProps & { class?: any }>();
|
||||
const props = defineProps<
|
||||
AccordionItemProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
|
||||
<slot></slot>
|
||||
<AccordionItem
|
||||
v-slot="slotProps"
|
||||
data-slot="accordion-item"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('border-b last:border-b-0', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps"></slot>
|
||||
</AccordionItem>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import type { AccordionTriggerProps } from 'reka-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { ChevronDown } from '@lucide/vue';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AccordionHeader, AccordionTrigger } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AccordionTriggerProps & { class?: any }>();
|
||||
const props = defineProps<
|
||||
AccordionTriggerProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionHeader class="flex">
|
||||
<AccordionTrigger
|
||||
data-slot="accordion-trigger"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
|
@ -31,7 +31,7 @@ const delegatedProps = computed(() => {
|
|||
<slot></slot>
|
||||
<slot name="icon">
|
||||
<ChevronDown
|
||||
class="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200"
|
||||
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||
/>
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ const forwarded = useForwardPropsEmits(props, emits);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogRoot v-bind="forwarded">
|
||||
<slot></slot>
|
||||
<AlertDialogRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="alert-dialog"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps"></slot>
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import type { AlertDialogActionProps } from 'reka-ui';
|
||||
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AlertDialogAction } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AlertDialogActionProps>();
|
||||
import { buttonVariants } from '../button';
|
||||
|
||||
const props = defineProps<
|
||||
AlertDialogActionProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogAction v-bind="props">
|
||||
<AlertDialogAction
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(buttonVariants(), props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</AlertDialogAction>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import type { AlertDialogCancelProps } from 'reka-ui';
|
||||
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AlertDialogCancel } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AlertDialogCancelProps>();
|
||||
import { buttonVariants } from '../button';
|
||||
|
||||
const props = defineProps<
|
||||
AlertDialogCancelProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogCancel v-bind="props">
|
||||
<AlertDialogCancel
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</AlertDialogCancel>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,21 @@ import type { AlertDialogContentEmits, AlertDialogContentProps } from 'reka-ui';
|
|||
|
||||
import type { ClassType } from '@vben-core/typings';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import {
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui';
|
||||
|
||||
import AlertDialogOverlay from './AlertDialogOverlay.vue';
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
|
|
@ -32,11 +36,7 @@ const emits = defineEmits<
|
|||
AlertDialogContentEmits & { close: []; closed: []; opened: [] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, modal: _modal, open: _open, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
|
|
@ -60,6 +60,8 @@ defineExpose({
|
|||
<AlertDialogPortal>
|
||||
<Transition name="fade" appear>
|
||||
<AlertDialogOverlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
|
||||
v-if="open && modal"
|
||||
:style="{
|
||||
...(zIndex ? { zIndex } : {}),
|
||||
|
|
@ -71,15 +73,14 @@ defineExpose({
|
|||
/>
|
||||
</Transition>
|
||||
<AlertDialogContent
|
||||
data-slot="alert-dialog-content"
|
||||
ref="contentRef"
|
||||
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
|
||||
@animationend="onAnimationEnd"
|
||||
v-bind="forwarded"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'z-popup bg-background p-6 shadow-lg outline-hidden sm:rounded-xl',
|
||||
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
||||
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
{
|
||||
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
|
||||
!centered,
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogDescriptionProps } from 'reka-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AlertDialogDescription, useForwardProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AlertDialogDescriptionProps & { class?: any }>();
|
||||
const props = defineProps<
|
||||
AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogDescription
|
||||
data-slot="alert-dialog-description"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
:class="
|
||||
cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useScrollLock } from '@vben-core/composables';
|
||||
|
||||
useScrollLock();
|
||||
</script>
|
||||
<template>
|
||||
<div class="z-popup bg-overlay inset-0"></div>
|
||||
</template>
|
||||
|
|
@ -1,29 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import type { AlertDialogTitleProps } from 'reka-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { AlertDialogTitle, useForwardProps } from 'reka-ui';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AlertDialogTitle } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AlertDialogTitleProps & { class?: any }>();
|
||||
const props = defineProps<
|
||||
AlertDialogTitleProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogTitle
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('text-lg leading-none font-semibold tracking-tight', props.class)
|
||||
"
|
||||
data-slot="alert-dialog-title"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-lg font-semibold', props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</AlertDialogTitle>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import type { AlertDialogTriggerProps } from 'reka-ui';
|
||||
|
||||
import { AlertDialogTrigger } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AlertDialogTriggerProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogTrigger data-slot="alert-dialog-trigger" v-bind="props">
|
||||
<slot></slot>
|
||||
</AlertDialogTrigger>
|
||||
</template>
|
||||
|
|
@ -3,4 +3,7 @@ export { default as AlertDialogAction } from './AlertDialogAction.vue';
|
|||
export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
|
||||
export { default as AlertDialogContent } from './AlertDialogContent.vue';
|
||||
export { default as AlertDialogDescription } from './AlertDialogDescription.vue';
|
||||
export { default as AlertDialogFooter } from './AlertDialogFooter.vue';
|
||||
export { default as AlertDialogHeader } from './AlertDialogHeader.vue';
|
||||
export { default as AlertDialogTitle } from './AlertDialogTitle.vue';
|
||||
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue';
|
||||
|
|
|
|||
|
|
@ -1,27 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import type { AvatarVariants } from './avatar';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { AvatarRoot } from 'reka-ui';
|
||||
|
||||
import { avatarVariant } from './avatar';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: any;
|
||||
shape?: AvatarVariants['shape'];
|
||||
size?: AvatarVariants['size'];
|
||||
}>(),
|
||||
{
|
||||
shape: 'circle',
|
||||
size: 'sm',
|
||||
},
|
||||
);
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
|
||||
<AvatarRoot
|
||||
data-slot="avatar"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</AvatarRoot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import type { AvatarFallbackProps } from 'reka-ui';
|
||||
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { AvatarFallback } from 'reka-ui';
|
||||
|
||||
const props = defineProps<AvatarFallbackProps>();
|
||||
const props = defineProps<
|
||||
AvatarFallbackProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarFallback v-bind="props">
|
||||
<AvatarFallback
|
||||
data-slot="avatar-fallback"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</AvatarFallback>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -7,5 +7,11 @@ const props = defineProps<AvatarImageProps>();
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
|
||||
<AvatarImage
|
||||
data-slot="avatar-image"
|
||||
v-bind="props"
|
||||
class="aspect-square size-full"
|
||||
>
|
||||
<slot></slot>
|
||||
</AvatarImage>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import type { VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
export const avatarVariant = cva(
|
||||
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
shape: {
|
||||
circle: 'rounded-full',
|
||||
square: 'rounded-md',
|
||||
},
|
||||
size: {
|
||||
base: 'h-16 w-16 text-2xl',
|
||||
lg: 'h-32 w-32 text-5xl',
|
||||
sm: 'h-10 w-10 text-xs',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type AvatarVariants = VariantProps<typeof avatarVariant>;
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export * from './avatar';
|
||||
export { default as Avatar } from './Avatar.vue';
|
||||
export { default as AvatarFallback } from './AvatarFallback.vue';
|
||||
export { default as AvatarImage } from './AvatarImage.vue';
|
||||
|
|
|
|||
|
|
@ -1,18 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import type { BadgeVariants } from './badge';
|
||||
import type { PrimitiveProps } from 'reka-ui';
|
||||
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import type { BadgeVariants } from '.';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { badgeVariants } from './badge';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { Primitive } from 'reka-ui';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
variant?: BadgeVariants['variant'];
|
||||
}>();
|
||||
import { badgeVariants } from '.';
|
||||
|
||||
const props = defineProps<
|
||||
PrimitiveProps & {
|
||||
class?: HTMLAttributes['class'];
|
||||
variant?: BadgeVariants['variant'];
|
||||
}
|
||||
>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn(badgeVariants({ variant }), props.class)">
|
||||
<Primitive
|
||||
data-slot="badge"
|
||||
:class="cn(badgeVariants({ variant }), props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,23 +3,23 @@ import type { VariantProps } from 'class-variance-authority';
|
|||
import { cva } from 'class-variance-authority';
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-md border border-border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-accent hover:bg-accent text-primary-foreground shadow-sm',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover',
|
||||
outline: 'text-foreground',
|
||||
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav :class="props.class" aria-label="breadcrumb" role="navigation">
|
||||
<nav aria-label="breadcrumb" data-slot="breadcrumb" :class="props.class">
|
||||
<slot></slot>
|
||||
</nav>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { MoreHorizontal } from 'lucide-vue-next';
|
||||
import { MoreHorizontal } from '@lucide/vue';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
|
||||
aria-hidden="true"
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('flex size-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="h-4 w-4" />
|
||||
<MoreHorizontal class="size-4" />
|
||||
</slot>
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
:class="
|
||||
cn('hover:text-foreground inline-flex items-center gap-1.5', props.class)
|
||||
"
|
||||
data-slot="breadcrumb-item"
|
||||
:class="cn('inline-flex items-center gap-1.5', props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
<script lang="ts" setup>
|
||||
import type { PrimitiveProps } from 'reka-ui';
|
||||
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { Primitive } from 'reka-ui';
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & { class?: any }>(), {
|
||||
as: 'a',
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
as: 'a',
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="breadcrumb-link"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('hover:text-foreground transition-colors', props.class)"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
:class="
|
||||
cn(
|
||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="cn('text-foreground font-normal', props.class)"
|
||||
aria-current="page"
|
||||
aria-disabled="true"
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
:class="cn('text-foreground font-normal', props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { ChevronRight } from 'lucide-vue-next';
|
||||
import { ChevronRight } from '@lucide/vue';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
:class="cn('[&>svg]:size-3.5', props.class)"
|
||||
aria-hidden="true"
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('[&>svg]:size-3.5', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronRight />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui';
|
||||
|
||||
import type { ButtonVariants, ButtonVariantSize } from './types';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import type { ButtonVariants } from './button';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
|
|
@ -10,19 +12,21 @@ import { Primitive } from 'reka-ui';
|
|||
import { buttonVariants } from './button';
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
class?: any;
|
||||
size?: ButtonVariantSize;
|
||||
variant?: ButtonVariants;
|
||||
class?: HTMLAttributes['class'];
|
||||
size?: ButtonVariants['size'];
|
||||
variant?: ButtonVariants['variant'];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button',
|
||||
class: '',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="button"
|
||||
:data-variant="variant"
|
||||
:data-size="size"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cva } from 'class-variance-authority';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
|
|
@ -32,3 +32,4 @@ export const buttonVariants = cva(
|
|||
},
|
||||
},
|
||||
);
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
export * from './button';
|
||||
|
||||
export { default as Button } from './Button.vue';
|
||||
|
||||
export type * from './types';
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
export type ButtonVariantSize =
|
||||
| 'default'
|
||||
| 'icon'
|
||||
| 'lg'
|
||||
| 'sm'
|
||||
| 'xs'
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export type ButtonVariants =
|
||||
| 'default'
|
||||
| 'destructive'
|
||||
| 'ghost'
|
||||
| 'heavy'
|
||||
| 'icon'
|
||||
| 'link'
|
||||
| 'outline'
|
||||
| 'secondary'
|
||||
| null
|
||||
| undefined;
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card"
|
||||
:class="
|
||||
cn(
|
||||
'border-border bg-card text-card-foreground rounded-xl border',
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-action"
|
||||
:class="
|
||||
cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('p-6 pt-0', props.class)">
|
||||
<div data-slot="card-content" :class="cn('px-6', props.class)">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :class="cn('text-muted-foreground text-sm', props.class)">
|
||||
<p
|
||||
data-slot="card-description"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</p>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 p-5', props.class)">
|
||||
<div
|
||||
data-slot="card-header"
|
||||
:class="
|
||||
cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: any;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3 :class="cn('leading-none font-semibold tracking-tight', props.class)">
|
||||
<h3
|
||||
data-slot="card-title"
|
||||
:class="cn('leading-none font-semibold', props.class)"
|
||||
>
|
||||
<slot></slot>
|
||||
</h3>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export { default as Card } from './Card.vue';
|
||||
export { default as CardAction } from './CardAction.vue';
|
||||
export { default as CardContent } from './CardContent.vue';
|
||||
export { default as CardDescription } from './CardDescription.vue';
|
||||
export { default as CardFooter } from './CardFooter.vue';
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
<script setup lang="ts">
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { Check, Minus } from 'lucide-vue-next';
|
||||
import { Check } from '@lucide/vue';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui';
|
||||
|
||||
const props = defineProps<
|
||||
CheckboxRootProps & { class?: any; indeterminate?: boolean }
|
||||
CheckboxRootProps & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
const emits = defineEmits<CheckboxRootEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CheckboxRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="checkbox"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'peer border-border hover:border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-sm border transition focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<CheckboxIndicator
|
||||
class="flex h-full w-full items-center justify-center text-current"
|
||||
data-slot="checkbox-indicator"
|
||||
class="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<slot>
|
||||
<component :is="indeterminate ? Minus : Check" class="h-4 w-4" />
|
||||
<slot v-bind="slotProps">
|
||||
<Check class="size-3.5" />
|
||||
</slot>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const forwarded = useForwardPropsEmits(props, emits);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRoot v-bind="forwarded">
|
||||
<ContextMenuRoot data-slot="context-menu" v-bind="forwarded">
|
||||
<slot></slot>
|
||||
</ContextMenuRoot>
|
||||
</template>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue