Files
gt-agent-company/src/pages/agent-system/change-password/index.vue
sexygoat 3c62cc1cd1 first
2026-03-31 18:41:52 +08:00

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>