mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-16 04:44:45 +08:00
Merge pull request #961 from 0xObjc/codex/ops-openai-token-visibility
feat(ops): make OpenAI token stats optional
This commit is contained in:
@@ -371,6 +371,8 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
|
|||||||
IgnoreCountTokensErrors: true, // count_tokens 404 是预期行为,默认忽略
|
IgnoreCountTokensErrors: true, // count_tokens 404 是预期行为,默认忽略
|
||||||
IgnoreContextCanceled: true, // Default to true - client disconnects are not errors
|
IgnoreContextCanceled: true, // Default to true - client disconnects are not errors
|
||||||
IgnoreNoAvailableAccounts: false, // Default to false - this is a real routing issue
|
IgnoreNoAvailableAccounts: false, // Default to false - this is a real routing issue
|
||||||
|
DisplayOpenAITokenStats: false,
|
||||||
|
DisplayAlertEvents: true,
|
||||||
AutoRefreshEnabled: false,
|
AutoRefreshEnabled: false,
|
||||||
AutoRefreshIntervalSec: 30,
|
AutoRefreshIntervalSec: 30,
|
||||||
}
|
}
|
||||||
@@ -438,7 +440,7 @@ func (s *OpsService) GetOpsAdvancedSettings(ctx context.Context) (*OpsAdvancedSe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &OpsAdvancedSettings{}
|
cfg := defaultOpsAdvancedSettings()
|
||||||
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
|
if err := json.Unmarshal([]byte(raw), cfg); err != nil {
|
||||||
return defaultCfg, nil
|
return defaultCfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
97
backend/internal/service/ops_settings_advanced_test.go
Normal file
97
backend/internal/service/ops_settings_advanced_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetOpsAdvancedSettings_DefaultHidesOpenAITokenStats(t *testing.T) {
|
||||||
|
repo := newRuntimeSettingRepoStub()
|
||||||
|
svc := &OpsService{settingRepo: repo}
|
||||||
|
|
||||||
|
cfg, err := svc.GetOpsAdvancedSettings(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
|
||||||
|
}
|
||||||
|
if cfg.DisplayOpenAITokenStats {
|
||||||
|
t.Fatalf("DisplayOpenAITokenStats = true, want false by default")
|
||||||
|
}
|
||||||
|
if !cfg.DisplayAlertEvents {
|
||||||
|
t.Fatalf("DisplayAlertEvents = false, want true by default")
|
||||||
|
}
|
||||||
|
if repo.setCalls != 1 {
|
||||||
|
t.Fatalf("expected defaults to be persisted once, got %d", repo.setCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateOpsAdvancedSettings_PersistsOpenAITokenStatsVisibility(t *testing.T) {
|
||||||
|
repo := newRuntimeSettingRepoStub()
|
||||||
|
svc := &OpsService{settingRepo: repo}
|
||||||
|
|
||||||
|
cfg := defaultOpsAdvancedSettings()
|
||||||
|
cfg.DisplayOpenAITokenStats = true
|
||||||
|
cfg.DisplayAlertEvents = false
|
||||||
|
|
||||||
|
updated, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateOpsAdvancedSettings() error = %v", err)
|
||||||
|
}
|
||||||
|
if !updated.DisplayOpenAITokenStats {
|
||||||
|
t.Fatalf("DisplayOpenAITokenStats = false, want true")
|
||||||
|
}
|
||||||
|
if updated.DisplayAlertEvents {
|
||||||
|
t.Fatalf("DisplayAlertEvents = true, want false")
|
||||||
|
}
|
||||||
|
|
||||||
|
reloaded, err := svc.GetOpsAdvancedSettings(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOpsAdvancedSettings() after update error = %v", err)
|
||||||
|
}
|
||||||
|
if !reloaded.DisplayOpenAITokenStats {
|
||||||
|
t.Fatalf("reloaded DisplayOpenAITokenStats = false, want true")
|
||||||
|
}
|
||||||
|
if reloaded.DisplayAlertEvents {
|
||||||
|
t.Fatalf("reloaded DisplayAlertEvents = true, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOpsAdvancedSettings_BackfillsNewDisplayFlagsFromDefaults(t *testing.T) {
|
||||||
|
repo := newRuntimeSettingRepoStub()
|
||||||
|
svc := &OpsService{settingRepo: repo}
|
||||||
|
|
||||||
|
legacyCfg := map[string]any{
|
||||||
|
"data_retention": map[string]any{
|
||||||
|
"cleanup_enabled": false,
|
||||||
|
"cleanup_schedule": "0 2 * * *",
|
||||||
|
"error_log_retention_days": 30,
|
||||||
|
"minute_metrics_retention_days": 30,
|
||||||
|
"hourly_metrics_retention_days": 30,
|
||||||
|
},
|
||||||
|
"aggregation": map[string]any{
|
||||||
|
"aggregation_enabled": false,
|
||||||
|
},
|
||||||
|
"ignore_count_tokens_errors": true,
|
||||||
|
"ignore_context_canceled": true,
|
||||||
|
"ignore_no_available_accounts": false,
|
||||||
|
"ignore_invalid_api_key_errors": false,
|
||||||
|
"auto_refresh_enabled": false,
|
||||||
|
"auto_refresh_interval_seconds": 30,
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(legacyCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal legacy config: %v", err)
|
||||||
|
}
|
||||||
|
repo.values[SettingKeyOpsAdvancedSettings] = string(raw)
|
||||||
|
|
||||||
|
cfg, err := svc.GetOpsAdvancedSettings(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOpsAdvancedSettings() error = %v", err)
|
||||||
|
}
|
||||||
|
if cfg.DisplayOpenAITokenStats {
|
||||||
|
t.Fatalf("DisplayOpenAITokenStats = true, want false default backfill")
|
||||||
|
}
|
||||||
|
if !cfg.DisplayAlertEvents {
|
||||||
|
t.Fatalf("DisplayAlertEvents = false, want true default backfill")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,6 +98,8 @@ type OpsAdvancedSettings struct {
|
|||||||
IgnoreContextCanceled bool `json:"ignore_context_canceled"`
|
IgnoreContextCanceled bool `json:"ignore_context_canceled"`
|
||||||
IgnoreNoAvailableAccounts bool `json:"ignore_no_available_accounts"`
|
IgnoreNoAvailableAccounts bool `json:"ignore_no_available_accounts"`
|
||||||
IgnoreInvalidApiKeyErrors bool `json:"ignore_invalid_api_key_errors"`
|
IgnoreInvalidApiKeyErrors bool `json:"ignore_invalid_api_key_errors"`
|
||||||
|
DisplayOpenAITokenStats bool `json:"display_openai_token_stats"`
|
||||||
|
DisplayAlertEvents bool `json:"display_alert_events"`
|
||||||
AutoRefreshEnabled bool `json:"auto_refresh_enabled"`
|
AutoRefreshEnabled bool `json:"auto_refresh_enabled"`
|
||||||
AutoRefreshIntervalSec int `json:"auto_refresh_interval_seconds"`
|
AutoRefreshIntervalSec int `json:"auto_refresh_interval_seconds"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -841,6 +841,8 @@ export interface OpsAdvancedSettings {
|
|||||||
ignore_context_canceled: boolean
|
ignore_context_canceled: boolean
|
||||||
ignore_no_available_accounts: boolean
|
ignore_no_available_accounts: boolean
|
||||||
ignore_invalid_api_key_errors: boolean
|
ignore_invalid_api_key_errors: boolean
|
||||||
|
display_openai_token_stats: boolean
|
||||||
|
display_alert_events: boolean
|
||||||
auto_refresh_enabled: boolean
|
auto_refresh_enabled: boolean
|
||||||
auto_refresh_interval_seconds: number
|
auto_refresh_interval_seconds: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3709,6 +3709,11 @@ export default {
|
|||||||
refreshInterval15s: '15 seconds',
|
refreshInterval15s: '15 seconds',
|
||||||
refreshInterval30s: '30 seconds',
|
refreshInterval30s: '30 seconds',
|
||||||
refreshInterval60s: '60 seconds',
|
refreshInterval60s: '60 seconds',
|
||||||
|
dashboardCards: 'Dashboard Cards',
|
||||||
|
displayAlertEvents: 'Display alert events',
|
||||||
|
displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.',
|
||||||
|
displayOpenAITokenStats: 'Display OpenAI token request stats',
|
||||||
|
displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.',
|
||||||
autoRefreshCountdown: 'Auto refresh: {seconds}s',
|
autoRefreshCountdown: 'Auto refresh: {seconds}s',
|
||||||
validation: {
|
validation: {
|
||||||
title: 'Please fix the following issues',
|
title: 'Please fix the following issues',
|
||||||
|
|||||||
@@ -3883,6 +3883,11 @@ export default {
|
|||||||
refreshInterval15s: '15 秒',
|
refreshInterval15s: '15 秒',
|
||||||
refreshInterval30s: '30 秒',
|
refreshInterval30s: '30 秒',
|
||||||
refreshInterval60s: '60 秒',
|
refreshInterval60s: '60 秒',
|
||||||
|
dashboardCards: '仪表盘卡片',
|
||||||
|
displayAlertEvents: '展示告警事件',
|
||||||
|
displayAlertEventsHint: '控制运维监控仪表盘中告警事件卡片是否显示,默认开启。',
|
||||||
|
displayOpenAITokenStats: '展示 OpenAI Token 请求统计',
|
||||||
|
displayOpenAITokenStatsHint: '控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。',
|
||||||
autoRefreshCountdown: '自动刷新:{seconds}s',
|
autoRefreshCountdown: '自动刷新:{seconds}s',
|
||||||
validation: {
|
validation: {
|
||||||
title: '请先修正以下问题',
|
title: '请先修正以下问题',
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row: OpenAI Token Stats -->
|
<!-- Row: OpenAI Token Stats -->
|
||||||
<div v-if="opsEnabled && !(loading && !hasLoadedOnce)" class="grid grid-cols-1 gap-6">
|
<div v-if="opsEnabled && showOpenAITokenStats && !(loading && !hasLoadedOnce)" class="grid grid-cols-1 gap-6">
|
||||||
<OpsOpenAITokenStatsCard
|
<OpsOpenAITokenStatsCard
|
||||||
:platform-filter="platform"
|
:platform-filter="platform"
|
||||||
:group-id-filter="groupId"
|
:group-id-filter="groupId"
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alert Events -->
|
<!-- Alert Events -->
|
||||||
<OpsAlertEventsCard v-if="opsEnabled && !(loading && !hasLoadedOnce)" />
|
<OpsAlertEventsCard v-if="opsEnabled && showAlertEvents && !(loading && !hasLoadedOnce)" />
|
||||||
|
|
||||||
<!-- System Logs -->
|
<!-- System Logs -->
|
||||||
<OpsSystemLogTable
|
<OpsSystemLogTable
|
||||||
@@ -381,6 +381,8 @@ const showSettingsDialog = ref(false)
|
|||||||
const showAlertRulesCard = ref(false)
|
const showAlertRulesCard = ref(false)
|
||||||
|
|
||||||
// Auto refresh settings
|
// Auto refresh settings
|
||||||
|
const showAlertEvents = ref(true)
|
||||||
|
const showOpenAITokenStats = ref(false)
|
||||||
const autoRefreshEnabled = ref(false)
|
const autoRefreshEnabled = ref(false)
|
||||||
const autoRefreshIntervalMs = ref(30000) // default 30 seconds
|
const autoRefreshIntervalMs = ref(30000) // default 30 seconds
|
||||||
const autoRefreshCountdown = ref(0)
|
const autoRefreshCountdown = ref(0)
|
||||||
@@ -408,15 +410,22 @@ const { pause: pauseCountdown, resume: resumeCountdown } = useIntervalFn(
|
|||||||
{ immediate: false }
|
{ immediate: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load auto refresh settings from backend
|
// Load ops dashboard presentation settings from backend.
|
||||||
async function loadAutoRefreshSettings() {
|
async function loadDashboardAdvancedSettings() {
|
||||||
try {
|
try {
|
||||||
const settings = await opsAPI.getAdvancedSettings()
|
const settings = await opsAPI.getAdvancedSettings()
|
||||||
|
showAlertEvents.value = settings.display_alert_events
|
||||||
|
showOpenAITokenStats.value = settings.display_openai_token_stats
|
||||||
autoRefreshEnabled.value = settings.auto_refresh_enabled
|
autoRefreshEnabled.value = settings.auto_refresh_enabled
|
||||||
autoRefreshIntervalMs.value = settings.auto_refresh_interval_seconds * 1000
|
autoRefreshIntervalMs.value = settings.auto_refresh_interval_seconds * 1000
|
||||||
autoRefreshCountdown.value = settings.auto_refresh_interval_seconds
|
autoRefreshCountdown.value = settings.auto_refresh_interval_seconds
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[OpsDashboard] Failed to load auto refresh settings', err)
|
console.error('[OpsDashboard] Failed to load dashboard advanced settings', err)
|
||||||
|
showAlertEvents.value = true
|
||||||
|
showOpenAITokenStats.value = false
|
||||||
|
autoRefreshEnabled.value = false
|
||||||
|
autoRefreshIntervalMs.value = 30000
|
||||||
|
autoRefreshCountdown.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +473,8 @@ function onCustomTimeRangeChange(startTime: string, endTime: string) {
|
|||||||
customEndTime.value = endTime
|
customEndTime.value = endTime
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSettingsSaved() {
|
async function onSettingsSaved() {
|
||||||
|
await loadDashboardAdvancedSettings()
|
||||||
loadThresholds()
|
loadThresholds()
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
@@ -774,7 +784,7 @@ onMounted(async () => {
|
|||||||
loadThresholds()
|
loadThresholds()
|
||||||
|
|
||||||
// Load auto refresh settings
|
// Load auto refresh settings
|
||||||
await loadAutoRefreshSettings()
|
await loadDashboardAdvancedSettings()
|
||||||
|
|
||||||
if (opsEnabled.value) {
|
if (opsEnabled.value) {
|
||||||
await fetchData()
|
await fetchData()
|
||||||
@@ -816,7 +826,7 @@ watch(autoRefreshEnabled, (enabled) => {
|
|||||||
// Reload auto refresh settings after settings dialog is closed
|
// Reload auto refresh settings after settings dialog is closed
|
||||||
watch(showSettingsDialog, async (show) => {
|
watch(showSettingsDialog, async (show) => {
|
||||||
if (!show) {
|
if (!show) {
|
||||||
await loadAutoRefreshSettings()
|
await loadDashboardAdvancedSettings()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -208,35 +208,39 @@ function onNextPage() {
|
|||||||
:description="t('admin.ops.openaiTokenStats.empty')"
|
:description="t('admin.ops.openaiTokenStats.empty')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-else class="overflow-x-auto">
|
<div v-else class="space-y-3">
|
||||||
<table class="min-w-full text-left text-xs md:text-sm">
|
<div class="overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700">
|
||||||
<thead>
|
<div class="max-h-[420px] overflow-auto">
|
||||||
<tr class="border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-gray-400">
|
<table class="min-w-full text-left text-xs md:text-sm">
|
||||||
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.model') }}</th>
|
<thead class="sticky top-0 z-10 bg-white dark:bg-dark-800">
|
||||||
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.requestCount') }}</th>
|
<tr class="border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-gray-400">
|
||||||
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.avgTokensPerSec') }}</th>
|
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.model') }}</th>
|
||||||
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.avgFirstTokenMs') }}</th>
|
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.requestCount') }}</th>
|
||||||
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.totalOutputTokens') }}</th>
|
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.avgTokensPerSec') }}</th>
|
||||||
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.avgDurationMs') }}</th>
|
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.avgFirstTokenMs') }}</th>
|
||||||
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.requestsWithFirstToken') }}</th>
|
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.totalOutputTokens') }}</th>
|
||||||
</tr>
|
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.avgDurationMs') }}</th>
|
||||||
</thead>
|
<th class="px-2 py-2 font-semibold">{{ t('admin.ops.openaiTokenStats.table.requestsWithFirstToken') }}</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<tr
|
</thead>
|
||||||
v-for="row in items"
|
<tbody>
|
||||||
:key="row.model"
|
<tr
|
||||||
class="border-b border-gray-100 text-gray-700 dark:border-dark-800 dark:text-gray-200"
|
v-for="row in items"
|
||||||
>
|
:key="row.model"
|
||||||
<td class="px-2 py-2 font-medium">{{ row.model }}</td>
|
class="border-b border-gray-100 text-gray-700 last:border-b-0 dark:border-dark-800 dark:text-gray-200"
|
||||||
<td class="px-2 py-2">{{ formatInt(row.request_count) }}</td>
|
>
|
||||||
<td class="px-2 py-2">{{ formatRate(row.avg_tokens_per_sec) }}</td>
|
<td class="px-2 py-2 font-medium">{{ row.model }}</td>
|
||||||
<td class="px-2 py-2">{{ formatRate(row.avg_first_token_ms) }}</td>
|
<td class="px-2 py-2">{{ formatInt(row.request_count) }}</td>
|
||||||
<td class="px-2 py-2">{{ formatInt(row.total_output_tokens) }}</td>
|
<td class="px-2 py-2">{{ formatRate(row.avg_tokens_per_sec) }}</td>
|
||||||
<td class="px-2 py-2">{{ formatInt(row.avg_duration_ms) }}</td>
|
<td class="px-2 py-2">{{ formatRate(row.avg_first_token_ms) }}</td>
|
||||||
<td class="px-2 py-2">{{ formatInt(row.requests_with_first_token) }}</td>
|
<td class="px-2 py-2">{{ formatInt(row.total_output_tokens) }}</td>
|
||||||
</tr>
|
<td class="px-2 py-2">{{ formatInt(row.avg_duration_ms) }}</td>
|
||||||
</tbody>
|
<td class="px-2 py-2">{{ formatInt(row.requests_with_first_token) }}</td>
|
||||||
</table>
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="viewMode === 'topn'" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
<div v-if="viewMode === 'topn'" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{ t('admin.ops.openaiTokenStats.totalModels', { total }) }}
|
{{ t('admin.ops.openaiTokenStats.totalModels', { total }) }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -543,6 +543,31 @@ async function saveAllSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dashboard Cards -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h5 class="text-xs font-semibold text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.dashboardCards') }}</h5>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.displayAlertEvents') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
|
{{ t('admin.ops.settings.displayAlertEventsHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="advancedSettings.display_alert_events" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.ops.settings.displayOpenAITokenStats') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
|
{{ t('admin.ops.settings.displayOpenAITokenStatsHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="advancedSettings.display_openai_token_stats" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -196,6 +196,23 @@ describe('OpsOpenAITokenStatsCard', () => {
|
|||||||
expect(wrapper.find('.empty-state').exists()).toBe(true)
|
expect(wrapper.find('.empty-state').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('数据表使用固定高度滚动容器,避免纵向无限增长', async () => {
|
||||||
|
mockGetOpenAITokenStats.mockResolvedValue(sampleResponse)
|
||||||
|
|
||||||
|
const wrapper = mount(OpsOpenAITokenStatsCard, {
|
||||||
|
props: { refreshToken: 0 },
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
Select: SelectStub,
|
||||||
|
EmptyState: EmptyStateStub,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.find('.max-h-\\[420px\\]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('接口异常时显示错误提示', async () => {
|
it('接口异常时显示错误提示', async () => {
|
||||||
mockGetOpenAITokenStats.mockRejectedValue(new Error('加载失败'))
|
mockGetOpenAITokenStats.mockRejectedValue(new Error('加载失败'))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user