6 changed files with 718 additions and 45 deletions
@ -0,0 +1,599 @@
@@ -0,0 +1,599 @@
|
||||
<template> |
||||
<div class="user-profile"> |
||||
<div class="profile-header"> |
||||
<h1>👤 个人资料</h1> |
||||
<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">❌</div> |
||||
<h3>加载失败</h3> |
||||
<p>{{ error }}</p> |
||||
<button @click="loadUserProfile" class="retry-btn">重试</button> |
||||
</div> |
||||
|
||||
<!-- 用户信息内容 --> |
||||
<div v-else-if="userInfo" class="profile-content"> |
||||
<!-- 基本信息卡片 --> |
||||
<div class="profile-card"> |
||||
<div class="card-header"> |
||||
<h2>📋 基本信息</h2> |
||||
</div> |
||||
<div class="card-content"> |
||||
<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> |
||||
|
||||
<!-- 登录信息卡片 --> |
||||
<div class="profile-card"> |
||||
<div class="card-header"> |
||||
<h2>🔐 登录信息</h2> |
||||
</div> |
||||
<div class="card-content"> |
||||
<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> |
||||
|
||||
<!-- 角色信息卡片 --> |
||||
<div class="profile-card"> |
||||
<div class="card-header"> |
||||
<h2>👑 角色信息</h2> |
||||
</div> |
||||
<div class="card-content"> |
||||
<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> |
||||
|
||||
<!-- 操作按钮 --> |
||||
<div class="profile-actions"> |
||||
<button @click="refreshProfile" class="action-btn refresh-btn"> |
||||
🔄 刷新信息 |
||||
</button> |
||||
<button @click="changePassword" class="action-btn password-btn"> |
||||
🔒 修改密码 |
||||
</button> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 空状态 --> |
||||
<div v-else class="empty-state"> |
||||
<div class="empty-icon">👤</div> |
||||
<h3>暂无用户信息</h3> |
||||
<p>请先登录以查看您的个人资料</p> |
||||
<button @click="goToLogin" class="login-btn">去登录</button> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { ref, onMounted, inject } from 'vue' |
||||
import { useRouter } from 'vue-router' |
||||
import { userService } from '@/services/userService' |
||||
|
||||
export default { |
||||
name: 'UserProfile', |
||||
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 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 = () => { |
||||
// TODO: 实现修改密码功能 |
||||
console.log('修改密码功能待实现') |
||||
} |
||||
|
||||
// 去登录 |
||||
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, |
||||
loadUserProfile, |
||||
refreshProfile, |
||||
changePassword, |
||||
goToLogin, |
||||
formatDate, |
||||
getStatusText, |
||||
getStatusClass |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.user-profile { |
||||
padding: 20px; |
||||
height: 100%; |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
.profile-header { |
||||
text-align: center; |
||||
margin-bottom: 30px; |
||||
} |
||||
|
||||
.profile-header h1 { |
||||
font-size: 2.5rem; |
||||
color: var(--text-primary); |
||||
margin-bottom: 10px; |
||||
} |
||||
|
||||
.profile-subtitle { |
||||
font-size: 1.1rem; |
||||
color: var(--text-secondary); |
||||
margin: 0; |
||||
} |
||||
|
||||
.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: 1rem; |
||||
margin-top: 20px; |
||||
} |
||||
|
||||
.retry-btn:hover { |
||||
background: #c53030; |
||||
} |
||||
|
||||
.profile-content { |
||||
max-width: 800px; |
||||
margin: 0 auto; |
||||
} |
||||
|
||||
.profile-card { |
||||
background: var(--card-bg); |
||||
border-radius: 8px; |
||||
padding: 24px; |
||||
margin-bottom: 24px; |
||||
box-shadow: 0 2px 8px var(--shadow-color); |
||||
} |
||||
|
||||
.card-header { |
||||
margin: 0 0 20px 0; |
||||
color: var(--text-primary); |
||||
font-size: 18px; |
||||
border-bottom: 2px solid var(--border-color); |
||||
padding-bottom: 8px; |
||||
} |
||||
|
||||
.card-header h2 { |
||||
margin: 0; |
||||
font-size: 18px; |
||||
color: var(--text-primary); |
||||
} |
||||
|
||||
.info-grid { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 0; |
||||
} |
||||
|
||||
.info-item { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
margin-bottom: 20px; |
||||
padding: 16px 0; |
||||
border-bottom: 1px solid #f5f5f5; |
||||
} |
||||
|
||||
.info-item:last-child { |
||||
border-bottom: none; |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
.info-item label { |
||||
font-weight: 500; |
||||
color: var(--text-primary); |
||||
min-width: 200px; |
||||
} |
||||
|
||||
.info-item span { |
||||
color: var(--text-primary); |
||||
font-size: 14px; |
||||
text-align: right; |
||||
flex: 1; |
||||
} |
||||
|
||||
.status-badge { |
||||
display: inline-block; |
||||
padding: 6px 12px; |
||||
border-radius: 20px; |
||||
font-size: 0.85rem; |
||||
font-weight: 600; |
||||
text-align: center; |
||||
min-width: 80px; |
||||
} |
||||
|
||||
.status-active { |
||||
background: #d4edda; |
||||
color: #155724; |
||||
} |
||||
|
||||
.status-inactive { |
||||
background: #f8d7da; |
||||
color: #721c24; |
||||
} |
||||
|
||||
.status-pending { |
||||
background: #fff3cd; |
||||
color: #856404; |
||||
} |
||||
|
||||
.status-unknown { |
||||
background: #e2e3e5; |
||||
color: #383d41; |
||||
} |
||||
|
||||
.roles-list { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 20px; |
||||
} |
||||
|
||||
.role-item { |
||||
background: var(--card-bg); |
||||
border-radius: 8px; |
||||
padding: 20px; |
||||
border: 1px solid var(--border-color); |
||||
margin-bottom: 16px; |
||||
} |
||||
|
||||
.role-header { |
||||
margin-bottom: 16px; |
||||
} |
||||
|
||||
.role-name { |
||||
display: block; |
||||
font-size: 1.1rem; |
||||
font-weight: 600; |
||||
color: var(--text-primary); |
||||
margin-bottom: 8px; |
||||
} |
||||
|
||||
.role-description { |
||||
color: var(--text-secondary); |
||||
font-size: 0.9rem; |
||||
} |
||||
|
||||
.role-permissions h4 { |
||||
margin: 0 0 12px 0; |
||||
color: var(--text-primary); |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
.permissions-grid { |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.permission-tag { |
||||
background: var(--accent-color); |
||||
color: white; |
||||
padding: 4px 12px; |
||||
border-radius: 16px; |
||||
font-size: 0.85rem; |
||||
border: none; |
||||
} |
||||
|
||||
.no-roles { |
||||
text-align: center; |
||||
color: var(--text-secondary); |
||||
padding: 40px 20px; |
||||
} |
||||
|
||||
.profile-actions { |
||||
display: flex; |
||||
gap: 16px; |
||||
justify-content: flex-end; |
||||
margin-top: 30px; |
||||
} |
||||
|
||||
.action-btn { |
||||
padding: 12px 24px; |
||||
border: none; |
||||
border-radius: 6px; |
||||
cursor: pointer; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.refresh-btn { |
||||
background: #1976d2; |
||||
color: white; |
||||
} |
||||
|
||||
.refresh-btn:hover { |
||||
background: #1565c0; |
||||
} |
||||
|
||||
.edit-btn { |
||||
background: #f39c12; |
||||
color: white; |
||||
} |
||||
|
||||
.edit-btn:hover { |
||||
background: #e67e22; |
||||
} |
||||
|
||||
.password-btn { |
||||
background: #e74c3c; |
||||
color: white; |
||||
} |
||||
|
||||
.password-btn:hover { |
||||
background: #c0392b; |
||||
} |
||||
|
||||
.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; |
||||
} |
||||
|
||||
.empty-state p { |
||||
color: var(--text-secondary); |
||||
margin-bottom: 30px; |
||||
} |
||||
|
||||
.login-btn { |
||||
background: #1976d2; |
||||
color: white; |
||||
border: none; |
||||
padding: 12px 32px; |
||||
border-radius: 6px; |
||||
cursor: pointer; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.login-btn:hover { |
||||
background: #1565c0; |
||||
} |
||||
|
||||
/* 响应式设计 */ |
||||
@media (max-width: 768px) { |
||||
.user-profile { |
||||
padding: 15px; |
||||
} |
||||
|
||||
.profile-header h1 { |
||||
font-size: 2rem; |
||||
} |
||||
|
||||
.info-item { |
||||
flex-direction: column; |
||||
align-items: flex-start; |
||||
gap: 12px; |
||||
} |
||||
|
||||
.info-item label { |
||||
min-width: auto; |
||||
} |
||||
|
||||
.info-item span { |
||||
text-align: left; |
||||
} |
||||
|
||||
.profile-actions { |
||||
flex-direction: column; |
||||
align-items: center; |
||||
} |
||||
|
||||
.action-btn { |
||||
width: 100%; |
||||
max-width: 300px; |
||||
} |
||||
} |
||||
</style> |
Binary file not shown.
Loading…
Reference in new issue