You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
388 lines
9.7 KiB
388 lines
9.7 KiB
<template> |
|
<div class="user-profile management-page"> |
|
<div class="page-header"> |
|
<h2><i class="fas fa-user"></i> 个人资料</h2> |
|
<p class="profile-subtitle">查看和管理您的账户信息</p> |
|
</div> |
|
|
|
<!-- 加载状态 --> |
|
<div v-if="loading" class="loading-container"> |
|
<div class="loading-spinner"></div> |
|
<p>正在加载用户信息...</p> |
|
</div> |
|
|
|
<!-- 错误状态 --> |
|
<div v-else-if="error" class="error-container"> |
|
<div class="error-icon"><i class="fas fa-exclamation-triangle"></i></div> |
|
<h3>加载失败</h3> |
|
<p>{{ error }}</p> |
|
<button @click="loadUserProfile" class="retry-btn"> |
|
<i class="fas fa-redo"></i> 重试 |
|
</button> |
|
</div> |
|
|
|
<!-- 用户信息内容 --> |
|
<div v-else-if="userInfo" class="settings-content"> |
|
<!-- 基本信息卡片 --> |
|
<div class="settings-section"> |
|
<h3><i class="fas fa-info-circle"></i> 基本信息</h3> |
|
<div class="info-grid"> |
|
<div class="info-item"> |
|
<label>用户ID</label> |
|
<span>{{ userInfo.id || 'N/A' }}</span> |
|
</div> |
|
<div class="info-item"> |
|
<label>用户名</label> |
|
<span>{{ userInfo.username || userInfo.name || 'N/A' }}</span> |
|
</div> |
|
<div class="info-item"> |
|
<label>邮箱</label> |
|
<span>{{ userInfo.email || 'N/A' }}</span> |
|
</div> |
|
<div class="info-item"> |
|
<label>手机号</label> |
|
<span>{{ userInfo.phone || 'N/A' }}</span> |
|
</div> |
|
<div class="info-item"> |
|
<label>状态</label> |
|
<span :class="['status-badge', getStatusClass(userInfo.status)]"> |
|
{{ getStatusText(userInfo.status) }} |
|
</span> |
|
</div> |
|
<div class="info-item"> |
|
<label>注册时间</label> |
|
<span>{{ formatDate(userInfo.created_at) || formatDate(userInfo.createdAt) || 'N/A' }}</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 登录信息卡片 --> |
|
<div class="settings-section"> |
|
<h3><i class="fas fa-lock"></i> 登录信息</h3> |
|
<div class="info-grid"> |
|
<div class="info-item"> |
|
<label>上次登录时间</label> |
|
<span>{{ formatDate(userInfo.last_login_at) || formatDate(userInfo.lastLogin) || 'N/A' }}</span> |
|
</div> |
|
<div class="info-item"> |
|
<label>上次登录IP</label> |
|
<span>{{ userInfo.last_login_ip || userInfo.lastLoginIP || 'N/A' }}</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 角色信息卡片 --> |
|
<div class="settings-section"> |
|
<h3><i class="fas fa-crown"></i> 角色信息</h3> |
|
<div v-if="userInfo.roles && userInfo.roles.length > 0" class="roles-list"> |
|
<div v-for="role in userInfo.roles" :key="role.id" class="role-item"> |
|
<div class="role-header"> |
|
<span class="role-name">{{ role.name }}</span> |
|
<span class="role-description">{{ role.description || '无描述' }}</span> |
|
</div> |
|
<div class="role-permissions"> |
|
<h4>权限列表:</h4> |
|
<div class="permissions-grid"> |
|
<span |
|
v-for="permission in role.permissions" |
|
:key="permission.id" |
|
class="permission-tag" |
|
> |
|
{{ permission.name }} |
|
</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<div v-else class="no-roles"> |
|
<p>暂无角色信息</p> |
|
</div> |
|
</div> |
|
|
|
<!-- 操作按钮 --> |
|
<div class="settings-actions"> |
|
<button @click="refreshProfile" class="btn btn-secondary"> |
|
<i class="fas fa-sync-alt"></i> 刷新信息 |
|
</button> |
|
<button @click="changePassword" class="btn btn-primary"> |
|
<i class="fas fa-lock"></i> 修改密码 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<!-- 空状态 --> |
|
<div v-else class="empty-state"> |
|
<div class="empty-icon"><i class="fas fa-user"></i></div> |
|
<h3>暂无用户信息</h3> |
|
<p>请先登录以查看您的个人资料</p> |
|
<button @click="goToLogin" class="login-btn"> |
|
<i class="fas fa-sign-in-alt"></i> 去登录 |
|
</button> |
|
</div> |
|
|
|
<!-- 密码修改弹窗 --> |
|
<PasswordChangeModal |
|
v-model:visible="showPasswordModal" |
|
:is-force-change="isForceChange" |
|
@close="showPasswordModal = false" |
|
@success="onPasswordChangeSuccess" |
|
/> |
|
</div> |
|
</template> |
|
|
|
<script> |
|
import { ref, onMounted, inject } from 'vue' |
|
import { useRouter } from 'vue-router' |
|
import { userService } from '../services/userService.js' |
|
import PasswordChangeModal from '../components/PasswordChangeModal.vue' |
|
|
|
export default { |
|
name: 'UserProfile', |
|
components: { |
|
PasswordChangeModal |
|
}, |
|
setup() { |
|
const router = useRouter() |
|
|
|
// 注入依赖 |
|
const currentUser = inject('currentUser', null) |
|
const isLoggedIn = inject('isLoggedIn', false) |
|
const showLoginModal = inject('showLoginModal', null) |
|
|
|
// 响应式数据 |
|
const loading = ref(false) |
|
const error = ref(null) |
|
const userInfo = ref(null) |
|
|
|
// 密码修改弹窗状态 |
|
const showPasswordModal = ref(false) |
|
const isForceChange = ref(false) |
|
|
|
// 加载用户资料 |
|
const loadUserProfile = async () => { |
|
if (!isLoggedIn.value) { |
|
error.value = '请先登录' |
|
return |
|
} |
|
|
|
loading.value = true |
|
error.value = null |
|
|
|
try { |
|
const token = localStorage.getItem('token') |
|
if (!token) { |
|
throw new Error('未找到认证token') |
|
} |
|
|
|
const response = await userService.getCurrentUser(token) |
|
userInfo.value = response.data || response |
|
|
|
console.log('加载的用户信息:', userInfo.value) |
|
} catch (err) { |
|
console.error('加载用户资料失败:', err) |
|
error.value = err.response?.data?.message || err.message || '加载用户资料失败' |
|
} finally { |
|
loading.value = false |
|
} |
|
} |
|
|
|
// 刷新资料 |
|
const refreshProfile = () => { |
|
loadUserProfile() |
|
} |
|
|
|
|
|
|
|
// 修改密码 |
|
const changePassword = () => { |
|
isForceChange.value = false |
|
showPasswordModal.value = true |
|
} |
|
|
|
// 强制修改密码 |
|
const forceChangePassword = () => { |
|
isForceChange.value = true |
|
showPasswordModal.value = true |
|
} |
|
|
|
// 密码修改成功回调 |
|
const onPasswordChangeSuccess = () => { |
|
// 刷新用户信息 |
|
loadUserProfile() |
|
} |
|
|
|
// 去登录 |
|
const goToLogin = () => { |
|
if (showLoginModal) { |
|
showLoginModal() |
|
} else { |
|
router.push('/') |
|
} |
|
} |
|
|
|
// 格式化日期 |
|
const formatDate = (dateString) => { |
|
if (!dateString) return null |
|
try { |
|
const date = new Date(dateString) |
|
return date.toLocaleString('zh-CN', { |
|
year: 'numeric', |
|
month: '2-digit', |
|
day: '2-digit', |
|
hour: '2-digit', |
|
minute: '2-digit', |
|
second: '2-digit' |
|
}) |
|
} catch (err) { |
|
return dateString |
|
} |
|
} |
|
|
|
// 获取状态文本 |
|
const getStatusText = (status) => { |
|
const statusMap = { |
|
1: '正常', |
|
0: '禁用', |
|
2: '待验证' |
|
} |
|
return statusMap[status] || '未知' |
|
} |
|
|
|
// 获取状态样式类 |
|
const getStatusClass = (status) => { |
|
const classMap = { |
|
1: 'status-active', |
|
0: 'status-inactive', |
|
2: 'status-pending' |
|
} |
|
return classMap[status] || 'status-unknown' |
|
} |
|
|
|
// 组件挂载时加载数据 |
|
onMounted(() => { |
|
if (isLoggedIn.value) { |
|
loadUserProfile() |
|
} |
|
}) |
|
|
|
return { |
|
loading, |
|
error, |
|
userInfo, |
|
currentUser, |
|
isLoggedIn, |
|
showPasswordModal, |
|
isForceChange, |
|
loadUserProfile, |
|
refreshProfile, |
|
changePassword, |
|
forceChangePassword, |
|
onPasswordChangeSuccess, |
|
goToLogin, |
|
formatDate, |
|
getStatusText, |
|
getStatusClass |
|
} |
|
} |
|
} |
|
</script> |
|
|
|
<style scoped> |
|
@import '../../../assets/common-styles.css'; |
|
|
|
/* 个人资料特定样式 */ |
|
.loading-container { |
|
text-align: center; |
|
padding: 60px 20px; |
|
} |
|
|
|
.loading-spinner { |
|
width: 50px; |
|
height: 50px; |
|
border: 4px solid #f3f3f3; |
|
border-top: 4px solid #3498db; |
|
border-radius: 50%; |
|
animation: spin 1s linear infinite; |
|
margin: 0 auto 20px; |
|
} |
|
|
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
.error-container { |
|
text-align: center; |
|
padding: 60px 20px; |
|
background: #fff5f5; |
|
border-radius: 12px; |
|
border: 1px solid #fed7d7; |
|
} |
|
|
|
.error-icon { |
|
font-size: 3rem; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.retry-btn { |
|
background: #e53e3e; |
|
color: white; |
|
border: none; |
|
padding: 12px 24px; |
|
border-radius: 8px; |
|
cursor: pointer; |
|
font-size: 12px; |
|
margin-top: 20px; |
|
} |
|
|
|
.retry-btn:hover { |
|
background: #c53030; |
|
} |
|
|
|
.status-badge.pending { |
|
background: #fff3cd; |
|
color: #856404; |
|
} |
|
|
|
.status-badge.unknown { |
|
background: #e2e3e5; |
|
color: #383d41; |
|
} |
|
|
|
.empty-state { |
|
text-align: center; |
|
padding: 80px 20px; |
|
} |
|
|
|
.empty-icon { |
|
font-size: 4rem; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.empty-state h3 { |
|
color: var(--text-primary); |
|
margin-bottom: 10px; |
|
font-size: 12px; |
|
} |
|
|
|
.empty-state p { |
|
color: var(--text-secondary); |
|
margin-bottom: 30px; |
|
font-size: 12px; |
|
} |
|
|
|
.login-btn { |
|
background: #1976d2; |
|
color: white; |
|
border: none; |
|
padding: 12px 32px; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
font-size: 12px; |
|
font-weight: 500; |
|
} |
|
|
|
.login-btn:hover { |
|
background: #1565c0; |
|
} |
|
</style>
|
|
|