Files
workspace/code/fms/fms-vue/src/components/ui/message/message-container.vue
T
2026-08-17 22:52:27 +08:00

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>