mirror of https://github.com/halo-dev/halo
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
parent
814dc8921a
commit
7b8613049a
|
@ -498,7 +498,7 @@ core:
|
||||||
size: 文件大小
|
size: 文件大小
|
||||||
owner: 上传者
|
owner: 上传者
|
||||||
creation_time: 上传时间
|
creation_time: 上传时间
|
||||||
permalink: 原始链接
|
permalink: 链接
|
||||||
preview:
|
preview:
|
||||||
click_to_exit: 点击退出预览
|
click_to_exit: 点击退出预览
|
||||||
video_not_support: 当前浏览器不支持该视频播放
|
video_not_support: 当前浏览器不支持该视频播放
|
||||||
|
|
|
@ -498,7 +498,7 @@ core:
|
||||||
size: 文件大小
|
size: 文件大小
|
||||||
owner: 上傳者
|
owner: 上傳者
|
||||||
creation_time: 上傳時間
|
creation_time: 上傳時間
|
||||||
permalink: 原始連結
|
permalink: 連結
|
||||||
preview:
|
preview:
|
||||||
click_to_exit: 點擊離開預覽
|
click_to_exit: 點擊離開預覽
|
||||||
video_not_support: 當前瀏覽器不支援該影片播放
|
video_not_support: 當前瀏覽器不支援該影片播放
|
||||||
|
|
|
@ -15,16 +15,17 @@ import { isImage } from "@/utils/image";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import AttachmentPermalinkList from "./AttachmentPermalinkList.vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
attachment: Attachment | null;
|
attachment: Attachment | undefined;
|
||||||
mountToBody?: boolean;
|
mountToBody?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
attachment: null,
|
attachment: undefined,
|
||||||
mountToBody: false,
|
mountToBody: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -68,7 +69,10 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
emit("update:visible", visible);
|
emit("update:visible", visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
onlyPreview.value = false;
|
onlyPreview.value = false;
|
||||||
emit("close");
|
|
||||||
|
setTimeout(() => {
|
||||||
|
emit("close");
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -187,9 +191,7 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
<VDescriptionItem
|
<VDescriptionItem
|
||||||
:label="$t('core.attachment.detail_modal.fields.permalink')"
|
:label="$t('core.attachment.detail_modal.fields.permalink')"
|
||||||
>
|
>
|
||||||
<a target="_blank" :href="attachment?.status?.permalink">
|
<AttachmentPermalinkList :attachment="attachment" />
|
||||||
{{ attachment?.status?.permalink }}
|
|
||||||
</a>
|
|
||||||
</VDescriptionItem>
|
</VDescriptionItem>
|
||||||
</VDescription>
|
</VDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Attachment, Group, Policy, User } from "@halo-dev/api-client";
|
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 { ref, watch } from "vue";
|
||||||
import type { AttachmentLike } from "@halo-dev/console-shared";
|
import type { AttachmentLike } from "@halo-dev/console-shared";
|
||||||
import { apiClient } from "@/utils/api-client";
|
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 type { Content, Editor } from "@halo-dev/richtext-editor";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
import { matchMediaType } from "@/utils/media-type";
|
||||||
|
|
||||||
interface useAttachmentControlReturn {
|
interface useAttachmentControlReturn {
|
||||||
attachments: Ref<Attachment[] | undefined>;
|
attachments: Ref<Attachment[] | undefined>;
|
||||||
|
@ -327,3 +329,71 @@ export function useAttachmentSelect(
|
||||||
onAttachmentSelect,
|
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 ``;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue