Remove attachment thumbnails modal and related code

Deleted the AttachmentThumbnailsModal, related components, and composables for managing and displaying attachment thumbnails. Updated AttachmentList and AttachmentDetailModal to remove references to the thumbnails modal and related permissions. Simplified thumbnail display to show only available image thumbnails in the detail modal, and updated i18n files to remove unused thumbnail modal and status translations. Centralized thumbnail width mapping in utils/thumbnail.ts.
Ryan Wang 2025-09-30 16:23:57 +08:00
parent 4ff058eac2
commit 69f126efa4
17 changed files with 26 additions and 526 deletions

View File

@ -33,14 +33,12 @@ import { useRouteQuery } from "@vueuse/router";
import type { Ref } from "vue";
import { computed, onMounted, provide, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import RiMultiImageLine from "~icons/ri/multi-image-line";
import AttachmentDetailModal from "./components/AttachmentDetailModal.vue";
import AttachmentError from "./components/AttachmentError.vue";
import AttachmentGroupList from "./components/AttachmentGroupList.vue";
import AttachmentListItem from "./components/AttachmentListItem.vue";
import AttachmentLoading from "./components/AttachmentLoading.vue";
import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue";
import AttachmentThumbnailsModal from "./components/AttachmentThumbnailsModal.vue";
import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
import { useAttachmentControl } from "./composables/use-attachment";
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
@ -230,9 +228,6 @@ watch(
}
}
);
// Thumbnails modal
const thumbnailsVisible = ref(false);
</script>
<template>
<AttachmentDetailModal
@ -261,23 +256,11 @@ const thumbnailsVisible = ref(false);
v-if="policyVisible"
@close="policyVisible = false"
/>
<AttachmentThumbnailsModal
v-if="thumbnailsVisible"
@close="thumbnailsVisible = false"
/>
<VPageHeader :title="$t('core.attachment.title')">
<template #icon>
<IconFolder />
</template>
<template #actions>
<HasPermission :permissions="['*']">
<VButton size="sm" @click="thumbnailsVisible = true">
<template #icon>
<RiMultiImageLine />
</template>
{{ $t("core.attachment.actions.thumbnails") }}
</VButton>
</HasPermission>
<VButton
v-permission="['system:attachments:manage']"
size="sm"

View File

@ -232,16 +232,15 @@ const showDisplayNameForm = ref(false);
>
<AttachmentPermalinkList :attachment="attachment" />
</VDescriptionItem>
<HasPermission
v-if="!!attachment?.status?.thumbnails"
:permissions="['*']"
>
<VDescriptionItem
v-if="
isImage(attachment?.spec.mediaType) &&
!!attachment?.status?.thumbnails
"
:label="$t('core.attachment.detail_modal.fields.thumbnails')"
>
<AttachmentSingleThumbnailList :attachment="attachment" />
</VDescriptionItem>
</HasPermission>
</VDescription>
</div>
</div>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { SIZE_MAP } from "../composables/use-thumbnail-detail";
import { THUMBNAIL_WIDTH_MAP } from "@/utils/thumbnail";
const { size, permalink } = defineProps<{
size: string;
@ -19,9 +19,7 @@ const { size, permalink } = defineProps<{
/>
</a>
<div class="flex min-w-0 flex-1 flex-col space-y-2 text-xs text-gray-900">
<span class="font-semibold">
{{ SIZE_MAP[size] }}
</span>
<span class="font-semibold"> {{ THUMBNAIL_WIDTH_MAP[size] }}w </span>
<a
:href="permalink"
target="_blank"

View File

@ -38,7 +38,7 @@ const thumbnails = computed(() => {
});
</script>
<template>
<ul class="flex flex-col space-y-2">
<ul v-if="thumbnails.length" class="flex flex-col space-y-2">
<AttachmentSingleThumbnailItem
v-for="thumbnail in thumbnails"
:key="thumbnail.size"
@ -46,4 +46,7 @@ const thumbnails = computed(() => {
:permalink="thumbnail.permalink"
/>
</ul>
<span v-else>
{{ $t("core.attachment.detail_modal.preview.not_support_thumbnail") }}
</span>
</template>

View File

@ -1,96 +0,0 @@
<script lang="ts" setup>
import { formatDatetime } from "@/utils/date";
import {
LocalThumbnailStatusPhaseEnum,
type LocalThumbnail,
} from "@halo-dev/api-client";
import { VButton, VEntity, VEntityField } from "@halo-dev/components";
import { toRefs } from "vue";
import { useThumbnailControl } from "../composables/use-thumbnail-control";
import {
SIZE_MAP,
useThumbnailDetail,
} from "../composables/use-thumbnail-detail";
const props = defineProps<{
thumbnail: LocalThumbnail;
}>();
const { thumbnail } = toRefs(props);
const { phase } = useThumbnailDetail(thumbnail);
const { handleRetry } = useThumbnailControl(thumbnail);
</script>
<template>
<VEntity>
<template #start>
<VEntityField>
<template #description>
<a
:href="thumbnail.spec.thumbnailUri"
target="_blank"
class="block flex-none"
>
<img
v-if="
thumbnail.status.phase ===
LocalThumbnailStatusPhaseEnum.Succeeded
"
:src="thumbnail.spec.thumbnailUri"
alt=""
class="h-10 w-10 rounded-md object-cover"
loading="lazy"
/>
<div
v-else
class="flex h-10 w-10 items-center justify-center rounded-md border"
>
<component
:is="phase.icon"
class="h-4.5 w-4.5"
:class="phase.color"
/>
</div>
</a>
</template>
</VEntityField>
<VEntityField :title="SIZE_MAP[thumbnail.spec.size]">
<template #description>
<a
:href="thumbnail.spec.thumbnailUri"
target="_blank"
class="truncate text-xs text-gray-500 hover:text-gray-900"
>
{{ thumbnail.spec.thumbnailUri }}
</a>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField
:description="formatDatetime(thumbnail.metadata.creationTimestamp)"
/>
<VEntityField>
<template #description>
<component
:is="phase.icon"
v-tooltip="$t(phase.label)"
class="h-4.5 w-4.5"
:class="phase.color"
/>
</template>
</VEntityField>
<VEntityField
v-if="
thumbnail.status.phase !== LocalThumbnailStatusPhaseEnum.Succeeded
"
>
<template #description>
<VButton size="sm" @click="handleRetry">
{{ $t("core.common.buttons.retry") }}
</VButton>
</template>
</VEntityField>
</template>
</VEntity>
</template>

View File

@ -1,216 +0,0 @@
<script lang="ts" setup>
import FilterDropdown from "@/components/filter/FilterDropdown.vue";
import { storageAnnotations } from "@/constants/annotations";
import {
coreApiClient,
LocalThumbnailStatusPhaseEnum,
} from "@halo-dev/api-client";
import {
Toast,
VButton,
VEmpty,
VEntityContainer,
VLoading,
VModal,
VPagination,
VSpace,
} from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
import { chunk } from "lodash-es";
import { ref, useTemplateRef, watch } from "vue";
import { useI18n } from "vue-i18n";
import AttachmentThumbnailItem from "./AttachmentThumbnailItem.vue";
const emit = defineEmits<{
(event: "close"): void;
}>();
const modal = useTemplateRef<InstanceType<typeof VModal> | null>("modal");
const page = ref(1);
const size = ref(20);
const selectedStatus = ref<LocalThumbnailStatusPhaseEnum | undefined>(
undefined
);
watch(
() => selectedStatus.value,
() => {
page.value = 1;
}
);
const { t } = useI18n();
const {
data: thumbnails,
isLoading,
refetch,
} = useQuery({
queryKey: ["core:attachments:thumbnails", page, size, selectedStatus],
queryFn: async () => {
const fieldSelector: string[] = [];
if (selectedStatus.value) {
fieldSelector.push(`status.phase=${selectedStatus.value}`);
}
const { data } =
await coreApiClient.storage.localThumbnail.listLocalThumbnail({
page: page.value,
size: size.value,
fieldSelector,
});
return data;
},
refetchInterval: (data) => {
const hasAbnormalData = data?.items?.some(
(thumbnail) =>
thumbnail.status.phase !== LocalThumbnailStatusPhaseEnum.Succeeded
);
return hasAbnormalData ? 1000 : false;
},
});
async function handleRetryAllFailed() {
try {
const { data } =
await coreApiClient.storage.localThumbnail.listLocalThumbnail({
fieldSelector: [`status.phase=${LocalThumbnailStatusPhaseEnum.Failed}`],
});
const failedThumbnails = data.items;
if (!failedThumbnails.length) {
Toast.info(
t(
"core.attachment.thumbnails_modal.operations.retry_all_failed.tips_empty"
)
);
return;
}
const chunkedFailedThumbnails = chunk(failedThumbnails, 5);
for (const chunk of chunkedFailedThumbnails) {
await Promise.all(
chunk.map(async (thumbnail) => {
await coreApiClient.storage.localThumbnail.patchLocalThumbnail({
name: thumbnail.metadata.name,
jsonPatchInner: [
{
op: "add",
path: "/status/phase",
value: LocalThumbnailStatusPhaseEnum.Pending,
},
{
op: "add",
path: "/metadata/annotations",
value: {
[storageAnnotations.RETRY_TIMESTAMP]: Date.now().toString(),
},
},
],
});
})
);
}
Toast.success(
t(
"core.attachment.thumbnails_modal.operations.retry_all_failed.tips_success"
)
);
await refetch();
} catch (error) {
console.error("Failed to retry all failed thumbnails", error);
}
}
</script>
<template>
<VModal
ref="modal"
:centered="false"
:width="1000"
:title="$t('core.attachment.thumbnails_modal.title')"
:layer-closable="true"
@close="emit('close')"
>
<div>
<div class="mb-4 flex items-center justify-between">
<VSpace spacing="lg" class="flex-wrap">
<FilterDropdown
v-model="selectedStatus"
:items="[
{
label: $t('core.common.filters.item_labels.all'),
value: undefined,
},
{
label: $t('core.attachment.thumbnails.phase.pending'),
value: LocalThumbnailStatusPhaseEnum.Pending,
},
{
label: $t('core.attachment.thumbnails.phase.succeeded'),
value: LocalThumbnailStatusPhaseEnum.Succeeded,
},
{
label: $t('core.attachment.thumbnails.phase.failed'),
value: LocalThumbnailStatusPhaseEnum.Failed,
},
]"
:label="$t('core.common.filters.labels.status')"
/>
</VSpace>
<VButton size="sm" @click="handleRetryAllFailed">
{{
$t(
"core.attachment.thumbnails_modal.operations.retry_all_failed.button"
)
}}
</VButton>
</div>
<VLoading v-if="isLoading" />
<VEmpty
v-else-if="!thumbnails?.items?.length"
:title="$t('core.attachment.thumbnails_modal.empty.title')"
:message="$t('core.attachment.thumbnails_modal.empty.message')"
>
<template #actions>
<VButton @click="refetch">
{{ $t("core.common.buttons.refresh") }}
</VButton>
</template>
</VEmpty>
<div v-else class="overflow-hidden rounded-base border">
<VEntityContainer>
<AttachmentThumbnailItem
v-for="thumbnail in thumbnails.items"
:key="thumbnail.metadata.name"
:thumbnail="thumbnail"
/>
</VEntityContainer>
</div>
<div class="mt-4">
<VPagination
v-model:page="page"
v-model:size="size"
:page-label="$t('core.components.pagination.page_label')"
:size-label="$t('core.components.pagination.size_label')"
:total-label="
$t('core.components.pagination.total_label', {
total: thumbnails?.total || 0,
})
"
:total="thumbnails?.total || 0"
:size-options="[20, 30, 50, 100]"
/>
</div>
</div>
<template #footer>
<VButton @click="modal?.close()">
{{ $t("core.common.buttons.close") }}
</VButton>
</template>
</VModal>
</template>

View File

@ -1,46 +0,0 @@
import { storageAnnotations } from "@/constants/annotations";
import {
coreApiClient,
LocalThumbnailStatusPhaseEnum,
type LocalThumbnail,
} from "@halo-dev/api-client";
import { Toast } from "@halo-dev/components";
import { useQueryClient } from "@tanstack/vue-query";
import type { Ref } from "vue";
import { useI18n } from "vue-i18n";
export function useThumbnailControl(thumbnail: Ref<LocalThumbnail>) {
const queryClient = useQueryClient();
const { t } = useI18n();
async function handleRetry() {
await coreApiClient.storage.localThumbnail.patchLocalThumbnail({
name: thumbnail.value.metadata.name,
jsonPatchInner: [
{
op: "add",
path: "/status/phase",
value: LocalThumbnailStatusPhaseEnum.Pending,
},
{
op: "add",
path: "/metadata/annotations",
value: {
...thumbnail.value.metadata.annotations,
[storageAnnotations.RETRY_TIMESTAMP]: Date.now().toString(),
},
},
],
});
await queryClient.invalidateQueries({
queryKey: ["core:attachments:thumbnails"],
});
Toast.success(t("core.common.toast.operation_success"));
}
return {
handleRetry,
};
}

View File

@ -1,51 +0,0 @@
import {
GetThumbnailByUriSizeEnum,
LocalThumbnailStatusPhaseEnum,
type LocalThumbnail,
} from "@halo-dev/api-client";
import { IconCheckboxCircle, IconErrorWarning } from "@halo-dev/components";
import { computed, type Component, type Ref } from "vue";
import RiTimeLine from "~icons/ri/time-line";
export const SIZE_MAP: Record<GetThumbnailByUriSizeEnum, string> = {
XL: "1600w",
L: "1200w",
M: "800w",
S: "400w",
};
export const PHASE_MAP: Record<
LocalThumbnailStatusPhaseEnum,
{
icon: Component;
label: string;
color: string;
}
> = {
PENDING: {
icon: RiTimeLine,
label: "core.attachment.thumbnails.phase.pending",
color: "text-gray-500",
},
SUCCEEDED: {
icon: IconCheckboxCircle,
label: "core.attachment.thumbnails.phase.succeeded",
color: "text-green-500",
},
FAILED: {
icon: IconErrorWarning,
label: "core.attachment.thumbnails.phase.failed",
color: "text-red-500",
},
};
export function useThumbnailDetail(thumbnail: Ref<LocalThumbnail>) {
const phase = computed(() => {
return PHASE_MAP[
thumbnail.value.status.phase || LocalThumbnailStatusPhaseEnum.Pending
];
});
return {
phase,
};
}

View File

@ -12,6 +12,7 @@ import {
ExtensionColumn,
ExtensionColumns,
ExtensionCommands,
ExtensionDetails,
ExtensionDocument,
ExtensionDraggable,
ExtensionDropcursor,
@ -44,7 +45,6 @@ import {
ExtensionTrailingNode,
ExtensionUnderline,
ExtensionVideo,
ExtensionDetails,
RichTextEditor,
useEditor,
} from "../index";

View File

@ -1,11 +1,11 @@
import ToolbarItem from "@/components/toolbar/ToolbarItem.vue";
import { i18n } from "@/locales";
import type { Editor, Range } from "@/tiptap/vue-3";
import type { ExtensionOptions } from "@/types";
import TiptapDetails, { type DetailsOptions } from "@tiptap/extension-details";
import TiptapDetailsContent from "@tiptap/extension-details-content";
import TiptapDetailsSummary from "@tiptap/extension-details-summary";
import type { Editor, Range } from "@/tiptap/vue-3";
import { markRaw } from "vue";
import ToolbarItem from "@/components/toolbar/ToolbarItem.vue";
import { i18n } from "@/locales";
import MdiExpandHorizontal from "~icons/mdi/expand-horizontal";
const getRenderContainer = (node: HTMLElement) => {

View File

@ -10,6 +10,7 @@ import ExtensionBold from "./bold";
import ExtensionBulletList from "./bullet-list";
import ExtensionCode from "./code";
import ExtensionColor from "./color";
import ExtensionDetails from "./details";
import ExtensionFontSize from "./font-size";
import ExtensionHeading from "./heading";
import ExtensionHighlight from "./highlight";
@ -26,7 +27,6 @@ import ExtensionTable from "./table";
import ExtensionTaskList from "./task-list";
import ExtensionTextAlign from "./text-align";
import ExtensionUnderline from "./underline";
import ExtensionDetails from "./details";
// Custom extensions
import {
@ -124,6 +124,7 @@ export {
ExtensionColumn,
ExtensionColumns,
ExtensionCommands,
ExtensionDetails,
ExtensionDocument,
ExtensionDraggable,
ExtensionDropcursor,
@ -158,7 +159,6 @@ export {
ExtensionTrailingNode,
ExtensionUnderline,
ExtensionVideo,
ExtensionDetails,
RangeSelection,
};

View File

@ -33,11 +33,3 @@ export enum patAnnotations {
export enum secretAnnotations {
DESCRIPTION = "secret.halo.run/description",
}
// storage
export enum storageAnnotations {
URI = "storage.halo.run/uri",
// Frontend custom annotations, for retry operation
RETRY_TIMESTAMP = "storage.halo.run/retry-timestamp",
}

View File

@ -200,10 +200,6 @@ core:
list:
fields:
private: Private
reply_modal:
fields:
hidden:
label: Private reply
detail_modal:
fields:
owner: Commentator
@ -221,8 +217,6 @@ core:
original_comment: Original comment
content: Reply content
attachment:
actions:
thumbnails: Thumbnails
filters:
sort:
items:
@ -231,6 +225,8 @@ core:
detail_modal:
fields:
thumbnails: Thumbnails
preview:
not_support_thumbnail: This image does not support generating thumbnails.
permalink_list:
relative: Relative path
absolute: Absolute path
@ -250,21 +246,6 @@ core:
label: URL
toast:
success: Downloaded successfully
thumbnails_modal:
title: Thumbnail records
empty:
title: No thumbnail records
message: There are currently no thumbnail records, thumbnails will be automatically generated after uploading image attachments.
operations:
retry_all_failed:
button: Retry all failed records
tips_empty: No failed thumbnail records
tips_success: All failed records retried successfully
thumbnails:
phase:
pending: Pending
succeeded: Succeeded
failed: Failed
uc_attachment:
empty:
title: There are no attachments.

View File

@ -631,7 +631,6 @@ core:
ungrouped: Ungrouped
actions:
storage_policies: Storage Policies
thumbnails: Thumbnails
empty:
title: There are no attachments in the current group.
message: >-
@ -693,6 +692,7 @@ core:
video_not_support: The current browser does not support video playback.
audio_not_support: The current browser does not support audio playback.
not_support: This file does not support preview.
not_support_thumbnail: This image does not support generating thumbnails.
permalink_list:
relative: Relative path
absolute: Absolute path
@ -780,23 +780,6 @@ core:
operations:
select:
result: ({count} items selected)
thumbnails_modal:
title: Thumbnail records
empty:
title: No thumbnail records
message: >-
There are currently no thumbnail records, thumbnails will be
automatically generated after uploading image attachments.
operations:
retry_all_failed:
button: Retry all failed records
tips_empty: No failed thumbnail records
tips_success: All failed records retried successfully
thumbnails:
phase:
pending: Pending
succeeded: Succeeded
failed: Failed
uc_attachment:
empty:
title: There are no attachments.

View File

@ -600,7 +600,6 @@ core:
ungrouped: 未分组
actions:
storage_policies: 存储策略
thumbnails: 缩略图
empty:
title: 当前分组没有附件
message: 当前分组没有附件,你可以尝试刷新或者上传附件
@ -660,6 +659,7 @@ core:
video_not_support: 当前浏览器不支持该视频播放
audio_not_support: 当前浏览器不支持该音频播放
not_support: 此文件不支持预览
not_support_thumbnail: 此图片不支持生成缩略图
group_editing_modal:
titles:
create: 新增附件分组
@ -737,21 +737,6 @@ core:
permalink_list:
relative: 相对路径
absolute: 完整路径
thumbnails_modal:
title: 缩略图记录
empty:
title: 没有缩略图记录
message: 当前没有缩略图记录,缩略图会在上传图片附件后自动生成。
operations:
retry_all_failed:
button: 重试所有失败记录
tips_empty: 没有失败记录
tips_success: 重试所有失败记录成功
thumbnails:
phase:
pending: 等待生成
succeeded: 生成成功
failed: 生成失败
uc_attachment:
empty:
title: 当前没有附件

View File

@ -585,7 +585,6 @@ core:
ungrouped: 未分組
actions:
storage_policies: 存儲策略
thumbnails: 縮略圖
empty:
title: 當前分組沒有附件
message: 當前分組沒有附件,你可以嘗試重整或者上傳附件
@ -645,6 +644,7 @@ core:
video_not_support: 當前瀏覽器不支援該影片播放
audio_not_support: 當前瀏覽器不支援該音頻播放
not_support: 此文件不支援預覽
not_support_thumbnail: 此圖片不支持生成縮略圖
group_editing_modal:
titles:
create: 新增附件分組
@ -722,21 +722,6 @@ core:
permalink_list:
relative: 相對路徑
absolute: 完整路徑
thumbnails_modal:
title: 縮略圖記錄
empty:
title: 沒有縮略圖記錄
message: 當前沒有縮略圖記錄,縮略圖會在上傳圖片附件後自動生成。
operations:
retry_all_failed:
button: 重試所有失敗記錄
tips_empty: 沒有失敗記錄
tips_success: 重試所有失敗記錄成功
thumbnails:
phase:
pending: 等待生成
succeeded: 生成成功
failed: 生成失敗
uc_attachment:
empty:
title: 當前沒有附件

View File

@ -1,6 +1,6 @@
import type { GetThumbnailByUriSizeEnum } from "@halo-dev/api-client";
const THUMBNAIL_WIDTH_MAP: Record<GetThumbnailByUriSizeEnum, number> = {
export const THUMBNAIL_WIDTH_MAP: Record<GetThumbnailByUriSizeEnum, number> = {
XL: 1600,
L: 1200,
M: 800,