liu
7 小时以前 611dd8268e40caa4d97e8954b85b5adaaef99afa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<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';
 
import { message } from 'ant-design-vue';
 
import { clockIn, clockOut } from '#/api/hrm/attendance/record';
 
const emit = defineEmits(['success']);
 
const DEFAULT_LOCATION = '公司前台';
 
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 {
    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 (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');
}
 
/** 下班打卡 */
function handleClockOut() {
  return submitClock('out');
}
 
const [Modal] = useVbenModal({
  onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      location.value = DEFAULT_LOCATION;
    }
  },
});
</script>
 
<template>
  <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: 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"
        >
          上班打卡
        </a-button>
        <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>