@@ -81,6 +81,7 @@ import {
8181 IconCharacterRecognition ,
8282 IconLink ,
8383 IconUserFollow ,
84+ Toast ,
8485 VTabItem ,
8586 VTabs ,
8687} from " @halo-dev/components" ;
@@ -104,6 +105,11 @@ import {
104105} from " vue" ;
105106import { formatDatetime } from " @/utils/date" ;
106107import { useAttachmentSelect } from " @/modules/contents/attachments/composables/use-attachment" ;
108+ import { apiClient } from " @/utils/api-client" ;
109+ import * as fastq from " fastq" ;
110+ import type { queueAsPromised } from " fastq" ;
111+ import type { Attachment } from " @halo-dev/api-client" ;
112+ import { useFetchAttachmentPolicy } from " @/modules/contents/attachments/composables/use-attachment-policy" ;
107113
108114const props = withDefaults (
109115 defineProps <{
@@ -168,6 +174,7 @@ const editor = useEditor({
168174 ExtensionText ,
169175 ExtensionImage .configure ({
170176 inline: true ,
177+ allowBase64: false ,
171178 HTMLAttributes: {
172179 loading: " lazy" ,
173180 },
@@ -250,8 +257,144 @@ const editor = useEditor({
250257 handleGenerateTableOfContent ();
251258 });
252259 },
260+ editorProps: {
261+ handleDrop : (view , event : DragEvent , _ , moved ) => {
262+ if (! moved && event .dataTransfer && event .dataTransfer .files ) {
263+ const images = Array .from (event .dataTransfer .files ).filter ((file ) =>
264+ file .type .startsWith (" image/" )
265+ ) as File [];
266+
267+ if (images .length === 0 ) {
268+ return ;
269+ }
270+
271+ event .preventDefault ();
272+
273+ images .forEach ((file , index ) => {
274+ uploadQueue .push ({
275+ file ,
276+ process : (url : string ) => {
277+ const { schema } = view .state ;
278+ const coordinates = view .posAtCoords ({
279+ left: event .clientX ,
280+ top: event .clientY ,
281+ });
282+
283+ if (! coordinates ) return ;
284+
285+ const node = schema .nodes .image .create ({
286+ src: url ,
287+ });
288+
289+ const transaction = view .state .tr .insert (
290+ coordinates .pos + index ,
291+ node
292+ );
293+
294+ editor .value ?.view .dispatch (transaction );
295+ },
296+ });
297+ });
298+
299+ return true ;
300+ }
301+ return false ;
302+ },
303+ handlePaste : (view , event : ClipboardEvent , slice ) => {
304+ const images = Array .from (event .clipboardData ?.items || [])
305+ .map ((item ) => {
306+ return item .getAsFile ();
307+ })
308+ .filter ((file ) => {
309+ return file && file .type .startsWith (" image/" );
310+ }) as File [];
311+
312+ if (images .length === 0 ) {
313+ return ;
314+ }
315+
316+ event .preventDefault ();
317+
318+ images .forEach ((file ) => {
319+ uploadQueue .push ({
320+ file ,
321+ process : (url : string ) => {
322+ editor .value
323+ ?.chain ()
324+ .focus ()
325+ .insertContent ([
326+ {
327+ type: " image" ,
328+ attrs: {
329+ src: url ,
330+ },
331+ },
332+ ])
333+ .run ();
334+ },
335+ });
336+ });
337+ },
338+ },
253339});
254340
341+ // image drag and paste upload
342+ const { policies } = useFetchAttachmentPolicy ({ fetchOnMounted: true });
343+
344+ type Task = {
345+ file: File ;
346+ process: (permalink : string ) => void ;
347+ };
348+
349+ const uploadQueue: queueAsPromised <Task > = fastq .promise (asyncWorker , 1 );
350+
351+ async function asyncWorker(arg : Task ): Promise <void > {
352+ if (! policies .value .length ) {
353+ Toast .warning (" 目前没有可用的存储策略" );
354+ return ;
355+ }
356+
357+ const { data : attachmentData } = await apiClient .attachment .uploadAttachment ({
358+ file: arg .file ,
359+ policyName: policies .value [0 ].metadata .name ,
360+ });
361+
362+ const permalink = await handleFetchPermalink (attachmentData , 3 );
363+
364+ if (permalink ) {
365+ arg .process (permalink );
366+ }
367+ }
368+
369+ const handleFetchPermalink = async (
370+ attachment : Attachment ,
371+ maxRetry : number
372+ ): Promise <string | undefined > => {
373+ if (maxRetry === 0 ) {
374+ Toast .error (` 获取附件永久链接失败:${attachment .spec .displayName } ` );
375+ return undefined ;
376+ }
377+
378+ const { data } =
379+ await apiClient .extension .storage .attachment .getstorageHaloRunV1alpha1Attachment (
380+ {
381+ name: attachment .metadata .name ,
382+ }
383+ );
384+
385+ if (data .status ?.permalink ) {
386+ return data .status .permalink ;
387+ }
388+
389+ return await new Promise ((resolve ) => {
390+ const timer = setTimeout (() => {
391+ const permalink = handleFetchPermalink (attachment , maxRetry - 1 );
392+ clearTimeout (timer );
393+ resolve (permalink );
394+ }, 300 );
395+ });
396+ };
397+
255398const toolbarMenuItems = computed (() => {
256399 if (! editor .value ) return [];
257400 return [
0 commit comments