fix(ui): align help icons, account notes, and settings navigation

Use transparent help triggers and keep note text styling stable.
Show notes in account dialogs and simplify upstream labels.
Keep settings navigation segmented on narrow screens.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-10-29 20:45:32 +08:00
co-authored by Codex
parent 23b85172b5
commit 12771aca87
31 changed files with 43 additions and 56 deletions
+1
View File
@@ -418,6 +418,7 @@ utility / arbitrary variant;Vue Transition、跨浏览器 Range、动态富文
- 表格斑马纹使用 `table-row-stripe-bg`;选中行悬停使用 `table-row-selected-hover-bg`,不被普通 Hover 覆盖
- 结构化浮层头部使用 `popover-header-bg`,明暗模式分别检查它与表格背景的区分
- Card 和选项默认不增加装饰性边框;键盘焦点必须保留可见反馈
- 帮助与说明图标常态、Hover 和 Active 均保持透明背景,按钮使用 `variant="link"`,保留键盘焦点反馈;已有说明浮层时不重复显示原生 `title` 提示
- 阴影保持中性,`shadowStrength` 只调节层级强弱,不给阴影染品牌色
## 扩展指南
@@ -146,7 +146,7 @@ defineExpose({ focusField })
<template v-if="field.description" #label-extra>
<ZPopover trigger="hover-click" placement="top-start">
<template #reference>
<ZIconButton :aria-label="t('common.form.fieldHelp', { label: field.label })" class="size-3.5!" :title="undefined">
<ZIconButton variant="link" :aria-label="t('common.form.fieldHelp', { label: field.label })" class="size-3.5!" :title="undefined">
<Info class="size-3.5" />
</ZIconButton>
</template>
@@ -19,10 +19,10 @@ defineProps<{
<template #reference>
<button
type="button"
class="inline-flex size-4 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
class="inline-flex size-4 items-center justify-center rounded-full bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
:class="tone === 'warning'
? 'bg-cp-warning-container text-cp-warning-on-container hover:bg-cp-warning-container-hover'
: 'bg-cp-primary-container text-cp-primary-on-container hover:bg-cp-primary-container-hover'"
? 'text-cp-warning-text'
: 'text-cp-primary-text'"
:aria-label="triggerLabel"
>
<Info class="size-3" />
+1 -3
View File
@@ -118,7 +118,6 @@
"noModelsAvailable": "No models available for testing"
},
"credentials": {
"wsFirst": "WS first",
"accountName": "Account name",
"enterUpstreamName": "Enter the upstream name",
"baseUrlHelp": "Enter the API base URL, endpoint paths are appended automatically",
@@ -126,7 +125,7 @@
"apiKeyPlaceholder": "Leave blank to keep the current key",
"enterUpstreamKey": "Enter the upstream key",
"transportLabel": "Transport",
"webSocketHelp": "WS first requires upstream support for Responses WebSocket",
"webSocketHelp": "WS requires upstream support for Responses WebSocket",
"nameRequired": "Enter an account name",
"validation": {
"urlLength": "Upstream API URL must not exceed 2,048 characters",
@@ -163,7 +162,6 @@
"editor": {
"title": "Edit account",
"transportLabel": "Upstream transport",
"upstreamConnection": "Upstream connection",
"loadError": "Could not load upstream settings",
"notesHelp": "Up to 500 characters, leave blank to clear notes",
"oauthUnavailable": "This account has no OAuth upstream settings",
@@ -118,7 +118,6 @@
"noModelsAvailable": "没有可测试模型"
},
"credentials": {
"wsFirst": "WS 优先",
"accountName": "账号名称",
"enterUpstreamName": "输入上游名称",
"baseUrlHelp": "填写 API 前缀,接口路径会自动追加",
@@ -126,7 +125,7 @@
"apiKeyPlaceholder": "留空保留当前密钥",
"enterUpstreamKey": "输入上游密钥",
"transportLabel": "传输方式",
"webSocketHelp": "WS 优先需要上游支持 Responses WebSocket",
"webSocketHelp": "WS 需要上游支持 Responses WebSocket",
"nameRequired": "请输入账号名称",
"validation": {
"urlLength": "上游 API 地址不能超过 2048 个字符",
@@ -163,7 +162,6 @@
"editor": {
"title": "编辑账号",
"transportLabel": "上游传输方式",
"upstreamConnection": "上游连接",
"loadError": "上游设置读取失败",
"notesHelp": "最多 500 字,留空可清除备注",
"oauthUnavailable": "该账号没有 OAuth 上游设置",
@@ -118,7 +118,6 @@
"noModelsAvailable": "沒有可測試模型"
},
"credentials": {
"wsFirst": "WS 優先",
"accountName": "賬號名稱",
"enterUpstreamName": "輸入上游名稱",
"baseUrlHelp": "填寫 API 前綴,接口路徑會自動追加",
@@ -126,7 +125,7 @@
"apiKeyPlaceholder": "留空保留當前密鑰",
"enterUpstreamKey": "輸入上游密鑰",
"transportLabel": "傳輸方式",
"webSocketHelp": "WS 優先需要上游支持 Responses WebSocket",
"webSocketHelp": "WS 需要上游支持 Responses WebSocket",
"nameRequired": "請輸入賬號名稱",
"validation": {
"urlLength": "上游 API 地址不能超過 2048 個字符",
@@ -163,7 +162,6 @@
"editor": {
"title": "編輯賬號",
"transportLabel": "上游傳輸方式",
"upstreamConnection": "上游連接",
"loadError": "上游設置讀取失敗",
"notesHelp": "最多 500 字,留空可清除備註",
"oauthUnavailable": "該賬號沒有 OAuth 上游設置",
@@ -481,7 +481,7 @@ watch(
/>
<ZPopover placement="top-start">
<template #reference>
<ZButton variant="text" size="small" square :aria-label="t('systemUpdate.channels.help')">
<ZButton variant="link" size="small" square :aria-label="t('systemUpdate.channels.help')">
<CircleHelp class="size-4" />
</ZButton>
</template>
@@ -3,7 +3,6 @@ import type { ApiKeyAccountForm } from '../utils/upstreamApiKey'
import { ZFormItem, ZInput, ZSegmented } from '@codex-proxy/ui'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
defineProps<{ disabled?: boolean, editing?: boolean }>()
@@ -11,13 +10,10 @@ defineProps<{ disabled?: boolean, editing?: boolean }>()
const { t } = useI18n({ useScope: 'global' })
const form = defineModel<ApiKeyAccountForm>({ required: true })
const transportOptions = computed(() => ([
const transportOptions = [
{ label: 'HTTP / SSE', value: 'http' },
{
label: t('accounts.credentials.wsFirst'),
value: 'prefer_websocket',
},
]))
{ label: 'WS', value: 'prefer_websocket' },
]
</script>
<template>
@@ -58,7 +58,7 @@ function connectionLogClass(tone: string) {
<section
class="flex items-center justify-between gap-4 rounded-cp-card bg-cp-fill-quaternary px-4 py-3"
>
<AccountIdentityCell :account="account" size="lg" show-plan />
<AccountIdentityCell :account="account" size="lg" show-plan show-notes />
<AccountStatusBadge
:status="account.status"
:error-reason="account.errorReason"
@@ -59,6 +59,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
class="min-w-0 flex-1"
:account="account"
size="lg"
show-notes
/>
<div class="flex shrink-0 items-center gap-3">
<AccountPlanBadge :authentication-kind="account.authenticationKind" :plan-type="account.planType" :plan-type-display="account.planTypeDisplay" size="sm" />
@@ -73,12 +74,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
<ZSegmented v-model="oauthTransport" :aria-label="t('accounts.editor.transportLabel')" :options="transportOptions" :disabled="saving || !configurationReady" />
</ZFormItem>
<section v-if="isOpenAiApiKeyAccount(account)" class="grid gap-4">
<h3 class="m-0 text-cp font-heavy text-cp-text">
{{ t('accounts.editor.upstreamConnection') }}
</h3>
<AccountApiKeyFields v-model="apiKey" editing :disabled="saving || !configurationReady" />
</section>
<AccountApiKeyFields v-if="isOpenAiApiKeyAccount(account)" v-model="apiKey" editing :disabled="saving || !configurationReady" />
<AccountSettingsFields
v-model:enabled="enabled"
@@ -196,7 +196,7 @@ watch(search, () => {
<ZPopover v-if="item.unavailable" class="mr-1 shrink-0" trigger="hover-click" placement="top" role="tooltip">
<template #reference="{ open, id }">
<ZIconButton
class="bg-transparent! hover:bg-transparent! active:bg-transparent!"
variant="link"
size="small"
:aria-label="t('accounts.modelAccess.notInCurrentModelCatalog', { id: item.id })"
:aria-describedby="open ? id : undefined"
@@ -45,8 +45,7 @@ watch(() => props.notes, recheckOpen, { flush: 'post' })
<template #reference>
<button
type="button"
class="inline-flex min-w-0 max-w-full touch-manipulation items-center rounded-cp-sm border-0 bg-transparent p-0 text-left text-cp-xs leading-4 text-cp-text-tertiary outline-none transition-colors hover:text-cp-primary-text focus-visible:ring-2 focus-visible:ring-cp-control-outline motion-reduce:transition-none"
:class="open ? 'text-cp-primary-text' : ''"
class="inline-flex min-w-0 max-w-full cursor-default touch-manipulation items-center rounded-cp-sm border-0 bg-transparent p-0 text-left text-cp-xs leading-4 text-cp-text-tertiary outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
:aria-label="t('accounts.notes.viewAccountNotes')"
aria-haspopup="dialog"
:aria-expanded="open"
@@ -69,6 +69,8 @@ function handleRetentionKeydown(event: KeyboardEvent) {
<ZPopover v-model="retentionOpen" trigger="hover-click" placement="top-start" :hover-delay="240">
<template #reference>
<ZIconButton
variant="link"
:title="undefined"
:aria-label="t('accounts.importTasks.taskRetentionInstructions')"
size="small"
:aria-expanded="retentionOpen"
@@ -116,6 +116,8 @@ function handleExplanationKeydown(event: KeyboardEvent) {
<ZPopover v-model="explanationOpen" trigger="hover-click" placement="top-start" :hover-delay="240">
<template #reference>
<ZIconButton
variant="link"
:title="undefined"
:aria-label="t('accounts.quotaForecast.forecastDescription')"
size="small"
:aria-expanded="explanationOpen"
@@ -73,7 +73,7 @@ watch(() => props.accountId, () => {
</h3>
<ZPopover trigger="click" placement="bottom-start" role="dialog">
<template #reference="{ open, id }">
<ZIconButton size="small" class="size-6! p-0!" :aria-label="t('accounts.quotaReset.automaticUsageRules')" :aria-expanded="open" :aria-controls="id">
<ZIconButton variant="link" :title="undefined" size="small" class="size-6! p-0!" :aria-label="t('accounts.quotaReset.automaticUsageRules')" :aria-expanded="open" :aria-controls="id">
<CircleHelp class="size-3.5" />
</ZIconButton>
</template>
@@ -22,6 +22,7 @@ function handleKeydown(event: KeyboardEvent) {
<ZPopover v-model="open" trigger="hover-click" placement="top-start" :hover-delay="240" class="shrink-0">
<template #reference>
<ZIconButton
variant="link"
:aria-label="label"
size="small"
class="size-5!"
@@ -23,7 +23,7 @@ const invalid = computed(() => props.rows.filter(row => row.error))
<template #label-extra>
<ZPopover class="-my-1" trigger="hover-click" placement="bottom-start">
<template #reference="{ open }">
<ZIconButton :aria-label="t('proxies.batchCreate.formatDescription')" :title="undefined" :aria-expanded="open" class="size-6! hover:bg-transparent! active:bg-transparent!">
<ZIconButton variant="link" :aria-label="t('proxies.batchCreate.formatDescription')" :title="undefined" :aria-expanded="open" class="size-6!">
<CircleHelp class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
@@ -37,7 +37,7 @@ const affinityOptions = computed(() => ([
<template #label-extra>
<ZPopover class="-my-1" trigger="hover-click" placement="top-start">
<template #reference="{ open }">
<ZIconButton :aria-label="t('settings.affinity.accountAffinityInstructions')" :title="undefined" :aria-expanded="open" class="size-6! hover:bg-transparent! active:bg-transparent!">
<ZIconButton variant="link" :aria-label="t('settings.affinity.accountAffinityInstructions')" :title="undefined" :aria-expanded="open" class="size-6!">
<CircleAlert class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
@@ -20,7 +20,7 @@ const model = defineModel<string>('model', { required: true })
{{ t('settings.warmup.title') }}
<ZPopover trigger="hover-click" placement="top-start">
<template #reference="{ open }">
<ZIconButton :aria-label="t('settings.warmup.helpTitle')" :title="undefined" :aria-expanded="open" class="size-6! hover:bg-transparent! active:bg-transparent!">
<ZIconButton variant="link" :aria-label="t('settings.warmup.helpTitle')" :title="undefined" :aria-expanded="open" class="size-6!">
<CircleAlert class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
@@ -41,7 +41,7 @@ const openaiGuardianReservedConcurrency = defineModel<string>('openaiGuardianRes
<template #label-extra>
<ZPopover class="-my-1" trigger="hover-click" placement="top-start">
<template #reference="{ open }">
<ZIconButton :aria-label="t('settings.concurrency.queueTimeoutHelpTitle')" :title="undefined" :aria-expanded="open" class="size-6! hover:bg-transparent! active:bg-transparent!">
<ZIconButton variant="link" :aria-label="t('settings.concurrency.queueTimeoutHelpTitle')" :title="undefined" :aria-expanded="open" class="size-6!">
<CircleAlert class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
@@ -56,7 +56,7 @@ const openaiGuardianReservedConcurrency = defineModel<string>('openaiGuardianRes
<template #label-extra>
<ZPopover class="-my-1" trigger="hover-click" placement="top-start">
<template #reference="{ open }">
<ZIconButton :aria-label="t('settings.concurrency.autoReview.helpTitle')" :title="undefined" :aria-expanded="open" class="size-6! hover:bg-transparent! active:bg-transparent!">
<ZIconButton variant="link" :aria-label="t('settings.concurrency.autoReview.helpTitle')" :title="undefined" :aria-expanded="open" class="size-6!">
<CircleAlert class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
@@ -112,7 +112,7 @@ function confirm() {
{{ field.label }}
<ZPopover trigger="hover-click" placement="top-start">
<template #reference="{ open: helpOpen }">
<ZIconButton :aria-label="t('settings.scheduling.about', { label: field.label })" :title="undefined" :aria-expanded="helpOpen" class="size-6! hover:bg-transparent! active:bg-transparent!">
<ZIconButton variant="link" :aria-label="t('settings.scheduling.about', { label: field.label })" :title="undefined" :aria-expanded="helpOpen" class="size-6!">
<CircleHelp class="size-3.5" />
</ZIconButton>
</template>
@@ -138,7 +138,7 @@ function canDelete(record: BackupRecord): boolean {
<span>{{ t('settings.backup.backupRecords') }}</span>
<ZPopover trigger="hover-click" placement="bottom-start">
<template #reference="{ open }">
<ZIconButton :aria-label="t('settings.backup.backupScopeDescription')" :title="undefined" :aria-expanded="open" class="size-6! hover:bg-transparent! active:bg-transparent!">
<ZIconButton variant="link" :aria-label="t('settings.backup.backupScopeDescription')" :title="undefined" :aria-expanded="open" class="size-6!">
<CircleHelp class="size-3.5" aria-hidden="true" />
</ZIconButton>
</template>
@@ -36,9 +36,10 @@ const minCodexCliVersion = defineModel<string>('minCodexCliVersion', { required:
<span class="inline-flex items-center gap-1.5">
<span>{{ t('settings.clientVersion.clientVersionRestrictions') }}</span>
<ZIconButton
variant="link"
:aria-label="t('settings.clientVersion.openGuide')"
:title="undefined"
class="size-6! hover:bg-transparent! active:bg-transparent!"
class="size-6!"
@click="emit('help')"
>
<CircleHelp class="size-3.5" aria-hidden="true" />
@@ -101,6 +101,8 @@ function removeRule(id: string) {
<ZPopover trigger="hover-click" placement="top-start" :hover-delay="240">
<template #reference="{ open }">
<ZIconButton
variant="link"
:title="undefined"
size="small"
:aria-label="t('settings.errorProtection.matchingHelpTitle')"
:aria-expanded="open"
@@ -113,7 +113,7 @@ function remove(rule: PrivacyRule) {
<ZSelect v-model="model.onError" size="small" class="w-34" :options="failureOptions" :aria-label="t('settings.privacy.ruleExecutionFailed')" :disabled="disabled" />
<ZPopover trigger="hover-click" placement="top-end">
<template #reference="{ open: helpOpen }">
<ZIconButton size="small" :aria-label="t('settings.privacy.ruleExecutionInstructions')" :title="undefined" class="hover:bg-transparent! active:bg-transparent!" :aria-expanded="helpOpen">
<ZIconButton variant="link" size="small" :aria-label="t('settings.privacy.ruleExecutionInstructions')" :title="undefined" :aria-expanded="helpOpen">
<CircleHelp class="size-3.5" />
</ZIconButton>
</template>
@@ -62,7 +62,7 @@ watch(() => step.value.action, (action) => {
<template #label-extra>
<ZPopover trigger="hover-click">
<template #reference="{ open: helpOpen }">
<ZIconButton size="small" :aria-label="t('settings.privacy.steps.fieldPathSyntax')" :title="undefined" class="-my-2 hover:bg-transparent! active:bg-transparent!" :aria-expanded="helpOpen">
<ZIconButton variant="link" size="small" :aria-label="t('settings.privacy.steps.fieldPathSyntax')" :title="undefined" class="-my-2" :aria-expanded="helpOpen">
<CircleHelp class="size-3.5" />
</ZIconButton>
</template>
@@ -84,7 +84,7 @@ watch(() => step.value.action, (action) => {
<template #label-extra>
<ZPopover trigger="hover-click">
<template #reference="{ open: helpOpen }">
<ZIconButton size="small" :aria-label="t('settings.privacy.steps.regularSyntaxDescription')" :title="undefined" class="-my-2 hover:bg-transparent! active:bg-transparent!" :aria-expanded="helpOpen">
<ZIconButton variant="link" size="small" :aria-label="t('settings.privacy.steps.regularSyntaxDescription')" :title="undefined" class="-my-2" :aria-expanded="helpOpen">
<CircleHelp class="size-3.5" />
</ZIconButton>
</template>
@@ -114,7 +114,7 @@ function moveWithKeyboard(index: number, offset: -1 | 1) {
</h3>
<ZPopover trigger="hover-click">
<template #reference="{ open: helpOpen }">
<ZIconButton size="small" :aria-label="t('settings.privacy.steps.helpTitle')" :title="undefined" class="size-5! hover:bg-transparent! active:bg-transparent!" :aria-expanded="helpOpen">
<ZIconButton variant="link" size="small" :aria-label="t('settings.privacy.steps.helpTitle')" :title="undefined" class="size-5!" :aria-expanded="helpOpen">
<CircleHelp class="size-3.5" />
</ZIconButton>
</template>
+2 -9
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ZButton, ZConfirmDialog, ZIconButton, ZPageHeader, ZSegmented, ZSelect } from '@codex-proxy/ui'
import { ZButton, ZConfirmDialog, ZIconButton, ZPageHeader, ZSegmented } from '@codex-proxy/ui'
import { Save, Undo2 } from '@lucide/vue'
@@ -133,14 +133,7 @@ watch(section, (value) => {
<ZSegmented
:model-value="section"
:aria-label="t('settings.page.sectionsLabel')"
class="hidden! bg-(--cp-input-bg)! sm:inline-grid!"
:options="sectionOptions"
@update:model-value="switchSection"
/>
<ZSelect
:model-value="section"
:aria-label="t('settings.page.sectionsLabel')"
class="w-full sm:hidden"
class="w-full sm:w-auto"
:options="sectionOptions"
@update:model-value="switchSection"
/>
@@ -162,7 +162,7 @@ function submit() {
<template #footer>
<ZPopover v-model="helpOpen" class="mr-auto self-center" placement="top-start" trigger="hover-click">
<template #reference>
<ZButton variant="text" :aria-expanded="helpOpen" :aria-controls="helpOpen ? helpId : undefined">
<ZButton variant="link" :aria-expanded="helpOpen" :aria-controls="helpOpen ? helpId : undefined">
<template #icon>
<Info class="size-3.5" aria-hidden="true" />
</template>
@@ -76,7 +76,7 @@ const providerOptions = computed(() => props.providers.map(value => ({
<div class="flex items-center gap-1">
<ZPopover v-model="syncInfoOpen" placement="bottom-end" trigger="hover-click">
<template #reference>
<ZIconButton :aria-label="t('settings.pricing.synchronizationInstructions')" size="small" :aria-expanded="syncInfoOpen" :aria-controls="syncInfoOpen ? syncInfoId : undefined">
<ZIconButton variant="link" :title="undefined" :aria-label="t('settings.pricing.synchronizationInstructions')" size="small" :aria-expanded="syncInfoOpen" :aria-controls="syncInfoOpen ? syncInfoId : undefined">
<CircleAlert class="size-3.5" />
</ZIconButton>
</template>
@@ -15,10 +15,10 @@ const infoId = useId()
<ZPopover v-model="infoOpen" placement="bottom-start" trigger="hover-click">
<template #reference>
<ZIconButton
variant="link"
size="small"
:aria-label="t('settings.pricing.unitHelp')"
:title="undefined"
class="hover:bg-transparent! active:bg-transparent!"
:aria-expanded="infoOpen"
:aria-controls="infoOpen ? infoId : undefined"
>