Ver Fonte

退款功能修改

skyline há 1 mês atrás
pai
commit
a01db3058d

+ 306 - 0
haha-admin-web/src/views/order/refund/utils/hook.tsx

@@ -0,0 +1,306 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { addDialog } from "@/components/ReDialog";
+import type { PaginationProps } from "@pureadmin/table";
+import {
+  ElForm,
+  ElFormItem,
+  ElInput,
+  ElDescriptions,
+  ElDescriptionsItem,
+  ElImage,
+  ElTable,
+  ElTableColumn
+} from "element-plus";
+import {
+  getRefundApplicationList,
+  getRefundApplicationDetail,
+  reviewRefundApplication
+} from "@/api/order";
+import type { RefundApplicationItem, RefundApplicationSearchForm } from "../../utils/types";
+import { type Ref, ref, reactive, onMounted } from "vue";
+import {
+  initPagination,
+  handlePageSizeChange,
+  handleCurrentPageChange,
+  resetPagination
+} from "@/utils/paginationHelper";
+
+export function useRefund(tableRef: Ref) {
+  const form = reactive<RefundApplicationSearchForm>({
+    orderNo: "",
+    status: "",
+    startDate: "",
+    endDate: ""
+  });
+
+  const formRef = ref();
+  const dataList = ref<RefundApplicationItem[]>([]);
+  const loading = ref(true);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  const columns: TableColumnList = [
+    { label: "申请编号", prop: "applicationNo", minWidth: 160 },
+    { label: "订单编号", prop: "orderNo", minWidth: 160 },
+    { label: "用户ID", prop: "userId", minWidth: 80 },
+    {
+      label: "退款金额",
+      prop: "refundAmount",
+      minWidth: 100,
+      cellRenderer: ({ row }: any) => (
+        <span style="color: #f56c6c; font-weight: 600;">¥{row.refundAmount || "0.00"}</span>
+      )
+    },
+    {
+      label: "申请原因",
+      prop: "reason",
+      minWidth: 180,
+      formatter: ({ reason }: any) => reason || "-"
+    },
+    {
+      label: "退款状态",
+      prop: "status",
+      minWidth: 90,
+      cellRenderer: ({ row }: any) => {
+        const statusMap: Record<number, { text: string; type: string }> = {
+          0: { text: "待审核", type: "warning" },
+          1: { text: "已通过", type: "success" },
+          2: { text: "已拒绝", type: "danger" }
+        };
+        const s = statusMap[row.status] || { text: "未知", type: "info" };
+        return <el-tag type={s.type as any} size="small">{s.text}</el-tag>;
+      }
+    },
+    {
+      label: "申请时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }: any) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 200,
+      slot: "operation"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+      if (form.orderNo) searchParams.orderNo = form.orderNo;
+      if (form.status !== "") searchParams.status = parseInt(form.status as string, 10);
+      if (form.startDate) searchParams.startDate = form.startDate;
+      if (form.endDate) searchParams.endDate = form.endDate;
+
+      const { data } = await getRefundApplicationList(searchParams);
+      dataList.value = data.list || [];
+      pagination.total = Number(data.total) || 0;
+    } catch (error) {
+      console.error("获取退款申请列表失败:", error);
+      dataList.value = [];
+      pagination.total = 0;
+    } finally {
+      setTimeout(() => { loading.value = false; }, 300);
+    }
+  }
+
+  function resetForm(formEl: any) {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  }
+
+  function handleSizeChange(val: number) {
+    handlePageSizeChange(val, pagination, onSearch);
+  }
+
+  function handleCurrentChange(val: number) {
+    handleCurrentPageChange(val, pagination, onSearch);
+  }
+
+  async function handleDetail(row: RefundApplicationItem) {
+    try {
+      const { data } = await getRefundApplicationDetail(row.id);
+      const app = data;
+
+      const statusMap: Record<number, { text: string; type: string }> = {
+        0: { text: "待审核", type: "warning" },
+        1: { text: "已通过", type: "success" },
+        2: { text: "已拒绝", type: "danger" }
+      };
+      const s = statusMap[app.status] || { text: "未知", type: "info" };
+
+      addDialog({
+        title: "退款申请详情 - " + app.applicationNo,
+        width: "600px",
+        draggable: true,
+        closeOnClickModal: false,
+        contentRenderer: () => (
+          <div style="max-height: 70vh; overflow-y: auto; padding: 0 4px;">
+            <div style="font-size: 15px; font-weight: 600; margin-bottom: 10px; color: var(--el-text-color-primary);">基本信息</div>
+            <ElDescriptions column={2} border size="small">
+              <ElDescriptionsItem label="申请编号">{app.applicationNo}</ElDescriptionsItem>
+              <ElDescriptionsItem label="订单编号">{app.orderNo}</ElDescriptionsItem>
+              <ElDescriptionsItem label="申请人用户ID">{app.userId}</ElDescriptionsItem>
+              <ElDescriptionsItem label="退款金额">
+                <span style="color: #f56c6c; font-weight: 600;">¥{(app.refundAmount || 0).toFixed(2)}</span>
+              </ElDescriptionsItem>
+              <ElDescriptionsItem label="退款状态">
+                <el-tag type={s.type as any} size="small">{s.text}</el-tag>
+              </ElDescriptionsItem>
+              <ElDescriptionsItem label="申请时间">
+                {app.createTime ? dayjs(app.createTime).format("YYYY-MM-DD HH:mm:ss") : "-"}
+              </ElDescriptionsItem>
+              <ElDescriptionsItem label="申请原因" span={2}>{app.reason || "-"}</ElDescriptionsItem>
+              {app.reviewTime && (
+                <ElDescriptionsItem label="审核时间">
+                  {dayjs(app.reviewTime).format("YYYY-MM-DD HH:mm:ss")}
+                </ElDescriptionsItem>
+              )}
+              {app.reviewRemark && (
+                <ElDescriptionsItem label="审核备注" span={2}>{app.reviewRemark}</ElDescriptionsItem>
+              )}
+            </ElDescriptions>
+
+            {app.refundProducts && app.refundProducts.length > 0 && (
+              <>
+                <div style="font-size: 15px; font-weight: 600; margin: 14px 0 10px; color: var(--el-text-color-primary);">退款商品</div>
+                <ElTable data={app.refundProducts} border size="small" style="width: 100%">
+                  <ElTableColumn label="商品名称" min-width={180}>
+                    {{
+                      default: ({ row: r }: any) => (
+                        <div style="display: flex; align-items: center; gap: 8px;">
+                          {r.pic ? (
+                            <ElImage
+                              src={r.pic}
+                              style="width: 40px; height: 40px; border-radius: 4px; flex-shrink: 0;"
+                              fit="cover"
+                            >
+                              {{
+                                error: () => (
+                                  <div style="width: 40px; height: 40px; background: #f5f5f5; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #999;">暂无</div>
+                                )
+                              }}
+                            </ElImage>
+                          ) : (
+                            <div style="width: 40px; height: 40px; background: #f5f5f5; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #999; flex-shrink: 0;">暂无</div>
+                          )}
+                          <span>{r.productName}</span>
+                        </div>
+                      )
+                    }}
+                  </ElTableColumn>
+                  <ElTableColumn label="单价" width={100} align="right"
+                    v-slots={{ default: ({ row: r }: any) => <span>¥{(r.price || 0).toFixed(2)}</span> }}
+                  />
+                  <ElTableColumn label="退款数量" width={90} align="center"
+                    v-slots={{ default: ({ row: r }: any) => <span>×{r.quantity || 1}</span> }}
+                  />
+                  <ElTableColumn label="退款金额" width={110} align="right"
+                    v-slots={{
+                      default: ({ row: r }: any) => (
+                        <span style="color: #e6a23c; font-weight: 600;">
+                          ¥{((r.price || 0) * (r.quantity || 1)).toFixed(2)}
+                        </span>
+                      )
+                    }}
+                  />
+                </ElTable>
+              </>
+            )}
+          </div>
+        ),
+        hideFooter: true
+      });
+    } catch (error) {
+      message("获取退款详情失败", { type: "error" });
+    }
+  }
+
+  async function handleApprove(row: RefundApplicationItem) {
+    try {
+      const res = await reviewRefundApplication(row.id, { approved: true });
+      if (res.code === 200) {
+        message("退款申请已通过", { type: "success" });
+        onSearch();
+      } else {
+        message(res.message || "操作失败", { type: "error" });
+      }
+    } catch (error) {
+      message("操作失败", { type: "error" });
+    }
+  }
+
+  function handleReject(row: RefundApplicationItem) {
+    const remarkRef = ref("");
+    const formRef2 = ref();
+
+    addDialog({
+      title: "拒绝退款 - " + row.applicationNo,
+      width: "35%",
+      draggable: true,
+      closeOnClickModal: false,
+      contentRenderer: () => (
+        <ElForm ref={formRef2} model={{ remark: remarkRef.value }}>
+          <ElFormItem
+            label="拒绝原因"
+            prop="remark"
+            rules={[{ required: true, message: "请输入拒绝原因", trigger: "blur" }]}
+          >
+            <ElInput
+              v-model={remarkRef.value}
+              type="textarea"
+              placeholder="请输入拒绝原因"
+              rows={3}
+            />
+          </ElFormItem>
+        </ElForm>
+      ),
+      beforeSure: async (done) => {
+        const valid = await formRef2.value?.validate().catch(() => false);
+        if (!valid) return;
+
+        try {
+          const res = await reviewRefundApplication(row.id, {
+            approved: false,
+            remark: remarkRef.value
+          });
+          if (res.code === 200) {
+            message("退款申请已拒绝", { type: "success" });
+            done();
+            onSearch();
+          } else {
+            message(res.message || "操作失败", { type: "error" });
+          }
+        } catch (error) {
+          message("操作失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    onSearch,
+    resetForm,
+    handleDetail,
+    handleApprove,
+    handleReject,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}

+ 26 - 2
haha-admin-web/src/views/order/utils/hook.tsx

@@ -18,6 +18,7 @@ import {
   ElDatePicker,
   ElDescriptions,
   ElDescriptionsItem,
+  ElImage,
   ElTable,
   ElTableColumn,
   ElMessageBox
@@ -428,7 +429,7 @@ export function useOrder(tableRef: Ref) {
 
     addDialog({
       title: `订单退款 - ${row.orderNo}`,
-      width: "56%",
+      width: "40%",
       draggable: true,
       closeOnClickModal: false,
       fullscreen: deviceDetection(),
@@ -460,7 +461,30 @@ export function useOrder(tableRef: Ref) {
                     )
                   }}
                 />
-                <ElTableColumn prop="productName" label="商品名称" min-width={140} />
+                <ElTableColumn label="商品名称" min-width={160}>
+                    {{
+                      default: ({ row: r }: any) => (
+                        <div style="display: flex; align-items: center; gap: 8px;">
+                          {r.pic ? (
+                            <ElImage
+                              src={r.pic}
+                              style="width: 40px; height: 40px; border-radius: 4px; flex-shrink: 0;"
+                              fit="cover"
+                            >
+                              {{
+                                error: () => (
+                                  <div style="width: 40px; height: 40px; background: #f5f5f5; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #999;">暂无</div>
+                                )
+                              }}
+                            </ElImage>
+                          ) : (
+                            <div style="width: 40px; height: 40px; background: #f5f5f5; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #999; flex-shrink: 0;">暂无</div>
+                          )}
+                          <span>{r.productName}</span>
+                        </div>
+                      )
+                    }}
+                  </ElTableColumn>
                 <ElTableColumn label="单价" width={90} align="right"
                   v-slots={{ default: ({ row: r }: any) => <span>¥{(r.price || 0).toFixed(2)}</span> }}
                 />

+ 1 - 0
haha-admin-web/src/views/order/utils/types.ts

@@ -59,6 +59,7 @@ export interface OrderSearchForm {
 export interface RefundProductItem {
   productId: number;
   productName: string;
+  pic?: string;
   quantity: number;
   price: number;
 }

+ 2 - 1
haha-admin-web/vite.config.ts

@@ -38,7 +38,8 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
     "/dict",
     "/timed-discount",
     "/layer-templates",
-    "/distribution"
+    "/distribution",
+    "/refund"
   ];
   
   // 动态生成代理配置

+ 28 - 0
haha-common/src/main/java/com/haha/common/vo/RefundApplicationVO.java

@@ -0,0 +1,28 @@
+package com.haha.common.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 退款申请详情展示对象
+ */
+@Data
+public class RefundApplicationVO {
+    private Long id;
+    private String applicationNo;
+    private Long orderId;
+    private String orderNo;
+    private Long userId;
+    private BigDecimal refundAmount;
+    private String reason;
+    private List<RefundProductVO> refundProducts;
+    private Integer status;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime reviewTime;
+    private String reviewRemark;
+}

+ 16 - 0
haha-common/src/main/java/com/haha/common/vo/RefundProductVO.java

@@ -0,0 +1,16 @@
+package com.haha.common.vo;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+/**
+ * 退款商品展示对象
+ */
+@Data
+public class RefundProductVO {
+    private Long productId;
+    private String productName;
+    private String pic;
+    private Integer quantity;
+    private BigDecimal price;
+}