Zzc 5134cf819f feat(screen): 添加应急预案内容组件和可伸缩模态框
- 添加 `EmergencyPlanContent.vue`,用于显示包含表单、步骤和资源分配的详细应急响应计划
- 添加 `StretchableModal.vue`,用于实现可定制、可调整大小的模态对话框,使用分段背景图
- 更新 `LeftPanel.vue` 以处理 view-plan 事件,整合计划查看功能
2025-11-21 16:57:48 +08:00

309 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<Teleport to="body">
<Transition name="popup-fade">
<div
v-if="visible"
class="stretchable-modal__overlay"
@click="handleOverlayClick"
>
<div
class="stretchable-modal__container"
:style="containerStyle"
@click.stop
>
<!-- 头部大弹窗top切图.png -->
<div class="stretchable-modal__header">
<slot name="header">
<h3 class="stretchable-modal__title">{{ title }}</h3>
</slot>
<button
v-if="showClose"
class="stretchable-modal__close"
@click="handleClose"
aria-label="关闭弹窗"
>
<img
src="../../assets/images/modal/closeIcon.png"
alt="关闭"
class="close-icon"
/>
</button>
</div>
<!-- 中间大弹窗mid切图.png -->
<div class="stretchable-modal__body">
<slot></slot>
</div>
<!-- 底部大弹窗bottom切图.png -->
<div v-if="$slots.footer" class="stretchable-modal__footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { computed, watch, onMounted, onUnmounted } from 'vue';
/**
* StretchableModal - 三段式可拉伸弹窗组件
*
* 特性:
* - 使用三张切图实现可拉伸的弹窗背景(头部、中间、底部)
* - 头部和底部仅支持横向拉伸,中间部分支持全方向拉伸
* - 标题居中显示
* - 支持多种交互方式关闭点击遮罩、ESC键、关闭按钮
* - 支持插槽自定义内容
*/
const props = defineProps({
/**
* 控制弹窗显示/隐藏
*/
visible: {
type: Boolean,
default: false
},
/**
* 弹窗标题
*/
title: {
type: String,
default: ''
},
/**
* 弹窗宽度,支持 CSS 单位或响应式值
* @example '800px' | 'clamp(778px, 90vw, 1200px)'
*/
width: {
type: String,
default: 'clamp(900px, 85vw, 1400px)'
},
/**
* 弹窗高度,支持 CSS 单位或响应式值
* @example '600px' | 'clamp(500px, 80vh, 800px)'
*/
height: {
type: String,
default: 'clamp(600px, 85vh, 900px)'
},
/**
* 点击遮罩层是否关闭弹窗
*/
closeOnClickModal: {
type: Boolean,
default: true
},
/**
* 按 ESC 键是否关闭弹窗
*/
closeOnPressEscape: {
type: Boolean,
default: true
},
/**
* 是否显示关闭按钮
*/
showClose: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:visible', 'close', 'open']);
/**
* 计算弹窗容器的样式
*/
const containerStyle = computed(() => ({
width: props.width,
height: props.height
}));
/**
* 处理遮罩层点击事件
*/
const handleOverlayClick = () => {
if (props.closeOnClickModal) {
handleClose();
}
};
/**
* 关闭弹窗
*/
const handleClose = () => {
emit('update:visible', false);
emit('close');
};
/**
* 处理 ESC 键按下事件
*/
const handleKeydown = (event) => {
if (props.visible && props.closeOnPressEscape && event.key === 'Escape') {
handleClose();
}
};
// 监听 visible 变化,触发相应事件
watch(() => props.visible, (newVal) => {
if (newVal) {
emit('open');
}
});
// 挂载和卸载时添加/移除键盘事件监听
onMounted(() => {
document.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
});
</script>
<style scoped lang="scss">
@use "@/styles/mixins.scss" as *;
.stretchable-modal {
&__overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(vw(5));
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: vw(20);
}
&__container {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
&__header {
position: relative;
height: vh(66);
min-height: vh(66);
background: url(../../assets/images/modal/大弹窗top切图.png) repeat-x;
background-size: 100% vh(66);
display: flex;
align-items: center;
justify-content: center;
padding: 0 vw(60);
}
&__title {
color: var(--text-white);
font-size: fs(20);
font-family: SourceHanSansCN-Bold, sans-serif;
font-weight: bold;
text-align: center;
margin: 0;
}
&__close {
position: absolute;
top: 34px;
right: 34px;
transform: translateY(-50%);
width: vw(32);
height: vw(32);
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
z-index: 10;
padding: 0;
&:hover {
opacity: 0.8;
transform: translateY(-50%) scale(1.1);
}
&:active {
transform: translateY(-50%) scale(0.95);
}
.close-icon {
width: 100%;
height: 100%;
object-fit: contain;
}
}
&__body {
flex: 1;
background: url(../../assets/images/modal/大弹窗mid切图.png) repeat;
background-size: 100% vh(66);
overflow-y: auto;
overflow-x: hidden;
padding: vw(20) vw(30);
// 自定义滚动条样式
&::-webkit-scrollbar {
width: vw(6);
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: vw(3);
}
&::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: vw(3);
&:hover {
background: var(--primary-light);
}
}
}
&__footer {
height: vh(66);
min-height: vh(66);
background: url(../../assets/images/modal/大弹窗bottom切图.png) repeat-x;
background-size: 100% vh(66);
display: flex;
align-items: center;
justify-content: center;
padding: 0 vw(30);
}
}
// 弹窗淡入淡出动画
.popup-fade-enter-active,
.popup-fade-leave-active {
transition: opacity 0.3s ease;
.stretchable-modal__container {
transition: transform 0.3s ease, opacity 0.3s ease;
}
}
.popup-fade-enter-from,
.popup-fade-leave-to {
opacity: 0;
.stretchable-modal__container {
transform: scale(0.9);
opacity: 0;
}
}
</style>