Files
it/frontend/src/components/SecureInput.vue
XCool f25b0307db
Some checks failed
CI/CD Pipeline / 测试 (18.x) (push) Has been cancelled
CI/CD Pipeline / 测试 (20.x) (push) Has been cancelled
CI/CD Pipeline / 安全检查 (push) Has been cancelled
CI/CD Pipeline / 部署 (push) Has been cancelled
CI/CD Pipeline / 通知 (push) Has been cancelled
初始化
2025-11-03 19:47:36 +08:00

442 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="secure-input" :class="{ 'has-error': error }">
<label v-if="label" class="secure-input__label" :for="inputId">
{{ label }}
<span v-if="required" class="secure-input__required">*</span>
</label>
<div class="secure-input__wrapper">
<input
:id="inputId"
ref="inputRef"
v-model="inputValue"
:type="showPassword ? 'text' : type"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:maxlength="maxLength"
:autocomplete="autocomplete"
:class="inputClasses"
@input="handleInput"
@blur="handleBlur"
@focus="handleFocus"
@keyup.enter="handleEnter"
/>
<button
v-if="type === 'password' && showToggle"
type="button"
class="secure-input__toggle"
@click="togglePasswordVisibility"
:aria-label="showPassword ? '隐藏密码' : '显示密码'"
>
<el-icon v-if="showPassword"><Hide /></el-icon>
<el-icon v-else><View /></el-icon>
</button>
<button
v-if="clearable && inputValue && !disabled && !readonly"
type="button"
class="secure-input__clear"
@click="clearInput"
aria-label="清除输入"
>
<el-icon><Close /></el-icon>
</button>
<div v-if="strengthIndicator && type === 'password'" class="secure-input__strength">
<div class="secure-input__strength-bar">
<div
class="secure-input__strength-fill"
:class="passwordStrengthClass"
:style="{ width: `${passwordStrengthPercentage}%` }"
></div>
</div>
<span class="secure-input__strength-text">{{ passwordStrengthText }}</span>
</div>
</div>
<div v-if="error" class="secure-input__error">{{ error }}</div>
<div v-else-if="hint" class="secure-input__hint">{{ hint }}</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { View, Hide, Close } from '@element-plus/icons-vue'
import { validatePassword, sanitizeInput } from '@/utils/security'
import { ElMessage } from 'element-plus'
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
},
type: {
type: String,
default: 'text',
validator: (value) => ['text', 'password', 'email', 'tel', 'url', 'number'].includes(value)
},
label: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
required: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
readonly: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: false
},
showToggle: {
type: Boolean,
default: true
},
strengthIndicator: {
type: Boolean,
default: false
},
maxLength: {
type: Number,
default: 255
},
autocomplete: {
type: String,
default: 'off'
},
validateOnBlur: {
type: Boolean,
default: true
},
sanitize: {
type: Boolean,
default: true
},
error: {
type: String,
default: ''
},
hint: {
type: String,
default: ''
},
size: {
type: String,
default: 'default',
validator: (value) => ['small', 'default', 'large'].includes(value)
}
})
const emit = defineEmits(['update:modelValue', 'input', 'blur', 'focus', 'enter', 'clear', 'strength-change'])
const inputRef = ref(null)
const inputValue = ref(props.modelValue)
const showPassword = ref(false)
const isFocused = ref(false)
const passwordStrength = ref(0)
// 生成唯一ID
const inputId = computed(() => `secure-input-${Math.random().toString(36).substring(2, 9)}`)
// 计算输入框样式类
const inputClasses = computed(() => {
return [
'secure-input__field',
`secure-input__field--${props.size}`,
{
'secure-input__field--focused': isFocused.value,
'secure-input__field--disabled': props.disabled,
'secure-input__field--readonly': props.readonly,
'secure-input__field--has-error': props.error,
'secure-input__field--password': props.type === 'password'
}
]
})
// 计算密码强度类
const passwordStrengthClass = computed(() => {
if (passwordStrength.value <= 2) return 'weak'
if (passwordStrength.value <= 3) return 'medium'
return 'strong'
})
// 计算密码强度百分比
const passwordStrengthPercentage = computed(() => {
return Math.min(passwordStrength.value * 20, 100)
})
// 计算密码强度文本
const passwordStrengthText = computed(() => {
if (passwordStrength.value <= 2) return '弱'
if (passwordStrength.value <= 3) return '中等'
return '强'
})
// 监听modelValue变化
watch(() => props.modelValue, (newVal) => {
inputValue.value = newVal
})
// 监听输入值变化
watch(inputValue, (newVal) => {
// 如果启用了输入清理,则清理输入
const processedValue = props.sanitize ? sanitizeInput(newVal) : newVal
// 更新密码强度仅当类型为password时
if (props.type === 'password' && props.strengthIndicator) {
updatePasswordStrength(processedValue)
}
// 触发更新事件
emit('update:modelValue', processedValue)
emit('input', processedValue)
})
// 更新密码强度
const updatePasswordStrength = (password) => {
if (!password) {
passwordStrength.value = 0
return
}
try {
const result = validatePassword(password)
passwordStrength.value = result.score
emit('strength-change', result)
} catch (error) {
console.error('密码强度验证失败:', error)
passwordStrength.value = 0
}
}
// 处理输入事件
const handleInput = (event) => {
// 输入事件已在watch中处理
}
// 处理焦点事件
const handleFocus = (event) => {
isFocused.value = true
emit('focus', event)
}
// 处理失焦事件
const handleBlur = (event) => {
isFocused.value = false
emit('blur', event)
}
// 处理回车事件
const handleEnter = (event) => {
emit('enter', event)
}
// 切换密码可见性
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value
}
// 清除输入
const clearInput = () => {
inputValue.value = ''
emit('clear')
nextTick(() => {
inputRef.value?.focus()
})
}
// 聚焦输入框
const focus = () => {
inputRef.value?.focus()
}
// 失焦输入框
const blur = () => {
inputRef.value?.blur()
}
// 选择文本
const select = () => {
inputRef.value?.select()
}
// 暴露方法
defineExpose({
focus,
blur,
select,
inputRef
})
onMounted(() => {
// 初始化密码强度
if (props.type === 'password' && props.strengthIndicator && inputValue.value) {
updatePasswordStrength(inputValue.value)
}
})
</script>
<style scoped>
.secure-input {
width: 100%;
margin-bottom: 16px;
}
.secure-input__label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.secure-input__required {
color: #f56c6c;
margin-left: 4px;
}
.secure-input__wrapper {
position: relative;
width: 100%;
}
.secure-input__field {
width: 100%;
padding: 12px 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #606266;
background-color: #fff;
transition: border-color 0.2s;
outline: none;
box-sizing: border-box;
}
.secure-input__field:focus {
border-color: #409eff;
}
.secure-input__field--small {
padding: 8px 12px;
font-size: 12px;
}
.secure-input__field--large {
padding: 16px 20px;
font-size: 16px;
}
.secure-input__field--disabled {
background-color: #f5f7fa;
color: #c0c4cc;
cursor: not-allowed;
}
.secure-input__field--readonly {
background-color: #f5f7fa;
color: #606266;
cursor: default;
}
.secure-input__field--has-error {
border-color: #f56c6c;
}
.secure-input__field--password {
padding-right: 80px;
}
.secure-input__toggle,
.secure-input__clear {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: #909399;
font-size: 16px;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
}
.secure-input__toggle:hover,
.secure-input__clear:hover {
color: #409eff;
}
.secure-input__toggle {
right: 12px;
}
.secure-input__clear {
right: 40px;
}
.secure-input__strength {
margin-top: 8px;
}
.secure-input__strength-bar {
height: 4px;
background-color: #e4e7ed;
border-radius: 2px;
overflow: hidden;
}
.secure-input__strength-fill {
height: 100%;
transition: width 0.3s, background-color 0.3s;
}
.secure-input__strength-fill.weak {
background-color: #f56c6c;
}
.secure-input__strength-fill.medium {
background-color: #e6a23c;
}
.secure-input__strength-fill.strong {
background-color: #67c23a;
}
.secure-input__strength-text {
font-size: 12px;
color: #909399;
margin-top: 4px;
display: block;
}
.secure-input__error {
color: #f56c6c;
font-size: 12px;
margin-top: 4px;
}
.secure-input__hint {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
.secure-input.has-error .secure-input__field {
border-color: #f56c6c;
}
</style>