mirror of https://github.com/halo-dev/halo-admin
feat: default editor supports pasting or dragging pictures to upload (#825)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 添加复制或者拖拽图片到编辑器上传的支持。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3109 Fixes https://github.com/halo-dev/halo/issues/2946 #### Screenshots: #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note Console 端的默认编辑器支持拖拽或者粘贴图片上传 ```pull/836/head
parent
658a8ce4ee
commit
8f6d543f4d
|
@ -63,6 +63,7 @@
|
|||
"colorjs.io": "^0.4.2",
|
||||
"dayjs": "^1.11.6",
|
||||
"emoji-mart": "^5.3.3",
|
||||
"fastq": "^1.15.0",
|
||||
"floating-vue": "2.0.0-beta.20",
|
||||
"fuse.js": "^6.6.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
|
|
|
@ -62,6 +62,7 @@ importers:
|
|||
eslint: ^8.28.0
|
||||
eslint-plugin-cypress: ^2.12.1
|
||||
eslint-plugin-vue: ^9.8.0
|
||||
fastq: ^1.15.0
|
||||
floating-vue: 2.0.0-beta.20
|
||||
fuse.js: ^6.6.2
|
||||
husky: ^8.0.2
|
||||
|
@ -131,6 +132,7 @@ importers:
|
|||
colorjs.io: 0.4.2
|
||||
dayjs: 1.11.6
|
||||
emoji-mart: 5.3.3
|
||||
fastq: 1.15.0
|
||||
floating-vue: 2.0.0-beta.20_vue@3.2.45
|
||||
fuse.js: 6.6.2
|
||||
lodash.clonedeep: 4.5.0
|
||||
|
@ -2728,7 +2730,7 @@ packages:
|
|||
engines: {node: '>= 8'}
|
||||
dependencies:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.13.0
|
||||
fastq: 1.15.0
|
||||
|
||||
/@polka/url/1.0.0-next.21:
|
||||
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
|
||||
|
@ -5977,8 +5979,8 @@ packages:
|
|||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
dev: true
|
||||
|
||||
/fastq/1.13.0:
|
||||
resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
|
||||
/fastq/1.15.0:
|
||||
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ import {
|
|||
IconCharacterRecognition,
|
||||
IconLink,
|
||||
IconUserFollow,
|
||||
Toast,
|
||||
VTabItem,
|
||||
VTabs,
|
||||
} from "@halo-dev/components";
|
||||
|
@ -104,6 +105,11 @@ import {
|
|||
} from "vue";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { useAttachmentSelect } from "@/modules/contents/attachments/composables/use-attachment";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import * as fastq from "fastq";
|
||||
import type { queueAsPromised } from "fastq";
|
||||
import type { Attachment } from "@halo-dev/api-client";
|
||||
import { useFetchAttachmentPolicy } from "@/modules/contents/attachments/composables/use-attachment-policy";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -168,6 +174,7 @@ const editor = useEditor({
|
|||
ExtensionText,
|
||||
ExtensionImage.configure({
|
||||
inline: true,
|
||||
allowBase64: false,
|
||||
HTMLAttributes: {
|
||||
loading: "lazy",
|
||||
},
|
||||
|
@ -250,8 +257,144 @@ const editor = useEditor({
|
|||
handleGenerateTableOfContent();
|
||||
});
|
||||
},
|
||||
editorProps: {
|
||||
handleDrop: (view, event: DragEvent, _, moved) => {
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files) {
|
||||
const images = Array.from(event.dataTransfer.files).filter((file) =>
|
||||
file.type.startsWith("image/")
|
||||
) as File[];
|
||||
|
||||
if (images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
images.forEach((file, index) => {
|
||||
uploadQueue.push({
|
||||
file,
|
||||
process: (url: string) => {
|
||||
const { schema } = view.state;
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
if (!coordinates) return;
|
||||
|
||||
const node = schema.nodes.image.create({
|
||||
src: url,
|
||||
});
|
||||
|
||||
const transaction = view.state.tr.insert(
|
||||
coordinates.pos + index,
|
||||
node
|
||||
);
|
||||
|
||||
editor.value?.view.dispatch(transaction);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handlePaste: (view, event: ClipboardEvent, slice) => {
|
||||
const images = Array.from(event.clipboardData?.items || [])
|
||||
.map((item) => {
|
||||
return item.getAsFile();
|
||||
})
|
||||
.filter((file) => {
|
||||
return file && file.type.startsWith("image/");
|
||||
}) as File[];
|
||||
|
||||
if (images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
images.forEach((file) => {
|
||||
uploadQueue.push({
|
||||
file,
|
||||
process: (url: string) => {
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertContent([
|
||||
{
|
||||
type: "image",
|
||||
attrs: {
|
||||
src: url,
|
||||
},
|
||||
},
|
||||
])
|
||||
.run();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// image drag and paste upload
|
||||
const { policies } = useFetchAttachmentPolicy({ fetchOnMounted: true });
|
||||
|
||||
type Task = {
|
||||
file: File;
|
||||
process: (permalink: string) => void;
|
||||
};
|
||||
|
||||
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
||||
|
||||
async function asyncWorker(arg: Task): Promise<void> {
|
||||
if (!policies.value.length) {
|
||||
Toast.warning("目前没有可用的存储策略");
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: attachmentData } = await apiClient.attachment.uploadAttachment({
|
||||
file: arg.file,
|
||||
policyName: policies.value[0].metadata.name,
|
||||
});
|
||||
|
||||
const permalink = await handleFetchPermalink(attachmentData, 3);
|
||||
|
||||
if (permalink) {
|
||||
arg.process(permalink);
|
||||
}
|
||||
}
|
||||
|
||||
const handleFetchPermalink = async (
|
||||
attachment: Attachment,
|
||||
maxRetry: number
|
||||
): Promise<string | undefined> => {
|
||||
if (maxRetry === 0) {
|
||||
Toast.error(`获取附件永久链接失败:${attachment.spec.displayName}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { data } =
|
||||
await apiClient.extension.storage.attachment.getstorageHaloRunV1alpha1Attachment(
|
||||
{
|
||||
name: attachment.metadata.name,
|
||||
}
|
||||
);
|
||||
|
||||
if (data.status?.permalink) {
|
||||
return data.status.permalink;
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
const permalink = handleFetchPermalink(attachment, maxRetry - 1);
|
||||
clearTimeout(timer);
|
||||
resolve(permalink);
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
const toolbarMenuItems = computed(() => {
|
||||
if (!editor.value) return [];
|
||||
return [
|
||||
|
|
Loading…
Reference in New Issue