2025-12-10 20:52:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="space-y-6 pb-8">
|
|
|
|
|
|
<!-- 用户表格 -->
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
class="overflow-hidden"
|
|
|
|
|
|
>
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<UserManagementHeader
|
|
|
|
|
|
:search-query="searchQuery"
|
|
|
|
|
|
:filter-role="filterRole"
|
|
|
|
|
|
:filter-group="filterGroup"
|
|
|
|
|
|
:filter-status="filterStatus"
|
|
|
|
|
|
:sort-option="sortOption"
|
|
|
|
|
|
:user-groups="userGroups"
|
|
|
|
|
|
:role-options="userRoleFilterOptions"
|
|
|
|
|
|
:status-options="userStatusFilterOptions"
|
|
|
|
|
|
:sort-options="userSortOptions"
|
|
|
|
|
|
:loading="usersStore.loading"
|
|
|
|
|
|
:can-operate-admin="authStore.canOperateAdmin"
|
|
|
|
|
|
@update:search-query="searchQuery = $event"
|
|
|
|
|
|
@update:filter-role="filterRole = $event"
|
|
|
|
|
|
@update:filter-group="filterGroup = $event"
|
|
|
|
|
|
@update:filter-status="filterStatus = $event"
|
|
|
|
|
|
@update:sort-option="sortOption = $event"
|
|
|
|
|
|
@open-groups="showUserGroupsDialog = true"
|
|
|
|
|
|
@create-user="openCreateDialog"
|
2026-07-15 23:47:19 +08:00
|
|
|
|
@refresh="handleManualRefresh"
|
2026-06-30 14:14:12 +08:00
|
|
|
|
/>
|
2025-12-13 22:26:47 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<UserSelectionToolbar
|
|
|
|
|
|
:is-all-filtered-selected="isAllFilteredSelected"
|
|
|
|
|
|
:is-partially-filtered-selected="isPartiallyFilteredSelected"
|
|
|
|
|
|
:filtered-user-count="filteredUserCount"
|
|
|
|
|
|
:current-page-count="paginatedUsers.length"
|
|
|
|
|
|
:selected-count="selectedCount"
|
|
|
|
|
|
:is-current-page-fully-selected="isCurrentPageFullySelected"
|
|
|
|
|
|
:can-clear-selection="canClearSelection"
|
|
|
|
|
|
:select-all-filtered="selectAllFiltered"
|
|
|
|
|
|
:loading="usersStore.loading"
|
|
|
|
|
|
:can-operate-admin="authStore.canOperateAdmin"
|
|
|
|
|
|
:group-count="userGroups.length"
|
|
|
|
|
|
@toggle-select-filtered="toggleSelectFiltered"
|
|
|
|
|
|
@toggle-select-current-page="toggleSelectCurrentPage"
|
|
|
|
|
|
@clear-selection="clearSelection"
|
|
|
|
|
|
@open-batch-dialog="openUserBatchDialog"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<UserManagementList
|
|
|
|
|
|
:rows="userRows"
|
|
|
|
|
|
:selected-id-set="selectedIdSet"
|
|
|
|
|
|
:select-all-filtered="selectAllFiltered"
|
|
|
|
|
|
:is-all-filtered-selected="isAllFilteredSelected"
|
|
|
|
|
|
:is-partially-filtered-selected="isPartiallyFilteredSelected"
|
|
|
|
|
|
:is-current-page-fully-selected="isCurrentPageFullySelected"
|
|
|
|
|
|
:selection-disabled="selectAllFiltered || usersStore.loading"
|
|
|
|
|
|
:loading="usersStore.loading"
|
|
|
|
|
|
:can-operate-admin="authStore.canOperateAdmin"
|
|
|
|
|
|
:has-filters="hasUserFilters"
|
|
|
|
|
|
:sort-by="sortBy"
|
|
|
|
|
|
:sort-order="sortOrder"
|
|
|
|
|
|
@toggle-selected="toggleOne"
|
|
|
|
|
|
@toggle-select-current-page="toggleSelectCurrentPage"
|
|
|
|
|
|
@edit="editUser"
|
|
|
|
|
|
@wallet="openWalletActionDialog"
|
|
|
|
|
|
@plans="manageUserPlans"
|
|
|
|
|
|
@api-keys="manageApiKeys"
|
|
|
|
|
|
@sessions="manageUserSessions"
|
|
|
|
|
|
@toggle-status="toggleUserStatus"
|
|
|
|
|
|
@delete="deleteUser"
|
|
|
|
|
|
@sort="handleTableSort"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 分页控件 -->
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
:current="currentPage"
|
2026-05-20 16:51:46 +08:00
|
|
|
|
:total="filteredUserCount"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:page-size="pageSize"
|
2026-01-17 20:21:03 +08:00
|
|
|
|
cache-key="users-page-size"
|
2026-05-20 16:51:46 +08:00
|
|
|
|
@update:current="handlePageChange"
|
|
|
|
|
|
@update:page-size="handlePageSizeChange"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 用户表单对话框(创建/编辑共用) -->
|
|
|
|
|
|
<UserFormDialog
|
2025-12-12 16:15:54 +08:00
|
|
|
|
ref="userFormDialogRef"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:open="showUserFormDialog"
|
|
|
|
|
|
:user="editingUser"
|
2026-05-09 21:47:33 +08:00
|
|
|
|
:groups="userGroups"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@close="closeUserFormDialog"
|
|
|
|
|
|
@submit="handleUserFormSubmit"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-07 19:16:49 +08:00
|
|
|
|
<UserBatchActionDialog
|
|
|
|
|
|
:open="showUserBatchDialog"
|
|
|
|
|
|
:selected-ids="selectedIds"
|
|
|
|
|
|
:select-all-filtered="selectAllFiltered"
|
|
|
|
|
|
:selected-count="selectedCount"
|
|
|
|
|
|
:filters="batchSelectionFilters"
|
2026-05-09 21:47:33 +08:00
|
|
|
|
:groups="userGroups"
|
2026-05-07 19:16:49 +08:00
|
|
|
|
@close="showUserBatchDialog = false"
|
|
|
|
|
|
@completed="handleUserBatchCompleted"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-09 21:47:33 +08:00
|
|
|
|
<UserGroupsDialog
|
|
|
|
|
|
:open="showUserGroupsDialog"
|
2026-05-20 17:41:06 +08:00
|
|
|
|
:users-version="userOptionsVersion"
|
2026-05-09 21:47:33 +08:00
|
|
|
|
@close="showUserGroupsDialog = false"
|
|
|
|
|
|
@changed="handleUserGroupsChanged"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<UserPlanDialog
|
|
|
|
|
|
:open="showUserPlansDialog"
|
|
|
|
|
|
:user-id="selectedUser?.id || null"
|
|
|
|
|
|
:user-name="selectedUser?.username || ''"
|
|
|
|
|
|
:entitlements="userPlanEntitlements"
|
|
|
|
|
|
:plans="grantableBillingPlans"
|
|
|
|
|
|
:selected-plan-id="selectedGrantPlanId"
|
|
|
|
|
|
:grant-reason="grantReason"
|
|
|
|
|
|
:loading-entitlements="loadingUserPlans"
|
|
|
|
|
|
:loading-plans="loadingBillingPlans"
|
|
|
|
|
|
:granting="grantingUserPlan"
|
|
|
|
|
|
:format-date-time="formatDateTime"
|
|
|
|
|
|
:format-plan-price="formatPlanPrice"
|
|
|
|
|
|
:format-plan-duration="formatPlanDuration"
|
|
|
|
|
|
:entitlement-labels="entitlementLabels"
|
|
|
|
|
|
@close="showUserPlansDialog = false"
|
|
|
|
|
|
@update:selected-plan-id="selectedGrantPlanId = $event"
|
|
|
|
|
|
@update:grant-reason="grantReason = $event"
|
|
|
|
|
|
@refresh-entitlements="loadUserPlanEntitlements"
|
|
|
|
|
|
@grant="grantPlanToSelectedUser"
|
|
|
|
|
|
/>
|
2026-05-13 01:18:38 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<UserApiKeysDialog
|
|
|
|
|
|
:open="showApiKeysDialog"
|
|
|
|
|
|
:api-keys="userApiKeys"
|
|
|
|
|
|
:creating="creatingApiKey"
|
|
|
|
|
|
:format-rate-limit="formatRateLimitSimple"
|
|
|
|
|
|
:format-concurrent-limit="formatConcurrentLimitSimple"
|
|
|
|
|
|
:format-ip-rules="formatIpRules"
|
|
|
|
|
|
@close="showApiKeysDialog = false"
|
|
|
|
|
|
@create-key="openCreateUserApiKeyDialog"
|
|
|
|
|
|
@edit-key="openEditUserApiKeyDialog"
|
|
|
|
|
|
@toggle-lock="toggleLockApiKey"
|
|
|
|
|
|
@delete-key="deleteApiKey"
|
|
|
|
|
|
@copy-full-key="copyFullKey"
|
|
|
|
|
|
/>
|
2026-05-13 01:18:38 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<UserApiKeyFormDialog
|
|
|
|
|
|
:open="showUserApiKeyFormDialog"
|
|
|
|
|
|
:form="userApiKeyForm"
|
|
|
|
|
|
:is-editing="Boolean(editingUserApiKey)"
|
|
|
|
|
|
:creating="creatingApiKey"
|
|
|
|
|
|
@close="closeUserApiKeyFormDialog"
|
|
|
|
|
|
@update:form="userApiKeyForm = $event"
|
|
|
|
|
|
@submit="submitUserApiKeyForm"
|
|
|
|
|
|
/>
|
2026-05-13 01:18:38 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<UserSessionsDialog
|
|
|
|
|
|
:open="showUserSessionsDialog"
|
|
|
|
|
|
:sessions="userSessions"
|
|
|
|
|
|
:loading="loadingUserSessions"
|
|
|
|
|
|
:action-loading="sessionDialogActionLoading"
|
|
|
|
|
|
:format-date="formatDate"
|
|
|
|
|
|
:format-session-meta="formatSessionMeta"
|
|
|
|
|
|
@close="showUserSessionsDialog = false"
|
|
|
|
|
|
@revoke-session="revokeSelectedUserSession"
|
|
|
|
|
|
@revoke-all="revokeAllSelectedUserSessions"
|
|
|
|
|
|
/>
|
2026-03-17 16:34:09 +08:00
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
<WalletOpsDrawer
|
|
|
|
|
|
:open="showWalletActionDialogState"
|
|
|
|
|
|
:wallet="walletActionTarget?.wallet || null"
|
|
|
|
|
|
:owner-name="walletActionTarget?.user.username || ''"
|
2026-06-30 14:14:12 +08:00
|
|
|
|
:owner-subtitle="walletActionTarget?.user.email || legacyT('未设置邮箱')"
|
|
|
|
|
|
:context-label="legacyT('用户钱包')"
|
2026-03-08 00:05:48 +08:00
|
|
|
|
accent="emerald"
|
|
|
|
|
|
@close="closeWalletActionDrawer"
|
|
|
|
|
|
@changed="handleWalletDrawerChanged"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
<NewApiKeyDialog
|
|
|
|
|
|
:open="showNewApiKeyDialog"
|
|
|
|
|
|
:api-key="newApiKey"
|
|
|
|
|
|
@close="closeNewApiKeyDialog"
|
|
|
|
|
|
@copy="copyApiKey"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-15 23:47:19 +08:00
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import { useUsersStore } from '@/stores/users'
|
2026-05-13 18:12:46 +08:00
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
2026-05-25 12:21:32 +08:00
|
|
|
|
import {
|
|
|
|
|
|
usersApi,
|
|
|
|
|
|
type User,
|
|
|
|
|
|
type ApiKey,
|
|
|
|
|
|
type UserSession,
|
|
|
|
|
|
type UserBatchActionResponse,
|
|
|
|
|
|
type UserBatchSelectionFilters,
|
|
|
|
|
|
type UserGroup,
|
|
|
|
|
|
type AdminUserPlanEntitlement,
|
|
|
|
|
|
type AdminUserSortBy,
|
|
|
|
|
|
type AdminUserSortOrder,
|
|
|
|
|
|
} from '@/api/users'
|
2026-03-17 16:34:09 +08:00
|
|
|
|
import { formatSessionMeta } from '@/types/session'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
import { adminWalletApi, type AdminWallet } from '@/api/admin-wallets'
|
2026-05-13 01:18:38 +08:00
|
|
|
|
import { adminBillingPlansApi, type BillingEntitlement, type BillingPlan } from '@/api/billing'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
|
|
import { useConfirm } from '@/composables/useConfirm'
|
2025-12-28 20:41:52 +08:00
|
|
|
|
import { useClipboard } from '@/composables/useClipboard'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import { adminApi } from '@/api/admin'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
import { walletStatusBadge, walletStatusLabel } from '@/utils/walletDisplay'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
// UI 组件
|
|
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Pagination,
|
|
|
|
|
|
} from '@/components/ui'
|
|
|
|
|
|
|
|
|
|
|
|
// 功能组件
|
2026-06-30 14:14:12 +08:00
|
|
|
|
import NewApiKeyDialog from '@/features/users/components/NewApiKeyDialog.vue'
|
|
|
|
|
|
import UserApiKeyFormDialog, { type UserApiKeyFormState } from '@/features/users/components/UserApiKeyFormDialog.vue'
|
|
|
|
|
|
import UserApiKeysDialog from '@/features/users/components/UserApiKeysDialog.vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import UserFormDialog, { type UserFormData } from '@/features/users/components/UserFormDialog.vue'
|
2026-05-07 19:16:49 +08:00
|
|
|
|
import UserBatchActionDialog from '@/features/users/components/UserBatchActionDialog.vue'
|
2026-05-09 21:47:33 +08:00
|
|
|
|
import UserGroupsDialog from '@/features/users/components/UserGroupsDialog.vue'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
import UserManagementHeader from '@/features/users/components/UserManagementHeader.vue'
|
|
|
|
|
|
import UserManagementList from '@/features/users/components/UserManagementList.vue'
|
|
|
|
|
|
import UserPlanDialog from '@/features/users/components/UserPlanDialog.vue'
|
|
|
|
|
|
import UserSelectionToolbar from '@/features/users/components/UserSelectionToolbar.vue'
|
|
|
|
|
|
import UserSessionsDialog from '@/features/users/components/UserSessionsDialog.vue'
|
|
|
|
|
|
import type { UserManagementRow } from '@/features/users/components/user-management-types'
|
|
|
|
|
|
import {
|
|
|
|
|
|
USER_ROLE_FILTER_OPTIONS,
|
|
|
|
|
|
USER_SORT_OPTIONS,
|
|
|
|
|
|
USER_STATUS_FILTER_OPTIONS,
|
|
|
|
|
|
formatUserRoleLabel,
|
|
|
|
|
|
userRoleBadgeVariant,
|
|
|
|
|
|
} from '@/features/users/components/user-management-config'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
import WalletOpsDrawer from '@/features/wallet/components/WalletOpsDrawer.vue'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2026-03-15 14:22:59 +08:00
|
|
|
|
import { formatTokens, formatRateLimitInheritable, formatRateLimitSimple, isRateLimitInherited, isRateLimitUnlimited } from '@/utils/format'
|
2026-05-14 11:14:20 +08:00
|
|
|
|
import {
|
|
|
|
|
|
mergeChatPiiRedactionFeatureSettings,
|
|
|
|
|
|
readChatPiiRedactionFeatureSettings,
|
|
|
|
|
|
} from '@/utils/featureSettings'
|
2025-12-12 20:22:15 +08:00
|
|
|
|
import { log } from '@/utils/logger'
|
2026-05-07 19:16:49 +08:00
|
|
|
|
import { useBatchSelection } from '@/composables/useBatchSelection'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
import { useI18n } from '@/i18n'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
const { success, error } = useToast()
|
2026-03-08 00:05:48 +08:00
|
|
|
|
const { confirmDanger } = useConfirm()
|
2025-12-28 20:41:52 +08:00
|
|
|
|
const { copyToClipboard } = useClipboard()
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const { legacyT, locale } = useI18n()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const usersStore = useUsersStore()
|
2026-05-13 18:12:46 +08:00
|
|
|
|
const authStore = useAuthStore()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
function localizedApiError(err: unknown, fallback: string): string {
|
|
|
|
|
|
return legacyT(parseApiError(err, fallback))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
// 用户表单对话框状态
|
|
|
|
|
|
const showUserFormDialog = ref(false)
|
|
|
|
|
|
const editingUser = ref<UserFormData | null>(null)
|
|
|
|
|
|
const userFormDialogRef = ref<InstanceType<typeof UserFormDialog>>()
|
|
|
|
|
|
|
|
|
|
|
|
// API Keys 对话框状态
|
|
|
|
|
|
const showApiKeysDialog = ref(false)
|
2026-03-17 16:34:09 +08:00
|
|
|
|
const showUserSessionsDialog = ref(false)
|
2026-05-13 01:18:38 +08:00
|
|
|
|
const showUserPlansDialog = ref(false)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const showNewApiKeyDialog = ref(false)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
const showUserApiKeyFormDialog = ref(false)
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const selectedUser = ref<User | null>(null)
|
|
|
|
|
|
const userApiKeys = ref<ApiKey[]>([])
|
2026-03-17 16:34:09 +08:00
|
|
|
|
const userSessions = ref<UserSession[]>([])
|
2026-05-13 01:18:38 +08:00
|
|
|
|
const userPlanEntitlements = ref<AdminUserPlanEntitlement[]>([])
|
|
|
|
|
|
const availableBillingPlans = ref<BillingPlan[]>([])
|
|
|
|
|
|
const selectedGrantPlanId = ref('')
|
|
|
|
|
|
const grantReason = ref('')
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const newApiKey = ref('')
|
|
|
|
|
|
const creatingApiKey = ref(false)
|
2026-03-17 16:34:09 +08:00
|
|
|
|
const loadingUserSessions = ref(false)
|
2026-05-13 01:18:38 +08:00
|
|
|
|
const loadingUserPlans = ref(false)
|
|
|
|
|
|
const loadingBillingPlans = ref(false)
|
|
|
|
|
|
const grantingUserPlan = ref(false)
|
2026-03-17 16:34:09 +08:00
|
|
|
|
const sessionDialogActionLoading = ref<string | null>(null)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
const editingUserApiKey = ref<ApiKey | null>(null)
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const userApiKeyForm = ref<UserApiKeyFormState>({
|
2026-03-15 14:22:59 +08:00
|
|
|
|
name: '',
|
2026-06-30 14:14:12 +08:00
|
|
|
|
rate_limit: undefined,
|
|
|
|
|
|
concurrent_limit: undefined,
|
2026-05-20 16:11:49 +08:00
|
|
|
|
ip_rules_text: '',
|
2026-05-14 11:14:20 +08:00
|
|
|
|
chat_pii_redaction_enabled: false,
|
|
|
|
|
|
chat_pii_redaction_placeholder_notice: true,
|
2026-03-15 14:22:59 +08:00
|
|
|
|
})
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 用户统计
|
2026-03-08 00:05:48 +08:00
|
|
|
|
const userWalletMap = ref<Record<string, AdminWallet>>({})
|
|
|
|
|
|
|
|
|
|
|
|
const showWalletActionDialogState = ref(false)
|
|
|
|
|
|
const walletActionTarget = ref<{ user: User; wallet: AdminWallet } | null>(null)
|
2026-05-07 19:16:49 +08:00
|
|
|
|
const showUserBatchDialog = ref(false)
|
2026-05-09 21:47:33 +08:00
|
|
|
|
const showUserGroupsDialog = ref(false)
|
2026-05-20 17:41:06 +08:00
|
|
|
|
const userOptionsVersion = ref(0)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
const searchQuery = ref('')
|
2026-05-20 16:51:46 +08:00
|
|
|
|
const filterRole = ref<'all' | User['role']>('all')
|
|
|
|
|
|
const filterStatus = ref<'all' | 'active' | 'inactive'>('all')
|
2026-05-09 21:47:33 +08:00
|
|
|
|
const filterGroup = ref('all')
|
2026-05-25 12:21:32 +08:00
|
|
|
|
const sortOption = ref<'default' | 'created_at_desc' | 'created_at_asc'>('default')
|
2026-05-09 21:47:33 +08:00
|
|
|
|
const userGroups = ref<UserGroup[]>([])
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const userRoleFilterOptions = USER_ROLE_FILTER_OPTIONS
|
|
|
|
|
|
const userStatusFilterOptions = USER_STATUS_FILTER_OPTIONS
|
|
|
|
|
|
const userSortOptions = USER_SORT_OPTIONS
|
2026-05-25 12:21:32 +08:00
|
|
|
|
const sortBy = computed<AdminUserSortBy | null>(() =>
|
|
|
|
|
|
sortOption.value === 'default' ? null : 'created_at'
|
|
|
|
|
|
)
|
|
|
|
|
|
const sortOrder = computed<AdminUserSortOrder>(() =>
|
|
|
|
|
|
sortOption.value === 'created_at_asc' ? 'asc' : 'desc'
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
const currentPage = ref(1)
|
|
|
|
|
|
const pageSize = ref(20)
|
2026-04-19 15:17:25 +08:00
|
|
|
|
const USERS_PAGE_CACHE_TTL_MS = 10 * 1000
|
|
|
|
|
|
const USER_WALLETS_CACHE_TTL_MS = 10 * 1000
|
2026-07-15 23:47:19 +08:00
|
|
|
|
const USERS_SEARCH_DEBOUNCE_MS = 300
|
2026-04-19 15:17:25 +08:00
|
|
|
|
let userWalletsRequestId = 0
|
2026-07-15 23:47:19 +08:00
|
|
|
|
let usersSearchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-05-20 17:41:06 +08:00
|
|
|
|
const filteredUsers = computed(() => usersStore.users)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-05-20 16:51:46 +08:00
|
|
|
|
const paginatedUsers = computed(() => filteredUsers.value)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-05-20 16:51:46 +08:00
|
|
|
|
const filteredUserCount = computed(() => usersStore.total)
|
2026-05-07 19:16:49 +08:00
|
|
|
|
const {
|
|
|
|
|
|
selectedIds,
|
|
|
|
|
|
selectAllFiltered,
|
|
|
|
|
|
selectedIdSet,
|
|
|
|
|
|
selectedCount,
|
|
|
|
|
|
isAllFilteredSelected,
|
|
|
|
|
|
isPartiallyFilteredSelected,
|
|
|
|
|
|
isCurrentPageFullySelected,
|
|
|
|
|
|
canClearSelection,
|
|
|
|
|
|
rememberItems: rememberBatchPageUsers,
|
|
|
|
|
|
resetSelection: resetBatchSelection,
|
|
|
|
|
|
toggleOne,
|
|
|
|
|
|
toggleSelectFiltered,
|
|
|
|
|
|
toggleSelectCurrentPage,
|
|
|
|
|
|
clearSelection,
|
|
|
|
|
|
} = useBatchSelection<User>({
|
|
|
|
|
|
pageItems: paginatedUsers,
|
|
|
|
|
|
filteredTotal: filteredUserCount,
|
|
|
|
|
|
getItemId: (user) => user.id,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const batchSelectionFilters = computed<UserBatchSelectionFilters>(() => {
|
|
|
|
|
|
const filters: UserBatchSelectionFilters = {}
|
|
|
|
|
|
const search = searchQuery.value.trim()
|
|
|
|
|
|
if (search) filters.search = search
|
2026-05-13 18:12:46 +08:00
|
|
|
|
if (filterRole.value === 'admin' || filterRole.value === 'audit_admin' || filterRole.value === 'user') filters.role = filterRole.value
|
2026-05-07 19:16:49 +08:00
|
|
|
|
if (filterStatus.value === 'active') filters.is_active = true
|
|
|
|
|
|
if (filterStatus.value === 'inactive') filters.is_active = false
|
2026-05-09 21:47:33 +08:00
|
|
|
|
if (filterGroup.value !== 'all') filters.group_id = filterGroup.value
|
2026-05-07 19:16:49 +08:00
|
|
|
|
return filters
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-13 01:18:38 +08:00
|
|
|
|
const grantableBillingPlans = computed(() =>
|
|
|
|
|
|
availableBillingPlans.value.filter((plan) => hasPackageEntitlement(plan.entitlements))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const hasUserFilters = computed(() =>
|
|
|
|
|
|
Boolean(searchQuery.value.trim())
|
|
|
|
|
|
|| filterRole.value !== 'all'
|
|
|
|
|
|
|| filterStatus.value !== 'all'
|
|
|
|
|
|
|| filterGroup.value !== 'all'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const userRows = computed<UserManagementRow[]>(() =>
|
|
|
|
|
|
paginatedUsers.value.map((user) => {
|
|
|
|
|
|
const totalBalance = getUserWalletTotalBalance(user)
|
|
|
|
|
|
const walletStatus = getUserWalletStatus(user.id)
|
|
|
|
|
|
return {
|
|
|
|
|
|
user,
|
|
|
|
|
|
roleLabel: legacyT(formatUserRoleLabel(user.role)),
|
|
|
|
|
|
roleBadgeVariant: userRoleBadgeVariant(user.role),
|
|
|
|
|
|
isUnlimited: isUserUnlimited(user),
|
|
|
|
|
|
hasWallet: Boolean(getUserWallet(user.id)),
|
|
|
|
|
|
totalBalanceLabel: formatCurrencyValue(totalBalance, '-'),
|
|
|
|
|
|
packageBalanceLabel: formatCurrencyValue(getUserPackageBalance(user), '$0.00'),
|
|
|
|
|
|
walletBalanceLabel: formatCurrencyValue(getUserWalletBalance(user), '$0.00'),
|
|
|
|
|
|
consumedLabel: `$${getUserWalletConsumed(user).toFixed(2)}`,
|
|
|
|
|
|
isNegativeBalance: isNegativeWalletValue(totalBalance),
|
|
|
|
|
|
walletStatusLabel: walletStatusLabel(walletStatus),
|
|
|
|
|
|
walletStatusVariant: walletStatusBadge(walletStatus),
|
|
|
|
|
|
requestCountLabel: formatNumber(user.request_count),
|
|
|
|
|
|
tokensLabel: formatTokens(user.total_tokens ?? 0),
|
|
|
|
|
|
rateLimitLabel: formatRateLimitInheritable(user.rate_limit),
|
|
|
|
|
|
rateLimitSource: formatUserEffectiveRateLimitSource(user),
|
|
|
|
|
|
rateLimitAsBadge: isRateLimitInherited(user.rate_limit) || isRateLimitUnlimited(user.rate_limit),
|
|
|
|
|
|
createdAtLabel: formatDate(user.created_at),
|
|
|
|
|
|
statusLabel: legacyT(user.is_active ? '活跃' : '禁用'),
|
|
|
|
|
|
statusVariant: user.is_active ? 'success' : 'destructive',
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-15 23:47:19 +08:00
|
|
|
|
function resetUserListForFilterChange() {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
currentPage.value = 1
|
2026-05-07 19:16:49 +08:00
|
|
|
|
resetBatchSelection()
|
2026-07-15 23:47:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearUsersSearchDebounce() {
|
|
|
|
|
|
if (usersSearchDebounceTimer) {
|
|
|
|
|
|
clearTimeout(usersSearchDebounceTimer)
|
|
|
|
|
|
usersSearchDebounceTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(searchQuery, () => {
|
|
|
|
|
|
resetUserListForFilterChange()
|
|
|
|
|
|
clearUsersSearchDebounce()
|
|
|
|
|
|
usersSearchDebounceTimer = setTimeout(() => {
|
|
|
|
|
|
usersSearchDebounceTimer = null
|
|
|
|
|
|
void refreshUsers()
|
|
|
|
|
|
}, USERS_SEARCH_DEBOUNCE_MS)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch([filterRole, filterStatus, filterGroup, sortOption], () => {
|
|
|
|
|
|
resetUserListForFilterChange()
|
|
|
|
|
|
clearUsersSearchDebounce()
|
2026-05-20 16:51:46 +08:00
|
|
|
|
void refreshUsers()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-07 19:16:49 +08:00
|
|
|
|
watch(paginatedUsers, (users) => rememberBatchPageUsers(users), { immediate: true })
|
|
|
|
|
|
|
2026-04-19 15:17:25 +08:00
|
|
|
|
onMounted(() => {
|
2026-07-15 23:47:19 +08:00
|
|
|
|
void refreshUsers({ preferCache: true }).then(() =>
|
|
|
|
|
|
loadUserWallets({ cacheTtlMs: USER_WALLETS_CACHE_TTL_MS })
|
|
|
|
|
|
)
|
|
|
|
|
|
void loadUserGroups()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
clearUsersSearchDebounce()
|
|
|
|
|
|
userWalletsRequestId += 1
|
2025-12-10 20:52:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-19 15:17:25 +08:00
|
|
|
|
async function refreshUsers(options: { preferCache?: boolean } = {}) {
|
|
|
|
|
|
const cacheTtlMs = options.preferCache ? USERS_PAGE_CACHE_TTL_MS : 0
|
2026-05-20 16:51:46 +08:00
|
|
|
|
const search = searchQuery.value.trim()
|
2026-07-15 23:47:19 +08:00
|
|
|
|
await usersStore.fetchUsers({
|
|
|
|
|
|
cacheTtlMs,
|
|
|
|
|
|
search: search || undefined,
|
|
|
|
|
|
role: filterRole.value === 'all' ? undefined : filterRole.value,
|
|
|
|
|
|
is_active: filterStatus.value === 'all' ? undefined : filterStatus.value === 'active',
|
|
|
|
|
|
group_id: filterGroup.value === 'all' ? undefined : filterGroup.value,
|
|
|
|
|
|
sort_by: sortBy.value ?? undefined,
|
|
|
|
|
|
sort_order: sortBy.value ? sortOrder.value : undefined,
|
|
|
|
|
|
skip: (currentPage.value - 1) * pageSize.value,
|
|
|
|
|
|
limit: pageSize.value,
|
2026-04-19 15:17:25 +08:00
|
|
|
|
})
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-15 23:47:19 +08:00
|
|
|
|
async function handleManualRefresh() {
|
|
|
|
|
|
clearUsersSearchDebounce()
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
refreshUsers(),
|
|
|
|
|
|
loadUserGroups(),
|
|
|
|
|
|
loadUserWallets(),
|
|
|
|
|
|
])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 12:21:32 +08:00
|
|
|
|
function handleTableSort(payload: { key: string, direction: AdminUserSortOrder }): void {
|
|
|
|
|
|
if (payload.key !== 'created_at') return
|
|
|
|
|
|
sortOption.value = payload.direction === 'asc' ? 'created_at_asc' : 'created_at_desc'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 16:51:46 +08:00
|
|
|
|
function handlePageChange(page: number): void {
|
|
|
|
|
|
currentPage.value = page
|
|
|
|
|
|
void refreshUsers({ preferCache: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePageSizeChange(size: number): void {
|
|
|
|
|
|
pageSize.value = size
|
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
resetBatchSelection()
|
|
|
|
|
|
void refreshUsers()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 21:47:33 +08:00
|
|
|
|
async function loadUserGroups(): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await usersStore.listUserGroups()
|
|
|
|
|
|
userGroups.value = response.items
|
|
|
|
|
|
if (filterGroup.value !== 'all' && !userGroups.value.some((group) => group.id === filterGroup.value)) {
|
|
|
|
|
|
filterGroup.value = 'all'
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
log.error('加载用户分组失败:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleUserGroupsChanged(): Promise<void> {
|
2026-07-15 23:47:19 +08:00
|
|
|
|
await Promise.all([refreshUsers(), loadUserGroups()])
|
2026-05-09 21:47:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 19:16:49 +08:00
|
|
|
|
function openUserBatchDialog(): void {
|
2026-05-09 21:47:33 +08:00
|
|
|
|
if (selectedCount.value === 0 && userGroups.value.length === 0) return
|
2026-05-07 19:16:49 +08:00
|
|
|
|
showUserBatchDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleUserBatchCompleted(_result: UserBatchActionResponse): Promise<void> {
|
2026-07-15 23:47:19 +08:00
|
|
|
|
await Promise.all([refreshUsers(), loadUserWallets()])
|
2026-05-07 19:16:49 +08:00
|
|
|
|
resetBatchSelection(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 17:41:06 +08:00
|
|
|
|
function invalidateUserOptions(): void {
|
|
|
|
|
|
userOptionsVersion.value += 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
function formatDate(dateString: string) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return new Date(dateString).toLocaleDateString(locale.value)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 01:18:38 +08:00
|
|
|
|
function formatDateTime(value?: string | null): string {
|
|
|
|
|
|
if (!value) return '-'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return new Date(value).toLocaleString(locale.value, {
|
2026-05-13 01:18:38 +08:00
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatPlanPrice(plan: BillingPlan): string {
|
|
|
|
|
|
return `${Number(plan.price_amount || 0).toFixed(2)} ${plan.price_currency || 'CNY'}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatPlanDuration(plan: BillingPlan): string {
|
|
|
|
|
|
const labels: Record<string, string> = {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
day: legacyT('天'),
|
|
|
|
|
|
month: legacyT('个月'),
|
|
|
|
|
|
year: legacyT('年'),
|
|
|
|
|
|
custom: legacyT('天'),
|
2026-05-13 01:18:38 +08:00
|
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const unit = labels[plan.duration_unit] || legacyT('天')
|
2026-05-13 01:18:38 +08:00
|
|
|
|
return `${Number(plan.duration_value || 1)}${unit}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function entitlementLabels(items: BillingEntitlement[] | undefined): string[] {
|
|
|
|
|
|
return (items || []).map((item) => {
|
|
|
|
|
|
if (item.type === 'wallet_credit') {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return `${legacyT('附赠余额')} $${Number(item.amount_usd || 0).toFixed(2)}`
|
2026-05-13 01:18:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (item.type === 'daily_quota') {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return `${legacyT('每日额度')} $${Number(item.daily_quota_usd || 0).toFixed(2)}`
|
2026-05-13 01:18:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (item.type === 'membership_group') {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return legacyT('会员权益')
|
2026-05-13 01:18:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
return item.type
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasPackageEntitlement(items: BillingEntitlement[] | undefined): boolean {
|
|
|
|
|
|
return (items || []).some((item) => item.type === 'daily_quota' || item.type === 'membership_group')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 15:17:25 +08:00
|
|
|
|
async function loadUserWallets(options: { cacheTtlMs?: number } = {}) {
|
|
|
|
|
|
const requestId = ++userWalletsRequestId
|
2026-03-08 00:05:48 +08:00
|
|
|
|
try {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
const wallets = await adminWalletApi.listAllWallets(
|
|
|
|
|
|
{ owner_type: 'user' },
|
|
|
|
|
|
{ cacheTtlMs: options.cacheTtlMs ?? 0 },
|
|
|
|
|
|
)
|
|
|
|
|
|
if (requestId !== userWalletsRequestId) return
|
2026-03-08 00:05:48 +08:00
|
|
|
|
userWalletMap.value = wallets
|
2026-04-19 15:17:25 +08:00
|
|
|
|
.filter((wallet) => !!wallet.user_id)
|
2026-03-08 00:05:48 +08:00
|
|
|
|
.reduce<Record<string, AdminWallet>>((acc, wallet) => {
|
|
|
|
|
|
acc[wallet.user_id as string] = wallet
|
|
|
|
|
|
return acc
|
|
|
|
|
|
}, {})
|
|
|
|
|
|
} catch (err) {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
if (requestId !== userWalletsRequestId) return
|
2026-03-08 00:05:48 +08:00
|
|
|
|
log.error('加载用户钱包失败:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
function formatNumber(value?: number | null): string {
|
|
|
|
|
|
const numericValue = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
|
|
|
|
|
return numericValue.toLocaleString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
function getUserWallet(userId: string): AdminWallet | null {
|
|
|
|
|
|
return userWalletMap.value[userId] || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isUserUnlimited(user: User): boolean {
|
|
|
|
|
|
const wallet = getUserWallet(user.id)
|
|
|
|
|
|
if (wallet?.limit_mode === 'unlimited' || wallet?.unlimited === true) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return Boolean(user.unlimited)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getUserWalletTotalBalance(user: User): number | null {
|
|
|
|
|
|
if (isUserUnlimited(user)) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const wallet = getUserWallet(user.id)
|
|
|
|
|
|
if (!wallet) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-05-13 01:18:38 +08:00
|
|
|
|
if (typeof wallet.total_available_balance === 'number' && Number.isFinite(wallet.total_available_balance)) {
|
|
|
|
|
|
return wallet.total_available_balance
|
|
|
|
|
|
}
|
|
|
|
|
|
return getUserWalletBalance(user) + getUserPackageBalance(user)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getUserWalletBalance(user: User): number {
|
|
|
|
|
|
const wallet = getUserWallet(user.id)
|
|
|
|
|
|
const value = wallet?.wallet_balance ?? wallet?.balance ?? 0
|
|
|
|
|
|
return Number.isFinite(value) ? value : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getUserPackageBalance(user: User): number {
|
|
|
|
|
|
const value = getUserWallet(user.id)?.package_balance ?? 0
|
|
|
|
|
|
return Number.isFinite(value) ? value : 0
|
2026-03-08 00:05:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getUserWalletConsumed(user: User): number {
|
|
|
|
|
|
return getUserWallet(user.id)?.total_consumed ?? 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getUserWalletStatus(userId: string): string | null {
|
|
|
|
|
|
return getUserWallet(userId)?.status ?? null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatCurrencyValue(value: number | null, nullLabel = '-'): string {
|
|
|
|
|
|
if (value == null) {
|
|
|
|
|
|
return nullLabel
|
|
|
|
|
|
}
|
|
|
|
|
|
return `$${value.toFixed(2)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 14:21:43 +08:00
|
|
|
|
function formatConcurrentLimitSimple(concurrentLimit?: number | null): string {
|
|
|
|
|
|
if (concurrentLimit == null || concurrentLimit === 0) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return legacyT('不限并发')
|
2026-04-17 14:21:43 +08:00
|
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return locale.value === 'en-US' ? `${concurrentLimit} concurrent` : `${concurrentLimit} 并发`
|
2026-04-17 14:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 16:11:49 +08:00
|
|
|
|
function formatIpRules(ipRules?: string[] | null): string {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return ipRules && ipRules.length > 0 ? ipRules.join(', ') : legacyT('不限制')
|
2026-05-18 20:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 16:11:49 +08:00
|
|
|
|
function parseIpRulesInput(value: string): string[] | null {
|
2026-05-18 20:48:28 +08:00
|
|
|
|
const items = value
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
return items.length > 0 ? items : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 21:47:33 +08:00
|
|
|
|
function formatUserEffectiveRateLimitSource(user: User): string {
|
|
|
|
|
|
const source = user.effective_policy?.rate_limit
|
|
|
|
|
|
if (!source) return ''
|
|
|
|
|
|
if (source.source === 'group' && source.group_name) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return `${legacyT('继承自分组:')}${source.group_name}`
|
2026-05-09 21:47:33 +08:00
|
|
|
|
}
|
2026-05-10 17:28:23 +08:00
|
|
|
|
if (source.source === 'combined') {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const groupNames = Array.isArray(source.group_names) ? source.group_names.join(locale.value === 'en-US' ? ', ' : '、') : ''
|
|
|
|
|
|
return groupNames ? `${legacyT('用户额外限制与分组叠加:')}${groupNames}` : legacyT('用户额外限制与分组叠加')
|
2026-05-10 17:28:23 +08:00
|
|
|
|
}
|
2026-05-09 21:47:33 +08:00
|
|
|
|
if (source.source === 'user') {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return legacyT('用户单独配置')
|
2026-05-09 21:47:33 +08:00
|
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
|
return legacyT('系统默认')
|
2026-05-09 21:47:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
function isNegativeWalletValue(value: number | null): boolean {
|
|
|
|
|
|
return typeof value === 'number' && value < 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
async function toggleUserStatus(user: User) {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const action = user.is_active ? '禁用' : '启用'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const localizedAction = legacyT(action)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const confirmed = await confirmDanger(
|
2026-06-30 14:14:12 +08:00
|
|
|
|
locale.value === 'en-US'
|
|
|
|
|
|
? `${localizedAction} user ${user.username}?`
|
|
|
|
|
|
: `确定要${action}用户 ${user.username} 吗?`,
|
|
|
|
|
|
locale.value === 'en-US' ? `${localizedAction} user` : `${action}用户`,
|
|
|
|
|
|
localizedAction
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await usersStore.updateUser(user.id, { is_active: !user.is_active })
|
2026-05-20 17:41:06 +08:00
|
|
|
|
invalidateUserOptions()
|
|
|
|
|
|
await refreshUsers()
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT(`用户已${action}`))
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '未知错误'), legacyT(`${action}用户失败`))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 用户表单对话框方法 ==========
|
|
|
|
|
|
|
|
|
|
|
|
function openCreateDialog() {
|
|
|
|
|
|
editingUser.value = null
|
|
|
|
|
|
showUserFormDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
function editUser(user: User) {
|
2026-01-28 01:01:05 +08:00
|
|
|
|
// 创建数组副本,避免与 store 数据共享引用
|
2025-12-10 20:52:44 +08:00
|
|
|
|
editingUser.value = {
|
|
|
|
|
|
id: user.id,
|
|
|
|
|
|
username: user.username,
|
|
|
|
|
|
email: user.email,
|
2026-03-08 00:05:48 +08:00
|
|
|
|
unlimited: user.unlimited,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
role: user.role,
|
|
|
|
|
|
is_active: user.is_active,
|
2026-05-09 21:47:33 +08:00
|
|
|
|
group_ids: (user.groups || []).map(group => group.id),
|
2026-05-14 11:14:20 +08:00
|
|
|
|
feature_settings: user.feature_settings ?? null,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
showUserFormDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeUserFormDialog() {
|
|
|
|
|
|
showUserFormDialog.value = false
|
|
|
|
|
|
editingUser.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
async function handleUserFormSubmit(data: UserFormData & { password?: string; unlimited?: boolean }) {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
userFormDialogRef.value?.setSaving(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (data.id) {
|
|
|
|
|
|
// 更新用户
|
2026-02-22 00:43:41 +08:00
|
|
|
|
const updateData: Record<string, unknown> = {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
username: data.username,
|
|
|
|
|
|
email: data.email || undefined,
|
2026-03-08 00:05:48 +08:00
|
|
|
|
unlimited: data.unlimited,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
role: data.role,
|
2026-05-09 21:47:33 +08:00
|
|
|
|
group_ids: data.group_ids ?? [],
|
2026-05-14 11:14:20 +08:00
|
|
|
|
feature_settings: data.feature_settings ?? null,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (data.password) {
|
|
|
|
|
|
updateData.password = data.password
|
|
|
|
|
|
}
|
|
|
|
|
|
await usersStore.updateUser(data.id, updateData)
|
2026-05-20 17:41:06 +08:00
|
|
|
|
invalidateUserOptions()
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('用户信息已更新'))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 创建用户
|
|
|
|
|
|
const newUser = await usersStore.createUser({
|
|
|
|
|
|
username: data.username,
|
2026-02-22 00:43:41 +08:00
|
|
|
|
password: data.password ?? '',
|
2025-12-10 20:52:44 +08:00
|
|
|
|
email: data.email || undefined,
|
2026-03-08 00:05:48 +08:00
|
|
|
|
initial_gift_usd: data.initial_gift_usd,
|
|
|
|
|
|
unlimited: data.unlimited,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
role: data.role,
|
2026-05-09 21:47:33 +08:00
|
|
|
|
group_ids: data.group_ids ?? [],
|
2026-05-14 11:14:20 +08:00
|
|
|
|
feature_settings: data.feature_settings ?? null,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
// 如果创建时指定为禁用,则更新状态
|
|
|
|
|
|
if (data.is_active === false && newUser) {
|
|
|
|
|
|
await usersStore.updateUser(newUser.id, { is_active: false })
|
|
|
|
|
|
}
|
2026-05-20 17:41:06 +08:00
|
|
|
|
invalidateUserOptions()
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('用户创建成功'))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
closeUserFormDialog()
|
2026-07-15 23:47:19 +08:00
|
|
|
|
await Promise.all([refreshUsers(), loadUserWallets()])
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const title = data.id ? '更新用户失败' : '创建用户失败'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '未知错误'), legacyT(title))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
userFormDialogRef.value?.setSaving(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
async function manageApiKeys(user: User) {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
selectedUser.value = user
|
|
|
|
|
|
showApiKeysDialog.value = true
|
|
|
|
|
|
await loadUserApiKeys(user.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 16:34:09 +08:00
|
|
|
|
async function manageUserSessions(user: User) {
|
|
|
|
|
|
selectedUser.value = user
|
|
|
|
|
|
showUserSessionsDialog.value = true
|
|
|
|
|
|
loadingUserSessions.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
userSessions.value = await usersStore.getUserSessions(user.id)
|
|
|
|
|
|
} catch (err) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '加载用户设备会话失败'), legacyT('加载用户设备会话失败'))
|
2026-03-17 16:34:09 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loadingUserSessions.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 01:18:38 +08:00
|
|
|
|
async function manageUserPlans(user: User) {
|
|
|
|
|
|
selectedUser.value = user
|
|
|
|
|
|
showUserPlansDialog.value = true
|
|
|
|
|
|
selectedGrantPlanId.value = ''
|
|
|
|
|
|
grantReason.value = ''
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
loadUserPlanEntitlements(user.id),
|
|
|
|
|
|
loadAvailableBillingPlans(),
|
|
|
|
|
|
])
|
|
|
|
|
|
if (!selectedGrantPlanId.value && grantableBillingPlans.value.length > 0) {
|
|
|
|
|
|
selectedGrantPlanId.value = grantableBillingPlans.value[0].id
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadUserPlanEntitlements(userId: string) {
|
|
|
|
|
|
loadingUserPlans.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await usersApi.listUserPlanEntitlements(userId)
|
|
|
|
|
|
userPlanEntitlements.value = response.items
|
|
|
|
|
|
} catch (err) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '加载用户套餐失败'), legacyT('加载用户套餐失败'))
|
2026-05-13 01:18:38 +08:00
|
|
|
|
userPlanEntitlements.value = []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingUserPlans.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAvailableBillingPlans() {
|
|
|
|
|
|
loadingBillingPlans.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await adminBillingPlansApi.list()
|
|
|
|
|
|
availableBillingPlans.value = response.items
|
|
|
|
|
|
if (
|
|
|
|
|
|
selectedGrantPlanId.value
|
|
|
|
|
|
&& !response.items.some((plan) => plan.id === selectedGrantPlanId.value)
|
|
|
|
|
|
) {
|
|
|
|
|
|
selectedGrantPlanId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '加载套餐列表失败'), legacyT('加载套餐列表失败'))
|
2026-05-13 01:18:38 +08:00
|
|
|
|
availableBillingPlans.value = []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingBillingPlans.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function grantPlanToSelectedUser() {
|
|
|
|
|
|
if (!selectedUser.value || !selectedGrantPlanId.value) return
|
|
|
|
|
|
grantingUserPlan.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await usersApi.grantUserPlan(selectedUser.value.id, {
|
|
|
|
|
|
plan_id: selectedGrantPlanId.value,
|
|
|
|
|
|
reason: grantReason.value.trim() || null,
|
|
|
|
|
|
})
|
|
|
|
|
|
userPlanEntitlements.value = response.items
|
|
|
|
|
|
grantReason.value = ''
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('套餐已发放'))
|
2026-05-13 01:18:38 +08:00
|
|
|
|
} catch (err) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '发放套餐失败'), legacyT('发放套餐失败'))
|
2026-05-13 01:18:38 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
grantingUserPlan.value = false
|
2026-03-17 16:34:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
async function loadUserApiKeys(userId: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
userApiKeys.value = await usersStore.getUserApiKeys(userId)
|
|
|
|
|
|
} catch (err) {
|
2025-12-12 20:22:15 +08:00
|
|
|
|
log.error('加载API Keys失败:', err)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
userApiKeys.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 14:22:59 +08:00
|
|
|
|
function openCreateUserApiKeyDialog() {
|
2026-05-14 11:14:20 +08:00
|
|
|
|
const redactionFeature = readChatPiiRedactionFeatureSettings(null)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
userApiKeyForm.value = {
|
|
|
|
|
|
name: `Key-${new Date().toISOString().split('T')[0]}`,
|
|
|
|
|
|
rate_limit: undefined,
|
2026-04-17 14:21:43 +08:00
|
|
|
|
concurrent_limit: undefined,
|
2026-05-20 16:11:49 +08:00
|
|
|
|
ip_rules_text: '',
|
2026-05-14 11:14:20 +08:00
|
|
|
|
chat_pii_redaction_enabled: redactionFeature.enabled,
|
|
|
|
|
|
chat_pii_redaction_placeholder_notice: redactionFeature.inject_model_instruction,
|
2026-03-15 14:22:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
editingUserApiKey.value = null
|
|
|
|
|
|
showUserApiKeyFormDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openEditUserApiKeyDialog(apiKey: ApiKey) {
|
2026-05-14 11:14:20 +08:00
|
|
|
|
const redactionFeature = readChatPiiRedactionFeatureSettings(apiKey.feature_settings)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
editingUserApiKey.value = apiKey
|
|
|
|
|
|
userApiKeyForm.value = {
|
|
|
|
|
|
name: apiKey.name || '',
|
|
|
|
|
|
rate_limit: apiKey.rate_limit ?? undefined,
|
2026-04-17 14:21:43 +08:00
|
|
|
|
concurrent_limit: apiKey.concurrent_limit ?? undefined,
|
2026-05-20 16:11:49 +08:00
|
|
|
|
ip_rules_text: apiKey.ip_rules?.join(', ') ?? '',
|
2026-05-14 11:14:20 +08:00
|
|
|
|
chat_pii_redaction_enabled: redactionFeature.enabled,
|
|
|
|
|
|
chat_pii_redaction_placeholder_notice: redactionFeature.inject_model_instruction,
|
2026-03-15 14:22:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
showUserApiKeyFormDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeUserApiKeyFormDialog() {
|
|
|
|
|
|
showUserApiKeyFormDialog.value = false
|
|
|
|
|
|
editingUserApiKey.value = null
|
|
|
|
|
|
userApiKeyForm.value = {
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
rate_limit: undefined,
|
2026-04-17 14:21:43 +08:00
|
|
|
|
concurrent_limit: undefined,
|
2026-05-20 16:11:49 +08:00
|
|
|
|
ip_rules_text: '',
|
2026-05-14 11:14:20 +08:00
|
|
|
|
chat_pii_redaction_enabled: false,
|
|
|
|
|
|
chat_pii_redaction_placeholder_notice: true,
|
2026-03-15 14:22:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitUserApiKeyForm() {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if (!selectedUser.value) return
|
2026-03-15 14:22:59 +08:00
|
|
|
|
if (!userApiKeyForm.value.name.trim()) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(legacyT('请输入密钥名称'), legacyT(editingUserApiKey.value ? '更新 API Key 失败' : '创建 API Key 失败'))
|
2026-03-15 14:22:59 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
creatingApiKey.value = true
|
|
|
|
|
|
try {
|
2026-05-20 16:11:49 +08:00
|
|
|
|
const ipRules = parseIpRulesInput(userApiKeyForm.value.ip_rules_text)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
if (editingUserApiKey.value) {
|
|
|
|
|
|
await usersStore.updateApiKey(selectedUser.value.id, editingUserApiKey.value.id, {
|
|
|
|
|
|
name: userApiKeyForm.value.name,
|
|
|
|
|
|
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
|
2026-04-17 14:21:43 +08:00
|
|
|
|
concurrent_limit: userApiKeyForm.value.concurrent_limit,
|
2026-05-20 16:11:49 +08:00
|
|
|
|
ip_rules: ipRules,
|
2026-05-14 11:14:20 +08:00
|
|
|
|
feature_settings: mergeChatPiiRedactionFeatureSettings(editingUserApiKey.value.feature_settings, {
|
|
|
|
|
|
enabled: userApiKeyForm.value.chat_pii_redaction_enabled,
|
|
|
|
|
|
inject_model_instruction: userApiKeyForm.value.chat_pii_redaction_placeholder_notice,
|
|
|
|
|
|
}),
|
2026-03-15 14:22:59 +08:00
|
|
|
|
})
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('API Key已更新'))
|
2026-03-15 14:22:59 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
const response = await usersStore.createApiKey(selectedUser.value.id, {
|
|
|
|
|
|
name: userApiKeyForm.value.name,
|
|
|
|
|
|
rate_limit: userApiKeyForm.value.rate_limit ?? 0,
|
2026-04-17 14:21:43 +08:00
|
|
|
|
concurrent_limit: userApiKeyForm.value.concurrent_limit,
|
2026-05-20 16:11:49 +08:00
|
|
|
|
ip_rules: ipRules,
|
2026-05-14 11:14:20 +08:00
|
|
|
|
feature_settings: mergeChatPiiRedactionFeatureSettings(null, {
|
|
|
|
|
|
enabled: userApiKeyForm.value.chat_pii_redaction_enabled,
|
|
|
|
|
|
inject_model_instruction: userApiKeyForm.value.chat_pii_redaction_placeholder_notice,
|
|
|
|
|
|
}),
|
2026-03-15 14:22:59 +08:00
|
|
|
|
})
|
|
|
|
|
|
newApiKey.value = response.key || ''
|
|
|
|
|
|
showNewApiKeyDialog.value = true
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('API Key创建成功'))
|
2026-03-15 14:22:59 +08:00
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
await loadUserApiKeys(selectedUser.value.id)
|
2026-03-15 14:22:59 +08:00
|
|
|
|
closeUserApiKeyFormDialog()
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '未知错误'), legacyT(editingUserApiKey.value ? '更新 API Key 失败' : '创建 API Key 失败'))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
creatingApiKey.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 16:34:09 +08:00
|
|
|
|
async function revokeSelectedUserSession(sessionId: string) {
|
|
|
|
|
|
if (!selectedUser.value) return
|
|
|
|
|
|
sessionDialogActionLoading.value = sessionId
|
|
|
|
|
|
try {
|
|
|
|
|
|
await usersStore.revokeUserSession(selectedUser.value.id, sessionId)
|
|
|
|
|
|
userSessions.value = userSessions.value.filter((session) => session.id !== sessionId)
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('设备已强制下线'))
|
2026-03-17 16:34:09 +08:00
|
|
|
|
} catch (err) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '强制下线失败'), legacyT('强制下线失败'))
|
2026-03-17 16:34:09 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
sessionDialogActionLoading.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function revokeAllSelectedUserSessions() {
|
|
|
|
|
|
if (!selectedUser.value) return
|
|
|
|
|
|
sessionDialogActionLoading.value = 'all'
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await usersStore.revokeAllUserSessions(selectedUser.value.id)
|
|
|
|
|
|
userSessions.value = []
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(result.revoked_count > 0
|
|
|
|
|
|
? legacyT(`已强制下线 ${result.revoked_count} 个设备`)
|
|
|
|
|
|
: legacyT('没有可下线的设备'))
|
2026-03-17 16:34:09 +08:00
|
|
|
|
} catch (err) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '强制下线全部设备失败'), legacyT('强制下线全部设备失败'))
|
2026-03-17 16:34:09 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
sessionDialogActionLoading.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
async function copyApiKey() {
|
2025-12-28 20:41:52 +08:00
|
|
|
|
await copyToClipboard(newApiKey.value)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function closeNewApiKeyDialog() {
|
|
|
|
|
|
showNewApiKeyDialog.value = false
|
|
|
|
|
|
newApiKey.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
async function deleteApiKey(apiKey: ApiKey) {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const confirmed = await confirmDanger(
|
2026-06-30 14:14:12 +08:00
|
|
|
|
locale.value === 'en-US'
|
|
|
|
|
|
? `Delete this API key?\n\n${apiKey.key_display || '****'}\n\nThis action cannot be undone.`
|
|
|
|
|
|
: `确定要删除这个API Key吗?\n\n${apiKey.key_display || '****'}\n\n此操作无法撤销。`,
|
|
|
|
|
|
legacyT('删除 API Key')
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await usersStore.deleteApiKey(selectedUser.value.id, apiKey.id)
|
|
|
|
|
|
await loadUserApiKeys(selectedUser.value.id)
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('API Key已删除'))
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '未知错误'), legacyT('删除 API Key 失败'))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
async function toggleLockApiKey(apiKey: ApiKey) {
|
2026-03-08 00:05:48 +08:00
|
|
|
|
if (!selectedUser.value) return
|
2026-01-16 01:18:54 +08:00
|
|
|
|
try {
|
2026-03-08 00:05:48 +08:00
|
|
|
|
const response = await adminApi.toggleUserApiKeyLock(selectedUser.value.id, apiKey.id)
|
2026-01-16 01:18:54 +08:00
|
|
|
|
// 更新本地状态
|
|
|
|
|
|
const index = userApiKeys.value.findIndex(k => k.id === apiKey.id)
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
userApiKeys.value[index].is_locked = response.is_locked
|
|
|
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT(response.message))
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-01-16 01:18:54 +08:00
|
|
|
|
log.error('切换密钥锁定状态失败:', err)
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '操作失败'), legacyT('锁定/解锁失败'))
|
2026-01-16 01:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
async function copyFullKey(apiKey: ApiKey) {
|
2026-03-08 00:05:48 +08:00
|
|
|
|
if (!selectedUser.value) return
|
2025-12-10 20:52:44 +08:00
|
|
|
|
try {
|
2026-03-08 00:05:48 +08:00
|
|
|
|
const response = await usersStore.getFullApiKey(selectedUser.value.id, apiKey.id)
|
2025-12-28 20:41:52 +08:00
|
|
|
|
await copyToClipboard(response.key)
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2025-12-12 20:22:15 +08:00
|
|
|
|
log.error('复制密钥失败:', err)
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '未知错误'), legacyT('复制密钥失败'))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
function openWalletActionDialog(user: User) {
|
|
|
|
|
|
const wallet = getUserWallet(user.id)
|
|
|
|
|
|
if (!wallet) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(legacyT('该用户的钱包尚未初始化,暂时无法进行资金操作'))
|
2026-03-08 00:05:48 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
walletActionTarget.value = {
|
|
|
|
|
|
user,
|
|
|
|
|
|
wallet,
|
|
|
|
|
|
}
|
|
|
|
|
|
showWalletActionDialogState.value = true
|
|
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
function closeWalletActionDrawer() {
|
|
|
|
|
|
showWalletActionDialogState.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleWalletDrawerChanged() {
|
|
|
|
|
|
await loadUserWallets()
|
|
|
|
|
|
if (!walletActionTarget.value) return
|
|
|
|
|
|
const latestWallet = getUserWallet(walletActionTarget.value.user.id)
|
|
|
|
|
|
if (latestWallet) {
|
|
|
|
|
|
walletActionTarget.value.wallet = latestWallet
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 00:43:41 +08:00
|
|
|
|
async function deleteUser(user: User) {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const confirmed = await confirmDanger(
|
2026-06-30 14:14:12 +08:00
|
|
|
|
locale.value === 'en-US'
|
|
|
|
|
|
? `Delete user ${user.username}?\n\nThis will delete:\n- User account\n- All API keys\n- All usage records\n\nThis action cannot be undone.`
|
|
|
|
|
|
: `确定要删除用户 ${user.username} 吗?\n\n此操作将删除:\n• 用户账户\n• 所有API密钥\n• 所有使用记录\n\n此操作无法撤销!`,
|
|
|
|
|
|
legacyT('删除用户')
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await usersStore.deleteUser(user.id)
|
2026-05-20 17:41:06 +08:00
|
|
|
|
invalidateUserOptions()
|
2026-05-20 17:32:47 +08:00
|
|
|
|
if (usersStore.users.length === 0 && currentPage.value > 1) {
|
|
|
|
|
|
currentPage.value -= 1
|
|
|
|
|
|
}
|
|
|
|
|
|
await refreshUsers()
|
2026-06-30 14:14:12 +08:00
|
|
|
|
success(legacyT('用户已删除'))
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-06-30 14:14:12 +08:00
|
|
|
|
error(localizedApiError(err, '未知错误'), legacyT('删除用户失败'))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|