feat(admin): add user balance/concurrency history modal

- Add new API endpoint GET /admin/users/:id/balance-history with pagination and type filter
- Add SumPositiveBalanceByUser for calculating total recharged amount
- Create UserBalanceHistoryModal component with:
  - User info header (email, username, created_at, current balance, notes, total recharged)
  - Type filter dropdown (all/balance/admin_balance/concurrency/admin_concurrency/subscription)
  - Quick deposit/withdraw buttons
  - Paginated history list with icons and colored values
- Add instant tooltip on balance column for better UX
- Add z-index prop to BaseDialog for modal stacking control
- Update i18n translations (zh/en)
This commit is contained in:
bayma888
2026-02-03 00:16:10 +08:00
parent c3d1891ccd
commit 606e29d390
12 changed files with 588 additions and 4 deletions

View File

@@ -300,8 +300,29 @@
</span>
</template>
<template #cell-balance="{ value }">
<span class="font-medium text-gray-900 dark:text-white">${{ value.toFixed(2) }}</span>
<template #cell-balance="{ value, row }">
<div class="flex items-center gap-2">
<div class="group relative">
<button
class="font-medium text-gray-900 underline decoration-dashed decoration-gray-300 underline-offset-4 transition-colors hover:text-primary-600 dark:text-white dark:decoration-dark-500 dark:hover:text-primary-400"
@click="handleBalanceHistory(row)"
>
${{ value.toFixed(2) }}
</button>
<!-- Instant tooltip -->
<div class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 shadow-lg transition-opacity duration-75 group-hover:opacity-100 dark:bg-dark-600">
{{ t('admin.users.balanceHistoryTip') }}
<div class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-dark-600"></div>
</div>
</div>
<button
@click.stop="handleDeposit(row)"
class="rounded px-2 py-0.5 text-xs font-medium text-emerald-600 transition-colors hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-900/20"
:title="t('admin.users.deposit')"
>
{{ t('admin.users.deposit') }}
</button>
</div>
</template>
<template #cell-usage="{ row }">
@@ -456,6 +477,15 @@
{{ t('admin.users.withdraw') }}
</button>
<!-- Balance History -->
<button
@click="handleBalanceHistory(user); closeActionMenu()"
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
>
<Icon name="dollar" size="sm" class="text-gray-400" :stroke-width="2" />
{{ t('admin.users.balanceHistory') }}
</button>
<div class="my-1 border-t border-gray-100 dark:border-dark-700"></div>
<!-- Delete (not for admin) -->
@@ -479,6 +509,7 @@
<UserApiKeysModal :show="showApiKeysModal" :user="viewingUser" @close="closeApiKeysModal" />
<UserAllowedGroupsModal :show="showAllowedGroupsModal" :user="allowedGroupsUser" @close="closeAllowedGroupsModal" @success="loadUsers" />
<UserBalanceModal :show="showBalanceModal" :user="balanceUser" :operation="balanceOperation" @close="closeBalanceModal" @success="loadUsers" />
<UserBalanceHistoryModal :show="showBalanceHistoryModal" :user="balanceHistoryUser" @close="closeBalanceHistoryModal" @deposit="handleDepositFromHistory" @withdraw="handleWithdrawFromHistory" />
<UserAttributesConfigModal :show="showAttributesModal" @close="handleAttributesModalClose" />
</AppLayout>
</template>
@@ -509,6 +540,7 @@ import UserEditModal from '@/components/admin/user/UserEditModal.vue'
import UserApiKeysModal from '@/components/admin/user/UserApiKeysModal.vue'
import UserAllowedGroupsModal from '@/components/admin/user/UserAllowedGroupsModal.vue'
import UserBalanceModal from '@/components/admin/user/UserBalanceModal.vue'
import UserBalanceHistoryModal from '@/components/admin/user/UserBalanceHistoryModal.vue'
const appStore = useAppStore()
@@ -828,6 +860,10 @@ const showBalanceModal = ref(false)
const balanceUser = ref<AdminUser | null>(null)
const balanceOperation = ref<'add' | 'subtract'>('add')
// Balance History modal state
const showBalanceHistoryModal = ref(false)
const balanceHistoryUser = ref<AdminUser | null>(null)
// 计算剩余天数
const getDaysRemaining = (expiresAt: string): number => {
const now = new Date()
@@ -1078,6 +1114,30 @@ const closeBalanceModal = () => {
balanceUser.value = null
}
const handleBalanceHistory = (user: AdminUser) => {
balanceHistoryUser.value = user
showBalanceHistoryModal.value = true
}
const closeBalanceHistoryModal = () => {
showBalanceHistoryModal.value = false
balanceHistoryUser.value = null
}
// Handle deposit from balance history modal
const handleDepositFromHistory = () => {
if (balanceHistoryUser.value) {
handleDeposit(balanceHistoryUser.value)
}
}
// Handle withdraw from balance history modal
const handleWithdrawFromHistory = () => {
if (balanceHistoryUser.value) {
handleWithdraw(balanceHistoryUser.value)
}
}
// 滚动时关闭菜单
const handleScroll = () => {
closeActionMenu()