Przeglądaj źródła

feat(订单实时): 支付订单页接线 —— lsy_order+lsy_refund 双通道、操作中挂起、抽屉静默联动

alphaH 1 tydzień temu
rodzic
commit
18f6a1f2a3

+ 4 - 3
frontend/src/api/module_payment/collection.ts

@@ -43,8 +43,8 @@ export function getLsyOrderPage(
   return request({ url: "/payment/collection/order/page", method: "get", params, silent });
 }
 
-export function getLsyOrderDetail(outTradeNo: string) {
-  return request({ url: `/payment/collection/order/${outTradeNo}`, method: "get" });
+export function getLsyOrderDetail(outTradeNo: string, silent = false) {
+  return request({ url: `/payment/collection/order/${outTradeNo}`, method: "get", silent });
 }
 
 export function getLsyNotifyLogs(outTradeNo: string) {
@@ -61,10 +61,11 @@ export function getLsyNotifyLogs(outTradeNo: string) {
  *   给的是「订单号**包含**本单号」的退款单,而抽屉要的是「**属于**本单的」。
  *   两者在订单号互为前缀时会给出不同的行,且客户端无法把它收敛回去。
  */
-export function getLsyOrderRefunds(outTradeNo: string) {
+export function getLsyOrderRefunds(outTradeNo: string, silent = false) {
   return request({
     url: `/payment/collection/order/${outTradeNo}/refunds`,
     method: "get",
+    silent,
   });
 }
 

+ 76 - 5
frontend/src/views/module_payment/collection/order/index.vue

@@ -182,7 +182,7 @@
         :page-sizes="[10, 20, 50, 100]"
         layout="total, sizes, prev, pager, next, jumper"
         style="margin-top: 12px; justify-content: flex-end"
-        @change="loadData"
+        @change="() => loadData()"
       />
     </el-card>
 
@@ -440,6 +440,7 @@ import {
 } from "@/api/module_payment/collection";
 import TenantAPI, { type TenantTable } from "@/api/module_system/tenant";
 import { useUserStore } from "@/store";
+import { useLiveRefetch } from "@/composables";
 
 const userStore = useUserStore();
 
@@ -874,10 +875,14 @@ function refundTagType(v: string) {
   if (v === "FAIL") return "danger";
   return "warning";
 }
-async function loadData() {
-  loading.value = true;
+async function loadData(silent = false) {
+  // ★ 静默刷新不碰 loading:它是一个全局开关,后台那一次把它拉起来,
+  //   正在看页面的人会看到表格「闪一下转圈」—— 这正是「用户无感知」禁止的动静。
+  if (!silent) {
+    loading.value = true;
+  }
   try {
-    const res: any = await getLsyOrderPage(query);
+    const res: any = await getLsyOrderPage(query, silent);
     // res 是 AxiosResponse(request.ts 的响应拦截器 `return response`,**不做解包**)
     // → res.data = {code,msg,data}  → res.data.data = PageResult
     // PageResult 的分页字段名是 `list`(不是 records/items);`total` 是单词,命名策略不影响它。
@@ -885,10 +890,72 @@ async function loadData() {
     list.value = res.data.data?.list || [];
     total.value = res.data.data?.total || 0;
   } finally {
-    loading.value = false;
+    if (!silent) {
+      loading.value = false;
+    }
   }
 }
 
+// ─────────────────── 实时联动(2026-10-05)───────────────────
+
+/**
+ * 退款确认框开着时也算「操作中」。
+ * 弹窗期间列表在脚下换行,操作员会重新核对金额 —— 而那个金额是从**行对象**里读的快照。
+ */
+const confirmingRefund = ref(false);
+
+/**
+ * 什么算「用户正在操作」,信号在此期间挂起:
+ * 勾选着行 / 批量退款的两个弹窗 / 提交与轮询在跑 / 退款确认框开着。
+ *
+ * ★ 详情抽屉**不在**这里 —— 抽屉要跟着实时变(见计划头「与已批 spec 的偏差」①):
+ *   收银台的人正盯着这一单等它从「支付中」翻成「支付成功」,挂起等于把这个功能关掉。
+ */
+const liveBusy = computed(
+  () =>
+    selectedRows.value.length > 0 ||
+    batchRefundVisible.value ||
+    batchRefundResultVisible.value ||
+    batchRefundRunning.value ||
+    confirmingRefund.value
+);
+
+/**
+ * 抽屉开着时的静默联动:两个请求都回来、且还是同一单,才整体覆盖。
+ *
+ * ★ 为什么不复用 `openDetail`:它**刻意先清空**(防上一次的内容在请求失败时冒充这一单),
+ *   那是「人点开某一单」的前提;后台刷新没有点错单的可能,闪一下空白反倒成了可见的动静。
+ * ★ 两个请求一起 await:只覆盖一半(比如详情新了、退款单还是旧的)会让界面自相矛盾。
+ */
+async function refreshOpenDetail() {
+  const no = detail.value?.out_trade_no;
+  if (!detailVisible.value || !no) {
+    return;
+  }
+  try {
+    const [d, rfs]: any[] = await Promise.all([
+      getLsyOrderDetail(no, true),
+      getLsyOrderRefunds(no, true),
+    ]);
+    // 请求在飞的时候人可能已经关了抽屉、或点开了另一单
+    if (detailVisible.value && detail.value?.out_trade_no === no) {
+      detail.value = d.data.data;
+      refunds.value = rfs.data.data || [];
+    }
+  } catch {
+    // 后台刷新失败静默:留着上一次的数据,比清空好
+  }
+}
+
+useLiveRefetch(
+  ["lsy_order", "lsy_refund"],
+  async () => {
+    await refreshOpenDetail();
+    await loadData(true);
+  },
+  { busy: liveBusy }
+);
+
 function handleSearch() {
   query.pageNo = 1;
   loadData();
@@ -951,6 +1018,7 @@ async function handleRefund(row: any) {
   }
 
   let input = "";
+  confirmingRefund.value = true;
   try {
     const { value } = await ElMessageBox.prompt(
       `订单 ${row.out_trade_no}:共 ${fen2yuan(row.total_amount)} 元,` +
@@ -969,6 +1037,9 @@ async function handleRefund(row: any) {
     input = value ?? "";
   } catch {
     return; // 取消(ElMessageBox 以 "cancel" reject),不发请求
+  } finally {
+    // finally 在 catch 里那个 return 之前执行 ⇒ 取消路径也会复位,不会把页面永久卡在 busy
+    confirmingRefund.value = false;
   }
 
   // 元 → 分。`Math.round` 而不是 `* 100` 取整:0.29 * 100 在二进制浮点下是 28.999…。