✨✨使用vue3打造一个el-form表单及高德地图的关联组件实例✨

news2024/11/24 8:35:44

✨1. 实现功能

  1. 🌟表单内显示省市县以及详细地址
    • 点击省市县输入框时,打开对应地图弹窗,进行位置选择
    • 选择位置回显入对应输入框
    • 表单内的省市县以及地址输入框同外嵌表单走相同的校验方式
    • 触发校验后点击reset实现清除校验与清空数据
  2. 🌟地图内展示地址搜索框以及地图坐标图
    • 搜索框展示当前经纬度地址
    • 搜索框可输入自定义地址,下拉菜单展示范围兴趣点和道路信息,点击可进行搜索
  3. 🌟 单独封装每个组件,使form-itemdialog以及amap三个组件可单独使用

✨2. 示例图

  1. 💖示例图1:💖
    在这里插入图片描述

  2. 💖💖示例图2:💖
    在这里插入图片描述

  3. 💖💖💖示例图3:💖
    在这里插入图片描述

  4. 💖💖💖💖示例图4:💖
    在这里插入图片描述

  5. 💖💖💖💖💖示例图5:💖
    在这里插入图片描述

✨3. 组件代码

🌹1. 组件目录结构

在这里插入图片描述

2. 🍗 🍖地图组件AmapContainer.vue
<template>
  <div v-loading="loading">
    <input type="text" class="address" v-model="iMap.address" id="inputAddress" />
    <div id="container"></div>
  </div>
</template>

<script setup lang="ts" name="AmapContainer">
import { ref, reactive, computed, watch, onMounted, onUnmounted } from "vue";
import AMapLoader from "@amap/amap-jsapi-loader";
import { AMAP_MAP_KEY, AMAP_SECRET_KEY } from "@/config";
import { getBrowserLang } from "@/utils";
import { useGlobalStore } from "@/stores/modules/global";
import { IMap } from "../interface/index";

const globalStore = useGlobalStore();
const language = computed(() => {
  if (globalStore.language == "zh") return "zh_cn";
  if (globalStore.language == "en") return "en";
  return getBrowserLang() == "zh" ? "zh_cn" : "en";
});

const loading = ref(true);

interface ExtendsWindow extends Window {
  _AMapSecurityConfig?: {
    securityJsCode: string;
  };
}
let _window: ExtendsWindow = window;

// 定义map实例
let map: any = null;

const iMap = reactive<IMap>({
  province: "",
  city: "",
  district: "",
  address: "",
  lnglat: [114.525918, 38.032612],
  canSubmit: true
});

watch(
  () => iMap.address,
  () => {
    iMap.canSubmit = !iMap.address;
  }
);

onMounted(() => {
  initMap();
});

onUnmounted(() => {
  map?.destroy();
});

// 初始化地图
const initMap = async () => {
  _window._AMapSecurityConfig = {
    securityJsCode: AMAP_SECRET_KEY // ❓高德秘钥👇👇下方会有👇👇
  };
  AMapLoader.load({
    key: AMAP_MAP_KEY, // ❓申请好的Web端开发者Key,首次调用 load 时必填👇👇下方会有👇👇
    version: "2.0", // ❓指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
    plugins: ["AMap.ToolBar", "AMap.Scale", "AMap.Marker", "AMap.Geocoder", "AMap.AutoComplete"] //需要使用的的插件列表
  })
    .then(AMap => {
      map = new AMap.Map("container", {
        // 设置地图容器id
        viewMode: "2D", // 是否为3D地图模式
        zoom: 11, // 初始化地图级别
        center: iMap.lnglat // 初始化地图中心点位置
      });
      //创建工具条插件实例
      const toolbar = new AMap.ToolBar({
        position: {
          top: "110px",
          right: "40px"
        }
      });
      map.addControl(toolbar);

      //创建比例尺插件实例
      const Scale = new AMap.Scale();
      map.addControl(Scale);

      //创建标记插件实例
      const Marker = new AMap.Marker({
        position: iMap.lnglat
      });
      map.addControl(Marker);

      //创建地理编码插件实例
      const Geocoder: any = new AMap.Geocoder({
        radius: 1000, //以已知坐标为中心点,radius为半径,返回范围内兴趣点和道路信息
        extensions: "base", //返回地址描述以及附近兴趣点和道路信息,默认“base | all”
        lang: language.value
      });

      //返回地理编码结果
      Geocoder.getAddress(iMap.lnglat, (status, result) => {
        if (status === "complete" && result.info === "OK") {
          iMap.province = result.regeocode.addressComponent.province;
          iMap.city = result.regeocode.addressComponent.city;
          iMap.district = result.regeocode.addressComponent.district;
          iMap.address = result.regeocode.formattedAddress;
          AutoComplete.setCity(iMap.address);
          loading.value = false;
        }
      });
      // 根据输入关键字提示匹配信息
      const AutoComplete = new AMap.AutoComplete({
        input: "inputAddress",
        city: iMap.address,
        datatype: "all",
        lang: language.value
      });

      AutoComplete.on("select", result => {
        iMap.lnglat = [result.poi.location.lng, result.poi.location.lat];
        setPointOrAddress();
      });

      //点击地图事件
      map.on("click", e => {
        iMap.lnglat = [e.lnglat.lng, e.lnglat.lat];
        setPointOrAddress();
      });

      // 设置地图点坐标与位置交互
      const setPointOrAddress = () => {
        Marker.setPosition(iMap.lnglat);
        map.setCenter(iMap.lnglat);
        map.setZoom(12);
        Geocoder.getAddress(iMap.lnglat, (status, result) => {
          if (status === "complete" && result.info === "OK") {
            iMap.province = result.regeocode.addressComponent.province;
            iMap.city = result.regeocode.addressComponent.city;
            iMap.district = result.regeocode.addressComponent.district;
            iMap.address = result.regeocode.formattedAddress;
          }
        });
      };
    })
    .catch(e => {
      console.log(e);
    });
};

defineExpose({
  iMap
});
</script>

<style scoped lang="scss">
@import "../index.scss";
</style>

<style lang="scss">
.amap-sug-result {
  z-index: 10000;
}
</style>

🍀3. 弹窗组件AmapDialog.vue 🍀
<template>
  <el-dialog :model-value="visible" title="请选择" width="800" :before-close="handleClose">
    <AmapContainer ref="amapContainer" />
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" :disabled="amapContainer?.iMap?.canSubmit" @click="handleConfirm">确认</el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script setup lang="ts" name="AmapExplore">
/*🔻
  // 使用方式
  // ❤️amapFlag: 控制弹窗显隐
  // ❤️iMap必须ref定义, 接收选择地址数据
  // 示例:
  // 💥<AmapExplore v-model:visible="amapFlag" v-model:amap="iMap" />💥
*/ 🔺 
import { ref, withDefaults } from "vue";
import { IAddress } from "../interface/index";
import AmapContainer from "./AmapContainer.vue";

withDefaults(
  defineProps<{
    visible: boolean;
    amap: Partial<IAddress>;
  }>(),
  {
    visible: false
  }
);

const amapContainer = ref();

// 定义emits
const emits = defineEmits<{
  "update:amap": [value: IAddress];
  "update:visible": [value: boolean];
}>();

const handleConfirm = () => {
  // delete amapContainer.value?.iMap?.canSubmit;
  emits("update:amap", amapContainer.value?.iMap);
  handleClose();
};

const handleClose = () => {
  emits("update:visible", false);
};
</script>

<style scoped lang="scss"></style>

🌼4. 表单组件AmapExplore/index.vue 🌼
<template>
  <el-row :gutter="gutter" :style="gutterStyle">
    <el-col :span="8">
      <el-form-item prop="province">
        <el-input
          v-model="iMapForm.province"
          ref="provinceRef"
          placeholder="省"
          size="large"
          style="width: 100%"
          @click="handleAmapChange"
        ></el-input>
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item prop="city">
        <el-input
          v-model="iMapForm.city"
          ref="cityRef"
          placeholder="市"
          size="large"
          style="width: 100%"
          @click="handleAmapChange"
        ></el-input>
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item prop="district">
        <el-input
          v-model="iMapForm.district"
          ref="districtRef"
          placeholder="县"
          size="large"
          style="width: 100%"
          @click="handleAmapChange"
        ></el-input>
      </el-form-item>
    </el-col>
  </el-row>
  <el-col :span="24">
    <el-form-item prop="address">
      <el-input v-model="iMapForm.address" placeholder="请输入详细地址" size="large" style="width: 100%"></el-input>
    </el-form-item>
  </el-col>
  <AmapDialog v-model:visible="amapFlag" v-model:amap="iMapForm" />
</template>

<script setup lang="ts" name="AmapExplore">
import { ref, reactive, watch, inject, watchEffect } from "vue";
import type { FormRules } from "element-plus";
import { IAddress } from "./interface/index";
import AmapDialog from "./components/AmapDialog.vue";

// 栅格间隔与样式
const gutter = 20;
const gutterStyle = {
  width: `calc(100% + ${gutter}px)`,
  "margin-bottom": `${gutter}px`
};

// 接收传入的formData和formRules
const { ruleForm, rules } = inject<{ ruleForm: Object; rules: any }>("aMap", { ruleForm: reactive({}), rules: reactive({}) });

const iMapForm = ref<IAddress>({
  province: "",
  city: "",
  district: "",
  address: "",
  lnglat: []
});

// 若地址有值,则赋予formData
watch(
  () => iMapForm,
  n => {
    // 为防止重复赋值
    if (n.value.province || n.value.city || n.value.district || n.value.address) {
      Object.assign(ruleForm, { ...iMapForm.value });
    }
  },
  {
    deep: true
  }
);

// 另处理经纬度lnglat
watch([() => iMapForm.value.province, () => iMapForm.value.city, () => iMapForm.value.district], n => {
  if (n.some(item => !item)) {
    iMapForm.value.lnglat = [];
  }
});

watch(
  () => iMapForm.value.lnglat,
  n => {
    if (!n.length) {
      Object.assign(ruleForm, iMapForm.value);
    }
  }
);

// 将formData赋值给iMapForm-主要作用为清空重置
watchEffect(() => {
  Object.assign(iMapForm.value, { ...ruleForm });
});

// form校验;
const iMapRules = reactive<FormRules<IAddress>>({
  province: [{ required: true, message: "请选择省", trigger: ["blur", "change"] }],
  city: [{ required: true, message: "请选择市", trigger: ["blur", "change"] }],
  district: [{ required: true, message: "请选择区、县", trigger: ["blur", "change"] }],
  address: [{ required: true, message: "请输入详细地址", trigger: ["blur", "change"] }]
});

// 合并校验数据;
watch(rules, () => Object.assign(rules, { ...iMapRules }), {
  immediate: true,
  deep: true
});

// 地图弹窗
const amapFlag = ref<boolean>(false);
const provinceRef = ref();
const cityRef = ref();
const districtRef = ref();
const handleAmapChange = () => {
  amapFlag.value = true;
  provinceRef.value.blur();
  cityRef.value.blur();
  districtRef.value.blur();
};
</script>

<style scoped lang="scss"></style>

5. 🌿scss文件 🌿
// AmapContainer
.address {
  box-sizing: border-box;
  width: 100%;
  height: 30px;
  padding: 0 12px;
  margin-bottom: 10px;
  line-height: 30px;
  border: 1px solid #ececec;
  border-radius: 4px;
}
#container {
  width: 100%;
  height: 400px;
  padding: 0;
  margin: 0;
}
🌴6. 类型定义interface/index.ts 🌴
export interface IAddress {
  province: string;
  city: string;
  district: string;
  address: string;
  lnglat: number[];
}
export interface IMap extends IAddress {
  canSubmit: boolean;
}

❕ ❕7. 地图组件内使用的高德AMAP_MAP_KEY和秘钥AMAP_SECRET_KEY可以自行设置

// 高德地图 key
export const AMAP_MAP_KEY: string = "****";
// 高德地图 安全密钥
export const AMAP_SECRET_KEY: string = "*****";

✨4. 父组件使用😎

  1. ☝️ 使用组件

<!-- 1. 使用组件 -->
<AmapExplore />
  1. ✌️使用provide向后代传入表单数据formData)和校验规则formRules

// 2. 传入formData和formRules
provide("aMap", { ruleForm, rules });
  1. 👋完整代码示例:
<template>
  <div class="card amap-example">
    <el-form ref="ruleFormRef" :model="ruleForm" :rules label-width="auto" style="max-width: 600px">
      <el-form-item label="Activity name" prop="name">
        <el-input v-model="ruleForm.name" />
      </el-form-item>
      <el-form-item label="地址" required>
        <!-- 1. 使用组件 -->
        <AmapExplore />
      </el-form-item>
      <el-form-item label="备注" prop="remark">
        <el-input v-model="ruleForm.remark" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submitForm(ruleFormRef)"> Create </el-button>
        <el-button @click="resetForm(ruleFormRef)">Reset</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts" name="amapExample">
import { reactive, ref, provide } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import AmapExplore from "@/components/AmapExplore/index.vue";

interface RuleForm {
  name: string;
  remark: string;
}
const ruleFormRef = ref<FormInstance>();
let ruleForm = reactive<RuleForm>({
  name: "",
  remark: ""
});

let rules = reactive<FormRules<RuleForm>>({
  name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
  remark: [{ required: true, message: "请输入备注", trigger: "blur" }]
});

// 2. 传入formData和formRules
provide("aMap", { ruleForm, rules });

const submitForm = async (formEl: FormInstance | undefined) => {
  console.log(ruleForm, "s");
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      console.log("submit!");
    } else {
      console.log("error submit!", fields);
    }
  });
};

const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.resetFields();
};
</script>

❗️ 5. 封装实例缺点💦

  1. 当选择地址之后,再次打开地图弹窗,更改地图标记点,地址会实时变更,
  2. 不论点击取消还是确认按钮,都会改变表单内部值
  3. 💢初始不会出现此问题💢
  4. 💪后续会改进😁😁😁😁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1666806.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web开发小知识点(二)

1.关于取余 我在Dart语言里&#xff08;flutter项目&#xff09; int checkNum (10 - 29) % 10; 那么checkNum等于1 但是在Vue项目里 const checkNum (10 - 29) % 10;却等于-9 语言的特性不同&#xff0c;导致结果也不同&#xff0c;如果要想和Dart保持一致&#xff0c;…

Task Office for Mac v9.0激活版:任务管理新境界

还在为繁琐的任务管理而烦恼吗&#xff1f;Task Office for Mac为您带来全新的任务管理体验。简洁明了的界面设计&#xff0c;让您轻松上手&#xff1b;强大的任务管理和项目管理功能&#xff0c;让您轻松掌握任务进度&#xff1b;多用户协作功能&#xff0c;让团队协作更加高效…

自定义实现 Java17+SpringBoot3+OpenAPI+Knife4j Starter

文章目录 前言正文1 创建starter项目1.1 依赖文件1.2 配置信息 2 自定义starer代码开发2.1 配置字段的定义2.2 自动配置类 3 验证starter3.1 测试项目的配置3.2 功能配置 application.yml3.3 测试代码3.3.1 实体类3.3.2 控制器13.3.2 控制器2 4 效果展示4.1 主页4.2 实体类列表…

day08-面向对象高级

1.代码块 1.1代码块引出 有时我们在使用构造方法时&#xff0c;除了进行属性的初始化外还需要使用一些其他的语句&#xff0c;以便更好的实现程序的功能&#xff0c;比如添加一些输出语句&#xff1b; 1.2 局部代码块 public void show(){System.out.println("show&quo…

Line Buffer概述

buffer在芯片物理上一般指的是SRAM&#xff0c;也可以指寄存器组。buffer的作用是用来在逻辑芯片上暂时存储数据&#xff0c;但不会是大量的数据。如果是大量数据一般会使用DRAM&#xff08;典型的指DDR&#xff09;作为存储芯片&#xff0c;用来存储大密度数据。line buffer可…

物流单打印机怎么调格式距离,佳易王物流托运单管理系统软件打印单据左边距调节教程

物流单打印机怎么调格式距离&#xff0c;佳易王物流托运单管理系统软件打印单据左边距调节教程 一、前言 以下软件操作教程以&#xff0c;佳易王物流单打印管理软件为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 1、佳易王物流单管理系统打印…

先进电机技术 —— 控制策略综述

一、先进电机控制策略综述 电机控制策略随着电力电子技术和微处理器技术的发展而日趋丰富和完善&#xff0c;各种先进的控制方法被广泛应用于直流电动机、交流电动机&#xff08;同步电机、感应电机&#xff09;等多种电机类型。下面是对几种主要先进电机控制策略的概述&#x…

ExcelVBA在选择区域(有合并)中删除清除空行

【问题】 关于删除空行&#xff0c;以前是用函数来完成工作的&#xff0c; 今天有人提出问题&#xff0c;传来这个文件&#xff0c; 现有数据&#xff0c;1w多行&#xff0c;其中有部分列有不同合并单元格&#xff0c;跨行也不一样。如果要进行筛选删除空行&#xff0c;有一定的…

工程师工具箱系列(1)MapStruct

文章目录 工程师工具箱系列&#xff08;1&#xff09;MapStruct芸芸众生初窥门径引入POM依赖创建转换器与方法进行使用IDEA好基友 游刃有余示例说明避免编写重复转换器实现复杂灵活转换 温故知新 工程师工具箱系列&#xff08;1&#xff09;MapStruct 芸芸众生 在Java项目开发…

2024年深圳市教师招聘报名流程(建议电脑)

2024年深圳市教师招聘报名流程&#xff08;建议电脑&#xff09; #深圳教师招聘 #深圳教招 #深圳教师招聘考试 #教师招聘报名照片处理 #深圳教师招聘笔试

法语语式与时态总结,柯桥零基础学法语

常用语式 法语中的常用语式分为&#xff1a;直陈式、条件式、虚拟式、命令式、不定式与分词式。 直陈式&#xff08;lindicatif&#xff09;初学法语时首先就要学直陈式&#xff0c;也是最常用的语式&#xff0c;表示确实发生的动作。 条件式&#xff08;le conditionnel&am…

动态规划----股票买卖问题(详解)

目录 一.买卖股票的最佳时机&#xff1a; 二.买卖股票的最佳时机含冷冻期&#xff1a; 三.买卖股票的最佳时期含⼿续费&#xff1a; 四.买卖股票的最佳时机III: 五.买卖股票的最佳时机IV: 买卖股票的最佳时机问题介绍&#xff1a;动态规划买卖股票的最佳时机是一个经典的…

LeetCode 题目 119:杨辉三角 II

作者介绍&#xff1a;10年大厂数据\经营分析经验&#xff0c;现任字节跳动数据部门负责人。 会一些的技术&#xff1a;数据分析、算法、SQL、大数据相关、python&#xff0c;欢迎探讨交流 欢迎加入社区&#xff1a;码上找工作 作者专栏每日更新&#xff1a; LeetCode解锁1000题…

Windows11“重置此电脑”后,Edge浏览器在微软应用商店显示“已安装”,但是开始菜单搜索不到的解决办法

Windows11“重置此电脑”后&#xff0c;Edge浏览器在微软应用商店显示“已安装”&#xff0c;但是开始菜单搜索不到的解决办法 为什么重新使用Edge&#xff1f;问题描述不该更新可用更新问过AI&#xff08;通义千问&#xff09;&#xff0c;并且AI提供方法全都无效。现象 操作步…

python3如何安装bs4

在python官网找到beautifulsoup模块的下载页面&#xff0c;点击"downloap"将该模块的安装包下载到本地。 将该安装包解压&#xff0c;然后在打开cmd&#xff0c;并通过cmd进入到该安装包解压后的文件夹目录下。 在该文件目录下输入"python install setup.py&quo…

nss刷题(2)

1、[NSSCTF 2022 Spring Recruit]ezgame 打开题目是一个游戏界面 发现是有分数的&#xff0c;猜测分数达到某个之后可以获得flag&#xff0c;查看源码看一下 看到末尾显示分数超过65后显示flag 在js中找到了一个score,将他的值改为大于65的数后随意玩一次就可以得到flag同时&a…

Python使用Rembg库去除图片背景

一、引入Rembg库 #库地址 https://github.com/danielgatis/rembg#CPU使用 pip install rembg # for library pip install rembg[cli] # for library cli#GPU使用&#xff08;系统支持onnxruntime-gpu&#xff09; pip install rembg[gpu] # for library pip install rembg[gp…

JAVA 集合(单列集合)

集合框架 1.集合的特点 a.只能存储引用数据类型的数据 b.长度可变 c.集合中有大量的方法,方便我们操作 2.分类: a.单列集合:一个元素就一个组成部分: list.add(“张三”) b.双列集合:一个元素有两部分构成: key 和 value map.put(“涛哥”,“金莲”) -> key,value叫做键值…

常用Linux命令详细总结

一、文档编辑、过滤、查看命令 1、cp 复制文件和目录 -a 复制文件并保持文件属性 -d 若源文件为链接文件&#xff0c;则复制链接文件属性而非文件本身 -i 覆盖文件前提示&#xff0c;如果不要提示&#xff0c;在命令前加上\ -r 递归复制&#xff0c;通常用于目录的复制 …

[muduo网络库]——muduo库Buffer类(剖析muduo网络库核心部分、设计思想)

接着之前我们[muduo网络库]——muduo库Socket类&#xff08;剖析muduo网络库核心部分、设计思想&#xff09;&#xff0c;我们接下来继续看muduo库中的Buffer类。其实Buffer在我的另一篇博客里面已经介绍过了深究muduo网络库的Buffer类&#xff01;&#xff01;&#xff01;&am…