feat: add image editor feature for attachment upload component (#5585)

#### What type of PR is this?

/area ui
/kind feature
/milestone 2.14.x

#### What this PR does / why we need it:

为上传附件的组件添加基本的图片编辑功能。

<img width="747" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/6816e045-ae4a-4d26-b3a4-23494fb39d5f">

#### Which issue(s) this PR fixes:

Fixes #5583 


#### Does this PR introduce a user-facing change?

```release-note
为上传附件的组件添加基本的图片编辑功能。
```
pull/5590/head
Ryan Wang 2024-03-25 12:16:08 +08:00 committed by GitHub
parent 499b9eabb5
commit 2af92396d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 115 additions and 33 deletions

View File

@ -1,10 +1,10 @@
<script lang="ts" setup>
import {
VModal,
IconAddCircle,
VAlert,
VDropdown,
VDropdownItem,
VModal,
} from "@halo-dev/components";
import { ref, watch } from "vue";
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
@ -119,6 +119,7 @@ watch(
:body-class="['!p-0']"
:visible="visible"
:width="650"
:centered="false"
:title="$t('core.attachment.upload_modal.title')"
@update:visible="onVisibleChange"
>

View File

@ -64,6 +64,7 @@
"@uppy/dashboard": "^3.7.1",
"@uppy/drag-drop": "^3.0.3",
"@uppy/file-input": "^3.0.4",
"@uppy/image-editor": "^2.4.4",
"@uppy/locales": "^3.5.0",
"@uppy/progress-bar": "^3.0.4",
"@uppy/status-bar": "^3.2.5",

View File

@ -89,6 +89,9 @@ importers:
'@uppy/file-input':
specifier: ^3.0.4
version: 3.0.4(@uppy/core@3.8.0)
'@uppy/image-editor':
specifier: ^2.4.4
version: 2.4.4(@uppy/core@3.8.0)
'@uppy/locales':
specifier: ^3.5.0
version: 3.5.0
@ -5618,7 +5621,7 @@ packages:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.4.19(typescript@5.3.3)
vue-component-type-helpers: 2.0.6
vue-component-type-helpers: 2.0.7
transitivePeerDependencies:
- encoding
- supports-color
@ -6698,6 +6701,17 @@ packages:
preact: 10.11.2
dev: false
/@uppy/image-editor@2.4.4(@uppy/core@3.8.0):
resolution: {integrity: sha512-ASjv2Mh4mhlhkh1isDvIj3tlbERG77jo/ETwtajRSERGV6Ga9w1QGG7vL19bmNW9McHPFMZvOKNojAgv4SAtPg==}
peerDependencies:
'@uppy/core': ^3.9.3
dependencies:
'@uppy/core': 3.8.0
'@uppy/utils': 5.7.4
cropperjs: 1.5.7
preact: 10.11.2
dev: false
/@uppy/informer@3.0.4(@uppy/core@3.8.0):
resolution: {integrity: sha512-gzocdxn8qAFsW2EryehwjghladaBgv6Isjte53FTBV7o/vjaHPP6huKGbYpljyuQi8i9V+KrmvNGslofssgJ4g==}
peerDependencies:
@ -6777,6 +6791,13 @@ packages:
preact: 10.11.2
dev: false
/@uppy/utils@5.7.4:
resolution: {integrity: sha512-0Xsr7Xqdrb9mgfY3hi0YdhIaAxUw6qJasAflNMwNsyLGt3kH4pLfQHucolBKfWglVGtk1vfb49hZYvJGpcpzYA==}
dependencies:
lodash: 4.17.21
preact: 10.11.2
dev: false
/@uppy/vue@1.1.0(@uppy/core@3.8.0)(@uppy/dashboard@3.7.1)(@uppy/drag-drop@3.0.3)(@uppy/file-input@3.0.4)(@uppy/progress-bar@3.0.4)(@uppy/status-bar@3.2.5)(vue@3.4.19):
resolution: {integrity: sha512-xUKjLq8R0VBQHPDBb/f/bCL8+f51qSrTqYS6JXA4oFQ3QXICQN72E3rAPOyEwkiy+45JJMHA6uKeC+v6H9dcVg==}
peerDependencies:
@ -8763,6 +8784,10 @@ packages:
resolution: {integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==}
dev: false
/cropperjs@1.5.7:
resolution: {integrity: sha512-sGj+G/ofKh+f6A4BtXLJwtcKJgMUsXYVUubfTo9grERiDGXncttefmue/fyQFvn8wfdyoD1KhDRYLfjkJFl0yw==}
dev: false
/cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
dependencies:
@ -17566,8 +17591,8 @@ packages:
resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
dev: true
/vue-component-type-helpers@2.0.6:
resolution: {integrity: sha512-qdGXCtoBrwqk1BT6r2+1Wcvl583ZVkuSZ3or7Y1O2w5AvWtlvvxwjGhmz5DdPJS9xqRdDlgTJ/38ehWnEi0tFA==}
/vue-component-type-helpers@2.0.7:
resolution: {integrity: sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==}
dev: true
/vue-demi@0.13.11(vue@3.4.19):

View File

@ -2,9 +2,11 @@
import { Dashboard } from "@uppy/vue";
import "@uppy/core/dist/style.css";
import "@uppy/dashboard/dist/style.css";
import Uppy, { type SuccessResponse } from "@uppy/core";
import type { Restrictions } from "@uppy/core";
import Uppy, { type SuccessResponse } from "@uppy/core";
import XHRUpload from "@uppy/xhr-upload";
import ImageEditor from "@uppy/image-editor";
import "@uppy/image-editor/dist/style.min.css";
import zh_CN from "@uppy/locales/lib/zh_CN";
import zh_TW from "@uppy/locales/lib/zh_TW";
import en_US from "@uppy/locales/lib/en_US";
@ -62,38 +64,61 @@ const uppy = computed(() => {
meta: props.meta,
restrictions: props.restrictions,
autoProceed: props.autoProceed,
}).use(XHRUpload, {
endpoint: `${import.meta.env.VITE_API_URL}${props.endpoint}`,
allowedMetaFields: props.allowedMetaFields,
withCredentials: true,
formData: true,
fieldName: props.name,
method: props.method,
limit: 5,
timeout: 0,
getResponseError: (responseText: string, response: unknown) => {
try {
const response = JSON.parse(responseText);
if (typeof response === "object" && response && response) {
const { title, detail } = (response || {}) as ProblemDetail;
const message = [title, detail].filter(Boolean).join(": ");
})
.use(XHRUpload, {
endpoint: `${import.meta.env.VITE_API_URL}${props.endpoint}`,
allowedMetaFields: props.allowedMetaFields,
withCredentials: true,
formData: true,
fieldName: props.name,
method: props.method,
limit: 5,
timeout: 0,
getResponseError: (responseText: string, response: unknown) => {
try {
const response = JSON.parse(responseText);
if (typeof response === "object" && response && response) {
const { title, detail } = (response || {}) as ProblemDetail;
const message = [title, detail].filter(Boolean).join(": ");
if (message) {
Toast.error(message, { duration: 5000 });
if (message) {
Toast.error(message, { duration: 5000 });
return new Error(message);
return new Error(message);
}
}
} catch (e) {
const responseBody = response as XMLHttpRequest;
const { status, statusText } = responseBody;
const defaultMessage = [status, statusText].join(": ");
Toast.error(defaultMessage, { duration: 5000 });
return new Error(defaultMessage);
}
} catch (e) {
const responseBody = response as XMLHttpRequest;
const { status, statusText } = responseBody;
const defaultMessage = [status, statusText].join(": ");
Toast.error(defaultMessage, { duration: 5000 });
return new Error(defaultMessage);
}
return new Error("Internal Server Error");
},
});
return new Error("Internal Server Error");
},
})
.use(ImageEditor, {
locale: {
strings: {
revert: i18n.global.t("core.components.uppy.image_editor.revert"),
rotate: i18n.global.t("core.components.uppy.image_editor.rotate"),
zoomIn: i18n.global.t("core.components.uppy.image_editor.zoom_in"),
zoomOut: i18n.global.t("core.components.uppy.image_editor.zoom_out"),
flipHorizontal: i18n.global.t(
"core.components.uppy.image_editor.flip_horizontal"
),
aspectRatioSquare: i18n.global.t(
"core.components.uppy.image_editor.aspect_ratio_square"
),
aspectRatioLandscape: i18n.global.t(
"core.components.uppy.image_editor.aspect_ratio_landscape"
),
aspectRatioPortrait: i18n.global.t(
"core.components.uppy.image_editor.aspect_ratio_portrait"
),
},
},
});
});
uppy.value.on("upload-success", (_, response: SuccessResponse) => {

View File

@ -1462,6 +1462,16 @@ core:
editor_provider_selector:
tooltips:
disallow: The content format is different and cannot be switched
uppy:
image_editor:
revert: "Revert"
rotate: "Rotate"
zoom_in: "Zoom in"
zoom_out: "Zoom out"
flip_horizontal: "Flip horizontal"
aspect_ratio_square: "Crop square"
aspect_ratio_landscape: "Crop landscape (16:9)"
aspect_ratio_portrait: "Crop portrait (9:16)"
composables:
content_cache:
toast_recovered: Recovered unsaved content from cache

View File

@ -1408,6 +1408,16 @@ core:
editor_provider_selector:
tooltips:
disallow: 内容格式不同,无法切换
uppy:
image_editor:
revert: "恢复"
rotate: "旋转"
zoom_in: "放大"
zoom_out: "缩小"
flip_horizontal: "水平翻转"
aspect_ratio_square: "裁剪为正方形"
aspect_ratio_landscape: "裁剪为横向 (16:9)"
aspect_ratio_portrait: "裁剪为纵向 (9:16)"
composables:
content_cache:
toast_recovered: 已从缓存中恢复未保存的内容

View File

@ -1374,6 +1374,16 @@ core:
editor_provider_selector:
tooltips:
disallow: 內容格式不同,無法切換
uppy:
image_editor:
revert: "還原"
rotate: "旋轉"
zoom_in: "放大"
zoom_out: "縮小"
flip_horizontal: "水平翻轉"
aspect_ratio_square: "裁剪為正方形"
aspect_ratio_landscape: "裁剪為橫向 (16:9)"
aspect_ratio_portrait: "裁剪為縱向 (9:16)"
composables:
content_cache:
toast_recovered: 已從緩存中恢復未保存的內容