mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-05-06 14:03:40 +08:00
enhance: polish sensitive value masking UI
This commit is contained in:
@@ -3,44 +3,33 @@ import settings, { PROTECTED_VALUE_PLACEHOLDER } from '@/api/settings'
|
||||
import { use2FAModal } from '@/components/TwoFA'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
path: string
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
const model = defineModel<string>({ required: true })
|
||||
|
||||
const { message } = useGlobalApp()
|
||||
const twoFAModal = use2FAModal()
|
||||
const inputValue = ref(props.modelValue)
|
||||
const revealedValue = ref('')
|
||||
const isDirty = ref(false)
|
||||
const show = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
watch(() => props.modelValue, value => {
|
||||
watch(model, value => {
|
||||
if (value === PROTECTED_VALUE_PLACEHOLDER) {
|
||||
inputValue.value = value
|
||||
revealedValue.value = ''
|
||||
isDirty.value = false
|
||||
show.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!isDirty.value)
|
||||
inputValue.value = value
|
||||
})
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (isDirty.value)
|
||||
return inputValue.value
|
||||
|
||||
if (show.value)
|
||||
return revealedValue.value
|
||||
return revealedValue.value || model.value
|
||||
|
||||
return props.modelValue
|
||||
if (model.value === PROTECTED_VALUE_PLACEHOLDER)
|
||||
return 'Sensitive value hidden'
|
||||
|
||||
return model.value
|
||||
})
|
||||
|
||||
async function ensureRevealedValue() {
|
||||
@@ -52,6 +41,7 @@ async function ensureRevealedValue() {
|
||||
await twoFAModal.open()
|
||||
const { value } = await settings.get_protected_value(props.path)
|
||||
revealedValue.value = value
|
||||
model.value = value
|
||||
return value
|
||||
}
|
||||
finally {
|
||||
@@ -60,41 +50,85 @@ async function ensureRevealedValue() {
|
||||
}
|
||||
|
||||
async function toggleShow() {
|
||||
if (!show.value && !isDirty.value)
|
||||
if (!show.value)
|
||||
await ensureRevealedValue()
|
||||
|
||||
show.value = !show.value
|
||||
}
|
||||
|
||||
async function copyValue() {
|
||||
const value = isDirty.value ? inputValue.value : await ensureRevealedValue()
|
||||
const value = show.value ? model.value : await ensureRevealedValue()
|
||||
await navigator.clipboard.writeText(value)
|
||||
message.success($gettext('Copied'))
|
||||
}
|
||||
|
||||
function updateValue(value: string) {
|
||||
inputValue.value = value
|
||||
isDirty.value = true
|
||||
emit('update:modelValue', value)
|
||||
if (!show.value)
|
||||
return
|
||||
|
||||
model.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AInput
|
||||
:value="displayValue"
|
||||
:type="show ? 'text' : 'password'"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
>
|
||||
<template #suffix>
|
||||
<ASpace size="small">
|
||||
<a @click.prevent="copyValue">
|
||||
{{ $gettext('Copy') }}
|
||||
</a>
|
||||
<a @click.prevent="toggleShow">
|
||||
{{ show ? $gettext('Hide') : isLoading ? $gettext('Loading...') : $gettext('Show') }}
|
||||
</a>
|
||||
</ASpace>
|
||||
</template>
|
||||
</AInput>
|
||||
<div class="sensitive-input-shell" :class="{ 'is-protected': !show }">
|
||||
<AInput
|
||||
:value="displayValue"
|
||||
:readonly="!show"
|
||||
:type="show ? 'text' : 'text'"
|
||||
:placeholder="placeholder"
|
||||
@update:value="updateValue"
|
||||
>
|
||||
<template #suffix>
|
||||
<ASpace size="small">
|
||||
<a @click.prevent="copyValue">
|
||||
{{ $gettext('Copy') }}
|
||||
</a>
|
||||
<a @click.prevent="toggleShow">
|
||||
{{ show ? $gettext('Hide') : isLoading ? $gettext('Loading...') : $gettext('Show') }}
|
||||
</a>
|
||||
</ASpace>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.sensitive-input-shell {
|
||||
position: relative;
|
||||
transition: filter 0.2s ease;
|
||||
border-radius: 10px;
|
||||
|
||||
&.is-protected {
|
||||
:deep(.ant-input) {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 8px rgba(15, 23, 42, 0.72);
|
||||
user-select: none;
|
||||
cursor: not-allowed;
|
||||
caret-color: transparent;
|
||||
}
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
background: rgba(148, 163, 184, 0.10);
|
||||
border-color: rgba(148, 163, 184, 0.32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark .sensitive-input-shell {
|
||||
&.is-protected {
|
||||
:deep(.ant-input) {
|
||||
text-shadow: 0 0 8px rgba(226, 232, 240, 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sensitive-input-shell {
|
||||
&.is-protected {
|
||||
:deep(.ant-input) {
|
||||
text-shadow: 0 0 8px rgba(226, 232, 240, 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import settings from '@/api/settings'
|
||||
import settings, { PROTECTED_VALUE_PLACEHOLDER } from '@/api/settings'
|
||||
import { use2FAModal } from '@/components/TwoFA'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -42,11 +42,21 @@ async function ensureRevealedValue() {
|
||||
}
|
||||
}
|
||||
|
||||
const displayString = computed(() => {
|
||||
if (show.value)
|
||||
return revealedValue.value
|
||||
|
||||
if (!revealedValue.value && props.value === PROTECTED_VALUE_PLACEHOLDER)
|
||||
return 'Sensitive value hidden'
|
||||
|
||||
return revealedValue.value || props.value
|
||||
})
|
||||
|
||||
const maskedString = computed(() => {
|
||||
if (show.value)
|
||||
return revealedValue.value
|
||||
|
||||
return maskText(revealedValue.value || props.value)
|
||||
return maskText(displayString.value)
|
||||
})
|
||||
|
||||
async function toggleShow() {
|
||||
@@ -63,8 +73,26 @@ async function copyValue() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<span class="mr-2">{{ maskedString }}</span>
|
||||
<div class="sensitive-row">
|
||||
<span
|
||||
class="mr-2 sensitive-value"
|
||||
:class="{ 'is-protected': !show }"
|
||||
>
|
||||
<span
|
||||
v-if="show"
|
||||
class="sensitive-value__text"
|
||||
>
|
||||
{{ displayString }}
|
||||
</span>
|
||||
<template v-else>
|
||||
<span class="sensitive-value__text sensitive-value__text--sizer">
|
||||
{{ maskedString }}
|
||||
</span>
|
||||
<span class="sensitive-value__blurred" aria-hidden="true">
|
||||
{{ maskedString }}
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
<a
|
||||
class="mr-2"
|
||||
@click="copyValue"
|
||||
@@ -76,5 +104,95 @@ async function copyValue() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.sensitive-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sensitive-value {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
max-width: min(100%, 420px);
|
||||
width: min(100%, 420px);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background: rgba(148, 163, 184, 0.06);
|
||||
border: 1px solid rgba(148, 163, 184, 0.20);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
|
||||
&__text {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
font-family: ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: filter 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&__text--sizer {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&__blurred {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 6px 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 8px rgba(15, 23, 42, 0.72);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.is-protected {
|
||||
background: rgba(148, 163, 184, 0.11);
|
||||
border-color: rgba(148, 163, 184, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.dark .sensitive-value {
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border-color: rgba(148, 163, 184, 0.16);
|
||||
|
||||
&.is-protected {
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border-color: rgba(148, 163, 184, 0.20);
|
||||
}
|
||||
|
||||
.sensitive-value__blurred {
|
||||
text-shadow: 0 0 8px rgba(226, 232, 240, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sensitive-value {
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border-color: rgba(148, 163, 184, 0.16);
|
||||
|
||||
&.is-protected {
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border-color: rgba(148, 163, 184, 0.20);
|
||||
}
|
||||
}
|
||||
|
||||
.sensitive-value__blurred {
|
||||
text-shadow: 0 0 8px rgba(226, 232, 240, 0.75);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user