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 @@