BatchPayDetail.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. <template>
  2. <el-card v-loading="loading">
  3. <template v-if="order" #header>
  4. <div class="card-header">
  5. <span>批次详情 {{ order.out_batch_no }}</span>
  6. <div>
  7. <el-button
  8. v-if="order.status === 'INIT'"
  9. v-hasPerm="['module_payment:account:transfer']"
  10. type="primary"
  11. :loading="payLoading"
  12. @click="handlePay"
  13. >
  14. 生成支付链接
  15. </el-button>
  16. <el-button
  17. v-if="order.status === 'INIT'"
  18. v-hasPerm="['module_payment:account:transfer']"
  19. type="danger"
  20. @click="handleClose"
  21. >
  22. 关闭批次
  23. </el-button>
  24. <el-button icon="Refresh" :loading="refreshing" @click="refresh">刷新</el-button>
  25. </div>
  26. </div>
  27. </template>
  28. <el-descriptions v-if="order" :column="3" border style="margin-bottom: 16px">
  29. <el-descriptions-item label="标题">{{ order.order_title || "-" }}</el-descriptions-item>
  30. <el-descriptions-item label="总金额(元)">¥{{ order.total_amount }}</el-descriptions-item>
  31. <el-descriptions-item label="笔数">{{ order.total_count }}</el-descriptions-item>
  32. <el-descriptions-item label="状态">
  33. <el-tag :type="STATUS_TAG[order.status] || 'info'">
  34. {{ STATUS_TEXT[order.status] || order.status }}
  35. </el-tag>
  36. </el-descriptions-item>
  37. <el-descriptions-item label="支付宝批次号">
  38. {{ order.batch_trans_id || "-" }}
  39. </el-descriptions-item>
  40. <el-descriptions-item label="创建时间">
  41. {{ order.created_time ? dayjs(order.created_time).format("YYYY-MM-DD HH:mm:ss") : "-" }}
  42. </el-descriptions-item>
  43. <el-descriptions-item label="转账场景">
  44. {{ order.transfer_scene_name || "-" }}
  45. </el-descriptions-item>
  46. <el-descriptions-item label="付款方UID">{{ order.payer_uid || "-" }}</el-descriptions-item>
  47. <el-descriptions-item label="备注">{{ order.remark || "-" }}</el-descriptions-item>
  48. <el-descriptions-item v-if="order.error_msg" label="错误信息">
  49. <span style="color: #f56c6c">{{ order.error_msg }}</span>
  50. </el-descriptions-item>
  51. </el-descriptions>
  52. <el-table v-if="order" :data="details" border stripe>
  53. <template #empty>
  54. <el-empty description="暂无数据" />
  55. </template>
  56. <el-table-column prop="out_biz_no" label="明细单号" min-width="200" />
  57. <el-table-column prop="payee_name" label="收款方姓名" width="120">
  58. <template #default="{ row }">{{ row.payee_name || "-" }}</template>
  59. </el-table-column>
  60. <el-table-column prop="payee_identity" label="收款方账号" width="200" />
  61. <el-table-column prop="amount" label="金额(元)" width="110">
  62. <template #default="{ row }">¥{{ row.amount }}</template>
  63. </el-table-column>
  64. <el-table-column label="状态" width="90">
  65. <template #default="{ row }">
  66. <el-tag :type="DETAIL_STATUS_TAG[row.status] || 'info'">
  67. {{ DETAIL_STATUS_TEXT[row.status] || row.status }}
  68. </el-tag>
  69. </template>
  70. </el-table-column>
  71. <el-table-column prop="error_msg" label="失败原因" min-width="140">
  72. <template #default="{ row }">{{ row.error_msg || "-" }}</template>
  73. </el-table-column>
  74. </el-table>
  75. <div v-if="order" class="mt-4 flex justify-end">
  76. <el-pagination
  77. v-model:current-page="pageNo"
  78. v-model:page-size="pageSize"
  79. :total="total"
  80. :page-sizes="[10, 20, 50, 100]"
  81. layout="total, sizes, prev, pager, next, jumper"
  82. @size-change="load"
  83. @current-change="load"
  84. />
  85. </div>
  86. </el-card>
  87. </template>
  88. <script setup lang="ts">
  89. import { computed, onMounted, ref } from "vue";
  90. import BatchPayAPI, { type BatchDetailItem, type BatchOrderVO } from "@/api/module_payment/batch";
  91. import { useEnterpriseStore } from "@/store";
  92. import { ElMessage, ElMessageBox } from "element-plus";
  93. import dayjs from "dayjs";
  94. const props = defineProps<{ outBatchNo: string; enterpriseId?: string }>();
  95. const enterpriseStore = useEnterpriseStore();
  96. const currentEnterpriseId = computed(() => enterpriseStore.getCurrentEnterprise?.enterprise_id);
  97. const enterpriseId = computed(() => props.enterpriseId || currentEnterpriseId.value);
  98. const order = ref<BatchOrderVO | null>(null);
  99. const details = ref<BatchDetailItem[]>([]);
  100. const total = ref(0);
  101. const pageNo = ref(1);
  102. const pageSize = ref(20);
  103. const loading = ref(false);
  104. const payLoading = ref(false);
  105. const refreshing = ref(false);
  106. type TagType = "primary" | "success" | "warning" | "info" | "danger";
  107. const STATUS_TAG: Record<string, TagType> = {
  108. INIT: "warning",
  109. SUCCESS: "success",
  110. DISUSE: "info",
  111. FAIL: "danger",
  112. };
  113. const STATUS_TEXT: Record<string, string> = {
  114. INIT: "受理中",
  115. SUCCESS: "成功",
  116. DISUSE: "已关闭",
  117. FAIL: "失败",
  118. };
  119. const DETAIL_STATUS_TAG: Record<string, TagType> = {
  120. INIT: "info",
  121. SUCCESS: "success",
  122. FAIL: "danger",
  123. };
  124. const DETAIL_STATUS_TEXT: Record<string, string> = {
  125. INIT: "处理中",
  126. SUCCESS: "成功",
  127. FAIL: "失败",
  128. };
  129. async function load() {
  130. if (!enterpriseId.value) {
  131. ElMessage.warning("未获取到企业信息");
  132. return;
  133. }
  134. loading.value = true;
  135. try {
  136. const res = await BatchPayAPI.batchDetail(
  137. enterpriseId.value,
  138. props.outBatchNo,
  139. pageNo.value,
  140. pageSize.value
  141. );
  142. order.value = res.data.data?.order || null;
  143. // 双保险解包: 后端 PageResult 序列化字段是 list(与存量 index.vue 一致)
  144. details.value = res.data.data?.details?.items || res.data.data?.details?.list || [];
  145. total.value = res.data.data?.details?.total || 0;
  146. } finally {
  147. loading.value = false;
  148. }
  149. }
  150. /** 刷新: 先调 batchQuery 同步支付宝侧批次+明细状态,再拉取本地详情展示(I1 手动兜底) */
  151. async function refresh() {
  152. if (!enterpriseId.value) {
  153. ElMessage.warning("未获取到企业信息");
  154. return;
  155. }
  156. refreshing.value = true;
  157. try {
  158. await BatchPayAPI.batchQuery(enterpriseId.value, props.outBatchNo);
  159. } catch {
  160. // 同步失败不阻塞本地展示(batchQuery 失败原因已由 request.ts 提示)
  161. } finally {
  162. refreshing.value = false;
  163. }
  164. await load();
  165. }
  166. async function handlePay() {
  167. if (!order.value || !enterpriseId.value) return;
  168. payLoading.value = true;
  169. try {
  170. const res = await BatchPayAPI.renderPay(enterpriseId.value, order.value.out_batch_no);
  171. // pageExecute 返回 HTML 表单,自带自动提交(与充值/签约链接处理一致)
  172. const payHtml = res.data.data?.pay_url || "";
  173. if (!payHtml) {
  174. ElMessage.warning("未获取到支付页面");
  175. return;
  176. }
  177. // 新窗口打开支付页(设计 M4): 避免 document.write 同页替换销毁当前 SPA
  178. const w = window.open("about:blank", "_blank");
  179. if (w) {
  180. w.document.write(payHtml);
  181. w.document.close();
  182. } else {
  183. ElMessage.warning("支付页弹窗被浏览器拦截,请允许弹窗后重试");
  184. }
  185. } finally {
  186. payLoading.value = false;
  187. }
  188. }
  189. async function handleClose() {
  190. if (!order.value || !enterpriseId.value) return;
  191. try {
  192. await ElMessageBox.confirm("关闭后该批次不可再支付,确定关闭?", "提示", { type: "warning" });
  193. } catch {
  194. return;
  195. }
  196. await BatchPayAPI.batchClose(enterpriseId.value, order.value.out_batch_no);
  197. ElMessage.success("批次已关闭");
  198. load();
  199. }
  200. onMounted(load);
  201. </script>
  202. <style lang="scss" scoped>
  203. .card-header {
  204. display: flex;
  205. justify-content: space-between;
  206. align-items: center;
  207. }
  208. </style>