Преглед на файлове

fix: 批量付款前端最终审查修复 - 分页list解包/支付页新窗口/场景报备必填/刷新同步

alphaH преди 6 дни
родител
ревизия
a22642c787

+ 16 - 3
frontend/src/api/module_payment/batch.ts

@@ -14,6 +14,19 @@ export interface BatchAuthorizeVO {
   created_time?: string;
 }
 
+/**
+ * 分页结果类型 — 后端 PageResult 全局 SNAKE_CASE 序列化后 JSON key 为 `list`
+ * (全局 PageResult<T> 类型声明为 items,与后端契约不符,本模块以后端实际契约为准,
+ * 保留 items 兼容字段以支持 `?.items ?? ?.list ?? []` 双保险解包,与存量 index.vue 一致)
+ */
+export interface BatchPageResult<T> {
+  items?: T;
+  list: T;
+  total: number;
+  page_no: number;
+  page_size: number;
+}
+
 /** 批次(后端 BatchOrderEntity) */
 export interface BatchOrderVO {
   out_batch_no: string;
@@ -83,7 +96,7 @@ export const BatchPayAPI = {
 
   /** 制单授权记录(分页) */
   authorizeList(params: { enterprise_id?: string; page_no?: number; page_size?: number }) {
-    return request<ApiResponse<PageResult<BatchAuthorizeVO[]>>>({
+    return request<ApiResponse<BatchPageResult<BatchAuthorizeVO[]>>>({
       url: `${API_PATH}/authorize/list`,
       method: "get",
       params,
@@ -135,7 +148,7 @@ export const BatchPayAPI = {
     page_no?: number;
     page_size?: number;
   }) {
-    return request<ApiResponse<PageResult<BatchOrderVO[]>>>({
+    return request<ApiResponse<BatchPageResult<BatchOrderVO[]>>>({
       url: `${API_PATH}/list`,
       method: "get",
       params,
@@ -144,7 +157,7 @@ export const BatchPayAPI = {
 
   /** 批次详情 + 明细分页(后端要求 enterprise_id 租户隔离) */
   batchDetail(enterpriseId: string, outBatchNo: string, pageNo = 1, pageSize = 20) {
-    return request<ApiResponse<{ order: BatchOrderVO; details: PageResult<BatchDetailItem[]> }>>({
+    return request<ApiResponse<{ order: BatchOrderVO; details: BatchPageResult<BatchDetailItem[]> }>>({
       url: `${API_PATH}/detail`,
       method: "get",
       params: {

+ 13 - 3
frontend/src/views/module_payment/account/components/BatchPayCreate.vue

@@ -23,7 +23,7 @@
           style="max-width: 420px"
         />
       </el-form-item>
-      <el-form-item label="转账场景">
+      <el-form-item label="转账场景" prop="transfer_scene_name">
         <el-select
           v-model="form.transfer_scene_name"
           placeholder="请选择转账场景"
@@ -33,7 +33,7 @@
           <el-option v-for="s in SCENE_OPTIONS" :key="s" :label="s" :value="s" />
         </el-select>
       </el-form-item>
-      <el-form-item label="场景报备信息">
+      <el-form-item label="场景报备信息" prop="scene_report" required>
         <div
           v-for="(r, idx) in reportInfos"
           :key="idx"
@@ -50,7 +50,7 @@
         >
           添加报备信息
         </el-button>
-        <div class="form-item-tip">2026 年新接入商户填,按支付宝要求填写</div>
+        <div class="form-item-tip">2026 年新接入商户填,按支付宝要求填写</div>
       </el-form-item>
       <el-form-item label="超时时间">
         <el-date-picker
@@ -208,6 +208,8 @@ const form = reactive({
 const rules: FormRules = {
   order_title: [{ required: true, message: "请输入批次标题", trigger: "blur" }],
   payer_uid: [{ required: true, message: "请输入付款方支付宝 UID", trigger: "blur" }],
+  // I3: 26 年新接入商户必传转账场景(后端 DTO 同步 @NotBlank 校验)
+  transfer_scene_name: [{ required: true, message: "请选择转账场景", trigger: "change" }],
 };
 
 /** 26 年新接入商户可选转账场景(与后端 BatchCreateDTO 注释一致) */
@@ -397,6 +399,14 @@ async function handleSubmit() {
   }
   const valid = await formRef.value?.validate().catch(() => false);
   if (!valid) return;
+  // I3: 场景报备信息必填(后端 DTO 同步 @NotEmpty 校验),至少一条 info_type/info_content 非空
+  const hasValidReport = reportInfos.value.some(
+    (r) => r.info_type.trim() !== "" && r.info_content.trim() !== ""
+  );
+  if (!hasValidReport) {
+    ElMessage.warning("请填写至少一条完整的场景报备信息(类型与内容均必填)");
+    return;
+  }
   if (detailRows.value.length === 0) {
     ElMessage.warning("请至少添加一条收款明细");
     return;

+ 29 - 5
frontend/src/views/module_payment/account/components/BatchPayDetail.vue

@@ -21,7 +21,7 @@
           >
             关闭批次
           </el-button>
-          <el-button icon="Refresh" @click="load">刷新</el-button>
+          <el-button icon="Refresh" :loading="refreshing" @click="refresh">刷新</el-button>
         </div>
       </div>
     </template>
@@ -108,6 +108,7 @@ const pageNo = ref(1);
 const pageSize = ref(20);
 const loading = ref(false);
 const payLoading = ref(false);
+const refreshing = ref(false);
 
 type TagType = "primary" | "success" | "warning" | "info" | "danger";
 const STATUS_TAG: Record<string, TagType> = {
@@ -147,13 +148,31 @@ async function load() {
       pageSize.value
     );
     order.value = res.data.data?.order || null;
-    details.value = res.data.data?.details?.items || [];
+    // 双保险解包: 后端 PageResult 序列化字段是 list(与存量 index.vue 一致)
+    details.value = res.data.data?.details?.items || res.data.data?.details?.list || [];
     total.value = res.data.data?.details?.total || 0;
   } finally {
     loading.value = false;
   }
 }
 
+/** 刷新: 先调 batchQuery 同步支付宝侧批次+明细状态,再拉取本地详情展示(I1 手动兜底) */
+async function refresh() {
+  if (!enterpriseId.value) {
+    ElMessage.warning("未获取到企业信息");
+    return;
+  }
+  refreshing.value = true;
+  try {
+    await BatchPayAPI.batchQuery(enterpriseId.value, props.outBatchNo);
+  } catch {
+    // 同步失败不阻塞本地展示(batchQuery 失败原因已由 request.ts 提示)
+  } finally {
+    refreshing.value = false;
+  }
+  await load();
+}
+
 async function handlePay() {
   if (!order.value || !enterpriseId.value) return;
   payLoading.value = true;
@@ -165,9 +184,14 @@ async function handlePay() {
       ElMessage.warning("未获取到支付页面");
       return;
     }
-    document.open();
-    document.write(payHtml);
-    document.close();
+    // 新窗口打开支付页(设计 M4): 避免 document.write 同页替换销毁当前 SPA
+    const w = window.open("about:blank", "_blank");
+    if (w) {
+      w.document.write(payHtml);
+      w.document.close();
+    } else {
+      ElMessage.warning("支付页弹窗被浏览器拦截,请允许弹窗后重试");
+    }
   } finally {
     payLoading.value = false;
   }

+ 12 - 6
frontend/src/views/module_payment/account/components/BatchPayList.vue

@@ -180,8 +180,9 @@ async function load() {
       page_no: pageNo.value,
       page_size: pageSize.value,
     });
-    list.value = res.data.data?.items || [];
-    total.value = res.data.data.total || 0;
+    // 双保险解包: 后端 PageResult 序列化字段是 list(全局类型声明 items 与实际不符),与存量 index.vue 一致
+    list.value = res.data.data?.items || res.data.data?.list || [];
+    total.value = res.data.data?.total || 0;
   } finally {
     loading.value = false;
   }
@@ -226,9 +227,14 @@ async function handlePay(row: BatchOrderVO) {
       ElMessage.warning("未获取到支付页面");
       return;
     }
-    document.open();
-    document.write(payHtml);
-    document.close();
+    // 新窗口打开支付页(设计 M4): 避免 document.write 同页替换销毁当前 SPA
+    const w = window.open("about:blank", "_blank");
+    if (w) {
+      w.document.write(payHtml);
+      w.document.close();
+    } else {
+      ElMessage.warning("支付页弹窗被浏览器拦截,请允许弹窗后重试");
+    }
   } finally {
     payingNo.value = "";
   }
@@ -272,7 +278,7 @@ async function handleExport() {
     window.URL.revokeObjectURL(url);
     ElMessage.success("导出成功");
   } catch {
-    ElMessage.error("导出失败,请稍后重试");
+    // N1: request.ts blob 分支已解析后端 JSON 并显示具体失败原因,此处不再二次提示
   } finally {
     exportLoading.value = false;
   }