This commit is contained in:
sion
2026-04-05 19:44:40 +08:00
parent 2fbc47117c
commit 2ea315cefb
71 changed files with 1266 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { toast } from 'vue-sonner'
import type { AdminRecord } from '@/services/api/monisuo-admin.api'
import { BasicPage } from '@/components/global-layout'
import { useCreateAdminMutation, useGetAdminListQuery, useToggleAdminStatusMutation } from '@/services/api/monisuo-admin.api'
const { data, isLoading, refetch } = useGetAdminListQuery()
const createMutation = useCreateAdminMutation()
const toggleStatusMutation = useToggleAdminStatusMutation()
const admins = computed(() => data.value?.data || [])
const showCreateDialog = ref(false)
const newAdmin = ref({ username: '', password: '', nickname: '', role: 2 as number })
function openCreateDialog() {
newAdmin.value = { username: '', password: '', nickname: '', role: 2 }
showCreateDialog.value = true
}
async function createAdmin() {
if (!newAdmin.value.username || !newAdmin.value.password) {
toast.error('请输入用户名和密码')
return
}
if (newAdmin.value.password.length < 4) {
toast.error('密码至少4位')
return
}
try {
await createMutation.mutateAsync(newAdmin.value)
toast.success('创建成功')
showCreateDialog.value = false
refetch()
}
catch (e: any) {
toast.error(e.message || e.response?.data?.msg || '创建失败')
}
}
async function toggleStatus(admin: AdminRecord) {
if (admin.isSystem === 1) {
toast.error('系统预置管理员不可操作')
return
}
const newStatus = admin.status === 1 ? 0 : 1
try {
await toggleStatusMutation.mutateAsync({ id: admin.id, status: newStatus })
toast.success(newStatus === 1 ? '已启用' : '已禁用')
refetch()
}
catch (e: any) {
toast.error(e.message || e.response?.data?.msg || '操作失败')
}
}
function roleText(role: number) {
if (role === 1) return '超级管理员'
if (role === 2) return '管理员'
if (role === 3) return '财务'
return '未知'
}
function roleBadge(role: number) {
if (role === 1) return 'default'
if (role === 3) return 'outline'
return 'secondary'
}
</script>
<template>
<BasicPage title="管理员管理" description="管理系统管理员和财务账号">
<div class="space-y-4">
<div class="flex justify-end">
<UiButton @click="openCreateDialog">
<Icon icon="lucide:plus" class="size-4 mr-2" />
新增账号
</UiButton>
</div>
<!-- PC端表格 -->
<UiCard class="hidden md:block overflow-x-auto p-4">
<UiTable>
<UiTableHeader>
<UiTableRow>
<UiTableHead class="w-[80px]">
ID
</UiTableHead>
<UiTableHead>用户名</UiTableHead>
<UiTableHead>昵称</UiTableHead>
<UiTableHead>角色</UiTableHead>
<UiTableHead>状态</UiTableHead>
<UiTableHead>创建时间</UiTableHead>
<UiTableHead class="text-right">
操作
</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-if="isLoading">
<UiTableCell :col-span="7" class="text-center py-8">
<UiSpinner class="mx-auto" />
</UiTableCell>
</UiTableRow>
<UiTableRow v-else-if="admins.length === 0">
<UiTableCell :col-span="7" class="text-center py-8 text-muted-foreground">
暂无数据
</UiTableCell>
</UiTableRow>
<UiTableRow v-for="admin in admins" :key="admin.id">
<UiTableCell>{{ admin.id }}</UiTableCell>
<UiTableCell class="font-mono font-medium">
{{ admin.username }}
</UiTableCell>
<UiTableCell>{{ admin.nickname }}</UiTableCell>
<UiTableCell>
<UiBadge :variant="roleBadge(admin.role)">
{{ roleText(admin.role) }}
</UiBadge>
</UiTableCell>
<UiTableCell>
<UiBadge :variant="admin.status === 1 ? 'default' : 'destructive'">
{{ admin.status === 1 ? '正常' : '禁用' }}
</UiBadge>
</UiTableCell>
<UiTableCell>{{ admin.createTime }}</UiTableCell>
<UiTableCell class="text-right">
<UiButton
v-if="admin.isSystem !== 1"
size="sm"
variant="ghost"
:disabled="toggleStatusMutation.isPending.value"
@click="toggleStatus(admin)"
>
<Icon
:icon="admin.status === 1 ? 'lucide:ban' : 'lucide:check-circle'"
class="size-4"
:class="admin.status === 1 ? 'text-destructive' : 'text-green-500'"
/>
</UiButton>
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
</UiCard>
<!-- 移动端卡片 -->
<div class="md:hidden space-y-3">
<div v-if="isLoading" class="text-center py-8">
<UiSpinner class="mx-auto" />
</div>
<template v-else-if="admins.length > 0">
<UiCard v-for="admin in admins" :key="admin.id" class="p-4">
<div class="flex items-start justify-between">
<div>
<div class="font-mono font-bold">
{{ admin.username }}
</div>
<div class="text-sm text-muted-foreground">
{{ admin.nickname }}
</div>
</div>
<div class="flex gap-1">
<UiBadge :variant="roleBadge(admin.role)">
{{ roleText(admin.role) }}
</UiBadge>
<UiBadge :variant="admin.status === 1 ? 'default' : 'destructive'">
{{ admin.status === 1 ? '正常' : '禁用' }}
</UiBadge>
</div>
</div>
<div v-if="admin.isSystem !== 1" class="mt-2 flex justify-end">
<UiButton
size="sm"
variant="outline"
:disabled="toggleStatusMutation.isPending.value"
@click="toggleStatus(admin)"
>
{{ admin.status === 1 ? '禁用' : '启用' }}
</UiButton>
</div>
</UiCard>
</template>
<div v-else class="text-center py-8 text-muted-foreground">
暂无数据
</div>
</div>
</div>
<!-- 新增账号弹窗 -->
<UiDialog v-model:open="showCreateDialog">
<UiDialogContent class="max-w-md">
<UiDialogHeader>
<UiDialogTitle>新增账号</UiDialogTitle>
</UiDialogHeader>
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<UiLabel>用户名 <span class="text-red-500">*</span></UiLabel>
<UiInput v-model="newAdmin.username" placeholder="请输入用户名" />
</div>
<div class="grid gap-2">
<UiLabel>密码 <span class="text-red-500">*</span></UiLabel>
<UiInput v-model="newAdmin.password" type="password" placeholder="至少4位" />
</div>
<div class="grid gap-2">
<UiLabel>昵称</UiLabel>
<UiInput v-model="newAdmin.nickname" placeholder="可选" />
</div>
<div class="grid gap-2">
<UiLabel>角色 <span class="text-red-500">*</span></UiLabel>
<UiSelect v-model="newAdmin.role">
<UiSelectTrigger>
<UiSelectValue placeholder="请选择角色" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem :value="2">
管理员
</UiSelectItem>
<UiSelectItem :value="3">
财务
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
</div>
<UiDialogFooter>
<UiButton variant="outline" @click="showCreateDialog = false">
取消
</UiButton>
<UiButton :disabled="createMutation.isPending.value" @click="createAdmin">
<UiSpinner v-if="createMutation.isPending.value" class="mr-2" />
创建
</UiButton>
</UiDialogFooter>
</UiDialogContent>
</UiDialog>
</BasicPage>
</template>

View File

@@ -0,0 +1,372 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { toast } from 'vue-sonner'
import type { OrderFund } from '@/services/api/monisuo-admin.api'
import { BasicPage } from '@/components/global-layout'
import { useApproveOrderMutation, useGetPendingOrdersQuery } from '@/services/api/monisuo-admin.api'
const pageNum = ref(1)
const pageSize = ref(10)
const { data: pendingData, isLoading: pendingLoading, refetch: refetchPending } = useGetPendingOrdersQuery({
pageNum: pageNum.value,
pageSize: pageSize.value,
})
const approveMutation = useApproveOrderMutation()
const pendingOrders = computed(() => pendingData.value?.data?.list || [])
const pendingTotal = computed(() => pendingData.value?.data?.total || 0)
const totalPages = computed(() => Math.ceil(pendingTotal.value / pageSize.value))
const showApproveDialog = ref(false)
const showDetailDialog = ref(false)
const currentOrder = ref<OrderFund | null>(null)
const approveStatus = ref(2)
const rejectReason = ref('')
const adminRemark = ref('')
function viewOrderDetail(order: OrderFund) {
currentOrder.value = order
showDetailDialog.value = true
}
function openApproveDialog(order: OrderFund, status: number) {
currentOrder.value = order
approveStatus.value = status
rejectReason.value = ''
adminRemark.value = ''
showApproveDialog.value = true
}
async function handleApprove() {
if (!currentOrder.value)
return
const action = approveStatus.value === 2 ? '通过' : '驳回'
try {
await approveMutation.mutateAsync({
orderNo: currentOrder.value.orderNo,
status: approveStatus.value,
rejectReason: rejectReason.value || undefined,
adminRemark: adminRemark.value || undefined,
})
toast.success(`订单已${action}`)
showApproveDialog.value = false
refetchPending()
}
catch (e: any) {
toast.error(e.message || e.response?.data?.msg || `${action}失败`)
}
}
function handlePageChange(page: number) {
pageNum.value = page
refetchPending()
}
function handlePageSizeChange(size: unknown) {
if (size === null || size === undefined)
return
pageSize.value = Number(size)
pageNum.value = 1
refetchPending()
}
function formatAmount(amount: number): string {
return amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text)
toast.success('已复制到剪贴板')
}
</script>
<template>
<BasicPage title="财务审批" description="审批提现订单">
<div class="space-y-4">
<!-- 提现待财务审核列表 -->
<UiCard class="hidden md:block overflow-x-auto p-4">
<UiTable>
<UiTableHeader>
<UiTableRow>
<UiTableHead>订单号</UiTableHead>
<UiTableHead>用户</UiTableHead>
<UiTableHead class="text-right">
提现金额
</UiTableHead>
<UiTableHead class="text-right">
手续费
</UiTableHead>
<UiTableHead class="text-right">
应收款项
</UiTableHead>
<UiTableHead>审批人</UiTableHead>
<UiTableHead class="hidden xl:table-cell">
时间
</UiTableHead>
<UiTableHead class="text-right">
操作
</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-if="pendingLoading">
<UiTableCell :col-span="8" class="text-center py-8">
<UiSpinner class="mx-auto" />
</UiTableCell>
</UiTableRow>
<UiTableRow v-else-if="pendingOrders.length === 0">
<UiTableCell :col-span="8" class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:inbox" class="size-8 mx-auto mb-2 opacity-50" />
<p>暂无待审核订单</p>
</UiTableCell>
</UiTableRow>
<UiTableRow v-for="order in pendingOrders" :key="order.id">
<UiTableCell class="font-mono text-xs">
{{ order.orderNo }}
</UiTableCell>
<UiTableCell>{{ order.username }}</UiTableCell>
<UiTableCell class="text-right font-mono font-medium">
${{ formatAmount(order.amount) }}
</UiTableCell>
<UiTableCell class="text-right font-mono text-muted-foreground">
-${{ formatAmount(order.fee || 0) }}
</UiTableCell>
<UiTableCell class="text-right font-mono font-bold text-green-600">
${{ formatAmount(order.receivableAmount || 0) }}
</UiTableCell>
<UiTableCell class="text-sm">
{{ order.approveAdminName || '-' }}
</UiTableCell>
<UiTableCell class="hidden xl:table-cell text-muted-foreground text-sm">
{{ order.createTime }}
</UiTableCell>
<UiTableCell class="text-right">
<div class="flex justify-end gap-1">
<UiButton size="sm" variant="ghost" @click="viewOrderDetail(order)">
<Icon icon="lucide:eye" class="size-4" />
</UiButton>
<UiButton
size="sm"
:disabled="approveMutation.isPending.value"
@click="openApproveDialog(order, 2)"
>
通过
</UiButton>
<UiButton
size="sm"
variant="destructive"
:disabled="approveMutation.isPending.value"
@click="openApproveDialog(order, 3)"
>
驳回
</UiButton>
</div>
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
</UiCard>
<!-- 移动端卡片 -->
<div class="md:hidden space-y-3">
<div v-if="pendingLoading" class="text-center py-8">
<UiSpinner class="mx-auto" />
</div>
<template v-else-if="pendingOrders.length > 0">
<UiCard v-for="order in pendingOrders" :key="order.id" class="p-4">
<div class="flex items-start justify-between">
<div class="space-y-1">
<div class="font-mono text-xs text-muted-foreground">
{{ order.orderNo }}
</div>
<div class="font-medium">
{{ order.username }}
</div>
</div>
<UiBadge variant="secondary">
待财务审核
</UiBadge>
</div>
<div class="mt-3 pt-3 border-t">
<div class="text-xl font-mono font-bold text-red-600 dark:text-red-400">
-${{ formatAmount(order.amount) }}
</div>
<div class="text-sm text-muted-foreground mt-1">
手续费: -${{ formatAmount(order.fee || 0) }} | 实际到账: ${{ formatAmount(order.receivableAmount || 0) }}
</div>
<div class="text-sm text-muted-foreground mt-1">
审批人: {{ order.approveAdminName || '-' }}
</div>
<div v-if="order.walletAddress" class="mt-2 text-sm">
<span class="text-muted-foreground">提现地址:</span>
<div class="font-mono text-xs break-all mt-1 flex items-center gap-1">
{{ order.walletAddress }}
<Icon icon="lucide:copy" class="size-3 cursor-pointer" @click="copyToClipboard(order.walletAddress!)" />
</div>
</div>
<div class="text-sm text-muted-foreground mt-1">
{{ order.createTime }}
</div>
</div>
<div class="mt-3 flex gap-2">
<UiButton size="sm" class="flex-1" @click="openApproveDialog(order, 2)">
通过
</UiButton>
<UiButton size="sm" variant="destructive" class="flex-1" @click="openApproveDialog(order, 3)">
驳回
</UiButton>
</div>
</UiCard>
</template>
<div v-else class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:inbox" class="size-8 mx-auto mb-2 opacity-50" />
<p>暂无待审核订单</p>
</div>
</div>
<!-- 分页 -->
<div v-if="pendingTotal > 0" class="flex items-center justify-between gap-4 px-2">
<div class="text-sm text-muted-foreground">
{{ pendingTotal }} 条待审核
</div>
<div class="flex items-center gap-2">
<UiButton
variant="outline"
size="icon"
class="h-8 w-8"
:disabled="pageNum <= 1"
@click="handlePageChange(pageNum - 1)"
>
<Icon icon="lucide:chevron-left" class="size-4" />
</UiButton>
<span class="text-sm min-w-[80px] text-center">
{{ pageNum }} / {{ totalPages }}
</span>
<UiButton
variant="outline"
size="icon"
class="h-8 w-8"
:disabled="pageNum >= totalPages"
@click="handlePageChange(pageNum + 1)"
>
<Icon icon="lucide:chevron-right" class="size-4" />
</UiButton>
</div>
</div>
</div>
<!-- 订单详情弹窗 -->
<UiDialog v-model:open="showDetailDialog">
<UiDialogContent class="max-w-md max-h-[90vh] overflow-y-auto">
<UiDialogHeader>
<UiDialogTitle>提现订单详情</UiDialogTitle>
</UiDialogHeader>
<div v-if="currentOrder" class="space-y-4">
<div class="grid grid-cols-3 gap-2 text-sm">
<div class="text-muted-foreground">订单号</div>
<div class="col-span-2 font-mono">{{ currentOrder.orderNo }}</div>
<div class="text-muted-foreground">用户</div>
<div class="col-span-2 font-medium">{{ currentOrder.username }}</div>
<div class="text-muted-foreground">提现金额</div>
<div class="col-span-2 font-mono font-bold text-lg">${{ formatAmount(currentOrder.amount) }}</div>
<div class="text-muted-foreground">手续费(10%)</div>
<div class="col-span-2 font-mono">-${{ formatAmount(currentOrder.fee || 0) }}</div>
<div class="text-muted-foreground">应收款项</div>
<div class="col-span-2 font-mono font-bold text-green-600">${{ formatAmount(currentOrder.receivableAmount || 0) }}</div>
<div class="text-muted-foreground">提现地址</div>
<div class="col-span-2">
<div v-if="currentOrder.walletAddress" class="flex items-start gap-1">
<span class="font-mono text-xs break-all">{{ currentOrder.walletAddress }}</span>
<Icon icon="lucide:copy" class="size-4 cursor-pointer flex-shrink-0" @click="copyToClipboard(currentOrder.walletAddress!)" />
</div>
<span v-else class="text-muted-foreground">-</span>
</div>
<div v-if="currentOrder.approveAdminName" class="text-muted-foreground">审批人</div>
<div v-if="currentOrder.approveAdminName" class="col-span-2">
{{ currentOrder.approveAdminName }}
</div>
<div class="text-muted-foreground">创建时间</div>
<div class="col-span-2">{{ currentOrder.createTime }}</div>
</div>
</div>
<UiDialogFooter>
<template v-if="currentOrder">
<UiButton variant="outline" @click="showDetailDialog = false">
关闭
</UiButton>
<UiButton @click="openApproveDialog(currentOrder, 2); showDetailDialog = false">
通过
</UiButton>
<UiButton variant="destructive" @click="openApproveDialog(currentOrder, 3); showDetailDialog = false">
驳回
</UiButton>
</template>
</UiDialogFooter>
</UiDialogContent>
</UiDialog>
<!-- 审批弹窗 -->
<UiDialog v-model:open="showApproveDialog">
<UiDialogContent class="max-w-md max-h-[90vh] overflow-y-auto">
<UiDialogHeader>
<UiDialogTitle>{{ approveStatus === 2 ? '通过提现' : '驳回提现' }}</UiDialogTitle>
</UiDialogHeader>
<div v-if="currentOrder" class="grid gap-4 py-4">
<div class="p-3 rounded-lg bg-muted/50 text-sm space-y-2">
<div>
<div class="text-muted-foreground">订单号</div>
<div class="font-mono">{{ currentOrder.orderNo }}</div>
</div>
<div>
<div class="text-muted-foreground">用户</div>
<div class="font-medium">{{ currentOrder.username }}</div>
</div>
<div>
<div class="text-muted-foreground">提现金额</div>
<div class="font-mono font-bold text-lg">${{ formatAmount(currentOrder.amount) }}</div>
</div>
<div>
<div class="text-muted-foreground">应收款项</div>
<div class="font-mono text-green-600">${{ formatAmount(currentOrder.receivableAmount || 0) }}</div>
</div>
</div>
<div v-if="approveStatus === 3" class="grid gap-2">
<UiLabel>驳回原因 <span class="text-red-500">*</span></UiLabel>
<UiInput v-model="rejectReason" placeholder="请输入驳回原因" />
</div>
<div class="grid gap-2">
<UiLabel>备注</UiLabel>
<UiInput v-model="adminRemark" placeholder="可选" />
</div>
</div>
<UiDialogFooter>
<UiButton variant="outline" @click="showApproveDialog = false">
取消
</UiButton>
<UiButton
:variant="approveStatus === 3 ? 'destructive' : 'default'"
:disabled="approveMutation.isPending.value || (approveStatus === 3 && !rejectReason.trim())"
@click="handleApprove"
>
<UiSpinner v-if="approveMutation.isPending.value" class="mr-2" />
确认
</UiButton>
</UiDialogFooter>
</UiDialogContent>
</UiDialog>
</BasicPage>
</template>