feat: add support for copying full path in attachment details (#7550)

#### What type of PR is this?

/area ui
/kind feature
/milestone 2.21.x

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

Add support for copying full path ina attachment details modal.

<img width="1023" alt="image" src="https://github.com/user-attachments/assets/1b337655-e774-4c8a-8a32-3dac83cb77b2" />

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

```release-note
支持在附件详情弹窗中复制完整的附件地址。
```
pull/7555/head
Ryan Wang 2025-06-13 23:20:44 +08:00 committed by GitHub
parent 21e115165f
commit 15b029af56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 60 additions and 19 deletions

View File

@ -1,19 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useGlobalInfoStore } from "@/stores/global-info";
import { matchMediaType } from "@/utils/media-type"; import { matchMediaType } from "@/utils/media-type";
import type { Attachment } from "@halo-dev/api-client"; import type { Attachment } from "@halo-dev/api-client";
import { Toast, VButton } from "@halo-dev/components"; import { Toast, VButton, VTabbar } from "@halo-dev/components";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { computed, toRefs } from "vue"; import { computed, ref, toRefs } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
attachment?: Attachment; attachment?: Attachment;
mountToBody?: boolean;
}>(), }>(),
{ {
attachment: undefined, attachment: undefined,
mountToBody: false,
} }
); );
@ -21,6 +20,9 @@ const { attachment } = toRefs(props);
const { copy } = useClipboard({ legacy: true }); const { copy } = useClipboard({ legacy: true });
const { t } = useI18n(); const { t } = useI18n();
const { globalInfo } = useGlobalInfoStore();
const activeId = ref<"relative" | "absolute">("relative");
const mediaType = computed(() => { const mediaType = computed(() => {
return attachment.value?.spec.mediaType; return attachment.value?.spec.mediaType;
@ -38,36 +40,48 @@ const isAudio = computed(() => {
return mediaType.value && matchMediaType(mediaType.value, "audio/*"); return mediaType.value && matchMediaType(mediaType.value, "audio/*");
}); });
const isLocalAttachment = computed(() => {
return props.attachment?.status?.permalink?.startsWith("/");
});
const permalink = computed(() => {
const { permalink: value } = props.attachment?.status || {};
if (!isLocalAttachment.value) {
return value;
}
if (activeId.value === "relative") {
return value;
}
return `${globalInfo?.externalUrl}${value}`;
});
const htmlText = computed(() => { const htmlText = computed(() => {
const { permalink } = attachment.value?.status || {};
const { displayName } = attachment.value?.spec || {}; const { displayName } = attachment.value?.spec || {};
if (isImage.value) { if (isImage.value) {
return `<img src="${permalink}" alt="${displayName}" />`; return `<img src="${permalink.value}" alt="${displayName}" />`;
} else if (isVideo.value) { } else if (isVideo.value) {
return `<video src="${permalink}"></video>`; return `<video src="${permalink.value}"></video>`;
} else if (isAudio.value) { } else if (isAudio.value) {
return `<audio src="${permalink}"></audio>`; return `<audio src="${permalink.value}"></audio>`;
} }
return `<a href="${permalink}">${displayName}</a>`; return `<a href="${permalink.value}">${displayName}</a>`;
}); });
const markdownText = computed(() => { const markdownText = computed(() => {
const { permalink } = attachment.value?.status || {};
const { displayName } = attachment.value?.spec || {}; const { displayName } = attachment.value?.spec || {};
if (isImage.value) { if (isImage.value) {
return `![${displayName}](${permalink})`; return `![${displayName}](${permalink.value})`;
} }
return `[${displayName}](${permalink})`; return `[${displayName}](${permalink.value})`;
}); });
const handleCopy = (format: "markdown" | "html" | "url") => { const handleCopy = (format: "markdown" | "html" | "url") => {
const { permalink } = attachment.value?.status || {};
if (!permalink) return;
if (format === "url") { if (format === "url") {
copy(permalink); copy(permalink.value || "");
} else if (format === "markdown") { } else if (format === "markdown") {
copy(markdownText.value); copy(markdownText.value);
} else if (format === "html") { } else if (format === "html") {
@ -87,7 +101,7 @@ const formats = computed(
{ {
label: "URL", label: "URL",
key: "url", key: "url",
value: attachment?.value?.status?.permalink, value: permalink.value,
}, },
{ {
label: "HTML", label: "HTML",
@ -105,7 +119,22 @@ const formats = computed(
</script> </script>
<template> <template>
<ul class="flex flex-col space-y-2"> <VTabbar
v-if="isLocalAttachment"
v-model:active-id="activeId"
:items="[
{
label: $t('core.attachment.permalink_list.relative'),
id: 'relative',
},
{
label: $t('core.attachment.permalink_list.absolute'),
id: 'absolute',
},
]"
type="outline"
></VTabbar>
<ul class="flex flex-col space-y-2 mt-3">
<li v-for="format in formats" :key="format.key"> <li v-for="format in formats" :key="format.key">
<div <div
class="flex w-full cursor-pointer items-center justify-between space-x-3 rounded border p-3 hover:border-primary" class="flex w-full cursor-pointer items-center justify-between space-x-3 rounded border p-3 hover:border-primary"

View File

@ -213,6 +213,9 @@ core:
items: items:
display_name_asc: Ascending order by display name display_name_asc: Ascending order by display name
display_name_desc: Descending order by display name display_name_desc: Descending order by display name
permalink_list:
relative: Relative path
absolute: Absolute path
group_editing_modal: group_editing_modal:
toast: toast:
group_name_exists: Group name already exists group_name_exists: Group name already exists

View File

@ -684,6 +684,9 @@ core:
video_not_support: The current browser does not support video playback. video_not_support: The current browser does not support video playback.
audio_not_support: The current browser does not support audio playback. audio_not_support: The current browser does not support audio playback.
not_support: This file does not support preview. not_support: This file does not support preview.
permalink_list:
relative: Relative path
absolute: Absolute path
group_editing_modal: group_editing_modal:
titles: titles:
create: Create attachment group create: Create attachment group

View File

@ -719,6 +719,9 @@ core:
operations: operations:
select: select:
result: (已选择 {count} 项) result: (已选择 {count} 项)
permalink_list:
relative: 相对路径
absolute: 完整路径
uc_attachment: uc_attachment:
empty: empty:
title: 当前没有附件 title: 当前没有附件

View File

@ -704,6 +704,9 @@ core:
operations: operations:
select: select:
result: (已選擇 {count} 項) result: (已選擇 {count} 項)
permalink_list:
relative: 相對路徑
absolute: 完整路徑
uc_attachment: uc_attachment:
empty: empty:
title: 當前沒有附件 title: 當前沒有附件