Digital Office Automation System
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

timeService.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. /**
  2. * 时间服务模块
  3. * 提供准确的服务器时间,避免依赖设备本地时间和时区
  4. */
  5. import { getServerTime } from "@/api/oa/attendance/clockIn";
  6. class TimeService {
  7. constructor() {
  8. // 服务器基准时间信息
  9. this.serverBaseTime = {
  10. year: 0,
  11. month: 0,
  12. day: 0,
  13. hour: 0,
  14. minute: 0,
  15. second: 0,
  16. timestamp: 0
  17. };
  18. // 本地性能计时器基准点(performance.now()的值)
  19. this.performanceTimeBase = 0;
  20. // 最后一次同步的performance时间
  21. this.lastSyncPerformanceTime = 0;
  22. // 同步间隔(5分钟)
  23. this.syncInterval = 5 * 60 * 1000;
  24. // 是否正在同步
  25. this.syncing = false;
  26. // 是否已成功同步过服务器时间
  27. this.hasSyncedServer = false;
  28. // 时间更新回调函数列表
  29. this.updateCallbacks = [];
  30. // 更新循环是否已启动
  31. this.updateLoopStarted = false;
  32. }
  33. /**
  34. * 解析服务器时间字符串为时间组件
  35. * @param {string} timeString 时间字符串 (YYYY-MM-DD HH:mm:ss 格式)
  36. * @returns {Object} 时间组件对象
  37. */
  38. parseTimeString(timeString) {
  39. const parts = timeString.split(' ');
  40. const datePart = parts[0].split('-');
  41. const timePart = parts[1].split(':');
  42. return {
  43. year: parseInt(datePart[0]),
  44. month: parseInt(datePart[1]),
  45. day: parseInt(datePart[2]),
  46. hour: parseInt(timePart[0]),
  47. minute: parseInt(timePart[1]),
  48. second: parseInt(timePart[2]),
  49. timestamp: new Date(timeString.replace(/-/g, '/')).getTime()
  50. };
  51. }
  52. /**
  53. * 格式化时间组件为字符串
  54. * @param {Object} timeComponents 时间组件
  55. * @returns {string} 格式化的时间字符串
  56. */
  57. formatTimeComponents(timeComponents) {
  58. const pad = (num) => String(num).padStart(2, '0');
  59. return `${timeComponents.year}-${pad(timeComponents.month)}-${pad(timeComponents.day)} ${pad(timeComponents.hour)}:${pad(timeComponents.minute)}:${pad(timeComponents.second)}`;
  60. }
  61. /**
  62. * 给时间组件添加毫秒数(纯数学计算,不依赖Date对象)
  63. * @param {Object} baseTime 基准时间组件
  64. * @param {number} milliseconds 要添加的毫秒数
  65. * @returns {Object} 新的时间组件
  66. */
  67. addMilliseconds(baseTime, milliseconds) {
  68. // 复制基准时间
  69. let result = { ...baseTime };
  70. // 将毫秒转换为秒并添加
  71. const totalSeconds = Math.floor(milliseconds / 1000);
  72. result.second += totalSeconds;
  73. // 处理秒的进位
  74. if (result.second >= 60) {
  75. const additionalMinutes = Math.floor(result.second / 60);
  76. result.second = result.second % 60;
  77. result.minute += additionalMinutes;
  78. }
  79. // 处理分钟的进位
  80. if (result.minute >= 60) {
  81. const additionalHours = Math.floor(result.minute / 60);
  82. result.minute = result.minute % 60;
  83. result.hour += additionalHours;
  84. }
  85. // 处理小时的进位
  86. if (result.hour >= 24) {
  87. const additionalDays = Math.floor(result.hour / 24);
  88. result.hour = result.hour % 24;
  89. result.day += additionalDays;
  90. // 处理跨月、跨年的复杂情况(简化处理)
  91. // 这里可以根据需要进一步完善
  92. result = this.handleDayOverflow(result);
  93. }
  94. // 更新时间戳
  95. result.timestamp = baseTime.timestamp + milliseconds;
  96. return result;
  97. }
  98. /**
  99. * 处理日期的进位(月、年)
  100. * @param {Object} timeComponents 时间组件
  101. * @returns {Object} 处理后的时间组件
  102. */
  103. handleDayOverflow(timeComponents) {
  104. const daysInMonth = this.getDaysInMonth(timeComponents.year, timeComponents.month);
  105. if (timeComponents.day > daysInMonth) {
  106. const monthsToAdd = Math.floor((timeComponents.day - 1) / daysInMonth);
  107. timeComponents.day = ((timeComponents.day - 1) % daysInMonth) + 1;
  108. timeComponents.month += monthsToAdd;
  109. if (timeComponents.month > 12) {
  110. const yearsToAdd = Math.floor((timeComponents.month - 1) / 12);
  111. timeComponents.month = ((timeComponents.month - 1) % 12) + 1;
  112. timeComponents.year += yearsToAdd;
  113. }
  114. }
  115. return timeComponents;
  116. }
  117. /**
  118. * 获取指定年月的天数
  119. * @param {number} year 年份
  120. * @param {number} month 月份 (1-12)
  121. * @returns {number} 该月的天数
  122. */
  123. getDaysInMonth(year, month) {
  124. // 使用数组而不是Date对象来避免时区问题
  125. const daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  126. if (month === 2 && this.isLeapYear(year)) {
  127. return 29;
  128. }
  129. return daysInMonths[month - 1];
  130. }
  131. /**
  132. * 判断是否为闰年
  133. * @param {number} year 年份
  134. * @returns {boolean} 是否为闰年
  135. */
  136. isLeapYear(year) {
  137. return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
  138. }
  139. /**
  140. * 同步服务器时间
  141. * @returns {Promise<boolean>} 同步是否成功
  142. */
  143. async syncServerTime() {
  144. if (this.syncing) {
  145. return false;
  146. }
  147. this.syncing = true;
  148. try {
  149. const syncStartPerformance = performance.now();
  150. const response = await getServerTime();
  151. const syncEndPerformance = performance.now();
  152. if (response.code === 200 && response.data) {
  153. console.log(response.data);
  154. // 计算网络延迟
  155. const networkDelay = (syncEndPerformance - syncStartPerformance) / 2;
  156. // 解析服务器时间并添加网络延迟补偿
  157. this.serverBaseTime = this.parseTimeString(response.data);
  158. this.serverBaseTime = this.addMilliseconds(this.serverBaseTime, networkDelay);
  159. // 记录performance基准时间点
  160. this.performanceTimeBase = syncEndPerformance;
  161. this.lastSyncPerformanceTime = syncEndPerformance;
  162. this.hasSyncedServer = true;
  163. console.log("服务器时间同步成功", {
  164. serverTime: this.formatTimeComponents(this.serverBaseTime),
  165. performanceBase: this.performanceTimeBase,
  166. networkDelay,
  167. });
  168. return true;
  169. }
  170. } catch (error) {
  171. console.warn("服务器时间同步失败,将使用本地时间:", error.message);
  172. } finally {
  173. this.syncing = false;
  174. }
  175. return false;
  176. }
  177. /**
  178. * 获取当前时间组件
  179. * @returns {Object} 当前时间组件
  180. */
  181. getCurrentTimeComponents() {
  182. const currentPerformanceTime = performance.now();
  183. // 检查是否需要重新同步(基于performance时间)
  184. if (this.hasSyncedServer &&
  185. currentPerformanceTime - this.lastSyncPerformanceTime > this.syncInterval) {
  186. // 异步同步,不阻塞当前调用
  187. this.syncServerTime();
  188. }
  189. // 如果已同步过服务器时间,使用服务器时间基准
  190. if (this.hasSyncedServer) {
  191. // 计算从基准点开始经过的时间
  192. const elapsedTime = currentPerformanceTime - this.performanceTimeBase;
  193. return this.addMilliseconds(this.serverBaseTime, elapsedTime);
  194. }
  195. // 降级到本地时间(解析当前本地时间)
  196. const now = new Date();
  197. const year = now.getFullYear();
  198. const month = now.getMonth() + 1;
  199. const day = now.getDate();
  200. const hour = now.getHours();
  201. const minute = now.getMinutes();
  202. const second = now.getSeconds();
  203. return {
  204. year, month, day, hour, minute, second,
  205. timestamp: now.getTime()
  206. };
  207. }
  208. /**
  209. * 获取当前准确时间(兼容性方法)
  210. * @returns {Date} 当前时间Date对象
  211. */
  212. getCurrentTime() {
  213. const timeComponents = this.getCurrentTimeComponents();
  214. return new Date(timeComponents.timestamp);
  215. }
  216. /**
  217. * 获取格式化的当前时间字符串(用于显示)
  218. * @returns {string} HH:mm:ss 格式的时间
  219. */
  220. getCurrentTimeString() {
  221. const timeComponents = this.getCurrentTimeComponents();
  222. const pad = (num) => String(num).padStart(2, "0");
  223. return `${pad(timeComponents.hour)}:${pad(timeComponents.minute)}:${pad(timeComponents.second)}`;
  224. }
  225. /**
  226. * 获取完整的当前日期时间字符串(用于提交)
  227. * @returns {string} YYYY-MM-DD HH:mm:ss 格式的时间
  228. */
  229. getCurrentDateTimeString() {
  230. const timeComponents = this.getCurrentTimeComponents();
  231. const pad = (num) => String(num).padStart(2, "0");
  232. return `${timeComponents.year}-${pad(timeComponents.month)}-${pad(timeComponents.day)} ${pad(timeComponents.hour)}:${pad(timeComponents.minute)}:${pad(timeComponents.second)}`;
  233. }
  234. /**
  235. * 比较当前时间与计划时间
  236. * @param {string} scheduleTime 计划时间 (HH:mm格式)
  237. * @returns {number} 比较结果 (-1: 早于, 0: 等于, 1: 晚于)
  238. */
  239. compareTimeWithSchedule(scheduleTime) {
  240. const timeComponents = this.getCurrentTimeComponents();
  241. const [scheduleHour, scheduleMinute] = scheduleTime.split(":").map(Number);
  242. const currentMinutes = timeComponents.hour * 60 + timeComponents.minute;
  243. const scheduleMinutes = scheduleHour * 60 + scheduleMinute;
  244. return Math.sign(currentMinutes - scheduleMinutes);
  245. }
  246. /**
  247. * 初始化时间服务
  248. * @returns {Promise<boolean>} 返回初始同步是否成功
  249. */
  250. async initialize() {
  251. const syncSuccess = await this.syncServerTime();
  252. // 使用基于performance时间的定期同步检查
  253. // 而不是依赖系统时间的setInterval
  254. this.startPeriodicSync();
  255. return syncSuccess;
  256. }
  257. /**
  258. * 启动基于performance时间的定期同步
  259. * 避免受设备时间修改影响
  260. */
  261. startPeriodicSync() {
  262. const checkSync = () => {
  263. const currentPerformanceTime = performance.now();
  264. // 如果距离上次同步超过间隔时间,执行同步
  265. if (this.hasSyncedServer &&
  266. currentPerformanceTime - this.lastSyncPerformanceTime > this.syncInterval) {
  267. this.syncServerTime();
  268. }
  269. // 每30秒检查一次是否需要同步
  270. setTimeout(checkSync, 30 * 1000);
  271. };
  272. // 30秒后开始第一次检查
  273. setTimeout(checkSync, 30 * 1000);
  274. }
  275. /**
  276. * 添加时间更新回调
  277. * @param {Function} callback 回调函数
  278. * @returns {Function} 用于移除回调的函数
  279. */
  280. onTimeUpdate(callback) {
  281. this.updateCallbacks.push(callback);
  282. // 启动更新循环(如果还没启动)
  283. if (!this.updateLoopStarted) {
  284. this.startUpdateLoop();
  285. }
  286. // 返回移除回调的函数
  287. return () => {
  288. const index = this.updateCallbacks.indexOf(callback);
  289. if (index > -1) {
  290. this.updateCallbacks.splice(index, 1);
  291. }
  292. };
  293. }
  294. /**
  295. * 启动基于requestAnimationFrame的更新循环
  296. * 确保稳定的1秒更新频率,不受系统时间影响
  297. */
  298. startUpdateLoop() {
  299. if (this.updateLoopStarted) return;
  300. this.updateLoopStarted = true;
  301. let lastUpdateTime = performance.now();
  302. const updateLoop = () => {
  303. const currentTime = performance.now();
  304. // 每秒触发一次更新(基于performance时间)
  305. if (currentTime - lastUpdateTime >= 1000) {
  306. // 执行所有回调
  307. this.updateCallbacks.forEach(callback => {
  308. try {
  309. callback();
  310. } catch (error) {
  311. console.error('时间更新回调执行失败:', error);
  312. }
  313. });
  314. lastUpdateTime = currentTime;
  315. }
  316. requestAnimationFrame(updateLoop);
  317. };
  318. requestAnimationFrame(updateLoop);
  319. }
  320. }
  321. // 创建全局时间服务实例
  322. const timeService = new TimeService();
  323. export default timeService;