feat: add modal component

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/3445/head
Ryan Wang 2022-04-26 21:52:56 +08:00
parent 3853d55fd1
commit c0e6267ccc
6 changed files with 212 additions and 1 deletions

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { VButton } from "@/components/base/button";
import { VModal } from "@/components/base/modal";
function initState() {
return {
visible: false,
};
}
</script>
<template>
<Story title="Modal" :init-state="initState">
<template #default="{ state }">
<VButton type="secondary" @click="state.visible = true">打开</VButton>
<VModal v-model:visible="state.visible" title="测试">
Hello World
<VButton type="secondary" @click="state.visible = false">关闭</VButton>
</VModal>
</template>
</Story>
</template>

View File

@ -0,0 +1,161 @@
<script lang="ts" setup>
import { VButton } from "../button";
import { computed } from "vue";
import { IconClose } from "@/core/icons";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
},
width: {
type: Number,
default: 500,
},
fullscreen: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:visible", "close"]);
const wrapperClasses = computed(() => {
return {
"modal-wrapper-fullscreen": props.fullscreen,
};
});
const contentStyles = computed(() => {
return {
maxWidth: props.width + "px",
};
});
function handleClose() {
emit("update:visible", false);
emit("close");
}
</script>
<template>
<transition
enter-active-class="ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-show="visible"
:class="wrapperClasses"
aria-modal="true"
class="modal-wrapper transform transition-all duration-200"
role="dialog"
tabindex="0"
@keyup.esc="handleClose()"
>
<div class="modal-layer" @click="handleClose()" />
<div :style="contentStyles" class="modal-content">
<div class="modal-header">
<div class="modal-header-title">{{ title }}</div>
<div class="modal-header-actions">
<div class="modal-header-action" @click="handleClose()">
<IconClose />
</div>
</div>
</div>
<div class="modal-body">
<slot />
</div>
<div class="modal-footer">
<slot name="footer">
<VButton @click="handleClose"></VButton>
</slot>
</div>
</div>
</div>
</transition>
</template>
<style lang="scss">
.modal-wrapper {
@apply fixed;
@apply top-0 left-0;
@apply w-full h-full;
@apply flex flex-row;
@apply items-center justify-center;
z-index: 99999;
.modal-layer {
@apply flex-none;
@apply absolute;
@apply top-0 left-0;
@apply w-full h-full;
background: #9e9eaa;
opacity: 0.6;
}
.modal-content {
@apply flex;
@apply flex-col;
@apply relative;
@apply bg-white;
@apply items-stretch;
@apply shadow-xl;
width: calc(100vw - 20px);
max-height: calc(100vh - 20px);
border-radius: 4px;
.modal-header {
@apply flex;
@apply justify-between;
@apply border-b;
.modal-header-title {
@apply self-center;
@apply text-base;
@apply font-bold;
padding: 12px 16px;
}
.modal-header-actions {
@apply self-center;
@apply h-full;
.modal-header-action {
@apply cursor-pointer;
padding: 12px 16px;
&:hover {
@apply bg-gray-100;
}
}
}
}
.modal-body {
@apply overflow-x-hidden overflow-y-auto;
word-wrap: break-word;
padding: 12px 16px;
}
.modal-footer {
@apply border-t;
padding: 12px 16px;
}
}
&.modal-wrapper-fullscreen {
.modal-content {
width: 100vw !important;
max-width: 100vw !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0;
}
}
}
</style>

View File

@ -0,0 +1,8 @@
import { describe, expect, it } from "vitest";
import { VModal } from "../index";
describe("Modal", () => {
it("should render", () => {
expect(VModal).toBeDefined();
});
});

View File

@ -0,0 +1 @@
export { default as VModal } from "./Modal.vue";

View File

@ -26,6 +26,9 @@ import IconEye from "~icons/ri/eye-line";
import IconFolder from "~icons/ri/folder-2-line";
// @ts-ignore
import IconMore from "~icons/ri/more-line";
// @ts-ignore
import IconClose from "~icons/ri/close-line";
export {
IconDashboard,
IconArrowRight,
@ -41,4 +44,5 @@ export {
IconEye,
IconFolder,
IconMore,
IconClose,
};

View File

@ -1,6 +1,22 @@
<template>
<FilledLayout> </FilledLayout>
<FilledLayout>
<VModal v-model:visible="visible" title="登录">
<div class="flex justify-between">
<div>
<VButton type="secondary" @click="visible = false">关闭</VButton>
</div>
<div>
<VButton type="secondary" @click="visible = true">打开</VButton>
</div>
</div>
</VModal>
<VButton type="secondary" @click="visible = true">打开</VButton>
</FilledLayout>
</template>
<script lang="ts" setup>
import { FilledLayout } from "@/layouts";
import { VModal } from "@/components/base/modal";
import { VButton } from "@/components/base/button";
import { ref } from "vue";
const visible = ref(false);
</script>