znlgis 博客

GIS开发与技术分享

AI 水务前端核心功能详解

一、地图系统

1. 地图组件(DemoMap.vue)

基于 OpenLayers 实现的地图组件:

import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
import { fromLonLat } from 'ol/proj'

// 创建地图实例
const map = new Map({
  target: 'map',
  layers: [
    new TileLayer({
      source: new OSM(),
      properties: { name: 'OpenStreetMap', isBase: true }
    })
  ],
  view: new View({
    center: fromLonLat([116.4074, 39.9042]), // 北京坐标
    zoom: 10
  })
})

2. 地图实例管理(mapInstance.js)

全局地图实例管理:

// mapInstance.js
export let mapInstance = null

export function setMap(map) {
  mapInstance = map
}

export function getMap() {
  return mapInstance
}

使用方式:

import { mapInstance, setMap } from './mapInstance.js'

// 设置地图实例
setMap(map)

// 在其他地方使用
const map = mapInstance
map.addLayer(newLayer)

3. 坐标转换

OpenLayers 默认使用 EPSG:3857 投影,需要进行坐标转换:

import { fromLonLat, toLonLat } from 'ol/proj'

// 经纬度转投影坐标
const mapCoord = fromLonLat([116.4074, 39.9042])

// 投影坐标转经纬度
const lonLat = toLonLat([12914834, 4865942])

二、GeoJSON 服务

1. GeoJSON 数据提取

从 AI 响应中提取 GeoJSON 数据:

// geoJsonService.js
export function extractGeoJsonFromResponse(responseText) {
  if (!responseText || typeof responseText !== 'string') return null

  try {
    let parsedData = JSON.parse(responseText)
    let geoJson = []
    
    if (parsedData?.data) {
      const content = parsedData.data
      const parsed = JSON.parse(content)
      const arr = Array.isArray(parsed) ? parsed : parsed != null ? [parsed] : []

      for (const item of arr) {
        const geom = item.geom
        geoJson.push(JSON.parse(geom))
      }
    }

    return geoJson
  } catch (error) {
    console.warn('提取GeoJSON时出错:', error)
    return null
  }
}

2. 在地图上显示 GeoJSON

import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import GeoJSON from 'ol/format/GeoJSON'
import { Style, Stroke, Fill, Circle } from 'ol/style'

export function displayGeoJsonOnMap(geoJsonData) {
  if (!geoJsonData || !mapInstance) {
    console.warn('无法显示GeoJSON: 缺少数据或地图实例')
    return null
  }

  // 标准化为 FeatureCollection
  const featureCollection = normalizeToFeatureCollection(geoJsonData)
  
  // 创建矢量数据源
  const vectorSource = new VectorSource({
    features: new GeoJSON().readFeatures(featureCollection, {
      featureProjection: 'EPSG:3857',
      dataProjection: 'EPSG:4326'
    })
  })

  // 创建矢量图层
  const vectorLayer = new VectorLayer({
    source: vectorSource,
    style: createHighlightStyle(),
    properties: { 
      name: 'geojson-highlight',
      isGeoJsonLayer: true,
      timestamp: Date.now()
    }
  })

  // 添加到地图
  mapInstance.addLayer(vectorLayer)

  // 添加闪烁动画
  addBlinkingAnimation(vectorLayer)

  // 调整视图
  fitViewToGeoJson(vectorSource)

  return vectorLayer
}

3. 高亮样式

function createHighlightStyle() {
  return new Style({
    stroke: new Stroke({
      color: '#ff4444',
      width: 3,
      lineDash: [5, 5]
    }),
    fill: new Fill({
      color: 'rgba(255, 68, 68, 0.2)'
    }),
    image: new Circle({
      radius: 8,
      stroke: new Stroke({
        color: '#ff4444',
        width: 3
      }),
      fill: new Fill({
        color: 'rgba(255, 68, 68, 0.3)'
      })
    })
  })
}

4. 闪烁动画效果

function addBlinkingAnimation(layer) {
  let opacity = 1
  let direction = -0.1
  
  const animate = () => {
    opacity += direction
    if (opacity <= 0.3) {
      opacity = 0.3
      direction = 0.1
    } else if (opacity >= 1) {
      opacity = 1
      direction = -0.1
    }
    
    layer.setOpacity(opacity)
    
    // 持续闪烁5秒
    if (Date.now() - layer.get('timestamp') < 5000) {
      requestAnimationFrame(animate)
    } else {
      layer.setOpacity(1)
    }
  }
  
  requestAnimationFrame(animate)
}

5. 清除 GeoJSON 图层

export function clearGeoJsonLayers() {
  if (!mapInstance) return

  const layersToRemove = []
  mapInstance.getLayers().forEach(layer => {
    if (layer.get('isGeoJsonLayer')) {
      layersToRemove.push(layer)
    }
  })

  layersToRemove.forEach(layer => {
    mapInstance.removeLayer(layer)
  })
}

三、Dify AI 客户端

1. 发送聊天消息

支持流式和普通两种响应模式:

// difyClient.js
export async function sendChatMessage(query, opt = {}) {
  const {
    onMessage = () => {},        // 流式增量文本回调
    onCompleted = () => {},      // 完成回调
    onError = () => {},          // 错误回调
    onGeoJsonDetected = () => {},// GeoJSON 检测回调
    abortSignal,                 // 取消信号
    inputs = {},                 // 自定义变量
    stream = true,               // 是否使用流式
    user = 'web-user'            // 用户标识
  } = opt

  const url = BASE_URL + '/v1/chat-messages'
  const body = {
    inputs,
    query,
    response_mode: stream ? 'streaming' : 'blocking',
    user
  }

  // 流式处理
  if (stream) {
    const resp = await fetch(url, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body),
      signal: abortSignal
    })
    
    const reader = resp.body.getReader()
    const decoder = new TextDecoder('utf-8')
    let buffer = ''
    let fullResponseText = ''
    
    while (true) {
      const { value, done } = await reader.read()
      if (done) break
      buffer += decoder.decode(value, { stream: true })
      
      // SSE 解析
      // ...处理流式数据
    }
  }
}

2. 使用示例

import { sendChatMessage } from './services/difyClient'

// 发送消息
sendChatMessage('查询北京市水系', {
  onMessage: (chunk) => {
    // 处理增量文本
    console.log('收到:', chunk)
  },
  onCompleted: () => {
    console.log('完成')
  },
  onError: (err) => {
    console.error('错误:', err)
  },
  onGeoJsonDetected: (geoJson) => {
    // 自动在地图上显示
    displayGeoJsonOnMap(geoJson)
  }
})

3. 取消请求

const controller = new AbortController()

sendChatMessage('查询...', {
  abortSignal: controller.signal,
  // ...其他选项
})

// 取消请求
controller.abort()

四、GeoServer 集成

1. REST API 封装

// GeoServerRestApi.js
import axios from 'axios'

const geoserverApi = axios.create({
  baseURL: '/geoserver',
  timeout: 10000
})

// 获取服务信息
export async function getAbout() {
  const response = await geoserverApi.get('/rest/about/status.json')
  return response.data
}

// 获取图层列表
export async function getLayers() {
  const response = await geoserverApi.get('/rest/layers.json')
  return response.data
}

2. 图层管理

// Layers.js
export async function getLayerList() {
  const data = await getLayers()
  return data.layers?.layer || []
}

export async function getLayerInfo(layerName) {
  const response = await geoserverApi.get(`/rest/layers/${layerName}.json`)
  return response.data
}

五、面板组件

1. 图层面板(LayerPanel.vue)

功能:

2. AI 助手面板(AIAssistantPanel.vue)

功能:

3. 可调整布局

// App.vue 中的布局调整逻辑
const startVDrag = (e) => {
  isDraggingV.value = true
  startY.value = e.clientY
  startHeight.value = topHeight.value
  document.body.classList.add('resizing-v')
}

const startHDrag = (e) => {
  isDraggingH.value = true
  startX.value = e.clientX
  startLeftWidth.value = leftWidth.value
  document.body.classList.add('resizing-h')
}

布局状态自动保存:

// 保存到 localStorage
localStorage.setItem('layerPanelHeight', String(topHeight.value))
localStorage.setItem('leftPanelWidth', String(leftWidth.value))

// 读取保存的状态
const storedHeight = parseInt(localStorage.getItem('layerPanelHeight') || '0', 10)
const storedWidth = parseInt(localStorage.getItem('leftPanelWidth') || '0', 10)

六、开发工具

GeoJSON 测试工具

开发模式下自动加载的测试工具:

// geoJsonTestUtils.js
// 在控制台可用的测试方法

// 测试 GeoJSON 渲染
window.testGeoJson = (geoJson) => {
  displayGeoJsonOnMap(geoJson)
}

// 清除测试图层
window.clearGeoJsonLayers = () => {
  clearGeoJsonLayers()
}

// 测试数据
window.testData = {
  point: {
    type: 'Point',
    coordinates: [116.4074, 39.9042]
  },
  polygon: {
    type: 'Polygon',
    coordinates: [[[116.3, 39.8], [116.5, 39.8], [116.5, 40.0], [116.3, 40.0], [116.3, 39.8]]]
  }
}

使用方式:

// 浏览器控制台
window.testGeoJson(window.testData.point)
window.testGeoJson(window.testData.polygon)
window.clearGeoJsonLayers()