111
This commit is contained in:
@@ -19,6 +19,8 @@ const showEditDialog = ref(false)
|
||||
const showPriceDialog = ref(false)
|
||||
const priceInput = ref<number>(0)
|
||||
const editingCode = ref('')
|
||||
const editingInitialPrice = ref<number | null>(null)
|
||||
const editingCurrentPrice = ref<number>(0)
|
||||
|
||||
// 表单验证
|
||||
const formErrors = ref<{ code?: string, name?: string }>({})
|
||||
@@ -60,12 +62,14 @@ async function saveCoin() {
|
||||
refetch()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || '操作失败')
|
||||
toast.error(e.message || e.response?.data?.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openPriceDialog(coin: Coin) {
|
||||
editingCode.value = coin.code
|
||||
editingInitialPrice.value = coin.initialPrice ?? null
|
||||
editingCurrentPrice.value = coin.price
|
||||
priceInput.value = coin.price
|
||||
showPriceDialog.value = true
|
||||
}
|
||||
@@ -83,7 +87,7 @@ async function updatePrice() {
|
||||
refetch()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || '操作失败')
|
||||
toast.error(e.message || e.response?.data?.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +100,7 @@ async function toggleStatus(coin: Coin) {
|
||||
toast.success(`已${action} ${coin.code}`)
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || `${action}失败`)
|
||||
toast.error(e.message || e.response?.data?.msg || `${action}失败`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +132,10 @@ function formatPrice(price: number): string {
|
||||
<UiTableHead>代码</UiTableHead>
|
||||
<UiTableHead>名称</UiTableHead>
|
||||
<UiTableHead class="text-right">
|
||||
价格
|
||||
初始价格
|
||||
</UiTableHead>
|
||||
<UiTableHead class="text-right">
|
||||
当前价格
|
||||
</UiTableHead>
|
||||
<UiTableHead>价格类型</UiTableHead>
|
||||
<UiTableHead>状态</UiTableHead>
|
||||
@@ -139,12 +146,12 @@ function formatPrice(price: number): string {
|
||||
</UiTableHeader>
|
||||
<UiTableBody>
|
||||
<UiTableRow v-if="isLoading">
|
||||
<UiTableCell :col-span="7" class="text-center py-8">
|
||||
<UiTableCell :col-span="8" class="text-center py-8">
|
||||
<UiSpinner class="mx-auto" />
|
||||
</UiTableCell>
|
||||
</UiTableRow>
|
||||
<UiTableRow v-else-if="coins.length === 0">
|
||||
<UiTableCell :col-span="7" class="text-center py-8 text-muted-foreground">
|
||||
<UiTableCell :col-span="8" class="text-center py-8 text-muted-foreground">
|
||||
暂无数据
|
||||
</UiTableCell>
|
||||
</UiTableRow>
|
||||
@@ -154,7 +161,10 @@ function formatPrice(price: number): string {
|
||||
{{ coin.code }}
|
||||
</UiTableCell>
|
||||
<UiTableCell>{{ coin.name }}</UiTableCell>
|
||||
<UiTableCell class="text-right font-mono">
|
||||
<UiTableCell class="text-right font-mono text-muted-foreground">
|
||||
{{ coin.initialPrice != null ? `$${formatPrice(coin.initialPrice)}` : '--' }}
|
||||
</UiTableCell>
|
||||
<UiTableCell class="text-right font-mono font-semibold text-green-600 dark:text-green-400">
|
||||
${{ formatPrice(coin.price) }}
|
||||
</UiTableCell>
|
||||
<UiTableCell>
|
||||
@@ -224,6 +234,9 @@ function formatPrice(price: number): string {
|
||||
<div class="text-xl font-mono font-bold text-green-600 dark:text-green-400">
|
||||
${{ formatPrice(coin.price) }}
|
||||
</div>
|
||||
<div v-if="coin.initialPrice != null" class="text-xs text-muted-foreground mt-1">
|
||||
初始价格: ${{ formatPrice(coin.initialPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<UiButton size="sm" variant="outline" class="flex-1" @click="openEditDialog(coin)">
|
||||
@@ -268,6 +281,7 @@ function formatPrice(price: number): string {
|
||||
<UiInput
|
||||
v-model="editingCoin.code"
|
||||
placeholder="BTC"
|
||||
:disabled="!!editingCoin.id"
|
||||
:class="{ 'border-red-500': formErrors.code }"
|
||||
@input="formErrors.code = undefined"
|
||||
/>
|
||||
@@ -289,7 +303,7 @@ function formatPrice(price: number): string {
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<UiLabel>价格类型</UiLabel>
|
||||
<UiSelect v-model="editingCoin.priceType">
|
||||
<UiSelect v-model="editingCoin.priceType" :disabled="!!editingCoin.id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue />
|
||||
</UiSelectTrigger>
|
||||
@@ -303,10 +317,31 @@ function formatPrice(price: number): string {
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<div v-if="editingCoin.priceType === 2" class="grid gap-2">
|
||||
<!-- 新增时:设置初始价格 -->
|
||||
<div v-if="editingCoin.priceType === 2 && !editingCoin.id" class="grid gap-2">
|
||||
<UiLabel>初始价格 ($)</UiLabel>
|
||||
<UiInput v-model.number="editingCoin.price" type="number" step="0.000001" placeholder="0.00" />
|
||||
</div>
|
||||
<!-- 编辑时:显示初始价格(只读)和当前价格(只读) -->
|
||||
<template v-if="editingCoin.priceType === 2 && editingCoin.id">
|
||||
<div v-if="editingCoin.initialPrice != null" class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-muted-foreground">初始价格</span>
|
||||
<div class="font-mono mt-1">
|
||||
${{ formatPrice(editingCoin.initialPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">当前价格</span>
|
||||
<div class="font-mono mt-1 text-green-600 dark:text-green-400">
|
||||
${{ formatPrice(editingCoin.price) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground bg-muted/50 rounded-md p-3">
|
||||
尚未调价,初始价格将在首次调价时锁定
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<UiDialogFooter>
|
||||
<UiButton variant="outline" @click="showEditDialog = false">
|
||||
@@ -327,6 +362,23 @@ function formatPrice(price: number): string {
|
||||
<UiDialogTitle>调整价格 - {{ editingCode }}</UiDialogTitle>
|
||||
</UiDialogHeader>
|
||||
<div class="grid gap-4 py-4">
|
||||
<div v-if="editingInitialPrice != null" class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-muted-foreground">初始价格</span>
|
||||
<div class="font-mono mt-1">
|
||||
${{ formatPrice(editingInitialPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">当前价格</span>
|
||||
<div class="font-mono mt-1 text-green-600 dark:text-green-400">
|
||||
${{ formatPrice(editingCurrentPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground bg-muted/50 rounded-md p-3">
|
||||
首次调价后,当前价格将被锁定为初始价格,之后只能调整当前价格
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<UiLabel>新价格 ($)</UiLabel>
|
||||
<UiInput
|
||||
|
||||
@@ -5,9 +5,12 @@ import { toast } from 'vue-sonner'
|
||||
import type { OrderFund } from '@/services/api/monisuo-admin.api'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useApproveOrderMutation, useGetAllOrdersQuery, useGetPendingOrdersQuery } from '@/services/api/monisuo-admin.api'
|
||||
|
||||
const pageNum = ref(1)
|
||||
// 分离分页状态
|
||||
const pendingPage = ref(1)
|
||||
const allPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const activeTab = ref('pending')
|
||||
|
||||
@@ -15,13 +18,16 @@ const activeTab = ref('pending')
|
||||
const filterType = ref<number | string>('all')
|
||||
const filterStatus = ref<number | string>('all')
|
||||
|
||||
// 是否已加载过全部订单
|
||||
const allLoaded = ref(false)
|
||||
|
||||
const { data: pendingData, isLoading: pendingLoading, refetch: refetchPending } = useGetPendingOrdersQuery({
|
||||
pageNum: pageNum.value,
|
||||
pageNum: pendingPage.value,
|
||||
pageSize: pageSize.value,
|
||||
})
|
||||
|
||||
const { data: allData, isLoading: allLoading, refetch: refetchAll } = useGetAllOrdersQuery({
|
||||
pageNum: pageNum.value,
|
||||
pageNum: allPage.value,
|
||||
pageSize: pageSize.value,
|
||||
type: filterType.value === 'all' ? undefined : filterType.value as number,
|
||||
status: filterStatus.value === 'all' ? undefined : filterStatus.value as number,
|
||||
@@ -29,13 +35,24 @@ const { data: allData, isLoading: allLoading, refetch: refetchAll } = useGetAllO
|
||||
|
||||
const approveMutation = useApproveOrderMutation()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const adminRole = computed(() => authStore.adminInfo?.role ?? 2)
|
||||
|
||||
const pendingOrders = computed(() => pendingData.value?.data?.list || [])
|
||||
const pendingTotal = computed(() => pendingData.value?.data?.total || 0)
|
||||
const allOrders = computed(() => allData.value?.data?.list || [])
|
||||
const allTotal = computed(() => allData.value?.data?.total || 0)
|
||||
|
||||
const currentTotal = computed(() => activeTab.value === 'pending' ? pendingTotal.value : allTotal.value)
|
||||
const totalPages = computed(() => Math.ceil(currentTotal.value / pageSize.value))
|
||||
const pendingTotalPages = computed(() => Math.ceil(pendingTotal.value / pageSize.value))
|
||||
const allTotalPages = computed(() => Math.ceil(allTotal.value / pageSize.value))
|
||||
|
||||
// 切换 tab 时懒加载全部订单
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'all' && !allLoaded.value) {
|
||||
allLoaded.value = true
|
||||
refetchAll()
|
||||
}
|
||||
})
|
||||
|
||||
const showApproveDialog = ref(false)
|
||||
const showDetailDialog = ref(false)
|
||||
@@ -74,32 +91,37 @@ async function handleApprove() {
|
||||
toast.success(`订单已${action}`)
|
||||
showApproveDialog.value = false
|
||||
refetchPending()
|
||||
refetchAll()
|
||||
if (allLoaded.value) refetchAll()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || `${action}失败`)
|
||||
toast.error(e.message || e.response?.data?.msg || `${action}失败`)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pageNum.value = page
|
||||
refetchPending()
|
||||
refetchAll()
|
||||
if (activeTab.value === 'pending') {
|
||||
pendingPage.value = page
|
||||
refetchPending()
|
||||
} else {
|
||||
allPage.value = page
|
||||
refetchAll()
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: unknown) {
|
||||
if (size === null || size === undefined)
|
||||
return
|
||||
pageSize.value = Number(size)
|
||||
pageNum.value = 1
|
||||
pendingPage.value = 1
|
||||
allPage.value = 1
|
||||
refetchPending()
|
||||
refetchAll()
|
||||
if (allLoaded.value) refetchAll()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filterType.value = 'all'
|
||||
filterStatus.value = 'all'
|
||||
pageNum.value = 1
|
||||
allPage.value = 1
|
||||
refetchAll()
|
||||
}
|
||||
|
||||
@@ -123,6 +145,7 @@ function getStatusVariant(order: OrderFund): 'default' | 'secondary' | 'destruct
|
||||
// 提现
|
||||
if (status === 1) return 'default' // 待审批
|
||||
if (status === 2) return 'default' // 已完成
|
||||
if (status === 5) return 'secondary' // 待财务审核
|
||||
return 'destructive' // 已驳回/已取消
|
||||
}
|
||||
}
|
||||
@@ -157,12 +180,14 @@ function getStatusText(order: OrderFund): string {
|
||||
// 充值: 仅待确认(status=2)可审批
|
||||
// 提现: 仅待审批(status=1)可审批
|
||||
function canApprove(order: OrderFund): boolean {
|
||||
const role = adminRole.value
|
||||
if (order.type === 1) {
|
||||
return order.status === 2 // 充值待确认
|
||||
}
|
||||
else {
|
||||
return order.status === 1 // 提现待审批
|
||||
}
|
||||
if (role === 2) return order.status === 1 // 管理员: 提现待审批
|
||||
if (role === 3) return order.status === 5 // 财务: 待财务审核
|
||||
if (role === 1) return order.status === 1 || order.status === 5 // 超管
|
||||
return false
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
@@ -437,6 +462,9 @@ function copyToClipboard(text: string) {
|
||||
<UiTableHead class="hidden xl:table-cell">
|
||||
时间
|
||||
</UiTableHead>
|
||||
<UiTableHead class="hidden lg:table-cell">
|
||||
审批人
|
||||
</UiTableHead>
|
||||
<UiTableHead class="hidden lg:table-cell">
|
||||
备注
|
||||
</UiTableHead>
|
||||
@@ -447,12 +475,12 @@ function copyToClipboard(text: string) {
|
||||
</UiTableHeader>
|
||||
<UiTableBody>
|
||||
<UiTableRow v-if="allLoading">
|
||||
<UiTableCell :col-span="9" class="text-center py-8">
|
||||
<UiTableCell :col-span="10" class="text-center py-8">
|
||||
<UiSpinner class="mx-auto" />
|
||||
</UiTableCell>
|
||||
</UiTableRow>
|
||||
<UiTableRow v-else-if="allOrders.length === 0">
|
||||
<UiTableCell :col-span="9" class="text-center py-8 text-muted-foreground">
|
||||
<UiTableCell :col-span="10" class="text-center py-8 text-muted-foreground">
|
||||
暂无数据
|
||||
</UiTableCell>
|
||||
</UiTableRow>
|
||||
@@ -488,6 +516,10 @@ function copyToClipboard(text: string) {
|
||||
完成: {{ order.confirmTime }}
|
||||
</div>
|
||||
</UiTableCell>
|
||||
<UiTableCell class="hidden lg:table-cell text-sm text-muted-foreground">
|
||||
<div v-if="order.approveAdminName">{{ order.approveAdminName }}</div>
|
||||
<span v-else>-</span>
|
||||
</UiTableCell>
|
||||
<UiTableCell class="hidden lg:table-cell text-sm text-muted-foreground max-w-[120px] truncate">
|
||||
{{ order.rejectReason || order.adminRemark || '-' }}
|
||||
</UiTableCell>
|
||||
@@ -547,6 +579,9 @@ function copyToClipboard(text: string) {
|
||||
<div v-if="order.confirmTime" class="text-xs text-muted-foreground">
|
||||
完成: {{ order.confirmTime }}
|
||||
</div>
|
||||
<div v-if="order.approveAdminName" class="text-xs text-muted-foreground">
|
||||
审批人: {{ order.approveAdminName }}
|
||||
</div>
|
||||
<div v-if="order.rejectReason || order.adminRemark" class="text-sm text-muted-foreground mt-1">
|
||||
备注: {{ order.rejectReason || order.adminRemark }}
|
||||
</div>
|
||||
@@ -566,9 +601,9 @@ function copyToClipboard(text: string) {
|
||||
</UiTabs>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="currentTotal > 0" class="flex flex-col sm:flex-row items-center justify-between gap-4 px-2">
|
||||
<div v-if="(activeTab === 'pending' ? pendingTotal : allTotal) > 0" class="flex flex-col sm:flex-row items-center justify-between gap-4 px-2">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
共 {{ currentTotal }} 条记录
|
||||
共 {{ activeTab === 'pending' ? pendingTotal : allTotal }} 条记录
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -596,20 +631,20 @@ function copyToClipboard(text: string) {
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="pageNum <= 1"
|
||||
@click="handlePageChange(pageNum - 1)"
|
||||
:disabled="(activeTab === 'pending' ? pendingPage : allPage) <= 1"
|
||||
@click="handlePageChange((activeTab === 'pending' ? pendingPage : allPage) - 1)"
|
||||
>
|
||||
<Icon icon="lucide:chevron-left" class="size-4" />
|
||||
</UiButton>
|
||||
<span class="text-sm min-w-[80px] text-center">
|
||||
{{ pageNum }} / {{ totalPages }}
|
||||
{{ activeTab === 'pending' ? pendingPage : allPage }} / {{ activeTab === 'pending' ? pendingTotalPages : allTotalPages }}
|
||||
</span>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="pageNum >= totalPages"
|
||||
@click="handlePageChange(pageNum + 1)"
|
||||
:disabled="(activeTab === 'pending' ? pendingPage : allPage) >= (activeTab === 'pending' ? pendingTotalPages : allTotalPages)"
|
||||
@click="handlePageChange((activeTab === 'pending' ? pendingPage : allPage) + 1)"
|
||||
>
|
||||
<Icon icon="lucide:chevron-right" class="size-4" />
|
||||
</UiButton>
|
||||
@@ -665,6 +700,24 @@ function copyToClipboard(text: string) {
|
||||
</UiBadge>
|
||||
</div>
|
||||
|
||||
<!-- 手续费信息(提现) -->
|
||||
<template v-if="currentOrder.type === 2 && currentOrder.fee">
|
||||
<div class="text-muted-foreground">
|
||||
手续费(10%)
|
||||
</div>
|
||||
<div class="col-span-2 font-mono">
|
||||
-${{ currentOrder.fee }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="currentOrder.type === 2 && currentOrder.receivableAmount">
|
||||
<div class="text-muted-foreground">
|
||||
应收款项
|
||||
</div>
|
||||
<div class="col-span-2 font-mono font-bold text-green-600">
|
||||
${{ currentOrder.receivableAmount }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 充值/提现地址 -->
|
||||
<div class="text-muted-foreground">
|
||||
{{ currentOrder.type === 1 ? '充值地址' : '提现地址' }}
|
||||
@@ -704,6 +757,19 @@ function copyToClipboard(text: string) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 财务审批人 -->
|
||||
<template v-if="currentOrder.financeAdminName">
|
||||
<div class="text-muted-foreground">
|
||||
财务审批人
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
{{ currentOrder.financeAdminName }}
|
||||
<span v-if="currentOrder.financeApproveTime" class="text-xs text-muted-foreground ml-2">
|
||||
{{ currentOrder.financeApproveTime }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 管理员确认时间 -->
|
||||
<template v-if="currentOrder.confirmTime">
|
||||
<div class="text-muted-foreground">
|
||||
@@ -714,6 +780,19 @@ function copyToClipboard(text: string) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 审批人信息 -->
|
||||
<template v-if="currentOrder.approveAdminName">
|
||||
<div class="text-muted-foreground">
|
||||
审批人
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
{{ currentOrder.approveAdminName }}
|
||||
<span v-if="currentOrder.approveTime" class="text-xs text-muted-foreground ml-2">
|
||||
{{ currentOrder.approveTime }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="currentOrder.rejectReason">
|
||||
<div class="text-muted-foreground text-red-500">
|
||||
驳回原因
|
||||
|
||||
@@ -44,7 +44,7 @@ async function toggleStatus(user: User) {
|
||||
toast.success(`已${action}用户 ${user.username}`)
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || `${action}失败`)
|
||||
toast.error(e.message || e.response?.data?.msg || `${action}失败`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ async function saveWallet() {
|
||||
refetch()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || '操作失败')
|
||||
toast.error(e.message || e.response?.data?.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ async function setDefault(wallet: ColdWallet) {
|
||||
toast.success(`已将 ${wallet.name} 设为默认`)
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || '设置失败')
|
||||
toast.error(e.message || e.response?.data?.msg || '设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ async function toggleStatus(wallet: ColdWallet) {
|
||||
toast.success(wallet.status === 1 ? '已禁用' : '已启用')
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || '操作失败')
|
||||
toast.error(e.message || e.response?.data?.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ async function deleteWallet(wallet: ColdWallet) {
|
||||
toast.success('钱包已删除')
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.response?.data?.msg || '删除失败')
|
||||
toast.error(e.message || e.response?.data?.msg || '删除失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user