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

✨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. 💪后续会改进😁😁😁😁
  • 23
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值