Merge pull request #2797 from wucm667/feat/account-list-created-at-column

feat(admin): 账号管理列表新增创建时间列
This commit is contained in:
Wesley Liddick 2026-05-27 22:10:21 +08:00 committed by GitHub
commit cc077862b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 135 additions and 1 deletions

View File

@ -0,0 +1,52 @@
package admin
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func setupAccountListRouter() (*gin.Engine, *stubAdminService) {
gin.SetMode(gin.TestMode)
router := gin.New()
adminSvc := newStubAdminService()
handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router.GET("/api/v1/admin/accounts", handler.List)
return router, adminSvc
}
func TestAccountHandlerListIncludesCreatedAt(t *testing.T) {
router, adminSvc := setupAccountListRouter()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&sort_by=created_at&sort_order=desc", nil)
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, "created_at", adminSvc.lastListAccounts.sortBy)
var payload struct {
Data struct {
Items []struct {
ID int64 `json:"id"`
CreatedAt string `json:"created_at"`
} `json:"items"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
require.Len(t, payload.Data.Items, 1)
createdAt := payload.Data.Items[0].CreatedAt
require.NotEmpty(t, createdAt)
require.True(t, strings.HasSuffix(createdAt, "Z"), "created_at should be serialized as UTC")
parsed, err := time.Parse(time.RFC3339Nano, createdAt)
require.NoError(t, err)
_, offset := parsed.Zone()
require.Equal(t, 0, offset)
}

View File

@ -3078,6 +3078,7 @@ export default {
usageWindows: 'Usage Windows',
proxy: 'Proxy',
lastUsed: 'Last Used',
createdAt: 'Created',
expiresAt: 'Expires At',
actions: 'Actions'
},

View File

@ -3116,6 +3116,7 @@ export default {
usageWindows: '用量窗口',
proxy: '代理',
lastUsed: '最近使用',
createdAt: '创建时间',
expiresAt: '过期时间',
actions: '操作'
},

View File

@ -301,6 +301,9 @@
<template #cell-last_used_at="{ value }">
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatRelativeTime(value) }}</span>
</template>
<template #cell-created_at="{ value }">
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatDateTime(value) }}</span>
</template>
<template #cell-expires_at="{ row, value }">
<div class="flex flex-col items-start gap-1">
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatExpiresAt(value) }}</span>
@ -509,6 +512,7 @@ const ACCOUNT_SORTABLE_KEYS = new Set([
'priority',
'rate_multiplier',
'last_used_at',
'created_at',
'expires_at'
])
const loadInitialAccountSortState = (): AccountSortState => {
@ -1127,6 +1131,7 @@ const allColumns = computed(() => {
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
{ key: 'rate_multiplier', label: t('admin.accounts.columns.billingRateMultiplier'), sortable: true },
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
{ key: 'created_at', label: t('admin.accounts.columns.createdAt'), sortable: true },
{ key: 'expires_at', label: t('admin.accounts.columns.expiresAt'), sortable: true },
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }

View File

@ -63,7 +63,14 @@ vi.mock('vue-i18n', async () => {
const DataTableStub = {
props: ['columns', 'data'],
template: '<div data-test="data-table"></div>'
template: `
<div data-test="data-table">
<span v-for="column in columns" :key="column.key" data-test="column-key">{{ column.key }}</span>
<div v-for="row in data" :key="row.id">
<slot name="cell-created_at" :value="row.created_at" :row="row" />
</div>
</div>
`
}
const AccountBulkActionsBarStub = {
@ -149,4 +156,72 @@ describe('admin AccountsView bulk edit scope', () => {
expect(wrapper.get('[data-test="bulk-edit-modal"]').attributes('data-show')).toBe('true')
expect(wrapper.get('[data-test="bulk-edit-modal"]').attributes('data-target-mode')).toBe('filtered')
})
it('renders the created_at column by default', async () => {
listAccounts.mockResolvedValue({
items: [
{
id: 1,
name: 'test-account',
platform: 'anthropic',
type: 'oauth',
status: 'active',
schedulable: true,
created_at: '2026-03-07T10:00:00Z',
updated_at: '2026-03-07T10:00:00Z'
}
],
total: 1,
page: 1,
page_size: 20,
pages: 1
})
const wrapper = mount(AccountsView, {
global: {
stubs: {
AppLayout: { template: '<div><slot /></div>' },
TablePageLayout: {
template: '<div><slot name="filters" /><slot name="table" /><slot name="pagination" /></div>'
},
DataTable: DataTableStub,
Pagination: true,
ConfirmDialog: true,
AccountTableActions: { template: '<div><slot name="beforeCreate" /><slot name="after" /></div>' },
AccountTableFilters: { template: '<div></div>' },
AccountBulkActionsBar: AccountBulkActionsBarStub,
AccountActionMenu: true,
ImportDataModal: true,
ReAuthAccountModal: true,
AccountTestModal: true,
AccountStatsModal: true,
ScheduledTestsPanel: true,
SyncFromCrsModal: true,
TempUnschedStatusModal: true,
ErrorPassthroughRulesModal: true,
TLSFingerprintProfilesModal: true,
CreateAccountModal: true,
EditAccountModal: true,
BulkEditAccountModal: BulkEditAccountModalStub,
PlatformTypeBadge: true,
AccountCapacityCell: true,
AccountStatusIndicator: true,
AccountTodayStatsCell: true,
AccountGroupsCell: true,
AccountUsageCell: true,
Icon: true
}
}
})
await flushPromises()
const columnKeys = wrapper.findAll('[data-test="column-key"]').map(node => node.text())
expect(columnKeys).toContain('created_at')
const columns = wrapper.getComponent(DataTableStub).props('columns') as Array<{ key: string; label: string; sortable: boolean }>
expect(columns.find(column => column.key === 'created_at')).toMatchObject({
label: 'admin.accounts.columns.createdAt',
sortable: true
})
})
})