| | |
| | | <script lang="ts" setup> |
| | | import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Alert, Button, Input, Spin, message } from 'ant-design-vue'; |
| | | import { Alert, Button, Input, InputNumber, Spin, message } from 'ant-design-vue'; |
| | | |
| | | import { loadBaiduMapSdk } from '#/components/map'; |
| | | |
| | |
| | | const loading = ref(false); |
| | | const searchKeyword = ref(''); |
| | | const selectedAddress = ref(''); |
| | | |
| | | /** |
| | | * 手工填写模式。 |
| | | * |
| | | * 触发场景:百度地图 SDK 拿不到(企业网络拦截 api.map.baidu.com、 |
| | | * 浏览器插件拦截、ak 域名白名单未放开等)。 |
| | | * 此前这种情况会直接抛 "Cannot read properties of null (reading 'Map')" 或 |
| | | * "百度地图 SDK 加载失败",用户连考勤规则都存不了。 |
| | | * 现在改为:地图不可用时自动切换到手工输入经纬度,保证业务不中断。 |
| | | */ |
| | | const manualMode = ref(false); |
| | | const manualName = ref(''); |
| | | const manualLongitude = ref<number>(); |
| | | const manualLatitude = ref<number>(); |
| | | |
| | | let map: AnyMap; |
| | | let marker: AnyMap; |
| | |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 切换到手工填写模式。 |
| | | * |
| | | * 百度地图依赖第三方 CDN(api.map.baidu.com),在内网、被防火墙拦截、 |
| | | * 浏览器插件拦截、ak 域名白名单未放开的场景下都拿不到 SDK。 |
| | | * 此时必须降级,否则用户连考勤规则都存不了(后端要求定位开启时地点名称+经纬度必填)。 |
| | | */ |
| | | function switchToManual(reason: string) { |
| | | if (disposed) { |
| | | return; |
| | | } |
| | | manualMode.value = true; |
| | | loading.value = false; |
| | | if (!manualName.value) { |
| | | manualName.value = selectedAddress.value || ''; |
| | | } |
| | | if (!Number.isFinite(manualLongitude.value) && Number.isFinite(props.longitude)) { |
| | | manualLongitude.value = Number(props.longitude); |
| | | } |
| | | if (!Number.isFinite(manualLatitude.value) && Number.isFinite(props.latitude)) { |
| | | manualLatitude.value = Number(props.latitude); |
| | | } |
| | | message.warning(`${reason},已切换为手工填写坐标`); |
| | | } |
| | | |
| | | /** 手工填写坐标后回填表单 */ |
| | | function applyManual() { |
| | | const longitude = Number(manualLongitude.value); |
| | | const latitude = Number(manualLatitude.value); |
| | | if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) { |
| | | message.warning('请填写经度和纬度'); |
| | | return; |
| | | } |
| | | if (longitude < -180 || longitude > 180) { |
| | | message.warning('经度应在 -180 ~ 180 之间'); |
| | | return; |
| | | } |
| | | if (latitude < -90 || latitude > 90) { |
| | | message.warning('纬度应在 -90 ~ 90 之间'); |
| | | return; |
| | | } |
| | | const name = manualName.value.trim(); |
| | | emit('select', { |
| | | address: name || undefined, |
| | | latitude: Number(latitude.toFixed(7)), |
| | | locationName: name || undefined, |
| | | longitude: Number(longitude.toFixed(7)), |
| | | }); |
| | | message.success('坐标已应用'); |
| | | } |
| | | |
| | | async function handleSearch() { |
| | | const keyword = searchKeyword.value.trim(); |
| | | if (!keyword || !geocoder) { |
| | |
| | | } |
| | | // SDK 回调触发但 BMapGL 未挂载(常见于 ak 无效 / 域名未授权 / 被网络拦截) |
| | | if (!window.BMapGL) { |
| | | message.error('百度地图加载失败,请检查地图 Key 配置或网络'); |
| | | switchToManual('地图组件未就绪,可手工填写坐标'); |
| | | return; |
| | | } |
| | | const hasCoordinates = |
| | |
| | | selectedAddress.value = location.address ?? ''; |
| | | } |
| | | } catch (error: any) { |
| | | message.error(error?.message || '地图初始化失败'); |
| | | // 地图不可用(网络拦截 / ak 未授权 / 超时)时降级为手工填写,不能阻塞业务 |
| | | switchToManual(error?.message || '地图加载失败'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | |
| | | type="warning" |
| | | show-icon |
| | | message="百度地图尚未配置" |
| | | description="请配置 VITE_BAIDU_MAP_KEY 后重新构建前端;未配置时可手工填写经纬度。" |
| | | description="未配置 VITE_BAIDU_MAP_KEY,请手工填写打卡地点的经纬度。" |
| | | /> |
| | | <Spin v-else :spinning="loading"> |
| | | <div v-if="!disabled" class="mb-2 flex gap-2"> |
| | | <Input |
| | | v-model:value="searchKeyword" |
| | | allow-clear |
| | | placeholder="输入省、市、区和具体地点,例如:乌鲁木齐市厂区" |
| | | @press-enter="handleSearch" |
| | | <template v-else> |
| | | <!-- 地图不可用时的手工填写模式,保证业务不被第三方 CDN 卡住 --> |
| | | <template v-if="manualMode"> |
| | | <Alert |
| | | type="info" |
| | | show-icon |
| | | class="mb-2" |
| | | message="地图不可用,已切换为手工填写坐标" |
| | | description="如长期无法加载地图,请检查服务器/浏览器是否允许访问 api.map.baidu.com,或联系管理员确认百度地图 Key 的域名白名单。" |
| | | /> |
| | | <Button :loading="loading" @click="handleSearch">搜索地址</Button> |
| | | </div> |
| | | <div |
| | | ref="mapContainer" |
| | | class="h-[360px] w-full overflow-hidden rounded border border-gray-200" |
| | | ></div> |
| | | <div |
| | | class="mt-2 flex flex-wrap justify-between gap-2 text-xs text-gray-500" |
| | | > |
| | | <span> |
| | | {{ |
| | | disabled |
| | | ? '打卡位置及允许范围' |
| | | : '点击地图选择打卡中心点;蓝色圆圈表示允许打卡范围' |
| | | }} |
| | | </span> |
| | | <span v-if="selectedAddress">{{ selectedAddress }}</span> |
| | | </div> |
| | | </Spin> |
| | | <div class="grid grid-cols-3 gap-2"> |
| | | <Input |
| | | v-model:value="manualName" |
| | | :disabled="disabled" |
| | | placeholder="打卡地点名称,如:厂区大门" |
| | | /> |
| | | <InputNumber |
| | | v-model:value="manualLongitude" |
| | | :disabled="disabled" |
| | | :precision="7" |
| | | :step="0.0001" |
| | | class="w-full" |
| | | placeholder="经度,如 87.616848" |
| | | /> |
| | | <InputNumber |
| | | v-model:value="manualLatitude" |
| | | :disabled="disabled" |
| | | :precision="7" |
| | | :step="0.0001" |
| | | class="w-full" |
| | | placeholder="纬度,如 43.825592" |
| | | /> |
| | | </div> |
| | | <div class="mt-2 flex items-center justify-between"> |
| | | <span class="text-xs text-gray-500"> |
| | | 坐标须为 WGS84(GPS 原始坐标),允许范围 {{ radiusMeters }} 米 |
| | | </span> |
| | | <Button v-if="!disabled" type="primary" @click="applyManual"> |
| | | 应用坐标 |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | <Spin v-else :spinning="loading"> |
| | | <div v-if="!disabled" class="mb-2 flex gap-2"> |
| | | <Input |
| | | v-model:value="searchKeyword" |
| | | allow-clear |
| | | placeholder="输入省、市、区和具体地点,例如:乌鲁木齐市厂区" |
| | | @press-enter="handleSearch" |
| | | /> |
| | | <Button :loading="loading" @click="handleSearch">搜索地址</Button> |
| | | </div> |
| | | <div |
| | | ref="mapContainer" |
| | | class="h-[360px] w-full overflow-hidden rounded border border-gray-200" |
| | | ></div> |
| | | <div |
| | | class="mt-2 flex flex-wrap justify-between gap-2 text-xs text-gray-500" |
| | | > |
| | | <span> |
| | | {{ |
| | | disabled |
| | | ? '打卡位置及允许范围' |
| | | : '点击地图选择打卡中心点;蓝色圆圈表示允许打卡范围' |
| | | }} |
| | | </span> |
| | | <span v-if="selectedAddress">{{ selectedAddress }}</span> |
| | | </div> |
| | | </Spin> |
| | | </template> |
| | | </template> |