hook.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import dayjs from "dayjs";
  2. import { message } from "@/utils/message";
  3. import { addDialog } from "@/components/ReDialog";
  4. import type { PaginationProps } from "@pureadmin/table";
  5. import {
  6. ElForm,
  7. ElFormItem,
  8. ElInput,
  9. ElDescriptions,
  10. ElDescriptionsItem,
  11. ElImage,
  12. ElTable,
  13. ElTableColumn
  14. } from "element-plus";
  15. import {
  16. getRefundApplicationList,
  17. getRefundApplicationDetail,
  18. reviewRefundApplication
  19. } from "@/api/order";
  20. import type { RefundApplicationItem, RefundApplicationSearchForm } from "../../utils/types";
  21. import { type Ref, ref, reactive, onMounted } from "vue";
  22. import {
  23. initPagination,
  24. handlePageSizeChange,
  25. handleCurrentPageChange,
  26. resetPagination
  27. } from "@/utils/paginationHelper";
  28. export function useRefund(tableRef: Ref) {
  29. const form = reactive<RefundApplicationSearchForm>({
  30. orderNo: "",
  31. status: "",
  32. startDate: "",
  33. endDate: ""
  34. });
  35. const formRef = ref();
  36. const dataList = ref<RefundApplicationItem[]>([]);
  37. const loading = ref(true);
  38. const pagination = reactive<PaginationProps>(initPagination());
  39. const columns: TableColumnList = [
  40. { label: "申请编号", prop: "applicationNo", minWidth: 160 },
  41. { label: "订单编号", prop: "orderNo", minWidth: 160 },
  42. { label: "用户ID", prop: "userId", minWidth: 80 },
  43. {
  44. label: "退款金额",
  45. prop: "refundAmount",
  46. minWidth: 100,
  47. cellRenderer: ({ row }: any) => (
  48. <span style="color: #f56c6c; font-weight: 600;">¥{row.refundAmount || "0.00"}</span>
  49. )
  50. },
  51. {
  52. label: "申请原因",
  53. prop: "reason",
  54. minWidth: 180,
  55. formatter: ({ reason }: any) => reason || "-"
  56. },
  57. {
  58. label: "退款状态",
  59. prop: "status",
  60. minWidth: 90,
  61. cellRenderer: ({ row }: any) => {
  62. const statusMap: Record<number, { text: string; type: string }> = {
  63. 0: { text: "待审核", type: "warning" },
  64. 1: { text: "已通过", type: "success" },
  65. 2: { text: "已拒绝", type: "danger" }
  66. };
  67. const s = statusMap[row.status] || { text: "未知", type: "info" };
  68. return <el-tag type={s.type as any} size="small">{s.text}</el-tag>;
  69. }
  70. },
  71. {
  72. label: "申请时间",
  73. prop: "createTime",
  74. minWidth: 160,
  75. formatter: ({ createTime }: any) =>
  76. createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
  77. },
  78. {
  79. label: "操作",
  80. fixed: "right",
  81. width: 200,
  82. slot: "operation"
  83. }
  84. ];
  85. async function onSearch() {
  86. loading.value = true;
  87. try {
  88. const searchParams: any = {
  89. page: pagination.currentPage,
  90. pageSize: pagination.pageSize
  91. };
  92. if (form.orderNo) searchParams.orderNo = form.orderNo;
  93. if (form.status !== "") searchParams.status = parseInt(form.status as string, 10);
  94. if (form.startDate) searchParams.startDate = form.startDate;
  95. if (form.endDate) searchParams.endDate = form.endDate;
  96. const { data } = await getRefundApplicationList(searchParams);
  97. dataList.value = data.list || [];
  98. pagination.total = Number(data.total) || 0;
  99. } catch (error) {
  100. console.error("获取退款申请列表失败:", error);
  101. dataList.value = [];
  102. pagination.total = 0;
  103. } finally {
  104. setTimeout(() => { loading.value = false; }, 300);
  105. }
  106. }
  107. function resetForm(formEl: any) {
  108. if (!formEl) return;
  109. formEl.resetFields();
  110. resetPagination(pagination, onSearch);
  111. }
  112. function handleSizeChange(val: number) {
  113. handlePageSizeChange(val, pagination, onSearch);
  114. }
  115. function handleCurrentChange(val: number) {
  116. handleCurrentPageChange(val, pagination, onSearch);
  117. }
  118. async function handleDetail(row: RefundApplicationItem) {
  119. try {
  120. const { data } = await getRefundApplicationDetail(row.id);
  121. const app = data;
  122. const statusMap: Record<number, { text: string; type: string }> = {
  123. 0: { text: "待审核", type: "warning" },
  124. 1: { text: "已通过", type: "success" },
  125. 2: { text: "已拒绝", type: "danger" }
  126. };
  127. const s = statusMap[app.status] || { text: "未知", type: "info" };
  128. addDialog({
  129. title: "退款申请详情 - " + app.applicationNo,
  130. width: "600px",
  131. draggable: true,
  132. closeOnClickModal: false,
  133. contentRenderer: () => (
  134. <div style="max-height: 70vh; overflow-y: auto; padding: 0 4px;">
  135. <div style="font-size: 15px; font-weight: 600; margin-bottom: 10px; color: var(--el-text-color-primary);">基本信息</div>
  136. <ElDescriptions column={2} border size="small">
  137. <ElDescriptionsItem label="申请编号">{app.applicationNo}</ElDescriptionsItem>
  138. <ElDescriptionsItem label="订单编号">{app.orderNo}</ElDescriptionsItem>
  139. <ElDescriptionsItem label="申请人用户ID">{app.userId}</ElDescriptionsItem>
  140. <ElDescriptionsItem label="退款金额">
  141. <span style="color: #f56c6c; font-weight: 600;">¥{(app.refundAmount || 0).toFixed(2)}</span>
  142. </ElDescriptionsItem>
  143. <ElDescriptionsItem label="退款状态">
  144. <el-tag type={s.type as any} size="small">{s.text}</el-tag>
  145. </ElDescriptionsItem>
  146. <ElDescriptionsItem label="申请时间">
  147. {app.createTime ? dayjs(app.createTime).format("YYYY-MM-DD HH:mm:ss") : "-"}
  148. </ElDescriptionsItem>
  149. <ElDescriptionsItem label="申请原因" span={2}>{app.reason || "-"}</ElDescriptionsItem>
  150. {app.reviewTime && (
  151. <ElDescriptionsItem label="审核时间">
  152. {dayjs(app.reviewTime).format("YYYY-MM-DD HH:mm:ss")}
  153. </ElDescriptionsItem>
  154. )}
  155. {app.reviewRemark && (
  156. <ElDescriptionsItem label="审核备注" span={2}>{app.reviewRemark}</ElDescriptionsItem>
  157. )}
  158. </ElDescriptions>
  159. {app.refundProducts && app.refundProducts.length > 0 && (
  160. <>
  161. <div style="font-size: 15px; font-weight: 600; margin: 14px 0 10px; color: var(--el-text-color-primary);">退款商品</div>
  162. <ElTable data={app.refundProducts} border size="small" style="width: 100%">
  163. <ElTableColumn label="商品名称" min-width={180}>
  164. {{
  165. default: ({ row: r }: any) => (
  166. <div style="display: flex; align-items: center; gap: 8px;">
  167. {r.pic ? (
  168. <ElImage
  169. src={r.pic}
  170. style="width: 40px; height: 40px; border-radius: 4px; flex-shrink: 0;"
  171. fit="cover"
  172. >
  173. {{
  174. error: () => (
  175. <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>
  176. )
  177. }}
  178. </ElImage>
  179. ) : (
  180. <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>
  181. )}
  182. <span>{r.productName}</span>
  183. </div>
  184. )
  185. }}
  186. </ElTableColumn>
  187. <ElTableColumn label="单价" width={100} align="right"
  188. v-slots={{ default: ({ row: r }: any) => <span>¥{(r.price || 0).toFixed(2)}</span> }}
  189. />
  190. <ElTableColumn label="退款数量" width={90} align="center"
  191. v-slots={{ default: ({ row: r }: any) => <span>×{r.quantity || 1}</span> }}
  192. />
  193. <ElTableColumn label="退款金额" width={110} align="right"
  194. v-slots={{
  195. default: ({ row: r }: any) => (
  196. <span style="color: #e6a23c; font-weight: 600;">
  197. ¥{((r.price || 0) * (r.quantity || 1)).toFixed(2)}
  198. </span>
  199. )
  200. }}
  201. />
  202. </ElTable>
  203. </>
  204. )}
  205. </div>
  206. ),
  207. hideFooter: true
  208. });
  209. } catch (error) {
  210. message("获取退款详情失败", { type: "error" });
  211. }
  212. }
  213. async function handleApprove(row: RefundApplicationItem) {
  214. try {
  215. const res = await reviewRefundApplication(row.id, { approved: true });
  216. if (res.code === 200) {
  217. message("退款申请已通过", { type: "success" });
  218. onSearch();
  219. } else {
  220. message(res.message || "操作失败", { type: "error" });
  221. }
  222. } catch (error) {
  223. message("操作失败", { type: "error" });
  224. }
  225. }
  226. function handleReject(row: RefundApplicationItem) {
  227. const remarkRef = ref("");
  228. const formRef2 = ref();
  229. addDialog({
  230. title: "拒绝退款 - " + row.applicationNo,
  231. width: "35%",
  232. draggable: true,
  233. closeOnClickModal: false,
  234. contentRenderer: () => (
  235. <ElForm ref={formRef2} model={{ remark: remarkRef.value }}>
  236. <ElFormItem
  237. label="拒绝原因"
  238. prop="remark"
  239. rules={[{ required: true, message: "请输入拒绝原因", trigger: "blur" }]}
  240. >
  241. <ElInput
  242. v-model={remarkRef.value}
  243. type="textarea"
  244. placeholder="请输入拒绝原因"
  245. rows={3}
  246. />
  247. </ElFormItem>
  248. </ElForm>
  249. ),
  250. beforeSure: async (done) => {
  251. const valid = await formRef2.value?.validate().catch(() => false);
  252. if (!valid) return;
  253. try {
  254. const res = await reviewRefundApplication(row.id, {
  255. approved: false,
  256. remark: remarkRef.value
  257. });
  258. if (res.code === 200) {
  259. message("退款申请已拒绝", { type: "success" });
  260. done();
  261. onSearch();
  262. } else {
  263. message(res.message || "操作失败", { type: "error" });
  264. }
  265. } catch (error) {
  266. message("操作失败", { type: "error" });
  267. }
  268. }
  269. });
  270. }
  271. onMounted(() => {
  272. onSearch();
  273. });
  274. return {
  275. form,
  276. loading,
  277. columns,
  278. dataList,
  279. pagination,
  280. onSearch,
  281. resetForm,
  282. handleDetail,
  283. handleApprove,
  284. handleReject,
  285. handleSizeChange,
  286. handleCurrentChange
  287. };
  288. }