mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-04-28 15:18:37 +00:00
Revert "Merge pull request #424 from Wangnov/feat/i18n"
This reverts commit1d915d8327, reversing changes made to009f7c84f6.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<div class="mt-3">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">{{ t('user.createApiKeyModal.title') }}</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900">Create New API Key</h3>
|
||||
<button class="text-gray-400 hover:text-gray-600" @click="$emit('close')">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -23,16 +23,13 @@
|
||||
|
||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700" for="name">
|
||||
{{ t('user.createApiKeyModal.form.nameLabel') }}
|
||||
{{ t('user.createApiKeyModal.form.nameRequired') }}
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-gray-700" for="name"> Name * </label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
:disabled="loading"
|
||||
:placeholder="t('user.createApiKeyModal.form.namePlaceholder')"
|
||||
placeholder="Enter API key name"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
@@ -40,14 +37,14 @@
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700" for="description">
|
||||
{{ t('user.createApiKeyModal.form.descriptionLabel') }}
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
v-model="form.description"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
:disabled="loading"
|
||||
:placeholder="t('user.createApiKeyModal.form.descriptionPlaceholder')"
|
||||
placeholder="Optional description"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
@@ -76,7 +73,7 @@
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
{{ t('user.createApiKeyModal.buttons.cancel') }}
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
@@ -104,9 +101,9 @@
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
{{ t('user.createApiKeyModal.buttons.creating') }}
|
||||
Creating...
|
||||
</span>
|
||||
<span v-else>{{ t('user.createApiKeyModal.buttons.createApiKey') }}</span>
|
||||
<span v-else>Create API Key</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -124,13 +121,11 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h4 class="text-sm font-medium text-green-800">
|
||||
{{ t('user.createApiKeyModal.success.title') }}
|
||||
</h4>
|
||||
<h4 class="text-sm font-medium text-green-800">API Key Created Successfully!</h4>
|
||||
<div class="mt-3">
|
||||
<p class="mb-2 text-sm text-green-700">
|
||||
<strong>{{ t('user.createApiKeyModal.success.warning.important') }}</strong>
|
||||
{{ t('user.createApiKeyModal.success.warning.message') }}
|
||||
<strong>Important:</strong> Copy your API key now. You won't be able to see it
|
||||
again!
|
||||
</p>
|
||||
<div class="rounded-md border border-green-300 bg-white p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -154,7 +149,7 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('user.createApiKeyModal.buttons.copy') }}
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +159,7 @@
|
||||
class="rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
||||
@click="handleClose"
|
||||
>
|
||||
{{ t('user.createApiKeyModal.buttons.done') }}
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,7 +172,6 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
@@ -190,7 +184,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'created'])
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -211,7 +204,7 @@ const resetForm = () => {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name.trim()) {
|
||||
error.value = t('user.createApiKeyModal.validation.nameRequired')
|
||||
error.value = 'API key name is required'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,14 +221,13 @@ const handleSubmit = async () => {
|
||||
|
||||
if (result.success) {
|
||||
newApiKey.value = result.apiKey
|
||||
showToast(t('user.createApiKeyModal.messages.createSuccess'), 'success')
|
||||
showToast('API key created successfully!', 'success')
|
||||
} else {
|
||||
error.value = result.message || t('user.createApiKeyModal.errors.createFailed')
|
||||
error.value = result.message || 'Failed to create API key'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Create API key error:', err)
|
||||
error.value =
|
||||
err.response?.data?.message || err.message || t('user.createApiKeyModal.errors.createFailed')
|
||||
error.value = err.response?.data?.message || err.message || 'Failed to create API key'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -244,10 +236,10 @@ const handleSubmit = async () => {
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showToast(t('user.createApiKeyModal.messages.copySuccess'), 'success')
|
||||
showToast('API key copied to clipboard!', 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
showToast(t('user.createApiKeyModal.messages.copyFailed'), 'error')
|
||||
showToast('Failed to copy to clipboard', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
<div class="space-y-6">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">
|
||||
{{ t('user.userApiKeysManager.title') }}
|
||||
</h1>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">My API Keys</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">
|
||||
{{ t('user.userApiKeysManager.description') }}
|
||||
Manage your API keys to access Claude Relay services
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
@@ -23,7 +21,7 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('user.userApiKeysManager.buttons.createApiKey') }}
|
||||
Create API Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +43,8 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-700">
|
||||
{{ t('user.userApiKeysManager.warnings.maxKeysReached', { maxApiKeys }) }}
|
||||
You have reached the maximum number of API keys ({{ maxApiKeys }}). Please delete an
|
||||
existing key to create a new one.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +72,7 @@
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">{{ t('user.userApiKeysManager.loading') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-500">Loading API keys...</p>
|
||||
</div>
|
||||
|
||||
<!-- API Keys List -->
|
||||
@@ -101,37 +100,29 @@
|
||||
v-if="apiKey.isDeleted === 'true' || apiKey.deletedAt"
|
||||
class="ml-2 inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800"
|
||||
>
|
||||
{{ t('user.userApiKeysManager.status.deleted') }}
|
||||
Deleted
|
||||
</span>
|
||||
<span
|
||||
v-else-if="!apiKey.isActive"
|
||||
class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800"
|
||||
>
|
||||
{{ t('user.userApiKeysManager.status.deleted') }}
|
||||
Deleted
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ apiKey.description || t('user.userApiKeysManager.status.noDescription') }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">{{ apiKey.description || 'No description' }}</p>
|
||||
<div class="mt-1 flex items-center space-x-4 text-xs text-gray-400">
|
||||
<span
|
||||
>{{ t('user.userApiKeysManager.dateLabels.created') }}:
|
||||
{{ formatDate(apiKey.createdAt) }}</span
|
||||
>
|
||||
<span>Created: {{ formatDate(apiKey.createdAt) }}</span>
|
||||
<span v-if="apiKey.isDeleted === 'true' || apiKey.deletedAt"
|
||||
>{{ t('user.userApiKeysManager.dateLabels.deleted') }}:
|
||||
{{ formatDate(apiKey.deletedAt) }}</span
|
||||
>Deleted: {{ formatDate(apiKey.deletedAt) }}</span
|
||||
>
|
||||
<span v-else-if="apiKey.lastUsedAt"
|
||||
>{{ t('user.userApiKeysManager.dateLabels.lastUsed') }}:
|
||||
{{ formatDate(apiKey.lastUsedAt) }}</span
|
||||
>Last used: {{ formatDate(apiKey.lastUsedAt) }}</span
|
||||
>
|
||||
<span v-else>{{ t('user.userApiKeysManager.status.neverUsed') }}</span>
|
||||
<span v-else>Never used</span>
|
||||
<span
|
||||
v-if="apiKey.expiresAt && !(apiKey.isDeleted === 'true' || apiKey.deletedAt)"
|
||||
>{{ t('user.userApiKeysManager.dateLabels.expires') }}:
|
||||
{{ formatDate(apiKey.expiresAt) }}</span
|
||||
>Expires: {{ formatDate(apiKey.expiresAt) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,10 +131,7 @@
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Usage Stats -->
|
||||
<div class="text-right text-xs text-gray-500">
|
||||
<div>
|
||||
{{ formatNumber(apiKey.usage?.requests || 0) }}
|
||||
{{ t('user.userApiKeysManager.usage.requests') }}
|
||||
</div>
|
||||
<div>{{ formatNumber(apiKey.usage?.requests || 0) }} requests</div>
|
||||
<div v-if="apiKey.usage?.totalCost">${{ apiKey.usage.totalCost.toFixed(4) }}</div>
|
||||
</div>
|
||||
|
||||
@@ -151,7 +139,7 @@
|
||||
<div class="flex items-center space-x-1">
|
||||
<button
|
||||
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-gray-600"
|
||||
:title="t('user.userApiKeysManager.actions.viewApiKey')"
|
||||
title="View API Key"
|
||||
@click="showApiKey(apiKey)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -177,7 +165,7 @@
|
||||
allowUserDeleteApiKeys
|
||||
"
|
||||
class="inline-flex items-center rounded border border-transparent p-1 text-red-400 hover:text-red-600"
|
||||
:title="t('user.userApiKeysManager.actions.deleteApiKey')"
|
||||
title="Delete API Key"
|
||||
@click="deleteApiKey(apiKey)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -211,12 +199,8 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">
|
||||
{{ t('user.userApiKeysManager.emptyState.title') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ t('user.userApiKeysManager.emptyState.description') }}
|
||||
</p>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No API keys</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Get started by creating your first API key.</p>
|
||||
<div class="mt-6">
|
||||
<button
|
||||
class="inline-flex items-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
@@ -230,7 +214,7 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('user.userApiKeysManager.buttons.createApiKey') }}
|
||||
Create API Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,10 +236,10 @@
|
||||
<!-- Confirm Delete Modal -->
|
||||
<ConfirmModal
|
||||
confirm-class="bg-red-600 hover:bg-red-700"
|
||||
:confirm-text="t('user.userApiKeysManager.buttons.delete')"
|
||||
:message="t('user.userApiKeysManager.confirmDelete.message', { name: selectedApiKey?.name })"
|
||||
confirm-text="Delete"
|
||||
:message="`Are you sure you want to delete '${selectedApiKey?.name}'? This action cannot be undone.`"
|
||||
:show="showDeleteModal"
|
||||
:title="t('user.userApiKeysManager.confirmDelete.title')"
|
||||
title="Delete API Key"
|
||||
@cancel="showDeleteModal = false"
|
||||
@confirm="handleDeleteConfirm"
|
||||
/>
|
||||
@@ -264,15 +248,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import CreateApiKeyModal from './CreateApiKeyModal.vue'
|
||||
import ViewApiKeyModal from './ViewApiKeyModal.vue'
|
||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loading = ref(true)
|
||||
@@ -310,12 +291,7 @@ const formatNumber = (num) => {
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return null
|
||||
const localeMap = {
|
||||
'zh-cn': 'zh-CN',
|
||||
'zh-tw': 'zh-TW',
|
||||
en: 'en-US'
|
||||
}
|
||||
return new Date(dateString).toLocaleDateString(localeMap[locale.value] || 'en-US', {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@@ -330,7 +306,7 @@ const loadApiKeys = async () => {
|
||||
apiKeys.value = await userStore.getUserApiKeys(true) // Include deleted keys
|
||||
} catch (error) {
|
||||
console.error('Failed to load API keys:', error)
|
||||
showToast(t('user.userApiKeysManager.messages.loadFailed'), 'error')
|
||||
showToast('Failed to load API keys', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -351,12 +327,12 @@ const handleDeleteConfirm = async () => {
|
||||
const result = await userStore.deleteApiKey(selectedApiKey.value.id)
|
||||
|
||||
if (result.success) {
|
||||
showToast(t('user.userApiKeysManager.messages.deleteSuccess'), 'success')
|
||||
showToast('API key deleted successfully', 'success')
|
||||
await loadApiKeys()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete API key:', error)
|
||||
showToast(t('user.userApiKeysManager.messages.deleteFailed'), 'error')
|
||||
showToast('Failed to delete API key', 'error')
|
||||
} finally {
|
||||
showDeleteModal.value = false
|
||||
selectedApiKey.value = null
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="space-y-6">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">{{ t('user.userUsageStats.title') }}</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">{{ t('user.userUsageStats.subtitle') }}</p>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Usage Statistics</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">View your API usage statistics and costs</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
<select
|
||||
@@ -11,10 +11,10 @@
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
@change="loadUsageStats"
|
||||
>
|
||||
<option value="day">{{ t('user.userUsageStats.periodSelection.day') }}</option>
|
||||
<option value="week">{{ t('user.userUsageStats.periodSelection.week') }}</option>
|
||||
<option value="month">{{ t('user.userUsageStats.periodSelection.month') }}</option>
|
||||
<option value="quarter">{{ t('user.userUsageStats.periodSelection.quarter') }}</option>
|
||||
<option value="day">Last 24 Hours</option>
|
||||
<option value="week">Last 7 Days</option>
|
||||
<option value="month">Last 30 Days</option>
|
||||
<option value="quarter">Last 90 Days</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">{{ t('user.userUsageStats.loadingStats') }}</p>
|
||||
<p class="mt-2 text-sm text-gray-500">Loading usage statistics...</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
@@ -66,9 +66,7 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">
|
||||
{{ t('user.userUsageStats.statsCards.totalRequests') }}
|
||||
</dt>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">Total Requests</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
{{ formatNumber(usageStats?.totalRequests || 0) }}
|
||||
</dd>
|
||||
@@ -98,9 +96,7 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">
|
||||
{{ t('user.userUsageStats.statsCards.inputTokens') }}
|
||||
</dt>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">Input Tokens</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
{{ formatNumber(usageStats?.totalInputTokens || 0) }}
|
||||
</dd>
|
||||
@@ -130,9 +126,7 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">
|
||||
{{ t('user.userUsageStats.statsCards.outputTokens') }}
|
||||
</dt>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">Output Tokens</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
{{ formatNumber(usageStats?.totalOutputTokens || 0) }}
|
||||
</dd>
|
||||
@@ -162,9 +156,7 @@
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">
|
||||
{{ t('user.userUsageStats.statsCards.totalCost') }}
|
||||
</dt>
|
||||
<dt class="truncate text-sm font-medium text-gray-500">Total Cost</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
${{ (usageStats?.totalCost || 0).toFixed(4) }}
|
||||
</dd>
|
||||
@@ -178,9 +170,7 @@
|
||||
<!-- Daily Usage Chart -->
|
||||
<div v-if="!loading && usageStats" class="rounded-lg bg-white shadow">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">
|
||||
{{ t('user.userUsageStats.usageTrend.title') }}
|
||||
</h3>
|
||||
<h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">Daily Usage Trend</h3>
|
||||
|
||||
<!-- Placeholder for chart - you can integrate Chart.js or similar -->
|
||||
<div
|
||||
@@ -200,14 +190,10 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">
|
||||
{{ t('user.userUsageStats.usageTrend.chartTitle') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ t('user.userUsageStats.usageTrend.dailyTrendsDescription') }}
|
||||
</p>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">Usage Chart</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Daily usage trends would be displayed here</p>
|
||||
<p class="mt-2 text-xs text-gray-400">
|
||||
{{ t('user.userUsageStats.usageTrend.chartIntegrationNote') }}
|
||||
(Chart integration can be added with Chart.js, D3.js, or similar library)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,9 +206,7 @@
|
||||
class="rounded-lg bg-white shadow"
|
||||
>
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">
|
||||
{{ t('user.userUsageStats.modelUsage.title') }}
|
||||
</h3>
|
||||
<h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">Usage by Model</h3>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="model in usageStats.modelStats"
|
||||
@@ -238,13 +222,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-900">
|
||||
{{
|
||||
t('user.userUsageStats.modelUsage.requestsCount', {
|
||||
count: formatNumber(model.requests)
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p class="text-sm text-gray-900">{{ formatNumber(model.requests) }} requests</p>
|
||||
<p class="text-xs text-gray-500">${{ model.cost.toFixed(4) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,9 +233,7 @@
|
||||
<!-- Detailed Usage Table -->
|
||||
<div v-if="!loading && userApiKeys.length > 0" class="rounded-lg bg-white shadow">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">
|
||||
{{ t('user.userUsageStats.apiKeyUsage.title') }}
|
||||
</h3>
|
||||
<h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">Usage by API Key</h3>
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
@@ -266,37 +242,37 @@
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
scope="col"
|
||||
>
|
||||
{{ t('user.userUsageStats.apiKeyUsage.headers.apiKey') }}
|
||||
API Key
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
scope="col"
|
||||
>
|
||||
{{ t('user.userUsageStats.apiKeyUsage.headers.requests') }}
|
||||
Requests
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
scope="col"
|
||||
>
|
||||
{{ t('user.userUsageStats.apiKeyUsage.headers.inputTokens') }}
|
||||
Input Tokens
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
scope="col"
|
||||
>
|
||||
{{ t('user.userUsageStats.apiKeyUsage.headers.outputTokens') }}
|
||||
Output Tokens
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
scope="col"
|
||||
>
|
||||
{{ t('user.userUsageStats.apiKeyUsage.headers.cost') }}
|
||||
Cost
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
scope="col"
|
||||
>
|
||||
{{ t('user.userUsageStats.apiKeyUsage.headers.status') }}
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -331,10 +307,10 @@
|
||||
>
|
||||
{{
|
||||
apiKey.isDeleted === 'true' || apiKey.deletedAt
|
||||
? t('user.userUsageStats.apiKeyUsage.status.deleted')
|
||||
? 'Deleted'
|
||||
: apiKey.isActive
|
||||
? t('user.userUsageStats.apiKeyUsage.status.active')
|
||||
: t('user.userUsageStats.apiKeyUsage.status.disabled')
|
||||
? 'Active'
|
||||
: 'Disabled'
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
@@ -363,11 +339,10 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">
|
||||
{{ t('user.userUsageStats.noData.title') }}
|
||||
</h3>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No usage data</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{{ t('user.userUsageStats.noData.description') }}
|
||||
You haven't made any API requests yet. Create an API key and start using the service to see
|
||||
usage statistics.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,12 +350,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loading = ref(true)
|
||||
@@ -409,7 +381,7 @@ const loadUsageStats = async () => {
|
||||
userApiKeys.value = apiKeys
|
||||
} catch (error) {
|
||||
console.error('Failed to load usage stats:', error)
|
||||
showToast(t('user.userUsageStats.loadFailed'), 'error')
|
||||
showToast('Failed to load usage statistics', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<div class="mt-3">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">{{ t('user.viewApiKeyModal.title') }}</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900">API Key Details</h3>
|
||||
<button class="text-gray-400 hover:text-gray-600" @click="emit('close')">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -24,35 +24,29 @@
|
||||
<div v-if="apiKey" class="space-y-4">
|
||||
<!-- API Key Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{
|
||||
t('user.viewApiKeyModal.fields.name')
|
||||
}}</label>
|
||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<p class="mt-1 text-sm text-gray-900">{{ apiKey.name }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div v-if="apiKey.description">
|
||||
<label class="block text-sm font-medium text-gray-700">{{
|
||||
t('user.viewApiKeyModal.fields.description')
|
||||
}}</label>
|
||||
<label class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<p class="mt-1 text-sm text-gray-900">{{ apiKey.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- API Key -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{
|
||||
t('user.viewApiKeyModal.fields.apiKey')
|
||||
}}</label>
|
||||
<label class="block text-sm font-medium text-gray-700">API Key</label>
|
||||
<div class="mt-1 flex items-center space-x-2">
|
||||
<div class="flex-1">
|
||||
<div v-if="showFullKey" class="rounded-md border border-gray-300 bg-gray-50 p-3">
|
||||
<code class="break-all font-mono text-sm text-gray-900">{{
|
||||
apiKey.key || t('user.viewApiKeyModal.apiKeyDisplay.notAvailable')
|
||||
apiKey.key || 'Not available'
|
||||
}}</code>
|
||||
</div>
|
||||
<div v-else class="rounded-md border border-gray-300 bg-gray-50 p-3">
|
||||
<code class="font-mono text-sm text-gray-900">{{
|
||||
apiKey.keyPreview || t('user.viewApiKeyModal.apiKeyDisplay.keyPreview')
|
||||
apiKey.keyPreview || 'cr_****'
|
||||
}}</code>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,11 +90,7 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{
|
||||
showFullKey
|
||||
? t('user.viewApiKeyModal.buttons.hide')
|
||||
: t('user.viewApiKeyModal.buttons.show')
|
||||
}}
|
||||
{{ showFullKey ? 'Hide' : 'Show' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showFullKey && apiKey.key"
|
||||
@@ -115,20 +105,18 @@
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('user.viewApiKeyModal.buttons.copy') }}
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!apiKey.key" class="mt-1 text-xs text-gray-500">
|
||||
{{ t('user.viewApiKeyModal.apiKeyDisplay.fullKeyNotice') }}
|
||||
Full API key is only shown when first created or regenerated
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{
|
||||
t('user.viewApiKeyModal.fields.status')
|
||||
}}</label>
|
||||
<label class="block text-sm font-medium text-gray-700">Status</label>
|
||||
<div class="mt-1">
|
||||
<span
|
||||
:class="[
|
||||
@@ -136,47 +124,33 @@
|
||||
apiKey.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
]"
|
||||
>
|
||||
{{
|
||||
apiKey.isActive
|
||||
? t('user.viewApiKeyModal.status.active')
|
||||
: t('user.viewApiKeyModal.status.disabled')
|
||||
}}
|
||||
{{ apiKey.isActive ? 'Active' : 'Disabled' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Stats -->
|
||||
<div v-if="apiKey.usage" class="border-t border-gray-200 pt-4">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">{{
|
||||
t('user.viewApiKeyModal.fields.usageStatistics')
|
||||
}}</label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">Usage Statistics</label>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-500"
|
||||
>{{ t('user.viewApiKeyModal.usageStats.requests') }}:</span
|
||||
>
|
||||
<span class="text-gray-500">Requests:</span>
|
||||
<span class="ml-2 font-medium">{{ formatNumber(apiKey.usage.requests || 0) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500"
|
||||
>{{ t('user.viewApiKeyModal.usageStats.inputTokens') }}:</span
|
||||
>
|
||||
<span class="text-gray-500">Input Tokens:</span>
|
||||
<span class="ml-2 font-medium">{{
|
||||
formatNumber(apiKey.usage.inputTokens || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500"
|
||||
>{{ t('user.viewApiKeyModal.usageStats.outputTokens') }}:</span
|
||||
>
|
||||
<span class="text-gray-500">Output Tokens:</span>
|
||||
<span class="ml-2 font-medium">{{
|
||||
formatNumber(apiKey.usage.outputTokens || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500"
|
||||
>{{ t('user.viewApiKeyModal.usageStats.totalCost') }}:</span
|
||||
>
|
||||
<span class="text-gray-500">Total Cost:</span>
|
||||
<span class="ml-2 font-medium"
|
||||
>${{ (apiKey.usage.totalCost || 0).toFixed(4) }}</span
|
||||
>
|
||||
@@ -187,17 +161,15 @@
|
||||
<!-- Timestamps -->
|
||||
<div class="space-y-2 border-t border-gray-200 pt-4 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">{{ t('user.viewApiKeyModal.timestamps.created') }}:</span>
|
||||
<span class="text-gray-500">Created:</span>
|
||||
<span class="text-gray-900">{{ formatDate(apiKey.createdAt) }}</span>
|
||||
</div>
|
||||
<div v-if="apiKey.lastUsedAt" class="flex justify-between">
|
||||
<span class="text-gray-500"
|
||||
>{{ t('user.viewApiKeyModal.timestamps.lastUsed') }}:</span
|
||||
>
|
||||
<span class="text-gray-500">Last Used:</span>
|
||||
<span class="text-gray-900">{{ formatDate(apiKey.lastUsedAt) }}</span>
|
||||
</div>
|
||||
<div v-if="apiKey.expiresAt" class="flex justify-between">
|
||||
<span class="text-gray-500">{{ t('user.viewApiKeyModal.timestamps.expires') }}:</span>
|
||||
<span class="text-gray-500">Expires:</span>
|
||||
<span
|
||||
:class="[
|
||||
'font-medium',
|
||||
@@ -214,7 +186,7 @@
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ t('user.viewApiKeyModal.buttons.close') }}
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,11 +197,8 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { showToast } from '@/utils/toast'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -256,26 +225,22 @@ const formatNumber = (num) => {
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return null
|
||||
const { locale } = useI18n()
|
||||
return new Date(dateString).toLocaleDateString(
|
||||
locale.value === 'zh-cn' ? 'zh-CN' : locale.value === 'zh-tw' ? 'zh-TW' : 'en-US',
|
||||
{
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}
|
||||
)
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showToast(t('user.viewApiKeyModal.messages.copySuccess'), 'success')
|
||||
showToast('Copied to clipboard!', 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
showToast(t('user.viewApiKeyModal.messages.copyFailed'), 'error')
|
||||
showToast('Failed to copy to clipboard', 'error')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user