Merge branch 'dev' of http://222.212.85.86:8222/bdzl2/bxztApp into dev
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 27 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/上箭头 .png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/下箭头.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/交调icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/交调icon定位.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/应急力量icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/普通公路icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/服务设施icon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/桥梁icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/桥梁icon定位.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/气象预警icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/视频icon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/视频icon定位.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/阻断事件icon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/隧洞icon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/隧洞icon定位.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
packages/screen/src/views/cockpit/assets/legendTool/风险路段icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@ -14,9 +14,12 @@
|
|||||||
<img src="../assets/img/block-stat-icon.png" alt="icon" class="stat-icon" />
|
<img src="../assets/img/block-stat-icon.png" alt="icon" class="stat-icon" />
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-title">{{ stat.title }}</div>
|
<div class="stat-title">{{ stat.title }}</div>
|
||||||
<div class="stat-value">{{ stat.value }}</div>
|
<div class="stat-value-container">
|
||||||
<div class="stat-unit">公里</div>
|
<div class="stat-value">{{ stat.value }}</div>
|
||||||
<img src="../assets/img/block-stat-decoration.png" alt="decoration" class="stat-decoration" />
|
<div class="stat-unit">公里</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <img src="../assets/img/block-stat-decoration.png" alt="decoration" class="stat-decoration" /> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,13 +116,24 @@ const stats = ref([
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-unit {
|
.stat-unit {
|
||||||
position: absolute;
|
// position: absolute;
|
||||||
top: vh(50);
|
// top: vh(50);
|
||||||
right: vw(20);
|
// right: vw(20);
|
||||||
color: rgba(255, 255, 255, 1);
|
color: rgba(255, 255, 255, 1);
|
||||||
font-size: fs(15);
|
font-size: fs(15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.stat-value-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: url(../assets/img/block-stat-decoration.png) no-repeat;
|
||||||
|
background-size: 100% auto;
|
||||||
|
background-position: bottom;
|
||||||
|
padding: 0 vw(40) vh(10) 0;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-decoration {
|
.stat-decoration {
|
||||||
width: vw(154);
|
width: vw(154);
|
||||||
height: vh(29);
|
height: vh(29);
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<div class="center-panel">
|
<div class="center-panel">
|
||||||
<MapCenter />
|
<MapCenter />
|
||||||
|
<LegendToolbar style="position: absolute; bottom: 10px; right: 50%; transform: translateX(50%);"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
@ -26,6 +27,7 @@ import EmergencyResources from './EmergencyResources.vue'
|
|||||||
import MapCenter from './MapCenter.vue'
|
import MapCenter from './MapCenter.vue'
|
||||||
import BlockEvent from './BlockEvent.vue'
|
import BlockEvent from './BlockEvent.vue'
|
||||||
import YearStatistics from './YearStatistics.vue'
|
import YearStatistics from './YearStatistics.vue'
|
||||||
|
import LegendToolbar from './LegendToolbar.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -72,7 +74,7 @@ import YearStatistics from './YearStatistics.vue'
|
|||||||
|
|
||||||
/* 嵌入场景的 CSS 自定义属性 */
|
/* 嵌入场景的 CSS 自定义属性 */
|
||||||
--cockpit-side-width: minmax(15rem, 28%);
|
--cockpit-side-width: minmax(15rem, 28%);
|
||||||
--cockpit-gap: 1.25rem;
|
--cockpit-gap: 1rem;
|
||||||
--cockpit-padding-x: 0.625rem;
|
--cockpit-padding-x: 0.625rem;
|
||||||
--cockpit-padding-top: 0;
|
--cockpit-padding-top: 0;
|
||||||
--cockpit-padding-bottom: 0;
|
--cockpit-padding-bottom: 0;
|
||||||
@ -102,6 +104,7 @@ import YearStatistics from './YearStatistics.vue'
|
|||||||
min-height: 0; /* 允许网格在 flex 上下文中收缩 */
|
min-height: 0; /* 允许网格在 flex 上下文中收缩 */
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--cockpit-side-width) 1fr var(--cockpit-side-width);
|
grid-template-columns: var(--cockpit-side-width) 1fr var(--cockpit-side-width);
|
||||||
|
grid-auto-rows: 1fr; /* 强制行占据可用高度 */
|
||||||
gap: var(--cockpit-gap);
|
gap: var(--cockpit-gap);
|
||||||
padding: var(--cockpit-padding-top) var(--cockpit-padding-x) var(--cockpit-padding-bottom);
|
padding: var(--cockpit-padding-top) var(--cockpit-padding-x) var(--cockpit-padding-bottom);
|
||||||
}
|
}
|
||||||
@ -112,6 +115,7 @@ import YearStatistics from './YearStatistics.vue'
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--cockpit-gap);
|
gap: var(--cockpit-gap);
|
||||||
min-width: 0; /* 防止在窄容器中溢出 */
|
min-width: 0; /* 防止在窄容器中溢出 */
|
||||||
|
min-height: 0; /* 允许 flex 子元素收缩并启用滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-panel {
|
.center-panel {
|
||||||
|
|||||||
@ -13,24 +13,26 @@
|
|||||||
<span>应急设备</span>
|
<span>应急设备</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="table-body">
|
||||||
v-for="(resource, index) in resources"
|
<div
|
||||||
:key="resource.id"
|
v-for="(resource, index) in resources"
|
||||||
class="table-row"
|
:key="resource.id"
|
||||||
:class="{ 'row-alt': index % 2 === 0 }"
|
class="table-row"
|
||||||
>
|
:class="{ 'row-alt': index % 2 === 0 }"
|
||||||
<div class="row-number">{{ index + 1 }}</div>
|
>
|
||||||
<span class="district-name">{{ resource.name }}</span>
|
<div class="row-number">{{ index + 1 }}</div>
|
||||||
<span class="count green">{{ resource.stations }}</span>
|
<span class="district-name">{{ resource.qxmc }}</span>
|
||||||
<span class="count orange">{{ resource.supplies }}</span>
|
<span class="count green">{{ resource.yhzCount }}</span>
|
||||||
<div class="equipment-cell">
|
<span class="count orange">{{ resource.wzCount }}</span>
|
||||||
<span class="count" :class="resource.equipmentClass">{{ resource.equipment }}</span>
|
<div class="equipment-cell">
|
||||||
<img
|
<span class="count" :class="resource.equipmentClass">{{ resource.sbCount }}</span>
|
||||||
v-if="resource.hasAlert"
|
<img
|
||||||
src="../assets/img/emergency-alert-icon.png"
|
v-if="resource.hasAlert"
|
||||||
alt="alert"
|
src="../assets/img/emergency-alert-icon.png"
|
||||||
class="alert-icon"
|
alt="alert"
|
||||||
/>
|
class="alert-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +40,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { request } from '@shared/utils/request'
|
||||||
|
|
||||||
const resources = ref([
|
const resources = ref([
|
||||||
{
|
{
|
||||||
@ -96,6 +99,25 @@ const resources = ref([
|
|||||||
hasAlert: false
|
hasAlert: false
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 请求后端接口 /district/statistics
|
||||||
|
const getDistrictStatistics = async () => {
|
||||||
|
const res = await request({
|
||||||
|
url: '/snow-ops-platform/district/statistics',
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
console.log(res)
|
||||||
|
if(res.code === '00000') {
|
||||||
|
resources.value = res.data
|
||||||
|
} else {
|
||||||
|
console.log(res.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDistrictStatistics()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -106,6 +128,7 @@ const resources = ref([
|
|||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
padding: vw(20) vw(30);
|
padding: vw(20) vw(30);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -131,6 +154,7 @@ const resources = ref([
|
|||||||
|
|
||||||
.resource-table {
|
.resource-table {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@ -149,6 +173,34 @@ const resources = ref([
|
|||||||
margin-bottom: vh(5);
|
margin-bottom: vh(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(28, 161, 255, 0.4) transparent;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: vw(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(28, 161, 255, 0.4);
|
||||||
|
border-radius: vw(2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(28, 161, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
height: vh(35);
|
height: vh(35);
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
446
packages/screen/src/views/cockpit/components/LegendToolbar.vue
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
<template>
|
||||||
|
<div class="legend-toolbar">
|
||||||
|
<!-- 图例面板(包含图例容器和折叠按钮) -->
|
||||||
|
<div class="legend-panel">
|
||||||
|
<!-- 图例容器 -->
|
||||||
|
<transition name="legend-collapse">
|
||||||
|
<div
|
||||||
|
v-show="!isCollapsed"
|
||||||
|
class="legend-container"
|
||||||
|
:style="{ backgroundImage: `url(${backgroundImg})` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="item in computedLegendItems"
|
||||||
|
:key="item.key"
|
||||||
|
class="legend-item"
|
||||||
|
:class="{ active: isActive(item.key) }"
|
||||||
|
@click="handleItemClick(item.key)"
|
||||||
|
>
|
||||||
|
<img :src="item.icon" :alt="item.label" class="legend-icon" />
|
||||||
|
<span class="legend-label">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- 折叠/展开按钮 -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="collapse-toggle"
|
||||||
|
:aria-label="isCollapsed ? '展开图例' : '折叠图例'"
|
||||||
|
:aria-expanded="!isCollapsed"
|
||||||
|
@click="toggleCollapse"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="isCollapsed ? upArrowIcon : downArrowIcon"
|
||||||
|
:alt="isCollapsed ? '展开图例' : '折叠图例'"
|
||||||
|
class="collapse-icon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 清除按钮 -->
|
||||||
|
<!-- <div class="clear-button" @click="handleClearClick">
|
||||||
|
<img :src="clearIcon" alt="清除" class="clear-icon" />
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
// 导入图例背景图和控制图标
|
||||||
|
import backgroundImg from '../assets/legendTool/图例工具栏iconBg.png'
|
||||||
|
import downArrowIcon from '../assets/legendTool/下箭头.png'
|
||||||
|
import upArrowIcon from '../assets/legendTool/上箭头 .png'
|
||||||
|
// import clearIcon from '../assets/legend/清除icon.png'
|
||||||
|
|
||||||
|
// 导入图例图标(普通图标和定位图标)
|
||||||
|
import commonRoadIcon from '../assets/legendTool/普通公路icon.png'
|
||||||
|
import commonRoadMarkerIcon from '../assets/legendTool/普通公路icon定位.png'
|
||||||
|
import videoIcon from '../assets/legendTool/视频icon.png'
|
||||||
|
import videoMarkerIcon from '../assets/legendTool/视频icon定位.png'
|
||||||
|
import trafficMonitorIcon from '../assets/legendTool/交调icon.png'
|
||||||
|
import trafficMonitorMarkerIcon from '../assets/legendTool/交调icon定位.png'
|
||||||
|
import bridgeIcon from '../assets/legendTool/桥梁icon.png'
|
||||||
|
import bridgeMarkerIcon from '../assets/legendTool/桥梁icon定位.png'
|
||||||
|
import tunnelIcon from '../assets/legendTool/隧洞icon.png'
|
||||||
|
import tunnelMarkerIcon from '../assets/legendTool/隧洞icon定位.png'
|
||||||
|
import serviceFacilityIcon from '../assets/legendTool/服务设施icon.png'
|
||||||
|
import serviceFacilityMarkerIcon from '../assets/legendTool/服务设施icon定位.png'
|
||||||
|
import riskRoadIcon from '../assets/legendTool/风险路段icon.png'
|
||||||
|
import riskRoadMarkerIcon from '../assets/legendTool/风险路段icon定位.png'
|
||||||
|
import hazardPointIcon from '../assets/legendTool/涉灾隐患点icon.png'
|
||||||
|
import hazardPointMarkerIcon from '../assets/legendTool/涉灾隐患点icon定位.png'
|
||||||
|
import blockEventIcon from '../assets/legendTool/阻断事件icon.png'
|
||||||
|
import blockEventMarkerIcon from '../assets/legendTool/阻断事件icon定位.png'
|
||||||
|
import emergencyForceIcon from '../assets/legendTool/应急力量icon.png'
|
||||||
|
import emergencyForceMarkerIcon from '../assets/legendTool/应急力量icon定位.png'
|
||||||
|
import weatherAlertIcon from '../assets/legendTool/气象预警icon.png'
|
||||||
|
|
||||||
|
// 默认图例项配置(包含普通图标和地图定位图标)
|
||||||
|
const defaultLegendItems = [
|
||||||
|
{ key: 'commonRoad', label: '普通公路', icon: commonRoadIcon, markerIcon: commonRoadMarkerIcon },
|
||||||
|
{ key: 'video', label: '视频', icon: videoIcon, markerIcon: videoMarkerIcon },
|
||||||
|
{ key: 'trafficMonitor', label: '交调', icon: trafficMonitorIcon, markerIcon: trafficMonitorMarkerIcon },
|
||||||
|
{ key: 'bridge', label: '桥梁', icon: bridgeIcon, markerIcon: bridgeMarkerIcon },
|
||||||
|
{ key: 'tunnel', label: '隧洞', icon: tunnelIcon, markerIcon: tunnelMarkerIcon },
|
||||||
|
{ key: 'serviceFacility', label: '服务设施', icon: serviceFacilityIcon, markerIcon: serviceFacilityMarkerIcon },
|
||||||
|
{ key: 'riskRoad', label: '风险路段', icon: riskRoadIcon, markerIcon: riskRoadMarkerIcon },
|
||||||
|
{ key: 'hazardPoint', label: '涉灾隐患点', icon: hazardPointIcon, markerIcon: hazardPointMarkerIcon },
|
||||||
|
{ key: 'blockEvent', label: '阻断事件', icon: blockEventIcon, markerIcon: blockEventMarkerIcon },
|
||||||
|
{ key: 'emergencyForce', label: '应急力量', icon: emergencyForceIcon, markerIcon: emergencyForceMarkerIcon },
|
||||||
|
{ key: 'weatherAlert', label: '气象预警', icon: weatherAlertIcon, markerIcon: weatherAlertIcon }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 定义 props
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* 自定义图例项配置
|
||||||
|
* @type {Array<{ key: string; label: string; icon: string; markerIcon?: string; markerType?: string }>}
|
||||||
|
*/
|
||||||
|
legendItems: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker 数据配置(可选)
|
||||||
|
* 如果提供,将在事件中传递完整的 marker 数据
|
||||||
|
* @type {Record<string, { markers?: Array; icon?: string; markerIcon?: string; meta?: any }>}
|
||||||
|
*/
|
||||||
|
markerConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始激活的图例项 key 列表
|
||||||
|
* @type {Array<string>}
|
||||||
|
*/
|
||||||
|
defaultActive: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持 v-model 的激活项列表(受控模式)
|
||||||
|
* @type {Array<string>}
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容旧版:标记点切换回调函数
|
||||||
|
* @deprecated 推荐使用 @marker-toggle 事件
|
||||||
|
*/
|
||||||
|
onMarkerToggle: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义 emits
|
||||||
|
const emit = defineEmits(['marker-toggle', 'clear', 'update:modelValue', 'collapse-change'])
|
||||||
|
|
||||||
|
// 计算图例项配置(支持自定义)
|
||||||
|
const computedLegendItems = computed(() => {
|
||||||
|
return props.legendItems && props.legendItems.length > 0
|
||||||
|
? props.legendItems
|
||||||
|
: defaultLegendItems
|
||||||
|
})
|
||||||
|
|
||||||
|
// 激活的图例项(使用 Set 管理唯一值)
|
||||||
|
// 支持受控和非受控两种模式
|
||||||
|
const internalActiveItems = ref(new Set(props.defaultActive))
|
||||||
|
|
||||||
|
// 折叠状态(默认展开)
|
||||||
|
const isCollapsed = ref(false)
|
||||||
|
|
||||||
|
// 如果是受控模式(提供了 modelValue),同步外部状态
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue !== null && Array.isArray(newValue)) {
|
||||||
|
internalActiveItems.value = new Set(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 同步 defaultActive 的变化
|
||||||
|
watch(
|
||||||
|
() => props.defaultActive,
|
||||||
|
(newValue) => {
|
||||||
|
// 仅在非受控模式下同步
|
||||||
|
if (props.modelValue === null && Array.isArray(newValue)) {
|
||||||
|
internalActiveItems.value = new Set(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取当前激活项集合(优先使用外部状态)
|
||||||
|
const activeItems = computed(() => {
|
||||||
|
return props.modelValue !== null && Array.isArray(props.modelValue)
|
||||||
|
? new Set(props.modelValue)
|
||||||
|
: internalActiveItems.value
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断图例项是否激活
|
||||||
|
* @param {string} key - 图例项 key
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const isActive = (key) => {
|
||||||
|
return activeItems.value.has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建事件 payload
|
||||||
|
* @param {string} key - 图例项 key
|
||||||
|
* @param {boolean} active - 是否激活
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
const buildPayload = (key, active) => {
|
||||||
|
const payload = { key, active }
|
||||||
|
const legendItem = computedLegendItems.value.find((item) => item.key === key)
|
||||||
|
|
||||||
|
// 如果提供了 markerConfig,附加完整数据
|
||||||
|
if (props.markerConfig && props.markerConfig[key]) {
|
||||||
|
const config = props.markerConfig[key]
|
||||||
|
if (config.markers) payload.markers = config.markers
|
||||||
|
if (config.icon) payload.icon = config.icon
|
||||||
|
if (config.markerIcon) payload.markerIcon = config.markerIcon
|
||||||
|
if (config.meta) payload.meta = config.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从默认图例项或自定义图例项中获取图标信息
|
||||||
|
if (legendItem) {
|
||||||
|
// 只在 payload 中不存在时才赋值(markerConfig 优先)
|
||||||
|
if (!payload.icon && legendItem.icon) {
|
||||||
|
payload.icon = legendItem.icon
|
||||||
|
}
|
||||||
|
if (!payload.markerIcon && legendItem.markerIcon) {
|
||||||
|
payload.markerIcon = legendItem.markerIcon
|
||||||
|
}
|
||||||
|
if (legendItem.label) payload.label = legendItem.label
|
||||||
|
if (legendItem.markerType) payload.markerType = legendItem.markerType
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理图例项点击事件
|
||||||
|
* @param {string} key - 图例项 key
|
||||||
|
*/
|
||||||
|
const handleItemClick = (key) => {
|
||||||
|
const newIsActive = !activeItems.value.has(key)
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const isControlled = Array.isArray(props.modelValue)
|
||||||
|
if (isControlled) {
|
||||||
|
// 受控模式:通过 emit 通知父组件更新
|
||||||
|
const newActiveKeys = newIsActive
|
||||||
|
? [...props.modelValue, key]
|
||||||
|
: props.modelValue.filter((k) => k !== key)
|
||||||
|
emit('update:modelValue', newActiveKeys)
|
||||||
|
} else {
|
||||||
|
// 非受控模式:直接更新内部状态
|
||||||
|
const newSet = new Set(internalActiveItems.value)
|
||||||
|
if (newIsActive) {
|
||||||
|
newSet.add(key)
|
||||||
|
} else {
|
||||||
|
newSet.delete(key)
|
||||||
|
}
|
||||||
|
internalActiveItems.value = newSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建并发送事件
|
||||||
|
const payload = buildPayload(key, newIsActive)
|
||||||
|
emit('marker-toggle', payload)
|
||||||
|
|
||||||
|
// 兼容旧版 props 回调
|
||||||
|
if (props.onMarkerToggle) {
|
||||||
|
props.onMarkerToggle(
|
||||||
|
key,
|
||||||
|
payload.markers || [],
|
||||||
|
payload.icon || null,
|
||||||
|
!newIsActive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换图例工具栏的折叠状态
|
||||||
|
*/
|
||||||
|
const toggleCollapse = () => {
|
||||||
|
isCollapsed.value = !isCollapsed.value
|
||||||
|
emit('collapse-change', { collapsed: isCollapsed.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理清除按钮点击事件
|
||||||
|
*/
|
||||||
|
const handleClearClick = () => {
|
||||||
|
const clearedKeys = Array.from(activeItems.value)
|
||||||
|
|
||||||
|
// 逐一发送关闭通知
|
||||||
|
clearedKeys.forEach((key) => {
|
||||||
|
const payload = buildPayload(key, false)
|
||||||
|
emit('marker-toggle', payload)
|
||||||
|
|
||||||
|
// 兼容旧版回调
|
||||||
|
if (props.onMarkerToggle) {
|
||||||
|
props.onMarkerToggle(key, payload.markers || [], payload.icon || null, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const isControlled = Array.isArray(props.modelValue)
|
||||||
|
if (isControlled) {
|
||||||
|
emit('update:modelValue', [])
|
||||||
|
} else {
|
||||||
|
internalActiveItems.value = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送清除事件
|
||||||
|
emit('clear', { cleared: clearedKeys })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '@/styles/mixins.scss' as *;
|
||||||
|
|
||||||
|
.legend-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: vw(15);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: vh(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: vh(10) vw(20);
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
gap: vw(20);
|
||||||
|
border-radius: vw(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: vh(8);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: vh(8) vw(8);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(vh(-2));
|
||||||
|
|
||||||
|
.legend-icon {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.legend-icon {
|
||||||
|
filter: brightness(1.3) drop-shadow(0 0 vw(8) rgba(30, 144, 255, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-label {
|
||||||
|
color: #1e90ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-icon {
|
||||||
|
width: vw(40);
|
||||||
|
height: vh(40);
|
||||||
|
object-fit: contain;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-label {
|
||||||
|
font-size: fs(12);
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 vh(1) vh(2) rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
padding: vh(4) 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(vh(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgba(30, 144, 255, 0.8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
width: vw(24);
|
||||||
|
height: vh(24);
|
||||||
|
object-fit: contain;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折叠动画
|
||||||
|
.legend-collapse-enter-active,
|
||||||
|
.legend-collapse-leave-active {
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-collapse-enter-from,
|
||||||
|
.legend-collapse-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleY(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(vh(-2));
|
||||||
|
box-shadow: 0 vh(4) vh(12) rgba(30, 144, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-icon {
|
||||||
|
width: vw(40);
|
||||||
|
height: vh(40);
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -15,16 +15,16 @@
|
|||||||
<div class="badge-container">
|
<div class="badge-container">
|
||||||
<!-- 六边形徽章 -->
|
<!-- 六边形徽章 -->
|
||||||
<div class="level-badge">
|
<div class="level-badge">
|
||||||
<img
|
<!-- <img
|
||||||
src="../assets/img/weather-badge-hexagon.png"
|
src="../assets/img/weather-badge-hexagon.png"
|
||||||
alt="badge"
|
alt="badge"
|
||||||
class="badge-bg"
|
class="badge-bg"
|
||||||
/>
|
/> -->
|
||||||
<span class="level-count" :style="{ color: level.color }">{{ level.count }}</span>
|
<span class="level-count" :style="{ color: level.color }">{{ level.count }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底座光圈(双背景图) -->
|
<!-- 底座光圈(双背景图) -->
|
||||||
<div class="glow-base"></div>
|
<!-- <div class="glow-base"></div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部文字 -->
|
<!-- 底部文字 -->
|
||||||
@ -158,7 +158,7 @@ const districts = ref([
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: vh(10);
|
// gap: vh(10);
|
||||||
|
|
||||||
// 徽章和光圈容器
|
// 徽章和光圈容器
|
||||||
.badge-container {
|
.badge-container {
|
||||||
@ -177,6 +177,7 @@ const districts = ref([
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
background: url(../assets/img/weather-badge-bg.png) center center / 100% 100% no-repeat;
|
||||||
|
|
||||||
.badge-bg {
|
.badge-bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -192,6 +193,7 @@ const districts = ref([
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
margin-top: vh(-18);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||