|
|
<template> |
|
|
<div v-if="visible" class="password-modal-overlay" @click="handleOverlayClick"> |
|
|
<div class="password-modal" @click.stop> |
|
|
<div class="modal-header"> |
|
|
<h3>{{ title }}</h3> |
|
|
<button class="close-btn" @click="handleClose">×</button> |
|
|
</div> |
|
|
|
|
|
<div class="modal-body"> |
|
|
<form @submit.prevent="handleSubmit"> |
|
|
<!-- 当前密码 --> |
|
|
<div class="form-group" v-if="!isForceChange"> |
|
|
<label>当前密码 *</label> |
|
|
<input |
|
|
v-model="form.currentPassword" |
|
|
type="password" |
|
|
required |
|
|
placeholder="请输入当前密码" |
|
|
:class="{ 'error': errors.currentPassword }" |
|
|
/> |
|
|
<div v-if="errors.currentPassword" class="error-message"> |
|
|
{{ errors.currentPassword }} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- 新密码 --> |
|
|
<div class="form-group"> |
|
|
<label>新密码 *</label> |
|
|
<input |
|
|
v-model="form.newPassword" |
|
|
type="password" |
|
|
required |
|
|
placeholder="请输入新密码" |
|
|
@input="validatePassword" |
|
|
@keyup="updateRequirements(form.newPassword)" |
|
|
:class="{ 'error': errors.newPassword }" |
|
|
/> |
|
|
<div v-if="errors.newPassword" class="error-message"> |
|
|
{{ errors.newPassword }} |
|
|
</div> |
|
|
|
|
|
<!-- 密码强度指示器 --> |
|
|
<div class="password-strength" > |
|
|
<div class="strength-bar"> |
|
|
<div |
|
|
class="strength-fill" |
|
|
:class="strengthClass" |
|
|
:style="{ width: strengthPercentage + '%' }" |
|
|
></div> |
|
|
</div> |
|
|
<div class="strength-text"> |
|
|
强度: {{ strengthText }} ({{ passwordLevel }}级) |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- 密码要求提示 --> |
|
|
<div class="password-requirements" > |
|
|
<div class="requirement" :class="{ 'met': requirements.length }"> |
|
|
✓ 密码长度{{ form.newPassword.length }}位 |
|
|
</div> |
|
|
<div class="requirement" :class="{ 'met': requirements.uppercase }"> |
|
|
✓ 包含大写字母 |
|
|
</div> |
|
|
<div class="requirement" :class="{ 'met': requirements.lowercase }"> |
|
|
✓ 包含小写字母 |
|
|
</div> |
|
|
<div class="requirement" :class="{ 'met': requirements.numbers }"> |
|
|
✓ 包含数字 |
|
|
</div> |
|
|
<div class="requirement" :class="{ 'met': requirements.special }"> |
|
|
✓ 包含特殊字符 |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- 确认新密码 --> |
|
|
<div class="form-group"> |
|
|
<label>确认新密码 *</label> |
|
|
<input |
|
|
v-model="form.confirmPassword" |
|
|
type="password" |
|
|
required |
|
|
placeholder="请再次输入新密码" |
|
|
@input="validateConfirmPassword" |
|
|
:class="{ 'error': errors.confirmPassword }" |
|
|
/> |
|
|
<div v-if="errors.confirmPassword" class="error-message"> |
|
|
{{ errors.confirmPassword }} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- 操作按钮 --> |
|
|
<div class="form-actions"> |
|
|
<button |
|
|
type="button" |
|
|
class="btn btn-secondary" |
|
|
@click="handleClose" |
|
|
:disabled="loading" |
|
|
> |
|
|
取消 |
|
|
</button> |
|
|
<button |
|
|
type="submit" |
|
|
class="btn btn-primary" |
|
|
:disabled="loading || !isFormValid" |
|
|
> |
|
|
<span v-if="loading" class="loading-spinner"></span> |
|
|
{{ submitText }} |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<!-- 强制修改密码提示 --> |
|
|
<div v-if="isForceChange" class="force-change-notice"> |
|
|
<div class="notice-icon">⚠️</div> |
|
|
<div class="notice-text"> |
|
|
<strong>重要提示:</strong>您的密码已被重置,必须立即修改密码才能继续使用系统。 |
|
|
</div> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
|
|
|
<script> |
|
|
import { ref, reactive, computed, watch, onMounted } from 'vue' |
|
|
import { userService } from '@/services/userService' |
|
|
|
|
|
export default { |
|
|
name: 'PasswordChangeModal', |
|
|
props: { |
|
|
visible: { |
|
|
type: Boolean, |
|
|
default: false |
|
|
}, |
|
|
isForceChange: { |
|
|
type: Boolean, |
|
|
default: false |
|
|
} |
|
|
}, |
|
|
emits: ['close', 'success'], |
|
|
setup(props, { emit }) { |
|
|
const loading = ref(false) |
|
|
const policy = ref({ |
|
|
minLength: 6, |
|
|
minCharTypes: 1, |
|
|
preventReuse: 3, |
|
|
level: 1, |
|
|
minRequiredLevel: 1, |
|
|
requireUppercase: false, |
|
|
requireLowercase: false, |
|
|
requireNumbers: false, |
|
|
requireSpecial: false |
|
|
}) |
|
|
|
|
|
const form = reactive({ |
|
|
currentPassword: '', |
|
|
newPassword: '', |
|
|
confirmPassword: '' |
|
|
}) |
|
|
|
|
|
const errors = reactive({ |
|
|
currentPassword: '', |
|
|
newPassword: '', |
|
|
confirmPassword: '' |
|
|
}) |
|
|
|
|
|
const requirements = reactive({ |
|
|
length: false, |
|
|
uppercase: false, |
|
|
lowercase: false, |
|
|
numbers: false, |
|
|
special: false |
|
|
}) |
|
|
|
|
|
const passwordStrength = ref(0) |
|
|
const passwordLevel = ref(0) |
|
|
|
|
|
// 计算属性 |
|
|
const title = computed(() => { |
|
|
return props.isForceChange ? '强制修改密码' : '修改密码' |
|
|
}) |
|
|
|
|
|
const submitText = computed(() => { |
|
|
return props.isForceChange ? '确认修改' : '修改密码' |
|
|
}) |
|
|
|
|
|
const strengthClass = computed(() => { |
|
|
if (passwordStrength.value >= 4) return 'strong' |
|
|
if (passwordStrength.value >= 3) return 'medium' |
|
|
if (passwordStrength.value >= 2) return 'weak' |
|
|
return 'very-weak' |
|
|
}) |
|
|
|
|
|
const strengthText = computed(() => { |
|
|
if (passwordStrength.value >= 4) return '强' |
|
|
if (passwordStrength.value >= 3) return '中' |
|
|
if (passwordStrength.value >= 2) return '弱' |
|
|
return '很弱' |
|
|
}) |
|
|
|
|
|
const strengthPercentage = computed(() => { |
|
|
// 将0-5的等级转换为0-100的百分比显示 |
|
|
return (passwordStrength.value / 5) * 100 |
|
|
}) |
|
|
|
|
|
const isFormValid = computed(() => { |
|
|
if (props.isForceChange) { |
|
|
return form.newPassword && form.confirmPassword && |
|
|
form.newPassword === form.confirmPassword && |
|
|
!errors.newPassword && !errors.confirmPassword |
|
|
} |
|
|
return form.currentPassword && form.newPassword && form.confirmPassword && |
|
|
form.newPassword === form.confirmPassword && |
|
|
!errors.currentPassword && !errors.newPassword && !errors.confirmPassword |
|
|
}) |
|
|
|
|
|
// 方法 |
|
|
const loadPasswordPolicy = async () => { |
|
|
try { |
|
|
console.log('开始加载密码策略...') |
|
|
const response = await userService.getPasswordPolicy() |
|
|
console.log('密码策略API响应:', response) |
|
|
console.log('响应类型:', typeof response) |
|
|
console.log('response.data 类型:', typeof response.data) |
|
|
console.log('response.data 内容:', response.data) |
|
|
|
|
|
// 修复:直接检查response.data的结构 |
|
|
if (response.data && response.data.code === 200 && response.data.data) { |
|
|
policy.value = response.data.data |
|
|
console.log('密码策略加载成功:', policy.value) |
|
|
} else if (response && response.code === 200 && response.data) { |
|
|
// 备用检查:如果response.data不存在,直接检查response |
|
|
policy.value = response.data |
|
|
console.log('密码策略加载成功(备用路径):', policy.value) |
|
|
} else { |
|
|
console.warn('密码策略响应格式不正确:', response) |
|
|
console.warn('尝试解析响应结构...') |
|
|
if (response.data) { |
|
|
console.warn('response.data.code:', response.data.code) |
|
|
console.warn('response.data.data:', response.data.data) |
|
|
} |
|
|
if (response) { |
|
|
console.warn('response.code:', response.code) |
|
|
console.warn('response.data:', response.data) |
|
|
} |
|
|
// 使用默认策略作为后备 |
|
|
policy.value = { |
|
|
minLength: 6, |
|
|
minCharTypes: 1, |
|
|
preventReuse: 3, |
|
|
level: 1, |
|
|
minRequiredLevel: 1, |
|
|
requireUppercase: false, |
|
|
requireLowercase: false, |
|
|
requireNumbers: false, |
|
|
requireSpecial: false |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('加载密码策略失败:', error) |
|
|
// 使用默认策略作为后备 |
|
|
policy.value = { |
|
|
minLength: 6, |
|
|
minCharTypes: 1, |
|
|
preventReuse: 3, |
|
|
level: 1, |
|
|
minRequiredLevel: 1, |
|
|
requireUppercase: false, |
|
|
requireLowercase: false, |
|
|
requireNumbers: false, |
|
|
requireSpecial: false |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const validatePassword = async () => { |
|
|
errors.newPassword = '' |
|
|
|
|
|
if (!form.newPassword) { |
|
|
// 如果密码为空,重置所有要求状态 |
|
|
updateRequirements('') |
|
|
return |
|
|
} |
|
|
|
|
|
try { |
|
|
console.log('正在验证密码:', form.newPassword) |
|
|
const response = await userService.validatePassword(form.newPassword) |
|
|
console.log('后端验证响应:', response) |
|
|
|
|
|
// 修复:检查多种可能的响应结构 |
|
|
let result = null |
|
|
if (response.data && response.data.code === 200 && response.data.data) { |
|
|
result = response.data.data |
|
|
} else if (response && response.code === 200 && response.data) { |
|
|
result = response.data |
|
|
} |
|
|
|
|
|
if (result) { |
|
|
console.log('验证结果:', result) |
|
|
|
|
|
if (!result.is_valid) { |
|
|
// 确保错误信息是数组格式 |
|
|
if (Array.isArray(result.errors)) { |
|
|
errors.newPassword = result.errors.join('; ') |
|
|
} else if (typeof result.errors === 'string') { |
|
|
errors.newPassword = result.errors |
|
|
} else { |
|
|
errors.newPassword = '密码验证失败' |
|
|
} |
|
|
} |
|
|
|
|
|
passwordStrength.value = result.strength || 0 |
|
|
passwordLevel.value = result.level || 0 |
|
|
|
|
|
console.log('设置密码强度:', passwordStrength.value, '等级:', passwordLevel.value) |
|
|
|
|
|
// 更新要求满足状态 |
|
|
updateRequirements(form.newPassword) |
|
|
} else { |
|
|
console.warn('密码验证响应格式不正确:', response) |
|
|
// 即使验证失败,也要更新要求状态 |
|
|
updateRequirements(form.newPassword) |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('密码验证失败:', error) |
|
|
// 即使验证失败,也要更新要求状态 |
|
|
updateRequirements(form.newPassword) |
|
|
} |
|
|
} |
|
|
|
|
|
const updateRequirements = (password) => { |
|
|
console.log('updateRequirements 被调用,密码:', password) |
|
|
|
|
|
// 重置所有状态 |
|
|
requirements.length = false |
|
|
requirements.uppercase = false |
|
|
requirements.lowercase = false |
|
|
requirements.numbers = false |
|
|
requirements.special = false |
|
|
|
|
|
// 如果密码为空,直接返回 |
|
|
if (!password || password.length === 0) { |
|
|
return |
|
|
} |
|
|
|
|
|
let hasUppercase = false |
|
|
let hasLowercase = false |
|
|
let hasNumbers = false |
|
|
let hasSpecial = false |
|
|
|
|
|
for (const char of password) { |
|
|
// 使用更准确的字符类型检测 |
|
|
const charCode = char.charCodeAt(0) |
|
|
|
|
|
if (charCode >= 65 && charCode <= 90) { // A-Z |
|
|
hasUppercase = true |
|
|
} else if (charCode >= 97 && charCode <= 122) { // a-z |
|
|
hasLowercase = true |
|
|
} else if (charCode >= 48 && charCode <= 57) { // 0-9 |
|
|
hasNumbers = true |
|
|
} else { |
|
|
// 特殊字符:包括标点符号、符号、空格等 |
|
|
hasSpecial = true |
|
|
} |
|
|
} |
|
|
|
|
|
console.log('字符类型检测结果:', { |
|
|
hasUppercase: hasUppercase, |
|
|
hasLowercase: hasLowercase, |
|
|
hasNumbers: hasNumbers, |
|
|
hasSpecial: hasSpecial, |
|
|
length: password.length |
|
|
}) |
|
|
|
|
|
// 更新要求状态 |
|
|
requirements.length = password.length >= 6 // 要求至少6位 |
|
|
requirements.uppercase = hasUppercase |
|
|
requirements.lowercase = hasLowercase |
|
|
requirements.numbers = hasNumbers |
|
|
requirements.special = hasSpecial |
|
|
|
|
|
// 计算字符类型数量 |
|
|
let charTypes = 0 |
|
|
if (hasUppercase) charTypes++ |
|
|
if (hasLowercase) charTypes++ |
|
|
if (hasNumbers) charTypes++ |
|
|
if (hasSpecial) charTypes++ |
|
|
|
|
|
// 计算密码强度 |
|
|
const strength = calculatePasswordStrength(password, charTypes) |
|
|
passwordStrength.value = strength |
|
|
passwordLevel.value = strength |
|
|
|
|
|
console.log('更新后的要求状态:', { |
|
|
length: requirements.length, |
|
|
uppercase: requirements.uppercase, |
|
|
lowercase: requirements.lowercase, |
|
|
numbers: requirements.numbers, |
|
|
special: requirements.special |
|
|
}) |
|
|
console.log('计算出的密码强度:', strength) |
|
|
} |
|
|
|
|
|
// 前端密码强度计算方法 |
|
|
const calculatePasswordStrength = (password, charTypes) => { |
|
|
// 从最高等级开始判断,一旦满足条件就返回对应等级 |
|
|
|
|
|
// 5级:长度>=8,字符类型>=4 |
|
|
if (password.length >= 8 && charTypes >= 4) { |
|
|
return 5 |
|
|
} |
|
|
|
|
|
// 4级:长度>=8,字符类型>=3 |
|
|
if (password.length >= 8 && charTypes >= 3) { |
|
|
return 4 |
|
|
} |
|
|
|
|
|
// 3级:长度>=6,字符类型>=3 |
|
|
if (password.length >= 6 && charTypes >= 3) { |
|
|
return 3 |
|
|
} |
|
|
|
|
|
// 2级:长度>=6,字符类型>=2 |
|
|
if (password.length >= 6 && charTypes >= 2) { |
|
|
return 2 |
|
|
} |
|
|
|
|
|
// 1级:长度>=6,字符类型>=1 |
|
|
if (password.length >= 6 && charTypes >= 1) { |
|
|
return 1 |
|
|
} |
|
|
|
|
|
// 0级:长度>=1,字符类型>=1(任何密码都至少有一种字符类型) |
|
|
if (password.length >= 1) { |
|
|
return 0 |
|
|
} |
|
|
|
|
|
// 如果连0级都不满足,返回0 |
|
|
return 0 |
|
|
} |
|
|
|
|
|
const validateConfirmPassword = () => { |
|
|
errors.confirmPassword = '' |
|
|
|
|
|
if (form.newPassword !== form.confirmPassword) { |
|
|
errors.confirmPassword = '两次输入的密码不一致' |
|
|
} |
|
|
} |
|
|
|
|
|
const handleSubmit = async () => { |
|
|
if (!isFormValid.value) return |
|
|
|
|
|
loading.value = true |
|
|
|
|
|
try { |
|
|
const requestData = { |
|
|
current_password: form.currentPassword, |
|
|
new_password: form.newPassword, |
|
|
confirm_password: form.confirmPassword |
|
|
} |
|
|
|
|
|
await userService.changePassword(requestData) |
|
|
|
|
|
// 成功 |
|
|
emit('success') |
|
|
handleClose() |
|
|
|
|
|
// 显示成功消息 |
|
|
if (window.showToast) { |
|
|
window.showToast({ |
|
|
type: 'success', |
|
|
title: '密码修改成功', |
|
|
content: '您的密码已成功修改' |
|
|
}) |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('修改密码失败:', error) |
|
|
|
|
|
// 显示错误消息 |
|
|
if (error.response?.data?.message) { |
|
|
if (error.response.data.message.includes('当前密码不正确')) { |
|
|
errors.currentPassword = '当前密码不正确' |
|
|
} else if (error.response.data.message.includes('新密码不符合要求')) { |
|
|
errors.newPassword = error.response.data.message |
|
|
} else { |
|
|
errors.newPassword = error.response.data.message |
|
|
} |
|
|
} else { |
|
|
errors.newPassword = '修改密码失败,请重试' |
|
|
} |
|
|
} finally { |
|
|
loading.value = false |
|
|
} |
|
|
} |
|
|
|
|
|
const handleClose = () => { |
|
|
if (loading.value) return |
|
|
|
|
|
// 重置表单 |
|
|
form.currentPassword = '' |
|
|
form.newPassword = '' |
|
|
form.confirmPassword = '' |
|
|
|
|
|
// 清除错误 |
|
|
Object.keys(errors).forEach(key => { |
|
|
errors[key] = '' |
|
|
}) |
|
|
|
|
|
// 重置要求状态 |
|
|
Object.keys(requirements).forEach(key => { |
|
|
requirements[key] = false |
|
|
}) |
|
|
|
|
|
passwordStrength.value = 0 |
|
|
passwordLevel.value = 0 |
|
|
|
|
|
emit('close') |
|
|
} |
|
|
|
|
|
const handleOverlayClick = () => { |
|
|
// 密码修改模态窗不应该在点击外部区域时关闭 |
|
|
// 避免用户意外丢失已输入的密码内容 |
|
|
// 只有通过明确的取消按钮或关闭按钮才能关闭 |
|
|
return false |
|
|
} |
|
|
|
|
|
// 监听器 |
|
|
watch(() => props.visible, (newVal) => { |
|
|
if (newVal) { |
|
|
console.log('模态框显示,开始加载密码策略') |
|
|
loadPasswordPolicy() |
|
|
} |
|
|
}) |
|
|
|
|
|
// 监听密码变化,实时更新要求状态 |
|
|
watch(() => form.newPassword, (newPassword) => { |
|
|
if (newPassword) { |
|
|
console.log('密码变化,更新要求状态:', newPassword) |
|
|
updateRequirements(newPassword) |
|
|
} else { |
|
|
// 密码为空时重置所有状态 |
|
|
Object.keys(requirements).forEach(key => { |
|
|
requirements[key] = false |
|
|
}) |
|
|
// 重置密码强度 |
|
|
passwordStrength.value = 0 |
|
|
passwordLevel.value = 0 |
|
|
} |
|
|
}) |
|
|
|
|
|
// 生命周期 |
|
|
onMounted(() => { |
|
|
console.log('组件挂载,检查是否需要加载密码策略') |
|
|
if (props.visible) { |
|
|
loadPasswordPolicy() |
|
|
} |
|
|
// 如果有初始密码内容,计算强度 |
|
|
if (form.newPassword) { |
|
|
updateRequirements(form.newPassword) |
|
|
} |
|
|
}) |
|
|
|
|
|
return { |
|
|
loading, |
|
|
policy, |
|
|
form, |
|
|
errors, |
|
|
requirements, |
|
|
passwordStrength, |
|
|
passwordLevel, |
|
|
title, |
|
|
submitText, |
|
|
strengthClass, |
|
|
strengthText, |
|
|
strengthPercentage, |
|
|
isFormValid, |
|
|
validatePassword, |
|
|
validateConfirmPassword, |
|
|
updateRequirements, |
|
|
handleSubmit, |
|
|
handleClose, |
|
|
handleOverlayClick |
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
|
|
|
<style scoped> |
|
|
.password-modal-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background: rgba(0, 0, 0, 0.5); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.password-modal { |
|
|
background: var(--card-bg); |
|
|
border-radius: 12px; |
|
|
width: 90%; |
|
|
max-width: 500px; |
|
|
max-height: 90vh; |
|
|
overflow-y: auto; |
|
|
box-shadow: 0 8px 32px var(--shadow-color); |
|
|
} |
|
|
|
|
|
.modal-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
padding: 20px 24px; |
|
|
border-bottom: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.modal-header h3 { |
|
|
margin: 0; |
|
|
color: var(--text-primary); |
|
|
font-size: 18px; |
|
|
} |
|
|
|
|
|
.close-btn { |
|
|
background: none; |
|
|
border: none; |
|
|
font-size: 24px; |
|
|
cursor: pointer; |
|
|
color: var(--text-muted); |
|
|
padding: 4px; |
|
|
border-radius: 4px; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.close-btn:hover { |
|
|
background: var(--bg-secondary); |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.modal-body { |
|
|
padding: 24px; |
|
|
} |
|
|
|
|
|
.form-group { |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.form-group label { |
|
|
display: block; |
|
|
margin-bottom: 8px; |
|
|
font-weight: 500; |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.form-group input { |
|
|
width: 100%; |
|
|
padding: 12px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: 6px; |
|
|
font-size: 14px; |
|
|
background: var(--input-bg); |
|
|
color: var(--text-primary); |
|
|
transition: all 0.2s; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
.form-group input:focus { |
|
|
outline: none; |
|
|
border-color: var(--accent-color); |
|
|
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2); |
|
|
} |
|
|
|
|
|
.form-group input.error { |
|
|
border-color: #f44336; |
|
|
} |
|
|
|
|
|
.error-message { |
|
|
color: #f44336; |
|
|
font-size: 12px; |
|
|
margin-top: 4px; |
|
|
} |
|
|
|
|
|
/* 密码强度指示器 */ |
|
|
.password-strength { |
|
|
margin-top: 12px; |
|
|
} |
|
|
|
|
|
.strength-bar { |
|
|
width: 100%; |
|
|
height: 6px; |
|
|
background: #e0e0e0; |
|
|
border-radius: 3px; |
|
|
overflow: hidden; |
|
|
margin-bottom: 8px; |
|
|
} |
|
|
|
|
|
.strength-fill { |
|
|
height: 100%; |
|
|
transition: width 0.3s ease; |
|
|
} |
|
|
|
|
|
.strength-fill.very-weak { |
|
|
background: #f44336; |
|
|
} |
|
|
|
|
|
.strength-fill.weak { |
|
|
background: #ff9800; |
|
|
} |
|
|
|
|
|
.strength-fill.medium { |
|
|
background: #ffc107; |
|
|
} |
|
|
|
|
|
.strength-fill.strong { |
|
|
background: #4caf50; |
|
|
} |
|
|
|
|
|
.strength-text { |
|
|
font-size: 12px; |
|
|
color: var(--text-secondary); |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
/* 密码要求提示 */ |
|
|
.password-requirements { |
|
|
margin-top: 12px; |
|
|
padding: 12px; |
|
|
background: var(--bg-secondary); |
|
|
border-radius: 6px; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
.requirement { |
|
|
color: var(--text-muted); |
|
|
margin-bottom: 4px; |
|
|
transition: color 0.2s; |
|
|
} |
|
|
|
|
|
.requirement.met { |
|
|
color: #4caf50; |
|
|
} |
|
|
|
|
|
.requirement:last-child { |
|
|
margin-bottom: 0; |
|
|
} |
|
|
|
|
|
/* 操作按钮 */ |
|
|
.form-actions { |
|
|
display: flex; |
|
|
gap: 12px; |
|
|
justify-content: flex-end; |
|
|
margin-top: 30px; |
|
|
} |
|
|
|
|
|
/* 强制修改密码提示 */ |
|
|
.force-change-notice { |
|
|
margin-top: 20px; |
|
|
padding: 16px; |
|
|
background: #fff3cd; |
|
|
border: 1px solid #ffeaa7; |
|
|
border-radius: 8px; |
|
|
display: flex; |
|
|
align-items: flex-start; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.notice-icon { |
|
|
font-size: 20px; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.notice-text { |
|
|
color: #856404; |
|
|
font-size: 14px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.notice-text strong { |
|
|
color: #856404; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
padding: 12px 24px; |
|
|
border: none; |
|
|
border-radius: 6px; |
|
|
cursor: pointer; |
|
|
font-size: 14px; |
|
|
font-weight: 500; |
|
|
transition: all 0.2s; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.btn:disabled { |
|
|
opacity: 0.6; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.btn-primary { |
|
|
background: var(--accent-color); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-primary:hover:not(:disabled) { |
|
|
background: var(--accent-hover); |
|
|
} |
|
|
|
|
|
.btn-secondary { |
|
|
background: var(--bg-secondary); |
|
|
color: var(--text-primary); |
|
|
border: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.btn-secondary:hover:not(:disabled) { |
|
|
background: var(--bg-tertiary); |
|
|
} |
|
|
|
|
|
/* 加载动画 */ |
|
|
.loading-spinner { |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
border: 2px solid transparent; |
|
|
border-top: 2px solid currentColor; |
|
|
border-radius: 50%; |
|
|
animation: spin 1s linear infinite; |
|
|
} |
|
|
|
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
/* 响应式设计 */ |
|
|
@media (max-width: 768px) { |
|
|
.password-modal { |
|
|
width: 95%; |
|
|
margin: 20px; |
|
|
} |
|
|
|
|
|
.modal-header, |
|
|
.modal-body { |
|
|
padding: 16px; |
|
|
} |
|
|
|
|
|
.form-actions { |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
width: 100%; |
|
|
justify-content: center; |
|
|
} |
|
|
} |
|
|
</style>
|
|
|
|