tuqin
3 天以前 84abbd63fa039a39358fb8db7526b567422f3406
src/views/hrm/attendance/rule/modules/location-map-picker.vue
@@ -2,7 +2,7 @@
<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';
@@ -46,6 +46,20 @@
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;
@@ -254,6 +268,57 @@
  });
}
/**
 * 切换到手工填写模式。
 *
 * 百度地图依赖第三方 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) {
@@ -295,7 +360,7 @@
    }
    // SDK 回调触发但 BMapGL 未挂载(常见于 ak 无效 / 域名未授权 / 被网络拦截)
    if (!window.BMapGL) {
      message.error('百度地图加载失败,请检查地图 Key 配置或网络');
      switchToManual('地图组件未就绪,可手工填写坐标');
      return;
    }
    const hasCoordinates =
@@ -329,7 +394,8 @@
      selectedAddress.value = location.address ?? '';
    }
  } catch (error: any) {
    message.error(error?.message || '地图初始化失败');
    // 地图不可用(网络拦截 / ak 未授权 / 超时)时降级为手工填写,不能阻塞业务
    switchToManual(error?.message || '地图加载失败');
  } finally {
    loading.value = false;
  }
@@ -384,33 +450,76 @@
    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>