| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- <template>
- <div class="water-page gis-page">
- <div class="page-heading">
- <div class="page-title">
- <span class="page-title-icon"><el-icon><MapLocation/></el-icon></span>
- <div><h2>供水设施 GIS 一张图</h2><span class="muted">设施点位分布与查询定位</span></div>
- </div>
- <div class="heading-actions">
- <el-button :icon="Refresh" :loading="loading" @click="refreshScene">刷新</el-button>
- </div>
- </div>
- <el-card shadow="never" class="water-panel filter-card">
- <template #header>
- <div class="card-title">
- <span class="card-heading"><el-icon><Search/></el-icon><span>设施筛选</span></span>
- </div>
- </template>
- <el-form :inline="true" class="filter-form">
- <el-form-item label="关键字">
- <el-input v-model="searchKeyword" class="keyword-input" clearable placeholder="设施名称 / 编号 / 地址"
- :prefix-icon="Search" @keyup.enter="applyFilters"/>
- </el-form-item>
- <el-form-item label="设施类型">
- <el-select v-model="selectedFacilityType" class="status-select" clearable placeholder="全部设施类型">
- <el-option v-for="item in facilityTypes" :key="item.value" :label="item.label" :value="item.value"/>
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="applyFilters">查询</el-button>
- <el-button @click="resetFilters">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- <el-card shadow="never" class="water-panel map-card">
- <WaterGisPageShell
- embedded
- eyebrow="WATER SUPPLY FACILITY GIS"
- title="供水设施 GIS 一张图"
- :loading="loading"
- >
- <template #actions>
- <el-input
- v-model="searchKeyword"
- class="search-input"
- placeholder="搜索设施名称/编号/地址"
- clearable
- :prefix-icon="Search"
- @keyup.enter="handleSearch"
- />
- <el-select v-model="selectedFacilityType" class="type-select" placeholder="全部设施类型" clearable>
- <el-option v-for="item in facilityTypes" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- <el-button type="primary" :icon="Refresh" @click="refreshScene">刷新</el-button>
- <el-button :icon="Aim" @click="fitBounds">全图</el-button>
- </template>
- <template #metrics>
- <div class="metric">
- <div class="metric-icon"><el-icon><Odometer/></el-icon></div>
- <div class="metric-body">
- <span>设施总数</span>
- <div class="metric-value"><strong>{{ summaryStats.total }}</strong><small>处</small></div>
- </div>
- </div>
- <div class="metric">
- <div class="metric-icon"><el-icon><Connection/></el-icon></div>
- <div class="metric-body">
- <span>供水管网</span>
- <div class="metric-value"><strong>{{ summaryStats.pipeCount }}</strong><small>条</small></div>
- </div>
- </div>
- <div class="metric">
- <div class="metric-icon"><el-icon><OfficeBuilding/></el-icon></div>
- <div class="metric-body">
- <span>水厂</span>
- <div class="metric-value"><strong>{{ summaryStats.plantCount }}</strong><small>座</small></div>
- </div>
- </div>
- <div class="metric">
- <div class="metric-icon"><el-icon><SetUp/></el-icon></div>
- <div class="metric-body">
- <span>泵站</span>
- <div class="metric-value"><strong>{{ summaryStats.pumpStationCount }}</strong><small>座</small></div>
- </div>
- </div>
- </template>
- <template #controls>
- <div class="panel-section">
- <h3>图层控制</h3>
- <div class="layer-list">
- <label v-for="item in facilityTypes" :key="item.value" class="layer-row">
- <el-checkbox v-model="layerState[item.value]">{{ item.label }}</el-checkbox>
- </label>
- </div>
- </div>
- </template>
- <template #legend>
- <div class="panel-section legend-panel">
- <h3>图例</h3>
- <div class="legend-row" v-for="item in facilityTypes" :key="item.value">
- <i class="legend-dot" :style="{ background: item.color }"></i>{{ item.label }}
- </div>
- </div>
- </template>
- <template #results>
- <div class="panel-section result-section">
- <h3>搜索设施</h3>
- <el-alert v-if="loadError" type="error" :closable="false" show-icon :title="loadError">
- <el-button link type="primary" @click="loadGisData">重试</el-button>
- </el-alert>
- <el-empty
- v-else-if="!filteredFeatures.length"
- description="暂无可显示的供水设施"
- :image-size="80"
- />
- <div v-else class="device-result-list">
- <button v-for="feature in filteredFeatures" :key="feature.id || feature.name" type="button" @click="locateFeature(feature)">
- <span class="device-name">{{ feature.name }}</span>
- <span class="device-meta">{{ labelMap[feature.type] || '供水设施' }} · {{ feature.code || '-' }}</span>
- <el-tag size="small" type="primary">{{ labelMap[feature.type] || '供水设施' }}</el-tag>
- </button>
- </div>
- </div>
- </template>
- <template #map>
- <GisMapCanvas
- ref="gisMapRef"
- :points="mapPoints"
- :lines="mapLines"
- cluster-label="设施集合"
- :center="centerPoint"
- :zoom="15"
- @point-click="locateFeature"
- @line-click="locateFeature"
- @map-click="detailOpen = false"
- @ready="handleMapReady"
- @error="mapError = $event"
- />
- <el-alert v-if="mapError" class="map-error" type="warning" show-icon :closable="false" :title="mapError">
- <el-button link type="primary" @click="loadGisData">重试</el-button>
- </el-alert>
- <el-alert v-else-if="loadError" class="map-error" type="error" show-icon :closable="false" :title="loadError">
- <el-button link type="primary" @click="loadGisData">重试</el-button>
- </el-alert>
- <el-empty
- v-if="dataLoaded && !mapError && !loadError && !visibleFeatures.length"
- class="map-empty"
- description="当前筛选下没有可显示的供水设施"
- />
- </template>
- <template #drawer>
- <el-drawer v-model="detailOpen" title="设施详情" size="420px" direction="rtl" class="detail-drawer" append-to-body>
- <div v-loading="detailLoading" class="device-detail">
- <el-alert v-if="detailError" type="error" show-icon :closable="false" title="设施详情加载失败,请重试" />
- <template v-if="detailRecord">
- <section class="detail-hero">
- <h3>{{ detailTarget?.name || '供水设施' }}</h3>
- <p>{{ labelMap[detailTarget?.type] || '供水设施' }} · {{ detailTarget?.code || '-' }}</p>
- <div class="status-tags">
- <el-tag type="primary">{{ labelMap[detailTarget?.type] || '供水设施' }}</el-tag>
- </div>
- </section>
- <section class="detail-section">
- <h4>基础信息</h4>
- <dl>
- <template v-for="item in detailEntries" :key="item.label">
- <dt>{{ item.label }}</dt>
- <dd>{{ item.value }}</dd>
- </template>
- </dl>
- </section>
- </template>
- <el-empty v-else-if="!detailLoading && !detailError" description="暂无设施详情" />
- </div>
- </el-drawer>
- </template>
- </WaterGisPageShell>
- </el-card>
- </div>
- </template>
- <script setup>
- import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
- import { Aim, Connection, MapLocation, Odometer, OfficeBuilding, Refresh, Search, SetUp } from '@element-plus/icons-vue'
- import { ElMessage } from 'element-plus'
- import GisMapCanvas from '@/components/GisMapCanvas.vue'
- import WaterGisPageShell from '@/components/WaterGisPageShell.vue'
- import { getWaterFacilityOverview, getWaterFacilityFeatures, getWaterFacilityDetail } from '@/api/pipeNetwork/waterSupply'
- import { normalizeGeoJson, unwrapResponse } from '@/utils/waterSupplyModel'
- import { getRequestErrorMessage } from '@/utils/waterFacilityQuery'
- import { buildFacilityMapModel, filterFacilityMapModel, findFacilityFeature, formatFacilityDetail } from '@/utils/waterFacilityGisModel'
- import { getFacilityPointIcon } from '@/utils/gisMapColors'
- const centerPoint = { lng: 110.3965, lat: 28.4638 }
- const defaultLayerState = () => ({
- source: true,
- plant: true,
- pumpStation: true,
- pipe: true,
- user: true
- })
- const facilityTypes = [
- { label: '水源地', value: 'source', color: '#1f77d0' },
- { label: '水厂', value: 'plant', color: '#2ecc71' },
- { label: '泵站', value: 'pumpStation', color: '#e74c3c' },
- { label: '管网', value: 'pipe', color: '#f1c40f' },
- { label: '用水户', value: 'user', color: '#333333' }
- ]
- const colorMap = {
- source: '#1f77d0',
- plant: '#2ecc71',
- pumpStation: '#e74c3c',
- pipe: '#f1c40f',
- user: '#333333'
- }
- const labelMap = {
- source: '水源地',
- plant: '水厂',
- pumpStation: '泵站',
- pipe: '供水管网',
- user: '用水户'
- }
- const gisMapRef = ref(null)
- const searchKeyword = ref('')
- const selectedFacilityType = ref('')
- const mapError = ref('')
- const loadError = ref('')
- const dataLoaded = ref(false)
- const loading = ref(false)
- const mapReady = ref(false)
- const layerState = reactive(defaultLayerState())
- const facilityOverview = ref([])
- const pipelineData = ref([])
- const pointData = ref([])
- const detailOpen = ref(false)
- const detailLoading = ref(false)
- const detailError = ref('')
- const detailRecord = ref(null)
- const detailTarget = ref(null)
- const typeCount = type => {
- const row = facilityOverview.value.find(item => item.facilityType === type)
- return Number(row?.totalCount ?? row?.count ?? 0)
- }
- const summaryStats = computed(() => ({
- total: facilityTypes.reduce((sum, item) => sum + typeCount(item.value), 0),
- sourceCount: typeCount('source'),
- plantCount: typeCount('plant'),
- pumpStationCount: typeCount('pumpStation'),
- pipeCount: typeCount('pipe'),
- userCount: typeCount('user')
- }))
- const fullMapModel = computed(() => ({ points: pointData.value, lines: pipelineData.value }))
- const visibleMapModel = computed(() => filterFacilityMapModel(fullMapModel.value, layerState))
- const visibleFeatures = computed(() => [
- ...visibleMapModel.value.points.map(item => ({ ...item, kind: 'point' })),
- ...visibleMapModel.value.lines.map(item => ({ ...item, kind: 'pipeline' }))
- ])
- const filteredFeatures = computed(() => {
- const keyword = searchKeyword.value.trim().toLocaleLowerCase()
- return visibleFeatures.value.filter((feature) => {
- const matchedType = !selectedFacilityType.value || feature.type === selectedFacilityType.value
- const matchedKeyword = !keyword || [feature.name, feature.code, feature.road]
- .some(value => String(value || '').toLocaleLowerCase().includes(keyword))
- return matchedType && matchedKeyword
- })
- })
- const mapPoints = computed(() => visibleMapModel.value.points.map(point => ({
- ...point,
- color: colorMap[point.type] || '#457b9d',
- iconUrl: getFacilityPointIcon(point.type),
- name: point.name || point.code || '供水设施'
- })))
- const mapLines = computed(() => visibleMapModel.value.lines.map(line => ({
- ...line,
- color: colorMap[line.type] || '#fcbf49',
- iconUrl: getFacilityPointIcon(line.type),
- name: line.name || '供水管网'
- })))
- const detailEntries = computed(() => formatFacilityDetail(detailRecord.value, detailTarget.value?.type))
- watch(layerState, () => {
- if (mapReady.value) nextTick(() => gisMapRef.value?.fitBounds())
- }, { deep: true })
- onMounted(async () => {
- await nextTick()
- await loadGisData()
- })
- function handleMapReady() {
- mapReady.value = true
- if (dataLoaded.value) nextTick(() => gisMapRef.value?.fitBounds())
- }
- async function loadGisData() {
- loading.value = true
- mapError.value = ''
- loadError.value = ''
- dataLoaded.value = false
- try {
- const [overviewRes, featureRes] = await Promise.all([
- getWaterFacilityOverview(),
- getWaterFacilityFeatures()
- ])
- const overviewData = unwrapResponse(overviewRes)
- facilityOverview.value = Array.isArray(overviewData) ? overviewData : (overviewData?.facilityOverview || [])
- const geoJson = normalizeGeoJson(featureRes)
- const model = buildFacilityMapModel(geoJson)
- pipelineData.value = model.lines
- pointData.value = model.points
- dataLoaded.value = true
- await nextTick()
- if (mapReady.value) gisMapRef.value?.fitBounds()
- } catch (e) {
- console.error('加载供水GIS数据失败', e)
- facilityOverview.value = []
- pipelineData.value = []
- pointData.value = []
- dataLoaded.value = true
- loadError.value = getRequestErrorMessage(e, '供水GIS数据加载失败,请重试。')
- } finally {
- loading.value = false
- }
- }
- function fitBounds() {
- gisMapRef.value?.fitBounds()
- }
- function handleSearch() {
- const keyword = searchKeyword.value.trim()
- if (!keyword) {
- ElMessage.warning('请输入搜索关键字')
- return
- }
- const matched = findFacilityFeature(visibleMapModel.value, keyword)
- if (!matched) {
- ElMessage.info('未找到匹配要素')
- return
- }
- gisMapRef.value?.locatePoint(matched)
- openFacilityDetail(matched)
- ElMessage.success(`已定位到 ${matched.name}`)
- }
- function featureFocus(feature) {
- if (Array.isArray(feature.points) && feature.points.length) {
- const first = feature.points[0]
- const last = feature.points[feature.points.length - 1]
- return {
- lng: (first.lng + last.lng) / 2,
- lat: (first.lat + last.lat) / 2
- }
- }
- return feature
- }
- function locateFeature(feature) {
- gisMapRef.value?.locatePoint(featureFocus(feature))
- openFacilityDetail(feature)
- }
- async function openFacilityDetail(feature) {
- detailTarget.value = feature
- detailOpen.value = true
- detailRecord.value = null
- detailError.value = ''
- await loadFacilityDetail()
- }
- async function loadFacilityDetail() {
- const feature = detailTarget.value
- if (!feature?.type || feature.id === null || feature.id === undefined) return
- detailLoading.value = true
- detailError.value = ''
- try {
- detailRecord.value = unwrapResponse(await getWaterFacilityDetail(feature.type, feature.id))
- } catch (error) {
- detailRecord.value = null
- detailError.value = getRequestErrorMessage(error, '设施详情查询失败,请重试。')
- } finally {
- detailLoading.value = false
- }
- }
- function applyFilters() {
- if (searchKeyword.value.trim()) {
- handleSearch()
- return
- }
- loadGisData()
- }
- function resetFilters() {
- searchKeyword.value = ''
- selectedFacilityType.value = ''
- detailOpen.value = false
- loadGisData()
- }
- async function refreshScene() {
- layerState.source = true
- layerState.plant = true
- layerState.pumpStation = true
- layerState.pipe = true
- layerState.user = true
- searchKeyword.value = ''
- selectedFacilityType.value = ''
- detailOpen.value = false
- await loadGisData()
- fitBounds()
- }
- </script>
- <style scoped>
- .header-actions {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .search-input,
- .type-select {
- width: 240px;
- }
- .panel-section h3 {
- margin: 0 0 8px;
- font-size: 14px;
- color: #29567e;
- }
- .layer-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .layer-row {
- display: flex;
- align-items: center;
- }
- .legend-panel {
- border-top: 1px solid #e4eef8;
- padding-top: 12px;
- }
- .legend-row {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 6px;
- font-size: 13px;
- color: #4d667d;
- }
- .legend-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- flex-shrink: 0;
- }
- .result-section {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- }
- .device-result-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .device-result-list button {
- width: 100%;
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 3px 8px;
- align-items: center;
- border: 1px solid #dbe8f5;
- background: #fff;
- border-radius: 8px;
- padding: 8px 10px;
- text-align: left;
- cursor: pointer;
- }
- .device-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-weight: 600;
- color: #274b6d;
- }
- .device-meta {
- grid-column: 1 / -1;
- font-size: 12px;
- color: #7793aa;
- }
- .device-detail {
- min-height: 240px;
- }
- .detail-hero h3,
- .detail-section h4 {
- margin: 0 0 8px;
- color: #1c4b78;
- }
- .detail-hero p {
- margin: 0 0 10px;
- color: #678197;
- font-size: 13px;
- }
- .status-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- }
- .detail-section {
- margin-top: 18px;
- padding-top: 12px;
- border-top: 1px solid #e5eef7;
- }
- .detail-section dl {
- display: grid;
- grid-template-columns: 88px minmax(0, 1fr);
- gap: 7px 10px;
- margin: 0;
- font-size: 13px;
- }
- .detail-section dt {
- color: #71889c;
- }
- .detail-section dd {
- margin: 0;
- color: #2b4c67;
- overflow-wrap: anywhere;
- }
- .muted {
- margin: 0;
- color: #7d93a6;
- font-size: 13px;
- }
- @media (max-width: 1200px) {
- .search-input,
- .type-select {
- width: 180px;
- }
- }
- </style>
- <style lang="scss">
- @use '../components/waterGisPageTheme.scss';
- </style>
|