feat: add dialog component

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/588/head
Ryan Wang 2022-06-29 14:53:57 +08:00
parent f825047eb5
commit cd338f0b1f
14 changed files with 274 additions and 14 deletions

View File

@ -45,10 +45,10 @@
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/packages/components#readme",
"license": "MIT",
"devDependencies": {
"@iconify-json/ri": "^1.1.2",
"@iconify-json/ri": "^1.1.3",
"@rollup/plugin-typescript": "^8.3.3",
"histoire": "^0.7.8",
"unplugin-icons": "^0.14.5",
"unplugin-icons": "^0.14.6",
"vite-plugin-dts": "^1.2.0"
},
"peerDependencies": {

View File

@ -13,3 +13,4 @@ export * from "./components/tabs";
export * from "./components/tag";
export * from "./components/textarea";
export * from "./components/switch";
export * from "./components/dialog";

View File

@ -0,0 +1,36 @@
<script lang="ts" setup>
import { VButton } from "@/components/button";
import { VDialog } from "@/components/dialog";
const initState = () => {
return {
visible: false,
title: "提示",
description: "是否确定删除这篇文章?",
confirmText: "确定",
cancelText: "取消",
type: "info",
onConfirm: () => {
alert("已删除");
},
onCancel: () => {
alert("已取消");
},
};
};
</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>
</Story>
</template>

View File

@ -0,0 +1,128 @@
<script lang="ts" setup>
import { VModal } from "@/components/modal";
import { VSpace } from "@/components/space";
import { VButton } from "@/components/button";
import {
IconCheckboxCircle,
IconClose,
IconErrorWarning,
IconForbidLine,
IconInformation,
} from "@/icons/icons";
import type { PropType } from "vue";
import { computed, ref } from "vue";
import type { Type } from "@/components/dialog/interface";
const props = defineProps({
type: {
type: String as PropType<Type>,
default: "info",
},
title: {
type: String,
default: "提示",
},
description: {
type: String,
default: "",
},
confirmText: {
type: String,
default: "确定",
},
cancelText: {
type: String,
default: "取消",
},
visible: {
type: Boolean,
default: false,
},
onConfirm: {
type: Function as PropType<() => void>,
},
onCancel: {
type: Function as PropType<() => void>,
},
});
const emit = defineEmits(["update:visible", "close"]);
const icons = {
success: {
icon: IconCheckboxCircle,
color: "green",
},
info: {
icon: IconInformation,
color: "blue",
},
warning: {
icon: IconErrorWarning,
color: "orange",
},
error: {
icon: IconForbidLine,
color: "red",
},
};
const icon = computed(() => icons[props.type]);
const loading = ref(false);
const handleCancel = () => {
if (props.onCancel) {
props.onCancel();
}
handleClose();
};
const handleConfirm = async () => {
if (props.onConfirm) {
loading.value = true;
await props.onConfirm();
}
handleClose();
};
const handleClose = () => {
loading.value = false;
emit("update:visible", false);
emit("close");
};
</script>
<template>
<VModal :visible="visible" :width="450" @close="handleCancel()">
<div class="flex justify-between items-start py-2 mb-2">
<div>
<div
:class="`ring-${icon.color}-100`"
class="inline-flex rounded-full bg-teal-50 p-1.5 ring-4"
>
<component
:is="icon.icon"
:class="`text-${icon.color}-500`"
class="w-5 h-5"
></component>
</div>
</div>
<div>
<IconClose class="cursor-pointer" @click="handleCancel" />
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex-1 flex flex-col items-stretch gap-2">
<div class="text-base text-gray-900 font-bold">{{ title }}</div>
<div class="text-sm text-gray-700">{{ description }}</div>
</div>
</div>
<template #footer>
<VSpace>
<VButton :loading="loading" type="secondary" @click="handleConfirm">
{{ confirmText }}
</VButton>
<VButton @click="handleCancel">{{ cancelText }}</VButton>
</VSpace>
</template>
</VModal>
</template>

View File

@ -0,0 +1,26 @@
<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"
:description="options.description"
:onCancel="options.onCancel"
:onConfirm="options.onConfirm"
:title="options.title"
:type="options.type"
></VDialog>
</template>

View File

@ -0,0 +1,9 @@
import { describe, expect, it } from "vitest";
import { mount } from "@vue/test-utils";
import { VDialog } from "../index";
describe("Dialog", () => {
it("should render", () => {
expect(mount(VDialog)).toBeDefined();
});
});

View File

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

View File

@ -0,0 +1,15 @@
export type Type = "success" | "info" | "warning" | "error";
export const DialogProviderProvideKey = "DIALOG_PROVIDER_PROVIDE_KEY";
export interface useDialogOptions {
type?: Type;
visible: boolean;
title: string;
description?: string;
confirmText?: string;
cancelText?: string;
onConfirm?: () => void;
onCancel?: () => void;
}
export type useDialogUserOptions = Omit<useDialogOptions, "type" | "visible">;

View File

@ -0,0 +1,36 @@
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) => {
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

@ -46,7 +46,7 @@ function handleClose() {
}
</script>
<template>
<Teleport to="body">
<Teleport to="body" :disabled="true">
<div
v-show="rootVisible"
:class="wrapperClasses"
@ -81,7 +81,7 @@ function handleClose() {
:style="contentStyles"
class="modal-content transform transition-all"
>
<div class="modal-header">
<div v-if="$slots.header || title" class="modal-header">
<slot name="header">
<div class="modal-header-title">{{ title }}</div>
<div class="modal-header-actions flex flex-row">

View File

@ -79,6 +79,8 @@ import IconShieldUser from "~icons/ri/shield-user-line";
import IconGitBranch from "~icons/ri/git-branch-line";
// @ts-ignore
import IconStopCircle from "~icons/ri/stop-circle-line";
// @ts-ignore
import IconForbidLine from "~icons/ri/forbid-line";
export {
IconDashboard,
@ -121,4 +123,5 @@ export {
IconShieldUser,
IconGitBranch,
IconStopCircle,
IconForbidLine,
};

View File

@ -1,7 +1,7 @@
import axios from "axios";
const token =
"eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJIYWxvIE93bmVyIiwic3ViIjoiYWRtaW4iLCJleHAiOjE2NTY0NzM3NTMsImlhdCI6MTY1NjM4NzM1Mywic2NvcGUiOlsiUk9MRV9zdXBlci1yb2xlIl19.vJ7l6jo3CJv7h4XTDvqjY70qngpaiiYhJL7_vXNPRY2Rz2NdmosMzy-BIRYPRuJnhU33uQ5LjXY5K2YwyQinrIsT66JsfckuYi6slAQY2rUC3929wC3gBcMJp9Z--VGRA701vDoecGWnGh68XR-RCK_uGcMnNhC4A1bCsb-nrn4TYdUndM0Aa2OsGP6G0IjzuyvXwwoAGB2JojBdGzXVz2KujHsaELziAZ2Kx78wsEIREN0pZGXnapB1-0nqgjLQ9VuGl62bRAkyzirwbasgB6Tk8njz-TR_uIL-smyWt_LUwX5I97lrM5VCtMmBlT999_Zi8MgzWTHeEUobzbI4ng";
"eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJIYWxvIE93bmVyIiwic3ViIjoiYWRtaW4iLCJleHAiOjE2NTY1NjAzMjQsImlhdCI6MTY1NjQ3MzkyNCwic2NvcGUiOlsiUk9MRV9zdXBlci1yb2xlIl19.CloSZG47VmzUd7uLUHb1q-0mmu0SE3zoRVHVAFeN4P9izXc-Sy2ZZc_3QKQPb4HyfWV1HoFDr2GeOKudbkAJIBvvJ1rr-NLh1szj0le06aeK1U2RqANgR0oI4HxSWrPu9Mnz-r04L1D-KrrmnG7votQ6ekeTPwlKIyEI6zl-0NlCqGdv0g3FV37dDbu8-9QrgtTOvRcpymLYGJ-vY2CqGm1z7vNbgMSneIhbRxe6lXfwkbvjtCp3P3Mkj5_14cAt_FQEUHGYnZZH_dNtdU2NZBpMXIgZYE0CaTBes4ciH1N62cwIT05iBTN_tdmDwEsg-BYjvI5IqVmrt2CwT_IcBg"
const axiosInstance = axios.create({
headers: {
Authorization: `Bearer ${token}`,

View File

@ -111,16 +111,16 @@ importers:
packages/components:
specifiers:
'@iconify-json/ri': ^1.1.2
'@iconify-json/ri': ^1.1.3
'@rollup/plugin-typescript': ^8.3.3
histoire: ^0.7.8
unplugin-icons: ^0.14.5
unplugin-icons: ^0.14.6
vite-plugin-dts: ^1.2.0
devDependencies:
'@iconify-json/ri': 1.1.2
'@iconify-json/ri': 1.1.3
'@rollup/plugin-typescript': 8.3.3
histoire: 0.7.8
unplugin-icons: 0.14.5
unplugin-icons: 0.14.6
vite-plugin-dts: 1.2.0
packages/shared:
@ -1532,8 +1532,8 @@ packages:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
/@iconify-json/ri/1.1.2:
resolution: {integrity: sha512-qLlDrpkxJi+aUxY2xipKC08aI2seaxFsrEWC3Wm5WJxTtZco+G8SUBpzNpQaKzjN4IwpzcZv8T37Q3aREXDk/A==}
/@iconify-json/ri/1.1.3:
resolution: {integrity: sha512-YQ45kQNpuHc2bso4fDGhooWou43qy7njD/I5l7vpjcujb+P/K2BfLASbWYTTUKu6lMersuFmO8F7NdGzy6eGWw==}
dependencies:
'@iconify/types': 1.1.0
dev: true
@ -6640,8 +6640,8 @@ packages:
engines: {node: '>= 0.8'}
dev: true
/unplugin-icons/0.14.5:
resolution: {integrity: sha512-fxi/fuBZXtZu64L8iAPj+ecu/rnSvTbfR14RO44xIWdsI/Ohpzs9Gve7+nHIgD6JFrdtCfzGnXWBEVPbMGWX3A==}
/unplugin-icons/0.14.6:
resolution: {integrity: sha512-8sxDiL4l+TV4zufZfrskgHZZSDFoGOCBgYsefRMM4inQ3Z6KhgMSuNyew7U7D/xG//rwxgD7bN+Dv+YAZEEfEw==}
peerDependencies:
'@svgr/core': '>=5.5.0'
'@vue/compiler-sfc': ^3.0.2

View File

@ -1,9 +1,12 @@
<script lang="ts" setup>
import { RouterView } from "vue-router";
import { VDialogProvider } from "@halo-dev/components";
</script>
<template>
<RouterView />
<VDialogProvider>
<RouterView />
</VDialogProvider>
</template>
<style lang="scss">