feat(screen): 添加应急预案内容组件和可伸缩模态框

- 添加 `EmergencyPlanContent.vue`,用于显示包含表单、步骤和资源分配的详细应急响应计划
- 添加 `StretchableModal.vue`,用于实现可定制、可调整大小的模态对话框,使用分段背景图
- 更新 `LeftPanel.vue` 以处理 view-plan 事件,整合计划查看功能
This commit is contained in:
Zzc 2025-11-21 16:57:48 +08:00
parent fd260aa453
commit 5134cf819f
3 changed files with 1105 additions and 2 deletions

View File

@ -18,7 +18,10 @@
</CollapsiblePanel>
<CollapsiblePanel title="快速响应" subtitle="「力量调度」">
<ForceDispatch @start-dispatch="handleStartDispatch" />
<ForceDispatch
@start-dispatch="handleStartDispatch"
@view-plan="handleViewPlan"
/>
</CollapsiblePanel>
</div>
@ -107,7 +110,7 @@ const handleCloseVideoModal = () => {
}
//
const emit = defineEmits(['start-dispatch'])
const emit = defineEmits(['start-dispatch', 'view-plan'])
/**
* 处理力量调度启动事件向上传递给父组件
@ -115,6 +118,13 @@ const emit = defineEmits(['start-dispatch'])
const handleStartDispatch = (payload) => {
emit('start-dispatch', payload)
}
/**
* 处理查看智能应急方案事件向上传递给父组件
*/
const handleViewPlan = (plan) => {
emit('view-plan', plan)
}
</script>
<style scoped lang="scss">

View File

@ -0,0 +1,785 @@
<template>
<div class="emergency-plan-content">
<!-- 1. 现场指挥部 -->
<section class="plan-section">
<div class="section-header">
<img
src="../../assets/images/modal/弹窗title.png"
alt=""
class="title-icon"
/>
<h3 class="section-title">现场指挥部</h3>
</div>
<div class="section-body">
<div class="form-grid">
<div class="form-item">
<label>指挥长</label>
<el-select
v-model="formData.commander"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option label="王军" value="王军" />
<el-option label="李明" value="李明" />
<el-option label="张伟" value="张伟" />
</el-select>
</div>
<div class="form-item">
<label>副指挥长</label>
<el-select
v-model="formData.viceCommander"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option label="刘勇" value="刘勇" />
<el-option label="陈强" value="陈强" />
<el-option label="赵军" value="赵军" />
</el-select>
</div>
</div>
</div>
</section>
<!-- 2. 多路协同处置三级指挥中心 -->
<section class="plan-section">
<div class="section-header">
<img
src="../../assets/images/modal/弹窗title.png"
alt=""
class="title-icon"
/>
<h3 class="section-title">多路协同处置三级指挥中心</h3>
</div>
<div class="section-body">
<div class="form-grid grid-2x2">
<div class="form-item">
<label>交通管控</label>
<el-select
v-model="formData.trafficControl"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option label="公安交警、交通执法队" value="公安交警、交通执法队" />
<el-option label="交通管理局" value="交通管理局" />
</el-select>
</div>
<div class="form-item">
<label>交通信息发布</label>
<el-select
v-model="formData.infoRelease"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option label="融媒体中心" value="融媒体中心" />
<el-option label="新闻中心" value="新闻中心" />
</el-select>
</div>
<div class="form-item">
<label>人车调拨</label>
<el-select
v-model="formData.vehicleDispatch"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option label="xxx消防队" value="xxx消防队" />
<el-option label="应急救援队" value="应急救援队" />
</el-select>
</div>
<div class="form-item">
<label>人员救援</label>
<el-select
v-model="formData.personnelRescue"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option label="xx医院" value="xx医院" />
<el-option label="中心医院" value="中心医院" />
</el-select>
</div>
</div>
</div>
</section>
<!-- 3. 公路抢通方案 -->
<section class="plan-section">
<div class="section-header">
<img
src="../../assets/images/modal/弹窗title.png"
alt=""
class="title-icon"
/>
<h3 class="section-title">公路抢通方案</h3>
</div>
<div class="section-body">
<div class="form-grid grid-2-cols">
<div class="form-item">
<label>抢通方式</label>
<el-select
v-model="formData.clearanceMethod"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option
label="两端对接抢通,必要时开辟工作面"
value="两端对接抢通,必要时开辟工作面"
/>
<el-option label="单端推进抢通" value="单端推进抢通" />
<el-option label="多点同时作业" value="多点同时作业" />
</el-select>
</div>
<div class="form-item">
<label>预计抢通时间</label>
<el-select
v-model="formData.estimatedTime"
class="custom-select"
popper-class="custom-dropdown"
>
<el-option label="6小时" value="6小时" />
<el-option label="8小时" value="8小时" />
<el-option label="12小时" value="12小时" />
<el-option label="24小时" value="24小时" />
</el-select>
</div>
</div>
<div class="plan-steps">
<div v-for="step in clearanceSteps" :key="step.id" class="step-item">
<label class="step-label">{{ step.number }}{{ step.title }}</label>
<div class="step-content">{{ step.content }}</div>
</div>
</div>
</div>
</section>
<!-- 4. 力量调派方案 -->
<section class="plan-section">
<div class="section-header">
<img
src="../../assets/images/modal/弹窗title.png"
alt=""
class="title-icon"
/>
<h3 class="section-title">力量调派方案</h3>
<button class="add-btn" @click="handleAddPlan">新增</button>
</div>
<div class="section-body">
<div class="dispatch-plans">
<div
v-for="plan in dispatchPlans"
:key="plan.id"
class="dispatch-card"
>
<h4 class="plan-name">
<span class="plan-icon"></span>
{{ plan.name }}
</h4>
<div class="resource-grid">
<div
v-for="(resource, index) in plan.resources"
:key="index"
class="resource-item"
>
<span class="resource-label">{{ resource.label }}</span>
<span class="resource-value">{{ resource.value }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 5. 后续处治 -->
<section class="plan-section">
<div class="section-header">
<img
src="../../assets/images/modal/弹窗title.png"
alt=""
class="title-icon"
/>
<h3 class="section-title">后续处治</h3>
</div>
<div class="section-body">
<p class="follow-up-text">{{ followUpText }}</p>
</div>
</section>
</div>
</template>
<script setup>
import { reactive } from "vue";
// Element Plus
// import { ElSelect, ElOption } from 'element-plus'
/**
* 表单数据
*/
const formData = reactive({
commander: "王军",
viceCommander: "刘勇",
trafficControl: "公安交警、交通执法队",
infoRelease: "融媒体中心",
vehicleDispatch: "xxx消防队",
personnelRescue: "xx医院",
clearanceMethod: "两端对接抢通,必要时开辟工作面",
estimatedTime: "6小时",
});
/**
* 公路抢通方案步骤
*/
const clearanceSteps = [
{
id: 1,
number: "①",
title: "评估封控",
content:
"对现场进行封控,清点各类应急物资提前到达现场,划定作业区与危险区,设立观察哨,全程监控边坡状况。",
},
{
id: 2,
number: "②",
title: "边坡排险",
content:
"在确保安全的前提下,使用长臂挖掘机或人工在安全地域进行清除工作。注意下方施救作业。",
},
{
id: 3,
number: "③",
title: "梯形清方",
content:
'按照机械自上而下,装载机抢先挖入土石堆入车,确土车上车主生,增土车队运输,将受阻路段疏通清理通道。确保抢险不停工,形成高效的"挖一装一运"循环。',
},
{
id: 4,
number: "④",
title: "开辟工作面",
content:
"根据地质情况而规划车,利用挖掘机、装载机完成设备增加的工作面。增加作业效率。",
},
{
id: 5,
number: "⑤",
title: "交通疏导",
content:
"清理出足够疏散车辆的临时性通道,临时道路根据道路情况设置限量所制路牌。在应急抢险下,试行错位通车。",
},
{
id: 6,
number: "⑥",
title: "全断面抢通",
content: "将剩余集聚废弃物的清理清除,恢复道路原貌状。",
},
];
/**
* 力量调派预案
*/
const dispatchPlans = [
{
id: 1,
name: "基地1xxxxx名称",
resources: [
{ label: "装载机挖掘机", value: "1台" },
{ label: "平板拖车", value: "1台" },
{ label: "自卸货车", value: "4台" },
{ label: "工程车", value: "1台" },
{ label: "人员", value: "15人" },
{ label: "标志标牌", value: "5块" },
{ label: "铁锹", value: "30件" },
{ label: "铁镐", value: "10件" },
{ label: "麻袋、砂石袋等", value: "若干" },
],
},
{
id: 2,
name: "基地2xxxxx名称",
resources: [
{ label: "装载机挖掘机", value: "1台" },
{ label: "平板拖车", value: "1台" },
{ label: "自卸货车", value: "4台" },
{ label: "工程车", value: "1台" },
{ label: "人员", value: "15人" },
{ label: "标志标牌", value: "5块" },
{ label: "铁锹", value: "30件" },
{ label: "铁镐", value: "10件" },
{ label: "麻袋、砂石袋等", value: "若干" },
],
},
];
/**
* 后续处治文本
*/
const followUpText =
'将该处于方案大部分障碍清点,设置"注意落石"等标志牌,进行巡逻管控,进行巡逻处理信号监测统设备。';
/**
* 处理新增预案按钮点击
*/
const handleAddPlan = () => {
console.log("[EmergencyPlanContent] 点击新增预案");
// TODO:
};
</script>
<style scoped lang="scss">
@use "@/styles/mixins.scss" as *;
.emergency-plan-content {
padding: 0;
color: var(--text-white);
}
/* 区块样式 */
.plan-section {
margin-bottom: vh(16);
// border: 1px solid rgba(28, 161, 255, 0.3);
border-radius: vw(4);
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
}
/* 区块头部 */
.section-header {
display: flex;
align-items: center;
gap: vw(8);
padding: vh(2) vw(18);
background: #1f497a;
.title-icon {
width: vw(8);
height: vh(32);
object-fit: contain;
}
.section-title {
font-size: fs(18);
font-family: SourceHanSansCN-Bold, sans-serif;
font-weight: bold;
color: var(--text-white);
margin: 0;
}
.add-btn {
margin-left: auto;
padding: vh(6) vw(16);
background: var(--primary-color);
border: none;
border-radius: vw(4);
color: var(--text-white);
font-size: fs(14);
font-family: SourceHanSansCN-Medium, sans-serif;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: var(--primary-light);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
}
/* 区块内容 */
.section-body {
padding: vh(14) vw(18);
}
/* 表单网格 */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: vw(16) vh(12);
&.grid-2x2 {
grid-template-columns: repeat(2, 1fr);
}
&.grid-2-cols {
grid-template-columns: 1fr 1fr;
}
}
/* 表单项 */
.form-item {
display: flex;
align-items: center;
gap: vw(10);
label {
width: vw(100);
font-size: fs(14);
font-family: SourceHanSansCN-Regular, sans-serif;
color: var(--text-gray);
white-space: nowrap;
flex-shrink: 0;
}
// Element Plus el-select
:deep(.custom-select) {
width: 100%;
background: #052044 !important;
//
.el-select__wrapper {
background: #052044 !important;
box-shadow: none !important;
}
//
.el-input {
background: #052044 !important;
&__wrapper {
background-color: #052044 !important;
background: #052044 !important;
border: 1px solid rgba(28, 161, 255, 0.3) !important;
border-radius: vw(4);
box-shadow: none !important;
padding: vh(6) vw(12);
transition: none !important;
&:hover {
border-color: var(--primary-color) !important;
background-color: #052044 !important;
background: #052044 !important;
}
&.is-focus,
&.is-focused {
border-color: var(--primary-color) !important;
box-shadow: none !important;
background-color: #052044 !important;
background: #052044 !important;
}
}
&__inner {
color: #fff !important;
font-size: fs(13);
font-family: SourceHanSansCN-Regular, sans-serif;
height: auto;
background-color: transparent !important;
background: transparent !important;
// placeholder
&:not(:placeholder-shown) {
color: #fff !important;
opacity: 1 !important;
}
}
}
//
.el-input {
color: #fff !important;
opacity: 1 !important;
&__inner {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
//
&[readonly] {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
}
&:disabled {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
}
}
//
&__suffix {
background-color: transparent !important;
background: transparent !important;
}
//
&__prefix {
background-color: transparent !important;
background: transparent !important;
}
}
//
.el-select__input {
color: #fff !important;
opacity: 1 !important;
}
//
.el-select__selected-item {
color: #fff !important;
opacity: 1 !important;
}
// placeholder placeholder
.el-input__inner::placeholder {
color: rgba(255, 255, 255, 0.5) !important;
}
// placeholder
.el-select__placeholder.is-transparent {
color: rgba(255, 255, 255, 0.5) !important;
}
// input readonly
input.el-input__inner {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
}
//
.el-select__caret {
color: var(--text-gray) !important;
}
//
.el-select__clear {
color: var(--text-gray) !important;
}
//
.el-select__tags {
background: transparent !important;
}
}
}
/* 方案步骤 */
.plan-steps {
margin-top: vh(16);
display: flex;
flex-direction: column;
gap: vh(12);
}
.step-item {
display: flex;
align-items: center;
gap: vw(10);
.step-label {
width: vw(100);
font-size: fs(14);
font-family: SourceHanSansCN-Regular, sans-serif;
color: var(--text-gray);
white-space: nowrap;
flex-shrink: 0;
}
.step-content {
flex: 1;
font-size: fs(13);
font-family: SourceHanSansCN-Regular, sans-serif;
line-height: 1.6;
color: var(--text-white);
padding: vh(8) vw(12);
background: #052044;
border-radius: vw(4);
}
}
/* 调派预案 */
.dispatch-plans {
display: flex;
flex-direction: column;
gap: vh(12);
}
.dispatch-card {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(28, 161, 255, 0.2);
border-radius: vw(8);
padding: vh(12) vw(14);
.plan-name {
font-size: fs(15);
font-family: SourceHanSansCN-Bold, sans-serif;
font-weight: bold;
color: var(--text-white);
margin: 0 0 vh(10) 0;
.plan-icon {
display: inline-block;
width: 9px;
height: 9px;
border: 2px solid #4fecff;
border-radius: 50%;
margin-right: 4px;
}
}
.resource-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: vw(8) vh(8);
}
.resource-item {
display: flex;
// flex-direction: column;
gap: vh(2);
.resource-label {
font-size: fs(12);
font-family: SourceHanSansCN-Regular, sans-serif;
color: var(--text-gray);
width: vw(100);
}
.resource-value {
font-size: fs(13);
font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 500;
color: var(--text-white);
}
}
}
/* 后续处治文本 */
.follow-up-text {
font-size: fs(13);
font-family: SourceHanSansCN-Regular, sans-serif;
line-height: 1.8;
color: var(--text-white);
margin: 0;
padding: vh(10) vw(14);
background: #052044;
border-radius: vw(4);
}
</style>
<style lang="scss">
// el-select
.custom-select {
.el-input__wrapper {
background-color: #052044 !important;
background: #052044 !important;
}
.el-input {
background-color: #052044 !important;
background: #052044 !important;
color: #fff !important;
opacity: 1 !important;
&__inner {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
//
&[readonly] {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
}
&:disabled {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
}
// placeholder
&:not(:placeholder-shown) {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
}
}
}
//
.el-select__input {
color: #fff !important;
opacity: 1 !important;
}
//
.el-select__selected-item {
color: #fff !important;
opacity: 1 !important;
}
// placeholder placeholder
.el-input__inner::placeholder {
color: rgba(255, 255, 255, 0.5) !important;
}
// placeholder
.el-select__placeholder.is-transparent {
color: rgba(255, 255, 255, 0.5) !important;
}
// input readonly
input.el-input__inner {
color: #fff !important;
opacity: 1 !important;
-webkit-text-fill-color: #fff !important;
}
}
// body scoped
.custom-dropdown {
background: #052044 !important;
border: 1px solid rgba(28, 161, 255, 0.3) !important;
//
.el-select-dropdown__wrap {
background: #052044 !important;
}
//
.el-select-dropdown__list {
background: #052044 !important;
padding: 0;
}
//
.el-select-dropdown__item {
background: #052044 !important;
color: #fff;
font-size: 13px;
font-family: SourceHanSansCN-Regular, sans-serif;
&:hover {
background: rgba(28, 161, 255, 0.2) !important;
}
&.is-selected {
background: rgba(28, 161, 255, 0.3) !important;
color: #fff;
font-weight: 500;
}
&.is-hovering {
background: rgba(28, 161, 255, 0.2) !important;
}
}
//
.el-select-dropdown__empty {
color: rgba(179, 204, 226, 1);
background: #052044 !important;
}
//
.el-scrollbar__view {
background: #052044 !important;
}
}
</style>

View File

@ -0,0 +1,308 @@
<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>