开发一个前后端分离的webgis城市共享单车投放管理系统(3)

文章同步更新于我的个人博客:松果猿的博客,欢迎访问获取更多技术分享。

同时,您也可以关注我的微信公众号:松果猿的代码工坊,获取最新文章推送和编程技巧。

前言

上期文章我们实现了绘制功能和高亮弹窗显示,现在我们来开发地点标注功能

前端界面搭建

大致想一下UI界面:

好吧有点简陋,不过无所谓了,切图仔开工!

将上图的@/components/MapControl.vue中的菜单栏对象改为下图,上图只是测试效果的假数据,这次我们将地点标注功能放在在这里

新建@/components/Container.vue,这里标签逻辑部分用的ts语法,写法规范而且方便纠错

vue 复制代码
<template>
  <div class="container">
    <div class="input">
      <el-input style="width:300px" v-model="inputValue" placeholder="请输入地点标注名称" />
    </div>
    <div>
      <el-button-group>
    <el-button style="width:100px" type="primary">保  存</el-button>
    <el-button style="width:100px" type="primary">
      退  出
    </el-button>
  </el-button-group>
    </div>

    <div class="table">
      <el-table :data="tableData" style="width: 100%">
        <el-table-column label="Id" width="50">
          <template #default="scope">
            <div style="display: flex; align-items: center">
              <el-icon><timer /></el-icon>
              <span>{{ scope.row.id }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="Name" width="100">
          <template #default="scope">
            <div style="display: flex; align-items: center">
              <el-icon><timer /></el-icon>
              <span >{{ scope.row.name }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="Type" width="80">
          <template #default="scope">
            <el-popover
              effect="light"
              trigger="hover"
              placement="top"
              width="auto"
            >
              <template #reference>
                <el-tag>{{ scope.row.type }}</el-tag>
              </template>
            </el-popover>
          </template>
        </el-table-column>
        <el-table-column label="Operations" width="150">
          <template #default="scope">
            <el-button
              size="small"
              @click="handleEdit(scope.$index, scope.row)"
            >
              定位
            </el-button>
            <el-button
              size="small"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)"
            >
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const inputValue = ref("");

interface PointLabel {
  id: string;
  name: string;
  type: string;
}

const handleEdit = (index: number, row: PointLabel) => {
  console.log(index, row);
};
const handleDelete = (index: number, row: PointLabel) => {
  console.log(index, row);
};

const tableData: PointLabel[] = [
  {
    id: "1",
    name: "地点1",
    type: "point",
  },

  {
    id: "2",
    name: "地点2",
    type: "point",
  },
  {
    id: "3",
    name: "地点3",
    type: "point",
  },
];
</script>

<style scoped lang="scss">
.container {
  position: absolute;
  bottom: 200px;
  right: 20px;
  width: 400px;
  height:350px;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  align-items: center;
  padding: 20px;
  border-radius: 10px;
  background-color: #fff;
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(0, 0, 0, 0.1);
}
</style>

引入

界面如下:

再通过组件通信控制显示退出

子组件通过defineEmits注册一个自定义事件,而后触发emit去调用该自定义事件,并传递参数给父组件

@/components/MapControl.vue

@/components/Container.vue

现在来写后端

开发后端接口

创建PostgreSQL数据库和表:这里不再赘述,不然篇幅太长,网上都有教程

sql

sql 复制代码
CREATE TABLE locations (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    type VARCHAR(255) NOT NULL,
    lon DOUBLE PRECISION NOT NULL,
    lat DOUBLE PRECISION NOT NULL
);

我之前写过一篇创建springboot初始项目的文章,我们直接把创建好的初始化项目拿过来:

总览一下项目目录结构:

css 复制代码
src
├── main
│   ├── java
│   │   └── com
│   │       └── beson
│   │           ├── DemoApplication.java
│   │           ├── controller
│   │               └── LocationController.java
│   │           ├── mapper
│   │           │   └── LocationMapper.java
│   │           ├── model
│   │           │   └── Location.java
│   │           └── service
│   │               └── LocationService.java
│   └── resources
│       ├── application.yml
│       ├── mapper
│       │   └── LocationMapper.xml
│       ├── static
└── test
    └── java
        └── com
            └── beson
                └── DemoApplicationTests.java

pom.xml添加MyBatis和PostgreSQL的依赖:

xml 复制代码
   <!-- Spring Boot Starter MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>

    <!-- PostgreSQL JDBC Driver -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.23</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

application.yml中配置PostgreSQL数据源:

yaml 复制代码
# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/your_database
    username: your_username
    password: your_password
    driver-class-name: org.postgresql.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.beson.model

创建实体类来映射数据库表:

java 复制代码
package com.beson.model;

import lombok.Data;

@Data
public class Location {
    private Long id;
    private String name;
    private String type;
    private Double lon;
    private Double lat;
}
LocationController.java
java 复制代码
package com.beson.controller;

import com.beson.model.Location;
import com.beson.service.LocationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/locations")
public class LocationController {

    @Autowired
    private LocationService locationService;

    @GetMapping
    public List<Location> getAllLocations() {
        return locationService.getAllLocations();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Location> getLocationById(@PathVariable Long id) {
        Location location = locationService.getLocationById(id);
        return location != null ? ResponseEntity.ok(location) : ResponseEntity.notFound().build();
    }

    @PostMapping
    public ResponseEntity<Void> createLocation(@RequestBody Location[] locations) {
        for (Location location : locations) {
            locationService.createLocation(location);
        }
        return ResponseEntity.ok().build();
    }

    @PutMapping("/{id}")
    public ResponseEntity<Void> updateLocation(@PathVariable Long id, @RequestBody Location locationDetails) {
        Location location = locationService.getLocationById(id);
        if (location != null) {
            location.setName(locationDetails.getName());
            location.setType(locationDetails.getType());
            location.setLon(locationDetails.getLon());
            location.setLat(locationDetails.getLat());
            locationService.updateLocation(location);
            return ResponseEntity.ok().build();
        }
        return ResponseEntity.notFound().build();
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteLocation(@PathVariable Long id) {
        locationService.deleteLocation(id);
        return ResponseEntity.noContent().build();
    }
}
LocationMapper.java
java 复制代码
@Mapper
public interface LocationMapper {

    @Select("SELECT * FROM locations")
    List<Location> findAll();

    @Select("SELECT * FROM locations WHERE id = #{id}")
    Location findById(Long id);

    @Insert("INSERT INTO locations (name, type, lon, lat) VALUES (#{name}, #{type}, #{lon}, #{lat})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insert(Location location);

    @Update("UPDATE locations SET name = #{name}, type = #{type}, lon = #{longitude}, lat = #{latitude} WHERE id = #{id}")
    void update(Location location);

    @Delete("DELETE FROM locations WHERE id = #{id}")
    void delete(Long id);
}
LocationService.java
java 复制代码
package com.beson.service;

import com.beson.mapper.LocationMapper;
import com.beson.model.Location;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class LocationService {

    @Autowired
    private LocationMapper locationMapper;

    public List<Location> getAllLocations() {
        return locationMapper.findAll();
    }

    public Location getLocationById(Long id) {
        return locationMapper.findById(id);
    }

    public void createLocation(Location location) {
        locationMapper.insert(location);
    }

    public void updateLocation(Location location) {
        locationMapper.update(location);
    }

    public void deleteLocation(Long id) {
        locationMapper.delete(id);
    }
}

连接数据库

全局同源策略配置:

WebConfig.java

java 复制代码
package com.beson.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("localhost:5173")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

启动成功:

前后端交互

地图逻辑代码不能全部写在OIMap.vue组件里,要不然代码太屎山,我们写一个自定义hooks用于地点标记的相关操作。

不过在此之前,我们需要把map对象存储在pinia中,方便在自定义hooks中调用(一开始我把创建后的map对象存储在pinia里,结果后面写hooks使用时有问题,困恼我好久,然后才知道map的存储有问题,后面换了方式,直接将pinia里的响应式对象作为map对象调用,如下)

将我们之前存储绘制mapStore.js中添加一个map响应数据

然后将OlMap.vue中的所有map对象改为如下所示:

新建@/stores/markerStore.js用于管理新增的地点和从后端获取的标记数据

javascript 复制代码
import { defineStore } from "pinia";
import { transform } from 'ol/proj';

export const useMarkerStore = defineStore("marker", {
  state: () => ({
    // 新增的标记点
    newMarkers: [],
    // 新增的单个标记点
    newMarker: {
      name: "",
      lon: "",
      lat: "",
      type: ""
    },
    // 标记点数据
    markers: [],
  }),
  actions: {
    addMarker() {
      const markerCopy = {
        name: this.newMarker.name,
        lon: this.newMarker.lon,
        lat: this.newMarker.lat,
        type: this.newMarker.type
      };
      this.newMarkers.push(markerCopy);
    },
    setMarkerName(name) {
      this.newMarker.name = name;
    },
    setMarkerType(type) {
      this.newMarker.type = type;
    },
    setMarkerCoordinate(lon, lat) {
      const [transformedLon, transformedLat] = transform(
        [lon, lat],
        'EPSG:3857',
        'EPSG:4326'
      )
      this.newMarker.lon = transformedLon
      this.newMarker.lat = transformedLat
    },

  },
});

新建useMapMarkers.js用于实现地点标记的相关逻辑

javascript 复制代码
import { ref } from "vue";
import axios from "axios";
import { transform } from "ol/proj";
import { Point } from "ol/geom";
import { Feature } from "ol";
import { Style, Icon, Text, Fill, Stroke } from "ol/style";
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import markerIcon from "@/assets/img/marker.png";
import { useMarkerStore } from "@/stores/markerStore.js";
import { useMapStore } from "@/stores/mapStore.js";
import { ElMessage } from "element-plus";
import add_markerIcon from "@/assets/img/add_marker.png";
export function useMapMarkers() {
  const markerStore = useMarkerStore();
  const mapStore = useMapStore();
  // 标记图层
  const markerLayer = ref(null);

  // 初始化标记图层
  const initMarkerLayer = () => {
    if (!mapStore.map) {
      console.error("Map is not initialized");
      return;
    }

    // 初始化标记图层
    const vectorSource = new VectorSource();
    markerLayer.value = new VectorLayer({
      source: vectorSource,
    });

    mapStore.map.addLayer(markerLayer.value);
  };

  // 获取标记点数据
  const fetchMarkers = async () => {
    // 清除之前的图层
    if (markerLayer.value) {
      mapStore.map.removeLayer(markerLayer.value);
    }

    initMarkerLayer();
    try {
      const response = await axios.get("http://localhost:8080/locations");
      markerStore.markers = response.data;
      renderMarkers();
    } catch (err) {
      ElMessage.error("获取标记点数据失败:" + err.message);
    }
  };

  // 渲染标记点
  const renderMarkers = () => {
    if (!markerLayer.value) return;

    // 清空标记图层
    const source = markerLayer.value.getSource();
    source.clear();

    // 渲染标记点
    markerStore.markers.forEach((marker) => {
      const coordinate = transform(
        [parseFloat(marker.lon), parseFloat(marker.lat)],
        "EPSG:4326",
        "EPSG:3857"
      );

      const feature = new Feature({
        geometry: new Point(coordinate),
        properties: marker,
      });

      // 设置标记点样式,包含图标和文本
      feature.setStyle(
        new Style({
          image: new Icon({
            width: 30,
            src: markerIcon,
            anchor: [0.5, 1],
          }),
          text: new Text({
            text: marker.name || "", // 显示地点名称
            offsetY: 15, // 文本垂直偏移量
            fill: new Fill({
              color: "#2D3436", // 深色文本
            }),
            font: "bold 14px sans-serif", // 加粗字体
            stroke: new Stroke({  // 添加文字描边
              color: '#FFFFFF',
              width: 3
            }),
            padding: [5, 5, 5, 5],  // 文本内边距
            textBaseline: 'bottom',
            textAlign: 'center'
          }),
        })
      );

      source.addFeature(feature);
    });
  };

  // 删除标记点
  const removeMarker = async (markerId) => {
    try {
      const response = await axios.delete(
        `http://localhost:8080/locations/${markerId}`
      );
      if (response.status === 200) {
        // 从 store 中移除该标记点
        markerStore.markers = markerStore.markers.filter(
          (marker) => marker.id !== markerId
        );
        // 更新图层
        if (markerLayer.value) {
          mapStore.map.removeLayer(markerLayer.value);
        }
        await fetchMarkers();
        return response;
      }
    } catch (error) {
      throw error;
    }
  };
  //点击事件
  const clickEvent = (evt) => {
    const coordinate = evt.coordinate;

    // 创建新的要素
    const feature = new Feature({
      geometry: new Point(coordinate),
    });

    // 设置要素样式
    feature.setStyle(
      new Style({
        image: new Icon({
          width: 30,
          src: add_markerIcon,
          anchor: [0.5, 1],
        }),
        text: new Text({
          text: markerStore.newMarker.name || "", // 显示地点名称
          offsetY: 15, // 文本垂直偏移量
          fill: new Fill({
            color: "#2D3436",
          }),
          font: "bold 14px sans-serif",
          stroke: new Stroke({
            color: '#FFFFFF',
            width: 3
          }),
          padding: [5, 5, 5, 5],
          textBaseline: 'bottom',
          textAlign: 'center'
        }),
      })
    );

    // 添加到标记图层
    const source = markerLayer.value.getSource();
    source.addFeature(feature);

    markerStore.setMarkerCoordinate(coordinate[0], coordinate[1]);
    markerStore.addMarker();
  };

  // 添加新标记点的方法
  const addMarkers = () => {
    initMarkerLayer();
    if (!mapStore.map) {
      console.error("Map is not initialized");
      return;
    }
    mapStore.map.on("click", clickEvent);
  };

  // 添加保存标记点的方法
  const PostMarker = async () => {
    try {
      const response = await axios.post(
        "http://localhost:8080/locations",
        markerStore.newMarkers
      );
      if (response.status === 200) {
        ElMessage.success("标记点添加成功");
        markerStore.newMarkers = [];
        // 清除临时标记图层并重新获取所有标记点
        if (markerLayer.value) {
          mapStore.map.removeLayer(markerLayer.value);
        }
        await fetchMarkers();
      }
    } catch (error) {
      ElMessage.error("添加标记点失败:" + error.message);
    }
  };

  // 获取标记点信息
  const getMarkerInfo = (feature) => {
    if (!feature) return null;
    return feature.get("properties");
  };

  //取消点击事件
  const cancelClick = () => {
    mapStore.map.un("click", clickEvent);
  };

  return {
    initMarkerLayer,
    fetchMarkers,
    PostMarker,
    addMarkers,
    removeMarker,
    getMarkerInfo,
    cancelClick,
  };
}

在OlMap.vue中挂载钩子中添加如下方法以实现标记图层的渲染

再来对Container.vue进行调整,最终代码如下:

vue 复制代码
<template>
  <div class="container">
    <div class="input">
      <el-input
        style="width: 300px"
        v-model="nameValue"
        placeholder="请输入地点标注名称"
        @change="markerStore.setMarkerName(nameValue)"
      />
    </div>
    <div class="input">
      <el-input
        style="width: 300px"
        v-model="typeValue"
        placeholder="请输入地点类型"
        @change="markerStore.setMarkerType(typeValue)"
      />
    </div>
    <div>
      <el-button-group>
        <el-button style="width: 100px" type="primary" @click="handleSave"
          >保 存</el-button
        >
        <el-button style="width: 100px" type="primary" @click="handleRefresh">
          刷 新
        </el-button>
        <el-button style="width: 100px" type="primary" @click="handleExit">
          退 出
        </el-button>
      </el-button-group>
    </div>

    <div class="table">
      <el-table :data="tableData" class="table-data">
        <el-table-column label="Id" width="50">
          <template #default="scope">
            <div style="display: flex; align-items: center">
              <el-icon><timer /></el-icon>
              <span>{{ scope.row.id }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="Name" width="100">
          <template #default="scope">
            <div style="display: flex; align-items: center">
              <el-icon><timer /></el-icon>
              <span>{{ scope.row.name }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="Type" width="80">
          <template #default="scope">
            <el-popover
              effect="light"
              trigger="hover"
              placement="top"
              width="auto"
            >
              <template #reference>
                <el-tag>{{ scope.row.type }}</el-tag>
              </template>
            </el-popover>
          </template>
        </el-table-column>
        <el-table-column label="Operations" width="150">
          <template #default="scope">
            <el-button
              size="small"
              @click="handleLocate(scope.$index, scope.row)"
            >
              定位
            </el-button>
            <el-button
              size="small"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)"
            >
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUpdated } from "vue";
const nameValue = ref("");
const typeValue = ref("");
import { useMarkerStore } from "@/stores/markerStore.js";
import { useMapMarkers } from "@/hooks/useMapMarkers";
import { ElMessage } from "element-plus";

const markerStore = useMarkerStore();
const mapMarkers = useMapMarkers();

interface PointLabel {
  id: string;
  name: string;
  type: string;
}

const handleLocate = (index: number, row: PointLabel) => {
  console.log(index, row);
};
const handleDelete = async (index: number, row: PointLabel) => {
  try {
    await mapMarkers.removeMarker(row.id);
    await mapMarkers.fetchMarkers();
    tableData.value = markerStore.markers;
    ElMessage.success("删除成功");
  } catch (error) {
    ElMessage.error("删除失败");
  }
};

const tableData = ref<PointLabel[]>([]);

onMounted(() => {
  mapMarkers.addMarkers();
  tableData.value = markerStore.markers;
});
//保存操作
const handleSave = async () => {
  try {
    await mapMarkers.PostMarker();
    await mapMarkers.fetchMarkers();
    tableData.value = markerStore.markers;
    ElMessage.success("保存成功");
  } catch (error) {
    ElMessage.error("保存失败");
  }
};
//刷新操作
const handleRefresh = async () => {
  try {
    await mapMarkers.fetchMarkers();
    tableData.value = markerStore.markers;
    ElMessage.success("刷新成功");
  } catch (error) {
    ElMessage.error("刷新失败");
  }
};
//退出操作
const emit = defineEmits(["visible"]);
const handleExit = () => {
  mapMarkers.cancelClick();
  emit("visible", false);
  console.log("触发退出");
};
</script>

<style scoped lang="scss">
.container {
  position: absolute;
  bottom: 150px;
  right: 20px;
  width: 400px;
  height: 400px;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  align-items: center;
  padding: 20px;
  border-radius: 10px;
  background-color: #fff;
  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(0, 0, 0, 0.1);
  .table {
    width: 100%;
    height: 200px;
    overflow-y: scroll;
  }
}
</style>

这里的定位逻辑还没实现,等到后面再说啦

项目地址:github.com/songguo1/Sh...

相关推荐
类人_猿14 分钟前
ASP.NET Web(.Net Framework)POST无法正常接收数据
前端·asp.net·.net·post·post请求
城沐小巷33 分钟前
外卖点餐系统小程序
前端·后端·微信小程序
东方隐侠安全团队-千里1 小时前
网安瞭望台第6期 :XMLRPC npm 库被恶意篡改、API与SDK的区别
前端·网络·网络安全·npm·node.js
Answer_ism1 小时前
【CSS】一篇掌握CSS
前端·css·html
Layue000001 小时前
学习HTML第三十四天
前端·笔记·学习·html
浪潮行舟1 小时前
css:怎么设置div背景图的透明度为0.6不影响内部元素
前端·css
anyup_前端梦工厂1 小时前
FlyHttp 的诞生:从认识各种网络请求开始
前端·ajax·uni-app
nlp研究牲2 小时前
在无界面ubuntu服务器上配置chrome浏览器,结合undetected_chromedriver实现数据抓取!不使用sudo权限安装chrome浏览器!
运维·服务器·前端·人工智能·chrome
前端熊猫2 小时前
封装类与封装函数
开发语言·前端·javascript
前端加油站2 小时前
声明式与命令式 Modal 之争
前端·javascript·react.js