feat(channel-monitor-v2): 补齐管理端配置、隐藏吞吐开关与测试

管理端 V2 配置面板与系统设置中的模式/隐藏 RPM·TPM 开关,纳入关键
vitest 与 Makefile 前端关键路径,覆盖门控与展示行为。
This commit is contained in:
IanShaw027
2026-08-07 11:05:32 +08:00
parent d8c5024cee
commit d2d91ff9a8
10 changed files with 244 additions and 27 deletions
@@ -1,6 +1,55 @@
<template>
<AppLayout>
<TablePageLayout>
<div class="w-full min-w-0 space-y-6 pb-8">
<header
class="page-header mb-0 rounded-3xl bg-white p-5 shadow-sm ring-1 ring-gray-900/5 dark:bg-dark-800 dark:ring-dark-700 sm:p-6"
>
<h1 class="page-title flex items-center gap-2 text-xl font-black text-gray-900 dark:text-white">
<span class="inline-flex h-8 w-8 items-center justify-center rounded-xl bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-400">
<Icon name="chart" size="sm" />
</span>
{{ t('admin.channelMonitor.title') }}
</h1>
<p class="page-description mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
isV1Mode
? t('channelMonitorV2.admin.descriptionV1')
: t('channelMonitorV2.admin.descriptionV2')
}}
</p>
<div class="mt-4 border-t border-gray-100 pt-4 dark:border-dark-700">
<div
class="tabs inline-flex w-full max-w-xl flex-wrap sm:w-auto"
role="tablist"
:aria-label="t('channelMonitorV2.admin.tabAria')"
>
<button
type="button"
role="tab"
class="tab flex-1 sm:flex-none"
:class="adminMonitorTab === 'v2' ? 'tab-active' : ''"
:aria-selected="adminMonitorTab === 'v2'"
@click="adminMonitorTab = 'v2'"
>
{{ t('channelMonitorV2.admin.tabV2') }}
</button>
<button
type="button"
role="tab"
class="tab flex-1 sm:flex-none"
:class="adminMonitorTab === 'legacy' ? 'tab-active' : ''"
:aria-selected="adminMonitorTab === 'legacy'"
@click="adminMonitorTab = 'legacy'"
>
{{ isV1Mode ? t('channelMonitorV2.admin.tabV1Active') : t('channelMonitorV2.admin.tabV1History') }}
</button>
</div>
</div>
</header>
<MonitorSettingsPanel v-if="adminMonitorTab === 'v2'" />
<TablePageLayout v-else>
<template #filters>
<MonitorFiltersBar
v-model:search="searchQuery"
@@ -80,7 +129,8 @@
@update:pageSize="onPageSizeChange"
/>
</template>
</TablePageLayout>
</TablePageLayout>
</div>
<MonitorFormDialog
:show="showDialog"
@@ -115,7 +165,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { extractApiErrorMessage } from '@/utils/apiError'
@@ -144,9 +194,13 @@ import MonitorPrimaryModelCell from '@/components/admin/monitor/MonitorPrimaryMo
import MonitorActionsCell from '@/components/admin/monitor/MonitorActionsCell.vue'
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
import { useChannelMonitorFormat } from '@/composables/useChannelMonitorFormat'
import MonitorSettingsPanel from '@/features/channel-monitor-v2/MonitorSettingsPanel.vue'
import { isChannelMonitorV1Mode } from '@/utils/featureFlags'
const { t } = useI18n()
const appStore = useAppStore()
const isV1Mode = computed(() => isChannelMonitorV1Mode())
const adminMonitorTab = ref<'v2' | 'legacy'>(isChannelMonitorV1Mode() ? 'legacy' : 'v2')
const {
providerLabel,
providerBadgeClass,
@@ -265,6 +319,10 @@ async function toggleEnabled(row: ChannelMonitor) {
}
async function handleRunNow(row: ChannelMonitor) {
if (!isV1Mode.value) {
appStore.showError(t('admin.channelMonitor.runFailed'))
return
}
if (runningId.value != null) return
runningId.value = row.id
try {
@@ -318,7 +376,12 @@ async function confirmDelete() {
}
}
onMounted(reload)
watch(adminMonitorTab, (tab) => {
if (tab === 'legacy' && monitors.value.length === 0) void reload()
})
onMounted(() => {
if (adminMonitorTab.value === 'legacy') void reload()
})
onUnmounted(() => {
if (searchTimeout) clearTimeout(searchTimeout)
abortController?.abort()
+82 -15
View File
@@ -6816,21 +6816,77 @@
<Toggle v-model="form.channel_monitor_enabled" />
</div>
<div v-if="form.channel_monitor_enabled">
<label class="input-label">
{{ t('admin.settings.features.channelMonitor.defaultInterval') }}
<span class="text-red-500">*</span>
</label>
<input
v-model.number="form.channel_monitor_default_interval_seconds"
type="number"
min="15"
max="3600"
class="input"
/>
<p class="mt-1 text-xs text-gray-400">
{{ t('admin.settings.features.channelMonitor.defaultIntervalHint') }}
</p>
<div v-if="form.channel_monitor_enabled" class="space-y-5">
<div>
<label class="input-label">
{{ t('admin.settings.features.channelMonitor.mode') }}
</label>
<div class="mt-1.5 inline-flex w-full max-w-md rounded-lg border border-gray-200 bg-gray-50 p-1 dark:border-dark-600 dark:bg-dark-900/40">
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
:class="
form.channel_monitor_mode === 'v2'
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
"
@click="form.channel_monitor_mode = 'v2'"
>
{{ t('admin.settings.features.channelMonitor.modeV2') }}
</button>
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
:class="
form.channel_monitor_mode === 'v1'
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
"
@click="form.channel_monitor_mode = 'v1'"
>
{{ t('admin.settings.features.channelMonitor.modeV1') }}
</button>
</div>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
form.channel_monitor_mode === 'v1'
? t('admin.settings.features.channelMonitor.modeV1Hint')
: t('admin.settings.features.channelMonitor.modeV2Hint')
}}
</p>
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
{{ t('admin.settings.features.channelMonitor.modeHint') }}
</p>
</div>
<div v-if="form.channel_monitor_mode === 'v1'">
<label class="input-label">
{{ t('admin.settings.features.channelMonitor.defaultInterval') }}
<span class="text-red-500">*</span>
</label>
<input
v-model.number="form.channel_monitor_default_interval_seconds"
type="number"
min="15"
max="3600"
class="input"
/>
<p class="mt-1 text-xs text-gray-400">
{{ t('admin.settings.features.channelMonitor.defaultIntervalHint') }}
</p>
</div>
<div v-if="form.channel_monitor_mode === 'v2'" class="flex items-start justify-between gap-4">
<div class="min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ t('admin.settings.features.channelMonitor.hideThroughput') }}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.features.channelMonitor.hideThroughputHint') }}
</p>
</div>
<Toggle v-model="form.channel_monitor_hide_throughput" />
</div>
</div>
</div>
</div>
@@ -9157,6 +9213,8 @@ type SettingsForm = Omit<
| "wechat_connect_mp_enabled"
| "wechat_connect_mobile_enabled"
> & {
/** Form always binds a concrete boolean (SystemSettings marks this optional). */
channel_monitor_hide_throughput: boolean;
smtp_password: string;
turnstile_secret_key: string;
tencent_captcha_app_secret_key: string;
@@ -9458,7 +9516,9 @@ const form = reactive<SettingsForm>({
account_quota_notify_emails: [] as NotifyEmailEntry[],
// Channel Monitor feature switch
channel_monitor_enabled: true,
channel_monitor_mode: 'v2' as 'v1' | 'v2',
channel_monitor_default_interval_seconds: 60,
channel_monitor_hide_throughput: false,
// Available Channels feature switch
available_channels_enabled: false,
// Model Plaza feature switches + description
@@ -10455,6 +10515,11 @@ async function loadSettings() {
: defaultFingerprintSignalRows();
form.login_agreement_mode =
settings.login_agreement_mode === "checkbox" ? "checkbox" : "modal";
form.channel_monitor_mode =
settings.channel_monitor_mode === "v1" ? "v1" : "v2";
form.channel_monitor_hide_throughput = Boolean(
settings.channel_monitor_hide_throughput
);
form.login_agreement_updated_at =
settings.login_agreement_updated_at || "2026-03-31";
form.login_agreement_documents =
@@ -11092,8 +11157,10 @@ async function saveSettings() {
).filter((e) => e.email.trim() !== ""),
// Channel Monitor feature switch
channel_monitor_enabled: form.channel_monitor_enabled,
channel_monitor_mode: form.channel_monitor_mode === 'v1' ? 'v1' : 'v2',
channel_monitor_default_interval_seconds:
Number(form.channel_monitor_default_interval_seconds) || 60,
channel_monitor_hide_throughput: Boolean(form.channel_monitor_hide_throughput),
// Available Channels feature switch
available_channels_enabled: form.available_channels_enabled,
// Model Plaza feature switches + description
@@ -18,6 +18,17 @@ const {
showError: vi.fn(),
}))
vi.mock('@/utils/featureFlags', () => ({
isChannelMonitorV1Mode: () => true,
isChannelMonitorV2Mode: () => false,
getChannelMonitorMode: () => 'v1' as const,
}))
vi.mock('@/features/channel-monitor-v2/MonitorSettingsPanel.vue', () => ({
default: { name: 'MonitorSettingsPanel', template: '<div data-testid="v2-settings" />' },
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
channelMonitor: {
@@ -14,6 +14,17 @@ const { listTemplates } = vi.hoisted(() => ({
listTemplates: vi.fn(),
}))
vi.mock('@/utils/featureFlags', () => ({
isChannelMonitorV1Mode: () => true,
isChannelMonitorV2Mode: () => false,
getChannelMonitorMode: () => 'v1' as const,
}))
vi.mock('@/features/channel-monitor-v2/MonitorSettingsPanel.vue', () => ({
default: { name: 'MonitorSettingsPanel', template: '<div data-testid="v2-settings" />' },
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
channelMonitor: {