|
|
@@ -0,0 +1,384 @@ |
|
|
|
/** |
|
|
|
* 时间服务模块 |
|
|
|
* 提供准确的服务器时间,避免依赖设备本地时间和时区 |
|
|
|
*/ |
|
|
|
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; |