Browse Source

docs(@vben/docs): 添加 VCropper 图片裁剪组件文档 (#7904)

* docs(@vben/docs): 添加 VCropper 图片裁剪组件文档

- 新增中文文档 docs/src/components/common-ui/vben-cropper.md
- 新增英文文档 docs/src/en/components/common-ui/vben-cropper.md
- 新增基础用法示例 demos/vben-cropper/basic
- 新增固定比例裁剪示例 demos/vben-cropper/aspect-ratio
- 更新侧边栏配置添加 Cropper 入口

* fix: 更正跨域图片描述并修复 demo 内存泄漏

- 文档更正:网络图片导出裁剪结果需服务端 CORS 支持
- 修复 URL.createObjectURL 内存泄漏:添加 revokeObjectURL 释放
JyQAQ 1 tháng trước cách đây
mục cha
commit
aac4e88353

+ 4 - 0
docs/.vitepress/config/en.mts

@@ -198,6 +198,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
           link: 'common-ui/vben-ellipsis-text',
           text: 'EllipsisText',
         },
+        {
+          link: 'common-ui/vben-cropper',
+          text: 'Cropper',
+        },
         {
           link: 'common-ui/vben-tiptap',
           text: 'Tiptap RichTextEditor',

+ 4 - 0
docs/.vitepress/config/zh.mts

@@ -196,6 +196,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
           link: 'common-ui/vben-ellipsis-text',
           text: 'EllipsisText 省略文本',
         },
+        {
+          link: 'common-ui/vben-cropper',
+          text: 'Cropper 图片裁剪',
+        },
         {
           link: 'common-ui/vben-tiptap',
           text: 'Tiptap 富文本编辑器',

+ 172 - 0
docs/src/components/common-ui/vben-cropper.md

@@ -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>
+```

+ 102 - 0
docs/src/demos/vben-cropper/aspect-ratio/index.vue

@@ -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>

+ 70 - 0
docs/src/demos/vben-cropper/basic/index.vue

@@ -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>

+ 159 - 0
docs/src/en/components/common-ui/vben-cropper.md

@@ -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>
+```