feat: add payout account review flow

This commit is contained in:
2026-01-16 15:17:43 +08:00
parent daaacc3fa4
commit 028c462eaa
21 changed files with 1100 additions and 151 deletions

View File

@@ -110,7 +110,7 @@ export const CreatorService = {
body: { action, reason }
});
},
async listPayoutAccounts({ page, limit, tenant_id, tenant_code, tenant_name, user_id, username, type, created_at_from, created_at_to } = {}) {
async listPayoutAccounts({ page, limit, tenant_id, tenant_code, tenant_name, user_id, username, type, status, created_at_from, created_at_to } = {}) {
const iso = (d) => {
if (!d) return undefined;
const date = d instanceof Date ? d : new Date(d);
@@ -127,6 +127,7 @@ export const CreatorService = {
user_id,
username,
type,
status,
created_at_from: iso(created_at_from),
created_at_to: iso(created_at_to)
};
@@ -143,6 +144,16 @@ export const CreatorService = {
if (!accountID) throw new Error('accountID is required');
return requestJson(`/super/v1/payout-accounts/${accountID}`, { method: 'DELETE' });
},
async reviewPayoutAccount(accountID, { action, reason } = {}) {
if (!accountID) throw new Error('accountID is required');
return requestJson(`/super/v1/payout-accounts/${accountID}/review`, {
method: 'POST',
body: {
action,
reason
}
});
},
async createInvite(tenantID, { max_uses, expires_at, remark } = {}) {
if (!tenantID) throw new Error('tenantID is required');
return requestJson(`/super/v1/tenants/${tenantID}/invites`, {

View File

@@ -74,6 +74,7 @@ const payoutTenantName = ref('');
const payoutUserID = ref(null);
const payoutUsername = ref('');
const payoutType = ref('');
const payoutStatus = ref('');
const payoutCreatedAtFrom = ref(null);
const payoutCreatedAtTo = ref(null);
@@ -84,6 +85,18 @@ const joinStatusOptions = [
{ label: '已驳回', value: 'rejected' }
];
const payoutStatusOptions = [
{ label: '全部', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '已通过', value: 'approved' },
{ label: '已驳回', value: 'rejected' }
];
const payoutReviewOptions = [
{ label: '通过', value: 'approve' },
{ label: '驳回', value: 'reject' }
];
const reviewDialogVisible = ref(false);
const reviewSubmitting = ref(false);
const reviewAction = ref('approve');
@@ -100,6 +113,12 @@ const payoutRemoveDialogVisible = ref(false);
const payoutRemoveSubmitting = ref(false);
const payoutRemoveTarget = ref(null);
const payoutReviewDialogVisible = ref(false);
const payoutReviewSubmitting = ref(false);
const payoutReviewAction = ref('approve');
const payoutReviewReason = ref('');
const payoutReviewTarget = ref(null);
const inviteDialogVisible = ref(false);
const inviteSubmitting = ref(false);
const inviteTenantID = ref(null);
@@ -140,6 +159,19 @@ function getJoinStatusSeverity(value) {
}
}
function getPayoutStatusSeverity(value) {
switch (value) {
case 'pending':
return 'warn';
case 'approved':
return 'success';
case 'rejected':
return 'danger';
default:
return 'secondary';
}
}
async function ensureStatusOptionsLoaded() {
if (statusOptions.value.length > 0) return;
statusOptionsLoading.value = true;
@@ -240,6 +272,7 @@ async function loadPayoutAccounts() {
user_id: payoutUserID.value || undefined,
username: payoutUsername.value,
type: payoutType.value || undefined,
status: payoutStatus.value || undefined,
created_at_from: payoutCreatedAtFrom.value || undefined,
created_at_to: payoutCreatedAtTo.value || undefined
});
@@ -345,6 +378,7 @@ function onPayoutReset() {
payoutUserID.value = null;
payoutUsername.value = '';
payoutType.value = '';
payoutStatus.value = '';
payoutCreatedAtFrom.value = null;
payoutCreatedAtTo.value = null;
payoutAccountsPage.value = 1;
@@ -466,6 +500,35 @@ async function confirmRemovePayoutAccount() {
}
}
function openPayoutReviewDialog(row, action) {
payoutReviewTarget.value = row;
payoutReviewAction.value = action || 'approve';
payoutReviewReason.value = '';
payoutReviewDialogVisible.value = true;
}
async function confirmPayoutReview() {
const targetID = payoutReviewTarget.value?.id;
if (!targetID) return;
const reason = payoutReviewReason.value.trim();
if (payoutReviewAction.value === 'reject' && !reason) {
toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回时需填写原因', life: 3000 });
return;
}
payoutReviewSubmitting.value = true;
try {
await CreatorService.reviewPayoutAccount(targetID, { action: payoutReviewAction.value, reason });
toast.add({ severity: 'success', summary: '审核完成', detail: `账户ID: ${targetID}`, life: 3000 });
payoutReviewDialogVisible.value = false;
await loadPayoutAccounts();
} catch (error) {
toast.add({ severity: 'error', summary: '审核失败', detail: error?.message || '无法审核结算账户', life: 4000 });
} finally {
payoutReviewSubmitting.value = false;
}
}
function openInviteDialog(row) {
inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null;
inviteMaxUses.value = 1;
@@ -826,6 +889,9 @@ onMounted(() => {
<SearchField label="类型">
<InputText v-model="payoutType" placeholder="bank/alipay" class="w-full" @keyup.enter="onPayoutSearch" />
</SearchField>
<SearchField label="状态">
<Select v-model="payoutStatus" :options="payoutStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField>
<SearchField label="创建时间 From">
<DatePicker v-model="payoutCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
</SearchField>
@@ -873,6 +939,15 @@ onMounted(() => {
<Column field="name" header="账户名称" style="min-width: 14rem" />
<Column field="account" header="账号" style="min-width: 14rem" />
<Column field="realname" header="收款人" style="min-width: 12rem" />
<Column header="状态" style="min-width: 12rem">
<template #body="{ data }">
<div class="flex flex-col">
<Tag :value="data?.status_description || data?.status || '-'" :severity="getPayoutStatusSeverity(data?.status)" />
<span v-if="data?.review_reason" class="text-xs text-muted-color mt-1 truncate max-w-[220px]">原因{{ data.review_reason }}</span>
<span v-if="data?.reviewed_at" class="text-xs text-muted-color">审核时间{{ formatDate(data.reviewed_at) }}</span>
</div>
</template>
</Column>
<Column field="created_at" header="创建时间" style="min-width: 14rem">
<template #body="{ data }">
{{ formatDate(data.created_at) }}
@@ -880,7 +955,11 @@ onMounted(() => {
</Column>
<Column header="操作" style="min-width: 8rem">
<template #body="{ data }">
<Button label="删除" icon="pi pi-trash" severity="danger" text size="small" class="p-0" @click="openPayoutRemoveDialog(data)" />
<div class="flex flex-col gap-1">
<Button v-if="data?.status === 'pending'" label="通过" icon="pi pi-check" text size="small" class="p-0 justify-start" @click="openPayoutReviewDialog(data, 'approve')" />
<Button v-if="data?.status === 'pending'" label="驳回" icon="pi pi-times" severity="danger" text size="small" class="p-0 justify-start" @click="openPayoutReviewDialog(data, 'reject')" />
<Button label="删除" icon="pi pi-trash" severity="danger" text size="small" class="p-0 justify-start" @click="openPayoutRemoveDialog(data)" />
</div>
</template>
</Column>
</DataTable>
@@ -1001,6 +1080,30 @@ onMounted(() => {
</template>
</Dialog>
<Dialog v-model:visible="payoutReviewDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">结算账户审核</span>
<span class="text-muted-color">账户ID: {{ payoutReviewTarget?.id ?? '-' }}</span>
</div>
</template>
<div class="flex flex-col gap-4">
<div class="text-sm text-muted-color">审核结算账户信息请确认处理动作与备注</div>
<div>
<label class="block font-medium mb-2">审核动作</label>
<Select v-model="payoutReviewAction" :options="payoutReviewOptions" optionLabel="label" optionValue="value" class="w-full" />
</div>
<div>
<label class="block font-medium mb-2">审核说明</label>
<InputText v-model="payoutReviewReason" placeholder="驳回时建议填写原因" class="w-full" />
</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="payoutReviewDialogVisible = false" :disabled="payoutReviewSubmitting" />
<Button label="确认审核" icon="pi pi-check" severity="success" @click="confirmPayoutReview" :loading="payoutReviewSubmitting" :disabled="payoutReviewSubmitting || (payoutReviewAction === 'reject' && !payoutReviewReason.trim())" />
</template>
</Dialog>
<Dialog v-model:visible="inviteDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">