- 添加 `EmergencyPlanContent.vue`,用于显示包含表单、步骤和资源分配的详细应急响应计划 - 添加 `StretchableModal.vue`,用于实现可定制、可调整大小的模态对话框,使用分段背景图 - 更新 `LeftPanel.vue` 以处理 view-plan 事件,整合计划查看功能
309 lines
6.3 KiB
Vue
309 lines
6.3 KiB
Vue
<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>
|