mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 08:25:35 +00:00
feat(channel-monitor-v2): 补齐管理端配置、隐藏吞吐开关与测试
管理端 V2 配置面板与系统设置中的模式/隐藏 RPM·TPM 开关,纳入关键 vitest 与 Makefile 前端关键路径,覆盖门控与展示行为。
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user