b60c003ec2
完善 2026 主题的移动端与桌面端交互体验,修复多项可访问性和信息展示问题。 - 允许浏览器缩放,补充焦点样式、按钮名称、表单标签和对话框键盘焦点管理\n- 支持 reduced-motion,扩大公共页、后台导航、记录列表和分页控件的触控区域\n- 优化系统设置分区导航、密码显示、字段语义和底部粘性保存栏\n- 压缩移动文件列表操作区,改善桌面表格滚动可达性和统计值截断\n- 优化仪表盘指标、趋势摘要、进度条语义及中英文文案
107 lines
3.5 KiB
Vue
107 lines
3.5 KiB
Vue
<template>
|
|
<form @submit.prevent="$emit('submit')">
|
|
<div class="mb-6 relative">
|
|
<label
|
|
for="code"
|
|
class="block text-sm font-medium mb-2"
|
|
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
|
>{{ t('retrieve.codeInput.label') }}</label
|
|
>
|
|
<div class="relative">
|
|
<input
|
|
id="code"
|
|
v-model="code"
|
|
type="text"
|
|
inputmode="text"
|
|
autocomplete="one-time-code"
|
|
ref="codeInput"
|
|
class="w-full px-4 py-3 rounded-lg placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition duration-300 pr-10"
|
|
:class="[
|
|
isDarkMode ? 'bg-gray-700 bg-opacity-50' : 'bg-gray-100',
|
|
{ 'ring-2 ring-red-500': error },
|
|
isDarkMode ? 'text-gray-300' : 'text-gray-800'
|
|
]"
|
|
:placeholder="t('retrieve.codeInput.placeholder')"
|
|
required
|
|
:aria-invalid="error"
|
|
:readonly="inputStatus.readonly"
|
|
maxlength="5"
|
|
@focus="isInputFocused = true"
|
|
@blur="isInputFocused = false"
|
|
/>
|
|
<div
|
|
v-if="inputStatus.loading"
|
|
class="absolute inset-y-0 right-0 flex items-center pr-3"
|
|
>
|
|
<span
|
|
class="animate-spin rounded-full h-5 w-5 border-b-2 border-indigo-500"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="absolute -bottom-0.5 left-2 h-0.5 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 transition-all duration-300 ease-in-out"
|
|
:class="{ 'w-97-100': isInputFocused, 'w-0': !isInputFocused }"
|
|
></div>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
class="group relative w-full overflow-hidden rounded-lg bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 px-4 py-3 font-bold text-white transition duration-300 hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 disabled:cursor-not-allowed disabled:opacity-50 motion-safe:hover:scale-[1.02] disabled:hover:scale-100"
|
|
:disabled="inputStatus.loading || code.trim().length !== 5"
|
|
>
|
|
<span class="flex items-center justify-center relative z-10">
|
|
<span>{{ inputStatus.loading ? t('common.loading') : t('retrieve.submit') }}</span>
|
|
<ArrowRightIcon
|
|
class="w-5 h-5 ml-2 transition-transform duration-300 transform group-hover:translate-x-1"
|
|
/>
|
|
</span>
|
|
<span
|
|
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
|
></span>
|
|
</button>
|
|
</form>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, inject, computed } from 'vue'
|
|
import { ArrowRightIcon } from 'lucide-vue-next'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
const { t } = useI18n()
|
|
|
|
interface InputStatus {
|
|
readonly: boolean
|
|
loading: boolean
|
|
}
|
|
|
|
interface Props {
|
|
inputStatus: InputStatus
|
|
error?: boolean
|
|
modelValue: string
|
|
}
|
|
|
|
interface Emits {
|
|
submit: []
|
|
'update:modelValue': [value: string]
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const emit = defineEmits<Emits>()
|
|
|
|
const isDarkMode = inject('isDarkMode')
|
|
const code = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => emit('update:modelValue', value)
|
|
})
|
|
const isInputFocused = ref(false)
|
|
const codeInput = ref<HTMLInputElement>()
|
|
|
|
defineExpose({
|
|
focus: () => codeInput.value?.focus()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.w-97-100 {
|
|
width: calc(100% - 1rem);
|
|
}
|
|
</style> |