|
|
|
|
<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>
|