Merge pull request #2797 from wucm667/feat/account-list-created-at-column
feat(admin): 账号管理列表新增创建时间列
This commit is contained in:
commit
cc077862b3
52
backend/internal/handler/admin/account_handler_list_test.go
Normal file
52
backend/internal/handler/admin/account_handler_list_test.go
Normal 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)
|
||||
}
|
||||
@ -3078,6 +3078,7 @@ export default {
|
||||
usageWindows: 'Usage Windows',
|
||||
proxy: 'Proxy',
|
||||
lastUsed: 'Last Used',
|
||||
createdAt: 'Created',
|
||||
expiresAt: 'Expires At',
|
||||
actions: 'Actions'
|
||||
},
|
||||
|
||||
@ -3116,6 +3116,7 @@ export default {
|
||||
usageWindows: '用量窗口',
|
||||
proxy: '代理',
|
||||
lastUsed: '最近使用',
|
||||
createdAt: '创建时间',
|
||||
expiresAt: '过期时间',
|
||||
actions: '操作'
|
||||
},
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user