1.5.1原版
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PhotoGroup } from "@/types";
|
||||
import { submitForm } from "@formkit/core";
|
||||
import { axiosInstance } from "@halo-dev/api-client";
|
||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import { useMagicKeys } from "@vueuse/core";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
group?: PhotoGroup;
|
||||
}>(),
|
||||
{
|
||||
group: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const initialFormState: PhotoGroup = {
|
||||
apiVersion: "core.halo.run/v1alpha1",
|
||||
kind: "PhotoGroup",
|
||||
metadata: {
|
||||
name: "",
|
||||
generateName: "photo-group-",
|
||||
},
|
||||
spec: {
|
||||
displayName: "",
|
||||
priority: 0,
|
||||
},
|
||||
status: {
|
||||
photoCount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const formState = ref<PhotoGroup>(initialFormState);
|
||||
const isSubmitting = ref(false);
|
||||
const modal = useTemplateRef<InstanceType<typeof VModal> | null>("modal");
|
||||
|
||||
const isUpdateMode = computed(() => {
|
||||
return !!formState.value.metadata.creationTimestamp;
|
||||
});
|
||||
|
||||
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value ? "编辑分组" : "新建分组";
|
||||
});
|
||||
|
||||
const annotationsGroupFormRef = ref();
|
||||
|
||||
const handleCreateOrUpdateGroup = async () => {
|
||||
annotationsGroupFormRef.value?.handleSubmit();
|
||||
await nextTick();
|
||||
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } = annotationsGroupFormRef.value || {};
|
||||
if (customFormInvalid || specFormInvalid) {
|
||||
return;
|
||||
}
|
||||
formState.value.metadata.annotations = {
|
||||
...annotations,
|
||||
...customAnnotations,
|
||||
};
|
||||
try {
|
||||
isSubmitting.value = true;
|
||||
if (isUpdateMode.value) {
|
||||
await axiosInstance.put(
|
||||
`/apis/core.halo.run/v1alpha1/photogroups/${formState.value.metadata.name}`,
|
||||
formState.value
|
||||
);
|
||||
} else {
|
||||
await axiosInstance.post("/apis/core.halo.run/v1alpha1/photogroups", formState.value);
|
||||
}
|
||||
modal.value?.close();
|
||||
} catch (e) {
|
||||
console.error("Failed to create photo group", e);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.group) {
|
||||
formState.value = cloneDeep(props.group);
|
||||
}
|
||||
});
|
||||
|
||||
const { ControlLeft_Enter, Meta_Enter } = useMagicKeys();
|
||||
|
||||
watch(ControlLeft_Enter, (v) => {
|
||||
if (v && !isMac) {
|
||||
submitForm("photo-group-form");
|
||||
}
|
||||
});
|
||||
|
||||
watch(Meta_Enter, (v) => {
|
||||
if (v && isMac) {
|
||||
submitForm("photo-group-form");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<VModal ref="modal" :width="600" :title="modalTitle" @close="emit('close')">
|
||||
<FormKit
|
||||
id="photo-group-form"
|
||||
v-model="formState.spec"
|
||||
name="photo-group-form"
|
||||
type="form"
|
||||
@submit="handleCreateOrUpdateGroup"
|
||||
>
|
||||
<div class=":uno: md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class=":uno: md:col-span-1">
|
||||
<div class=":uno: sticky top-0">
|
||||
<span class=":uno: text-base text-gray-900 font-medium"> 常规 </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class=":uno: mt-5 md:col-span-3 md:mt-0 divide-y divide-gray-100">
|
||||
<FormKit
|
||||
name="displayName"
|
||||
label="分组名称"
|
||||
type="text"
|
||||
validation="required"
|
||||
help="可根据此名称查询图片"
|
||||
></FormKit>
|
||||
</div>
|
||||
</div>
|
||||
</FormKit>
|
||||
<div class=":uno: py-5">
|
||||
<div class=":uno: border-t border-gray-200"></div>
|
||||
</div>
|
||||
<div class=":uno: md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class=":uno: md:col-span-1">
|
||||
<div class=":uno: sticky top-0">
|
||||
<span class=":uno: text-base text-gray-900 font-medium"> 元数据 </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class=":uno: mt-5 md:col-span-3 md:mt-0 divide-y divide-gray-100">
|
||||
<AnnotationsForm
|
||||
:key="formState.metadata.name"
|
||||
ref="annotationsGroupFormRef"
|
||||
:value="formState.metadata.annotations"
|
||||
kind="PhotoGroup"
|
||||
group="core.halo.run"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton :loading="isSubmitting" type="secondary" @click="submitForm('photo-group-form')">
|
||||
提交 {{ `${isMac ? "⌘" : "Ctrl"} + ↵` }}
|
||||
</VButton>
|
||||
<VButton @click="emit('close')">取消 Esc</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
||||
@@ -0,0 +1,205 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PhotoGroup, PhotoGroupList } from "../types/index";
|
||||
import { axiosInstance } from "@halo-dev/api-client";
|
||||
import {
|
||||
Dialog,
|
||||
IconList,
|
||||
VButton,
|
||||
VCard,
|
||||
VDropdownItem,
|
||||
VEmpty,
|
||||
VEntity,
|
||||
VEntityField,
|
||||
VLoading,
|
||||
VStatusDot,
|
||||
} from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { ref } from "vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import GroupEditingModal from "./GroupEditingModal.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", group?: string): void;
|
||||
}>();
|
||||
|
||||
const groupEditingModal = ref(false);
|
||||
|
||||
const updateGroup = ref<PhotoGroup>();
|
||||
|
||||
const selectedGroup = useRouteQuery<string>("photo-group");
|
||||
|
||||
const groups = ref<PhotoGroup[]>([]);
|
||||
|
||||
const { refetch, isLoading } = useQuery<PhotoGroup[]>({
|
||||
queryKey: ["plugin:photos:groups"],
|
||||
queryFn: async () => {
|
||||
const { data } = await axiosInstance.get<PhotoGroupList>("/apis/console.api.photo.halo.run/v1alpha1/photogroups");
|
||||
return data.items
|
||||
.map((group) => {
|
||||
if (group.spec) {
|
||||
group.spec.priority = group.spec.priority || 0;
|
||||
}
|
||||
return group;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return (a.spec?.priority || 0) - (b.spec?.priority || 0);
|
||||
});
|
||||
},
|
||||
refetchInterval(data) {
|
||||
const hasDeletingGroup = data?.some((group) => !!group.metadata.deletionTimestamp);
|
||||
return hasDeletingGroup ? 1000 : false;
|
||||
},
|
||||
onSuccess(data) {
|
||||
groups.value = data;
|
||||
|
||||
if (selectedGroup.value) {
|
||||
const groupNames = data.map((group) => group.metadata.name);
|
||||
if (groupNames.includes(selectedGroup.value)) {
|
||||
emit("select", selectedGroup.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length) {
|
||||
handleSelectedClick(data[0]);
|
||||
} else {
|
||||
selectedGroup.value = "";
|
||||
emit("select", "");
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const handleSaveInBatch = async () => {
|
||||
try {
|
||||
const promises = groups.value?.map((group: PhotoGroup, index) => {
|
||||
if (group.spec) {
|
||||
group.spec.priority = index;
|
||||
}
|
||||
return axiosInstance.put(`/apis/core.halo.run/v1alpha1/photogroups/${group.metadata.name}`, group);
|
||||
});
|
||||
if (promises) {
|
||||
await Promise.all(promises);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (group: PhotoGroup) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该分组吗?",
|
||||
description: "将同时删除该分组下的所有图片,该操作不可恢复。",
|
||||
confirmType: "danger",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await axiosInstance.delete(`/apis/console.api.photo.halo.run/v1alpha1/photogroups/${group.metadata.name}`);
|
||||
refetch();
|
||||
} catch (e) {
|
||||
console.error("Failed to delete photo group", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenEditingModal = (group?: PhotoGroup) => {
|
||||
groupEditingModal.value = true;
|
||||
updateGroup.value = group;
|
||||
};
|
||||
|
||||
const handleSelectedClick = (group: PhotoGroup) => {
|
||||
selectedGroup.value = group.metadata.name;
|
||||
emit("select", group.metadata.name);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
refetch,
|
||||
});
|
||||
|
||||
function onGroupEditingModalClose() {
|
||||
groupEditingModal.value = false;
|
||||
refetch();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<GroupEditingModal v-if="groupEditingModal" :group="updateGroup" @close="onGroupEditingModalClose" />
|
||||
<VCard :body-class="[':uno: !p-0']" title="分组">
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!groups || !groups.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者新建分组" title="当前没有分组">
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton size="sm" @click="refetch()"> 刷新</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEmpty>
|
||||
</Transition>
|
||||
<Transition v-else appear name="fade">
|
||||
<div class=":uno: w-full overflow-x-auto">
|
||||
<table class=":uno: w-full border-spacing-0">
|
||||
<VueDraggable
|
||||
v-model="groups"
|
||||
class=":uno: divide-y divide-gray-100"
|
||||
group="group"
|
||||
handle=".drag-element"
|
||||
item-key="metadata.name"
|
||||
tag="tbody"
|
||||
@update="handleSaveInBatch"
|
||||
>
|
||||
<VEntity
|
||||
v-for="group in groups"
|
||||
:key="group.metadata.name"
|
||||
:is-selected="selectedGroup === group.metadata.name"
|
||||
class=":uno: group"
|
||||
@click="handleSelectedClick(group)"
|
||||
>
|
||||
<template #prepend>
|
||||
<div
|
||||
class=":uno: drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all group-hover:flex hover:bg-gray-200"
|
||||
>
|
||||
<IconList class=":uno: size-3.5" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #start>
|
||||
<VEntityField
|
||||
:title="group.spec?.displayName"
|
||||
:description="`${group.status.photoCount || 0} 个图片`"
|
||||
></VEntityField>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<VEntityField v-if="group.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
|
||||
<template #dropdownItems>
|
||||
<VDropdownItem @click="handleOpenEditingModal(group)"> 修改 </VDropdownItem>
|
||||
<VDropdownItem type="danger" @click="handleDelete(group)"> 删除 </VDropdownItem>
|
||||
</template>
|
||||
</VEntity>
|
||||
</VueDraggable>
|
||||
</table>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<template v-if="!isLoading" #footer>
|
||||
<!-- @unocss-skip-start -->
|
||||
<VButton
|
||||
v-permission="['plugin:photos:manage']"
|
||||
block
|
||||
type="secondary"
|
||||
@click="handleOpenEditingModal(undefined)"
|
||||
>
|
||||
新增分组
|
||||
</VButton>
|
||||
<!-- @unocss-skip-end -->
|
||||
</template>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
src: string;
|
||||
alt?: string;
|
||||
classes?: string | string[];
|
||||
}>(),
|
||||
{
|
||||
src: "",
|
||||
alt: "",
|
||||
classes: "",
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
const loadImage = async () => {
|
||||
const image = new Image();
|
||||
image.src = props.src;
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = (err) => reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await loadImage();
|
||||
} catch (e) {
|
||||
error.value = true;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<template v-if="isLoading">
|
||||
<slot name="loading"> loading... </slot>
|
||||
</template>
|
||||
<template v-else-if="error">
|
||||
<slot name="error"> error </slot>
|
||||
</template>
|
||||
<img v-else class=":uno: size-full object-cover" :src="src" :alt="alt" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,148 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Photo } from "@/types";
|
||||
import { submitForm } from "@formkit/core";
|
||||
import { axiosInstance } from "@halo-dev/api-client";
|
||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
photo?: Photo;
|
||||
group?: string;
|
||||
}>(),
|
||||
{
|
||||
photo: undefined,
|
||||
group: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
(event: "saved", photo: Photo): void;
|
||||
}>();
|
||||
|
||||
const initialFormState: Photo = {
|
||||
metadata: {
|
||||
name: "",
|
||||
generateName: "photo-",
|
||||
},
|
||||
spec: {
|
||||
displayName: "",
|
||||
url: "",
|
||||
cover: "",
|
||||
groupName: props.group || "",
|
||||
},
|
||||
kind: "Photo",
|
||||
apiVersion: "core.halo.run/v1alpha1",
|
||||
} as Photo;
|
||||
|
||||
const formState = ref<Photo>(cloneDeep(initialFormState));
|
||||
const isSubmitting = ref<boolean>(false);
|
||||
const modal = useTemplateRef<InstanceType<typeof VModal> | null>("modal");
|
||||
|
||||
const isUpdateMode = computed(() => {
|
||||
return !!formState.value.metadata.creationTimestamp;
|
||||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value ? "编辑图片" : "添加图片";
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.photo) {
|
||||
formState.value = cloneDeep(props.photo);
|
||||
}
|
||||
});
|
||||
|
||||
const annotationsFormRef = ref();
|
||||
|
||||
const handleSavePhoto = async () => {
|
||||
annotationsFormRef.value?.handleSubmit();
|
||||
await nextTick();
|
||||
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } = annotationsFormRef.value || {};
|
||||
if (customFormInvalid || specFormInvalid) {
|
||||
return;
|
||||
}
|
||||
formState.value.metadata.annotations = {
|
||||
...annotations,
|
||||
...customAnnotations,
|
||||
};
|
||||
try {
|
||||
isSubmitting.value = true;
|
||||
if (isUpdateMode.value) {
|
||||
await axiosInstance.put<Photo>(
|
||||
`/apis/core.halo.run/v1alpha1/photos/${formState.value.metadata.name}`,
|
||||
formState.value
|
||||
);
|
||||
} else {
|
||||
if (props.group) {
|
||||
formState.value.spec.groupName = props.group;
|
||||
}
|
||||
const { data } = await axiosInstance.post<Photo>(`/apis/core.halo.run/v1alpha1/photos`, formState.value);
|
||||
emit("saved", data);
|
||||
}
|
||||
modal.value?.close();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<VModal ref="modal" :title="modalTitle" :width="650" @close="emit('close')">
|
||||
<template #actions>
|
||||
<slot name="append-actions" />
|
||||
</template>
|
||||
|
||||
<FormKit
|
||||
id="photo-form"
|
||||
v-model="formState.spec"
|
||||
name="photo-form"
|
||||
:actions="false"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
type="form"
|
||||
@submit="handleSavePhoto"
|
||||
>
|
||||
<div class=":uno: md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class=":uno: md:col-span-1">
|
||||
<div class=":uno: sticky top-0">
|
||||
<span class=":uno: text-base text-gray-900 font-medium"> 常规 </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class=":uno: mt-5 md:col-span-3 md:mt-0 divide-y divide-gray-100">
|
||||
<FormKit name="displayName" label="名称" type="text" validation="required"></FormKit>
|
||||
<FormKit name="url" label="图片地址" type="attachment" :accepts="['image/*']" validation="required"></FormKit>
|
||||
<FormKit name="cover" label="封面" type="attachment" :accepts="['image/*']"></FormKit>
|
||||
<FormKit name="description" label="描述" type="textarea"></FormKit>
|
||||
</div>
|
||||
</div>
|
||||
</FormKit>
|
||||
<div class=":uno: py-5">
|
||||
<div class=":uno: border-t border-gray-200"></div>
|
||||
</div>
|
||||
<div class=":uno: md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class=":uno: md:col-span-1">
|
||||
<div class=":uno: sticky top-0">
|
||||
<span class=":uno: text-base text-gray-900 font-medium"> 元数据 </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class=":uno: mt-5 md:col-span-3 md:mt-0 divide-y divide-gray-100">
|
||||
<AnnotationsForm
|
||||
:key="formState.metadata.name"
|
||||
ref="annotationsFormRef"
|
||||
:value="formState.metadata.annotations"
|
||||
kind="Photo"
|
||||
group="core.halo.run"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton :loading="isSubmitting" type="secondary" @click="submitForm('photo-form')"> 保存 </VButton>
|
||||
<VButton @click="modal?.close()">取消</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user