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

<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>