111
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ChevronsUpDown,
|
||||
KeyRound,
|
||||
LogOut,
|
||||
UserRoundCog,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { useChangePasswordMutation } from '@/services/api/monisuo-admin.api'
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { User } from './types'
|
||||
@@ -15,6 +19,40 @@ const { user } = defineProps<
|
||||
|
||||
const { logout } = useAuth()
|
||||
const { isMobile, open } = useSidebar()
|
||||
|
||||
const changePasswordMutation = useChangePasswordMutation()
|
||||
const showPasswordDialog = ref(false)
|
||||
const passwordForm = ref({ oldPassword: '', newPassword: '', confirmPassword: '' })
|
||||
|
||||
function openPasswordDialog() {
|
||||
passwordForm.value = { oldPassword: '', newPassword: '', confirmPassword: '' }
|
||||
showPasswordDialog.value = true
|
||||
}
|
||||
|
||||
async function handleChangePassword() {
|
||||
const { oldPassword, newPassword, confirmPassword } = passwordForm.value
|
||||
if (!oldPassword || !newPassword) {
|
||||
toast.error('请填写完整')
|
||||
return
|
||||
}
|
||||
if (newPassword.length < 4) {
|
||||
toast.error('新密码至少4位')
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
toast.error('两次密码不一致')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await changePasswordMutation.mutateAsync({ oldPassword, newPassword })
|
||||
toast.success('密码修改成功,请重新登录')
|
||||
showPasswordDialog.value = false
|
||||
logout()
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.error(e.message || e.response?.data?.msg || '修改失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,6 +104,10 @@ const { isMobile, open } = useSidebar()
|
||||
<UserRoundCog />
|
||||
设置
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="openPasswordDialog">
|
||||
<KeyRound />
|
||||
修改密码
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
@@ -77,4 +119,36 @@ const { isMobile, open } = useSidebar()
|
||||
</UiDropdownMenu>
|
||||
</UiSidebarMenuItem>
|
||||
</UiSidebarMenu>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<UiDialog v-model:open="showPasswordDialog">
|
||||
<UiDialogContent class="max-w-md">
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>修改密码</UiDialogTitle>
|
||||
</UiDialogHeader>
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<UiLabel>旧密码</UiLabel>
|
||||
<UiInput v-model="passwordForm.oldPassword" type="password" placeholder="请输入旧密码" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<UiLabel>新密码</UiLabel>
|
||||
<UiInput v-model="passwordForm.newPassword" type="password" placeholder="至少4位" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<UiLabel>确认新密码</UiLabel>
|
||||
<UiInput v-model="passwordForm.confirmPassword" type="password" placeholder="再次输入新密码" @keyup.enter="handleChangePassword" />
|
||||
</div>
|
||||
</div>
|
||||
<UiDialogFooter>
|
||||
<UiButton variant="outline" @click="showPasswordDialog = false">
|
||||
取消
|
||||
</UiButton>
|
||||
<UiButton :disabled="changePasswordMutation.isPending.value" @click="handleChangePassword">
|
||||
<UiSpinner v-if="changePasswordMutation.isPending.value" class="mr-2" />
|
||||
确认修改
|
||||
</UiButton>
|
||||
</UiDialogFooter>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
import { useAdminLoginMutation } from '@/services/api/monisuo-admin.api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
export function useAuth() {
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { isLogin, adminInfo } = storeToRefs(authStore)
|
||||
@@ -15,6 +18,7 @@ export function useAuth() {
|
||||
|
||||
function logout() {
|
||||
authStore.logout()
|
||||
queryClient.clear()
|
||||
router.push({ path: '/auth/sign-in' })
|
||||
}
|
||||
|
||||
@@ -52,7 +56,7 @@ export function useAuth() {
|
||||
}
|
||||
catch (e: any) {
|
||||
console.error('Login error:', e)
|
||||
error.value = e.response?.data?.msg || '网络错误,请稍后重试'
|
||||
error.value = e.message || e.response?.data?.msg || '网络错误,请稍后重试'
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -22,6 +22,10 @@ export function useAxios() {
|
||||
})
|
||||
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
const data = response.data as any
|
||||
if (data && data.code && data.code !== '0000') {
|
||||
return Promise.reject(new Error(data.msg || '请求失败'))
|
||||
}
|
||||
return response
|
||||
}, (error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { Coins, DollarSign, Palette, Receipt, Settings, TrendingUp, Users } from 'lucide-vue-next'
|
||||
import { CircleDollarSign, Coins, DollarSign, Palette, Receipt, Settings, ShieldCheck, TrendingUp, Users } from 'lucide-vue-next'
|
||||
|
||||
import type { NavGroup } from '@/components/app-sidebar/types'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
export function useSidebar() {
|
||||
const authStore = useAuthStore()
|
||||
const role = computed(() => authStore.adminInfo?.role ?? 2)
|
||||
const isSuperAdmin = computed(() => role.value === 1)
|
||||
|
||||
const settingsNavItems = [
|
||||
{ title: '外观设置', url: '/settings/appearance', icon: Palette },
|
||||
]
|
||||
|
||||
const navData = ref<NavGroup[]>([
|
||||
const allNavItems: NavGroup[] = [
|
||||
{
|
||||
title: 'Monisuo 管理',
|
||||
items: [
|
||||
{ title: '数据看板', url: '/monisuo/dashboard', icon: DollarSign },
|
||||
{ title: '用户管理', url: '/monisuo/users', icon: Users },
|
||||
{ title: '币种管理', url: '/monisuo/coins', icon: Coins },
|
||||
{ title: '订单审批', url: '/monisuo/orders', icon: Receipt },
|
||||
{ title: '业务分析', url: '/monisuo/analytics', icon: TrendingUp },
|
||||
{ title: '数据看板', url: '/monisuo/dashboard', icon: DollarSign, roles: [1] },
|
||||
{ title: '用户管理', url: '/monisuo/users', icon: Users, roles: [1] },
|
||||
{ title: '币种管理', url: '/monisuo/coins', icon: Coins, roles: [1] },
|
||||
{ title: '订单审批', url: '/monisuo/orders', icon: Receipt, roles: [1, 2] },
|
||||
{ title: '财务审批', url: '/monisuo/finance-orders', icon: CircleDollarSign, roles: [1, 3] },
|
||||
{ title: '业务分析', url: '/monisuo/analytics', icon: TrendingUp, roles: [1] },
|
||||
{ title: '管理员管理', url: '/monisuo/admins', icon: ShieldCheck, roles: [1] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -24,7 +31,19 @@ export function useSidebar() {
|
||||
{ title: 'Settings', icon: Settings, items: settingsNavItems },
|
||||
],
|
||||
},
|
||||
])
|
||||
]
|
||||
|
||||
const navData = computed<NavGroup[]>(() => {
|
||||
return allNavItems.map(group => ({
|
||||
title: group.title,
|
||||
items: group.items.filter((item) => {
|
||||
const roles = (item as any).roles as number[] | undefined
|
||||
if (!roles)
|
||||
return true
|
||||
return roles.includes(role.value)
|
||||
}),
|
||||
})).filter(group => group.items.length > 0)
|
||||
})
|
||||
|
||||
const otherPages = ref<NavGroup[]>([])
|
||||
|
||||
@@ -32,5 +51,6 @@ export function useSidebar() {
|
||||
navData,
|
||||
otherPages,
|
||||
settingsNavItems,
|
||||
isSuperAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,37 +1,72 @@
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import pinia from '@/plugins/pinia/setup'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// 不需要认证的白名单路由
|
||||
const WHITE_LIST = ['/auth/sign-in', '/auth/sign-up', '/auth/forgot-password']
|
||||
|
||||
export function authGuard(router: Router) {
|
||||
router.beforeEach((to, _from) => {
|
||||
// 仅超级管理员可访问的路由前缀
|
||||
const SUPER_ADMIN_ONLY = ['/monisuo/dashboard', '/monisuo/users', '/monisuo/coins', '/monisuo/analytics', '/monisuo/admins']
|
||||
|
||||
export function setupAuthGuard(router: Router) {
|
||||
router.beforeEach((to) => {
|
||||
const authStore = useAuthStore(pinia)
|
||||
const { isLogin } = storeToRefs(authStore)
|
||||
const { isLogin, adminInfo } = storeToRefs(authStore)
|
||||
|
||||
// 检查是否在白名单中
|
||||
const isInWhiteList = WHITE_LIST.some(path => to.path === path || to.path.startsWith(path + '/'))
|
||||
|
||||
console.log('[AuthGuard]', to.path, 'isInWhiteList:', isInWhiteList, 'isLogin:', unref(isLogin))
|
||||
|
||||
// 如果已登录且访问登录页,重定向到首页
|
||||
if (unref(isLogin) && to.path === '/auth/sign-in') {
|
||||
const role = unref(adminInfo)?.role
|
||||
if (role === 2) {
|
||||
return { path: '/monisuo/orders' }
|
||||
}
|
||||
if (role === 3) {
|
||||
return { path: '/monisuo/finance-orders' }
|
||||
}
|
||||
return { name: '/monisuo/dashboard' }
|
||||
}
|
||||
|
||||
// 如果未登录且不在白名单中,重定向到登录页
|
||||
if (!unref(isLogin) && !isInWhiteList) {
|
||||
console.log('[AuthGuard] Redirecting to login')
|
||||
return {
|
||||
name: '/auth/sign-in',
|
||||
query: { redirect: to.fullPath },
|
||||
}
|
||||
}
|
||||
|
||||
// 已登录时检查角色权限
|
||||
if (unref(isLogin) && !isInWhiteList) {
|
||||
const role = unref(adminInfo)?.role
|
||||
const isSuperAdmin = role === 1
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
const needsSuperAdmin = SUPER_ADMIN_ONLY.some(prefix => to.path.startsWith(prefix))
|
||||
if (needsSuperAdmin) {
|
||||
// 非超管尝试访问超管页面,根据角色重定向
|
||||
if (role === 3) {
|
||||
return { path: '/monisuo/finance-orders' }
|
||||
}
|
||||
return { path: '/monisuo/orders' }
|
||||
}
|
||||
|
||||
// 财务角色(role=3)只能访问财务审批页面和设置
|
||||
if (role === 3) {
|
||||
const allowed = to.path.startsWith('/monisuo/finance-orders') || to.path.startsWith('/settings')
|
||||
if (!allowed) {
|
||||
return { path: '/monisuo/finance-orders' }
|
||||
}
|
||||
}
|
||||
|
||||
// 普通管理员(role=2)不能访问财务审批页面
|
||||
if (role === 2 && to.path.startsWith('/monisuo/finance-orders')) {
|
||||
return { path: '/monisuo/orders' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ import type { Router } from 'vue-router'
|
||||
|
||||
import nprogress from 'nprogress'
|
||||
|
||||
import { authGuard } from './auth-guard'
|
||||
import { setupAuthGuard } from './auth-guard'
|
||||
|
||||
/**
|
||||
* global router guard
|
||||
* now only used for progress bar
|
||||
*/
|
||||
function setupCommonGuard(router: Router) {
|
||||
router.beforeEach(() => {
|
||||
@@ -21,5 +20,5 @@ function setupCommonGuard(router: Router) {
|
||||
|
||||
export function createRouterGuard(router: Router) {
|
||||
setupCommonGuard(router)
|
||||
authGuard(router)
|
||||
setupAuthGuard(router)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface Coin {
|
||||
code: string
|
||||
name: string
|
||||
price: number
|
||||
initialPrice?: number
|
||||
priceType: number // 1: 实时 2: 手动
|
||||
status: number
|
||||
createTime: string
|
||||
@@ -57,7 +58,9 @@ export interface OrderFund {
|
||||
username: string
|
||||
type: number // 1: 充值 2: 提现
|
||||
amount: number
|
||||
status: number // 充值: 1待付款 2待确认 3已完成 4已驳回 5已取消; 提现: 1待审批 2已完成 3已驳回 4已取消
|
||||
fee?: number // 手续费
|
||||
receivableAmount?: number // 应收款项
|
||||
status: number // 充值: 1待付款 2待确认 3已完成 4已驳回 5已取消; 提现: 1待审批 2已完成 3已驳回 4已取消 5待财务审核
|
||||
walletId?: number
|
||||
walletAddress?: string
|
||||
withdrawContact?: string
|
||||
@@ -66,6 +69,12 @@ export interface OrderFund {
|
||||
createTime: string
|
||||
rejectReason?: string
|
||||
adminRemark?: string
|
||||
approveAdminId?: number
|
||||
approveAdminName?: string
|
||||
approveTime?: string
|
||||
financeAdminId?: number
|
||||
financeAdminName?: string
|
||||
financeApproveTime?: string
|
||||
}
|
||||
|
||||
export interface ColdWallet {
|
||||
@@ -351,7 +360,91 @@ export function useToggleWalletStatusMutation() {
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 分析相关 API ==========
|
||||
// ========== 管理员管理 API ==========
|
||||
|
||||
export interface AdminRecord {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
role: number
|
||||
permissions?: string
|
||||
isSystem?: number
|
||||
status?: number
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
export function useGetAdminListQuery() {
|
||||
const { axiosInstance } = useAxios()
|
||||
|
||||
return useQuery<ApiResult<AdminRecord[]>, AxiosError>({
|
||||
queryKey: ['useGetAdminListQuery'],
|
||||
queryFn: async () => {
|
||||
const response = await axiosInstance.get('/admin/admin/list')
|
||||
return response.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateAdminMutation() {
|
||||
const { axiosInstance } = useAxios()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<ApiResult<{ id: number, username: string, role: number }>, AxiosError, { username: string, password: string, nickname?: string, role?: number }>({
|
||||
mutationKey: ['useCreateAdminMutation'],
|
||||
mutationFn: async (params) => {
|
||||
const response = await axiosInstance.post('/admin/admin/create', params)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['useGetAdminListQuery'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useToggleAdminStatusMutation() {
|
||||
const { axiosInstance } = useAxios()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<ApiResult<void>, AxiosError, { id: number, status: number }>({
|
||||
mutationKey: ['useToggleAdminStatusMutation'],
|
||||
mutationFn: async (params) => {
|
||||
const response = await axiosInstance.post('/admin/admin/status', params)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['useGetAdminListQuery'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function usePromoteUserToAdminMutation() {
|
||||
const { axiosInstance } = useAxios()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<ApiResult<{ id: number, username: string, role: number }>, AxiosError, { username: string, password: string, nickname?: string }>({
|
||||
mutationKey: ['usePromoteUserToAdminMutation'],
|
||||
mutationFn: async (params) => {
|
||||
const response = await axiosInstance.post('/admin/user/promote-admin', params)
|
||||
return response.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['useGetAdminListQuery'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['useGetUserListQuery'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useChangePasswordMutation() {
|
||||
const { axiosInstance } = useAxios()
|
||||
|
||||
return useMutation<ApiResult<void>, AxiosError, { oldPassword: string, newPassword: string }>({
|
||||
mutationKey: ['useChangePasswordMutation'],
|
||||
mutationFn: async (params) => {
|
||||
const response = await axiosInstance.post('/admin/change-password', params)
|
||||
return response.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 盈利分析
|
||||
export function useGetProfitAnalysisQuery(range: string = 'month') {
|
||||
|
||||
26
monisuo-admin/src/types/route-map.d.ts
vendored
26
monisuo-admin/src/types/route-map.d.ts
vendored
@@ -61,6 +61,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/monisuo/admins': RouteRecordInfo<
|
||||
'/monisuo/admins',
|
||||
'/monisuo/admins',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/monisuo/analytics': RouteRecordInfo<
|
||||
'/monisuo/analytics',
|
||||
'/monisuo/analytics',
|
||||
@@ -82,6 +89,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/monisuo/finance-orders': RouteRecordInfo<
|
||||
'/monisuo/finance-orders',
|
||||
'/monisuo/finance-orders',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/monisuo/orders': RouteRecordInfo<
|
||||
'/monisuo/orders',
|
||||
'/monisuo/orders',
|
||||
@@ -168,6 +182,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/monisuo/admins.vue': {
|
||||
routes:
|
||||
| '/monisuo/admins'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/monisuo/analytics.vue': {
|
||||
routes:
|
||||
| '/monisuo/analytics'
|
||||
@@ -186,6 +206,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/monisuo/finance-orders.vue': {
|
||||
routes:
|
||||
| '/monisuo/finance-orders'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/monisuo/orders.vue': {
|
||||
routes:
|
||||
| '/monisuo/orders'
|
||||
|
||||
Reference in New Issue
Block a user