160 lines
3.8 KiB
Vue
160 lines
3.8 KiB
Vue
<script setup>
|
|
import MessageItem from './message.vue'
|
|
import { messageState } from './message-store.js'
|
|
|
|
/* 9 个方位:分组容器始终存在,仅消息项进出,保证 enter/leave 动画稳定触发 */
|
|
const PLACEMENTS = [
|
|
'top-left',
|
|
'top-center',
|
|
'top-right',
|
|
'center-left',
|
|
'center',
|
|
'center-right',
|
|
'bottom-left',
|
|
'bottom-center',
|
|
'bottom-right',
|
|
]
|
|
|
|
/* ---------- 关闭某条 ---------- */
|
|
function close(id) {
|
|
const idx = messageState.items.findIndex((i) => i.id === id)
|
|
if (idx !== -1) messageState.items.splice(idx, 1)
|
|
}
|
|
|
|
/* ---------- 方位 → flex 对齐类 ---------- */
|
|
function placementClasses(p) {
|
|
const v = p.split('-')[0]
|
|
const h = p.split('-')[1] || 'center'
|
|
return [
|
|
`fms-placement-${v === 'center' ? 'center-v' : v}`,
|
|
`fms-placement-${h === 'center' ? 'center-h' : h}`,
|
|
]
|
|
}
|
|
|
|
/* 某个方位下当前存活的消息 */
|
|
function itemsOf(p) {
|
|
return messageState.items.filter(
|
|
(i) => (i.placement || messageState.placement || 'top-center') === p,
|
|
)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<div class="fms-message-container" aria-live="polite">
|
|
<TransitionGroup
|
|
v-for="p in PLACEMENTS"
|
|
:key="p"
|
|
name="fms-message"
|
|
tag="div"
|
|
:class="['fms-message-list', ...placementClasses(p)]"
|
|
>
|
|
<MessageItem
|
|
v-for="item in itemsOf(p)"
|
|
:key="item.id"
|
|
:id="item.id"
|
|
:type="item.type"
|
|
:content="item.content"
|
|
:duration="item.duration"
|
|
:closable="item.closable"
|
|
@close="close(item.id)"
|
|
/>
|
|
</TransitionGroup>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.fms-message-container {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: var(--fms-z-message);
|
|
/* 容器本身不拦截页面交互,仅子项可交互 */
|
|
pointer-events: none;
|
|
}
|
|
|
|
.fms-message-list {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding: var(--fms-feedback-offset);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* 垂直方位(主轴):上 / 中 / 下 */
|
|
.fms-placement-top {
|
|
justify-content: flex-start;
|
|
}
|
|
.fms-placement-center-v {
|
|
justify-content: center;
|
|
}
|
|
.fms-placement-bottom {
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
/* 水平方位(交叉轴):左 / 中 / 右 */
|
|
.fms-placement-left {
|
|
align-items: flex-start;
|
|
}
|
|
.fms-placement-center-h {
|
|
align-items: center;
|
|
}
|
|
.fms-placement-right {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
/* 入场/退场:基础淡入淡出 + 位移,方向随方位 */
|
|
.fms-message-enter-active,
|
|
.fms-message-leave-active {
|
|
transition:
|
|
opacity 0.28s ease,
|
|
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
.fms-message-enter-from,
|
|
.fms-message-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* 左侧锚定:从左侧滑入、向左侧滑出 */
|
|
.fms-placement-left .fms-message-enter-from,
|
|
.fms-placement-left .fms-message-leave-to {
|
|
transform: translateX(-24px);
|
|
}
|
|
/* 右侧锚定:从右侧滑入、向右侧滑出 */
|
|
.fms-placement-right .fms-message-enter-from,
|
|
.fms-placement-right .fms-message-leave-to {
|
|
transform: translateX(24px);
|
|
}
|
|
/* top-center:从顶部滑入/滑出 */
|
|
.fms-placement-top.fms-placement-center-h .fms-message-enter-from,
|
|
.fms-placement-top.fms-placement-center-h .fms-message-leave-to {
|
|
transform: translateY(-16px);
|
|
}
|
|
/* bottom-center:从底部滑入/滑出 */
|
|
.fms-placement-bottom.fms-placement-center-h .fms-message-enter-from,
|
|
.fms-placement-bottom.fms-placement-center-h .fms-message-leave-to {
|
|
transform: translateY(16px);
|
|
}
|
|
/* center 正中央:仅淡入淡出 */
|
|
|
|
/* 退场时脱离文档流,便于剩余项平滑上移 */
|
|
.fms-message-leave-active {
|
|
position: absolute;
|
|
}
|
|
|
|
.fms-message-move {
|
|
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.fms-message-enter-active,
|
|
.fms-message-leave-active,
|
|
.fms-message-move {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|