doge 7 часов назад
Родитель
Сommit
9b25d805ae

+ 45 - 1
docs/前端交付/bot-28/后台/后台实时推送-前端接入.md

@@ -272,6 +272,17 @@ Echo 回调收到的对象:
 
 字段含义与各列表接口文档一致,这里不重复。
 
+## 操作弹窗与列表更新
+
+**打开审核、编辑、打款等操作弹窗时,暂停当前列表的自动增删、替换和排序。** 收到推送后只标记「列表有更新」;弹窗全部关闭后再按原筛选、分页刷新一次。断线补发要求 `resync` 时也遵守同一个暂停规则,不能绕过它直接刷新列表。
+
+- 从点击操作按钮、开始读取详情时就暂停;详情请求失败或弹窗实际关闭后再释放。嵌套弹窗全部关闭后才恢复,不能只在点击提交按钮时提前恢复。
+- 弹窗草稿使用独立数据副本,不直接绑定表格行对象;弹窗开关由独立状态控制,表格使用记录 `id` 等稳定主键,避免刷新时重建整个页面。
+- 当前记录被其他人处理或删除时,弹窗提示状态已变化,不自动关闭或覆盖输入;提交时后端仍须校验最新状态,避免重复审核或打款。
+- 暂停范围只覆盖正在操作的列表。平台在线人数和待处理角标继续更新,不用冻结整个后台。
+
+下方参考示例提供 `holdOrderListUpdates()`:操作开始时调用一次,把返回的释放函数绑定到实际弹窗关闭和详情读取失败的路径。前端需接入真实弹窗生命周期,示例不会自动识别业务弹窗。
+
 ## 平台在线人数和待处理角标
 
 两个统计频道均监听 **`.stats.updated`**,`action` 为 `updated`、`key` 为 `{"scope":"all"}`,`data` 是对应 HTTP 接口的完整 `data` 对象。收到后直接替换统计对象,不逐项累加。
@@ -508,11 +519,44 @@ export class AdminRealtime {
 }
 
 // 体彩订单列表页
+let activeOrderDialogs = 0;
+let orderListNeedsSync = false;
+
+// 点击审核/编辑等操作时先调用;返回函数在弹窗实际关闭或详情加载失败时调用。
+// 每次操作各持有一个释放函数,可嵌套,重复调用同一释放函数不会提前解锁。
+function holdOrderListUpdates() {
+  activeOrderDialogs++;
+  let released = false;
+  return async () => {
+    if (released) return;
+    released = true;
+    activeOrderDialogs--;
+    if (activeOrderDialogs === 0 && orderListNeedsSync) {
+      orderListNeedsSync = false;
+      try { await fetchList(); }
+      catch (error) { orderListNeedsSync = true; throw error; }
+    }
+  };
+}
+
+async function resyncOrderList() {
+  if (activeOrderDialogs > 0) {
+    orderListNeedsSync = true;
+    return; // 延后至弹窗关闭;关闭后必须执行上面的完整列表同步
+  }
+  await fetchList();
+}
+
 realtime.watch('admin.orders.sport', ['order.created', 'order.updated', 'order.deleted'], (e) => {
+  if (activeOrderDialogs > 0) {
+    orderListNeedsSync = true;
+    // 可显示「列表有更新」;当前操作记录的状态变化提示由弹窗自己处理。
+    return;
+  }
   const index = rows.findIndex((r) => r.id === e.key.id);
   if (e.action === 'deleted') return index >= 0 && rows.splice(index, 1);
   if (!matchesFilters(e.data)) return index >= 0 && rows.splice(index, 1);
   if (index >= 0) rows.splice(index, 1, e.data);
   else if (e.action === 'created' && page === 1) rows.unshift(e.data);
-}, () => fetchList());
+}, resyncOrderList);
 ```

+ 8 - 2
docs/前端交付/交付清单.json

@@ -252,8 +252,8 @@
       "role": "后台",
       "kind": "正文",
       "git_status": "??",
-      "source_sha256": "a3a4165b57b68690f4411473603767c818091f4d8d7637afb5022a323709351e",
-      "delivery_sha256": "5d3b1cf974a06a413dd8bb9cb3d9f35a9fcf2aa893928e8259c9386e3231d149",
+      "source_sha256": "4847ee80e25838f7b9be376265effe2a446cbe77c1720d326bf677180ebf5197",
+      "delivery_sha256": "7176b8dc8b493fbb4ad754fff7d394590719452f861c6a260a8ac4c43ddc9a1d",
       "revised_on": "2026-11-01"
     }
   ],
@@ -381,6 +381,12 @@
       "project": "bot-28",
       "branch": "master",
       "description": "新增只读 WebSocket 浏览器检测页;新增平台在线人数和待处理角标两个统计频道,沿用 HTTP 返回与权限,按变化推送并支持 seq 补发;同步前端交付副本与校验值。"
+    },
+    {
+      "date": "2026-11-01",
+      "project": "bot-28",
+      "branch": "master",
+      "description": "补充操作弹窗期间暂停当前列表自动更新的交互约定及参考钩子;弹窗关闭后同步列表,统计角标独立更新;同步交付副本与校验值。"
     }
   ]
 }

+ 45 - 1
docs/总后台/后台实时推送-前端接入.md

@@ -272,6 +272,17 @@ Echo 回调收到的对象:
 
 字段含义与各列表接口文档一致,这里不重复。
 
+## 操作弹窗与列表更新
+
+**打开审核、编辑、打款等操作弹窗时,暂停当前列表的自动增删、替换和排序。** 收到推送后只标记「列表有更新」;弹窗全部关闭后再按原筛选、分页刷新一次。断线补发要求 `resync` 时也遵守同一个暂停规则,不能绕过它直接刷新列表。
+
+- 从点击操作按钮、开始读取详情时就暂停;详情请求失败或弹窗实际关闭后再释放。嵌套弹窗全部关闭后才恢复,不能只在点击提交按钮时提前恢复。
+- 弹窗草稿使用独立数据副本,不直接绑定表格行对象;弹窗开关由独立状态控制,表格使用记录 `id` 等稳定主键,避免刷新时重建整个页面。
+- 当前记录被其他人处理或删除时,弹窗提示状态已变化,不自动关闭或覆盖输入;提交时后端仍须校验最新状态,避免重复审核或打款。
+- 暂停范围只覆盖正在操作的列表。平台在线人数和待处理角标继续更新,不用冻结整个后台。
+
+下方参考示例提供 `holdOrderListUpdates()`:操作开始时调用一次,把返回的释放函数绑定到实际弹窗关闭和详情读取失败的路径。前端需接入真实弹窗生命周期,示例不会自动识别业务弹窗。
+
 ## 平台在线人数和待处理角标
 
 两个统计频道均监听 **`.stats.updated`**,`action` 为 `updated`、`key` 为 `{"scope":"all"}`,`data` 是对应 HTTP 接口的完整 `data` 对象。收到后直接替换统计对象,不逐项累加。
@@ -508,11 +519,44 @@ export class AdminRealtime {
 }
 
 // 体彩订单列表页
+let activeOrderDialogs = 0;
+let orderListNeedsSync = false;
+
+// 点击审核/编辑等操作时先调用;返回函数在弹窗实际关闭或详情加载失败时调用。
+// 每次操作各持有一个释放函数,可嵌套,重复调用同一释放函数不会提前解锁。
+function holdOrderListUpdates() {
+  activeOrderDialogs++;
+  let released = false;
+  return async () => {
+    if (released) return;
+    released = true;
+    activeOrderDialogs--;
+    if (activeOrderDialogs === 0 && orderListNeedsSync) {
+      orderListNeedsSync = false;
+      try { await fetchList(); }
+      catch (error) { orderListNeedsSync = true; throw error; }
+    }
+  };
+}
+
+async function resyncOrderList() {
+  if (activeOrderDialogs > 0) {
+    orderListNeedsSync = true;
+    return; // 延后至弹窗关闭;关闭后必须执行上面的完整列表同步
+  }
+  await fetchList();
+}
+
 realtime.watch('admin.orders.sport', ['order.created', 'order.updated', 'order.deleted'], (e) => {
+  if (activeOrderDialogs > 0) {
+    orderListNeedsSync = true;
+    // 可显示「列表有更新」;当前操作记录的状态变化提示由弹窗自己处理。
+    return;
+  }
   const index = rows.findIndex((r) => r.id === e.key.id);
   if (e.action === 'deleted') return index >= 0 && rows.splice(index, 1);
   if (!matchesFilters(e.data)) return index >= 0 && rows.splice(index, 1);
   if (index >= 0) rows.splice(index, 1, e.data);
   else if (e.action === 'created' && page === 1) rows.unshift(e.data);
-}, () => fetchList());
+}, resyncOrderList);
 ```