fix(frontend): 用 CSS text-security 替代 password 输入框,简化配额进度条 UI

- Input 组件 masked 模式改用 WebkitTextSecurity: disc 替代 type=password,
  避免浏览器密码管理器自动填充干扰
- LoginDialog/UserFormDialog/Settings/ProxyNodes 密码字段统一迁移到 masked 属性
- PoolManagement 配额进度条布局从 grid 改为 flex,移除未使用的
  getQuotaProgressDisplayClass/getQuotaProgressTooltip 函数

Close #249
Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
fawney19
2026-03-19 20:04:56 +08:00
parent a8620e133a
commit f573110725
6 changed files with 45 additions and 69 deletions

View File

@@ -6,6 +6,7 @@
<input <input
ref="inputRef" ref="inputRef"
:class="inputClass" :class="inputClass"
:style="maskStyle"
:value="modelValue" :value="modelValue"
:type="effectiveType" :type="effectiveType"
:autocomplete="autocompleteAttr" :autocomplete="autocompleteAttr"
@@ -41,6 +42,7 @@
v-else v-else
ref="inputRef" ref="inputRef"
:class="inputClass" :class="inputClass"
:style="maskStyle"
:value="modelValue" :value="modelValue"
:type="effectiveType" :type="effectiveType"
:autocomplete="autocompleteAttr" :autocomplete="autocompleteAttr"
@@ -79,7 +81,7 @@ interface Props {
size?: 'default' | 'sm' size?: 'default' | 'sm'
/** /**
* 遮蔽显示内容(用于 API Key 等敏感信息) * 遮蔽显示内容(用于 API Key 等敏感信息)
* 隐藏态使用真正的 password 输入框,显示态切换为 text * 始终使用 text 输入框,隐藏态通过样式进行遮蔽
* 同时会显示一个小眼睛按钮用于切换显示/隐藏 * 同时会显示一个小眼睛按钮用于切换显示/隐藏
*/ */
masked?: boolean masked?: boolean
@@ -116,11 +118,20 @@ const shouldDisableAutofill = computed(() => {
const effectiveType = computed(() => { const effectiveType = computed(() => {
const attrType = (attrs.type as string | undefined) ?? 'text' const attrType = (attrs.type as string | undefined) ?? 'text'
if (props.masked) { if (props.masked) {
return isVisible.value ? 'text' : 'password' return 'text'
} }
return attrType return attrType
}) })
const maskStyle = computed(() => {
if (!props.masked || isVisible.value) {
return undefined
}
return {
WebkitTextSecurity: 'disc'
}
})
// 过滤掉 type 和 class 属性,因为我们会单独处理 // 过滤掉 type 和 class 属性,因为我们会单独处理
const filteredAttrs = computed(() => { const filteredAttrs = computed(() => {
const { type: _type, class: _class, ...rest } = attrs const { type: _type, class: _class, ...rest } = attrs

View File

@@ -179,7 +179,8 @@
<Input <Input
id="login-password" id="login-password"
v-model="form.password" v-model="form.password"
type="password" type="text"
masked
required required
placeholder="输入密码" placeholder="输入密码"
autocomplete="off" autocomplete="off"

View File

@@ -119,7 +119,8 @@
<Input <Input
:id="`pwd-confirm-${formNonce}`" :id="`pwd-confirm-${formNonce}`"
v-model="form.confirmPassword" v-model="form.confirmPassword"
type="password" type="text"
masked
autocomplete="new-password" autocomplete="new-password"
data-form-type="other" data-form-type="other"
data-lpignore="true" data-lpignore="true"

View File

@@ -514,26 +514,18 @@
<div <div
v-for="(item, idx) in quotaProgressMap[key.key_id].slice(0, 2)" v-for="(item, idx) in quotaProgressMap[key.key_id].slice(0, 2)"
:key="`${key.key_id}-quota-${idx}`" :key="`${key.key_id}-quota-${idx}`"
class="space-y-0.5" class="flex flex-col gap-1 min-w-[140px] max-w-[208px]"
> >
<div class="flex items-center justify-between gap-2 min-w-0"> <div class="flex items-center justify-between text-[10px] leading-none">
<span <span class="text-muted-foreground font-medium shrink-0">{{ getQuotaProgressLabel(item.label) }}</span>
class="shrink-0 text-[10px] leading-none text-muted-foreground tabular-nums whitespace-nowrap"
:title="getQuotaProgressTooltip(item)"
>
{{ getQuotaProgressLabel(item.label) }}
</span>
<span <span
v-if="getQuotaProgressDisplayText(item)" v-if="getQuotaProgressDisplayText(item)"
class="min-w-0 truncate text-[9px] leading-none tabular-nums text-right" class="text-muted-foreground/80 tabular-nums truncate"
:class="getQuotaProgressDisplayClass(item)" :title="item.detail"
:title="getQuotaProgressTooltip(item)" >{{ getQuotaProgressDisplayText(item) }}</span>
>
{{ getQuotaProgressDisplayText(item) }}
</span>
</div> </div>
<div class="grid grid-cols-[minmax(0,1fr)_50px] items-center gap-2"> <div class="flex items-center gap-1.5">
<div class="relative h-1.5 rounded-full bg-border/80 overflow-hidden"> <div class="relative flex-1 h-1.5 rounded-full bg-border overflow-hidden">
<div <div
class="absolute left-0 top-0 h-full rounded-full transition-all duration-300" class="absolute left-0 top-0 h-full rounded-full transition-all duration-300"
:class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)" :class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)"
@@ -541,11 +533,9 @@
/> />
</div> </div>
<span <span
class="text-[11px] font-medium leading-none tabular-nums whitespace-nowrap text-right" class="shrink-0 text-[10px] font-medium tabular-nums leading-none"
:class="getQuotaRemainingClassByRemaining(item.remainingPercent)" :class="getQuotaRemainingClassByRemaining(item.remainingPercent)"
> >{{ item.remainingPercent.toFixed(1) }}%</span>
{{ item.remainingPercent.toFixed(1) }}%
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -979,26 +969,18 @@
<div <div
v-for="(item, idx) in quotaProgressMap[key.key_id]" v-for="(item, idx) in quotaProgressMap[key.key_id]"
:key="`${key.key_id}-quota-mobile-${idx}`" :key="`${key.key_id}-quota-mobile-${idx}`"
class="space-y-0.5" class="flex flex-col gap-1 min-w-0"
> >
<div class="flex items-center justify-between gap-2 min-w-0"> <div class="flex items-center justify-between text-[10px] leading-none">
<span <span class="text-muted-foreground font-medium shrink-0">{{ getQuotaProgressLabel(item.label) }}</span>
class="shrink-0 text-[10px] leading-none text-muted-foreground tabular-nums whitespace-nowrap"
:title="getQuotaProgressTooltip(item)"
>
{{ getQuotaProgressLabel(item.label) }}
</span>
<span <span
v-if="getQuotaProgressDisplayText(item)" v-if="getQuotaProgressDisplayText(item)"
class="min-w-0 truncate text-[9px] leading-none tabular-nums text-right" class="text-muted-foreground/80 tabular-nums truncate"
:class="getQuotaProgressDisplayClass(item)" :title="item.detail"
:title="getQuotaProgressTooltip(item)" >{{ getQuotaProgressDisplayText(item) }}</span>
>
{{ getQuotaProgressDisplayText(item) }}
</span>
</div> </div>
<div class="grid grid-cols-[minmax(0,1fr)_50px] items-center gap-2"> <div class="flex items-center gap-1.5">
<div class="relative h-1.5 rounded-full bg-border/80 overflow-hidden"> <div class="relative flex-1 h-1.5 rounded-full bg-border overflow-hidden">
<div <div
class="absolute left-0 top-0 h-full rounded-full transition-all duration-300" class="absolute left-0 top-0 h-full rounded-full transition-all duration-300"
:class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)" :class="getQuotaRemainingBarColorByRemaining(item.remainingPercent)"
@@ -1006,11 +988,9 @@
/> />
</div> </div>
<span <span
class="text-[11px] font-medium leading-none tabular-nums whitespace-nowrap text-right" class="shrink-0 text-[10px] font-medium tabular-nums leading-none"
:class="getQuotaRemainingClassByRemaining(item.remainingPercent)" :class="getQuotaRemainingClassByRemaining(item.remainingPercent)"
> >{{ item.remainingPercent.toFixed(1) }}%</span>
{{ item.remainingPercent.toFixed(1) }}%
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -2470,31 +2450,10 @@ function formatCompactQuotaCountdownText(text: string): string {
function getQuotaProgressDisplayText(item: QuotaProgressItem): string { function getQuotaProgressDisplayText(item: QuotaProgressItem): string {
const countdownText = getQuotaProgressCountdownText(item) const countdownText = getQuotaProgressCountdownText(item)
if (countdownText) return formatCompactQuotaCountdownText(countdownText) if (countdownText) return formatCompactQuotaCountdownText(countdownText)
// remainingPercent >= 100 时 getCodexResetCountdown 返回 null倒计时为空
// 但 UI 仍需展示"满额未消耗"的占位文本
if ((item.label === '5H' || item.label === '周') && item.remainingPercent >= 100) return '0天 00:00:00'
return item.detail?.trim() || '' return item.detail?.trim() || ''
} }
function getQuotaProgressDisplayClass(item: QuotaProgressItem): string {
const status = getQuotaProgressCountdown(item)
if (!status || status.isExpired) return 'text-muted-foreground/70'
if (status.isCritical) return 'text-destructive font-medium animate-pulse'
if (status.isUrgent) return 'text-amber-500 dark:text-amber-400'
return 'text-muted-foreground/70'
}
function getQuotaProgressTooltip(item: QuotaProgressItem): string {
if ((item.label === '5H' || item.label === '周') && item.remainingPercent >= 100) {
return ''
}
const detail = item.detail?.trim() || ''
const countdownText = getQuotaProgressCountdownText(item)
if (countdownText) {
return countdownText
}
return detail
}
function getQuotaLabelOrder(label: string): number { function getQuotaLabelOrder(label: string): number {
if (label === '5H') return 0 if (label === '5H') return 0

View File

@@ -466,7 +466,8 @@
<Label>密码</Label> <Label>密码</Label>
<Input <Input
v-model="addForm.password" v-model="addForm.password"
type="password" type="text"
masked
placeholder="可选" placeholder="可选"
autocomplete="new-password" autocomplete="new-password"
data-form-type="other" data-form-type="other"

View File

@@ -107,7 +107,8 @@
<Input <Input
id="old-password" id="old-password"
v-model="passwordForm.old_password" v-model="passwordForm.old_password"
type="password" type="text"
masked
class="mt-1" class="mt-1"
/> />
</div> </div>
@@ -116,7 +117,8 @@
<Input <Input
id="new-password" id="new-password"
v-model="passwordForm.new_password" v-model="passwordForm.new_password"
type="password" type="text"
masked
:placeholder="getPasswordPolicyPlaceholder(passwordPolicyLevel)" :placeholder="getPasswordPolicyPlaceholder(passwordPolicyLevel)"
class="mt-1" class="mt-1"
/> />
@@ -138,7 +140,8 @@
<Input <Input
id="confirm-password" id="confirm-password"
v-model="passwordForm.confirm_password" v-model="passwordForm.confirm_password"
type="password" type="text"
masked
placeholder="再次输入密码" placeholder="再次输入密码"
class="mt-1" class="mt-1"
/> />