fix(websearch): improve settings UI and hide config when globally disabled

- API Key show/copy buttons moved inside input field (inline icons)
- Proxy selector and test button on same row to save vertical space
- Test opens a dialog modal instead of inline display
- Hide all websearch config in channels/accounts when global toggle is off
This commit is contained in:
erio
2026-04-12 15:59:45 +08:00
parent cef22c70ab
commit 889b5b4f3b
5 changed files with 177 additions and 86 deletions

View File

@@ -1789,40 +1789,42 @@
<!-- Expanded content -->
<div v-if="expandedProviders[pIdx]" class="space-y-3 border-t border-gray-100 px-4 pb-4 pt-3 dark:border-dark-700">
<!-- API Key with show/copy -->
<!-- API Key with inline show/copy -->
<div>
<label class="text-xs text-gray-500">{{ t('admin.settings.webSearchEmulation.apiKey') }}</label>
<div class="flex items-center gap-1">
<div class="relative">
<input
v-model="provider.api_key"
:type="apiKeyVisible[pIdx] ? 'text' : 'password'"
class="input flex-1 text-sm"
class="input w-full pr-16 text-sm"
:placeholder="provider.api_key_configured ? '••••••••' : t('admin.settings.webSearchEmulation.apiKeyPlaceholder')"
/>
<button
type="button"
class="btn btn-secondary btn-sm px-2"
:title="apiKeyVisible[pIdx] ? t('admin.settings.webSearchEmulation.hideApiKey') : t('admin.settings.webSearchEmulation.showApiKey')"
@click="apiKeyVisible[pIdx] = !apiKeyVisible[pIdx]"
>
<svg v-if="!apiKeyVisible[pIdx]" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
</svg>
</button>
<button
type="button"
class="btn btn-secondary btn-sm px-2"
:title="t('admin.settings.webSearchEmulation.copyApiKey')"
@click="copyApiKey(pIdx)"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
<div class="absolute inset-y-0 right-0 flex items-center pr-1.5">
<button
type="button"
class="rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
:title="apiKeyVisible[pIdx] ? t('admin.settings.webSearchEmulation.hideApiKey') : t('admin.settings.webSearchEmulation.showApiKey')"
@click="apiKeyVisible[pIdx] = !apiKeyVisible[pIdx]"
>
<svg v-if="!apiKeyVisible[pIdx]" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
</svg>
</button>
<button
type="button"
class="rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
:title="t('admin.settings.webSearchEmulation.copyApiKey')"
@click="copyApiKey(pIdx)"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
</div>
@@ -1858,44 +1860,19 @@
<span class="text-xs text-gray-500">{{ provider.quota_used ?? 0 }} / {{ provider.quota_limit }}</span>
</div>
<!-- Proxy -->
<div>
<label class="text-xs text-gray-500">{{ t('admin.settings.webSearchEmulation.proxy') }}</label>
<ProxySelector v-model="provider.proxy_id" :proxies="webSearchProxies" />
</div>
<!-- Test button -->
<div class="border-t border-gray-100 pt-3 dark:border-dark-700">
<div class="flex items-center gap-2">
<input
v-model="wsTestQuery"
type="text"
class="input flex-1 text-sm"
:placeholder="t('admin.settings.webSearchEmulation.testDefaultQuery')"
@keyup.enter="testWebSearchProvider()"
/>
<button
type="button"
class="btn btn-secondary btn-sm"
:disabled="wsTestLoading"
@click="testWebSearchProvider()"
>
{{ wsTestLoading ? t('admin.settings.webSearchEmulation.testing') : t('admin.settings.webSearchEmulation.test') }}
</button>
</div>
<!-- Test results -->
<div v-if="wsTestResult" class="mt-2 rounded-lg bg-gray-50 p-3 text-xs dark:bg-dark-700">
<p class="mb-1 font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.webSearchEmulation.testResultProvider') }}: {{ wsTestResult.provider }}
</p>
<div v-if="wsTestResult.results.length === 0" class="text-gray-400">
{{ t('admin.settings.webSearchEmulation.testNoResults') }}
</div>
<div v-for="(r, rIdx) in wsTestResult.results.slice(0, 3)" :key="rIdx" class="mt-1">
<a :href="r.url" target="_blank" class="font-medium text-blue-600 hover:underline dark:text-blue-400">{{ r.title }}</a>
<p class="text-gray-500 dark:text-gray-400">{{ r.snippet && r.snippet.length > 120 ? r.snippet.slice(0, 120) + '...' : r.snippet }}</p>
</div>
<!-- Proxy + Test on same row -->
<div class="flex items-end gap-3">
<div class="flex-1">
<label class="text-xs text-gray-500">{{ t('admin.settings.webSearchEmulation.proxy') }}</label>
<ProxySelector v-model="provider.proxy_id" :proxies="webSearchProxies" />
</div>
<button
type="button"
class="btn btn-secondary btn-sm whitespace-nowrap"
@click="openTestDialog()"
>
{{ t('admin.settings.webSearchEmulation.test') }}
</button>
</div>
</div>
</div>
@@ -1903,6 +1880,50 @@
</div>
</div>
<!-- Web Search Test Dialog -->
<div v-if="wsTestDialogOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="wsTestDialogOpen = false">
<div class="mx-4 w-full max-w-lg rounded-xl bg-white p-6 shadow-xl dark:bg-dark-800">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
{{ t('admin.settings.webSearchEmulation.testResultTitle') }}
</h3>
<div class="flex items-center gap-2">
<input
v-model="wsTestQuery"
type="text"
class="input flex-1 text-sm"
:placeholder="t('admin.settings.webSearchEmulation.testDefaultQuery')"
@keyup.enter="testWebSearchProvider()"
/>
<button
type="button"
class="btn btn-primary btn-sm"
:disabled="wsTestLoading"
@click="testWebSearchProvider()"
>
{{ wsTestLoading ? t('admin.settings.webSearchEmulation.testing') : t('admin.settings.webSearchEmulation.test') }}
</button>
</div>
<!-- Test results -->
<div v-if="wsTestResult" class="mt-4 max-h-80 overflow-y-auto rounded-lg bg-gray-50 p-4 dark:bg-dark-700">
<p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.webSearchEmulation.testResultProvider') }}: {{ wsTestResult.provider }}
</p>
<div v-if="wsTestResult.results.length === 0" class="text-sm text-gray-400">
{{ t('admin.settings.webSearchEmulation.testNoResults') }}
</div>
<div v-for="(r, rIdx) in wsTestResult.results" :key="rIdx" class="mt-2 border-t border-gray-200 pt-2 first:mt-0 first:border-0 first:pt-0 dark:border-dark-600">
<a :href="r.url" target="_blank" class="text-sm font-medium text-blue-600 hover:underline dark:text-blue-400">{{ r.title }}</a>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{{ r.snippet }}</p>
</div>
</div>
<div class="mt-4 flex justify-end">
<button type="button" class="btn btn-secondary btn-sm" @click="wsTestDialogOpen = false">
{{ t('common.close') }}
</button>
</div>
</div>
</div>
</div><!-- /Tab: Gateway Claude Code, Scheduling -->
<!-- Tab: General -->
@@ -3016,6 +3037,12 @@ const apiKeyVisible = reactive<Record<number, boolean>>({})
const wsTestQuery = ref('')
const wsTestLoading = ref(false)
const wsTestResult = ref<WebSearchTestResult | null>(null)
const wsTestDialogOpen = ref(false)
function openTestDialog() {
wsTestResult.value = null
wsTestDialogOpen.value = true
}
function toggleProviderExpand(idx: number) {
expandedProviders[idx] = !expandedProviders[idx]