✨1. 实现功能
- 🌟表单内显示省市县以及详细地址
- 点击省市县输入框时,打开对应地图弹窗,进行位置选择
- 选择位置回显入对应输入框
- 表单内的省市县以及地址输入框同外嵌表单走相同的校验方式
- 触发校验后点击
reset
实现清除校验与清空数据
- 🌟地图内展示地址搜索框以及地图坐标图
- 搜索框展示当前经纬度地址
- 搜索框可输入自定义地址,下拉菜单展示范围兴趣点和道路信息,点击可进行搜索
- 🌟 单独封装每个组件,使
form-item
和dialog
以及amap
三个组件可单独使用
✨2. 示例图
-
💖示例图1:💖
-
💖💖示例图2:💖
-
💖💖💖示例图3:💖
-
💖💖💖💖示例图4:💖
-
💖💖💖💖💖示例图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. 使用组件 -->
<AmapExplore />
- ✌️使用
provide
向后代传入表单数据(formData
)和校验规则(formRules
)
// 2. 传入formData和formRules
provide("aMap", { ruleForm, rules });
- 👋完整代码示例:
<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. 封装实例缺点💦
- 当选择地址之后,再次打开地图弹窗,更改地图标记点,地址会实时变更,
- 不论点击取消还是确认按钮,都会改变表单内部值
- 💢初始不会出现此问题💢
- 💪后续会改进😁😁😁😁