Unverified Commit a413fa3b authored by 程序猿MT's avatar 程序猿MT Committed by GitHub
Browse files

Merge branch 'Wei-Shaw:main' into main

parents 3a8dbf5a 254f1254
<template> <template>
<AppLayout> <AppLayout>
<div class="space-y-6"> <TablePageLayout>
<!-- Page Header Actions --> <template #actions>
<div class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<button <button
@click="loadApiKeys" @click="loadApiKeys"
...@@ -36,9 +36,9 @@ ...@@ -36,9 +36,9 @@
{{ t('keys.createKey') }} {{ t('keys.createKey') }}
</button> </button>
</div> </div>
</template>
<!-- API Keys Table --> <template #table>
<div class="card overflow-hidden">
<DataTable :columns="columns" :data="apiKeys" :loading="loading"> <DataTable :columns="columns" :data="apiKeys" :loading="loading">
<template #cell-key="{ value, row }"> <template #cell-key="{ value, row }">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
</template> </template>
<template #cell-created_at="{ value }"> <template #cell-created_at="{ value }">
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatDate(value) }}</span> <span class="text-sm text-gray-500 dark:text-dark-400">{{ formatDateTime(value) }}</span>
</template> </template>
<template #cell-actions="{ row }"> <template #cell-actions="{ row }">
...@@ -235,7 +235,7 @@ ...@@ -235,7 +235,7 @@
<button <button
@click="editKey(row)" @click="editKey(row)"
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400" class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400"
title="Edit" :title="t('common.edit')"
> >
<svg <svg
class="h-4 w-4" class="h-4 w-4"
...@@ -255,7 +255,7 @@ ...@@ -255,7 +255,7 @@
<button <button
@click="confirmDelete(row)" @click="confirmDelete(row)"
class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400" class="rounded-lg p-2 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400"
title="Delete" :title="t('common.delete')"
> >
<svg <svg
class="h-4 w-4" class="h-4 w-4"
...@@ -283,9 +283,9 @@ ...@@ -283,9 +283,9 @@
/> />
</template> </template>
</DataTable> </DataTable>
</div> </template>
<!-- Pagination --> <template #pagination>
<Pagination <Pagination
v-if="pagination.total > 0" v-if="pagination.total > 0"
:page="pagination.page" :page="pagination.page"
...@@ -293,7 +293,8 @@ ...@@ -293,7 +293,8 @@
:page-size="pagination.page_size" :page-size="pagination.page_size"
@update:page="handlePageChange" @update:page="handlePageChange"
/> />
</div> </template>
</TablePageLayout>
<!-- Create/Edit Modal --> <!-- Create/Edit Modal -->
<Modal <Modal
...@@ -496,6 +497,7 @@ import { useAppStore } from '@/stores/app' ...@@ -496,6 +497,7 @@ import { useAppStore } from '@/stores/app'
const { t } = useI18n() const { t } = useI18n()
import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api' import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import DataTable from '@/components/common/DataTable.vue' import DataTable from '@/components/common/DataTable.vue'
import Pagination from '@/components/common/Pagination.vue' import Pagination from '@/components/common/Pagination.vue'
import Modal from '@/components/common/Modal.vue' import Modal from '@/components/common/Modal.vue'
...@@ -507,6 +509,7 @@ import GroupBadge from '@/components/common/GroupBadge.vue' ...@@ -507,6 +509,7 @@ import GroupBadge from '@/components/common/GroupBadge.vue'
import type { ApiKey, Group, PublicSettings, SubscriptionType, GroupPlatform } from '@/types' import type { ApiKey, Group, PublicSettings, SubscriptionType, GroupPlatform } from '@/types'
import type { Column } from '@/components/common/types' import type { Column } from '@/components/common/types'
import type { BatchApiKeyUsageStats } from '@/api/usage' import type { BatchApiKeyUsageStats } from '@/api/usage'
import { formatDateTime } from '@/utils/format'
interface GroupOption { interface GroupOption {
value: number value: number
...@@ -624,15 +627,6 @@ const copyToClipboard = async (text: string, keyId: number) => { ...@@ -624,15 +627,6 @@ const copyToClipboard = async (text: string, keyId: number) => {
} }
} }
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
const loadApiKeys = async () => { const loadApiKeys = async () => {
loading.value = true loading.value = true
try { try {
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
/> />
<StatCard <StatCard
:title="t('profile.memberSince')" :title="t('profile.memberSince')"
:value="formatMemberSince(user?.created_at || '')" :value="formatDate(user?.created_at || '', 'YYYY-MM')"
:icon="CalendarIcon" :icon="CalendarIcon"
icon-variant="primary" icon-variant="primary"
/> />
...@@ -267,6 +267,7 @@ import { ref, computed, h, onMounted } from 'vue' ...@@ -267,6 +267,7 @@ import { ref, computed, h, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { formatDate } from '@/utils/format'
const { t } = useI18n() const { t } = useI18n()
import { userAPI, authAPI } from '@/api' import { userAPI, authAPI } from '@/api'
...@@ -358,15 +359,6 @@ const formatCurrency = (value: number): string => { ...@@ -358,15 +359,6 @@ const formatCurrency = (value: number): string => {
return `$${value.toFixed(2)}` return `$${value.toFixed(2)}`
} }
const formatMemberSince = (dateString: string): string => {
if (!dateString) return 'N/A'
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short'
})
}
const handleChangePassword = async () => { const handleChangePassword = async () => {
// Validate password match // Validate password match
if (passwordForm.value.new_password !== passwordForm.value.confirm_password) { if (passwordForm.value.new_password !== passwordForm.value.confirm_password) {
......
...@@ -377,7 +377,7 @@ ...@@ -377,7 +377,7 @@
{{ getHistoryItemTitle(item) }} {{ getHistoryItemTitle(item) }}
</p> </p>
<p class="text-xs text-gray-500 dark:text-dark-400"> <p class="text-xs text-gray-500 dark:text-dark-400">
{{ formatDate(item.used_at) }} {{ formatDateTime(item.used_at) }}
</p> </p>
</div> </div>
</div> </div>
...@@ -447,6 +447,7 @@ import { useAuthStore } from '@/stores/auth' ...@@ -447,6 +447,7 @@ import { useAuthStore } from '@/stores/auth'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { redeemAPI, authAPI, type RedeemHistoryItem } from '@/api' import { redeemAPI, authAPI, type RedeemHistoryItem } from '@/api'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import { formatDateTime } from '@/utils/format'
const { t } = useI18n() const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
...@@ -472,18 +473,6 @@ const history = ref<RedeemHistoryItem[]>([]) ...@@ -472,18 +473,6 @@ const history = ref<RedeemHistoryItem[]>([])
const loadingHistory = ref(false) const loadingHistory = ref(false)
const contactInfo = ref('') const contactInfo = ref('')
const formatDate = (dateString: string) => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
// Helper functions for history display // Helper functions for history display
const isBalanceType = (type: string) => { const isBalanceType = (type: string) => {
return type === 'balance' || type === 'admin_balance' return type === 'balance' || type === 'admin_balance'
......
...@@ -257,6 +257,7 @@ import { useAppStore } from '@/stores/app' ...@@ -257,6 +257,7 @@ import { useAppStore } from '@/stores/app'
import subscriptionsAPI from '@/api/subscriptions' import subscriptionsAPI from '@/api/subscriptions'
import type { UserSubscription } from '@/types' import type { UserSubscription } from '@/types'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import { formatDateOnly } from '@/utils/format'
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStore() const appStore = useAppStore()
...@@ -300,11 +301,7 @@ function formatExpirationDate(expiresAt: string): string { ...@@ -300,11 +301,7 @@ function formatExpirationDate(expiresAt: string): string {
return t('userSubscriptions.status.expired') return t('userSubscriptions.status.expired')
} }
const dateStr = expires.toLocaleDateString(undefined, { const dateStr = formatDateOnly(expires)
year: 'numeric',
month: 'short',
day: 'numeric'
})
if (days === 0) { if (days === 0) {
return `${dateStr} (Today)` return `${dateStr} (Today)`
......
<template> <template>
<AppLayout> <AppLayout>
<div class="space-y-6"> <TablePageLayout>
<!-- Summary Stats Cards --> <template #actions>
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4"> <div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<!-- Total Requests --> <!-- Total Requests -->
<div class="card p-4"> <div class="card p-4">
...@@ -132,8 +132,9 @@ ...@@ -132,8 +132,9 @@
</div> </div>
</div> </div>
</div> </div>
</template>
<!-- Filters --> <template #filters>
<div class="card"> <div class="card">
<div class="px-6 py-4"> <div class="px-6 py-4">
<div class="flex flex-wrap items-end gap-4"> <div class="flex flex-wrap items-end gap-4">
...@@ -170,10 +171,16 @@ ...@@ -170,10 +171,16 @@
</div> </div>
</div> </div>
</div> </div>
</template>
<!-- Usage Table --> <template #table>
<div class="card overflow-hidden">
<DataTable :columns="columns" :data="usageLogs" :loading="loading"> <DataTable :columns="columns" :data="usageLogs" :loading="loading">
<template #cell-api_key="{ row }">
<span class="text-sm text-gray-900 dark:text-white">{{
row.api_key?.name || '-'
}}</span>
</template>
<template #cell-model="{ value }"> <template #cell-model="{ value }">
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span> <span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
</template> </template>
...@@ -379,9 +386,9 @@ ...@@ -379,9 +386,9 @@
<EmptyState :message="t('usage.noRecords')" /> <EmptyState :message="t('usage.noRecords')" />
</template> </template>
</DataTable> </DataTable>
</div> </template>
<!-- Pagination --> <template #pagination>
<Pagination <Pagination
v-if="pagination.total > 0" v-if="pagination.total > 0"
:page="pagination.page" :page="pagination.page"
...@@ -389,7 +396,8 @@ ...@@ -389,7 +396,8 @@
:page-size="pagination.page_size" :page-size="pagination.page_size"
@update:page="handlePageChange" @update:page="handlePageChange"
/> />
</div> </template>
</TablePageLayout>
</AppLayout> </AppLayout>
</template> </template>
...@@ -399,6 +407,7 @@ import { useI18n } from 'vue-i18n' ...@@ -399,6 +407,7 @@ import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { usageAPI, keysAPI } from '@/api' import { usageAPI, keysAPI } from '@/api'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import DataTable from '@/components/common/DataTable.vue' import DataTable from '@/components/common/DataTable.vue'
import Pagination from '@/components/common/Pagination.vue' import Pagination from '@/components/common/Pagination.vue'
import EmptyState from '@/components/common/EmptyState.vue' import EmptyState from '@/components/common/EmptyState.vue'
...@@ -406,6 +415,7 @@ import Select from '@/components/common/Select.vue' ...@@ -406,6 +415,7 @@ import Select from '@/components/common/Select.vue'
import DateRangePicker from '@/components/common/DateRangePicker.vue' import DateRangePicker from '@/components/common/DateRangePicker.vue'
import type { UsageLog, ApiKey, UsageQueryParams, UsageStatsResponse } from '@/types' import type { UsageLog, ApiKey, UsageQueryParams, UsageStatsResponse } from '@/types'
import type { Column } from '@/components/common/types' import type { Column } from '@/components/common/types'
import { formatDateTime } from '@/utils/format'
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStore() const appStore = useAppStore()
...@@ -414,6 +424,7 @@ const appStore = useAppStore() ...@@ -414,6 +424,7 @@ const appStore = useAppStore()
const usageStats = ref<UsageStatsResponse | null>(null) const usageStats = ref<UsageStatsResponse | null>(null)
const columns = computed<Column[]>(() => [ const columns = computed<Column[]>(() => [
{ key: 'api_key', label: t('usage.apiKeyFilter'), sortable: false },
{ key: 'model', label: t('usage.model'), sortable: true }, { key: 'model', label: t('usage.model'), sortable: true },
{ key: 'stream', label: t('usage.type'), sortable: false }, { key: 'stream', label: t('usage.type'), sortable: false },
{ key: 'tokens', label: t('usage.tokens'), sortable: false }, { key: 'tokens', label: t('usage.tokens'), sortable: false },
...@@ -505,17 +516,6 @@ const formatCacheTokens = (value: number): string => { ...@@ -505,17 +516,6 @@ const formatCacheTokens = (value: number): string => {
return value.toLocaleString() return value.toLocaleString()
} }
const formatDateTime = (dateString: string): string => {
const date = new Date(dateString)
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const loadUsageLogs = async () => { const loadUsageLogs = async () => {
loading.value = true loading.value = true
try { try {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment