feat: add supports for copying attachment permalink (#3835)

#### What type of PR is this?

/kind feature
/area console
/milestone 2.5.x

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

添加复制附件链接的功能,支持三种格式:URL、HTML 格式、Markdown 格式。

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

Fixes #3801 

#### Special notes for your reviewer:

测试方式:

1. 上传若干不同格式的附件。
2. 打开附件详情,检查列出的格式是否正确。
3. 检查复制链接的内容是否正确。

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

```release-note
Console 端的附件详情支持复制链接的功能。
```
pull/3859/head^2
Ryan Wang 2023-04-26 15:32:12 +08:00 committed by GitHub
parent 814dc8921a
commit 7b8613049a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 9 deletions

View File

@ -498,7 +498,7 @@ core:
size: 文件大小
owner: 上传者
creation_time: 上传时间
permalink: 原始链接
permalink: 链接
preview:
click_to_exit: 点击退出预览
video_not_support: 当前浏览器不支持该视频播放

View File

@ -498,7 +498,7 @@ core:
size: 文件大小
owner: 上傳者
creation_time: 上傳時間
permalink: 原始連結
permalink: 連結
preview:
click_to_exit: 點擊離開預覽
video_not_support: 當前瀏覽器不支援該影片播放

View File

@ -15,16 +15,17 @@ import { isImage } from "@/utils/image";
import { formatDatetime } from "@/utils/date";
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
import { useQuery } from "@tanstack/vue-query";
import AttachmentPermalinkList from "./AttachmentPermalinkList.vue";
const props = withDefaults(
defineProps<{
visible: boolean;
attachment: Attachment | null;
attachment: Attachment | undefined;
mountToBody?: boolean;
}>(),
{
visible: false,
attachment: null,
attachment: undefined,
mountToBody: false,
}
);
@ -68,7 +69,10 @@ const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
onlyPreview.value = false;
emit("close");
setTimeout(() => {
emit("close");
}, 200);
}
};
</script>
@ -187,9 +191,7 @@ const onVisibleChange = (visible: boolean) => {
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.permalink')"
>
<a target="_blank" :href="attachment?.status?.permalink">
{{ attachment?.status?.permalink }}
</a>
<AttachmentPermalinkList :attachment="attachment" />
</VDescriptionItem>
</VDescription>
</div>

View File

@ -0,0 +1,74 @@
<script lang="ts" setup>
import { VButton } from "@halo-dev/components";
import type { Attachment } from "@halo-dev/api-client";
import { useAttachmentPermalinkCopy } from "../composables/use-attachment";
import { toRefs, computed } from "vue";
const props = withDefaults(
defineProps<{
visible: boolean;
attachment?: Attachment;
mountToBody?: boolean;
}>(),
{
visible: false,
attachment: undefined,
mountToBody: false,
}
);
const { attachment } = toRefs(props);
const { handleCopy, htmlText, markdownText } =
useAttachmentPermalinkCopy(attachment);
const formats = computed(
(): {
label: string;
key: "url" | "html" | "markdown";
value?: string;
}[] => {
return [
{
label: "URL",
key: "url",
value: attachment?.value?.status?.permalink,
},
{
label: "HTML",
key: "html",
value: htmlText.value,
},
{
label: "Markdown",
key: "markdown",
value: markdownText.value,
},
];
}
);
</script>
<template>
<ul class="flex flex-col space-y-2">
<li v-for="format in formats" :key="format.key">
<div
class="flex w-full cursor-pointer items-center justify-between space-x-3 rounded border p-3 hover:border-primary"
>
<div class="flex flex-1 flex-col space-y-2 text-xs text-gray-900">
<span class="font-semibold">
{{ format.label }}
</span>
<span class="break-all">
{{ format.value }}
</span>
</div>
<div>
<VButton size="sm" @click="handleCopy(format.key)">
{{ $t("core.common.buttons.copy") }}
</VButton>
</div>
</div>
</li>
</ul>
</template>

View File

@ -1,5 +1,5 @@
import type { Attachment, Group, Policy, User } from "@halo-dev/api-client";
import type { Ref } from "vue";
import { computed, type Ref } from "vue";
import { ref, watch } from "vue";
import type { AttachmentLike } from "@halo-dev/console-shared";
import { apiClient } from "@/utils/api-client";
@ -7,6 +7,8 @@ import { Dialog, Toast } from "@halo-dev/components";
import type { Content, Editor } from "@halo-dev/richtext-editor";
import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import { useClipboard } from "@vueuse/core";
import { matchMediaType } from "@/utils/media-type";
interface useAttachmentControlReturn {
attachments: Ref<Attachment[] | undefined>;
@ -327,3 +329,71 @@ export function useAttachmentSelect(
onAttachmentSelect,
};
}
export function useAttachmentPermalinkCopy(
attachment: Ref<Attachment | undefined>
) {
const { copy } = useClipboard();
const { t } = useI18n();
const mediaType = computed(() => {
return attachment.value?.spec.mediaType;
});
const isImage = computed(() => {
return mediaType.value && matchMediaType(mediaType.value, "image/*");
});
const isVideo = computed(() => {
return mediaType.value && matchMediaType(mediaType.value, "video/*");
});
const isAudio = computed(() => {
return mediaType.value && matchMediaType(mediaType.value, "audio/*");
});
const htmlText = computed(() => {
const { permalink } = attachment.value?.status || {};
const { displayName } = attachment.value?.spec || {};
if (isImage.value) {
return `<img src="${permalink}" alt="${displayName}" />`;
} else if (isVideo.value) {
return `<video src="${permalink}"></video>`;
} else if (isAudio.value) {
return `<audio src="${permalink}"></audio>`;
}
return `<a href="${permalink}">${displayName}</a>`;
});
const markdownText = computed(() => {
const { permalink } = attachment.value?.status || {};
const { displayName } = attachment.value?.spec || {};
if (isImage.value) {
return `![${displayName}](${permalink})`;
}
return `[${displayName}](${permalink})`;
});
const handleCopy = (format: "markdown" | "html" | "url") => {
const { permalink } = attachment.value?.status || {};
if (!permalink) return;
if (format === "url") {
copy(permalink);
} else if (format === "markdown") {
copy(markdownText.value);
} else if (format === "html") {
copy(htmlText.value);
}
Toast.success(t("core.common.toast.copy_success"));
};
return {
htmlText,
markdownText,
handleCopy,
};
}