ci/woodpecker/push/main Pipeline was successful
- MapCanvas: 层聚焦与回全景联动;右栏停用改自绘可拖详情浮层;目标树默认收起+localStorage 持久化;底部跨层关系摘要条;聚焦态只绘聚焦层同层连线 - StrategyLayer: 层标题可点击聚焦并标「聚焦中」;非聚焦层压缩为 ≤48px 单行摘要(层名—达成率/目标数/mini 色条) - MapSummaryBar: 汇总条压缩 ≤44px、5 项同排;窄屏无横向溢出 - MapEditBar: 新增「全景 / L1–L4」聚焦切换 chip(内联 SVG,无 emoji) - 只读渲染层改动:不写 canvas_data / map_connections,无新增写链路调用
197 lines
7.0 KiB
Vue
197 lines
7.0 KiB
Vue
<template>
|
||
<div class="map-editbar">
|
||
<div class="eb-left">
|
||
<el-input
|
||
:model-value="search"
|
||
class="eb-search"
|
||
size="small"
|
||
clearable
|
||
placeholder="搜索目标 / 负责人 / KPI"
|
||
@update:model-value="(v: string) => $emit('update:search', v)"
|
||
>
|
||
<template #prefix>
|
||
<svg class="eb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||
<circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" />
|
||
</svg>
|
||
</template>
|
||
</el-input>
|
||
|
||
<div class="eb-chips" role="group" aria-label="状态筛选">
|
||
<button
|
||
v-for="opt in filterOptions"
|
||
:key="opt.value"
|
||
type="button"
|
||
class="eb-chip"
|
||
:class="{ on: filter === opt.value }"
|
||
@click="$emit('update:filter', opt.value)"
|
||
>{{ opt.label }}</button>
|
||
</div>
|
||
|
||
<div class="eb-chips eb-layers" role="group" aria-label="层筛选">
|
||
<button
|
||
v-for="cfg in layerConfigs"
|
||
:key="cfg.key"
|
||
type="button"
|
||
class="eb-chip layer-chip"
|
||
:class="{ off: !visibleLayers[cfg.key] }"
|
||
:title="visibleLayers[cfg.key] ? '隐藏该层' : '显示该层'"
|
||
@click="$emit('toggle-layer', cfg.key)"
|
||
>
|
||
<span class="layer-dot" :style="{ background: cfg.color }"></span>
|
||
{{ cfg.short }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="eb-right">
|
||
<el-dropdown trigger="click" @command="onAdd">
|
||
<el-button size="small" type="primary" plain>
|
||
<svg class="eb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||
<path d="M12 5v14M5 12h14" />
|
||
</svg>
|
||
加目标
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item v-for="cfg in layerConfigs" :key="cfg.key" :command="cfg.key">
|
||
<span class="dd-dot" :style="{ background: cfg.color }"></span>{{ cfg.label }}
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
|
||
<el-button size="small" @click="$emit('connect')">
|
||
<svg class="eb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||
<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1" />
|
||
<path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1" />
|
||
</svg>
|
||
连因果
|
||
</el-button>
|
||
|
||
<el-button size="small" :disabled="!canUndo" @click="$emit('undo')" title="撤销上一步画布编辑">
|
||
<svg class="eb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||
<path d="M9 14L4 9l5-5" /><path d="M4 9h10a6 6 0 0 1 0 12h-3" />
|
||
</svg>
|
||
撤销
|
||
</el-button>
|
||
|
||
<el-button size="small" @click="$emit('completeness')" title="数据完备度与清单(口径:地图65·23KPI判据)">
|
||
<svg class="eb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||
<path d="M4 19V5" /><path d="M4 19h16" /><rect x="7" y="11" width="3" height="6" rx=".5" />
|
||
<rect x="12" y="7" width="3" height="10" rx=".5" /><rect x="17" y="13" width="3" height="4" rx=".5" />
|
||
</svg>
|
||
数据完备度
|
||
</el-button>
|
||
|
||
<el-button size="small" @click="$emit('fit')" title="缩放到适合窗口">
|
||
<svg class="eb-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||
<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" />
|
||
</svg>
|
||
回全景
|
||
</el-button>
|
||
|
||
<el-button size="small" text @click="$emit('toggle-tree')" :title="treeOpen ? '收起左侧目标树(画布全宽)' : '展开左侧目标树'">
|
||
{{ treeOpen ? '收起目标树' : '展开目标树' }}
|
||
</el-button>
|
||
|
||
<!-- C 方案:聚焦开关(全景 = 默认四层等宽) -->
|
||
<div class="eb-chips eb-focus" role="group" aria-label="聚焦层">
|
||
<button
|
||
type="button"
|
||
class="eb-chip"
|
||
:class="{ on: !focusLayer }"
|
||
title="四层全景(默认)"
|
||
@click="$emit('focus-layer', null)"
|
||
>全景</button>
|
||
<button
|
||
v-for="cfg in layerConfigs"
|
||
:key="'focus-' + cfg.key"
|
||
type="button"
|
||
class="eb-chip"
|
||
:class="{ on: focusLayer === cfg.key }"
|
||
:title="focusLayer === cfg.key ? '再次点击退出聚焦' : '聚焦' + cfg.label"
|
||
@click="$emit('focus-layer', focusLayer === cfg.key ? null : cfg.key)"
|
||
>
|
||
<span class="layer-dot" :style="{ background: cfg.color }"></span>
|
||
{{ cfg.short }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
defineProps<{
|
||
search: string
|
||
filter: 'all' | 'risk' | 'nodata'
|
||
visibleLayers: Record<string, boolean>
|
||
layerConfigs: { key: string; label: string; short: string; color: string }[]
|
||
canUndo: boolean
|
||
treeOpen: boolean
|
||
/** C 方案聚焦层:null = 四层全景 */
|
||
focusLayer?: string | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:search', v: string): void
|
||
(e: 'update:filter', v: 'all' | 'risk' | 'nodata'): void
|
||
(e: 'toggle-layer', key: string): void
|
||
(e: 'add-objective', key: string): void
|
||
(e: 'connect'): void
|
||
(e: 'undo'): void
|
||
(e: 'fit'): void
|
||
(e: 'toggle-tree'): void
|
||
(e: 'completeness'): void
|
||
(e: 'focus-layer', key: string | null): void
|
||
}>()
|
||
|
||
const filterOptions: { value: 'all' | 'risk' | 'nodata'; label: string }[] = [
|
||
{ value: 'all', label: '全部' },
|
||
{ value: 'risk', label: '只看风险' },
|
||
{ value: 'nodata', label: '只看无数据' },
|
||
]
|
||
|
||
function onAdd(key: string) {
|
||
emit('add-objective', key)
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.map-editbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
flex-shrink: 0;
|
||
padding: 8px 10px;
|
||
margin-bottom: 10px;
|
||
background: var(--bh-bg-card, #fff);
|
||
border: 1px solid var(--bh-border, #E5E7EB);
|
||
border-radius: 8px;
|
||
}
|
||
.eb-left, .eb-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.eb-search { width: 220px; }
|
||
.eb-ico { width: 14px; height: 14px; vertical-align: -2px; }
|
||
.eb-chips { display: inline-flex; align-items: center; gap: 4px; padding: 2px; background: #F6F7F9; border: 1px solid #E5E7EB; border-radius: 6px; }
|
||
.eb-chip {
|
||
border: 0;
|
||
background: transparent;
|
||
color: #475569;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
padding: 2px 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
transition: background .15s, color .15s;
|
||
}
|
||
.eb-chip:hover { color: #635BFF; }
|
||
.eb-chip.on { background: #635BFF; color: #fff; }
|
||
.eb-chip.off { color: #94A3B8; text-decoration: line-through; }
|
||
.layer-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
||
.dd-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
||
</style>
|