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