}) | }) | ||||
} | } | ||||
/** | |||||
* 获取服务器当前时间 | |||||
* @returns Promise<Object> 服务器时间信息 | |||||
*/ | |||||
export function getServerTime() { | |||||
return request({ | |||||
url: '/dk/app/getRealTime', | |||||
method: 'get' | |||||
}) | |||||
} | |||||
/** | /** | ||||
* 逆地理编码根据经纬度获取地址 | * 逆地理编码根据经纬度获取地址 | ||||
* @param {*} longitude 经度 | * @param {*} longitude 经度 |
/** | |||||
* 时间服务模块 | |||||
* 提供准确的服务器时间,避免依赖设备本地时间和时区 | |||||
*/ | |||||
import { getServerTime } from "@/api/oa/attendance/clockIn"; | |||||
class TimeService { | |||||
constructor() { | |||||
// 服务器基准时间信息 | |||||
this.serverBaseTime = { | |||||
year: 0, | |||||
month: 0, | |||||
day: 0, | |||||
hour: 0, | |||||
minute: 0, | |||||
second: 0, | |||||
timestamp: 0 | |||||
}; | |||||
// 本地性能计时器基准点(performance.now()的值) | |||||
this.performanceTimeBase = 0; | |||||
// 最后一次同步的performance时间 | |||||
this.lastSyncPerformanceTime = 0; | |||||
// 同步间隔(5分钟) | |||||
this.syncInterval = 5 * 60 * 1000; | |||||
// 是否正在同步 | |||||
this.syncing = false; | |||||
// 是否已成功同步过服务器时间 | |||||
this.hasSyncedServer = false; | |||||
// 时间更新回调函数列表 | |||||
this.updateCallbacks = []; | |||||
// 更新循环是否已启动 | |||||
this.updateLoopStarted = false; | |||||
} | |||||
/** | |||||
* 解析服务器时间字符串为时间组件 | |||||
* @param {string} timeString 时间字符串 (YYYY-MM-DD HH:mm:ss 格式) | |||||
* @returns {Object} 时间组件对象 | |||||
*/ | |||||
parseTimeString(timeString) { | |||||
const parts = timeString.split(' '); | |||||
const datePart = parts[0].split('-'); | |||||
const timePart = parts[1].split(':'); | |||||
return { | |||||
year: parseInt(datePart[0]), | |||||
month: parseInt(datePart[1]), | |||||
day: parseInt(datePart[2]), | |||||
hour: parseInt(timePart[0]), | |||||
minute: parseInt(timePart[1]), | |||||
second: parseInt(timePart[2]), | |||||
timestamp: new Date(timeString.replace(/-/g, '/')).getTime() | |||||
}; | |||||
} | |||||
/** | |||||
* 格式化时间组件为字符串 | |||||
* @param {Object} timeComponents 时间组件 | |||||
* @returns {string} 格式化的时间字符串 | |||||
*/ | |||||
formatTimeComponents(timeComponents) { | |||||
const pad = (num) => String(num).padStart(2, '0'); | |||||
return `${timeComponents.year}-${pad(timeComponents.month)}-${pad(timeComponents.day)} ${pad(timeComponents.hour)}:${pad(timeComponents.minute)}:${pad(timeComponents.second)}`; | |||||
} | |||||
/** | |||||
* 给时间组件添加毫秒数(纯数学计算,不依赖Date对象) | |||||
* @param {Object} baseTime 基准时间组件 | |||||
* @param {number} milliseconds 要添加的毫秒数 | |||||
* @returns {Object} 新的时间组件 | |||||
*/ | |||||
addMilliseconds(baseTime, milliseconds) { | |||||
// 复制基准时间 | |||||
let result = { ...baseTime }; | |||||
// 将毫秒转换为秒并添加 | |||||
const totalSeconds = Math.floor(milliseconds / 1000); | |||||
result.second += totalSeconds; | |||||
// 处理秒的进位 | |||||
if (result.second >= 60) { | |||||
const additionalMinutes = Math.floor(result.second / 60); | |||||
result.second = result.second % 60; | |||||
result.minute += additionalMinutes; | |||||
} | |||||
// 处理分钟的进位 | |||||
if (result.minute >= 60) { | |||||
const additionalHours = Math.floor(result.minute / 60); | |||||
result.minute = result.minute % 60; | |||||
result.hour += additionalHours; | |||||
} | |||||
// 处理小时的进位 | |||||
if (result.hour >= 24) { | |||||
const additionalDays = Math.floor(result.hour / 24); | |||||
result.hour = result.hour % 24; | |||||
result.day += additionalDays; | |||||
// 处理跨月、跨年的复杂情况(简化处理) | |||||
// 这里可以根据需要进一步完善 | |||||
result = this.handleDayOverflow(result); | |||||
} | |||||
// 更新时间戳 | |||||
result.timestamp = baseTime.timestamp + milliseconds; | |||||
return result; | |||||
} | |||||
/** | |||||
* 处理日期的进位(月、年) | |||||
* @param {Object} timeComponents 时间组件 | |||||
* @returns {Object} 处理后的时间组件 | |||||
*/ | |||||
handleDayOverflow(timeComponents) { | |||||
const daysInMonth = this.getDaysInMonth(timeComponents.year, timeComponents.month); | |||||
if (timeComponents.day > daysInMonth) { | |||||
const monthsToAdd = Math.floor((timeComponents.day - 1) / daysInMonth); | |||||
timeComponents.day = ((timeComponents.day - 1) % daysInMonth) + 1; | |||||
timeComponents.month += monthsToAdd; | |||||
if (timeComponents.month > 12) { | |||||
const yearsToAdd = Math.floor((timeComponents.month - 1) / 12); | |||||
timeComponents.month = ((timeComponents.month - 1) % 12) + 1; | |||||
timeComponents.year += yearsToAdd; | |||||
} | |||||
} | |||||
return timeComponents; | |||||
} | |||||
/** | |||||
* 获取指定年月的天数 | |||||
* @param {number} year 年份 | |||||
* @param {number} month 月份 (1-12) | |||||
* @returns {number} 该月的天数 | |||||
*/ | |||||
getDaysInMonth(year, month) { | |||||
// 使用数组而不是Date对象来避免时区问题 | |||||
const daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; | |||||
if (month === 2 && this.isLeapYear(year)) { | |||||
return 29; | |||||
} | |||||
return daysInMonths[month - 1]; | |||||
} | |||||
/** | |||||
* 判断是否为闰年 | |||||
* @param {number} year 年份 | |||||
* @returns {boolean} 是否为闰年 | |||||
*/ | |||||
isLeapYear(year) { | |||||
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); | |||||
} | |||||
/** | |||||
* 同步服务器时间 | |||||
* @returns {Promise<boolean>} 同步是否成功 | |||||
*/ | |||||
async syncServerTime() { | |||||
if (this.syncing) { | |||||
return false; | |||||
} | |||||
this.syncing = true; | |||||
try { | |||||
const syncStartPerformance = performance.now(); | |||||
const response = await getServerTime(); | |||||
const syncEndPerformance = performance.now(); | |||||
if (response.code === 200 && response.data) { | |||||
console.log(response.data); | |||||
// 计算网络延迟 | |||||
const networkDelay = (syncEndPerformance - syncStartPerformance) / 2; | |||||
// 解析服务器时间并添加网络延迟补偿 | |||||
this.serverBaseTime = this.parseTimeString(response.data); | |||||
this.serverBaseTime = this.addMilliseconds(this.serverBaseTime, networkDelay); | |||||
// 记录performance基准时间点 | |||||
this.performanceTimeBase = syncEndPerformance; | |||||
this.lastSyncPerformanceTime = syncEndPerformance; | |||||
this.hasSyncedServer = true; | |||||
console.log("服务器时间同步成功", { | |||||
serverTime: this.formatTimeComponents(this.serverBaseTime), | |||||
performanceBase: this.performanceTimeBase, | |||||
networkDelay, | |||||
}); | |||||
return true; | |||||
} | |||||
} catch (error) { | |||||
console.warn("服务器时间同步失败,将使用本地时间:", error.message); | |||||
} finally { | |||||
this.syncing = false; | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* 获取当前时间组件 | |||||
* @returns {Object} 当前时间组件 | |||||
*/ | |||||
getCurrentTimeComponents() { | |||||
const currentPerformanceTime = performance.now(); | |||||
// 检查是否需要重新同步(基于performance时间) | |||||
if (this.hasSyncedServer && | |||||
currentPerformanceTime - this.lastSyncPerformanceTime > this.syncInterval) { | |||||
// 异步同步,不阻塞当前调用 | |||||
this.syncServerTime(); | |||||
} | |||||
// 如果已同步过服务器时间,使用服务器时间基准 | |||||
if (this.hasSyncedServer) { | |||||
// 计算从基准点开始经过的时间 | |||||
const elapsedTime = currentPerformanceTime - this.performanceTimeBase; | |||||
return this.addMilliseconds(this.serverBaseTime, elapsedTime); | |||||
} | |||||
// 降级到本地时间(解析当前本地时间) | |||||
const now = new Date(); | |||||
const year = now.getFullYear(); | |||||
const month = now.getMonth() + 1; | |||||
const day = now.getDate(); | |||||
const hour = now.getHours(); | |||||
const minute = now.getMinutes(); | |||||
const second = now.getSeconds(); | |||||
return { | |||||
year, month, day, hour, minute, second, | |||||
timestamp: now.getTime() | |||||
}; | |||||
} | |||||
/** | |||||
* 获取当前准确时间(兼容性方法) | |||||
* @returns {Date} 当前时间Date对象 | |||||
*/ | |||||
getCurrentTime() { | |||||
const timeComponents = this.getCurrentTimeComponents(); | |||||
return new Date(timeComponents.timestamp); | |||||
} | |||||
/** | |||||
* 获取格式化的当前时间字符串(用于显示) | |||||
* @returns {string} HH:mm:ss 格式的时间 | |||||
*/ | |||||
getCurrentTimeString() { | |||||
const timeComponents = this.getCurrentTimeComponents(); | |||||
const pad = (num) => String(num).padStart(2, "0"); | |||||
return `${pad(timeComponents.hour)}:${pad(timeComponents.minute)}:${pad(timeComponents.second)}`; | |||||
} | |||||
/** | |||||
* 获取完整的当前日期时间字符串(用于提交) | |||||
* @returns {string} YYYY-MM-DD HH:mm:ss 格式的时间 | |||||
*/ | |||||
getCurrentDateTimeString() { | |||||
const timeComponents = this.getCurrentTimeComponents(); | |||||
const pad = (num) => String(num).padStart(2, "0"); | |||||
return `${timeComponents.year}-${pad(timeComponents.month)}-${pad(timeComponents.day)} ${pad(timeComponents.hour)}:${pad(timeComponents.minute)}:${pad(timeComponents.second)}`; | |||||
} | |||||
/** | |||||
* 比较当前时间与计划时间 | |||||
* @param {string} scheduleTime 计划时间 (HH:mm格式) | |||||
* @returns {number} 比较结果 (-1: 早于, 0: 等于, 1: 晚于) | |||||
*/ | |||||
compareTimeWithSchedule(scheduleTime) { | |||||
const timeComponents = this.getCurrentTimeComponents(); | |||||
const [scheduleHour, scheduleMinute] = scheduleTime.split(":").map(Number); | |||||
const currentMinutes = timeComponents.hour * 60 + timeComponents.minute; | |||||
const scheduleMinutes = scheduleHour * 60 + scheduleMinute; | |||||
return Math.sign(currentMinutes - scheduleMinutes); | |||||
} | |||||
/** | |||||
* 初始化时间服务 | |||||
* @returns {Promise<boolean>} 返回初始同步是否成功 | |||||
*/ | |||||
async initialize() { | |||||
const syncSuccess = await this.syncServerTime(); | |||||
// 使用基于performance时间的定期同步检查 | |||||
// 而不是依赖系统时间的setInterval | |||||
this.startPeriodicSync(); | |||||
return syncSuccess; | |||||
} | |||||
/** | |||||
* 启动基于performance时间的定期同步 | |||||
* 避免受设备时间修改影响 | |||||
*/ | |||||
startPeriodicSync() { | |||||
const checkSync = () => { | |||||
const currentPerformanceTime = performance.now(); | |||||
// 如果距离上次同步超过间隔时间,执行同步 | |||||
if (this.hasSyncedServer && | |||||
currentPerformanceTime - this.lastSyncPerformanceTime > this.syncInterval) { | |||||
this.syncServerTime(); | |||||
} | |||||
// 每30秒检查一次是否需要同步 | |||||
setTimeout(checkSync, 30 * 1000); | |||||
}; | |||||
// 30秒后开始第一次检查 | |||||
setTimeout(checkSync, 30 * 1000); | |||||
} | |||||
/** | |||||
* 添加时间更新回调 | |||||
* @param {Function} callback 回调函数 | |||||
* @returns {Function} 用于移除回调的函数 | |||||
*/ | |||||
onTimeUpdate(callback) { | |||||
this.updateCallbacks.push(callback); | |||||
// 启动更新循环(如果还没启动) | |||||
if (!this.updateLoopStarted) { | |||||
this.startUpdateLoop(); | |||||
} | |||||
// 返回移除回调的函数 | |||||
return () => { | |||||
const index = this.updateCallbacks.indexOf(callback); | |||||
if (index > -1) { | |||||
this.updateCallbacks.splice(index, 1); | |||||
} | |||||
}; | |||||
} | |||||
/** | |||||
* 启动基于requestAnimationFrame的更新循环 | |||||
* 确保稳定的1秒更新频率,不受系统时间影响 | |||||
*/ | |||||
startUpdateLoop() { | |||||
if (this.updateLoopStarted) return; | |||||
this.updateLoopStarted = true; | |||||
let lastUpdateTime = performance.now(); | |||||
const updateLoop = () => { | |||||
const currentTime = performance.now(); | |||||
// 每秒触发一次更新(基于performance时间) | |||||
if (currentTime - lastUpdateTime >= 1000) { | |||||
// 执行所有回调 | |||||
this.updateCallbacks.forEach(callback => { | |||||
try { | |||||
callback(); | |||||
} catch (error) { | |||||
console.error('时间更新回调执行失败:', error); | |||||
} | |||||
}); | |||||
lastUpdateTime = currentTime; | |||||
} | |||||
requestAnimationFrame(updateLoop); | |||||
}; | |||||
requestAnimationFrame(updateLoop); | |||||
} | |||||
} | |||||
// 创建全局时间服务实例 | |||||
const timeService = new TimeService(); | |||||
export default timeService; |
<span>{{ userinfo.userName }}</span> | <span>{{ userinfo.userName }}</span> | ||||
<el-divider direction="vertical" /> | <el-divider direction="vertical" /> | ||||
<span>{{ userinfo.dept.deptName || $t('home.dept.noSet') }}</span> | <span>{{ userinfo.dept.deptName || $t('home.dept.noSet') }}</span> | ||||
<template v-if="userinfo.joinedDate"> | |||||
<el-divider direction="vertical" /> | |||||
<span>{{ $t('home.joinedDate') }}:{{ userinfo.joinedDate }} </span> | |||||
</template> | |||||
</p> | </p> | ||||
<p v-if="hasBirthday" class="birthday-message"> | <p v-if="hasBirthday" class="birthday-message"> | ||||
🎂{{ birthdayMessage }}🎉🎁🎈 | 🎂{{ birthdayMessage }}🎉🎁🎈 |
<div v-else class="container loading"> | <div v-else class="container loading"> | ||||
<div class="loading-text"> | <div class="loading-text"> | ||||
<!-- 正在加载考勤信息... --> | <!-- 正在加载考勤信息... --> | ||||
<i class="el-icon-loading"></i> | |||||
<i class="el-icon-loading" /> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
import LocationInfo from "./components/LocationInfo.vue"; | import LocationInfo from "./components/LocationInfo.vue"; | ||||
import { getGeocode } from "@/api/oa/attendance/clockIn"; | import { getGeocode } from "@/api/oa/attendance/clockIn"; | ||||
import moment from "moment"; | import moment from "moment"; | ||||
import timeService from "@/utils/timeService"; | |||||
export default { | export default { | ||||
name: "MCheckin", | name: "MCheckin", | ||||
components: { | components: { | ||||
// 时间相关 | // 时间相关 | ||||
currentTime: "", | currentTime: "", | ||||
currentCompleteDate: "", | currentCompleteDate: "", | ||||
timeIntervalId: null, | |||||
removeTimeUpdateCallback: null, | |||||
// 打卡成功地址 | // 打卡成功地址 | ||||
formattedAddress: "", | formattedAddress: "", | ||||
hasBirthday() { | hasBirthday() { | ||||
if (!this.userinfo.birthday) return false; | if (!this.userinfo.birthday) return false; | ||||
const today = new Date().toISOString().split("T")[0]; | |||||
// 使用服务器时间组件而不是系统时间 | |||||
const timeComponents = timeService.getCurrentTimeComponents(); | |||||
const birthday = this.userinfo.birthday.split("-"); | const birthday = this.userinfo.birthday.split("-"); | ||||
const month = birthday[1]; | const month = birthday[1]; | ||||
const day = birthday[2]; | const day = birthday[2]; | ||||
const todayMonth = today.split("-")[1]; | |||||
const todayDay = today.split("-")[2]; | |||||
const todayMonth = String(timeComponents.month).padStart(2, "0"); | |||||
const todayDay = String(timeComponents.day).padStart(2, "0"); | |||||
return month == todayMonth && day == todayDay; | return month == todayMonth && day == todayDay; | ||||
}, | }, | ||||
}, | }, | ||||
/** | /** | ||||
* 获取当前时区 | |||||
* 获取当前时区,根据考勤组时区 | |||||
* @returns {string} 时区信息 | * @returns {string} 时区信息 | ||||
*/ | */ | ||||
timeZone() { | timeZone() { | ||||
return Intl.DateTimeFormat().resolvedOptions().timeZone; | |||||
return ( | |||||
this.attendanceGroup.timeZone || | |||||
Intl.DateTimeFormat().resolvedOptions().timeZone | |||||
); | |||||
}, | }, | ||||
/** | /** | ||||
} | } | ||||
await this.initializeApp(); | await this.initializeApp(); | ||||
} catch (error) { | } catch (error) { | ||||
this.handleError(i18n.t("checkin.error.init"), error); | |||||
// this.handleError(this.$i18n.t("checkin.error.init"), error); | |||||
} | } | ||||
}, | }, | ||||
* 初始化应用 | * 初始化应用 | ||||
*/ | */ | ||||
async initializeApp() { | async initializeApp() { | ||||
// 初始化时间服务(获取服务器时间) | |||||
try { | |||||
const syncSuccess = await timeService.initialize(); | |||||
if (!syncSuccess) { | |||||
console.warn("服务器时间同步失败,将使用设备本地时间"); | |||||
// 可以选择性地显示警告提示 | |||||
// this.$toast.warning('时间同步失败,请确保网络连接正常'); | |||||
} | |||||
} catch (error) { | |||||
console.error("时间服务初始化失败:", error); | |||||
// 降级到本地时间,不影响正常功能 | |||||
} | |||||
this.startTimeUpdate(); | this.startTimeUpdate(); | ||||
await this.startLocationTracking(); | await this.startLocationTracking(); | ||||
await this.loadAttendanceData(); | await this.loadAttendanceData(); | ||||
}, | }, | ||||
/** | /** | ||||
* 开始时间更新 | |||||
* 开始时间更新(使用稳定的服务器时间更新机制) | |||||
*/ | */ | ||||
startTimeUpdate() { | startTimeUpdate() { | ||||
this.updateCurrentTime(); | this.updateCurrentTime(); | ||||
this.timeIntervalId = setInterval(() => { | |||||
// 使用timeService的稳定更新回调,不受系统时间影响 | |||||
this.removeTimeUpdateCallback = timeService.onTimeUpdate(() => { | |||||
this.updateCurrentTime(); | this.updateCurrentTime(); | ||||
}, 1000); | |||||
}); | |||||
}, | }, | ||||
/** | /** | ||||
* 更新当前时间显示 | |||||
* 更新当前时间显示(使用服务器时间) | |||||
*/ | */ | ||||
updateCurrentTime() { | updateCurrentTime() { | ||||
this.currentTime = new Date().toLocaleTimeString("zh-CN", { | |||||
hour12: false, | |||||
}); | |||||
this.currentTime = timeService.getCurrentTimeString(); | |||||
}, | }, | ||||
/** | /** | ||||
await AttendanceService.submitPunch(punchData); | await AttendanceService.submitPunch(punchData); | ||||
this.currentCompleteDate = AttendanceService.formatDateTime(new Date()); | |||||
this.currentCompleteDate = AttendanceService.formatDateTime(); | |||||
this.showSuccessDialog = true; | this.showSuccessDialog = true; | ||||
this.formattedAddress = "Loading..."; | this.formattedAddress = "Loading..."; | ||||
if (this.watchId !== null) { | if (this.watchId !== null) { | ||||
navigator.geolocation.clearWatch(this.watchId); | navigator.geolocation.clearWatch(this.watchId); | ||||
} | } | ||||
if (this.timeIntervalId) { | |||||
clearInterval(this.timeIntervalId); | |||||
if (this.removeTimeUpdateCallback) { | |||||
this.removeTimeUpdateCallback(); | |||||
} | } | ||||
}, | }, | ||||
}, | }, |
} from "@/api/oa/attendance/clockIn"; | } from "@/api/oa/attendance/clockIn"; | ||||
import i18n from "@/i18n"; | import i18n from "@/i18n"; | ||||
import timeService from "@/utils/timeService"; | |||||
// 打卡状态常量 | // 打卡状态常量 | ||||
export const PUNCH_STATUS = { | export const PUNCH_STATUS = { | ||||
/** | /** | ||||
* 创建安全的 Date 对象(兼容 IOS 系统) | * 创建安全的 Date 对象(兼容 IOS 系统) | ||||
* @param {string|number|Date} input 输入的日期参数 | * @param {string|number|Date} input 输入的日期参数 | ||||
* @param {boolean} useServerTime 是否使用服务器时间(默认为true) | |||||
* @returns {Date} 有效的 Date 对象 | * @returns {Date} 有效的 Date 对象 | ||||
* @throws {Error} 当无法创建有效日期时抛出错误 | * @throws {Error} 当无法创建有效日期时抛出错误 | ||||
*/ | */ | ||||
static createSafeDate(input) { | |||||
static createSafeDate(input, useServerTime = true) { | |||||
let date; | let date; | ||||
if (!input) { | if (!input) { | ||||
date = new Date(); | |||||
// 如果没有输入参数,优先使用服务器时间 | |||||
date = useServerTime ? timeService.getCurrentTime() : new Date(); | |||||
} else if (input instanceof Date) { | } else if (input instanceof Date) { | ||||
date = input; | date = input; | ||||
} else if (typeof input === "string") { | } else if (typeof input === "string") { | ||||
} | } | ||||
/** | /** | ||||
* 比较当前时间与计划时间 | |||||
* 比较当前时间与计划时间(使用服务器时间) | |||||
* @param {string} scheduleTime 计划时间 (HH:mm格式) | * @param {string} scheduleTime 计划时间 (HH:mm格式) | ||||
* @returns {number} 比较结果 (-1: 早于, 0: 等于, 1: 晚于) | * @returns {number} 比较结果 (-1: 早于, 0: 等于, 1: 晚于) | ||||
*/ | */ | ||||
static compareTimeWithSchedule(scheduleTime) { | static compareTimeWithSchedule(scheduleTime) { | ||||
const now = new Date(); | |||||
const [scheduleHour, scheduleMinute] = scheduleTime.split(":").map(Number); | |||||
const currentMinutes = now.getHours() * 60 + now.getMinutes(); | |||||
const scheduleMinutes = scheduleHour * 60 + scheduleMinute; | |||||
return Math.sign(currentMinutes - scheduleMinutes); | |||||
return timeService.compareTimeWithSchedule(scheduleTime); | |||||
} | } | ||||
/** | /** | ||||
/** | /** | ||||
* 格式化日期时间 | * 格式化日期时间 | ||||
* @param {Date} date 日期对象 | |||||
* @param {Date} date 日期对象,如果不传则使用服务器当前时间 | |||||
* @returns {string} 格式化后的日期时间 | * @returns {string} 格式化后的日期时间 | ||||
*/ | */ | ||||
static formatDateTime(date) { | static formatDateTime(date) { | ||||
return parseTime(date, "{y}-{m}-{d} {h}:{i}:{s}"); | |||||
const targetDate = date || timeService.getCurrentTime(); | |||||
return parseTime(targetDate, "{y}-{m}-{d} {h}:{i}:{s}"); | |||||
} | } | ||||
/** | /** | ||||
const isClockIn = type.includes("morning"); | const isClockIn = type.includes("morning"); | ||||
// 使用安全的日期创建方法 | |||||
// 使用安全的日期创建方法(优先使用服务器时间) | |||||
const currentDate = this.createSafeDate(); | const currentDate = this.createSafeDate(); | ||||
const currentTime = this.formatDateTime(currentDate); | const currentTime = this.formatDateTime(currentDate); | ||||
下班{{ scope.row.workEndTime }},午休:{{ scope.row.lunchTime }} | 下班{{ scope.row.workEndTime }},午休:{{ scope.row.lunchTime }} | ||||
小时 | 小时 | ||||
</p> | </p> | ||||
<p>时区:{{ scope.row.timeZone || "未设置" }}</p> | |||||
<p>关联员工:{{ scope.row.members.length }}人</p> | <p>关联员工:{{ scope.row.members.length }}人</p> | ||||
<p>打卡地点:{{ areaName(scope.row.areaId) || "Loading" }}</p> | <p>打卡地点:{{ areaName(scope.row.areaId) || "Loading" }}</p> | ||||
</template> | </template> | ||||
</el-col> | </el-col> | ||||
</el-row> | </el-row> | ||||
<el-row :gutter="10"> | <el-row :gutter="10"> | ||||
<el-col :span="6"> | |||||
<el-form-item label="考勤时区" prop=""> | |||||
<el-select | |||||
v-model="attendanceGroup.timeZone" | |||||
placeholder="请选择考勤时区" | |||||
> | |||||
<el-option | |||||
v-for="item in timeZones" | |||||
:key="item.value" | |||||
:label="item.label" | |||||
:value="item.value" | |||||
></el-option> | |||||
</el-select> | |||||
</el-form-item> | |||||
</el-col> | |||||
<el-col :span="6"> | <el-col :span="6"> | ||||
<el-form-item label="上班时间" prop="workStartTime"> | <el-form-item label="上班时间" prop="workStartTime"> | ||||
<el-time-picker | <el-time-picker | ||||
checkInType: "location", | checkInType: "location", | ||||
members: [], | members: [], | ||||
areaName: "", | areaName: "", | ||||
timeZone: "", | |||||
}, | }, | ||||
attendanceGroupModel: { | attendanceGroupModel: { | ||||
id: null, | id: null, | ||||
checkInType: "location", | checkInType: "location", | ||||
members: [], | members: [], | ||||
areaName: "", | areaName: "", | ||||
timeZone: "", | |||||
}, | }, | ||||
// 工作日选项 | // 工作日选项 | ||||
workDays: [ | workDays: [ | ||||
areaId: [ | areaId: [ | ||||
{ required: true, message: "请选择打卡区域", trigger: "change" }, | { required: true, message: "请选择打卡区域", trigger: "change" }, | ||||
], | ], | ||||
timeZone: [ | |||||
{ required: true, message: "请选择考勤时区", trigger: "change" }, | |||||
], | |||||
}, | }, | ||||
// 编辑模式 | // 编辑模式 | ||||
isEditMode: false, | isEditMode: false, | ||||
// 提示信息 | // 提示信息 | ||||
successMessage: "", | successMessage: "", | ||||
successMessageVisible: false, | successMessageVisible: false, | ||||
// 考勤时区 | |||||
timeZones: [ | |||||
{ value: "Asia/Shanghai", label: "东八区(Asia/Shanghai)" }, | |||||
{ value: "Asia/Tokyo", label: "东九区(Asia/Tokyo)" }, | |||||
], | |||||
}; | }; | ||||
}, | }, | ||||
computed: { | computed: { | ||||
areaId: this.attendanceGroup.areaId, | areaId: this.attendanceGroup.areaId, | ||||
areaName: "", | areaName: "", | ||||
members: this.attendanceGroup.members, | members: this.attendanceGroup.members, | ||||
timeZone: this.attendanceGroup.timeZone, | |||||
}; | }; | ||||
add(newGroup).then((res) => { | add(newGroup).then((res) => { | ||||
this.getList(); | this.getList(); |
<el-table-column | <el-table-column | ||||
label="考勤组" | label="考勤组" | ||||
prop="attendanceGroupName" | prop="attendanceGroupName" | ||||
width="120" | |||||
></el-table-column> | ></el-table-column> | ||||
<el-table-column label="上班" width="200"> | <el-table-column label="上班" width="200"> | ||||
<template slot-scope="scope"> | <template slot-scope="scope"> | ||||
</span> | </span> | ||||
</template> | </template> | ||||
</el-table-column> | </el-table-column> | ||||
<el-table-column label="最后更新时间" prop="updateTime" width="200"></el-table-column> | |||||
<el-table-column label="备注" prop="description"></el-table-column> | <el-table-column label="备注" prop="description"></el-table-column> | ||||
</el-table> | </el-table> | ||||