|
|
@@ -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
|
|
|
+ };
|
|
|
+}
|