refactor: api of dialog component (#646)

#### What type of PR is this?

/kind api-change
/kind improvement
/milestone 2.0

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

重构 Dialog 组件使用 API 的调用方式,改为与 Toast 组件一致。https://github.com/halo-dev/console/pull/644

同样的,使用此方式调用 Dialog 组件不限制在 Vue 组件。

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

需要测试后台各个操作的会话框是否正常。

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

```release-note
重构 Dialog 组件使用 API 的调用方式。
```
pull/645/head^2
Ryan Wang 2022-10-18 09:58:09 +08:00 committed by GitHub
parent 6d8a2ddd75
commit 512ee82216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 153 additions and 164 deletions

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { VButton } from "@/components/button";
import { VDialog } from "@/components/dialog";
import { Dialog } from "@/components/dialog";
const initState = () => {
return {
@ -18,19 +18,26 @@ const initState = () => {
},
};
};
function handleConfirm() {
Dialog.success({
title: "Hello",
description: "Hello World",
onConfirm: async () => {
await new Promise((resolve) =>
setTimeout(() => {
console.log("Timeout");
resolve("");
}, 1000)
);
},
});
}
</script>
<template>
<Story :init-state="initState" title="Dialog">
<template #default="{ state }">
<VButton type="danger" @click="state.visible = true">删除</VButton>
<VDialog
:cancel-text="state.cancelText"
:confirm-text="state.confirmText"
:description="state.description"
:title="state.title"
:type="state.type"
:visible="state.visible"
></VDialog>
<template #default>
<VButton @click="handleConfirm"></VButton>
</template>
</Story>
</template>

View File

@ -1,27 +0,0 @@
<script lang="ts" setup>
import { VDialog } from "./index";
import { provide, ref } from "vue";
import type { useDialogOptions } from "./interface";
import { DialogProviderProvideKey } from "@/components/dialog/interface";
const options = ref<useDialogOptions>({
visible: false,
title: "",
});
provide(DialogProviderProvideKey, options);
</script>
<template>
<slot />
<VDialog
v-model:visible="options.visible"
:cancel-text="options.cancelText"
:confirm-text="options.confirmText"
:confirm-type="options.confirmType"
:description="options.description"
:on-cancel="options.onCancel"
:on-confirm="options.onConfirm"
:title="options.title"
:type="options.type"
></VDialog>
</template>

View File

@ -0,0 +1,75 @@
import DialogComponent from "./Dialog.vue";
import { createVNode, render, type Component } from "vue";
import type { DialogProps } from "./interface";
export type DialogApiProps = Omit<DialogProps, "type" | "visible">;
export type DialogApi = (props?: DialogApiProps) => void;
export interface DialogEntry {
(props: DialogProps): void;
info: DialogApi;
success: DialogApi;
error: DialogApi;
warning: DialogApi;
}
const defaultProps: DialogProps = {
title: "",
visible: false,
};
const dialog: DialogEntry = (userProps: DialogProps) => {
const props = {
...defaultProps,
...userProps,
};
let container = document.body.querySelector(".dialog-container");
if (!container) {
container = document.createElement("div");
container.className = "dialog-container";
document.body.appendChild(container);
}
const { vnode, container: hostContainer } = createVNodeComponent(
DialogComponent,
props
);
if (hostContainer.firstElementChild) {
container.appendChild(hostContainer.firstElementChild);
}
if (vnode.component?.props) {
vnode.component.props.visible = true;
}
if (vnode?.props) {
// close emit
vnode.props.onClose = () => {
container?.remove();
render(null, hostContainer);
};
}
};
function createVNodeComponent(
component: Component,
props: Record<string, unknown>
) {
const vnode = createVNode(component, props);
const container = document.createElement("div");
render(vnode, container);
return { vnode, container };
}
dialog.success = (props?: DialogApiProps) =>
dialog({ ...props, type: "success" });
dialog.info = (props?: DialogApiProps) => dialog({ ...props, type: "info" });
dialog.warning = (props?: DialogApiProps) =>
dialog({ ...props, type: "warning" });
dialog.error = (props?: DialogApiProps) => dialog({ ...props, type: "error" });
export { dialog as Dialog };

View File

@ -1,3 +1,2 @@
export { default as VDialog } from "./Dialog.vue";
export { default as VDialogProvider } from "./DialogProvider.vue";
export * from "./use-dialog";
export { Dialog } from "./dialog-manager";

View File

@ -14,4 +14,16 @@ export interface useDialogOptions {
onCancel?: () => void;
}
export interface DialogProps {
type?: Type;
visible?: boolean;
title?: string;
description?: string;
confirmType?: ButtonType;
confirmText?: string;
cancelText?: string;
onConfirm?: () => void;
onCancel?: () => void;
}
export type useDialogUserOptions = Omit<useDialogOptions, "type" | "visible">;

View File

@ -1,39 +0,0 @@
import type { Ref } from "vue";
import { inject } from "vue";
import type {
Type,
useDialogOptions,
useDialogUserOptions,
} from "@/components/dialog/interface";
import { DialogProviderProvideKey } from "@/components/dialog/interface";
interface useDialogReturn {
success: (options: useDialogUserOptions) => void;
info: (options: useDialogUserOptions) => void;
warning: (options: useDialogUserOptions) => void;
error: (options: useDialogUserOptions) => void;
}
export function useDialog(): useDialogReturn {
const dialogOptions = inject<Ref<useDialogOptions>>(DialogProviderProvideKey);
if (!dialogOptions) {
throw new Error("DialogProvider is not mounted");
}
const createDialog = (type: Type) => (options: useDialogUserOptions) => {
// clear previous dialog
dialogOptions.value = { title: "", visible: false };
dialogOptions.value = { ...dialogOptions.value, ...options };
dialogOptions.value.type = type;
dialogOptions.value.visible = true;
};
return {
success: createDialog("success"),
info: createDialog("info"),
warning: createDialog("warning"),
error: createDialog("error"),
};
}

View File

@ -8,7 +8,7 @@ import {
VAvatar,
VSpace,
VButton,
useDialog,
Dialog,
} from "@halo-dev/components";
import type { MenuGroupType, MenuItemType } from "../types/menus";
import type { User } from "@halo-dev/api-client";
@ -21,7 +21,6 @@ const menus = inject<MenuGroupType[]>("menus");
const minimenus = inject<MenuItemType[]>("minimenus");
const route = useRoute();
const router = useRouter();
const dialog = useDialog();
const moreMenuVisible = ref(false);
const moreMenuRootVisible = ref(false);
@ -34,7 +33,7 @@ const handleRouteToProfile = () => {
};
const handleLogout = () => {
dialog.warning({
Dialog.warning({
title: "是否确认退出登录?",
onConfirm: async () => {
try {

View File

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { RouterView, useRoute } from "vue-router";
import { VDialogProvider } from "@halo-dev/components";
import { onMounted, provide, ref, watch, type Ref } from "vue";
import { useTitle } from "@vueuse/core";
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
@ -41,10 +40,8 @@ onMounted(() => {
</script>
<template>
<VDialogProvider>
<RouterView />
<GlobalSearchModal v-model:visible="globalSearchVisible" />
</VDialogProvider>
<RouterView />
<GlobalSearchModal v-model:visible="globalSearchVisible" />
</template>
<style lang="scss">

View File

@ -4,7 +4,7 @@ import {
IconCheckboxFill,
VCard,
IconDeleteBin,
useDialog,
Dialog,
} from "@halo-dev/components";
import type { AttachmentLike } from "@halo-dev/console-shared";
@ -103,10 +103,8 @@ const handleSelect = async (attachment: Attachment | undefined) => {
selectedAttachments.value.add(attachment);
};
const dialog = useDialog();
const handleDelete = async (attachment: Attachment) => {
dialog.warning({
Dialog.warning({
title: "确定要删除当前的附件吗?",
confirmType: "danger",
onConfirm: async () => {

View File

@ -9,7 +9,7 @@ import type { Ref } from "vue";
import { ref, watch } from "vue";
import type { AttachmentLike } from "@halo-dev/console-shared";
import { apiClient } from "@/utils/api-client";
import { useDialog } from "@halo-dev/components";
import { Dialog } from "@halo-dev/components";
import type { Content, Editor } from "@halo-dev/richtext-editor";
interface useAttachmentControlReturn {
@ -64,8 +64,6 @@ export function useAttachmentControl(filterOptions?: {
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>());
const checkedAll = ref(false);
const dialog = useDialog();
const handleFetchAttachments = async () => {
try {
loading.value = true;
@ -133,7 +131,7 @@ export function useAttachmentControl(filterOptions?: {
};
const handleDelete = (attachment: Attachment) => {
dialog.warning({
Dialog.warning({
title: "确定要删除该附件吗?",
description: "删除之后将无法恢复",
confirmType: "danger",
@ -160,7 +158,7 @@ export function useAttachmentControl(filterOptions?: {
};
const handleDeleteInBatch = () => {
dialog.warning({
Dialog.warning({
title: "确定要删除所选的附件吗?",
description: "删除之后将无法恢复",
confirmType: "danger",

View File

@ -9,7 +9,7 @@ import {
VSpace,
IconCloseCircle,
VEmpty,
useDialog,
Dialog,
} from "@halo-dev/components";
import CommentListItem from "./components/CommentListItem.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
@ -21,8 +21,6 @@ import type {
import { onMounted, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
const dialog = useDialog();
const comments = ref<ListedCommentList>({
page: 1,
size: 20,
@ -101,7 +99,7 @@ watch(
);
const handleDeleteInBatch = async () => {
dialog.warning({
Dialog.warning({
title: "确定要删除所选的评论吗?",
description: "将同时删除所有评论下的回复,该操作不可恢复。",
confirmType: "danger",
@ -126,7 +124,7 @@ const handleDeleteInBatch = async () => {
};
const handleApproveInBatch = async () => {
dialog.warning({
Dialog.warning({
title: "确定要审核通过所选评论吗?",
onConfirm: async () => {
try {

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import {
useDialog,
Dialog,
VAvatar,
VButton,
VEntity,
@ -44,8 +44,6 @@ const emit = defineEmits<{
(event: "reload"): void;
}>();
const dialog = useDialog();
const replies = ref<ListedReply[]>([] as ListedReply[]);
const selectedReply = ref<ListedReply>();
const hoveredReply = ref<ListedReply>();
@ -56,7 +54,7 @@ const replyModal = ref(false);
provide<Ref<ListedReply | undefined>>("hoveredReply", hoveredReply);
const handleDelete = async () => {
dialog.warning({
Dialog.warning({
title: "是否确认删除该评论?",
description: "删除评论的同时会删除该评论下的所有回复,该操作不可恢复。",
confirmType: "danger",
@ -75,7 +73,7 @@ const handleDelete = async () => {
};
const handleApproveReplyInBatch = async () => {
dialog.warning({
Dialog.warning({
title: "确定要审核通过该评论的所有回复吗?",
onConfirm: async () => {
try {

View File

@ -5,7 +5,7 @@ import {
VTag,
VEntityField,
VEntity,
useDialog,
Dialog,
VStatusDot,
IconReplyLine,
} from "@halo-dev/components";
@ -31,8 +31,6 @@ const emit = defineEmits<{
(event: "reply", reply: ListedReply): void;
}>();
const dialog = useDialog();
const quoteReply = computed(() => {
const { quoteReply: replyName } = props.reply.reply.spec;
@ -46,7 +44,7 @@ const quoteReply = computed(() => {
});
const handleDelete = async () => {
dialog.warning({
Dialog.warning({
title: "是否确认删除该回复?",
description: "该操作不可恢复。",
confirmType: "danger",

View File

@ -12,7 +12,7 @@ import {
VCard,
VPagination,
VSpace,
useDialog,
Dialog,
VEmpty,
VAvatar,
VStatusDot,
@ -42,8 +42,6 @@ enum SinglePagePhase {
PUBLISHED = "已发布",
}
const dialog = useDialog();
const singlePages = ref<ListedSinglePageList>({
page: 1,
size: 20,
@ -177,7 +175,7 @@ const handleSelectNext = async () => {
};
const handleDelete = async (singlePage: SinglePage) => {
dialog.warning({
Dialog.warning({
title: "是否确认删除该自定义页面?",
confirmType: "danger",
onConfirm: async () => {

View File

@ -9,7 +9,7 @@ import {
IconEyeOff,
IconTeam,
IconCloseCircle,
useDialog,
Dialog,
VButton,
VCard,
VEmpty,
@ -64,8 +64,6 @@ const selectedPostWithContent = ref<PostRequest | null>(null);
const checkedAll = ref(false);
const selectedPostNames = ref<string[]>([]);
const dialog = useDialog();
const handleFetchPosts = async () => {
try {
loading.value = true;
@ -205,7 +203,7 @@ const handleCheckAllChange = (e: Event) => {
};
const handleDelete = async (post: Post) => {
dialog.warning({
Dialog.warning({
title: "是否确认删除该文章?",
confirmType: "danger",
onConfirm: async () => {
@ -218,7 +216,7 @@ const handleDelete = async (post: Post) => {
};
const handleDeleteInBatch = async () => {
dialog.warning({
Dialog.warning({
title: "是否确认删除选中的文章?",
confirmType: "danger",
onConfirm: async () => {

View File

@ -4,7 +4,7 @@ import type { Ref } from "vue";
import { onMounted, ref } from "vue";
import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
import { buildCategoriesTree } from "@/modules/contents/posts/categories/utils";
import { useDialog } from "@halo-dev/components";
import { Dialog } from "@halo-dev/components";
interface usePostCategoryReturn {
categories: Ref<Category[]>;
@ -23,8 +23,6 @@ export function usePostCategory(options?: {
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
const loading = ref(false);
const dialog = useDialog();
const handleFetchCategories = async () => {
try {
loading.value = true;
@ -43,7 +41,7 @@ export function usePostCategory(options?: {
};
const handleDelete = async (category: CategoryTree) => {
dialog.warning({
Dialog.warning({
title: "确定要删除该分类吗?",
description: "删除此分类之后,对应文章的关联将被解除。该操作不可恢复。",
confirmType: "danger",

View File

@ -2,7 +2,7 @@ import { apiClient } from "@/utils/api-client";
import type { Tag } from "@halo-dev/api-client";
import type { Ref } from "vue";
import { onMounted, ref } from "vue";
import { useDialog } from "@halo-dev/components";
import { Dialog } from "@halo-dev/components";
interface usePostTagReturn {
tags: Ref<Tag[]>;
@ -19,8 +19,6 @@ export function usePostTag(options?: {
const tags = ref<Tag[]>([] as Tag[]);
const loading = ref(false);
const dialog = useDialog();
const handleFetchTags = async () => {
try {
loading.value = true;
@ -39,7 +37,7 @@ export function usePostTag(options?: {
};
const handleDelete = async (tag: Tag) => {
dialog.warning({
Dialog.warning({
title: "确定要删除该标签吗?",
description: "删除此标签之后,对应文章的关联将被解除。该操作不可恢复。",
confirmType: "danger",

View File

@ -2,7 +2,7 @@
import {
IconAddCircle,
IconListSettings,
useDialog,
Dialog,
VButton,
VCard,
VEmpty,
@ -35,8 +35,6 @@ const loading = ref(false);
const menuListRef = ref();
const menuItemEditingModal = ref();
const dialog = useDialog();
const handleFetchMenuItems = async () => {
try {
loading.value = true;
@ -115,7 +113,7 @@ const handleUpdateInBatch = useDebounceFn(async () => {
}, 500);
const handleDelete = async (menuItem: MenuTreeItem) => {
dialog.info({
Dialog.info({
title: "是否确定删除该菜单?",
description: "删除后将无法恢复",
confirmType: "danger",
@ -128,7 +126,7 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
if (childrenNames.length) {
setTimeout(() => {
dialog.info({
Dialog.info({
title: "检查到当前菜单下包含子菜单,是否删除?",
description: "如果选择否,那么所有子菜单将转移到一级菜单",
confirmType: "danger",

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import {
useDialog,
Dialog,
VButton,
VCard,
VEmpty,
@ -37,8 +37,6 @@ const loading = ref(false);
const selectedMenuToUpdate = ref<Menu>();
const menuEditingModal = ref<boolean>(false);
const dialog = useDialog();
const handleFetchMenus = async () => {
selectedMenuToUpdate.value = undefined;
try {
@ -71,7 +69,7 @@ const handleSelect = (menu: Menu) => {
};
const handleDeleteMenu = async (menu: Menu) => {
dialog.warning({
Dialog.warning({
title: "确定要删除该菜单吗?",
description: "将同时删除该菜单下的所有菜单项,该操作不可恢复。",
confirmType: "danger",

View File

@ -10,7 +10,7 @@ import {
VTag,
IconMore,
VButton,
useDialog,
Dialog,
} from "@halo-dev/components";
// types
@ -22,10 +22,8 @@ import { apiClient } from "@/utils/api-client";
const selectedTheme = inject<Ref<Theme | undefined>>("selectedTheme");
const isActivated = inject<ComputedRef<boolean>>("isActivated");
const dialog = useDialog();
const handleReloadThemeSetting = async () => {
dialog.warning({
Dialog.warning({
title: "是否确认刷新主题的设置表单?",
description: "此操作仅会刷新主题的设置表单,不会删除已有的配置。",
onConfirm: async () => {

View File

@ -2,7 +2,7 @@
import {
IconAddCircle,
IconGitHub,
useDialog,
Dialog,
VButton,
VEmpty,
VModal,
@ -45,8 +45,6 @@ const themes = ref<Theme[]>([]);
const loading = ref(false);
const themeInstall = ref(false);
const dialog = useDialog();
const handleFetchThemes = async () => {
try {
loading.value = true;
@ -61,7 +59,7 @@ const handleFetchThemes = async () => {
};
const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
dialog.warning({
Dialog.warning({
title: `${
deleteExtensions
? "是否确认删除该主题以及对应的配置?"

View File

@ -2,7 +2,7 @@ import type { ComputedRef, Ref } from "vue";
import { computed, onMounted, ref } from "vue";
import type { Theme } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { useDialog } from "@halo-dev/components";
import { Dialog } from "@halo-dev/components";
interface useThemeLifeCycleReturn {
loading: Ref<boolean>;
@ -21,8 +21,6 @@ export function useThemeLifeCycle(
return activatedTheme.value?.metadata.name === theme.value?.metadata.name;
});
const dialog = useDialog();
const handleFetchActivatedTheme = async () => {
try {
loading.value = true;
@ -54,7 +52,7 @@ export function useThemeLifeCycle(
};
const handleActiveTheme = async () => {
dialog.info({
Dialog.info({
title: "是否确认启用当前主题",
description: theme.value?.spec.displayName,
onConfirm: async () => {

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { useDialog, VModal } from "@halo-dev/components";
import { VModal, Dialog } from "@halo-dev/components";
import FilePondUpload from "@/components/upload/FilePondUpload.vue";
import { apiClient } from "@/utils/api-client";
import type { Plugin } from "@halo-dev/api-client";
@ -20,7 +20,6 @@ const emit = defineEmits<{
(event: "close"): void;
}>();
const dialog = useDialog();
const FilePondUploadRef = ref();
const handleVisibleChange = (visible: boolean) => {
@ -44,7 +43,7 @@ const uploadHandler = computed(() => {
const onUploaded = async (response: AxiosResponse) => {
const plugin = response.data as Plugin;
handleVisibleChange(false);
dialog.success({
Dialog.success({
title: "上传成功",
description: "是否启动当前安装的插件?",
onConfirm: async () => {

View File

@ -3,7 +3,7 @@ import { computed } from "vue";
import type { Plugin } from "@halo-dev/api-client";
import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client";
import { useDialog } from "@halo-dev/components";
import { Dialog } from "@halo-dev/components";
interface usePluginLifeCycleReturn {
isStarted: ComputedRef<boolean | undefined>;
@ -14,8 +14,6 @@ interface usePluginLifeCycleReturn {
export function usePluginLifeCycle(
plugin?: Ref<Plugin | undefined>
): usePluginLifeCycleReturn {
const dialog = useDialog();
const isStarted = computed(() => {
return (
plugin?.value?.status?.phase === "STARTED" && plugin.value?.spec.enabled
@ -27,7 +25,7 @@ export function usePluginLifeCycle(
const pluginToUpdate = cloneDeep(plugin.value);
dialog.info({
Dialog.info({
title: `确定要${pluginToUpdate.spec.enabled ? "停止" : "启动"}该插件吗?`,
onConfirm: async () => {
try {
@ -50,7 +48,7 @@ export function usePluginLifeCycle(
const { enabled } = plugin.value.spec;
dialog.warning({
Dialog.warning({
title: `${
deleteExtensions
? "是否确认卸载该插件以及对应的配置?"

View File

@ -8,7 +8,7 @@ import {
IconAddCircle,
IconArrowDown,
IconShieldUser,
useDialog,
Dialog,
VButton,
VCard,
VPageHeader,
@ -81,9 +81,8 @@ const handleCloneRole = (role: Role) => {
editingModal.value = true;
};
const dialog = useDialog();
const handleDelete = async (role: Role) => {
dialog.warning({
Dialog.warning({
title: "是否确定删除该权限?",
description: "此权限删除之后,相关联的用户将被删除角色绑定,此操作不可恢复",
confirmType: "danger",

View File

@ -13,7 +13,7 @@ import {
VAvatar,
VEntity,
VEntityField,
useDialog,
Dialog,
VStatusDot,
} from "@halo-dev/components";
import UserEditingModal from "./components/UserEditingModal.vue";
@ -30,8 +30,6 @@ import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const dialog = useDialog();
const checkedAll = ref(false);
const editingModal = ref<boolean>(false);
const passwordChangeModal = ref<boolean>(false);
@ -95,7 +93,7 @@ const handlePaginationChange = async ({
};
const handleDelete = async (user: User) => {
dialog.warning({
Dialog.warning({
title: "确定要删除该用户吗?",
description: "该操作不可恢复。",
confirmType: "danger",
@ -114,7 +112,7 @@ const handleDelete = async (user: User) => {
};
const handleDeleteInBatch = async () => {
dialog.warning({
Dialog.warning({
title: "是否确认删除选中的用户?",
confirmType: "danger",
onConfirm: async () => {