439 lines
12 KiB
Vue
439 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import { changePassword } from '@/api/auth'
|
|
|
|
// 表单数据
|
|
const formData = ref({
|
|
old_password: '',
|
|
new_password: '',
|
|
confirm_password: '',
|
|
})
|
|
|
|
// 密码可见性
|
|
const passwordVisible = ref({
|
|
old: false,
|
|
new: false,
|
|
confirm: false,
|
|
})
|
|
|
|
// 提交状态
|
|
const submitting = ref(false)
|
|
|
|
// 密码强度
|
|
const passwordStrength = computed(() => {
|
|
const password = formData.value.new_password
|
|
if (!password)
|
|
return { level: 0, text: '', color: '' }
|
|
|
|
let strength = 0
|
|
// 长度检查
|
|
if (password.length >= 8)
|
|
strength++
|
|
if (password.length >= 12)
|
|
strength++
|
|
// 复杂度检查
|
|
if (/[a-z]/.test(password))
|
|
strength++
|
|
if (/[A-Z]/.test(password))
|
|
strength++
|
|
if (/\d/.test(password))
|
|
strength++
|
|
if (/[^a-zA-Z\d]/.test(password))
|
|
strength++
|
|
|
|
if (strength <= 2)
|
|
return { level: 1, text: '弱', color: '#ff6b6b' }
|
|
if (strength <= 4)
|
|
return { level: 2, text: '中', color: '#e68815' }
|
|
return { level: 3, text: '强', color: '#3ed268' }
|
|
})
|
|
|
|
// 表单验证
|
|
const isFormValid = computed(() => {
|
|
const { old_password, new_password, confirm_password } = formData.value
|
|
return (
|
|
old_password.length > 0
|
|
&& new_password.length >= 6
|
|
&& new_password === confirm_password
|
|
)
|
|
})
|
|
|
|
// 切换密码可见性
|
|
function togglePasswordVisible(field: 'old' | 'new' | 'confirm') {
|
|
passwordVisible.value[field] = !passwordVisible.value[field]
|
|
}
|
|
|
|
// 提交修改
|
|
async function submitChange() {
|
|
// 验证表单
|
|
if (!formData.value.old_password) {
|
|
uni.showToast({
|
|
title: '请输入当前密码',
|
|
icon: 'none',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (formData.value.new_password.length < 6) {
|
|
uni.showToast({
|
|
title: '新密码至少6位',
|
|
icon: 'none',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (formData.value.new_password !== formData.value.confirm_password) {
|
|
uni.showToast({
|
|
title: '两次密码输入不一致',
|
|
icon: 'none',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (formData.value.old_password === formData.value.new_password) {
|
|
uni.showToast({
|
|
title: '新密码不能与当前密码相同',
|
|
icon: 'none',
|
|
})
|
|
return
|
|
}
|
|
|
|
submitting.value = true
|
|
|
|
try {
|
|
// 调用修改密码接口
|
|
await changePassword({
|
|
old_password: formData.value.old_password,
|
|
new_password: formData.value.new_password,
|
|
})
|
|
|
|
uni.showToast({
|
|
title: '密码修改成功',
|
|
icon: 'success',
|
|
duration: 2000,
|
|
})
|
|
|
|
// 清除登录状态
|
|
uni.removeStorageSync('admin-token')
|
|
uni.removeStorageSync('refresh_token')
|
|
uni.removeStorageSync('user_info')
|
|
|
|
// 延迟跳转到登录页
|
|
setTimeout(() => {
|
|
uni.reLaunch({
|
|
url: '/pages/common/login/index',
|
|
})
|
|
}, 2000)
|
|
}
|
|
catch (error: any) {
|
|
console.error('修改密码失败:', error)
|
|
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
|
if (error instanceof Error && error.message && !error.statusCode) {
|
|
uni.$u.toast(error.message)
|
|
}
|
|
// API 请求错误已由拦截器统一处理
|
|
}
|
|
finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
// 清空表单
|
|
function clearForm() {
|
|
formData.value = {
|
|
old_password: '',
|
|
new_password: '',
|
|
confirm_password: '',
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
|
<!-- 内容区域 -->
|
|
<view class="px-4 pt-5">
|
|
<!-- 提示信息 -->
|
|
<view class="bg-[#e3f2fd] rounded-12px p-4 mb-4 flex items-start gap-3">
|
|
<i class="i-mdi-information text-20px text-[#1976d2] mt-0.5" />
|
|
<view class="flex-1">
|
|
<text class="text-13px text-[#1976d2] block mb-1 font-600">
|
|
密码安全提示
|
|
</text>
|
|
<text class="text-12px text-[#1976d2] leading-relaxed">
|
|
为了您的账号安全,建议密码长度至少8位,包含大小写字母、数字和特殊字符。
|
|
</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 表单卡片 -->
|
|
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-4">
|
|
<!-- 当前密码 -->
|
|
<view class="mb-4">
|
|
<text class="text-13px text-[#999] block mb-2">
|
|
当前密码 <text class="text-[#ff6b6b]">*</text>
|
|
</text>
|
|
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
|
|
<i class="i-mdi-lock text-20px text-[#999]" />
|
|
<input
|
|
v-model="formData.old_password"
|
|
:password="!passwordVisible.old"
|
|
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
|
|
placeholder="请输入当前密码"
|
|
>
|
|
<i
|
|
:class="[
|
|
'text-20px text-[#999]',
|
|
passwordVisible.old ? 'i-mdi-eye-off' : 'i-mdi-eye',
|
|
]"
|
|
@click="togglePasswordVisible('old')"
|
|
/>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 新密码 -->
|
|
<view class="mb-4">
|
|
<view class="flex items-center justify-between mb-2">
|
|
<text class="text-13px text-[#999]">
|
|
新密码 <text class="text-[#ff6b6b]">*</text>
|
|
</text>
|
|
<view
|
|
v-if="formData.new_password"
|
|
class="flex items-center gap-1"
|
|
>
|
|
<text class="text-11px text-[#999]">
|
|
强度:
|
|
</text>
|
|
<text
|
|
:style="{ color: passwordStrength.color }"
|
|
class="text-11px font-600"
|
|
>
|
|
{{ passwordStrength.text }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
|
|
<i class="i-mdi-lock-outline text-20px text-[#999]" />
|
|
<input
|
|
v-model="formData.new_password"
|
|
:password="!passwordVisible.new"
|
|
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
|
|
placeholder="请输入新密码(至少6位)"
|
|
>
|
|
<i
|
|
:class="[
|
|
'text-20px text-[#999]',
|
|
passwordVisible.new ? 'i-mdi-eye-off' : 'i-mdi-eye',
|
|
]"
|
|
@click="togglePasswordVisible('new')"
|
|
/>
|
|
</view>
|
|
|
|
<!-- 密码强度指示器 -->
|
|
<view
|
|
v-if="formData.new_password"
|
|
class="flex items-center gap-1 mt-2"
|
|
>
|
|
<view
|
|
v-for="i in 3"
|
|
:key="i"
|
|
:class="[
|
|
'flex-1 h-2px rounded-full',
|
|
i <= passwordStrength.level ? 'opacity-100' : 'opacity-20',
|
|
]"
|
|
:style="{ backgroundColor: passwordStrength.color }"
|
|
/>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 确认密码 -->
|
|
<view>
|
|
<text class="text-13px text-[#999] block mb-2">
|
|
确认密码 <text class="text-[#ff6b6b]">*</text>
|
|
</text>
|
|
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
|
|
<i class="i-mdi-lock-check text-20px text-[#999]" />
|
|
<input
|
|
v-model="formData.confirm_password"
|
|
:password="!passwordVisible.confirm"
|
|
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
|
|
placeholder="请再次输入新密码"
|
|
>
|
|
<i
|
|
:class="[
|
|
'text-20px text-[#999]',
|
|
passwordVisible.confirm ? 'i-mdi-eye-off' : 'i-mdi-eye',
|
|
]"
|
|
@click="togglePasswordVisible('confirm')"
|
|
/>
|
|
</view>
|
|
|
|
<!-- 密码匹配提示 -->
|
|
<view
|
|
v-if="formData.confirm_password"
|
|
class="flex items-center gap-1 mt-2"
|
|
>
|
|
<i
|
|
:class="[
|
|
'text-14px',
|
|
formData.new_password === formData.confirm_password
|
|
? 'i-mdi-check-circle text-[#3ed268]'
|
|
: 'i-mdi-close-circle text-[#ff6b6b]',
|
|
]"
|
|
/>
|
|
<text
|
|
:class="[
|
|
'text-12px',
|
|
formData.new_password === formData.confirm_password
|
|
? 'text-[#3ed268]'
|
|
: 'text-[#ff6b6b]',
|
|
]"
|
|
>
|
|
{{ formData.new_password === formData.confirm_password ? '密码一致' : '密码不一致' }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 密码要求说明 -->
|
|
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-4">
|
|
<text class="text-14px font-600 text-[#212121] block mb-3">
|
|
密码要求
|
|
</text>
|
|
|
|
<view class="space-y-2">
|
|
<view class="flex items-start gap-2">
|
|
<i
|
|
:class="[
|
|
'text-14px mt-0.5',
|
|
formData.new_password.length >= 6
|
|
? 'i-mdi-check-circle text-[#3ed268]'
|
|
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
|
]"
|
|
/>
|
|
<text class="flex-1 text-13px text-[#666]">
|
|
至少6个字符(建议8个以上)
|
|
</text>
|
|
</view>
|
|
|
|
<view class="flex items-start gap-2">
|
|
<i
|
|
:class="[
|
|
'text-14px mt-0.5',
|
|
/[a-zA-Z]/.test(formData.new_password)
|
|
? 'i-mdi-check-circle text-[#3ed268]'
|
|
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
|
]"
|
|
/>
|
|
<text class="flex-1 text-13px text-[#666]">
|
|
包含字母
|
|
</text>
|
|
</view>
|
|
|
|
<view class="flex items-start gap-2">
|
|
<i
|
|
:class="[
|
|
'text-14px mt-0.5',
|
|
/\d/.test(formData.new_password)
|
|
? 'i-mdi-check-circle text-[#3ed268]'
|
|
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
|
]"
|
|
/>
|
|
<text class="flex-1 text-13px text-[#666]">
|
|
包含数字
|
|
</text>
|
|
</view>
|
|
|
|
<view class="flex items-start gap-2">
|
|
<i
|
|
:class="[
|
|
'text-14px mt-0.5',
|
|
/[^a-zA-Z\d]/.test(formData.new_password)
|
|
? 'i-mdi-check-circle text-[#3ed268]'
|
|
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
|
]"
|
|
/>
|
|
<text class="flex-1 text-13px text-[#666]">
|
|
包含特殊字符(推荐)
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 操作按钮 -->
|
|
<view class="space-y-3">
|
|
<!-- 确认修改 -->
|
|
<view
|
|
:class="[
|
|
'py-3 rounded-12px center',
|
|
'transition-all duration-200',
|
|
isFormValid && !submitting
|
|
? 'bg-[#212121] active:bg-[#000]'
|
|
: 'bg-[#ccc]',
|
|
]"
|
|
@click="submitChange"
|
|
>
|
|
<i
|
|
v-if="submitting"
|
|
class="i-mdi-loading animate-spin text-18px text-white mr-2"
|
|
/>
|
|
<text class="text-15px font-600 text-white">
|
|
{{ submitting ? '修改中...' : '确认修改' }}
|
|
</text>
|
|
</view>
|
|
|
|
<!-- 清空表单 -->
|
|
<view
|
|
class="
|
|
py-3 rounded-12px center
|
|
bg-white shadow-[0_2px_12px_rgba(0,0,0,0.06)]
|
|
transition-all duration-200
|
|
active:bg-[#f5f5f5]
|
|
"
|
|
@click="clearForm"
|
|
>
|
|
<i class="i-mdi-refresh text-18px text-[#666] mr-2" />
|
|
<text class="text-15px font-600 text-[#666]">
|
|
清空
|
|
</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 底部留白 -->
|
|
<view class="h-8" />
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.center {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.space-y-3 > view:not(:last-child) {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.space-y-2 > view:not(:last-child) {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.leading-relaxed {
|
|
line-height: 1.6;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.animate-spin {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
</style>
|