mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 20:41:03 +00:00
feat: 完成用户相关组件的完整国际化支持
* 扩展语言文件新增用户功能翻译键 - 新增 user.dashboard、user.login、user.management 翻译组 - 涵盖三语言支持(zh-cn/zh-tw/en) - 包含120+翻译键covering用户仪表板、登录、管理功能 * UserDashboardView.vue 完整国际化 - 集成useI18n composable - 国际化导航标签、统计卡片、账户信息 - 响应式翻译Toast消息和错误处理 * UserLoginView.vue 完整国际化 - 国际化登录表单标签、占位符、按钮文本 - 响应式验证消息和状态提示 - 支持动态语言切换 * UserManagementView.vue 完整国际化 - 国际化用户列表、搜索过滤器、操作按钮 - 响应式确认对话框和Toast通知 - 支持参数化翻译消息(用户名、数量等) Technical implementation: - 遵循Vue 3 Composition API最佳实践 - 保持响应式设计和暗黑模式兼容性 - 统一错误处理和用户体验
This commit is contained in:
@@ -664,6 +664,134 @@ export default {
|
|||||||
batchAllFailed: 'All items failed to process'
|
batchAllFailed: 'All items failed to process'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// User-related translations
|
||||||
|
user: {
|
||||||
|
// User Dashboard
|
||||||
|
dashboard: {
|
||||||
|
title: 'Dashboard Overview',
|
||||||
|
welcomeMessage: 'Welcome to your Claude Relay dashboard',
|
||||||
|
|
||||||
|
// Navigation tabs
|
||||||
|
overview: 'Overview',
|
||||||
|
apiKeys: 'API Keys',
|
||||||
|
usageStats: 'Usage Stats',
|
||||||
|
|
||||||
|
// Welcome section
|
||||||
|
welcome: 'Welcome',
|
||||||
|
|
||||||
|
// Stats cards
|
||||||
|
activeApiKeys: 'Active API Keys',
|
||||||
|
deletedApiKeys: 'Deleted API Keys',
|
||||||
|
totalRequests: 'Total Requests',
|
||||||
|
inputTokens: 'Input Tokens',
|
||||||
|
totalCost: 'Total Cost',
|
||||||
|
|
||||||
|
// Account information section
|
||||||
|
accountInformation: 'Account Information',
|
||||||
|
username: 'Username',
|
||||||
|
displayName: 'Display Name',
|
||||||
|
email: 'Email',
|
||||||
|
role: 'Role',
|
||||||
|
memberSince: 'Member Since',
|
||||||
|
lastLogin: 'Last Login',
|
||||||
|
notAvailable: 'N/A',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
logout: 'Logout',
|
||||||
|
logoutSuccess: 'Logged out successfully',
|
||||||
|
logoutFailed: 'Logout failed',
|
||||||
|
loadProfileFailed: 'Failed to load user profile',
|
||||||
|
loadStatsFailed: 'Failed to load API keys stats'
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Login
|
||||||
|
login: {
|
||||||
|
title: 'User Sign In',
|
||||||
|
subtitle: 'Sign in to your account to manage your API keys',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
usernamePlaceholder: 'Enter your username',
|
||||||
|
passwordPlaceholder: 'Enter your password',
|
||||||
|
signIn: 'Sign In',
|
||||||
|
signingIn: 'Signing In...',
|
||||||
|
adminLogin: 'Admin Login',
|
||||||
|
|
||||||
|
// Validation and error messages
|
||||||
|
requiredFields: 'Please enter both username and password',
|
||||||
|
loginSuccess: 'Login successful!',
|
||||||
|
loginFailed: 'Login failed'
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
management: {
|
||||||
|
title: 'User Management',
|
||||||
|
description: 'Manage users, their API keys, and view usage statistics',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
|
||||||
|
// Stats cards
|
||||||
|
totalUsers: 'Total Users',
|
||||||
|
activeUsers: 'Active Users',
|
||||||
|
totalApiKeys: 'Total API Keys',
|
||||||
|
totalCost: 'Total Cost',
|
||||||
|
|
||||||
|
// Search and filters
|
||||||
|
searchPlaceholder: 'Search users...',
|
||||||
|
allRoles: 'All Roles',
|
||||||
|
user: 'User',
|
||||||
|
admin: 'Admin',
|
||||||
|
allStatus: 'All Status',
|
||||||
|
active: 'Active',
|
||||||
|
disabled: 'Disabled',
|
||||||
|
|
||||||
|
// User list
|
||||||
|
users: 'Users',
|
||||||
|
loadingUsers: 'Loading users...',
|
||||||
|
noUsersFound: 'No users found',
|
||||||
|
noUsersMatch: 'No users match your search criteria.',
|
||||||
|
noUsersCreated: 'No users have been created yet.',
|
||||||
|
|
||||||
|
// User info and actions
|
||||||
|
displayName: 'Display Name',
|
||||||
|
email: 'Email',
|
||||||
|
role: 'Role',
|
||||||
|
username: 'Username',
|
||||||
|
apiKeysCount: 'API keys',
|
||||||
|
lastLogin: 'Last login',
|
||||||
|
neverLoggedIn: 'Never logged in',
|
||||||
|
requests: 'requests',
|
||||||
|
totalCostLabel: 'total cost',
|
||||||
|
|
||||||
|
// Action buttons and tooltips
|
||||||
|
viewUsageStats: 'View Usage Stats',
|
||||||
|
disableAllApiKeys: 'Disable All API Keys',
|
||||||
|
disableUser: 'Disable User',
|
||||||
|
enableUser: 'Enable User',
|
||||||
|
changeRole: 'Change Role',
|
||||||
|
|
||||||
|
// Confirmation dialogs
|
||||||
|
disableUserTitle: 'Disable User',
|
||||||
|
enableUserTitle: 'Enable User',
|
||||||
|
disableUserMessage: 'Are you sure you want to disable user "{username}"? This will prevent them from logging in.',
|
||||||
|
enableUserMessage: 'Are you sure you want to enable user "{username}"?',
|
||||||
|
disable: 'Disable',
|
||||||
|
enable: 'Enable',
|
||||||
|
|
||||||
|
disableAllKeysTitle: 'Disable All API Keys',
|
||||||
|
disableAllKeysMessage: 'Are you sure you want to disable all {count} API keys for user "{username}"? This will prevent them from using the service.',
|
||||||
|
disableKeys: 'Disable Keys',
|
||||||
|
|
||||||
|
// Success messages
|
||||||
|
userDisabledSuccess: 'User disabled successfully',
|
||||||
|
userEnabledSuccess: 'User enabled successfully',
|
||||||
|
keysDisabledSuccess: 'Disabled {count} API keys',
|
||||||
|
|
||||||
|
// Error messages
|
||||||
|
loadUsersError: 'Failed to load users',
|
||||||
|
toggleStatusError: 'Failed to toggleStatus',
|
||||||
|
disableKeysError: 'Failed to disableKeys'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
settings: {
|
settings: {
|
||||||
title: 'System Settings',
|
title: 'System Settings',
|
||||||
|
|||||||
@@ -664,6 +664,134 @@ export default {
|
|||||||
batchAllFailed: '所有项目处理失败'
|
batchAllFailed: '所有项目处理失败'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// User-related translations
|
||||||
|
user: {
|
||||||
|
// User Dashboard
|
||||||
|
dashboard: {
|
||||||
|
title: 'Dashboard Overview',
|
||||||
|
welcomeMessage: 'Welcome to your Claude Relay dashboard',
|
||||||
|
|
||||||
|
// Navigation tabs
|
||||||
|
overview: 'Overview',
|
||||||
|
apiKeys: 'API Keys',
|
||||||
|
usageStats: 'Usage Stats',
|
||||||
|
|
||||||
|
// Welcome section
|
||||||
|
welcome: 'Welcome',
|
||||||
|
|
||||||
|
// Stats cards
|
||||||
|
activeApiKeys: 'Active API Keys',
|
||||||
|
deletedApiKeys: 'Deleted API Keys',
|
||||||
|
totalRequests: 'Total Requests',
|
||||||
|
inputTokens: 'Input Tokens',
|
||||||
|
totalCost: 'Total Cost',
|
||||||
|
|
||||||
|
// Account information section
|
||||||
|
accountInformation: 'Account Information',
|
||||||
|
username: 'Username',
|
||||||
|
displayName: 'Display Name',
|
||||||
|
email: 'Email',
|
||||||
|
role: 'Role',
|
||||||
|
memberSince: 'Member Since',
|
||||||
|
lastLogin: 'Last Login',
|
||||||
|
notAvailable: 'N/A',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
logout: 'Logout',
|
||||||
|
logoutSuccess: 'Logged out successfully',
|
||||||
|
logoutFailed: 'Logout failed',
|
||||||
|
loadProfileFailed: 'Failed to load user profile',
|
||||||
|
loadStatsFailed: 'Failed to load API keys stats'
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Login
|
||||||
|
login: {
|
||||||
|
title: 'User Sign In',
|
||||||
|
subtitle: 'Sign in to your account to manage your API keys',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
usernamePlaceholder: 'Enter your username',
|
||||||
|
passwordPlaceholder: 'Enter your password',
|
||||||
|
signIn: 'Sign In',
|
||||||
|
signingIn: 'Signing In...',
|
||||||
|
adminLogin: 'Admin Login',
|
||||||
|
|
||||||
|
// Validation and error messages
|
||||||
|
requiredFields: 'Please enter both username and password',
|
||||||
|
loginSuccess: 'Login successful!',
|
||||||
|
loginFailed: 'Login failed'
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
management: {
|
||||||
|
title: 'User Management',
|
||||||
|
description: 'Manage users, their API keys, and view usage statistics',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
|
||||||
|
// Stats cards
|
||||||
|
totalUsers: 'Total Users',
|
||||||
|
activeUsers: 'Active Users',
|
||||||
|
totalApiKeys: 'Total API Keys',
|
||||||
|
totalCost: 'Total Cost',
|
||||||
|
|
||||||
|
// Search and filters
|
||||||
|
searchPlaceholder: 'Search users...',
|
||||||
|
allRoles: 'All Roles',
|
||||||
|
user: 'User',
|
||||||
|
admin: 'Admin',
|
||||||
|
allStatus: 'All Status',
|
||||||
|
active: 'Active',
|
||||||
|
disabled: 'Disabled',
|
||||||
|
|
||||||
|
// User list
|
||||||
|
users: 'Users',
|
||||||
|
loadingUsers: 'Loading users...',
|
||||||
|
noUsersFound: 'No users found',
|
||||||
|
noUsersMatch: 'No users match your search criteria.',
|
||||||
|
noUsersCreated: 'No users have been created yet.',
|
||||||
|
|
||||||
|
// User info and actions
|
||||||
|
displayName: 'Display Name',
|
||||||
|
email: 'Email',
|
||||||
|
role: 'Role',
|
||||||
|
username: 'Username',
|
||||||
|
apiKeysCount: 'API keys',
|
||||||
|
lastLogin: 'Last login',
|
||||||
|
neverLoggedIn: 'Never logged in',
|
||||||
|
requests: 'requests',
|
||||||
|
totalCostLabel: 'total cost',
|
||||||
|
|
||||||
|
// Action buttons and tooltips
|
||||||
|
viewUsageStats: 'View Usage Stats',
|
||||||
|
disableAllApiKeys: 'Disable All API Keys',
|
||||||
|
disableUser: 'Disable User',
|
||||||
|
enableUser: 'Enable User',
|
||||||
|
changeRole: 'Change Role',
|
||||||
|
|
||||||
|
// Confirmation dialogs
|
||||||
|
disableUserTitle: 'Disable User',
|
||||||
|
enableUserTitle: 'Enable User',
|
||||||
|
disableUserMessage: 'Are you sure you want to disable user "{username}"? This will prevent them from logging in.',
|
||||||
|
enableUserMessage: 'Are you sure you want to enable user "{username}"?',
|
||||||
|
disable: 'Disable',
|
||||||
|
enable: 'Enable',
|
||||||
|
|
||||||
|
disableAllKeysTitle: 'Disable All API Keys',
|
||||||
|
disableAllKeysMessage: 'Are you sure you want to disable all {count} API keys for user "{username}"? This will prevent them from using the service.',
|
||||||
|
disableKeys: 'Disable Keys',
|
||||||
|
|
||||||
|
// Success messages
|
||||||
|
userDisabledSuccess: 'User disabled successfully',
|
||||||
|
userEnabledSuccess: 'User enabled successfully',
|
||||||
|
keysDisabledSuccess: 'Disabled {count} API keys',
|
||||||
|
|
||||||
|
// Error messages
|
||||||
|
loadUsersError: 'Failed to load users',
|
||||||
|
toggleStatusError: 'Failed to toggleStatus',
|
||||||
|
disableKeysError: 'Failed to disableKeys'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Settings 设置页面
|
// Settings 设置页面
|
||||||
settings: {
|
settings: {
|
||||||
title: '系统设置',
|
title: '系统设置',
|
||||||
|
|||||||
@@ -664,6 +664,134 @@ export default {
|
|||||||
batchAllFailed: '所有項目處理失敗'
|
batchAllFailed: '所有項目處理失敗'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// User-related translations
|
||||||
|
user: {
|
||||||
|
// User Dashboard
|
||||||
|
dashboard: {
|
||||||
|
title: 'Dashboard Overview',
|
||||||
|
welcomeMessage: 'Welcome to your Claude Relay dashboard',
|
||||||
|
|
||||||
|
// Navigation tabs
|
||||||
|
overview: 'Overview',
|
||||||
|
apiKeys: 'API Keys',
|
||||||
|
usageStats: 'Usage Stats',
|
||||||
|
|
||||||
|
// Welcome section
|
||||||
|
welcome: 'Welcome',
|
||||||
|
|
||||||
|
// Stats cards
|
||||||
|
activeApiKeys: 'Active API Keys',
|
||||||
|
deletedApiKeys: 'Deleted API Keys',
|
||||||
|
totalRequests: 'Total Requests',
|
||||||
|
inputTokens: 'Input Tokens',
|
||||||
|
totalCost: 'Total Cost',
|
||||||
|
|
||||||
|
// Account information section
|
||||||
|
accountInformation: 'Account Information',
|
||||||
|
username: 'Username',
|
||||||
|
displayName: 'Display Name',
|
||||||
|
email: 'Email',
|
||||||
|
role: 'Role',
|
||||||
|
memberSince: 'Member Since',
|
||||||
|
lastLogin: 'Last Login',
|
||||||
|
notAvailable: 'N/A',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
logout: 'Logout',
|
||||||
|
logoutSuccess: 'Logged out successfully',
|
||||||
|
logoutFailed: 'Logout failed',
|
||||||
|
loadProfileFailed: 'Failed to load user profile',
|
||||||
|
loadStatsFailed: 'Failed to load API keys stats'
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Login
|
||||||
|
login: {
|
||||||
|
title: 'User Sign In',
|
||||||
|
subtitle: 'Sign in to your account to manage your API keys',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
usernamePlaceholder: 'Enter your username',
|
||||||
|
passwordPlaceholder: 'Enter your password',
|
||||||
|
signIn: 'Sign In',
|
||||||
|
signingIn: 'Signing In...',
|
||||||
|
adminLogin: 'Admin Login',
|
||||||
|
|
||||||
|
// Validation and error messages
|
||||||
|
requiredFields: 'Please enter both username and password',
|
||||||
|
loginSuccess: 'Login successful!',
|
||||||
|
loginFailed: 'Login failed'
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
management: {
|
||||||
|
title: 'User Management',
|
||||||
|
description: 'Manage users, their API keys, and view usage statistics',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
|
||||||
|
// Stats cards
|
||||||
|
totalUsers: 'Total Users',
|
||||||
|
activeUsers: 'Active Users',
|
||||||
|
totalApiKeys: 'Total API Keys',
|
||||||
|
totalCost: 'Total Cost',
|
||||||
|
|
||||||
|
// Search and filters
|
||||||
|
searchPlaceholder: 'Search users...',
|
||||||
|
allRoles: 'All Roles',
|
||||||
|
user: 'User',
|
||||||
|
admin: 'Admin',
|
||||||
|
allStatus: 'All Status',
|
||||||
|
active: 'Active',
|
||||||
|
disabled: 'Disabled',
|
||||||
|
|
||||||
|
// User list
|
||||||
|
users: 'Users',
|
||||||
|
loadingUsers: 'Loading users...',
|
||||||
|
noUsersFound: 'No users found',
|
||||||
|
noUsersMatch: 'No users match your search criteria.',
|
||||||
|
noUsersCreated: 'No users have been created yet.',
|
||||||
|
|
||||||
|
// User info and actions
|
||||||
|
displayName: 'Display Name',
|
||||||
|
email: 'Email',
|
||||||
|
role: 'Role',
|
||||||
|
username: 'Username',
|
||||||
|
apiKeysCount: 'API keys',
|
||||||
|
lastLogin: 'Last login',
|
||||||
|
neverLoggedIn: 'Never logged in',
|
||||||
|
requests: 'requests',
|
||||||
|
totalCostLabel: 'total cost',
|
||||||
|
|
||||||
|
// Action buttons and tooltips
|
||||||
|
viewUsageStats: 'View Usage Stats',
|
||||||
|
disableAllApiKeys: 'Disable All API Keys',
|
||||||
|
disableUser: 'Disable User',
|
||||||
|
enableUser: 'Enable User',
|
||||||
|
changeRole: 'Change Role',
|
||||||
|
|
||||||
|
// Confirmation dialogs
|
||||||
|
disableUserTitle: 'Disable User',
|
||||||
|
enableUserTitle: 'Enable User',
|
||||||
|
disableUserMessage: 'Are you sure you want to disable user "{username}"? This will prevent them from logging in.',
|
||||||
|
enableUserMessage: 'Are you sure you want to enable user "{username}"?',
|
||||||
|
disable: 'Disable',
|
||||||
|
enable: 'Enable',
|
||||||
|
|
||||||
|
disableAllKeysTitle: 'Disable All API Keys',
|
||||||
|
disableAllKeysMessage: 'Are you sure you want to disable all {count} API keys for user "{username}"? This will prevent them from using the service.',
|
||||||
|
disableKeys: 'Disable Keys',
|
||||||
|
|
||||||
|
// Success messages
|
||||||
|
userDisabledSuccess: 'User disabled successfully',
|
||||||
|
userEnabledSuccess: 'User enabled successfully',
|
||||||
|
keysDisabledSuccess: 'Disabled {count} API keys',
|
||||||
|
|
||||||
|
// Error messages
|
||||||
|
loadUsersError: 'Failed to load users',
|
||||||
|
toggleStatusError: 'Failed to toggleStatus',
|
||||||
|
disableKeysError: 'Failed to disableKeys'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Settings 設置頁面
|
// Settings 設置頁面
|
||||||
settings: {
|
settings: {
|
||||||
title: '系統設置',
|
title: '系統設置',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
]"
|
]"
|
||||||
@click="handleTabChange('overview')"
|
@click="handleTabChange('overview')"
|
||||||
>
|
>
|
||||||
Overview
|
{{ t('user.dashboard.overview') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
]"
|
]"
|
||||||
@click="handleTabChange('api-keys')"
|
@click="handleTabChange('api-keys')"
|
||||||
>
|
>
|
||||||
API Keys
|
{{ t('user.dashboard.apiKeys') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
]"
|
]"
|
||||||
@click="handleTabChange('usage')"
|
@click="handleTabChange('usage')"
|
||||||
>
|
>
|
||||||
Usage Stats
|
{{ t('user.dashboard.usageStats') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
Welcome, <span class="font-medium">{{ userStore.userName }}</span>
|
{{ t('user.dashboard.welcome') }}, <span class="font-medium">{{ userStore.userName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主题切换按钮 -->
|
<!-- 主题切换按钮 -->
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
>
|
>
|
||||||
Logout
|
{{ t('user.dashboard.logout') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
<!-- Overview Tab -->
|
<!-- Overview Tab -->
|
||||||
<div v-if="activeTab === 'overview'" class="space-y-6">
|
<div v-if="activeTab === 'overview'" class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">Dashboard Overview</h1>
|
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.dashboard.title') }}</h1>
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
Welcome to your Claude Relay dashboard
|
{{ t('user.dashboard.welcomeMessage') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Active API Keys
|
{{ t('user.dashboard.activeApiKeys') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
{{ apiKeysStats.active }}
|
{{ apiKeysStats.active }}
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Deleted API Keys
|
{{ t('user.dashboard.deletedApiKeys') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
{{ apiKeysStats.deleted }}
|
{{ apiKeysStats.deleted }}
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Total Requests
|
{{ t('user.dashboard.totalRequests') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
{{ formatNumber(userProfile?.totalUsage?.requests || 0) }}
|
{{ formatNumber(userProfile?.totalUsage?.requests || 0) }}
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Input Tokens
|
{{ t('user.dashboard.inputTokens') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
{{ formatNumber(userProfile?.totalUsage?.inputTokens || 0) }}
|
{{ formatNumber(userProfile?.totalUsage?.inputTokens || 0) }}
|
||||||
@@ -251,7 +251,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Total Cost
|
{{ t('user.dashboard.totalCost') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
${{ (userProfile?.totalUsage?.totalCost || 0).toFixed(4) }}
|
${{ (userProfile?.totalUsage?.totalCost || 0).toFixed(4) }}
|
||||||
@@ -267,30 +267,30 @@
|
|||||||
<div class="rounded-lg bg-white shadow dark:bg-gray-800">
|
<div class="rounded-lg bg-white shadow dark:bg-gray-800">
|
||||||
<div class="px-4 py-5 sm:p-6">
|
<div class="px-4 py-5 sm:p-6">
|
||||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
|
||||||
Account Information
|
{{ t('user.dashboard.accountInformation') }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-5 border-t border-gray-200 dark:border-gray-700">
|
<div class="mt-5 border-t border-gray-200 dark:border-gray-700">
|
||||||
<dl class="divide-y divide-gray-200 dark:divide-gray-700">
|
<dl class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Username</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.username') }}</dt>
|
||||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||||
{{ userProfile?.username }}
|
{{ userProfile?.username }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Display Name</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.displayName') }}</dt>
|
||||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||||
{{ userProfile?.displayName || 'N/A' }}
|
{{ userProfile?.displayName || t('user.dashboard.notAvailable') }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Email</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.email') }}</dt>
|
||||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||||
{{ userProfile?.email || 'N/A' }}
|
{{ userProfile?.email || t('user.dashboard.notAvailable') }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Role</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.role') }}</dt>
|
||||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||||
@@ -300,15 +300,15 @@
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Member Since</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.memberSince') }}</dt>
|
||||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||||
{{ formatDate(userProfile?.createdAt) }}
|
{{ formatDate(userProfile?.createdAt) }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
|
||||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Login</dt>
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.lastLogin') }}</dt>
|
||||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
|
||||||
{{ formatDate(userProfile?.lastLoginAt) || 'N/A' }}
|
{{ formatDate(userProfile?.lastLoginAt) || t('user.dashboard.notAvailable') }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -338,6 +338,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
@@ -347,6 +348,7 @@ import UserUsageStats from '@/components/user/UserUsageStats.vue'
|
|||||||
import TutorialView from '@/views/TutorialView.vue'
|
import TutorialView from '@/views/TutorialView.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
@@ -385,11 +387,11 @@ const handleTabChange = (tab) => {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await userStore.logout()
|
await userStore.logout()
|
||||||
showToast('Logged out successfully', 'success')
|
showToast(t('user.dashboard.logoutSuccess'), 'success')
|
||||||
router.push('/user-login')
|
router.push('/user-login')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error)
|
console.error('Logout error:', error)
|
||||||
showToast('Logout failed', 'error')
|
showToast(t('user.dashboard.logoutFailed'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +400,7 @@ const loadUserProfile = async () => {
|
|||||||
userProfile.value = await userStore.getUserProfile()
|
userProfile.value = await userStore.getUserProfile()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load user profile:', error)
|
console.error('Failed to load user profile:', error)
|
||||||
showToast('Failed to load user profile', 'error')
|
showToast(t('user.dashboard.loadProfileFailed'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
<span class="ml-2 text-xl font-bold text-gray-900 dark:text-white">Claude Relay</span>
|
<span class="ml-2 text-xl font-bold text-gray-900 dark:text-white">Claude Relay</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
User Sign In
|
{{ t('user.login.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
Sign in to your account to manage your API keys
|
{{ t('user.login.subtitle') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
for="username"
|
for="username"
|
||||||
>
|
>
|
||||||
Username
|
{{ t('user.login.username') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<input
|
<input
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-400 dark:focus:ring-blue-400 sm:text-sm"
|
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-400 dark:focus:ring-blue-400 sm:text-sm"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
name="username"
|
name="username"
|
||||||
placeholder="Enter your username"
|
:placeholder="t('user.login.usernamePlaceholder')"
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
for="password"
|
for="password"
|
||||||
>
|
>
|
||||||
Password
|
{{ t('user.login.password') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<input
|
<input
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-400 dark:focus:ring-blue-400 sm:text-sm"
|
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-400 dark:focus:ring-blue-400 sm:text-sm"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Enter your password"
|
:placeholder="t('user.login.passwordPlaceholder')"
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
{{ loading ? 'Signing In...' : 'Sign In' }}
|
{{ loading ? t('user.login.signingIn') : t('user.login.signIn') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
class="text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300"
|
class="text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
to="/admin-login"
|
to="/admin-login"
|
||||||
>
|
>
|
||||||
Admin Login
|
{{ t('user.login.adminLogin') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -146,12 +146,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
@@ -165,7 +167,7 @@ const form = reactive({
|
|||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!form.username || !form.password) {
|
if (!form.username || !form.password) {
|
||||||
error.value = 'Please enter both username and password'
|
error.value = t('user.login.requiredFields')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,11 +180,11 @@ const handleLogin = async () => {
|
|||||||
password: form.password
|
password: form.password
|
||||||
})
|
})
|
||||||
|
|
||||||
showToast('Login successful!', 'success')
|
showToast(t('user.login.loginSuccess'), 'success')
|
||||||
router.push('/user-dashboard')
|
router.push('/user-dashboard')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login error:', err)
|
console.error('Login error:', err)
|
||||||
error.value = err.response?.data?.message || err.message || 'Login failed'
|
error.value = err.response?.data?.message || err.message || t('user.login.loginFailed')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="sm:flex sm:items-center">
|
<div class="sm:flex sm:items-center">
|
||||||
<div class="sm:flex-auto">
|
<div class="sm:flex-auto">
|
||||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">User Management</h1>
|
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.management.title') }}</h1>
|
||||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
Manage users, their API keys, and view usage statistics
|
{{ t('user.management.description') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Refresh
|
{{ t('user.management.refresh') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Total Users
|
{{ t('user.management.totalUsers') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
{{ userStats?.totalUsers || 0 }}
|
{{ userStats?.totalUsers || 0 }}
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Active Users
|
{{ t('user.management.activeUsers') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
{{ userStats?.activeUsers || 0 }}
|
{{ userStats?.activeUsers || 0 }}
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Total API Keys
|
{{ t('user.management.totalApiKeys') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
{{ userStats?.totalApiKeys || 0 }}
|
{{ userStats?.totalApiKeys || 0 }}
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
<div class="ml-5 w-0 flex-1">
|
<div class="ml-5 w-0 flex-1">
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
<dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
Total Cost
|
{{ t('user.management.totalCost') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
<dd class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
${{ (userStats?.totalUsage?.totalCost || 0).toFixed(4) }}
|
${{ (userStats?.totalUsage?.totalCost || 0).toFixed(4) }}
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
class="block w-full rounded-md border-gray-300 pl-10 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
class="block w-full rounded-md border-gray-300 pl-10 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
||||||
placeholder="Search users..."
|
:placeholder="t('user.management.searchPlaceholder')"
|
||||||
type="search"
|
type="search"
|
||||||
@input="debouncedSearch"
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
@@ -198,9 +198,9 @@
|
|||||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
||||||
@change="loadUsers"
|
@change="loadUsers"
|
||||||
>
|
>
|
||||||
<option value="">All Roles</option>
|
<option value="">{{ t('user.management.allRoles') }}</option>
|
||||||
<option value="user">User</option>
|
<option value="user">{{ t('user.management.user') }}</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">{{ t('user.management.admin') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -211,9 +211,9 @@
|
|||||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
|
||||||
@change="loadUsers"
|
@change="loadUsers"
|
||||||
>
|
>
|
||||||
<option value="">All Status</option>
|
<option value="">{{ t('user.management.allStatus') }}</option>
|
||||||
<option value="true">Active</option>
|
<option value="true">{{ t('user.management.active') }}</option>
|
||||||
<option value="false">Disabled</option>
|
<option value="false">{{ t('user.management.disabled') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
<div class="overflow-hidden bg-white shadow dark:bg-gray-800 sm:rounded-md">
|
<div class="overflow-hidden bg-white shadow dark:bg-gray-800 sm:rounded-md">
|
||||||
<div class="border-b border-gray-200 px-4 py-5 dark:border-gray-700 sm:px-6">
|
<div class="border-b border-gray-200 px-4 py-5 dark:border-gray-700 sm:px-6">
|
||||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
|
||||||
Users
|
{{ t('user.management.users') }}
|
||||||
<span v-if="!loading" class="text-sm text-gray-500 dark:text-gray-400"
|
<span v-if="!loading" class="text-sm text-gray-500 dark:text-gray-400"
|
||||||
>({{ filteredUsers.length }} of {{ users.length }})</span
|
>({{ filteredUsers.length }} of {{ users.length }})</span
|
||||||
>
|
>
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Loading users...</p>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('user.management.loadingUsers') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Users List -->
|
<!-- Users List -->
|
||||||
@@ -299,7 +299,7 @@
|
|||||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ user.isActive ? 'Active' : 'Disabled' }}
|
{{ user.isActive ? t('user.management.active') : t('user.management.disabled') }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
@@ -318,18 +318,18 @@
|
|||||||
>
|
>
|
||||||
<span>@{{ user.username }}</span>
|
<span>@{{ user.username }}</span>
|
||||||
<span v-if="user.email">{{ user.email }}</span>
|
<span v-if="user.email">{{ user.email }}</span>
|
||||||
<span>{{ user.apiKeyCount || 0 }} API keys</span>
|
<span>{{ user.apiKeyCount || 0 }} {{ t('user.management.apiKeysCount') }}</span>
|
||||||
<span v-if="user.lastLoginAt"
|
<span v-if="user.lastLoginAt"
|
||||||
>Last login: {{ formatDate(user.lastLoginAt) }}</span
|
>{{ t('user.management.lastLogin') }}: {{ formatDate(user.lastLoginAt) }}</span
|
||||||
>
|
>
|
||||||
<span v-else>Never logged in</span>
|
<span v-else>{{ t('user.management.neverLoggedIn') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="user.totalUsage"
|
v-if="user.totalUsage"
|
||||||
class="mt-1 flex items-center space-x-4 text-xs text-gray-400 dark:text-gray-500"
|
class="mt-1 flex items-center space-x-4 text-xs text-gray-400 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
<span>{{ formatNumber(user.totalUsage.requests || 0) }} requests</span>
|
<span>{{ formatNumber(user.totalUsage.requests || 0) }} {{ t('user.management.requests') }}</span>
|
||||||
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} total cost</span>
|
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} {{ t('user.management.totalCostLabel') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
<!-- View Usage Stats -->
|
<!-- View Usage Stats -->
|
||||||
<button
|
<button
|
||||||
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-blue-600"
|
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-blue-600"
|
||||||
title="View Usage Stats"
|
:title="t('user.management.viewUsageStats')"
|
||||||
@click="viewUserStats(user)"
|
@click="viewUserStats(user)"
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
<button
|
<button
|
||||||
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
|
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
:disabled="user.apiKeyCount === 0"
|
:disabled="user.apiKeyCount === 0"
|
||||||
title="Disable All API Keys"
|
:title="t('user.management.disableAllApiKeys')"
|
||||||
@click="disableUserApiKeys(user)"
|
@click="disableUserApiKeys(user)"
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
? 'text-gray-400 hover:text-red-600'
|
? 'text-gray-400 hover:text-red-600'
|
||||||
: 'text-gray-400 hover:text-green-600'
|
: 'text-gray-400 hover:text-green-600'
|
||||||
]"
|
]"
|
||||||
:title="user.isActive ? 'Disable User' : 'Enable User'"
|
:title="user.isActive ? t('user.management.disableUser') : t('user.management.enableUser')"
|
||||||
@click="toggleUserStatus(user)"
|
@click="toggleUserStatus(user)"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -405,7 +405,7 @@
|
|||||||
<!-- Change Role -->
|
<!-- Change Role -->
|
||||||
<button
|
<button
|
||||||
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-purple-600"
|
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-purple-600"
|
||||||
title="Change Role"
|
:title="t('user.management.changeRole')"
|
||||||
@click="changeUserRole(user)"
|
@click="changeUserRole(user)"
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -437,10 +437,10 @@
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No users found</h3>
|
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ t('user.management.noUsersFound') }}</h3>
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{
|
||||||
searchQuery ? 'No users match your search criteria.' : 'No users have been created yet.'
|
searchQuery ? t('user.management.noUsersMatch') : t('user.management.noUsersCreated')
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -476,6 +476,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash-es'
|
||||||
@@ -483,6 +484,7 @@ import UserUsageStatsModal from '@/components/admin/UserUsageStatsModal.vue'
|
|||||||
import ChangeRoleModal from '@/components/admin/ChangeRoleModal.vue'
|
import ChangeRoleModal from '@/components/admin/ChangeRoleModal.vue'
|
||||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const users = ref([])
|
const users = ref([])
|
||||||
const userStats = ref(null)
|
const userStats = ref(null)
|
||||||
@@ -577,7 +579,7 @@ const loadUsers = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load users:', error)
|
console.error('Failed to load users:', error)
|
||||||
showToast('Failed to load users', 'error')
|
showToast(t('user.management.loadUsersError'), 'error')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -595,11 +597,11 @@ const viewUserStats = (user) => {
|
|||||||
const toggleUserStatus = (user) => {
|
const toggleUserStatus = (user) => {
|
||||||
selectedUser.value = user
|
selectedUser.value = user
|
||||||
confirmAction.value = {
|
confirmAction.value = {
|
||||||
title: user.isActive ? 'Disable User' : 'Enable User',
|
title: user.isActive ? t('user.management.disableUserTitle') : t('user.management.enableUserTitle'),
|
||||||
message: user.isActive
|
message: user.isActive
|
||||||
? `Are you sure you want to disable user "${user.username}"? This will prevent them from logging in.`
|
? t('user.management.disableUserMessage', { username: user.username })
|
||||||
: `Are you sure you want to enable user "${user.username}"?`,
|
: t('user.management.enableUserMessage', { username: user.username }),
|
||||||
confirmText: user.isActive ? 'Disable' : 'Enable',
|
confirmText: user.isActive ? t('user.management.disable') : t('user.management.enable'),
|
||||||
confirmClass: user.isActive ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700',
|
confirmClass: user.isActive ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700',
|
||||||
action: 'toggleStatus'
|
action: 'toggleStatus'
|
||||||
}
|
}
|
||||||
@@ -611,9 +613,9 @@ const disableUserApiKeys = (user) => {
|
|||||||
|
|
||||||
selectedUser.value = user
|
selectedUser.value = user
|
||||||
confirmAction.value = {
|
confirmAction.value = {
|
||||||
title: 'Disable All API Keys',
|
title: t('user.management.disableAllKeysTitle'),
|
||||||
message: `Are you sure you want to disable all ${user.apiKeyCount} API keys for user "${user.username}"? This will prevent them from using the service.`,
|
message: t('user.management.disableAllKeysMessage', { count: user.apiKeyCount, username: user.username }),
|
||||||
confirmText: 'Disable Keys',
|
confirmText: t('user.management.disableKeys'),
|
||||||
confirmClass: 'bg-red-600 hover:bg-red-700',
|
confirmClass: 'bg-red-600 hover:bg-red-700',
|
||||||
action: 'disableKeys'
|
action: 'disableKeys'
|
||||||
}
|
}
|
||||||
@@ -640,19 +642,19 @@ const handleConfirmAction = async () => {
|
|||||||
if (userIndex !== -1) {
|
if (userIndex !== -1) {
|
||||||
users.value[userIndex].isActive = !user.isActive
|
users.value[userIndex].isActive = !user.isActive
|
||||||
}
|
}
|
||||||
showToast(`User ${user.isActive ? 'disabled' : 'enabled'} successfully`, 'success')
|
showToast(user.isActive ? t('user.management.userDisabledSuccess') : t('user.management.userEnabledSuccess'), 'success')
|
||||||
}
|
}
|
||||||
} else if (action === 'disableKeys') {
|
} else if (action === 'disableKeys') {
|
||||||
const response = await apiClient.post(`/users/${user.id}/disable-keys`)
|
const response = await apiClient.post(`/users/${user.id}/disable-keys`)
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showToast(`Disabled ${response.disabledCount} API keys`, 'success')
|
showToast(t('user.management.keysDisabledSuccess', { count: response.disabledCount }), 'success')
|
||||||
await loadUsers() // Refresh to get updated counts
|
await loadUsers() // Refresh to get updated counts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to ${action}:`, error)
|
console.error(`Failed to ${action}:`, error)
|
||||||
showToast(`Failed to ${action}`, 'error')
|
showToast(t(`user.management.${action}Error`), 'error')
|
||||||
} finally {
|
} finally {
|
||||||
showConfirmModal.value = false
|
showConfirmModal.value = false
|
||||||
selectedUser.value = null
|
selectedUser.value = null
|
||||||
|
|||||||
Reference in New Issue
Block a user