Merge remote-tracking branch 'upstream/main' into feat/payment-system-v2

# Conflicts:
#	frontend/src/api/admin/settings.ts
#	frontend/src/stores/app.ts
#	frontend/src/types/index.ts
#	frontend/src/views/admin/SettingsView.vue
This commit is contained in:
erio
2026-04-11 18:24:49 +08:00
115 changed files with 3422 additions and 396 deletions

View File

@@ -49,7 +49,15 @@
</template>
<template #table>
<DataTable :columns="columns" :data="apiKeys" :loading="loading">
<DataTable
:columns="columns"
:data="apiKeys"
:loading="loading"
:server-side-sort="true"
default-sort-key="created_at"
default-sort-order="desc"
@sort="handleSort"
>
<template #cell-key="{ value, row }">
<div class="flex items-center gap-2">
<code class="code text-xs">
@@ -1114,6 +1122,10 @@ const pagination = ref({
total: 0,
pages: 0
})
const sortState = ref({
sort_by: 'created_at',
sort_order: 'desc' as 'asc' | 'desc'
})
// Filter state
const filterSearch = ref('')
@@ -1277,10 +1289,18 @@ const loadApiKeys = async () => {
loading.value = true
try {
// Build filters
const filters: { search?: string; status?: string; group_id?: number | string } = {}
const filters: {
search?: string
status?: string
group_id?: number | string
sort_by?: string
sort_order?: 'asc' | 'desc'
} = {}
if (filterSearch.value) filters.search = filterSearch.value
if (filterStatus.value) filters.status = filterStatus.value
if (filterGroupId.value !== '') filters.group_id = filterGroupId.value
filters.sort_by = sortState.value.sort_by
filters.sort_order = sortState.value.sort_order
const response = await keysAPI.list(pagination.value.page, pagination.value.page_size, filters, {
signal
@@ -1360,6 +1380,13 @@ const handlePageSizeChange = (pageSize: number) => {
loadApiKeys()
}
const handleSort = (key: string, order: 'asc' | 'desc') => {
sortState.value.sort_by = key
sortState.value.sort_order = order
pagination.value.page = 1
loadApiKeys()
}
const editKey = (key: ApiKey) => {
selectedKey.value = key
const hasIPRestriction = (key.ip_whitelist?.length > 0) || (key.ip_blacklist?.length > 0)

View File

@@ -149,7 +149,15 @@
</template>
<template #table>
<DataTable :columns="columns" :data="usageLogs" :loading="loading">
<DataTable
:columns="columns"
:data="usageLogs"
:loading="loading"
:server-side-sort="true"
default-sort-key="created_at"
default-sort-order="desc"
@sort="handleSort"
>
<template #cell-api_key="{ row }">
<span class="text-sm text-gray-900 dark:text-white">{{
row.api_key?.name || '-'
@@ -598,6 +606,10 @@ const pagination = reactive({
total: 0,
pages: 0
})
const sortState = reactive({
sort_by: 'created_at',
sort_order: 'desc' as 'asc' | 'desc'
})
const formatDuration = (ms: number): string => {
if (ms < 1000) return `${ms.toFixed(0)}ms`
@@ -660,6 +672,18 @@ const formatTokens = (value: number): string => {
return value.toLocaleString()
}
type UsageTableQueryParams = UsageQueryParams & {
sort_by?: string
sort_order?: 'asc' | 'desc'
}
const buildUsageQueryParams = (page: number, pageSize: number): UsageTableQueryParams => ({
page,
page_size: pageSize,
...filters.value,
sort_by: sortState.sort_by,
sort_order: sortState.sort_order
})
const loadUsageLogs = async () => {
if (abortController) {
@@ -670,13 +694,10 @@ const loadUsageLogs = async () => {
const { signal } = currentAbortController
loading.value = true
try {
const params: UsageQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
...filters.value
}
const response = await usageAPI.query(params, { signal })
const response = await usageAPI.query(
buildUsageQueryParams(pagination.page, pagination.page_size),
{ signal }
)
if (signal.aborted) {
return
}
@@ -758,6 +779,13 @@ const handlePageSizeChange = (pageSize: number) => {
loadUsageLogs()
}
const handleSort = (key: string, order: 'asc' | 'desc') => {
sortState.sort_by = key
sortState.sort_order = order
pagination.page = 1
loadUsageLogs()
}
/**
* Escape CSV value to prevent injection and handle special characters
*/
@@ -795,12 +823,7 @@ const exportToCSV = async () => {
const totalRequests = Math.ceil(pagination.total / pageSize)
for (let page = 1; page <= totalRequests; page++) {
const params: UsageQueryParams = {
page: page,
page_size: pageSize,
...filters.value
}
const response = await usageAPI.query(params)
const response = await usageAPI.query(buildUsageQueryParams(page, pageSize))
allLogs.push(...response.items)
}

View File

@@ -256,6 +256,17 @@ describe('user UsageView tooltip', () => {
await setupState.exportToCSV()
expect(exportedBlob).not.toBeNull()
const hasSortedExportQuery = query.mock.calls.some((call) => {
const params = call[0] as Record<string, unknown> | undefined
const config = call[1]
return (
params?.page_size === 100 &&
params?.sort_by === 'created_at' &&
params?.sort_order === 'desc' &&
config === undefined
)
})
expect(hasSortedExportQuery).toBe(true)
expect(clickSpy).toHaveBeenCalled()
expect(showSuccess).toHaveBeenCalled()