From 611dd8268e40caa4d97e8954b85b5adaaef99afa Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期一, 26 十月 2026 17:45:58 +0800
Subject: [PATCH] feat(hrm): 考勤规则支持地图定位打卡

---
 src/views/hrm/attendance/record/modules/clock-panel.vue |  116 ++++++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 92 insertions(+), 24 deletions(-)

diff --git a/src/views/hrm/attendance/record/modules/clock-panel.vue b/src/views/hrm/attendance/record/modules/clock-panel.vue
index 7f41eed..2190714 100644
--- a/src/views/hrm/attendance/record/modules/clock-panel.vue
+++ b/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">
+        姝e湪鑾峰彇瀹氫綅鈥�
+      </div>
     </div>
   </Modal>
-</template>
\ No newline at end of file
+</template>

--
Gitblit v1.9.3