From ba98243cc2bc230bffb528014331f70276dbffaa Mon Sep 17 00:00:00 2001 From: erio Date: Tue, 21 Apr 2026 01:42:58 +0800 Subject: [PATCH] feat(channel-monitor): gate UI by feature switch + polish form UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppSidebar 三处菜单项(管理端渠道监控、用户端/个人页渠道状态)按 channel_monitor_enabled 条件展开,关闭时隐藏 - ChannelStatusView setInterval 随开关启停:关闭 clearInterval, 开启/未知态自动启动,避免禁用功能后仍在轮询 - MonitorFormDialog provider Select 改为 3 色单选按钮 (openai=emerald / anthropic=orange / gemini=sky),i18n 文案 供应商 → 平台 / Provider → Platform - MonitorKeyPickerDialog 按钮列表改为 name/key/group 三列表格 + 搜索框,按 key.group.platform === provider 过滤,避免跨平台误选 - form.provider 变化时清空 api_key,修复切换平台仍保留旧 key 的 错配 bug - providerPickerClass 抽取到 useChannelMonitorFormat composable, 统一 emerald/orange/sky 颜色语义,消除硬编码 Tailwind class 重复 - maskApiKey 工具函数统一(utils/maskApiKey.ts),KeysView 与 MonitorKeyPickerDialog 共用 slice(0,6)...slice(-4) 策略 - bump version to 0.1.114.27 --- .../admin/monitor/MonitorFormDialog.vue | 35 +++++++- .../admin/monitor/MonitorKeyPickerDialog.vue | 84 ++++++++++++++----- frontend/src/components/layout/AppSidebar.vue | 12 ++- .../composables/useChannelMonitorFormat.ts | 27 ++++++ frontend/src/i18n/locales/en.ts | 2 +- frontend/src/i18n/locales/zh.ts | 2 +- frontend/src/utils/maskApiKey.ts | 6 ++ frontend/src/views/user/ChannelStatusView.vue | 24 +++++- frontend/src/views/user/KeysView.vue | 8 +- 9 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 frontend/src/utils/maskApiKey.ts diff --git a/frontend/src/components/admin/monitor/MonitorFormDialog.vue b/frontend/src/components/admin/monitor/MonitorFormDialog.vue index e1489ffb..56a06a9f 100644 --- a/frontend/src/components/admin/monitor/MonitorFormDialog.vue +++ b/frontend/src/components/admin/monitor/MonitorFormDialog.vue @@ -13,7 +13,20 @@
- + + + +
+
{{ t('common.loading') }}
-
+
{{ t('admin.channelMonitor.form.noActiveKey') }}
-
- +
+ + + + + + + + + + + + + + + +
{{ t('common.name') }}{{ t('keys.apiKey') }}{{ t('keys.group') }}
{{ k.name }}{{ maskApiKey(k.key) }} + + {{ k.group.name }} + + +
diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 23d0f4e9..8b9fbdea 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -611,7 +611,9 @@ const userNavItems = computed((): NavItem[] => { { path: '/dashboard', label: t('nav.dashboard'), icon: DashboardIcon }, { path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon }, { path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true }, - { path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }, + ...(appStore.cachedPublicSettings?.channel_monitor_enabled + ? [{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }] + : []), { path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true }, ...(appStore.cachedPublicSettings?.payment_enabled ? [ @@ -650,7 +652,9 @@ const personalNavItems = computed((): NavItem[] => { const items: NavItem[] = [ { path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon }, { path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true }, - { path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }, + ...(appStore.cachedPublicSettings?.channel_monitor_enabled + ? [{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon }] + : []), { path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true }, ...(appStore.cachedPublicSettings?.payment_enabled ? [ @@ -715,7 +719,9 @@ const adminNavItems = computed((): NavItem[] => { expandOnly: true, children: [ { path: '/admin/channels/pricing', label: t('nav.channelPricing'), icon: PriceTagIcon }, - { path: '/admin/channels/monitor', label: t('nav.channelMonitor'), icon: SignalIcon }, + ...(appStore.cachedPublicSettings?.channel_monitor_enabled + ? [{ path: '/admin/channels/monitor', label: t('nav.channelMonitor'), icon: SignalIcon }] + : []), ], }, { path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true }, diff --git a/frontend/src/composables/useChannelMonitorFormat.ts b/frontend/src/composables/useChannelMonitorFormat.ts index 7ffdaa42..a9253622 100644 --- a/frontend/src/composables/useChannelMonitorFormat.ts +++ b/frontend/src/composables/useChannelMonitorFormat.ts @@ -76,6 +76,32 @@ export function useChannelMonitorFormat() { } } + /** + * Tailwind class for a provider radio-button-style picker (active/inactive state). + * Reuses the same emerald/orange/sky palette as providerBadgeClass to keep + * visual semantics consistent across badges and pickers. + */ + function providerPickerClass(p: Provider | string, active: boolean): string { + switch (p) { + case PROVIDER_OPENAI: + return active + ? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-400' + : 'border-gray-200 bg-white text-gray-600 hover:border-emerald-300 hover:text-emerald-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-emerald-500/50' + case PROVIDER_ANTHROPIC: + return active + ? 'border-orange-500 bg-orange-50 text-orange-700 dark:bg-orange-500/15 dark:text-orange-300 dark:border-orange-400' + : 'border-gray-200 bg-white text-gray-600 hover:border-orange-300 hover:text-orange-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-orange-500/50' + case PROVIDER_GEMINI: + return active + ? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-400' + : 'border-gray-200 bg-white text-gray-600 hover:border-sky-300 hover:text-sky-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-sky-500/50' + default: + return active + ? 'border-gray-400 bg-gray-50 text-gray-700 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-200' + : 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400' + } + } + function formatLatency(ms: number | null | undefined): string { if (ms == null) return t('monitorCommon.latencyEmpty') return String(Math.round(ms)) @@ -110,6 +136,7 @@ export function useChannelMonitorFormat() { statusBadgeClass, providerLabel, providerBadgeClass, + providerPickerClass, formatLatency, formatPercent, formatAvailability, diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 51aa9920..ef3a4057 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2137,7 +2137,7 @@ export default { form: { name: 'Name', namePlaceholder: 'Enter monitor name', - provider: 'Provider', + provider: 'Platform', endpoint: 'Endpoint', endpointPlaceholder: 'https://api.example.com', useCurrentDomain: 'Use current service', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 021b8992..25bce657 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2216,7 +2216,7 @@ export default { form: { name: '名称', namePlaceholder: '输入监控名称', - provider: '供应商', + provider: '平台', endpoint: '上游地址', endpointPlaceholder: 'https://api.example.com', useCurrentDomain: '使用当前服务', diff --git a/frontend/src/utils/maskApiKey.ts b/frontend/src/utils/maskApiKey.ts new file mode 100644 index 00000000..ab54980b --- /dev/null +++ b/frontend/src/utils/maskApiKey.ts @@ -0,0 +1,6 @@ +// Mask an API key for display: reveals first 6 + last 4; short keys (≤12) show `first 4 + ***`. +export function maskApiKey(key: string): string { + if (!key) return '' + if (key.length <= 12) return `${key.slice(0, 4)}***` + return `${key.slice(0, 6)}...${key.slice(-4)}` +} diff --git a/frontend/src/views/user/ChannelStatusView.vue b/frontend/src/views/user/ChannelStatusView.vue index af427cca..d9100890 100644 --- a/frontend/src/views/user/ChannelStatusView.vue +++ b/frontend/src/views/user/ChannelStatusView.vue @@ -160,14 +160,34 @@ watch(items, () => { void ensureDetailsForWindow() }) +function startTimer() { + if (countdownTimer !== undefined) return + countdownTimer = setInterval(tick, 1000) as unknown as number +} + +function stopTimer() { + if (countdownTimer !== undefined) { + clearInterval(countdownTimer) + countdownTimer = undefined + } +} + +watch( + () => appStore.cachedPublicSettings?.channel_monitor_enabled, + (enabled) => { + if (enabled === false) stopTimer() + else startTimer() + }, +) + // ── Lifecycle ── onMounted(() => { void reload(false) - countdownTimer = setInterval(tick, 1000) as unknown as number + if (appStore.cachedPublicSettings?.channel_monitor_enabled !== false) startTimer() }) onBeforeUnmount(() => { - if (countdownTimer !== undefined) clearInterval(countdownTimer) + stopTimer() if (abortController) abortController.abort() }) diff --git a/frontend/src/views/user/KeysView.vue b/frontend/src/views/user/KeysView.vue index 34cccf9c..cf29e4bd 100644 --- a/frontend/src/views/user/KeysView.vue +++ b/frontend/src/views/user/KeysView.vue @@ -61,7 +61,7 @@