This commit is contained in:
sion
2026-04-05 19:43:31 +08:00
parent 5b9a80e3fe
commit 2fbc47117c
94 changed files with 1032 additions and 882 deletions

View File

@@ -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

View File

@@ -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">
驳回原因

View File

@@ -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}失败`)
}
}

View File

@@ -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>