<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>
|