소스 검색

feat(attendance): 增加请求取消功能,优化日期切换时的考勤数据加载

xieyong 4 일 전
부모
커밋
618669d56c
3개의 변경된 파일86개의 추가작업 그리고 6개의 파일을 삭제
  1. 23 3
      src/api/attendance.js
  2. 20 1
      src/pages/attendance/index.vue
  3. 43 2
      src/utils/auth.js

+ 23 - 3
src/api/attendance.js

@@ -244,15 +244,35 @@ function isServerMode() {
 /**
  * 查询每日考勤(管理端完整模型)
  * GET /hr/attendance/daily/{userId}/{date}
+ * @param {object} options
+ * @param {AbortSignal} [options.signal] 可选;切换日期时取消上一个未完成的请求
  */
-export async function queryDailyAttendance(userId, date) {
+export async function queryDailyAttendance(userId, date, { signal } = {}) {
   if (!isServerMode()) {
-    await new Promise((r) => setTimeout(r, 200))
+    // 演示模式没有真请求,用 setTimeout 模拟;中途也要响应 abort
+    if (signal?.aborted) {
+      const err = new Error('aborted')
+      err.name = 'AbortError'
+      throw err
+    }
+    await new Promise((resolve, reject) => {
+      const timer = setTimeout(resolve, 200)
+      if (signal) {
+        const onAbort = () => {
+          clearTimeout(timer)
+          const err = new Error('aborted')
+          err.name = 'AbortError'
+          reject(err)
+        }
+        if (signal.aborted) onAbort()
+        else signal.addEventListener('abort', onAbort, { once: true })
+      }
+    })
     const vo = ensureDemoDaily(date)
     return { ...vo }
   }
   const path = `/hr/attendance/daily/${userId}/${date}`
-  const res = await request({ url: path, method: 'GET' })
+  const res = await request({ url: path, method: 'GET', signal })
   return res.data || null
 }
 

+ 20 - 1
src/pages/attendance/index.vue

@@ -342,19 +342,38 @@ async function loadMonth(month) {
   }
 }
 
+// 记录当前在飞的 daily 请求 AbortController,切换日期时取消上一个
+let dailyAbortController = null;
+
 async function selectDate(date) {
+  // 切换日期前:取消上一个还在飞的请求,避免它的结果覆盖新选中日期的数据
+  if (dailyAbortController) {
+    dailyAbortController.abort();
+    dailyAbortController = null;
+  }
+  const controller = new AbortController();
+  dailyAbortController = controller;
   selectedDate.value = date;
   try {
     const user = getCurrentUser() || currentUser || {};
     const userId = (user && user.raw && user.raw.userId) || (user && user.raw && user.raw.id) || (user && user.id) || "";
-    const result = await queryDailyAttendance(userId, date);
+    const result = await queryDailyAttendance(userId, date, { signal: controller.signal });
+    // 期间用户又切到别的日期了,丢弃这次响应
+    if (controller.signal.aborted) return;
     // 后端未返回数据(null/undefined)→ 显式清空,
     // 否则 record 卡片会继续展示上一个日期的记录,与 selectedDate 不同步
     dailyDetail.value = result || null;
   } catch (error) {
+    // 主动取消:不弹 toast、不清数据(可能已经被新请求覆盖)
+    if (error?.name === 'AbortError' || controller.signal.aborted) return;
     // 接口异常时也清空,避免 toast 提示完之后 record 卡片还停在旧数据
     dailyDetail.value = null;
     uni.showToast({ title: error.message || "日期详情加载失败", icon: "none" });
+  } finally {
+    // 只清掉自己的 controller,避免覆盖掉期间已经开始的新请求
+    if (dailyAbortController === controller) {
+      dailyAbortController = null;
+    }
   }
 }
 

+ 43 - 2
src/utils/auth.js

@@ -61,6 +61,7 @@ export function request({
   token,
   sessionId,
   timeout = 15000,
+  signal,
 }) {
   return new Promise((resolve, reject) => {
     if (!url) {
@@ -89,13 +90,28 @@ export function request({
         }
       : { "content-type": "application/json", platform: "wxapp" };
 
-    uni.request({
+    // 已预先 abort:立刻 reject,不发请求
+    if (signal?.aborted) {
+      const err = new Error("aborted");
+      err.name = "AbortError";
+      reject(err);
+      return;
+    }
+
+    const task = uni.request({
       url: finalUrl,
       method,
       data,
       header,
       timeout,
       success: (response) => {
+        // 响应到达前 signal 被触发:当作 abort 处理,避免用过期数据覆盖 UI
+        if (signal?.aborted) {
+          const err = new Error("aborted");
+          err.name = "AbortError";
+          reject(err);
+          return;
+        }
         if (response.statusCode === 401) {
           // 服务端认为凭证过期:清本地会话 + 清考勤缓存 + 跳登录页
           logout();
@@ -124,8 +140,33 @@ export function request({
         }
         resolve(body);
       },
-      fail: (error) => reject(new Error(error.errMsg || "无法连接服务器")),
+      fail: (error) => {
+        // task.abort() 触发的失败:errMsg 形如 "request:fail abort",
+        // 与"网络断开"区分开,统一按 AbortError 上抛
+        if (signal?.aborted || /abort/i.test(String(error?.errMsg || ""))) {
+          const err = new Error("aborted");
+          err.name = "AbortError";
+          reject(err);
+          return;
+        }
+        reject(new Error(error.errMsg || "无法连接服务器"));
+      },
     });
+
+    // 把 signal 接到 task.abort() 上:signal 一旦触发就取消底层请求
+    if (signal) {
+      signal.addEventListener(
+        "abort",
+        () => {
+          try {
+            task.abort();
+          } catch {
+            // uni.request 在请求完成后再 abort 会抛错,无视即可
+          }
+        },
+        { once: true },
+      );
+    }
   });
 }