mirror of https://github.com/halo-dev/halo-admin
refactor: attachment upload component (#784)
#### What type of PR is this? /kind improvement #### What this PR does / why we need it: 优化上传附件的组件和附件库选择组件。 1. 附件上传支持缓存选择的分组和策略。 2. 附件上传支持选择分组。 3. 移除附件选择组件的上传 tab,改为和附件库管理中一样的上传组件。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2828 #### Screenshots: <img width="722" alt="image" src="https://user-images.githubusercontent.com/21301288/208612167-c7082be4-0fb8-4caa-b246-d15bac525e86.png"> #### Special notes for your reviewer: 测试方式: 1. 测试在附件管理中上传附件的功能是否正常。 2. 测试在附件选择组件中上传附件的功能是否正常。 #### Does this PR introduce a user-facing change? ```release-note 优化 Console 端上传附件的功能,支持缓存选择的分组和策略。 ```pull/799/head
parent
b29a72d7a5
commit
023831cdd4
|
@ -298,7 +298,6 @@ onMounted(() => {
|
|||
</AttachmentDetailModal>
|
||||
<AttachmentUploadModal
|
||||
v-model:visible="uploadVisible"
|
||||
:group="selectedGroup"
|
||||
@close="onUploadModalClose"
|
||||
/>
|
||||
<AttachmentPoliciesModal v-model:visible="policyVisible" />
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { VButton, VModal, VTabbar } from "@halo-dev/components";
|
||||
import { ref, markRaw, onMounted } from "vue";
|
||||
import CoreSelectorProvider from "./selector-providers/CoreSelectorProvider.vue";
|
||||
import UploadSelectorProvider from "./selector-providers/UploadSelectorProvider.vue";
|
||||
import type {
|
||||
AttachmentLike,
|
||||
AttachmentSelectProvider,
|
||||
|
@ -33,11 +32,6 @@ const attachmentSelectProviders = ref<AttachmentSelectProvider[]>([
|
|||
label: "附件库",
|
||||
component: markRaw(CoreSelectorProvider),
|
||||
},
|
||||
{
|
||||
id: "upload",
|
||||
label: "上传",
|
||||
component: markRaw(UploadSelectorProvider),
|
||||
},
|
||||
]);
|
||||
|
||||
// resolve plugin extension points
|
||||
|
|
|
@ -1,63 +1,72 @@
|
|||
<script lang="ts" setup>
|
||||
import { VModal, IconAddCircle, VAlert } from "@halo-dev/components";
|
||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||
import { computed, ref, watch, watchEffect } from "vue";
|
||||
import type { Policy, Group, PolicyTemplate } from "@halo-dev/api-client";
|
||||
import { ref, watch } from "vue";
|
||||
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
|
||||
import {
|
||||
useFetchAttachmentPolicy,
|
||||
useFetchAttachmentPolicyTemplate,
|
||||
} from "../composables/use-attachment-policy";
|
||||
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
group?: Group;
|
||||
}>(),
|
||||
{
|
||||
visible: false,
|
||||
group: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const groupName = computed(() => {
|
||||
if (props.group?.metadata.name === "ungrouped") {
|
||||
return "";
|
||||
}
|
||||
return props.group?.metadata.name;
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:visible", visible: boolean): void;
|
||||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const { groups, handleFetchGroups } = useFetchAttachmentGroup({
|
||||
fetchOnMounted: false,
|
||||
});
|
||||
const { policies, handleFetchPolicies } = useFetchAttachmentPolicy({
|
||||
fetchOnMounted: false,
|
||||
});
|
||||
const { policyTemplates, handleFetchPolicyTemplates } =
|
||||
useFetchAttachmentPolicyTemplate();
|
||||
|
||||
const selectedPolicy = ref<Policy>();
|
||||
const selectedGroupName = useLocalStorage("attachment-upload-group", "");
|
||||
const selectedPolicyName = useLocalStorage("attachment-upload-policy", "");
|
||||
const policyToCreate = ref<Policy>();
|
||||
const uploadVisible = ref(false);
|
||||
const policyEditingModal = ref(false);
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (
|
||||
props.group?.metadata.name &&
|
||||
props.group?.metadata.name !== "ungrouped"
|
||||
) {
|
||||
return `上传附件到分组:${props.group.spec.displayName}`;
|
||||
}
|
||||
return "上传附件到未分组";
|
||||
});
|
||||
watch(
|
||||
() => groups.value,
|
||||
() => {
|
||||
if (selectedGroupName.value === "") return;
|
||||
|
||||
watchEffect(() => {
|
||||
if (policies.value.length) {
|
||||
selectedPolicy.value = policies.value[0];
|
||||
const group = groups.value.find(
|
||||
(group) => group.metadata.name === selectedGroupName.value
|
||||
);
|
||||
if (!group) {
|
||||
selectedGroupName.value =
|
||||
groups.value.length > 0 ? groups.value[0].metadata.name : "";
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
watch(
|
||||
() => policies.value,
|
||||
() => {
|
||||
const policy = policies.value.find(
|
||||
(policy) => policy.metadata.name === selectedPolicyName.value
|
||||
);
|
||||
if (!policy) {
|
||||
selectedPolicyName.value =
|
||||
policies.value.length > 0 ? policies.value[0].metadata.name : "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleOpenCreateNewPolicyModal = (policyTemplate: PolicyTemplate) => {
|
||||
policyToCreate.value = {
|
||||
|
@ -93,6 +102,7 @@ watch(
|
|||
() => props.visible,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleFetchGroups();
|
||||
handleFetchPolicies();
|
||||
handleFetchPolicyTemplates();
|
||||
uploadVisible.value = true;
|
||||
|
@ -110,12 +120,36 @@ watch(
|
|||
:body-class="['!p-0']"
|
||||
:visible="visible"
|
||||
:width="650"
|
||||
:title="modalTitle"
|
||||
title="上传附件"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<div class="w-full p-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-gray-900">选择存储策略:</span>
|
||||
<span class="text-sm text-gray-800">选择分组:</span>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="(group, index) in [
|
||||
{ metadata: { name: '' }, spec: { displayName: '未分组' } },
|
||||
...groups,
|
||||
]"
|
||||
:key="index"
|
||||
:class="{
|
||||
'!bg-gray-200 !text-gray-900':
|
||||
group.metadata.name === selectedGroupName,
|
||||
}"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
@click="selectedGroupName = group.metadata.name"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-2 truncate">
|
||||
<span class="truncate text-sm">
|
||||
{{ group.spec.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-gray-800">选择存储策略:</span>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4">
|
||||
<div
|
||||
|
@ -123,10 +157,10 @@ watch(
|
|||
:key="index"
|
||||
:class="{
|
||||
'!bg-gray-200 !text-gray-900':
|
||||
selectedPolicy?.metadata.name === policy.metadata.name,
|
||||
selectedPolicyName === policy.metadata.name,
|
||||
}"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
@click="selectedPolicy = policy"
|
||||
@click="selectedPolicyName = policy.metadata.name"
|
||||
>
|
||||
<div class="flex flex-1 flex-col items-start truncate">
|
||||
<span class="truncate text-sm">
|
||||
|
@ -176,13 +210,13 @@ watch(
|
|||
<UppyUpload
|
||||
v-if="uploadVisible"
|
||||
endpoint="/apis/api.console.halo.run/v1alpha1/attachments/upload"
|
||||
:disabled="!selectedPolicy"
|
||||
:meta="{
|
||||
policyName: selectedPolicy?.metadata.name as string,
|
||||
groupName: groupName
|
||||
:disabled="!selectedPolicyName"
|
||||
:meta="{
|
||||
policyName: selectedPolicyName,
|
||||
groupName: selectedGroupName,
|
||||
}"
|
||||
:allowed-meta-fields="['policyName', 'groupName']"
|
||||
:note="selectedPolicy ? '' : '请先选择存储策略'"
|
||||
:note="selectedPolicyName ? '' : '请先选择存储策略'"
|
||||
/>
|
||||
</div>
|
||||
</VModal>
|
||||
|
|
|
@ -78,6 +78,14 @@ await handleFetchAttachments();
|
|||
readonly
|
||||
@select="onGroupChange"
|
||||
/>
|
||||
<div v-if="attachments.total > 0" class="mb-5">
|
||||
<VButton @click="uploadVisible = true">
|
||||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
上传
|
||||
</VButton>
|
||||
</div>
|
||||
<VEmpty
|
||||
v-if="!attachments.total && !loading"
|
||||
message="当前没有附件,你可以尝试刷新或者上传附件"
|
||||
|
@ -86,7 +94,7 @@ await handleFetchAttachments();
|
|||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchAttachments">刷新</VButton>
|
||||
<VButton type="secondary" @click="emit('change-provider', 'upload')">
|
||||
<VButton type="secondary" @click="uploadVisible = true">
|
||||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
|
|
|
@ -1,231 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
VEmpty,
|
||||
IconCheckboxFill,
|
||||
VCard,
|
||||
IconDeleteBin,
|
||||
Dialog,
|
||||
Toast,
|
||||
} from "@halo-dev/components";
|
||||
|
||||
import type { AttachmentLike } from "@halo-dev/console-shared";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import LazyImage from "@/components/image/LazyImage.vue";
|
||||
import type { Attachment } from "@halo-dev/api-client";
|
||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||
import AttachmentFileTypeIcon from "../AttachmentFileTypeIcon.vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { isImage } from "@/utils/image";
|
||||
import { useFetchAttachmentPolicy } from "../../composables/use-attachment-policy";
|
||||
import { useFetchAttachmentGroup } from "../../composables/use-attachment-group";
|
||||
import type { SuccessResponse } from "@uppy/core";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
selected: AttachmentLike[];
|
||||
}>(),
|
||||
{
|
||||
selected: () => [],
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:selected", attachments: AttachmentLike[]): void;
|
||||
}>();
|
||||
|
||||
const { policies } = useFetchAttachmentPolicy({ fetchOnMounted: true });
|
||||
const policyMap = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: "选择存储策略",
|
||||
value: "",
|
||||
},
|
||||
...policies.value.map((policy) => {
|
||||
return {
|
||||
label: policy.spec.displayName,
|
||||
value: policy.metadata.name,
|
||||
};
|
||||
}),
|
||||
];
|
||||
});
|
||||
const selectedPolicy = ref("");
|
||||
|
||||
const { groups } = useFetchAttachmentGroup({ fetchOnMounted: true });
|
||||
const groupMap = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: "选择分组",
|
||||
value: "",
|
||||
},
|
||||
...groups.value.map((group) => {
|
||||
return {
|
||||
label: group.spec.displayName,
|
||||
value: group.metadata.name,
|
||||
};
|
||||
}),
|
||||
];
|
||||
});
|
||||
const selectedGroup = ref("");
|
||||
|
||||
const attachments = ref<Set<Attachment>>(new Set<Attachment>());
|
||||
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>());
|
||||
|
||||
const onUploaded = async (response: SuccessResponse) => {
|
||||
const attachment = response.body as Attachment;
|
||||
|
||||
const { data } =
|
||||
await apiClient.extension.storage.attachment.getstorageHaloRunV1alpha1Attachment(
|
||||
{
|
||||
name: attachment.metadata.name,
|
||||
}
|
||||
);
|
||||
attachments.value.add(data);
|
||||
selectedAttachments.value.add(data);
|
||||
};
|
||||
|
||||
const handleSelect = async (attachment: Attachment | undefined) => {
|
||||
if (!attachment) return;
|
||||
if (selectedAttachments.value.has(attachment)) {
|
||||
selectedAttachments.value.delete(attachment);
|
||||
return;
|
||||
}
|
||||
selectedAttachments.value.add(attachment);
|
||||
};
|
||||
|
||||
const handleDelete = async (attachment: Attachment) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除当前的附件吗?",
|
||||
confirmType: "danger",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.storage.attachment.deletestorageHaloRunV1alpha1Attachment(
|
||||
{
|
||||
name: attachment.metadata.name,
|
||||
}
|
||||
);
|
||||
attachments.value.delete(attachment);
|
||||
selectedAttachments.value.delete(attachment);
|
||||
|
||||
Toast.success("删除成功");
|
||||
} catch (e) {
|
||||
console.error("Failed to delete attachment", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
emit("update:selected", Array.from(selectedAttachments.value));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-4 sm:flex-row">
|
||||
<div class="h-full w-full space-y-4 overflow-auto sm:w-96">
|
||||
<FormKit type="form">
|
||||
<FormKit
|
||||
v-model="selectedPolicy"
|
||||
type="select"
|
||||
:options="policyMap"
|
||||
label="存储策略"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="selectedGroup"
|
||||
:options="groupMap"
|
||||
type="select"
|
||||
label="分组"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<UppyUpload
|
||||
v-if="selectedPolicy"
|
||||
endpoint="/apis/api.console.halo.run/v1alpha1/attachments/upload"
|
||||
:disabled="!selectedPolicy"
|
||||
:meta="{
|
||||
policyName: selectedPolicy,
|
||||
groupName: selectedGroup,
|
||||
}"
|
||||
:allowed-meta-fields="['policyName', 'groupName']"
|
||||
:note="selectedPolicy ? '' : '请先选择存储策略'"
|
||||
@uploaded="onUploaded"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-full flex-1 overflow-auto">
|
||||
<VEmpty
|
||||
v-if="!attachments.size"
|
||||
message="当前没有上传的文件,你可以点击左侧区域上传文件"
|
||||
title="当前没有上传的文件"
|
||||
>
|
||||
</VEmpty>
|
||||
<div
|
||||
v-else
|
||||
class="grid grid-cols-3 gap-x-2 gap-y-3 p-0.5 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-6"
|
||||
role="list"
|
||||
>
|
||||
<VCard
|
||||
v-for="(attachment, index) in Array.from(attachments)"
|
||||
:key="index"
|
||||
:body-class="['!p-0']"
|
||||
:class="{
|
||||
'ring-1 ring-primary': selectedAttachments.has(attachment),
|
||||
}"
|
||||
class="hover:shadow"
|
||||
@click="handleSelect(attachment)"
|
||||
>
|
||||
<div class="group relative bg-white">
|
||||
<div
|
||||
class="aspect-w-10 aspect-h-8 block h-full w-full cursor-pointer overflow-hidden bg-gray-100"
|
||||
>
|
||||
<LazyImage
|
||||
v-if="isImage(attachment.spec.mediaType)"
|
||||
:key="attachment.metadata.name"
|
||||
:alt="attachment.spec.displayName"
|
||||
:src="attachment.status?.permalink"
|
||||
classes="pointer-events-none object-cover group-hover:opacity-75"
|
||||
>
|
||||
<template #loading>
|
||||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-gray-400">加载中...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-red-400">加载异常</span>
|
||||
</div>
|
||||
</template>
|
||||
</LazyImage>
|
||||
<AttachmentFileTypeIcon
|
||||
v-else
|
||||
:file-name="attachment.spec.displayName"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="pointer-events-none block truncate px-2 py-1 text-center text-xs font-medium text-gray-700"
|
||||
>
|
||||
{{ attachment.spec.displayName }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
:class="{ '!flex': selectedAttachments.has(attachment) }"
|
||||
class="absolute top-0 left-0 hidden h-1/3 w-full justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
|
||||
>
|
||||
<IconDeleteBin
|
||||
class="mt-1 mr-1 hidden h-5 w-5 cursor-pointer text-red-400 transition-all hover:text-red-600 group-hover:block"
|
||||
@click.stop="handleDelete(attachment)"
|
||||
/>
|
||||
<IconCheckboxFill
|
||||
:class="{
|
||||
'!text-primary': selectedAttachments.has(attachment),
|
||||
}"
|
||||
class="mt-1 mr-1 h-5 w-5 cursor-pointer text-white transition-all hover:text-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
Loading…
Reference in New Issue