496 changed files with 14245 additions and 7807 deletions
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"singleQuote": true |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
--- |
||||
|
'@vben/styles': patch |
||||
|
'@vben-core/form-ui': patch |
||||
|
'@vben/web-naive': patch |
||||
|
--- |
||||
|
|
||||
|
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray |
||||
@ -1 +1 @@ |
|||||
22.22.0 |
24.16.0 |
||||
|
|||||
@ -0,0 +1,85 @@ |
|||||
|
import { faker } from '@faker-js/faker'; |
||||
|
import { eventHandler, getQuery } from 'h3'; |
||||
|
import { verifyAccessToken } from '~/utils/jwt-utils'; |
||||
|
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; |
||||
|
|
||||
|
const formatterCN = new Intl.DateTimeFormat('zh-CN', { |
||||
|
timeZone: 'Asia/Shanghai', |
||||
|
year: 'numeric', |
||||
|
month: '2-digit', |
||||
|
day: '2-digit', |
||||
|
hour: '2-digit', |
||||
|
minute: '2-digit', |
||||
|
second: '2-digit', |
||||
|
}); |
||||
|
|
||||
|
function generateMockDataList(count: number) { |
||||
|
const dataList = []; |
||||
|
|
||||
|
for (let i = 0; i < count; i++) { |
||||
|
const dataItem: Record<string, any> = { |
||||
|
id: faker.string.uuid(), |
||||
|
name: faker.commerce.product(), |
||||
|
status: faker.helpers.arrayElement([0, 1]), |
||||
|
createTime: formatterCN.format( |
||||
|
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }), |
||||
|
), |
||||
|
deptId: faker.string.uuid(), |
||||
|
remark: faker.lorem.sentence(), |
||||
|
}; |
||||
|
|
||||
|
dataList.push(dataItem); |
||||
|
} |
||||
|
|
||||
|
return dataList; |
||||
|
} |
||||
|
|
||||
|
const mockData = generateMockDataList(100); |
||||
|
|
||||
|
export default eventHandler(async (event) => { |
||||
|
const userinfo = verifyAccessToken(event); |
||||
|
if (!userinfo) { |
||||
|
return unAuthorizedResponse(event); |
||||
|
} |
||||
|
|
||||
|
const { |
||||
|
page = 1, |
||||
|
pageSize = 20, |
||||
|
name, |
||||
|
id, |
||||
|
remark, |
||||
|
startTime, |
||||
|
endTime, |
||||
|
deptId, |
||||
|
status, |
||||
|
} = getQuery(event); |
||||
|
let listData = structuredClone(mockData); |
||||
|
if (name) { |
||||
|
listData = listData.filter((item) => |
||||
|
item.name.toLowerCase().includes(String(name).toLowerCase()), |
||||
|
); |
||||
|
} |
||||
|
if (id) { |
||||
|
listData = listData.filter((item) => |
||||
|
item.id.toLowerCase().includes(String(id).toLowerCase()), |
||||
|
); |
||||
|
} |
||||
|
if (remark) { |
||||
|
listData = listData.filter((item) => |
||||
|
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()), |
||||
|
); |
||||
|
} |
||||
|
if (startTime) { |
||||
|
listData = listData.filter((item) => item.createTime >= startTime); |
||||
|
} |
||||
|
if (endTime) { |
||||
|
listData = listData.filter((item) => item.createTime <= endTime); |
||||
|
} |
||||
|
if (['0', '1'].includes(status as string)) { |
||||
|
listData = listData.filter((item) => item.status === Number(status)); |
||||
|
} |
||||
|
if (deptId) { |
||||
|
listData = listData.filter((item) => item.deptId === deptId); |
||||
|
} |
||||
|
return usePageResponseSuccess(page as string, pageSize as string, listData); |
||||
|
}); |
||||
@ -1,3 +1,4 @@ |
|||||
export * from './auth'; |
export * from './auth'; |
||||
export * from './menu'; |
export * from './menu'; |
||||
|
export * from './upload'; |
||||
export * from './user'; |
export * from './user'; |
||||
|
|||||
@ -0,0 +1,25 @@ |
|||||
|
import { requestClient } from '#/api/request'; |
||||
|
|
||||
|
interface UploadFileParams { |
||||
|
file: File; |
||||
|
onError?: (error: Error) => void; |
||||
|
onProgress?: (progress: { percent: number }) => void; |
||||
|
onSuccess?: (data: any, file: File) => void; |
||||
|
} |
||||
|
export async function upload_file({ |
||||
|
file, |
||||
|
onError, |
||||
|
onProgress, |
||||
|
onSuccess, |
||||
|
}: UploadFileParams) { |
||||
|
try { |
||||
|
onProgress?.({ percent: 0 }); |
||||
|
|
||||
|
const data = await requestClient.upload('/upload', { file }); |
||||
|
|
||||
|
onProgress?.({ percent: 100 }); |
||||
|
onSuccess?.(data, file); |
||||
|
} catch (error) { |
||||
|
onError?.(error instanceof Error ? error : new Error(String(error))); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
|
import { NButton, NCard, useMessage } from 'naive-ui'; |
||||
|
|
||||
|
import { useVbenForm } from '#/adapter/form'; |
||||
|
|
||||
|
const message = useMessage(); |
||||
|
|
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
layout: 'vertical', |
||||
|
wrapperClass: 'grid-cols-1', |
||||
|
handleSubmit: (values) => { |
||||
|
message.success(`提交成功:${JSON.stringify(values)}`); |
||||
|
}, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'projectName', |
||||
|
label: '项目名称', |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'VbenFormFieldArray', |
||||
|
fieldName: 'members', |
||||
|
label: '项目成员', |
||||
|
// 初始化为空数组,供内部 useFieldArray 使用 |
||||
|
defaultValue: [], |
||||
|
componentProps: { |
||||
|
min: 1, |
||||
|
max: 5, |
||||
|
createRow: () => ({ |
||||
|
name: null, |
||||
|
age: null, |
||||
|
role: null, |
||||
|
joinDate: null, |
||||
|
active: true, |
||||
|
}), |
||||
|
// 每一列就是一个子字段,复用 vbenForm 的所有编辑组件 |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'name', |
||||
|
label: '姓名', |
||||
|
rules: 'required', |
||||
|
componentProps: { placeholder: '请输入姓名' }, |
||||
|
}, |
||||
|
{ |
||||
|
component: 'InputNumber', |
||||
|
fieldName: 'age', |
||||
|
label: '年龄', |
||||
|
componentProps: { min: 0, max: 150 }, |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
fieldName: 'role', |
||||
|
label: '角色', |
||||
|
rules: 'selectRequired', |
||||
|
componentProps: { |
||||
|
placeholder: '请选择', |
||||
|
options: [ |
||||
|
{ label: '前端', value: 'fe' }, |
||||
|
{ label: '后端', value: 'be' }, |
||||
|
{ label: '测试', value: 'qa' }, |
||||
|
{ label: '产品', value: 'pm' }, |
||||
|
], |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
component: 'DatePicker', |
||||
|
fieldName: 'joinDate', |
||||
|
label: '入职日期', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Switch', |
||||
|
fieldName: 'active', |
||||
|
label: '在职', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
function setFormValues() { |
||||
|
formApi.setValues({ |
||||
|
projectName: 'Vben Admin', |
||||
|
members: [ |
||||
|
{ name: '张三', age: 28, role: 'fe', joinDate: Date.now(), active: true }, |
||||
|
{ |
||||
|
name: '李四', |
||||
|
age: 32, |
||||
|
role: 'be', |
||||
|
joinDate: Date.now(), |
||||
|
active: false, |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async function getFormValues() { |
||||
|
const values = await formApi.getValues(); |
||||
|
message.info(JSON.stringify(values)); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page |
||||
|
description="基于 useVbenForm 的数组编辑器(VbenFormFieldArray):可增删行,每个单元格复用 vbenForm 注册的编辑组件,并享受逐格校验。" |
||||
|
title="数组编辑器表单" |
||||
|
> |
||||
|
<NCard title="数组编辑器"> |
||||
|
<template #header-extra> |
||||
|
<NButton class="mr-2" @click="setFormValues">设置表单值</NButton> |
||||
|
<NButton class="mr-2" @click="getFormValues">获取表单值</NButton> |
||||
|
<NButton type="primary" @click="formApi.submitForm()"> |
||||
|
提交校验 |
||||
|
</NButton> |
||||
|
</template> |
||||
|
<Form /> |
||||
|
</NCard> |
||||
|
</Page> |
||||
|
</template> |
||||
@ -0,0 +1,2 @@ |
|||||
|
import { register } from 'node:module'; |
||||
|
register('./dayjs-resolve-hook.mjs', import.meta.url); |
||||
@ -0,0 +1,10 @@ |
|||||
|
const DAYJS_SUBPATH_RE = /^dayjs\/(plugin|locale)\/([^./]+)$/; |
||||
|
|
||||
|
/** @type {import('node:module').ResolveHook} */ |
||||
|
export async function resolve(specifier, context, nextResolve) { |
||||
|
const match = specifier.match(DAYJS_SUBPATH_RE); |
||||
|
if (match) { |
||||
|
return nextResolve(`${specifier}.js`, context); |
||||
|
} |
||||
|
return nextResolve(specifier, context); |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
--- |
||||
|
outline: deep |
||||
|
--- |
||||
|
|
||||
|
# Vben Cropper 图片裁剪 |
||||
|
|
||||
|
`VCropper` 是一个纯原生实现的图片裁剪组件,支持自由比例和固定比例裁剪,可通过方法调用获取裁剪后的图片。 |
||||
|
|
||||
|
> 如果文档内没有参数说明,可以尝试在在线示例内寻找 |
||||
|
|
||||
|
::: info 写在前面 |
||||
|
|
||||
|
如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。 |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
## 基础用法 |
||||
|
|
||||
|
最基本的图片裁剪,支持自由比例调整。 |
||||
|
|
||||
|
<DemoPreview dir="demos/vben-cropper/basic" /> |
||||
|
|
||||
|
## 固定比例裁剪 |
||||
|
|
||||
|
通过 `aspectRatio` 属性设置裁剪比例,格式为 `"宽:高"`,如 `"1:1"`、`"16:9"`、`"3:4"` 等。 |
||||
|
|
||||
|
<DemoPreview dir="demos/vben-cropper/aspect-ratio" /> |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### Props |
||||
|
|
||||
|
| 属性名 | 描述 | 类型 | 默认值 | |
||||
|
| ------------- | ------------------------------------- | -------- | ------ | |
||||
|
| `img` | 图片地址(必填) | `string` | - | |
||||
|
| `width` | 容器宽度 | `number` | `500` | |
||||
|
| `height` | 容器高度 | `number` | `400` | |
||||
|
| `aspectRatio` | 裁剪比例,格式如 `"1:1"`、`"16:9"` 等 | `string` | - | |
||||
|
|
||||
|
### Methods |
||||
|
|
||||
|
通过 `ref` 调用组件方法: |
||||
|
|
||||
|
```vue |
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { VCropper } from '@vben/common-ui'; |
||||
|
|
||||
|
const cropperRef = ref<InstanceType<typeof VCropper>>(); |
||||
|
|
||||
|
const handleCrop = async () => { |
||||
|
const result = await cropperRef.value?.getCropImage(); |
||||
|
// result 为 Blob 或 base64 字符串 |
||||
|
}; |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
#### getCropImage |
||||
|
|
||||
|
裁剪并获取图片。 |
||||
|
|
||||
|
```ts |
||||
|
interface GetCropImageOptions { |
||||
|
/** 输出图片格式 */ |
||||
|
format?: 'image/jpeg' | 'image/png'; |
||||
|
/** 压缩质量(0-1),仅对 jpeg 格式有效 */ |
||||
|
quality?: number; |
||||
|
/** 输出类型 */ |
||||
|
outputType?: 'base64' | 'blob'; |
||||
|
/** 目标宽度(可选,不传则为原始裁剪宽度) */ |
||||
|
targetWidth?: number; |
||||
|
/** 目标高度(可选,不传则为原始裁剪高度) */ |
||||
|
targetHeight?: number; |
||||
|
} |
||||
|
|
||||
|
getCropImage( |
||||
|
format?: 'image/jpeg' | 'image/png', |
||||
|
quality?: number, |
||||
|
outputType?: 'base64' | 'blob', |
||||
|
targetWidth?: number, |
||||
|
targetHeight?: number, |
||||
|
): Promise<Blob | string | undefined> |
||||
|
``` |
||||
|
|
||||
|
**参数说明:** |
||||
|
|
||||
|
| 参数 | 类型 | 默认值 | 描述 | |
||||
|
| --- | --- | --- | --- | |
||||
|
| `format` | `'image/jpeg' \| 'image/png'` | `'image/png'` | 输出图片格式 | |
||||
|
| `quality` | `number` | `0.92` | 压缩质量(0-1),仅 jpeg 有效 | |
||||
|
| `outputType` | `'base64' \| 'blob'` | `'blob'` | 输出类型,base64 字符串或 Blob 对象 | |
||||
|
| `targetWidth` | `number` | - | 目标宽度,不传则使用原始裁剪宽度 | |
||||
|
| `targetHeight` | `number` | - | 目标高度,不传则使用原始裁剪高度 | |
||||
|
|
||||
|
## 功能特性 |
||||
|
|
||||
|
### 裁剪操作 |
||||
|
|
||||
|
- **拖拽移动** - 拖拽裁剪框中心区域移动裁剪位置 |
||||
|
- **边角调整** - 拖拽四角调整裁剪框大小 |
||||
|
- **边缘调整** - 拖拽四边中点调整单边 |
||||
|
|
||||
|
### 比例控制 |
||||
|
|
||||
|
- **自由比例** - 不设置 `aspectRatio` 时,可自由调整任意比例 |
||||
|
- **固定比例** - 设置 `aspectRatio` 后,裁剪框始终保持设定比例 |
||||
|
|
||||
|
### 高清屏适配 |
||||
|
|
||||
|
组件自动适配 Retina 等高清屏幕,保证输出图片清晰无模糊。 |
||||
|
|
||||
|
### 图片适配 |
||||
|
|
||||
|
- 图片自动等比缩放以完整显示在容器内 |
||||
|
- 支持本地图片和网络图片 |
||||
|
- 网络图片需目标服务端支持 CORS 才能导出裁剪结果 |
||||
|
|
||||
|
## 使用示例 |
||||
|
|
||||
|
```vue |
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { VCropper } from '@vben/common-ui'; |
||||
|
|
||||
|
const cropperRef = ref<InstanceType<typeof VCropper>>(); |
||||
|
const imageUrl = ref('https://example.com/image.jpg'); |
||||
|
const croppedImage = ref(''); |
||||
|
|
||||
|
// 获取裁剪后的 Blob 对象 |
||||
|
const handleCropBlob = async () => { |
||||
|
const blob = await cropperRef.value?.getCropImage('image/jpeg', 0.9, 'blob'); |
||||
|
if (blob instanceof Blob) { |
||||
|
// 上传到服务器或创建预览URL |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
croppedImage.value = url; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 获取裁剪后的 base64 字符串 |
||||
|
const handleCropBase64 = async () => { |
||||
|
const base64 = await cropperRef.value?.getCropImage('image/png', 1, 'base64'); |
||||
|
if (typeof base64 === 'string') { |
||||
|
croppedImage.value = base64; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 导出指定尺寸 |
||||
|
const handleCropWithSize = async () => { |
||||
|
const blob = await cropperRef.value?.getCropImage( |
||||
|
'image/jpeg', |
||||
|
0.9, |
||||
|
'blob', |
||||
|
200, // 目标宽度 |
||||
|
200, // 目标高度 |
||||
|
); |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div> |
||||
|
<VCropper |
||||
|
ref="cropperRef" |
||||
|
:img="imageUrl" |
||||
|
:width="500" |
||||
|
:height="400" |
||||
|
aspect-ratio="1:1" |
||||
|
/> |
||||
|
<button @click="handleCropBlob">裁剪</button> |
||||
|
<img v-if="croppedImage" :src="croppedImage" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
``` |
||||
@ -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` | - | |
||||
@ -0,0 +1,102 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { onBeforeUnmount, ref } from 'vue'; |
||||
|
|
||||
|
import { VCropper } from '@vben/common-ui'; |
||||
|
|
||||
|
const cropperRef = ref<InstanceType<typeof VCropper>>(); |
||||
|
const aspectRatio = ref('1:1'); |
||||
|
const imageUrl = ref('https://picsum.photos/seed/cropper-ratio/800/600'); |
||||
|
const croppedImage = ref(''); |
||||
|
|
||||
|
const aspectOptions = [ |
||||
|
{ label: '1:1 (正方形)', value: '1:1' }, |
||||
|
{ label: '16:9 (宽屏)', value: '16:9' }, |
||||
|
{ label: '4:3 (标准)', value: '4:3' }, |
||||
|
{ label: '3:4 (竖版)', value: '3:4' }, |
||||
|
{ label: '3:2 (照片)', value: '3:2' }, |
||||
|
]; |
||||
|
|
||||
|
// 释放旧的 object URL 以避免内存泄漏 |
||||
|
const revokeCroppedImage = () => { |
||||
|
if (croppedImage.value?.startsWith('blob:')) { |
||||
|
URL.revokeObjectURL(croppedImage.value); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleCrop = async () => { |
||||
|
const blob = await cropperRef.value?.getCropImage('image/jpeg', 0.9, 'blob'); |
||||
|
if (blob instanceof Blob) { |
||||
|
// 释放旧的 URL |
||||
|
revokeCroppedImage(); |
||||
|
croppedImage.value = URL.createObjectURL(blob); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleReset = () => { |
||||
|
// 释放 URL |
||||
|
revokeCroppedImage(); |
||||
|
croppedImage.value = ''; |
||||
|
imageUrl.value = `https://picsum.photos/seed/cropper-${Date.now()}/800/600`; |
||||
|
}; |
||||
|
|
||||
|
// 组件卸载时清理 |
||||
|
onBeforeUnmount(() => { |
||||
|
revokeCroppedImage(); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div> |
||||
|
<div class="mb-4"> |
||||
|
<label class="text-sm text-gray-500 mr-2">选择比例:</label> |
||||
|
<select v-model="aspectRatio" class="px-3 py-1 border rounded text-sm"> |
||||
|
<option |
||||
|
v-for="option in aspectOptions" |
||||
|
:key="option.value" |
||||
|
:value="option.value" |
||||
|
> |
||||
|
{{ option.label }} |
||||
|
</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<VCropper |
||||
|
ref="cropperRef" |
||||
|
:img="imageUrl" |
||||
|
:width="500" |
||||
|
:height="300" |
||||
|
:aspect-ratio="aspectRatio" |
||||
|
/> |
||||
|
|
||||
|
<div class="mt-4 flex gap-2"> |
||||
|
<button |
||||
|
class="px-4 py-2 bg-blue-500 rounded hover:bg-blue-600" |
||||
|
@click="handleCrop" |
||||
|
> |
||||
|
裁剪图片 |
||||
|
</button> |
||||
|
<button |
||||
|
class="px-4 py-2 bg-gray-500 rounded hover:bg-gray-600" |
||||
|
@click="handleReset" |
||||
|
> |
||||
|
重置 |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="croppedImage" class="mt-4"> |
||||
|
<p class="text-sm text-gray-500 mb-2"> |
||||
|
裁剪结果 (比例: {{ aspectRatio }}): |
||||
|
</p> |
||||
|
<img :src="croppedImage" class="max-w-full rounded border" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="mt-4"> |
||||
|
<p class="text-sm text-gray-500">提示:</p> |
||||
|
<ul class="mt-2 text-xs text-gray-400 list-disc pl-4"> |
||||
|
<li>设置固定比例后,裁剪框始终维持该比例</li> |
||||
|
<li>切换比例会自动重新计算裁剪框大小</li> |
||||
|
<li>比例格式为 "宽:高",如 "16:9"</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,70 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { onBeforeUnmount, ref } from 'vue'; |
||||
|
|
||||
|
import { VCropper } from '@vben/common-ui'; |
||||
|
|
||||
|
const cropperRef = ref<InstanceType<typeof VCropper>>(); |
||||
|
const imageUrl = ref('https://picsum.photos/seed/cropper-demo/800/600'); |
||||
|
const croppedImage = ref(''); |
||||
|
|
||||
|
// 释放旧的 object URL 以避免内存泄漏 |
||||
|
const revokeCroppedImage = () => { |
||||
|
if (croppedImage.value?.startsWith('blob:')) { |
||||
|
URL.revokeObjectURL(croppedImage.value); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleCrop = async () => { |
||||
|
const blob = await cropperRef.value?.getCropImage('image/jpeg', 0.9, 'blob'); |
||||
|
if (blob instanceof Blob) { |
||||
|
// 释放旧的 URL |
||||
|
revokeCroppedImage(); |
||||
|
croppedImage.value = URL.createObjectURL(blob); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleReset = () => { |
||||
|
// 释放 URL |
||||
|
revokeCroppedImage(); |
||||
|
croppedImage.value = ''; |
||||
|
// 重新加载图片以重置裁剪框 |
||||
|
imageUrl.value = `https://picsum.photos/seed/cropper-${Date.now()}/800/600`; |
||||
|
}; |
||||
|
|
||||
|
// 组件卸载时清理 |
||||
|
onBeforeUnmount(() => { |
||||
|
revokeCroppedImage(); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div> |
||||
|
<VCropper ref="cropperRef" :img="imageUrl" :width="500" :height="300" /> |
||||
|
<div class="mt-4 flex gap-2"> |
||||
|
<button |
||||
|
class="px-4 py-2 bg-blue-500 rounded hover:bg-blue-600" |
||||
|
@click="handleCrop" |
||||
|
> |
||||
|
裁剪图片 |
||||
|
</button> |
||||
|
<button |
||||
|
class="px-4 py-2 bg-gray-500 rounded hover:bg-gray-600" |
||||
|
@click="handleReset" |
||||
|
> |
||||
|
重置 |
||||
|
</button> |
||||
|
</div> |
||||
|
<div v-if="croppedImage" class="mt-4"> |
||||
|
<p class="text-sm text-gray-500 mb-2">裁剪结果:</p> |
||||
|
<img :src="croppedImage" class="max-w-full rounded border" /> |
||||
|
</div> |
||||
|
<div class="mt-4"> |
||||
|
<p class="text-sm text-gray-500">提示:</p> |
||||
|
<ul class="mt-2 text-xs text-gray-400 list-disc pl-4"> |
||||
|
<li>拖拽裁剪框中心区域可移动裁剪位置</li> |
||||
|
<li>拖拽四角或四边可调整裁剪框大小</li> |
||||
|
<li>默认为自由比例,可调整为任意比例</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
@ -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,159 @@ |
|||||
|
--- |
||||
|
outline: deep |
||||
|
--- |
||||
|
|
||||
|
# Vben Cropper Image Cropping |
||||
|
|
||||
|
`VCropper` is a pure native image cropping component that supports both free and fixed aspect ratio cropping, with method-based access to cropped results. |
||||
|
|
||||
|
> If some details are not obvious from the docs, check the live demos as well. |
||||
|
|
||||
|
::: info Note |
||||
|
|
||||
|
If you feel the current component implementation doesn't meet your needs, you can use native components directly or create your own component. The components provided by the framework are not constraints - use them at your discretion. |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
## Basic Usage |
||||
|
|
||||
|
Basic image cropping with free aspect ratio adjustment. |
||||
|
|
||||
|
<DemoPreview dir="demos/vben-cropper/basic" /> |
||||
|
|
||||
|
## Fixed Aspect Ratio |
||||
|
|
||||
|
Set the cropping ratio via the `aspectRatio` prop. The format is `"width:height"`, e.g. `"1:1"`, `"16:9"`, `"3:4"`. |
||||
|
|
||||
|
<DemoPreview dir="demos/vben-cropper/aspect-ratio" /> |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### Props |
||||
|
|
||||
|
| Property | Description | Type | Default | |
||||
|
| ------------- | ---------------------------------- | -------- | ------- | |
||||
|
| `img` | Image URL (required) | `string` | - | |
||||
|
| `width` | Container width | `number` | `500` | |
||||
|
| `height` | Container height | `number` | `400` | |
||||
|
| `aspectRatio` | Crop ratio, e.g. `"1:1"`, `"16:9"` | `string` | - | |
||||
|
|
||||
|
### Methods |
||||
|
|
||||
|
Call component methods via `ref`: |
||||
|
|
||||
|
```vue |
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { VCropper } from '@vben/common-ui'; |
||||
|
|
||||
|
const cropperRef = ref<InstanceType<typeof VCropper>>(); |
||||
|
|
||||
|
const handleCrop = async () => { |
||||
|
const result = await cropperRef.value?.getCropImage(); |
||||
|
// result is a Blob or base64 string |
||||
|
}; |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
#### getCropImage |
||||
|
|
||||
|
Crop and retrieve the image. |
||||
|
|
||||
|
```ts |
||||
|
getCropImage( |
||||
|
format?: 'image/jpeg' | 'image/png', |
||||
|
quality?: number, |
||||
|
outputType?: 'base64' | 'blob', |
||||
|
targetWidth?: number, |
||||
|
targetHeight?: number, |
||||
|
): Promise<Blob | string | undefined> |
||||
|
``` |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
| Parameter | Type | Default | Description | |
||||
|
| --- | --- | --- | --- | |
||||
|
| `format` | `'image/jpeg' \| 'image/png'` | `'image/png'` | Output image format | |
||||
|
| `quality` | `number` | `0.92` | Compression quality (0-1), only effective for jpeg | |
||||
|
| `outputType` | `'base64' \| 'blob'` | `'blob'` | Output type, base64 string or Blob object | |
||||
|
| `targetWidth` | `number` | - | Target width, defaults to original crop width if omitted | |
||||
|
| `targetHeight` | `number` | - | Target height, defaults to original crop height if omitted | |
||||
|
|
||||
|
## Features |
||||
|
|
||||
|
### Cropping Operations |
||||
|
|
||||
|
- **Drag to Move** - Drag the center area of the crop box to move its position |
||||
|
- **Corner Resize** - Drag the four corners to resize the crop box |
||||
|
- **Edge Resize** - Drag the midpoints of edges to adjust a single side |
||||
|
|
||||
|
### Aspect Ratio Control |
||||
|
|
||||
|
- **Free Ratio** - Without `aspectRatio`, adjust the crop box to any ratio |
||||
|
- **Fixed Ratio** - With `aspectRatio` set, the crop box maintains the specified ratio |
||||
|
|
||||
|
### HiDPI Support |
||||
|
|
||||
|
The component automatically adapts to Retina and other high-DPI screens, ensuring crisp output images. |
||||
|
|
||||
|
### Image Fitting |
||||
|
|
||||
|
- Images are automatically scaled to fit within the container |
||||
|
- Supports both local and remote images |
||||
|
- Remote images require CORS support from the server to export cropped results |
||||
|
|
||||
|
## Usage Example |
||||
|
|
||||
|
```vue |
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { VCropper } from '@vben/common-ui'; |
||||
|
|
||||
|
const cropperRef = ref<InstanceType<typeof VCropper>>(); |
||||
|
const imageUrl = ref('https://example.com/image.jpg'); |
||||
|
const croppedImage = ref(''); |
||||
|
|
||||
|
// Get cropped Blob |
||||
|
const handleCropBlob = async () => { |
||||
|
const blob = await cropperRef.value?.getCropImage('image/jpeg', 0.9, 'blob'); |
||||
|
if (blob instanceof Blob) { |
||||
|
// Upload to server or create preview URL |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
croppedImage.value = url; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Get cropped base64 string |
||||
|
const handleCropBase64 = async () => { |
||||
|
const base64 = await cropperRef.value?.getCropImage('image/png', 1, 'base64'); |
||||
|
if (typeof base64 === 'string') { |
||||
|
croppedImage.value = base64; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Export with specific dimensions |
||||
|
const handleCropWithSize = async () => { |
||||
|
const blob = await cropperRef.value?.getCropImage( |
||||
|
'image/jpeg', |
||||
|
0.9, |
||||
|
'blob', |
||||
|
200, // target width |
||||
|
200, // target height |
||||
|
); |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div> |
||||
|
<VCropper |
||||
|
ref="cropperRef" |
||||
|
:img="imageUrl" |
||||
|
:width="500" |
||||
|
:height="400" |
||||
|
aspect-ratio="1:1" |
||||
|
/> |
||||
|
<button @click="handleCropBlob">Crop</button> |
||||
|
<img v-if="croppedImage" :src="croppedImage" /> |
||||
|
</div> |
||||
|
</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` | - | |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue