This commit is contained in:
2025-07-15 18:02:29 +08:00
parent 2038ddc617
commit d636050aac
65 changed files with 17885 additions and 103 deletions
+525
View File
@@ -0,0 +1,525 @@
<template>
<div class="change-password-container">
<div class="page-header">
<h2 class="page-title">修改密码</h2>
</div>
<div class="password-layout">
<!-- 修改密码表单 -->
<div class="password-form-container">
<el-card shadow="hover" class="password-card">
<template #header>
<div class="card-header">
<el-icon><lock /></el-icon>
<span>密码修改</span>
</div>
</template>
<el-form
ref="passwordFormRef"
:model="passwordForm"
:rules="rules"
label-width="120px"
class="password-form"
status-icon
>
<el-form-item label="当前密码" prop="oldPassword">
<el-input
v-model="passwordForm.oldPassword"
type="password"
placeholder="请输入当前密码"
show-password
prefix-icon="Key"
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
show-password
prefix-icon="Lock"
/>
<!-- 密码强度指示器 -->
<div class="password-strength" v-if="passwordForm.newPassword">
<div class="strength-label">密码强度</div>
<div class="strength-bar">
<div
class="strength-indicator"
:class="[
passwordStrength === 'weak' ? 'weak' :
passwordStrength === 'medium' ? 'medium' :
passwordStrength === 'strong' ? 'strong' : ''
]"
:style="{ width: strengthPercentage + '%' }"
></div>
</div>
<div class="strength-text" :class="passwordStrength">
{{ passwordStrengthText }}
</div>
</div>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword">
<el-input
v-model="passwordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password
prefix-icon="Check"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="submitForm"
:loading="loading"
:disabled="submitDisabled"
class="submit-btn"
>
保存修改
</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 密码规则说明 -->
<div class="password-tips-container">
<el-card shadow="hover" class="tips-card">
<template #header>
<div class="card-header">
<el-icon><InfoFilled /></el-icon>
<span>密码要求</span>
</div>
</template>
<div class="tips-content">
<p class="tips-title">为了保障您的账号安全密码需满足以下要求</p>
<ul class="tips-list">
<li class="tips-item" :class="{ passed: hasMinLength }">
<el-icon :class="{ 'is-passed': hasMinLength }">
<component :is="hasMinLength ? 'CircleCheck' : 'CircleClose'" />
</el-icon>
<span>长度至少8个字符</span>
</li>
<li class="tips-item" :class="{ passed: hasUpperCase }">
<el-icon :class="{ 'is-passed': hasUpperCase }">
<component :is="hasUpperCase ? 'CircleCheck' : 'CircleClose'" />
</el-icon>
<span>包含至少一个大写字母 (A-Z)</span>
</li>
<li class="tips-item" :class="{ passed: hasLowerCase }">
<el-icon :class="{ 'is-passed': hasLowerCase }">
<component :is="hasLowerCase ? 'CircleCheck' : 'CircleClose'" />
</el-icon>
<span>包含至少一个小写字母 (a-z)</span>
</li>
<li class="tips-item" :class="{ passed: hasNumber }">
<el-icon :class="{ 'is-passed': hasNumber }">
<component :is="hasNumber ? 'CircleCheck' : 'CircleClose'" />
</el-icon>
<span>包含至少一个数字 (0-9)</span>
</li>
<li class="tips-item" :class="{ passed: hasSpecialChar }">
<el-icon :class="{ 'is-passed': hasSpecialChar }">
<component :is="hasSpecialChar ? 'CircleCheck' : 'CircleClose'" />
</el-icon>
<span>包含至少一个特殊字符 (@$!%*#?&)</span>
</li>
</ul>
<div class="tips-note">
<el-icon><Warning /></el-icon>
<p>为了您的账户安全请不要使用与其他网站相同的密码并定期更换密码</p>
</div>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
Lock,
Key,
InfoFilled,
CircleCheck,
CircleClose,
Warning,
Check
} from '@element-plus/icons-vue'
// 表单数据
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
// 表单引用
const passwordFormRef = ref(null)
// 加载状态
const loading = ref(false)
// 密码强度计算
const hasMinLength = computed(() =>
passwordForm.newPassword.length >= 8
)
const hasUpperCase = computed(() =>
/[A-Z]/.test(passwordForm.newPassword)
)
const hasLowerCase = computed(() =>
/[a-z]/.test(passwordForm.newPassword)
)
const hasNumber = computed(() =>
/\d/.test(passwordForm.newPassword)
)
const hasSpecialChar = computed(() =>
/[@$!%*#?&]/.test(passwordForm.newPassword)
)
// 密码强度计算
const passwordStrength = computed(() => {
let score = 0
// 最低长度要求
if (hasMinLength.value) score++
// 大写字母
if (hasUpperCase.value) score++
// 小写字母
if (hasLowerCase.value) score++
// 数字
if (hasNumber.value) score++
// 特殊字符
if (hasSpecialChar.value) score++
if (score <= 2) return 'weak'
if (score <= 4) return 'medium'
return 'strong'
})
// 密码强度百分比
const strengthPercentage = computed(() => {
if (passwordStrength.value === 'weak') return 30
if (passwordStrength.value === 'medium') return 70
return 100
})
// 密码强度文字描述
const passwordStrengthText = computed(() => {
if (passwordStrength.value === 'weak') return '弱'
if (passwordStrength.value === 'medium') return '中'
return '强'
})
// 提交按钮是否禁用
const submitDisabled = computed(() => {
return (
!passwordForm.oldPassword ||
!passwordForm.newPassword ||
!passwordForm.confirmPassword ||
passwordStrength.value === 'weak'
)
})
// 密码强度校验
const validatePasswordStrength = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'))
return
}
if (!hasMinLength.value) {
callback(new Error('密码长度至少为8个字符'))
return
}
if (!(hasUpperCase.value && hasLowerCase.value && hasNumber.value && hasSpecialChar.value)) {
callback(new Error('密码必须包含大小写字母、数字和特殊字符'))
return
}
callback()
}
// 确认密码校验
const validateConfirmPassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
return
}
if (value !== passwordForm.newPassword) {
callback(new Error('两次输入的密码不一致'))
return
}
callback()
}
// 表单校验规则
const rules = {
oldPassword: [
{ required: true, message: '请输入当前密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少为6个字符', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ validator: validatePasswordStrength, trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
// 提交表单
const submitForm = async () => {
if (!passwordFormRef.value) return
await passwordFormRef.value.validate(async (valid) => {
if (valid) {
try {
loading.value = true
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1500))
// 这里应该调用API修改密码
// const res = await api.changePassword(passwordForm)
ElMessage.success('密码修改成功,请重新登录')
loading.value = false
// 清空表单
resetForm()
// 实际项目中可能需要跳转到登录页
// router.push('/login')
} catch (error) {
loading.value = false
ElMessage.error('密码修改失败,请重试')
console.error(error)
}
}
})
}
// 重置表单
const resetForm = () => {
if (passwordFormRef.value) {
passwordFormRef.value.resetFields()
}
}
</script>
<style scoped>
.change-password-container {
padding: 24px;
min-height: 100%;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #1a1f36;
}
.password-layout {
display: flex;
gap: 24px;
}
.password-form-container {
flex: 1;
}
.password-tips-container {
width: 380px;
flex-shrink: 0;
}
.password-card,
.tips-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05) !important;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #1a1f36;
}
.card-header .el-icon {
margin-right: 8px;
font-size: 18px;
color: #409eff;
}
.password-form {
padding: 20px 0;
}
.password-strength {
margin-top: 8px;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.strength-label {
margin-right: 8px;
font-size: 13px;
color: #606266;
margin-bottom: 2px;
}
.strength-bar {
width: 120px;
height: 4px;
background-color: #e4e7ed;
border-radius: 2px;
overflow: hidden;
margin-right: 8px;
margin-bottom: 2px;
}
.strength-indicator {
height: 100%;
transition: width 0.3s ease, background-color 0.3s ease;
}
.strength-indicator.weak {
background-color: #f56c6c;
}
.strength-indicator.medium {
background-color: #e6a23c;
}
.strength-indicator.strong {
background-color: #67c23a;
}
.strength-text {
font-size: 13px;
font-weight: 500;
}
.strength-text.weak {
color: #f56c6c;
}
.strength-text.medium {
color: #e6a23c;
}
.strength-text.strong {
color: #67c23a;
}
.submit-btn {
width: 120px;
}
.tips-content {
padding: 0 0 15px;
}
.tips-title {
margin-top: 0;
margin-bottom: 16px;
font-size: 14px;
color: #606266;
}
.tips-list {
list-style: none;
padding: 0;
margin: 0 0 20px 0;
}
.tips-item {
display: flex;
align-items: center;
padding: 8px 0;
color: #606266;
font-size: 14px;
transition: color 0.3s;
}
.tips-item .el-icon {
margin-right: 8px;
font-size: 16px;
color: #c0c4cc;
transition: color 0.3s;
}
.tips-item .el-icon.is-passed {
color: #67c23a;
}
.tips-item.passed {
color: #67c23a;
}
.tips-note {
background-color: #fdf6ec;
border-radius: 4px;
padding: 10px 12px;
display: flex;
align-items: flex-start;
}
.tips-note .el-icon {
color: #e6a23c;
font-size: 16px;
margin-right: 8px;
margin-top: 3px;
flex-shrink: 0;
}
.tips-note p {
margin: 0;
font-size: 13px;
color: #af8741;
line-height: 1.5;
}
@media (max-width: 992px) {
.password-layout {
flex-direction: column;
}
.password-tips-container {
width: 100%;
}
}
</style>
+529
View File
@@ -0,0 +1,529 @@
<template>
<div class="user-info-container">
<div class="page-header">
<h2 class="page-title">个人信息</h2>
<div class="page-actions">
<el-button type="primary" @click="handleEdit" v-if="!isEditing">
<el-icon><edit /></el-icon>编辑资料
</el-button>
</div>
</div>
<div class="profile-layout">
<!-- 左侧用户基本信息卡片 -->
<div class="profile-sidebar">
<div class="profile-card">
<div class="profile-avatar-container">
<el-avatar
:size="120"
:src="userInfo.avatar || 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'"
class="profile-avatar"
/>
<div class="profile-avatar-edit" v-if="isEditing">
<el-upload
class="avatar-uploader"
action="/api/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
>
<div class="upload-trigger">
<el-icon><upload-filled /></el-icon>
<span>更换头像</span>
</div>
</el-upload>
</div>
</div>
<h3 class="profile-name">{{ userInfo.realName }}</h3>
<div class="profile-title">{{ userInfo.position }}</div>
<div class="profile-role">
<el-tag type="success" effect="plain">{{ userInfo.role }}</el-tag>
</div>
<div class="profile-stats">
<div class="stat-item">
<div class="stat-value">{{ userInfo.department }}</div>
<div class="stat-label">部门</div>
</div>
<div class="divider"></div>
<div class="stat-item">
<div class="stat-value">{{ formatDate(userInfo.lastLogin) }}</div>
<div class="stat-label">最近登录</div>
</div>
</div>
</div>
</div>
<!-- 右侧详细信息内容 -->
<div class="profile-content">
<!-- 查看模式 -->
<div v-if="!isEditing" class="info-display">
<el-card shadow="hover" class="info-card">
<template #header>
<div class="card-header">
<el-icon><user /></el-icon>
<span>账号信息</span>
</div>
</template>
<div class="info-list">
<div class="info-item">
<span class="info-label">用户名</span>
<span class="info-value">{{ userInfo.username }}</span>
</div>
<div class="info-item">
<span class="info-label">真实姓名</span>
<span class="info-value">{{ userInfo.realName }}</span>
</div>
<div class="info-item">
<span class="info-label">邮箱</span>
<span class="info-value">{{ userInfo.email }}</span>
</div>
<div class="info-item">
<span class="info-label">手机号</span>
<span class="info-value">{{ userInfo.phone }}</span>
</div>
</div>
</el-card>
<el-card shadow="hover" class="info-card bio-card">
<template #header>
<div class="card-header">
<el-icon><document /></el-icon>
<span>个人简介</span>
</div>
</template>
<div class="bio-content">
{{ userInfo.bio || '暂无个人简介' }}
</div>
</el-card>
<el-card shadow="hover" class="info-card">
<template #header>
<div class="card-header">
<el-icon><timer /></el-icon>
<span>时间信息</span>
</div>
</template>
<div class="info-list">
<div class="info-item">
<span class="info-label">账号创建时间</span>
<span class="info-value">{{ formatDateFull(userInfo.createTime) }}</span>
</div>
<div class="info-item">
<span class="info-label">最后登录时间</span>
<span class="info-value">{{ formatDateFull(userInfo.lastLogin) }}</span>
</div>
</div>
</el-card>
</div>
<!-- 编辑表单部分 -->
<div v-else class="info-edit">
<el-card shadow="hover" class="edit-card">
<template #header>
<div class="card-header">
<el-icon><edit-pen /></el-icon>
<span>编辑个人资料</span>
</div>
</template>
<el-form ref="userFormRef" :model="userForm" :rules="rules" label-width="100px" class="edit-form">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" disabled />
</el-form-item>
<el-form-item label="真实姓名" prop="realName">
<el-input v-model="userForm.realName" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email">
<template #prefix>
<el-icon><message /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="userForm.phone">
<template #prefix>
<el-icon><phone /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="部门">
<el-input v-model="userForm.department">
<template #prefix>
<el-icon><office-building /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="职位">
<el-input v-model="userForm.position" />
</el-form-item>
<el-form-item label="个人简介">
<el-input v-model="userForm.bio" type="textarea" :rows="4" resize="none" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="loading">保存</el-button>
<el-button @click="cancelEdit">取消</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
User, Edit, Document, Timer, EditPen, Message,
Phone, OfficeBuilding, UploadFilled
} from '@element-plus/icons-vue'
// 是否处于编辑模式
const isEditing = ref(false)
const loading = ref(false)
// 用户信息数据
const userInfo = reactive({
username: 'admin',
realName: '管理员',
email: 'admin@example.com',
phone: '13800138000',
department: '技术部',
position: '系统管理员',
role: '超级管理员',
createTime: '2023-01-01 00:00:00',
lastLogin: '2023-06-15 10:30:45',
bio: '系统管理员,负责系统的日常维护和管理工作。拥有丰富的系统管理经验,精通Linux服务器配置和维护,熟悉网络安全,对系统性能优化有独到见解。',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
})
// 表单数据
const userForm = reactive({...userInfo})
// 表单校验规则
const rules = {
realName: [
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
]
}
const userFormRef = ref(null)
// 日期格式化函数
const formatDate = (dateStr) => {
const date = new Date(dateStr)
const now = new Date()
// 如果是今天,只显示时间
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// 否则显示日期
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
}
// 完整日期格式化
const formatDateFull = (dateStr) => {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 编辑按钮点击事件
const handleEdit = () => {
Object.assign(userForm, userInfo)
isEditing.value = true
}
// 取消编辑
const cancelEdit = () => {
isEditing.value = false
}
// 提交表单
const submitForm = async () => {
if (!userFormRef.value) return
await userFormRef.value.validate(async (valid) => {
if (valid) {
try {
loading.value = true
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
// 更新本地用户信息
Object.assign(userInfo, userForm)
ElMessage.success('个人信息更新成功')
isEditing.value = false
loading.value = false
} catch (error) {
loading.value = false
ElMessage.error('保存失败,请重试')
console.error(error)
}
}
})
}
// 头像上传成功回调
const handleAvatarSuccess = (res) => {
userForm.avatar = res.url
}
// 获取用户信息
const fetchUserInfo = async () => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500))
// 实际项目中,应该从后端获取用户信息并更新userInfo
} catch (error) {
ElMessage.error('获取用户信息失败')
console.error(error)
}
}
onMounted(() => {
fetchUserInfo()
})
</script>
<style scoped>
.user-info-container {
padding: 24px;
min-height: 100%;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #1a1f36;
}
.profile-layout {
display: flex;
gap: 24px;
}
.profile-sidebar {
width: 300px;
flex-shrink: 0;
}
.profile-content {
flex: 1;
}
.profile-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 30px;
text-align: center;
margin-bottom: 24px;
}
.profile-avatar-container {
position: relative;
margin-bottom: 20px;
display: inline-block;
}
.profile-avatar {
border: 4px solid #fff;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.profile-avatar-edit {
position: absolute;
bottom: 0;
right: 0;
cursor: pointer;
}
.upload-trigger {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #409eff;
color: white;
font-size: 12px;
line-height: 1;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.5);
}
.upload-trigger span {
display: none;
}
.upload-trigger:hover {
width: auto;
height: auto;
border-radius: 18px;
padding: 4px 12px;
}
.upload-trigger:hover span {
display: inline-block;
margin-left: 4px;
}
.profile-name {
font-size: 22px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1a1f36;
}
.profile-title {
color: #667085;
font-size: 16px;
margin-bottom: 14px;
}
.profile-role {
margin-bottom: 24px;
}
.profile-stats {
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #edf2f7;
padding-top: 20px;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-value {
font-weight: 600;
color: #333;
font-size: 16px;
margin-bottom: 4px;
}
.stat-label {
color: #94a3b8;
font-size: 12px;
}
.divider {
width: 1px;
height: 30px;
background-color: #edf2f7;
margin: 0 15px;
}
.info-card {
margin-bottom: 24px;
border-radius: 8px;
overflow: hidden;
}
.bio-card {
position: relative;
}
.card-header {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #1a1f36;
}
.card-header .el-icon {
margin-right: 8px;
font-size: 18px;
color: #409eff;
}
.info-list {
display: grid;
gap: 16px;
}
.info-item {
display: flex;
align-items: center;
}
.info-label {
width: 120px;
color: #64748b;
font-size: 14px;
}
.info-value {
color: #334155;
font-weight: 500;
flex: 1;
}
.bio-content {
color: #475569;
line-height: 1.6;
font-size: 14px;
white-space: pre-line;
}
.edit-card {
border-radius: 8px;
overflow: hidden;
}
.edit-form {
padding: 16px 0;
}
@media (max-width: 992px) {
.profile-layout {
flex-direction: column;
}
.profile-sidebar {
width: 100%;
}
.profile-card {
padding: 20px;
}
}
</style>