Explorar o código

feat: add withdraw review admin page (Step 27)

yangyi hai 3 días
pai
achega
a6a7b88f8f

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
openapi.json


+ 4 - 0
src/api/models/index.ts

@@ -14,6 +14,7 @@ export * from "./deleteByIdParams";
 export * from "./fieldError";
 export * from "./getTransactionsParams";
 export * from "./listOrdersParams";
+export * from "./listPendingWithdrawalsParams";
 export * from "./listPostsParams";
 export * from "./listPreviousPostsParams";
 export * from "./loginDto";
@@ -34,6 +35,7 @@ export * from "./responseBoolean";
 export * from "./responseListFieldError";
 export * from "./responseListNotificationVo";
 export * from "./responseListRealnameAuthVo";
+export * from "./responseListWalletTransactionVo";
 export * from "./responseMapStringLong";
 export * from "./responseMapStringLongData";
 export * from "./responseMapStringObject";
@@ -49,6 +51,7 @@ export * from "./responseRealnameAuthVo";
 export * from "./responseUserVo";
 export * from "./responseVoid";
 export * from "./reviewBody";
+export * from "./reviewWithdrawBody";
 export * from "./submitBody";
 export * from "./updateConfigBody";
 export * from "./updateHitStatusBody";
@@ -61,4 +64,5 @@ export * from "./uploadBody";
 export * from "./uploadParams";
 export * from "./userDto";
 export * from "./userVo";
+export * from "./walletTransactionVo";
 export * from "./websiteMetaDto";

+ 12 - 0
src/api/models/listPendingWithdrawalsParams.ts

@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+
+export type ListPendingWithdrawalsParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 20 - 0
src/api/models/responseListWalletTransactionVo.ts

@@ -0,0 +1,20 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+import type { WalletTransactionVo } from "./walletTransactionVo";
+
+/**
+ * 后端统一的响应实体
+ */
+export interface ResponseListWalletTransactionVo {
+  /** 状态码;200:成功 */
+  code?: number;
+  /** 响应的具体数据 */
+  data?: WalletTransactionVo[];
+  /** 响应附加信息 */
+  message?: string;
+}

+ 9 - 0
src/api/models/reviewWithdrawBody.ts

@@ -0,0 +1,9 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+
+export type ReviewWithdrawBody = { [key: string]: unknown };

+ 21 - 0
src/api/models/walletTransactionVo.ts

@@ -0,0 +1,21 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * Serve API
+ * Serve应用接口文档
+ * OpenAPI spec version: 0.0.1-SNAPSHOT
+ */
+
+/**
+ * 钱包流水视图
+ */
+export interface WalletTransactionVo {
+  amount?: number;
+  balanceAfter?: number;
+  balanceBefore?: number;
+  createdAt?: string;
+  id?: string;
+  remark?: string;
+  status?: string;
+  type?: string;
+}

+ 38 - 0
src/api/wallet-controller.ts

@@ -9,12 +9,30 @@ import type {
   AdminRechargeBody,
   ApplyWithdrawBody,
   GetTransactionsParams,
+  ListPendingWithdrawalsParams,
   RechargeBody,
+  ResponseListWalletTransactionVo,
   ResponseMapStringObject,
+  ResponseVoid,
+  ReviewWithdrawBody,
 } from "./models";
 import { customAxiosInstance } from "../util/axios-instance";
 
 export const getWalletController = () => {
+  /**
+   * @summary 审核提现(管理员)
+   */
+  const reviewWithdraw = (
+    id: number,
+    reviewWithdrawBody: ReviewWithdrawBody,
+  ) => {
+    return customAxiosInstance<ResponseVoid>({
+      url: `/api/wallet/withdraw/${id}/review`,
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      data: reviewWithdrawBody,
+    });
+  };
   /**
    * @summary 提现申请
    */
@@ -48,6 +66,16 @@ export const getWalletController = () => {
       data: adminRechargeBody,
     });
   };
+  /**
+   * @summary 获取待审核提现列表(管理员)
+   */
+  const listPendingWithdrawals = (params?: ListPendingWithdrawalsParams) => {
+    return customAxiosInstance<ResponseListWalletTransactionVo>({
+      url: `/api/wallet/withdraw/pending`,
+      method: "GET",
+      params,
+    });
+  };
   /**
    * @summary 资金明细
    */
@@ -68,13 +96,18 @@ export const getWalletController = () => {
     });
   };
   return {
+    reviewWithdraw,
     applyWithdraw,
     recharge,
     adminRecharge,
+    listPendingWithdrawals,
     getTransactions,
     getBalance,
   };
 };
+export type ReviewWithdrawResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getWalletController>["reviewWithdraw"]>>
+>;
 export type ApplyWithdrawResult = NonNullable<
   Awaited<ReturnType<ReturnType<typeof getWalletController>["applyWithdraw"]>>
 >;
@@ -84,6 +117,11 @@ export type RechargeResult = NonNullable<
 export type AdminRechargeResult = NonNullable<
   Awaited<ReturnType<ReturnType<typeof getWalletController>["adminRecharge"]>>
 >;
+export type ListPendingWithdrawalsResult = NonNullable<
+  Awaited<
+    ReturnType<ReturnType<typeof getWalletController>["listPendingWithdrawals"]>
+  >
+>;
 export type GetTransactionsResult = NonNullable<
   Awaited<ReturnType<ReturnType<typeof getWalletController>["getTransactions"]>>
 >;

+ 7 - 0
src/router/index.ts

@@ -15,6 +15,7 @@ import UserView from "../view/UserView.vue"
 import SiteSettingsView from "../view/SiteSettingsView.vue"
 import PostManageView from "../view/PostManageView.vue"
 import RealnameReviewView from "../view/RealnameReviewView.vue"
+import WithdrawReviewView from "../view/WithdrawReviewView.vue"
 import {
   HomeFilled,
   User,
@@ -109,6 +110,12 @@ export const adminRoutes: RouteRecordRaw[] = [
         component: RealnameReviewView,
         meta: { title: '实名审核', icon: Edit, requiresAdmin: true }
       },
+      {
+        path: '/admin/withdraw',
+        name: 'adminWithdraw',
+        component: WithdrawReviewView,
+        meta: { title: '提现审核', icon: Edit, requiresAdmin: true }
+      },
       {
         path: '/admin/settings',
         name: 'settings',

+ 91 - 0
src/view/WithdrawReviewView.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="withdraw-review">
+    <el-card>
+      <template #header>
+        <div class="card-header">
+          <span>提现审核</span>
+          <el-button size="small" @click="fetchData">刷新</el-button>
+        </div>
+      </template>
+
+      <el-table :data="tableData" v-loading="loading" stripe style="width: 100%">
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column prop="userId" label="用户ID" width="80" />
+        <el-table-column label="金额" width="100">
+          <template #default="{ row }">¥{{ Math.abs(row.amount ?? 0).toFixed(2) }}</template>
+        </el-table-column>
+        <el-table-column prop="remark" label="备注" min-width="150" />
+        <el-table-column prop="createdAt" label="申请时间" width="160" />
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="{ row }">
+            <el-button size="small" type="success" @click="handleReview(row, true)">通过</el-button>
+            <el-button size="small" type="danger" @click="handleReview(row, false)">驳回</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <EmptyState v-if="!loading && tableData.length === 0" description="暂无待审核提现" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getWalletController } from '../api/wallet-controller'
+import EmptyState from '../components/EmptyState.vue'
+
+const api = getWalletController()
+const tableData = ref<Record<string, unknown>[]>([])
+const loading = ref(false)
+
+async function fetchData() {
+  loading.value = true
+  try {
+    const res = await api.listPendingWithdrawals({ pageNum: 1, pageSize: 50 })
+    if (res.code === 200 && res.data) {
+      tableData.value = res.data as Record<string, unknown>[]
+    }
+  } catch {
+    tableData.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleReview(row: Record<string, unknown>, approved: boolean) {
+  const action = approved ? '通过' : '驳回'
+  const promptTitle = action === '通过' ? '确认通过' : '驳回原因'
+  ElMessageBox.prompt(
+    `确定${action}该提现申请?${approved ? '' : '请输入驳回原因'}`,
+    promptTitle,
+    { inputType: 'textarea', inputPlaceholder: approved ? '' : '原因(可选)' },
+  ).then(async ({ value }) => {
+    try {
+      const res = await api.reviewWithdraw(Number(row.id), { approved, rejectReason: value || '' })
+      if (res.code === 200) {
+        ElMessage.success(`${action}成功`)
+        await fetchData()
+      } else {
+        ElMessage.error(`${action}失败`)
+      }
+    } catch {
+      ElMessage.error(`${action}失败`)
+    }
+  }).catch(() => {})
+}
+
+onMounted(() => fetchData())
+</script>
+
+<style scoped>
+.withdraw-review {
+  padding: 16px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 16px;
+  font-weight: 600;
+}
+</style>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio