This commit is contained in:
2026-03-02 12:59:41 +08:00
6 changed files with 817 additions and 382 deletions

View File

@@ -1,24 +1,23 @@
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AgentApi } from '#/api/agent';
export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
export function useWithdrawalColumns(
onActionClick?: OnActionClickFn<AgentApi.AgentWithdrawalListItem>,
): VxeTableGridOptions['columns'] {
return [
{
title: 'ID',
field: 'id',
width: 80,
title: '提现单号',
field: 'withdraw_no',
width: 180,
},
{
title: '代理ID',
field: 'agent_id',
width: 100,
},
{
title: '提现单号',
field: 'withdraw_no',
width: 180,
},
{
title: '提现金额',
field: 'amount',
@@ -26,27 +25,49 @@ export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
title: '税费金额',
field: 'tax_amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
},
{
title: '实际到账金额',
title: '实际到账',
field: 'actual_amount',
width: 120,
formatter: ({ cellValue }) => `¥${cellValue.toFixed(2)}`,
formatter: ({ cellValue }) => `¥${cellValue?.toFixed(2) || '0.00'}`,
},
{
title: '收款账号',
title: '税费',
field: 'tax_amount',
width: 100,
formatter: ({ cellValue }) => `¥${cellValue?.toFixed(2) || '0.00'}`,
},
{
title: '提现方式',
field: 'withdrawal_type',
width: 120,
formatter: ({ cellValue }: { cellValue: number }) => {
const methodMap: Record<number, string> = {
1: '支付宝',
2: '银行卡',
};
return methodMap[cellValue] || '未知';
},
},
{
title: '收款账户',
field: 'payee_account',
width: 180,
},
{
title: '收款人姓名',
title: '收款人',
field: 'payee_name',
width: 120,
},
{
title: '银行卡号',
field: 'bank_card_no',
width: 180,
},
{
title: '开户行',
field: 'bank_name',
width: 180,
},
{
title: '状态',
field: 'status',
@@ -55,11 +76,9 @@ export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
name: 'CellTag',
options: [
{ value: 1, color: 'warning', label: '待审核' },
{ value: 2, color: 'success', label: '审核通过' },
{ value: 3, color: 'error', label: '审核拒绝' },
{ value: 4, color: 'processing', label: '提现中' },
{ value: 5, color: 'success', label: '提现成功' },
{ value: 6, color: 'error', label: '提现失败' },
{ value: 2, color: 'processing', label: '通过' },
{ value: 3, color: 'error', label: '拒绝' },
{ value: 5, color: 'success', label: '已打款' },
],
},
},
@@ -69,18 +88,33 @@ export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
width: 200,
},
{
title: '创建时间',
title: '申请时间',
field: 'create_time',
width: 160,
sortable: true,
width: 180,
},
{
align: 'center',
slots: { default: 'operation' },
cellRender: {
attrs: {
nameField: 'withdraw_no',
nameTitle: '提现单号',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'audit',
text: '审核',
disabled: (row: AgentApi.AgentWithdrawalListItem) => {
return row.status !== 1; // 只有待审核状态(status=1)才能审核
},
},
],
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 120,
width: 100,
},
];
}
@@ -88,26 +122,48 @@ export function useWithdrawalColumns(): VxeTableGridOptions['columns'] {
export function useWithdrawalFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'withdraw_no',
label: '提现单号',
component: 'Input',
componentProps: {
placeholder: '请输入提现单号',
allowClear: true,
},
},
{
fieldName: 'agent_id',
label: '代理ID',
component: 'Input',
componentProps: {
placeholder: '请输入代理ID',
allowClear: true,
},
},
{
fieldName: 'withdrawal_type',
label: '提现方式',
component: 'Select',
componentProps: {
options: [
{ label: '支付宝', value: 1 },
{ label: '银行卡', value: 2 },
],
allowClear: true,
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: '待审核', value: 1 },
{ label: '审核通过', value: 2 },
{ label: '审核拒绝', value: 3 },
{ label: '提现中', value: 4 },
{ label: '提现成功', value: 5 },
{ label: '提现失败', value: 6 },
{ label: '通过', value: 2 },
{ label: '拒绝', value: 3 },
{ label: '已打款', value: 5 },
],
allowClear: true,
},
},
];
}

View File

@@ -1,15 +1,13 @@
<script lang="ts" setup>
import type { AgentApi } from '#/api/agent';
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Input, Modal, Space, message } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { auditWithdrawal, getAgentWithdrawalList } from '#/api/agent';
import { getAgentWithdrawalList } from '#/api/agent';
import type { AgentApi } from '#/api/agent';
import AuditModal from './modules/audit-modal.vue';
import { useWithdrawalColumns, useWithdrawalFormSchema } from './data';
interface Props {
@@ -28,9 +26,32 @@ const queryParams = computed(() => ({
...(props.agentId ? { agent_id: props.agentId } : {}),
}));
const auditRemark = ref('');
const auditModalVisible = ref(false);
const currentWithdrawal = ref<AgentApi.AgentWithdrawalListItem | null>(null);
// 审核弹窗
const [AuditModalComponent, auditModalApi] = useVbenModal({
connectedComponent: AuditModal,
destroyOnClose: true,
});
const handleActionClick = ({
code,
row,
}: {
code: string;
row: AgentApi.AgentWithdrawalListItem;
}) => {
if (code === 'audit') {
auditModalApi
.setData({
withdrawal: row,
onSuccess: () => {
auditModalApi.close();
// 刷新列表
gridApi.query();
},
})
.open();
}
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -38,7 +59,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
submitOnChange: true,
},
gridOptions: {
columns: useWithdrawalColumns(),
columns: useWithdrawalColumns(handleActionClick),
proxyConfig: {
ajax: {
query: async ({
@@ -63,90 +84,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
},
});
// 审核提现
function handleAudit(row: AgentApi.AgentWithdrawalListItem) {
currentWithdrawal.value = row;
auditRemark.value = '';
auditModalVisible.value = true;
}
// 确认审核
async function confirmAudit(status: number) {
if (!currentWithdrawal.value) return;
try {
await auditWithdrawal({
withdrawal_id: currentWithdrawal.value.id,
status,
remark: auditRemark.value || '',
});
message.success('审核成功');
auditModalVisible.value = false;
gridApi.query();
} catch (error) {
console.error('审核失败:', error);
}
}
// 取消审核
function cancelAudit() {
auditModalVisible.value = false;
auditRemark.value = '';
currentWithdrawal.value = null;
}
</script>
<template>
<Page :auto-content-height="!agentId">
<Grid :table-title="agentId ? '提现记录列表' : '所有提现记录'">
<template #operation="{ row }">
<Space>
<Button v-if="row.status === 1" type="link" size="small" @click="handleAudit(row)">
审核
</Button>
</Space>
</template>
</Grid>
<Modal v-model:open="auditModalVisible" title="审核提现" :width="600" @cancel="cancelAudit">
<div v-if="currentWithdrawal" class="audit-info">
<p><strong>提现单号</strong>{{ currentWithdrawal.withdraw_no }}</p>
<p>
<strong>提现金额</strong>¥{{ currentWithdrawal.amount.toFixed(2) }}
</p>
<p><strong>收款账号</strong>{{ currentWithdrawal.payee_account }}</p>
<p><strong>收款人姓名</strong>{{ currentWithdrawal.payee_name }}</p>
</div>
<div class="audit-remark">
<p><strong>审核备注</strong></p>
<Input.TextArea v-model:value="auditRemark" :rows="4" placeholder="请输入审核备注" />
</div>
<template #footer>
<Button @click="cancelAudit">取消</Button>
<Button type="primary" danger @click="confirmAudit(3)">
拒绝
</Button>
<Button type="primary" @click="confirmAudit(2)">通过</Button>
</template>
</Modal>
<Grid :table-title="agentId ? '提现记录列表' : '所有提现记录'" />
<AuditModalComponent />
</Page>
</template>
<style lang="less" scoped>
.audit-info {
margin-bottom: 16px;
p {
margin-bottom: 8px;
}
}
.audit-remark {
margin-top: 16px;
p {
margin-bottom: 8px;
}
}
</style>

View File

@@ -0,0 +1,358 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { auditWithdrawal } from '#/api/agent';
import type { AgentApi } from '#/api/agent';
interface ModalData {
withdrawal: AgentApi.AgentWithdrawalListItem;
onSuccess?: () => void;
}
const [Modal, modalApi] = useVbenModal({
onConfirm: () => handleSubmit(),
});
const modalData = computed(() => modalApi.getData<ModalData>());
const withdrawal = computed(() => modalData.value?.withdrawal);
const [Form, formApi] = useVbenForm({
schema: [
{
fieldName: 'status',
label: '审核结果',
component: 'RadioGroup',
componentProps: {
options: [
{ label: '通过', value: 2 },
{ label: '拒绝', value: 3 },
],
},
rules: 'selectRequired',
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
placeholder: '请输入备注信息(可选)',
rows: 4,
maxlength: 500,
showCount: true,
},
},
],
showDefaultActions: false,
});
const handleSubmit = async () => {
if (!withdrawal.value) {
return;
}
// 验证表单
const { valid } = await formApi.validate();
if (!valid) {
message.error('请完善审核信息');
return;
}
// 获取表单值
const values = await formApi.getValues();
if (!values) {
return;
}
// 确保 status 字段存在且有效2=通过3=拒绝)
if (values.status === undefined || values.status === null) {
message.error('请选择审核结果');
return;
}
if (values.status !== 2 && values.status !== 3) {
message.error('审核结果无效,请选择"通过"或"拒绝"');
return;
}
modalApi.lock(true);
try {
await auditWithdrawal({
withdrawal_id: withdrawal.value.id,
status: values.status,
remark: values.remark || '',
});
message.success('审核成功');
modalData.value?.onSuccess?.();
modalApi.close();
} catch (error: any) {
message.error(error?.message || '审核失败');
} finally {
modalApi.lock(false);
}
};
</script>
<template>
<Modal class="w-[700px]">
<div class="audit-modal">
<!-- 提现信息卡片 -->
<div v-if="withdrawal" class="withdrawal-info">
<div class="info-header">
<h3 class="info-title">
<span class="title-icon">💰</span>
提现信息
</h3>
</div>
<div class="info-content">
<div class="info-row">
<div class="info-item">
<span class="label">提现单号</span>
<span class="value highlight">{{ withdrawal.withdraw_no }}</span>
</div>
<div class="info-item">
<span class="label">代理ID</span>
<span class="value">{{ withdrawal.agent_id }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="label">提现金额</span>
<span class="value amount">¥{{ withdrawal.amount.toFixed(2) }}</span>
</div>
<div class="info-item">
<span class="label">实际到账</span>
<span class="value amount-success">¥{{ withdrawal.actual_amount?.toFixed(2) || '0.00' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="label">税费</span>
<span class="value tax">¥{{ withdrawal.tax_amount?.toFixed(2) || '0.00' }}</span>
</div>
<div class="info-item">
<span class="label">提现方式</span>
<span class="value">
<span :class="['method-tag', withdrawal.withdrawal_type === 1 ? 'alipay' : 'bank']">
{{ withdrawal.withdrawal_type === 1 ? '支付宝' : '银行卡' }}
</span>
</span>
</div>
</div>
<div class="info-row">
<div class="info-item full-width">
<span class="label">收款账户</span>
<span class="value">{{ withdrawal.payee_account }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="label">收款人</span>
<span class="value">{{ withdrawal.payee_name }}</span>
</div>
<div v-if="withdrawal.bank_card_no" class="info-item">
<span class="label">银行卡号</span>
<span class="value">{{ withdrawal.bank_card_no }}</span>
</div>
</div>
<div v-if="withdrawal.bank_name" class="info-row">
<div class="info-item full-width">
<span class="label">开户行</span>
<span class="value">{{ withdrawal.bank_name }}</span>
</div>
</div>
</div>
</div>
<!-- 审核表单 -->
<div class="audit-form">
<Form />
</div>
</div>
</Modal>
</template>
<style lang="less" scoped>
.audit-modal {
padding: 0;
.withdrawal-info {
margin-bottom: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
border-radius: 8px;
border: 1px solid #e8eaed;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
.info-header {
padding: 16px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
.info-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #ffffff;
display: flex;
align-items: center;
gap: 8px;
.title-icon {
font-size: 20px;
}
}
}
.info-content {
padding: 20px;
}
.info-row {
display: flex;
gap: 24px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.info-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
&.full-width {
flex: 1 1 100%;
}
.label {
font-size: 13px;
color: #8c8c8c;
font-weight: 500;
letter-spacing: 0.3px;
}
.value {
font-size: 15px;
color: #262626;
font-weight: 500;
word-break: break-all;
&.highlight {
color: #1890ff;
font-weight: 600;
font-family: 'Monaco', 'Menlo', monospace;
}
&.amount {
color: #fa8c16;
font-size: 18px;
font-weight: 700;
}
&.amount-success {
color: #52c41a;
font-size: 18px;
font-weight: 700;
}
&.tax {
color: #ff4d4f;
font-weight: 600;
}
.method-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
&.alipay {
background: linear-gradient(135deg, #1677ff 0%, #0958d9 100%);
color: #ffffff;
}
&.bank {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: #ffffff;
}
}
}
}
}
.audit-form {
padding: 0 4px;
:deep(.vben-form) {
.ant-form-item-label {
label {
font-weight: 500;
color: #262626;
}
}
.ant-radio-group {
.ant-radio-button-wrapper {
padding: 8px 24px;
height: auto;
font-weight: 500;
transition: all 0.3s;
&:first-child {
border-radius: 6px 0 0 6px;
}
&:last-child {
border-radius: 0 6px 6px 0;
}
&.ant-radio-button-wrapper-checked {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
}
}
.ant-input {
border-radius: 6px;
transition: all 0.3s;
&:focus,
&:hover {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
}
textarea.ant-input {
resize: vertical;
min-height: 100px;
border-radius: 6px;
padding: 12px;
font-size: 14px;
line-height: 1.6;
transition: all 0.3s;
&:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
}
}
}
}
</style>

View File

@@ -97,6 +97,11 @@ export function useColumns<T = OrderApi.Order>(
title: '创建时间',
width: 180,
},
{
field: 'update_time',
title: '更新时间',
width: 180,
},
{
field: 'pay_time',
title: '支付时间',