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
Ryan Wang 2022-12-26 14:32:32 +08:00 committed by GitHub
parent b29a72d7a5
commit 023831cdd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 273 deletions

View File

@ -298,7 +298,6 @@ onMounted(() => {
</AttachmentDetailModal>
<AttachmentUploadModal
v-model:visible="uploadVisible"
:group="selectedGroup"
@close="onUploadModalClose"
/>
<AttachmentPoliciesModal v-model:visible="policyVisible" />

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>