261 lines
4.8 KiB
Vue
261 lines
4.8 KiB
Vue
<script lang="ts" setup>
|
||
/**
|
||
* 消息专用场景弹层外壳(非 VbenModal)
|
||
* 遮罩 + 类型色调面板 + 关闭;内容由各场景组件填充
|
||
*/
|
||
import { onMounted, onUnmounted, watch } from 'vue';
|
||
|
||
import { X } from 'lucide-vue-next';
|
||
|
||
import type { NoticeSceneSize, NoticeSceneTone } from './meta';
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
open: boolean;
|
||
title?: string;
|
||
kicker?: string;
|
||
tone?: NoticeSceneTone;
|
||
size?: NoticeSceneSize;
|
||
bareHeader?: boolean;
|
||
}>(),
|
||
{
|
||
title: '消息详情',
|
||
kicker: 'NOTICE',
|
||
tone: 'default',
|
||
size: 'md',
|
||
bareHeader: false,
|
||
},
|
||
);
|
||
|
||
const emit = defineEmits<{
|
||
'update:open': [boolean];
|
||
close: [];
|
||
}>();
|
||
|
||
function close() {
|
||
emit('update:open', false);
|
||
emit('close');
|
||
}
|
||
|
||
function onKeydown(e: KeyboardEvent) {
|
||
if (e.key === 'Escape' && props.open) {
|
||
close();
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.open,
|
||
(v) => {
|
||
document.body.style.overflow = v ? 'hidden' : '';
|
||
},
|
||
);
|
||
|
||
onMounted(() => window.addEventListener('keydown', onKeydown));
|
||
onUnmounted(() => {
|
||
window.removeEventListener('keydown', onKeydown);
|
||
document.body.style.overflow = '';
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Teleport to="body">
|
||
<Transition name="ns">
|
||
<div
|
||
v-if="open"
|
||
class="ns-root"
|
||
role="presentation"
|
||
>
|
||
<div class="ns-mask" @click="close" />
|
||
<div
|
||
class="ns-panel"
|
||
:class="[`tone-${tone}`, `size-${size}`, { bare: bareHeader }]"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
:aria-label="title"
|
||
@click.stop
|
||
>
|
||
<button
|
||
type="button"
|
||
class="ns-close"
|
||
aria-label="关闭"
|
||
@click="close"
|
||
>
|
||
<X class="size-4" />
|
||
</button>
|
||
<header v-if="!bareHeader" class="ns-head">
|
||
<div class="ns-kicker">{{ kicker }}</div>
|
||
<h2 class="ns-title">{{ title }}</h2>
|
||
</header>
|
||
<div class="ns-body">
|
||
<slot />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.ns-root {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1200;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 24px 16px;
|
||
}
|
||
|
||
.ns-mask {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: hsl(0 0% 0% / 0.48);
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
|
||
.ns-panel {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: min(720px, 96vw);
|
||
max-height: min(88vh, 920px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border-radius: 20px;
|
||
border: 1px solid hsl(var(--border));
|
||
background: hsl(var(--card, var(--background)));
|
||
box-shadow:
|
||
0 24px 64px hsl(0 0% 0% / 0.28),
|
||
0 0 0 1px hsl(var(--foreground) / 0.03);
|
||
}
|
||
|
||
.ns-panel.size-lg {
|
||
width: min(860px, 96vw);
|
||
}
|
||
|
||
.ns-panel.size-xl {
|
||
width: min(960px, 96vw);
|
||
}
|
||
|
||
.ns-panel.tone-vip {
|
||
border-color: hsl(160 50% 40% / 0.35);
|
||
box-shadow:
|
||
0 24px 64px hsl(160 40% 12% / 0.45),
|
||
0 0 40px hsl(160 60% 40% / 0.12);
|
||
}
|
||
|
||
.ns-panel.tone-danger {
|
||
border-color: hsl(0 72% 50% / 0.35);
|
||
box-shadow:
|
||
0 24px 64px hsl(0 40% 10% / 0.4),
|
||
0 0 36px hsl(0 72% 50% / 0.12);
|
||
}
|
||
|
||
.ns-panel.tone-money {
|
||
border-color: hsl(var(--primary) / 0.35);
|
||
}
|
||
|
||
.ns-panel.tone-report {
|
||
border-color: hsl(160 60% 40% / 0.3);
|
||
}
|
||
|
||
.ns-panel.tone-invoice {
|
||
border-color: hsl(250 60% 55% / 0.3);
|
||
}
|
||
|
||
.ns-panel.tone-audit {
|
||
border-color: hsl(190 70% 42% / 0.28);
|
||
}
|
||
|
||
.ns-close {
|
||
position: absolute;
|
||
top: 14px;
|
||
right: 14px;
|
||
z-index: 3;
|
||
display: grid;
|
||
place-items: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
color: hsl(var(--muted-foreground));
|
||
background: hsl(var(--muted) / 0.55);
|
||
transition:
|
||
background 0.15s ease,
|
||
color 0.15s ease,
|
||
transform 0.15s ease;
|
||
}
|
||
|
||
.ns-close:hover {
|
||
color: hsl(var(--foreground));
|
||
background: hsl(var(--muted));
|
||
transform: scale(1.04);
|
||
}
|
||
|
||
.ns-head {
|
||
flex-shrink: 0;
|
||
padding: 20px 48px 0 22px;
|
||
}
|
||
|
||
.ns-kicker {
|
||
font-size: 11px;
|
||
font-weight: 750;
|
||
letter-spacing: 0.12em;
|
||
color: hsl(var(--primary));
|
||
}
|
||
|
||
.tone-danger .ns-kicker {
|
||
color: hsl(0 72% 52%);
|
||
}
|
||
|
||
.tone-vip .ns-kicker {
|
||
color: hsl(160 55% 42%);
|
||
}
|
||
|
||
.tone-invoice .ns-kicker {
|
||
color: hsl(250 60% 55%);
|
||
}
|
||
|
||
.ns-title {
|
||
margin: 6px 0 0;
|
||
font-size: 18px;
|
||
font-weight: 720;
|
||
letter-spacing: -0.02em;
|
||
color: hsl(var(--foreground));
|
||
}
|
||
|
||
.ns-body {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 16px 22px 22px;
|
||
}
|
||
|
||
.ns-panel.bare .ns-body {
|
||
padding-top: 18px;
|
||
}
|
||
|
||
/* enter / leave */
|
||
.ns-enter-active,
|
||
.ns-leave-active {
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
|
||
.ns-enter-active .ns-panel,
|
||
.ns-leave-active .ns-panel {
|
||
transition:
|
||
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
|
||
opacity 0.22s ease;
|
||
}
|
||
|
||
.ns-enter-from,
|
||
.ns-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.ns-enter-from .ns-panel,
|
||
.ns-leave-to .ns-panel {
|
||
opacity: 0;
|
||
transform: translateY(12px) scale(0.98);
|
||
}
|
||
</style>
|