| | |
| | | <script setup lang="ts"> |
| | | import { computed, onBeforeUnmount, ref } from "vue"; |
| | | import { onPullDownRefresh, onShow } from "@dcloudio/uni-app"; |
| | | import { |
| | | clockIn, |
| | | clockOut, |
| | | getAttendanceMyStatus, |
| | | type AttendanceClockRequest, |
| | | type AttendanceClockResponse, |
| | | type AttendanceMyStatus, |
| | | type AttendanceRecord, |
| | | } from "../../../api/hrm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { computed, onBeforeUnmount, ref } from "vue"; |
| | | import { onPullDownRefresh, onShow } from "@dcloudio/uni-app"; |
| | | import { |
| | | clockIn, |
| | | clockOut, |
| | | getAttendanceMyStatus, |
| | | type AttendanceClockRequest, |
| | | type AttendanceClockResponse, |
| | | type AttendanceMyStatus, |
| | | type AttendanceRecord, |
| | | } from "../../../api/hrm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import BaiduAttendanceMap from "./BaiduAttendanceMap.vue"; |
| | | |
| | | const { state } = useAuth(); |
| | | const COOLDOWN_SECONDS = 15; |
| | | const status = ref<AttendanceMyStatus>(); |
| | | const loading = ref(false); |
| | | const punching = ref<"" | "in" | "out">(""); |
| | | const positioning = ref(false); |
| | | const locationStatus = ref<"idle" | "locating" | "inside" | "outside" | "denied" | "error" | "poor">("idle"); |
| | | const currentAccuracy = ref<number>(); |
| | | const estimatedDistance = ref<number>(); |
| | | const lastClockResult = ref<AttendanceClockResponse>(); |
| | | const error = ref(""); |
| | | const now = ref(new Date()); |
| | | let clockOffset = 0; |
| | | let timer: ReturnType<typeof setInterval> | undefined; |
| | | const { state } = useAuth(); |
| | | const COOLDOWN_SECONDS = 15; |
| | | const status = ref<AttendanceMyStatus>(); |
| | | const loading = ref(false); |
| | | const punching = ref<"" | "in" | "out">(""); |
| | | const positioning = ref(false); |
| | | const locationStatus = ref< |
| | | "idle" | "locating" | "inside" | "outside" | "denied" | "error" |
| | | >("idle"); |
| | | const estimatedDistance = ref<number>(); |
| | | const currentLocation = ref<AttendanceClockRequest>(); |
| | | const lastClockResult = ref<AttendanceClockResponse>(); |
| | | const mapError = ref(""); |
| | | const mapVisible = ref(true); |
| | | const error = ref(""); |
| | | const now = ref(new Date()); |
| | | let clockOffset = 0; |
| | | let timer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | function pad(value: number) { return String(value).padStart(2, "0"); } |
| | | function localDate(value = new Date()) { return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`; } |
| | | function parseDateTime(value?: string) { |
| | | if (!value) return undefined; |
| | | const parsed = new Date(String(value).replace(" ", "T")); |
| | | return Number.isNaN(parsed.getTime()) ? undefined : parsed; |
| | | } |
| | | function clockTime(value?: string) { |
| | | const parsed = parseDateTime(value); |
| | | return parsed ? `${pad(parsed.getHours())}:${pad(parsed.getMinutes())}` : "--:--"; |
| | | } |
| | | function fullClockTime(value?: string) { |
| | | const parsed = parseDateTime(value); |
| | | return parsed ? `${localDate(parsed)} ${pad(parsed.getHours())}:${pad(parsed.getMinutes())}` : "å°æªæå¡"; |
| | | } |
| | | function syncNow() { now.value = new Date(Date.now() + clockOffset); } |
| | | function pad(value: number) { |
| | | return String(value).padStart(2, "0"); |
| | | } |
| | | function localDate(value = new Date()) { |
| | | return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad( |
| | | value.getDate() |
| | | )}`; |
| | | } |
| | | function parseDateTime(value?: string) { |
| | | if (!value) return undefined; |
| | | const parsed = new Date(String(value).replace(" ", "T")); |
| | | return Number.isNaN(parsed.getTime()) ? undefined : parsed; |
| | | } |
| | | function clockTime(value?: string) { |
| | | const parsed = parseDateTime(value); |
| | | return parsed |
| | | ? `${pad(parsed.getHours())}:${pad(parsed.getMinutes())}` |
| | | : "--:--"; |
| | | } |
| | | function fullClockTime(value?: string) { |
| | | const parsed = parseDateTime(value); |
| | | return parsed |
| | | ? `${localDate(parsed)} ${pad(parsed.getHours())}:${pad( |
| | | parsed.getMinutes() |
| | | )}` |
| | | : "å°æªæå¡"; |
| | | } |
| | | function syncNow() { |
| | | now.value = new Date(Date.now() + clockOffset); |
| | | } |
| | | |
| | | const rule = computed(() => status.value?.rule || undefined); |
| | | const record = computed(() => status.value?.record || undefined); |
| | | const locationRequired = computed(() => rule.value?.locationRequired === true); |
| | | const targetLocationName = computed(() => rule.value?.locationName || "èå¤å°ç¹"); |
| | | const locationStatusText = computed(() => { |
| | | if (!locationRequired.value) return "å½åè§åæªå¯ç¨ä½ç½®éå¶"; |
| | | if (locationStatus.value === "locating") return "æ£å¨è·åé«ç²¾åº¦å®ä½..."; |
| | | if (locationStatus.value === "denied") return "æªè·å¾å®ä½æéï¼è¯·å¨ç³»ç»è®¾ç½®ä¸å
许å®ä½"; |
| | | if (locationStatus.value === "poor") return `å®ä½ç²¾åº¦ ${Math.round(currentAccuracy.value || 0)} ç±³ï¼è¯·ç§»å¨å°å¼éå¤éè¯`; |
| | | if (locationStatus.value === "error") return "å®ä½å¤±è´¥ï¼è¯·æ£æ¥ç³»ç»å®ä½å¼å
³åéè¯"; |
| | | if (locationStatus.value === "inside") return `é¢è®¡å¨èå´å
· è·æå¡ç¹çº¦ ${Math.round(estimatedDistance.value || 0)} ç±³`; |
| | | if (locationStatus.value === "outside") return `é¢è®¡å¨èå´å¤ · è·æå¡ç¹çº¦ ${Math.round(estimatedDistance.value || 0)} ç±³`; |
| | | return "ç¹å»æå¡åå°è·åå½åä½ç½®"; |
| | | }); |
| | | function hideUnavailableMap(message: string) { |
| | | mapError.value = message; |
| | | mapVisible.value = false; |
| | | } |
| | | |
| | | function calculateDistance(longitude: number, latitude: number) { |
| | | const targetLongitude = rule.value?.longitude; |
| | | const targetLatitude = rule.value?.latitude; |
| | | if (targetLongitude === undefined || targetLatitude === undefined) return undefined; |
| | | const radians = (value: number) => value * Math.PI / 180; |
| | | const latDelta = radians(latitude - targetLatitude); |
| | | const lonDelta = radians(longitude - targetLongitude); |
| | | const value = Math.sin(latDelta / 2) ** 2 |
| | | + Math.cos(radians(targetLatitude)) * Math.cos(radians(latitude)) * Math.sin(lonDelta / 2) ** 2; |
| | | const bounded = Math.min(1, Math.max(0, value)); |
| | | return 6371000 * 2 * Math.atan2(Math.sqrt(bounded), Math.sqrt(1 - bounded)); |
| | | } |
| | | |
| | | function getCurrentLocation(): Promise<AttendanceClockRequest> { |
| | | if (!locationRequired.value) return Promise.resolve({}); |
| | | if (positioning.value) return Promise.reject(new Error("æ£å¨å®ä½ï¼è¯·ç¨å")); |
| | | positioning.value = true; |
| | | locationStatus.value = "locating"; |
| | | return new Promise((resolve, reject) => { |
| | | uni.getLocation({ |
| | | type: "wgs84", |
| | | isHighAccuracy: true, |
| | | highAccuracyExpireTime: 8000, |
| | | success: result => { |
| | | const accuracy = Number(result.accuracy || result.horizontalAccuracy || 0); |
| | | currentAccuracy.value = accuracy; |
| | | if (!Number.isFinite(accuracy) || accuracy <= 0) { |
| | | locationStatus.value = "error"; |
| | | reject(new Error("æªè·åå°ææçå®ä½ç²¾åº¦ï¼è¯·éæ°å®ä½")); |
| | | return; |
| | | } |
| | | if (accuracy > 100) { |
| | | locationStatus.value = "poor"; |
| | | reject(new Error(`å½åå®ä½ç²¾åº¦ä¸º ${Math.round(accuracy)} ç±³ï¼è¯·ç§»å¨å°å¼éå¤éæ°å®ä½`)); |
| | | return; |
| | | } |
| | | estimatedDistance.value = calculateDistance(result.longitude, result.latitude); |
| | | locationStatus.value = estimatedDistance.value !== undefined |
| | | && estimatedDistance.value > (rule.value?.radiusMeters || 200) ? "outside" : "inside"; |
| | | resolve({ |
| | | longitude: result.longitude, |
| | | latitude: result.latitude, |
| | | accuracy, |
| | | coordinateSystem: "WGS84", |
| | | }); |
| | | }, |
| | | fail: cause => { |
| | | const message = String(cause.errMsg || ""); |
| | | locationStatus.value = /auth|authorize|permission|deny/i.test(message) ? "denied" : "error"; |
| | | reject(new Error(locationStatus.value === "denied" ? "æªè·å¾å®ä½æéï¼æ æ³æå¡" : "å®ä½å¤±è´¥ï¼è¯·æ£æ¥å®ä½å¼å
³åéè¯")); |
| | | }, |
| | | complete: () => { positioning.value = false; }, |
| | | }); |
| | | const rule = computed(() => status.value?.rule || undefined); |
| | | const record = computed(() => status.value?.record || undefined); |
| | | const locationRequired = computed(() => rule.value?.locationRequired === true); |
| | | const targetLocationName = computed( |
| | | () => rule.value?.locationName || "èå¤å°ç¹" |
| | | ); |
| | | const isInsecureH5 = computed(() => { |
| | | // #ifdef H5 |
| | | return typeof window !== "undefined" && !window.isSecureContext; |
| | | }); |
| | | } |
| | | const locationStatusText = computed(() => { |
| | | if (!locationRequired.value) return "å½åè§åæªå¯ç¨ä½ç½®éå¶"; |
| | | if (isInsecureH5.value) return "æµè§å¨å®ä½éè¦ HTTPSï¼localhost è°è¯é¤å¤ï¼"; |
| | | if (locationStatus.value === "locating") return "æ£å¨è·åå½åä½ç½®..."; |
| | | if (locationStatus.value === "denied") |
| | | return "æªè·å¾å®ä½æéï¼è¯·å¨ç³»ç»è®¾ç½®ä¸å
许å®ä½"; |
| | | if (locationStatus.value === "error") |
| | | return "å®ä½å¤±è´¥ï¼è¯·æ£æ¥ç³»ç»å®ä½å¼å
³åéè¯"; |
| | | if (locationStatus.value === "inside") |
| | | return `é¢è®¡å¨èå´å
· è·æå¡ç¹çº¦ ${Math.round( |
| | | estimatedDistance.value || 0 |
| | | )} ç±³`; |
| | | if (locationStatus.value === "outside") |
| | | return `é¢è®¡å¨èå´å¤ · è·æå¡ç¹çº¦ ${Math.round( |
| | | estimatedDistance.value || 0 |
| | | )} ç±³`; |
| | | return "ç¹å»æå¡åå°è·åå½åä½ç½®"; |
| | | }); |
| | | |
| | | const dateText = computed(() => { |
| | | const weekdays = ["æææ¥", "ææä¸", "ææäº", "ææä¸", "ææå", "ææäº", "ææå
"]; |
| | | return `${now.value.getMonth() + 1}æ${now.value.getDate()}æ¥ ${weekdays[now.value.getDay()]}`; |
| | | }); |
| | | const timeText = computed(() => `${pad(now.value.getHours())}:${pad(now.value.getMinutes())}:${pad(now.value.getSeconds())}`); |
| | | const userName = computed(() => status.value?.employeeName || state.user?.nickname || state.user?.username || "å½åç¨æ·"); |
| | | const department = computed(() => status.value?.deptName || "æªåé
é¨é¨"); |
| | | const dateLabel = computed(() => { |
| | | const rest = status.value && status.value.workDay === false ? `ï¼${status.value.restDayReason || "伿¯æ¥"}ï¼` : ""; |
| | | return `${dateText.value}${rest} · ${rule.value?.name || "æ éç¨ç次"}`; |
| | | }); |
| | | const clockInLabel = computed(() => record.value?.clockInTypeName || (record.value?.clockInTime ? "å·²æå¡" : "æªæå¡")); |
| | | const clockOutLabel = computed(() => record.value?.clockOutTypeName || (record.value?.clockOutTime ? "å·²æå¡" : "æªæå¡")); |
| | | const workHourText = computed(() => { |
| | | const value = record.value?.workHours; |
| | | if (value === undefined || value === null) return ""; |
| | | const overtime = record.value?.overtimeHours; |
| | | const overtimeText = overtime ? ` · å ç ${overtime} å°æ¶` : ""; |
| | | return `工使¶é¿ ${value} å°æ¶${overtimeText}`; |
| | | }); |
| | | const cooldownLeft = computed(() => { |
| | | const clockInAt = parseDateTime(record.value?.clockInTime); |
| | | if (!clockInAt) return 0; |
| | | const left = COOLDOWN_SECONDS - Math.floor((now.value.getTime() - clockInAt.getTime()) / 1000); |
| | | return left > 0 ? left : 0; |
| | | }); |
| | | function calculateDistance(longitude: number, latitude: number) { |
| | | const targetLongitude = rule.value?.longitude; |
| | | const targetLatitude = rule.value?.latitude; |
| | | if (targetLongitude === undefined || targetLatitude === undefined) |
| | | return undefined; |
| | | const radians = (value: number) => (value * Math.PI) / 180; |
| | | const latDelta = radians(latitude - targetLatitude); |
| | | const lonDelta = radians(longitude - targetLongitude); |
| | | const value = |
| | | Math.sin(latDelta / 2) ** 2 + |
| | | Math.cos(radians(targetLatitude)) * |
| | | Math.cos(radians(latitude)) * |
| | | Math.sin(lonDelta / 2) ** 2; |
| | | const bounded = Math.min(1, Math.max(0, value)); |
| | | return 6371000 * 2 * Math.atan2(Math.sqrt(bounded), Math.sqrt(1 - bounded)); |
| | | } |
| | | |
| | | type PunchState = { action: "in" | "out" | undefined; title: string; status: string; hint: string; tone: string }; |
| | | const punch = computed<PunchState>(() => { |
| | | if (!status.value && error.value) { |
| | | return { action: undefined, title: "æ æ³æå¡", status: "èå¤ä¿¡æ¯å 载失败", hint: "ç¹å»çº¢è²æç¤ºæ¡éè¯", tone: "danger" }; |
| | | } |
| | | if (!status.value) { |
| | | return { action: undefined, title: "å è½½ä¸", status: "æ£å¨è¯»åèå¤ç¶æ", hint: "请ç¨å", tone: "idle" }; |
| | | } |
| | | const info = status.value.clockable; |
| | | if (!info) { |
| | | return { action: undefined, title: "æ æ³æå¡", status: "æªè·åå°æå¡ç¶æ", hint: "䏿巿°éè¯", tone: "idle" }; |
| | | } |
| | | const message = info.message || ""; |
| | | const action = info.nextAction === "in" || info.nextAction === "out" ? info.nextAction : undefined; |
| | | if (!action) { |
| | | const rest = status.value.workDay === false; |
| | | return { |
| | | action: undefined, |
| | | title: rest ? "仿¥ä¼æ¯" : "仿¥å·²æå¡", |
| | | status: message || (rest ? "仿¥ä¼æ¯ï¼æ éæå¡" : "仿¥å·²å®ææå¡"), |
| | | hint: message || "仿¥æ éæå¡", |
| | | tone: rest ? "idle" : "done", |
| | | }; |
| | | } |
| | | if (action === "out" && cooldownLeft.value > 0) { |
| | | return { action: undefined, title: "ä¸çå·²æå¡", status: "æå¡é´é", hint: `${cooldownLeft.value} ç§åå¯ä¸çæå¡`, tone: "cooldown" }; |
| | | } |
| | | return { |
| | | action, |
| | | title: action === "in" ? "ä¸çæå¡" : "ä¸çæå¡", |
| | | status: message, |
| | | hint: action === "in" ? "请ç¹å»å®æå°å²æå¡" : "请ç¹å»å®æç¦»å²æå¡", |
| | | tone: message.includes("è¶
è¿") || message.includes("è¿å°") ? "warning" : "ready", |
| | | }; |
| | | }); |
| | | |
| | | async function load() { |
| | | if (!state.accessToken || loading.value) return; |
| | | loading.value = true; |
| | | error.value = ""; |
| | | try { |
| | | const data = await getAttendanceMyStatus(state.accessToken); |
| | | status.value = data; |
| | | const serverAt = parseDateTime(data.serverTime); |
| | | if (serverAt) { |
| | | const offset = serverAt.getTime() - Date.now(); |
| | | // è®¾å¤æ¶é´ä¸æå¡å¨åå·®è¶
è¿ 12 å°æ¶è§ä¸ºæ¶åºé
ç½®å¼å¸¸ï¼ä¸ä½¿ç¨ |
| | | if (Math.abs(offset) < 12 * 3600 * 1000) clockOffset = offset; |
| | | function getCurrentLocation(): Promise<AttendanceClockRequest> { |
| | | if (!locationRequired.value) return Promise.resolve({}); |
| | | if (positioning.value) return Promise.reject(new Error("æ£å¨å®ä½ï¼è¯·ç¨å")); |
| | | if (isInsecureH5.value) { |
| | | locationStatus.value = "error"; |
| | | return Promise.reject( |
| | | new Error("å½åç½é¡µä¸æ¯å®å
¨ç¯å¢ï¼è¯·ä½¿ç¨ HTTPS åå®ä½æå¡") |
| | | ); |
| | | } |
| | | syncNow(); |
| | | } catch (cause) { |
| | | error.value = cause instanceof Error ? cause.message : "仿¥èå¤ä¿¡æ¯å 载失败"; |
| | | } finally { |
| | | loading.value = false; |
| | | positioning.value = true; |
| | | locationStatus.value = "locating"; |
| | | return new Promise((resolve, reject) => { |
| | | uni.getLocation({ |
| | | type: "wgs84", |
| | | isHighAccuracy: true, |
| | | highAccuracyExpireTime: 8000, |
| | | success: result => { |
| | | const accuracy = Number( |
| | | result.accuracy || result.horizontalAccuracy || 0 |
| | | ); |
| | | estimatedDistance.value = calculateDistance( |
| | | result.longitude, |
| | | result.latitude |
| | | ); |
| | | if (estimatedDistance.value === undefined) { |
| | | locationStatus.value = "error"; |
| | | reject(new Error("èå¤è§åæªé
ç½®ææçæå¡ç¹åæ ")); |
| | | return; |
| | | } |
| | | const allowedRadius = rule.value?.radiusMeters || 200; |
| | | locationStatus.value = |
| | | estimatedDistance.value > allowedRadius ? "outside" : "inside"; |
| | | const position: AttendanceClockRequest = { |
| | | longitude: result.longitude, |
| | | latitude: result.latitude, |
| | | accuracy: |
| | | Number.isFinite(accuracy) && accuracy > 0 ? accuracy : undefined, |
| | | coordinateSystem: "WGS84", |
| | | }; |
| | | currentLocation.value = position; |
| | | if (locationStatus.value === "outside") { |
| | | reject( |
| | | new Error( |
| | | `å½åä½ç½®è·æå¡ç¹çº¦ ${Math.round( |
| | | estimatedDistance.value |
| | | )} ç±³ï¼ä¸å¨å
许ç ${allowedRadius} ç±³èå´å
` |
| | | ) |
| | | ); |
| | | return; |
| | | } |
| | | resolve(position); |
| | | }, |
| | | fail: cause => { |
| | | const message = String(cause.errMsg || ""); |
| | | locationStatus.value = /auth|authorize|permission|deny/i.test(message) |
| | | ? "denied" |
| | | : "error"; |
| | | reject( |
| | | new Error( |
| | | locationStatus.value === "denied" |
| | | ? "æªè·å¾å®ä½æéï¼æ æ³æå¡" |
| | | : "å®ä½å¤±è´¥ï¼è¯·æ£æ¥å®ä½å¼å
³åéè¯" |
| | | ) |
| | | ); |
| | | }, |
| | | complete: () => { |
| | | positioning.value = false; |
| | | }, |
| | | }); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | async function confirmSmartPunch() { |
| | | const kind = punch.value.action; |
| | | if (!kind) return; |
| | | let position: AttendanceClockRequest; |
| | | try { |
| | | position = await getCurrentLocation(); |
| | | } catch (cause) { |
| | | uni.showToast({ title: cause instanceof Error ? cause.message : "å®ä½å¤±è´¥", icon: "none", duration: 3000 }); |
| | | return; |
| | | async function refreshPosition(showFailure = true) { |
| | | try { |
| | | await getCurrentLocation(); |
| | | } catch (cause) { |
| | | if (showFailure) { |
| | | uni.showToast({ |
| | | title: cause instanceof Error ? cause.message : "å®ä½å¤±è´¥", |
| | | icon: "none", |
| | | duration: 3000, |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | const action = kind === "in" ? "ä¸ç" : "ä¸ç"; |
| | | const locationText = locationRequired.value |
| | | ? `å½åä½ç½®è·â${targetLocationName.value}â约 ${Math.round(estimatedDistance.value || 0)} ç±³ã` |
| | | : "å½åè§åæªå¯ç¨ä½ç½®éå¶ã"; |
| | | uni.showModal({ |
| | | title: `${action}æå¡`, |
| | | content: `${locationText}确认è¿è¡${action}æå¡åï¼`, |
| | | success: result => { if (result.confirm) submitPunch(kind, position); }, |
| | | |
| | | const dateText = computed(() => { |
| | | const weekdays = [ |
| | | "æææ¥", |
| | | "ææä¸", |
| | | "ææäº", |
| | | "ææä¸", |
| | | "ææå", |
| | | "ææäº", |
| | | "ææå
", |
| | | ]; |
| | | return `${now.value.getMonth() + 1}æ${now.value.getDate()}æ¥ ${ |
| | | weekdays[now.value.getDay()] |
| | | }`; |
| | | }); |
| | | } |
| | | const timeText = computed( |
| | | () => |
| | | `${pad(now.value.getHours())}:${pad(now.value.getMinutes())}:${pad( |
| | | now.value.getSeconds() |
| | | )}` |
| | | ); |
| | | const userName = computed( |
| | | () => |
| | | status.value?.employeeName || |
| | | state.user?.nickname || |
| | | state.user?.username || |
| | | "å½åç¨æ·" |
| | | ); |
| | | const department = computed(() => status.value?.deptName || "æªåé
é¨é¨"); |
| | | const dateLabel = computed(() => { |
| | | const rest = |
| | | status.value && status.value.workDay === false |
| | | ? `ï¼${status.value.restDayReason || "伿¯æ¥"}ï¼` |
| | | : ""; |
| | | return `${dateText.value}${rest} · ${rule.value?.name || "æ éç¨ç次"}`; |
| | | }); |
| | | const clockInLabel = computed( |
| | | () => |
| | | record.value?.clockInTypeName || |
| | | (record.value?.clockInTime ? "å·²æå¡" : "æªæå¡") |
| | | ); |
| | | const clockOutLabel = computed( |
| | | () => |
| | | record.value?.clockOutTypeName || |
| | | (record.value?.clockOutTime ? "å·²æå¡" : "æªæå¡") |
| | | ); |
| | | const workHourText = computed(() => { |
| | | const value = record.value?.workHours; |
| | | if (value === undefined || value === null) return ""; |
| | | const overtime = record.value?.overtimeHours; |
| | | const overtimeText = overtime ? ` · å ç ${overtime} å°æ¶` : ""; |
| | | return `工使¶é¿ ${value} å°æ¶${overtimeText}`; |
| | | }); |
| | | const cooldownLeft = computed(() => { |
| | | const clockInAt = parseDateTime(record.value?.clockInTime); |
| | | if (!clockInAt) return 0; |
| | | const left = |
| | | COOLDOWN_SECONDS - |
| | | Math.floor((now.value.getTime() - clockInAt.getTime()) / 1000); |
| | | return left > 0 ? left : 0; |
| | | }); |
| | | |
| | | function applyPunch(kind: "in" | "out", next: AttendanceRecord) { |
| | | const current = status.value; |
| | | if (!current) return; |
| | | current.record = next; |
| | | current.clockable = kind === "in" |
| | | ? { clockIn: false, clockOut: true, nextAction: "out", message: "å·²ä¸çæå¡ï¼å¯ä¸çæå¡" } |
| | | : { clockIn: false, clockOut: false, nextAction: "none", message: "仿¥å·²å®ææå¡" }; |
| | | } |
| | | type PunchState = { |
| | | action: "in" | "out" | undefined; |
| | | title: string; |
| | | status: string; |
| | | hint: string; |
| | | tone: string; |
| | | }; |
| | | const punch = computed<PunchState>(() => { |
| | | if (!status.value && error.value) { |
| | | return { |
| | | action: undefined, |
| | | title: "æ æ³æå¡", |
| | | status: "èå¤ä¿¡æ¯å 载失败", |
| | | hint: "ç¹å»çº¢è²æç¤ºæ¡éè¯", |
| | | tone: "danger", |
| | | }; |
| | | } |
| | | if (!status.value) { |
| | | return { |
| | | action: undefined, |
| | | title: "å è½½ä¸", |
| | | status: "æ£å¨è¯»åèå¤ç¶æ", |
| | | hint: "请ç¨å", |
| | | tone: "idle", |
| | | }; |
| | | } |
| | | const info = status.value.clockable; |
| | | if (!info) { |
| | | return { |
| | | action: undefined, |
| | | title: "æ æ³æå¡", |
| | | status: "æªè·åå°æå¡ç¶æ", |
| | | hint: "䏿巿°éè¯", |
| | | tone: "idle", |
| | | }; |
| | | } |
| | | const message = info.message || ""; |
| | | const action = |
| | | info.nextAction === "in" || info.nextAction === "out" |
| | | ? info.nextAction |
| | | : undefined; |
| | | if (!action) { |
| | | const rest = status.value.workDay === false; |
| | | return { |
| | | action: undefined, |
| | | title: rest ? "仿¥ä¼æ¯" : "仿¥å·²æå¡", |
| | | status: message || (rest ? "仿¥ä¼æ¯ï¼æ éæå¡" : "仿¥å·²å®ææå¡"), |
| | | hint: message || "仿¥æ éæå¡", |
| | | tone: rest ? "idle" : "done", |
| | | }; |
| | | } |
| | | if (action === "out" && cooldownLeft.value > 0) { |
| | | return { |
| | | action: undefined, |
| | | title: "ä¸çå·²æå¡", |
| | | status: "æå¡é´é", |
| | | hint: `${cooldownLeft.value} ç§åå¯ä¸çæå¡`, |
| | | tone: "cooldown", |
| | | }; |
| | | } |
| | | return { |
| | | action, |
| | | title: action === "in" ? "ä¸çæå¡" : "ä¸çæå¡", |
| | | status: message, |
| | | hint: action === "in" ? "请ç¹å»å®æå°å²æå¡" : "请ç¹å»å®æç¦»å²æå¡", |
| | | tone: |
| | | message.includes("è¶
è¿") || message.includes("è¿å°") |
| | | ? "warning" |
| | | : "ready", |
| | | }; |
| | | }); |
| | | |
| | | async function submitPunch(kind: "in" | "out", position: AttendanceClockRequest) { |
| | | if (!state.accessToken || punching.value) return; |
| | | punching.value = kind; |
| | | try { |
| | | const result = kind === "in" |
| | | ? await clockIn(state.accessToken, position) |
| | | : await clockOut(state.accessToken, position); |
| | | lastClockResult.value = result; |
| | | applyPunch(kind, result.record); |
| | | const distanceText = result.distanceMeters === undefined ? "" : `ï¼è·æå¡ç¹ ${Math.round(result.distanceMeters)} ç±³`; |
| | | uni.showToast({ title: `${kind === "in" ? "ä¸ç" : "ä¸ç"}æå¡æå${distanceText}`, icon: "success", duration: 2600 }); |
| | | } catch (cause) { |
| | | uni.showToast({ title: cause instanceof Error ? cause.message : "æå¡å¤±è´¥", icon: "none", duration: 2600 }); |
| | | await load(); |
| | | } finally { |
| | | punching.value = ""; |
| | | async function load() { |
| | | if (!state.accessToken || loading.value) return; |
| | | loading.value = true; |
| | | error.value = ""; |
| | | try { |
| | | const data = await getAttendanceMyStatus(state.accessToken); |
| | | status.value = data; |
| | | if ( |
| | | data.rule?.locationRequired && |
| | | !currentLocation.value && |
| | | !isInsecureH5.value |
| | | ) { |
| | | void refreshPosition(false); |
| | | } |
| | | const serverAt = parseDateTime(data.serverTime); |
| | | if (serverAt) { |
| | | const offset = serverAt.getTime() - Date.now(); |
| | | // è®¾å¤æ¶é´ä¸æå¡å¨åå·®è¶
è¿ 12 å°æ¶è§ä¸ºæ¶åºé
ç½®å¼å¸¸ï¼ä¸ä½¿ç¨ |
| | | if (Math.abs(offset) < 12 * 3600 * 1000) clockOffset = offset; |
| | | } |
| | | syncNow(); |
| | | } catch (cause) { |
| | | error.value = |
| | | cause instanceof Error ? cause.message : "仿¥èå¤ä¿¡æ¯å 载失败"; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | onShow(() => { |
| | | syncNow(); |
| | | if (!timer) timer = setInterval(syncNow, 1000); |
| | | load(); |
| | | }); |
| | | onPullDownRefresh(async () => { await load(); uni.stopPullDownRefresh(); }); |
| | | onBeforeUnmount(() => { if (timer) { clearInterval(timer); timer = undefined; } }); |
| | | async function confirmSmartPunch() { |
| | | const kind = punch.value.action; |
| | | if (!kind) return; |
| | | let position: AttendanceClockRequest; |
| | | try { |
| | | position = await getCurrentLocation(); |
| | | } catch (cause) { |
| | | uni.showToast({ |
| | | title: cause instanceof Error ? cause.message : "å®ä½å¤±è´¥", |
| | | icon: "none", |
| | | duration: 3000, |
| | | }); |
| | | return; |
| | | } |
| | | const action = kind === "in" ? "ä¸ç" : "ä¸ç"; |
| | | const locationText = locationRequired.value |
| | | ? `å½åä½ç½®è·â${targetLocationName.value}â约 ${Math.round( |
| | | estimatedDistance.value || 0 |
| | | )} ç±³ã` |
| | | : "å½åè§åæªå¯ç¨ä½ç½®éå¶ã"; |
| | | uni.showModal({ |
| | | title: `${action}æå¡`, |
| | | content: `${locationText}确认è¿è¡${action}æå¡åï¼`, |
| | | success: result => { |
| | | if (result.confirm) submitPunch(kind, position); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | function applyPunch(kind: "in" | "out", next: AttendanceRecord) { |
| | | const current = status.value; |
| | | if (!current) return; |
| | | current.record = next; |
| | | current.clockable = |
| | | kind === "in" |
| | | ? { |
| | | clockIn: false, |
| | | clockOut: true, |
| | | nextAction: "out", |
| | | message: "å·²ä¸çæå¡ï¼å¯ä¸çæå¡", |
| | | } |
| | | : { |
| | | clockIn: false, |
| | | clockOut: false, |
| | | nextAction: "none", |
| | | message: "仿¥å·²å®ææå¡", |
| | | }; |
| | | } |
| | | |
| | | async function submitPunch( |
| | | kind: "in" | "out", |
| | | position: AttendanceClockRequest |
| | | ) { |
| | | if (!state.accessToken || punching.value) return; |
| | | punching.value = kind; |
| | | try { |
| | | const result = |
| | | kind === "in" |
| | | ? await clockIn(state.accessToken, position) |
| | | : await clockOut(state.accessToken, position); |
| | | lastClockResult.value = result; |
| | | applyPunch(kind, result.record); |
| | | const distanceText = |
| | | result.distanceMeters === undefined |
| | | ? "" |
| | | : `ï¼è·æå¡ç¹ ${Math.round(result.distanceMeters)} ç±³`; |
| | | uni.showToast({ |
| | | title: `${kind === "in" ? "ä¸ç" : "ä¸ç"}æå¡æå`, |
| | | icon: "success", |
| | | duration: 2600, |
| | | }); |
| | | } catch (cause) { |
| | | uni.showToast({ |
| | | title: cause instanceof Error ? cause.message : "æå¡å¤±è´¥", |
| | | icon: "none", |
| | | duration: 2600, |
| | | }); |
| | | await load(); |
| | | } finally { |
| | | punching.value = ""; |
| | | } |
| | | } |
| | | |
| | | onShow(() => { |
| | | syncNow(); |
| | | if (!timer) timer = setInterval(syncNow, 1000); |
| | | load(); |
| | | }); |
| | | onPullDownRefresh(async () => { |
| | | await load(); |
| | | uni.stopPullDownRefresh(); |
| | | }); |
| | | onBeforeUnmount(() => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | timer = undefined; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | </view> |
| | | <text class="date">{{ dateLabel }}</text> |
| | | </view> |
| | | |
| | | <view class="punch-stage"> |
| | | <text class="time">{{ timeText }}</text> |
| | | <text :class="['smart-status', `status-${punch.tone}`]">{{ punch.status }}</text> |
| | | <button :class="['punch-circle', `circle-${punch.tone}`]" :disabled="!punch.action || Boolean(punching) || positioning" @click="confirmSmartPunch"> |
| | | <button :class="['punch-circle', `circle-${punch.tone}`]" |
| | | :disabled="!punch.action || Boolean(punching) || positioning" |
| | | @click="confirmSmartPunch"> |
| | | <text class="circle-title">{{ positioning ? "å®ä½ä¸..." : punching ? "æå¡ä¸..." : punch.title }}</text> |
| | | <text class="circle-time">{{ pad(now.getHours()) }}:{{ pad(now.getMinutes()) }}</text> |
| | | </button> |
| | | <text class="smart-hint">{{ punch.hint }}</text> |
| | | <text class="rule-time">çæ¬¡ {{ rule?.workStartTime || "--:--" }} - {{ rule?.workEndTime || "--:--" }}</text> |
| | | </view> |
| | | |
| | | <!-- #ifdef H5 --> |
| | | <view v-if="locationRequired && mapVisible" |
| | | class="map-panel"> |
| | | <BaiduAttendanceMap :target-longitude="rule?.longitude" |
| | | :target-latitude="rule?.latitude" |
| | | :current-longitude="currentLocation?.longitude" |
| | | :current-latitude="currentLocation?.latitude" |
| | | :radius-meters="rule?.radiusMeters" |
| | | @error="hideUnavailableMap" /> |
| | | <view class="map-legend"> |
| | | <text><i class="legend-target" />è§å®æå¡ç¹åèå´</text> |
| | | <text><i class="legend-current" />æçä½ç½®</text> |
| | | </view> |
| | | <text v-if="mapError" |
| | | class="map-error">{{ mapError }}</text> |
| | | </view> |
| | | <!-- #endif --> |
| | | <view class="location-row"> |
| | | <view class="pin"><view /></view> |
| | | <view class="pin"> |
| | | <view /> |
| | | </view> |
| | | <view class="location-copy"> |
| | | <text>{{ locationRequired ? "è§å®æå¡å°ç¹" : "ä½ç½®éå¶" }}</text> |
| | | <strong>{{ locationRequired ? targetLocationName : "æªå¯ç¨" }}</strong> |
| | | <text v-if="locationRequired && rule?.locationAddress" class="location-address">{{ rule.locationAddress }}</text> |
| | | <text v-if="locationRequired && rule?.locationAddress" |
| | | class="location-address">{{ rule.locationAddress }}</text> |
| | | <text :class="['location-state', `location-${locationStatus}`]">{{ locationStatusText }}</text> |
| | | <text v-if="locationRequired" class="location-range">å
许èå´ {{ rule?.radiusMeters || 200 }} ç±³ · WGS84</text> |
| | | <text v-if="lastClockResult?.distanceMeters !== undefined" class="location-success">æè¿æå¡è·ç¦» {{ Math.round(lastClockResult.distanceMeters) }} ç±³</text> |
| | | <text v-if="locationRequired" |
| | | class="location-range">å
许èå´ {{ rule?.radiusMeters || 200 }} ç±³ · WGS84</text> |
| | | <text v-if="lastClockResult?.distanceMeters !== undefined" |
| | | class="location-success">æè¿æå¡è·ç¦» {{ Math.round(lastClockResult.distanceMeters) }} ç±³</text> |
| | | </view> |
| | | <button v-if="locationRequired" |
| | | class="relocate" |
| | | :loading="positioning" |
| | | @click="refreshPosition()">{{ positioning ? "å®ä½ä¸" : "éæ°å®ä½" }}</button> |
| | | </view> |
| | | |
| | | <view v-if="error" class="error" @click="load">{{ error }}ï¼ç¹å»éè¯</view> |
| | | |
| | | <view v-if="error" |
| | | class="error" |
| | | @click="load">{{ error }}ï¼ç¹å»éè¯</view> |
| | | <view class="section-title"><text>仿¥èå¤</text><text>{{ loading ? "æ´æ°ä¸" : localDate() }}</text></view> |
| | | <view class="timeline"> |
| | | <view class="timeline-row"> |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <text v-if="workHourText" class="work-hours">{{ workHourText }}</text> |
| | | |
| | | <text v-if="workHourText" |
| | | class="work-hours">{{ workHourText }}</text> |
| | | <view class="section-title"><text>èå¤è§å</text><text>{{ rule?.name || "--" }}</text></view> |
| | | <view class="rule-panel"> |
| | | <view><text>ä¸çæ¶é´</text><strong>{{ rule?.workStartTime || "--:--" }}</strong></view> |
| | |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .page { min-height: 100vh; box-sizing: border-box; padding: 24rpx; background: linear-gradient(155deg, #dcecff 0, #ebf7f3 34%, #f4f7fb 62%, #f4f7fb 100%); color: #10233f; } |
| | | .clock-head { padding: 12rpx 4rpx 0; } |
| | | .identity { display: flex; align-items: center; justify-content: space-between; } |
| | | .identity > view { display: flex; align-items: baseline; gap: 14rpx; } |
| | | .welcome { color: #61758f; font-size: 24rpx; } |
| | | .user { font-size: 34rpx; font-weight: 700; } |
| | | .dept { padding: 8rpx 14rpx; border-radius: 8rpx; background: #fff; color: #49617e; font-size: 23rpx; box-shadow: 0 4rpx 14rpx rgba(35, 75, 126, .08); } |
| | | .date { display: block; margin-top: 10rpx; color: #6f8198; font-size: 23rpx; } |
| | | .punch-stage { display: flex; flex-direction: column; align-items: center; padding: 38rpx 0 30rpx; } |
| | | .time { display: block; font-size: 66rpx; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0; } |
| | | .smart-status { margin-top: 8rpx; padding: 7rpx 18rpx; border-radius: 999rpx; font-size: 23rpx; } |
| | | .status-ready,.status-done { color: #08784c; background: #dcf6eb; } |
| | | .status-warning { color: #a65b00; background: #fff0d5; } |
| | | .status-danger { color: #c44040; background: #ffebeb; } |
| | | .status-overtime { color: #5e4ab5; background: #eeeaff; } |
| | | .status-cooldown { color: #3f668d; background: #e4edf6; } |
| | | .status-idle { color: #687789; background: #e7ebf0; } |
| | | .punch-circle { width: 286rpx; height: 286rpx; margin: 28rpx 0 20rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 14rpx solid rgba(255,255,255,.86); border-radius: 50%; color: #fff; line-height: 1.4; box-shadow: 0 18rpx 40rpx rgba(21, 105, 215, .26), 0 0 0 12rpx rgba(74, 145, 235, .11); } |
| | | .punch-circle::after { border: 0; } |
| | | .circle-ready { background: linear-gradient(150deg, #2d8af0, #1261cf); } |
| | | .circle-warning { background: linear-gradient(150deg, #f2a82d, #dc7815); box-shadow: 0 18rpx 40rpx rgba(211, 125, 21, .24), 0 0 0 12rpx rgba(242, 168, 45, .12); } |
| | | .circle-danger { background: linear-gradient(150deg, #ef7169, #cf3f45); box-shadow: 0 18rpx 40rpx rgba(199, 61, 66, .22), 0 0 0 12rpx rgba(239, 113, 105, .12); } |
| | | .circle-overtime { background: linear-gradient(150deg, #8774e7, #5944bd); box-shadow: 0 18rpx 40rpx rgba(89, 68, 189, .23), 0 0 0 12rpx rgba(135, 116, 231, .12); } |
| | | .circle-cooldown { background: linear-gradient(150deg, #8ca2b8, #607c98); box-shadow: 0 14rpx 34rpx rgba(76, 104, 132, .18), 0 0 0 12rpx rgba(140, 162, 184, .12); } |
| | | .circle-done,.circle-idle { background: #aab5c2; box-shadow: 0 12rpx 30rpx rgba(79, 95, 113, .16), 0 0 0 12rpx rgba(170, 181, 194, .12); } |
| | | .punch-circle[disabled] { opacity: 1; } |
| | | .circle-title { font-size: 36rpx; font-weight: 700; } |
| | | .circle-time { margin-top: 8rpx; font-size: 26rpx; opacity: .82; } |
| | | .smart-hint { color: #425a76; font-size: 25rpx; } |
| | | .rule-time { margin-top: 10rpx; color: #8492a5; font-size: 23rpx; } |
| | | .location-row { display: flex; align-items: center; gap: 20rpx; margin: 0 0 22rpx; padding: 20rpx 24rpx; border: 1rpx solid #dbe4ef; border-radius: 14rpx; background: #fff; } |
| | | .pin { width: 58rpx; height: 58rpx; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; border-radius: 50%; background: #e8f2ff; } |
| | | .pin view { width: 16rpx; height: 16rpx; border: 5rpx solid #1672e8; border-radius: 50%; } |
| | | .location-copy { min-width: 0; flex: 1; } |
| | | .location-copy > text { display: block; color: #73839a; font-size: 22rpx; } |
| | | .location-copy strong { display: block; margin-top: 4rpx; font-size: 28rpx; color: #10233f; } |
| | | .location-copy .location-address { margin-top: 5rpx; color: #667991; } |
| | | .location-copy .location-state { margin-top: 10rpx; color: #1672e8; } |
| | | .location-copy .location-outside,.location-copy .location-poor,.location-copy .location-denied,.location-copy .location-error { color: #c74a42; } |
| | | .location-copy .location-inside,.location-copy .location-success { color: #11865b; } |
| | | .location-copy .location-range { margin-top: 5rpx; color: #8a98aa; } |
| | | .error { margin-bottom: 20rpx; padding: 18rpx 22rpx; border-left: 6rpx solid #ef6a62; border-radius: 8rpx; background: #fff2f1; color: #bd3732; font-size: 24rpx; } |
| | | .section-title { display: flex; align-items: baseline; justify-content: space-between; margin: 34rpx 2rpx 16rpx; } |
| | | .section-title text:first-child { font-size: 30rpx; font-weight: 700; } |
| | | .section-title text:last-child { color: #7a899e; font-size: 23rpx; } |
| | | .timeline { position: relative; padding: 10rpx 24rpx; border: 1rpx solid #dbe4ef; border-radius: 14rpx; background: #fff; } |
| | | .timeline-row { position: relative; z-index: 1; display: flex; align-items: center; gap: 22rpx; padding: 24rpx 0; } |
| | | .dot { width: 22rpx; height: 22rpx; box-sizing: border-box; flex: 0 0 auto; border: 5rpx solid #c4ceda; border-radius: 50%; background: #fff; } |
| | | .dot.done { border-color: #17a56b; background: #17a56b; box-shadow: 0 0 0 8rpx #e2f6ee; } |
| | | .line { position: absolute; top: 64rpx; bottom: 64rpx; left: 34rpx; width: 2rpx; background: #dfe6ef; } |
| | | .event { min-width: 0; flex: 1; } |
| | | .event > view { display: flex; align-items: center; justify-content: space-between; } |
| | | .event-name { font-size: 27rpx; font-weight: 600; } |
| | | .state { color: #8795a7; font-size: 23rpx; } |
| | | .state.done { color: #159765; } |
| | | .event-time { display: block; margin-top: 8rpx; color: #708198; font-size: 23rpx; } |
| | | .work-hours { display: block; margin-top: 14rpx; padding: 0 6rpx; color: #5b7697; font-size: 23rpx; } |
| | | .rule-panel { display: grid; grid-template-columns: 1fr 1fr; border: 1rpx solid #dbe4ef; border-radius: 14rpx; background: #fff; overflow: hidden; } |
| | | .rule-panel view { padding: 24rpx; border-right: 1rpx solid #edf1f6; border-bottom: 1rpx solid #edf1f6; } |
| | | .rule-panel view:nth-child(2n) { border-right: 0; } |
| | | .rule-panel .rule-wide { grid-column: 1 / -1; border-right: 0; } |
| | | .rule-panel view:last-child { border-bottom: 0; } |
| | | .rule-panel text { display: block; color: #7b899d; font-size: 22rpx; } |
| | | .rule-panel strong { display: block; margin-top: 8rpx; font-size: 27rpx; font-weight: 650; } |
| | | .page { |
| | | min-height: 100vh; |
| | | box-sizing: border-box; |
| | | padding: 24rpx; |
| | | background: linear-gradient( |
| | | 155deg, |
| | | #dcecff 0, |
| | | #ebf7f3 34%, |
| | | #f4f7fb 62%, |
| | | #f4f7fb 100% |
| | | ); |
| | | color: #10233f; |
| | | } |
| | | .clock-head { |
| | | padding: 12rpx 4rpx 0; |
| | | } |
| | | .identity { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .identity > view { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 14rpx; |
| | | } |
| | | .welcome { |
| | | color: #61758f; |
| | | font-size: 24rpx; |
| | | } |
| | | .user { |
| | | font-size: 34rpx; |
| | | font-weight: 700; |
| | | } |
| | | .dept { |
| | | padding: 8rpx 14rpx; |
| | | border-radius: 8rpx; |
| | | background: #fff; |
| | | color: #49617e; |
| | | font-size: 23rpx; |
| | | box-shadow: 0 4rpx 14rpx rgba(35, 75, 126, 0.08); |
| | | } |
| | | .date { |
| | | display: block; |
| | | margin-top: 10rpx; |
| | | color: #6f8198; |
| | | font-size: 23rpx; |
| | | } |
| | | .punch-stage { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | padding: 38rpx 0 30rpx; |
| | | } |
| | | .time { |
| | | display: block; |
| | | font-size: 66rpx; |
| | | font-weight: 700; |
| | | font-variant-numeric: tabular-nums; |
| | | letter-spacing: 0; |
| | | } |
| | | .smart-status { |
| | | margin-top: 8rpx; |
| | | padding: 7rpx 18rpx; |
| | | border-radius: 999rpx; |
| | | font-size: 23rpx; |
| | | } |
| | | .status-ready, |
| | | .status-done { |
| | | color: #08784c; |
| | | background: #dcf6eb; |
| | | } |
| | | .status-warning { |
| | | color: #a65b00; |
| | | background: #fff0d5; |
| | | } |
| | | .status-danger { |
| | | color: #c44040; |
| | | background: #ffebeb; |
| | | } |
| | | .status-overtime { |
| | | color: #5e4ab5; |
| | | background: #eeeaff; |
| | | } |
| | | .status-cooldown { |
| | | color: #3f668d; |
| | | background: #e4edf6; |
| | | } |
| | | .status-idle { |
| | | color: #687789; |
| | | background: #e7ebf0; |
| | | } |
| | | .punch-circle { |
| | | width: 286rpx; |
| | | height: 286rpx; |
| | | margin: 28rpx 0 20rpx; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 14rpx solid rgba(255, 255, 255, 0.86); |
| | | border-radius: 50%; |
| | | color: #fff; |
| | | line-height: 1.4; |
| | | box-shadow: 0 18rpx 40rpx rgba(21, 105, 215, 0.26), |
| | | 0 0 0 12rpx rgba(74, 145, 235, 0.11); |
| | | } |
| | | .punch-circle::after { |
| | | border: 0; |
| | | } |
| | | .circle-ready { |
| | | background: linear-gradient(150deg, #2d8af0, #1261cf); |
| | | } |
| | | .circle-warning { |
| | | background: linear-gradient(150deg, #f2a82d, #dc7815); |
| | | box-shadow: 0 18rpx 40rpx rgba(211, 125, 21, 0.24), |
| | | 0 0 0 12rpx rgba(242, 168, 45, 0.12); |
| | | } |
| | | .circle-danger { |
| | | background: linear-gradient(150deg, #ef7169, #cf3f45); |
| | | box-shadow: 0 18rpx 40rpx rgba(199, 61, 66, 0.22), |
| | | 0 0 0 12rpx rgba(239, 113, 105, 0.12); |
| | | } |
| | | .circle-overtime { |
| | | background: linear-gradient(150deg, #8774e7, #5944bd); |
| | | box-shadow: 0 18rpx 40rpx rgba(89, 68, 189, 0.23), |
| | | 0 0 0 12rpx rgba(135, 116, 231, 0.12); |
| | | } |
| | | .circle-cooldown { |
| | | background: linear-gradient(150deg, #8ca2b8, #607c98); |
| | | box-shadow: 0 14rpx 34rpx rgba(76, 104, 132, 0.18), |
| | | 0 0 0 12rpx rgba(140, 162, 184, 0.12); |
| | | } |
| | | .circle-done, |
| | | .circle-idle { |
| | | background: #aab5c2; |
| | | box-shadow: 0 12rpx 30rpx rgba(79, 95, 113, 0.16), |
| | | 0 0 0 12rpx rgba(170, 181, 194, 0.12); |
| | | } |
| | | .punch-circle[disabled] { |
| | | opacity: 1; |
| | | } |
| | | .circle-title { |
| | | font-size: 36rpx; |
| | | font-weight: 700; |
| | | } |
| | | .circle-time { |
| | | margin-top: 8rpx; |
| | | font-size: 26rpx; |
| | | opacity: 0.82; |
| | | } |
| | | .smart-hint { |
| | | color: #425a76; |
| | | font-size: 25rpx; |
| | | } |
| | | .rule-time { |
| | | margin-top: 10rpx; |
| | | color: #8492a5; |
| | | font-size: 23rpx; |
| | | } |
| | | .map-panel { |
| | | margin-bottom: 16rpx; |
| | | padding: 12rpx; |
| | | border: 1rpx solid #dbe4ef; |
| | | border-radius: 18rpx; |
| | | background: #fff; |
| | | box-shadow: 0 8rpx 24rpx rgba(35, 75, 126, 0.07); |
| | | } |
| | | .map-legend { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 28rpx; |
| | | padding: 16rpx 8rpx 6rpx; |
| | | color: #718198; |
| | | font-size: 21rpx; |
| | | } |
| | | .map-legend text { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8rpx; |
| | | } |
| | | .map-legend i { |
| | | width: 14rpx; |
| | | height: 14rpx; |
| | | display: inline-block; |
| | | border-radius: 50%; |
| | | } |
| | | .legend-target { |
| | | background: #1677ff; |
| | | } |
| | | .legend-current { |
| | | background: #13a66a; |
| | | border: 3rpx solid #fff; |
| | | box-shadow: 0 0 0 1rpx #13a66a; |
| | | } |
| | | .map-error { |
| | | display: block; |
| | | padding: 8rpx; |
| | | color: #c74a42; |
| | | font-size: 21rpx; |
| | | } |
| | | .location-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 20rpx; |
| | | margin: 0 0 22rpx; |
| | | padding: 20rpx 24rpx; |
| | | border: 1rpx solid #dbe4ef; |
| | | border-radius: 14rpx; |
| | | background: #fff; |
| | | } |
| | | .pin { |
| | | width: 58rpx; |
| | | height: 58rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex: 0 0 auto; |
| | | border-radius: 50%; |
| | | background: #e8f2ff; |
| | | } |
| | | .pin view { |
| | | width: 16rpx; |
| | | height: 16rpx; |
| | | border: 5rpx solid #1672e8; |
| | | border-radius: 50%; |
| | | } |
| | | .location-copy { |
| | | min-width: 0; |
| | | flex: 1; |
| | | } |
| | | .location-copy > text { |
| | | display: block; |
| | | color: #73839a; |
| | | font-size: 22rpx; |
| | | } |
| | | .location-copy strong { |
| | | display: block; |
| | | margin-top: 4rpx; |
| | | font-size: 28rpx; |
| | | color: #10233f; |
| | | } |
| | | .location-copy .location-address { |
| | | margin-top: 5rpx; |
| | | color: #667991; |
| | | } |
| | | .location-copy .location-state { |
| | | margin-top: 10rpx; |
| | | color: #1672e8; |
| | | } |
| | | .location-copy .location-outside, |
| | | .location-copy .location-denied, |
| | | .location-copy .location-error { |
| | | color: #c74a42; |
| | | } |
| | | .location-copy .location-inside, |
| | | .location-copy .location-success { |
| | | color: #11865b; |
| | | } |
| | | .location-copy .location-range { |
| | | margin-top: 5rpx; |
| | | color: #8a98aa; |
| | | } |
| | | .relocate { |
| | | flex: none; |
| | | min-width: 128rpx; |
| | | min-height: 58rpx; |
| | | padding: 0 16rpx; |
| | | margin: 0; |
| | | color: #1672e8; |
| | | font-size: 22rpx; |
| | | line-height: 56rpx; |
| | | background: #eef6ff; |
| | | border-radius: 9rpx; |
| | | } |
| | | .relocate::after { |
| | | border: 0; |
| | | } |
| | | .error { |
| | | margin-bottom: 20rpx; |
| | | padding: 18rpx 22rpx; |
| | | border-left: 6rpx solid #ef6a62; |
| | | border-radius: 8rpx; |
| | | background: #fff2f1; |
| | | color: #bd3732; |
| | | font-size: 24rpx; |
| | | } |
| | | .section-title { |
| | | display: flex; |
| | | align-items: baseline; |
| | | justify-content: space-between; |
| | | margin: 34rpx 2rpx 16rpx; |
| | | } |
| | | .section-title text:first-child { |
| | | font-size: 30rpx; |
| | | font-weight: 700; |
| | | } |
| | | .section-title text:last-child { |
| | | color: #7a899e; |
| | | font-size: 23rpx; |
| | | } |
| | | .timeline { |
| | | position: relative; |
| | | padding: 10rpx 24rpx; |
| | | border: 1rpx solid #dbe4ef; |
| | | border-radius: 14rpx; |
| | | background: #fff; |
| | | } |
| | | .timeline-row { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 22rpx; |
| | | padding: 24rpx 0; |
| | | } |
| | | .dot { |
| | | width: 22rpx; |
| | | height: 22rpx; |
| | | box-sizing: border-box; |
| | | flex: 0 0 auto; |
| | | border: 5rpx solid #c4ceda; |
| | | border-radius: 50%; |
| | | background: #fff; |
| | | } |
| | | .dot.done { |
| | | border-color: #17a56b; |
| | | background: #17a56b; |
| | | box-shadow: 0 0 0 8rpx #e2f6ee; |
| | | } |
| | | .line { |
| | | position: absolute; |
| | | top: 64rpx; |
| | | bottom: 64rpx; |
| | | left: 34rpx; |
| | | width: 2rpx; |
| | | background: #dfe6ef; |
| | | } |
| | | .event { |
| | | min-width: 0; |
| | | flex: 1; |
| | | } |
| | | .event > view { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .event-name { |
| | | font-size: 27rpx; |
| | | font-weight: 600; |
| | | } |
| | | .state { |
| | | color: #8795a7; |
| | | font-size: 23rpx; |
| | | } |
| | | .state.done { |
| | | color: #159765; |
| | | } |
| | | .event-time { |
| | | display: block; |
| | | margin-top: 8rpx; |
| | | color: #708198; |
| | | font-size: 23rpx; |
| | | } |
| | | .work-hours { |
| | | display: block; |
| | | margin-top: 14rpx; |
| | | padding: 0 6rpx; |
| | | color: #5b7697; |
| | | font-size: 23rpx; |
| | | } |
| | | .rule-panel { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | border: 1rpx solid #dbe4ef; |
| | | border-radius: 14rpx; |
| | | background: #fff; |
| | | overflow: hidden; |
| | | } |
| | | .rule-panel view { |
| | | padding: 24rpx; |
| | | border-right: 1rpx solid #edf1f6; |
| | | border-bottom: 1rpx solid #edf1f6; |
| | | } |
| | | .rule-panel view:nth-child(2n) { |
| | | border-right: 0; |
| | | } |
| | | .rule-panel .rule-wide { |
| | | grid-column: 1 / -1; |
| | | border-right: 0; |
| | | } |
| | | .rule-panel view:last-child { |
| | | border-bottom: 0; |
| | | } |
| | | .rule-panel text { |
| | | display: block; |
| | | color: #7b899d; |
| | | font-size: 22rpx; |
| | | } |
| | | .rule-panel strong { |
| | | display: block; |
| | | margin-top: 8rpx; |
| | | font-size: 27rpx; |
| | | font-weight: 650; |
| | | } |
| | | </style> |