liu
8 小时以前 611dd8268e40caa4d97e8954b85b5adaaef99afa
src/views/hrm/attendance/record/modules/clock-panel.vue
@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { AttendanceClockLocation, AttendanceClockResponse } from '#/api/hrm/attendance/record';
import { ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
@@ -9,62 +11,124 @@
const emit = defineEmits(['success']);
const location = ref('公司前台');
const loading = ref(false);
const DEFAULT_LOCATION = '公司前台';
/** 上班打卡 */
async function handleClockIn() {
const location = ref(DEFAULT_LOCATION);
const loading = ref(false);
const locating = ref(false);
/**
 * 获取浏览器定位并统一为 WGS84 交给后端校验。
 * 定位不可用(不支持、拒绝授权、非 HTTPS)时返回 null,
 * 由后端按考勤规则决定是否允许无定位打卡。
 */
function getCurrentLocation(): Promise<AttendanceClockLocation | null> {
  if (!navigator.geolocation) {
    return Promise.resolve(null);
  }
  return new Promise((resolve) => {
    navigator.geolocation.getCurrentPosition(
      (position) =>
        resolve({
          longitude: Number(position.coords.longitude.toFixed(7)),
          latitude: Number(position.coords.latitude.toFixed(7)),
          accuracy: Number(position.coords.accuracy.toFixed(2)),
          coordinateSystem: 'WGS84',
        }),
      () => resolve(null),
      { enableHighAccuracy: true, maximumAge: 0, timeout: 15_000 },
    );
  });
}
/** 未启用定位范围校验时后端不返回距离,直接提示成功 */
function buildSuccessMessage(
  action: string,
  result?: AttendanceClockResponse,
) {
  if (result?.distanceMeters === null || result?.distanceMeters === undefined) {
    return `${action}成功`;
  }
  const place = result.locationName ? `距${result.locationName}` : '距打卡地点';
  return `${action}成功,${place} ${Number(result.distanceMeters).toFixed(1)} 米`;
}
async function submitClock(action: 'in' | 'out') {
  if (loading.value) {
    return;
  }
  loading.value = true;
  try {
    await clockIn({ location: location.value });
    message.success('上班打卡成功');
    locating.value = true;
    const position = await getCurrentLocation();
    locating.value = false;
    if (!position) {
      message.warning(
        '未获取到定位,将按打卡地点文本提交;若考勤规则要求定位,请开启定位权限后重试(非 HTTPS 环境浏览器会禁用定位)',
      );
    }
    const locationText = location.value.trim() || undefined;
    const result =
      action === 'in'
        ? await clockIn(position, locationText)
        : await clockOut(position, locationText);
    message.success(
      buildSuccessMessage(action === 'in' ? '上班打卡' : '下班打卡', result),
    );
    emit('success');
  } catch (e: any) {
    message.error(e.message || '打卡失败');
  } catch (error: any) {
    // 接口错误已由请求拦截器统一提示,这里只兜底本地异常,避免重复弹出
    const handledByInterceptor = Boolean(
      error?.response || error?.data || error?.isAxiosError || error?.code,
    );
    if (!handledByInterceptor) {
      message.error(error?.message || '打卡失败');
    }
  } finally {
    locating.value = false;
    loading.value = false;
  }
}
/** 上班打卡 */
function handleClockIn() {
  return submitClock('in');
}
/** 下班打卡 */
async function handleClockOut() {
  loading.value = true;
  try {
    await clockOut({ location: location.value });
    message.success('下班打卡成功');
    emit('success');
  } catch (e: any) {
    message.error(e.message || '打卡失败');
  } finally {
    loading.value = false;
  }
function handleClockOut() {
  return submitClock('out');
}
const [Modal, modalApi] = useVbenModal({
const [Modal] = useVbenModal({
  onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      location.value = '公司前台';
      location.value = DEFAULT_LOCATION;
    }
  },
});
</script>
<template>
  <Modal title="打卡" class="w-[400px]">
  <Modal title="打卡" class="w-[420px]">
    <div class="p-4">
      <div class="mb-4">
        <label class="mr-2">打卡地点:</label>
        <a-input
          v-model:value="location"
          placeholder="请输入打卡地点"
          style="width: 200px"
          style="width: 220px"
        />
      </div>
      <div class="mb-4 text-xs text-gray-500">
        点击打卡后会自动获取当前 GPS 位置,并按考勤规则中的地点与允许半径校验;浏览器定位不可用时,将按上方地点文本提交。
      </div>
      <div class="flex gap-4">
        <a-button
          type="primary"
          size="large"
          :loading="loading"
          :disabled="loading"
          @click="handleClockIn"
        >
          上班打卡
@@ -72,11 +136,15 @@
        <a-button
          size="large"
          :loading="loading"
          :disabled="loading"
          @click="handleClockOut"
        >
          下班打卡
        </a-button>
      </div>
      <div v-if="locating" class="mt-2 text-xs text-gray-400">
        正在获取定位…
      </div>
    </div>
  </Modal>
</template>
</template>