|
|
|
|
|
<!doctype html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<title>个人信息修改</title>
|
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
|
|
<meta name="description" content="个人信息修改页面" details="查看和修改用户的基本信息、联系方式等">
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body {
|
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
|
background-color: var(--bg-color-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.profile-container {
|
|
|
|
|
|
max-width: 1000px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.profile-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.profile-title {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 用户头像卡片 */
|
|
|
|
|
|
.avatar-section {
|
|
|
|
|
|
background: linear-gradient(135deg, var(--color-primary), color-mix(in srgb, var(--color-primary), black 20%));
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: 32px 24px;
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-container {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-preview {
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
height: 80px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-placeholder {
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
height: 80px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-info h3 {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin: 0 0 4px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-info p {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表单样式 */
|
|
|
|
|
|
.profile-section {
|
|
|
|
|
|
background: var(--bg-color);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
|
padding-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-icon {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-color);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--text-color);
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-description {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--text-color-secondary);
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 头像输入特殊样式 */
|
|
|
|
|
|
.avatar-input-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-preview-small {
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
border: 2px solid var(--border-color);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-placeholder-small {
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: var(--bg-color-secondary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--text-color-secondary);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
border: 2px solid var(--border-color);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 保存按钮区域 */
|
|
|
|
|
|
.save-section {
|
|
|
|
|
|
background: var(--bg-color);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
bottom: var(--spacing-lg);
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-info {
|
|
|
|
|
|
color: var(--text-color-secondary);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-info.changed {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 加载状态 */
|
|
|
|
|
|
.loading-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-spinner {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-loading {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.profile-container {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-section {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-input-group {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="profile-container">
|
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
|
<div class="profile-header">
|
|
|
|
|
|
<h1 class="profile-title">个人信息</h1>
|
|
|
|
|
|
<v-btn icon variant="ghost" :click="loadUserData" title="刷新">
|
|
|
|
|
|
<i class="fa-solid fa-rotate-right"></i>
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Avatar Section -->
|
|
|
|
|
|
<div class="avatar-section">
|
|
|
|
|
|
<div class="avatar-container">
|
|
|
|
|
|
<img v-if="user.icon" :src="user.icon" class="avatar-preview" alt="用户头像">
|
|
|
|
|
|
<div v-else class="avatar-placeholder">
|
|
|
|
|
|
{{ user.username ? user.username.charAt(0).toUpperCase() : 'U' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="avatar-info">
|
|
|
|
|
|
<h3>{{ user.nickname || user.username || '未设置昵称' }}</h3>
|
|
|
|
|
|
<p>用户ID: {{ user.id }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Basic Info Section -->
|
|
|
|
|
|
<div class="profile-section section-loading">
|
|
|
|
|
|
<div v-if="isLoading" class="loading-overlay">
|
|
|
|
|
|
<i class="fa-solid fa-spinner fa-spin loading-spinner"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<i class="fa-solid fa-user section-icon"></i>
|
|
|
|
|
|
<h2 class="section-title">基本信息</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">用户名</label>
|
|
|
|
|
|
<v-input v:value="user.username" placeholder="请输入用户名" :disabled="isLoading"></v-input>
|
|
|
|
|
|
<div class="form-description">用户名用于登录,建议使用英文或数字</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">昵称</label>
|
|
|
|
|
|
<v-input v:value="user.nickname" placeholder="请输入昵称" :disabled="isLoading"></v-input>
|
|
|
|
|
|
<div class="form-description">昵称将在页面中显示,可以使用中文</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">头像URL</label>
|
|
|
|
|
|
<div class="avatar-input-group">
|
|
|
|
|
|
<div class="avatar-input">
|
|
|
|
|
|
<v-input v:value="user.icon" placeholder="请输入头像图片URL" :disabled="isLoading"></v-input>
|
|
|
|
|
|
<div class="form-description">输入图片链接地址,支持jpg、png、gif格式</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<img v-if="user.icon" :src="user.icon" class="avatar-preview-small" alt="头像预览">
|
|
|
|
|
|
<div v-else class="avatar-placeholder-small">无</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Contact Info Section -->
|
|
|
|
|
|
<div class="profile-section section-loading">
|
|
|
|
|
|
<div v-if="isLoading" class="loading-overlay">
|
|
|
|
|
|
<i class="fa-solid fa-spinner fa-spin loading-spinner"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<i class="fa-solid fa-address-book section-icon"></i>
|
|
|
|
|
|
<h2 class="section-title">联系方式</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">电子邮箱</label>
|
|
|
|
|
|
<v-input v:value="user.email" placeholder="请输入电子邮箱" :disabled="isLoading"></v-input>
|
|
|
|
|
|
<div class="form-description">用于接收重要通知和找回密码</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">手机号码</label>
|
|
|
|
|
|
<v-input v:value="user.phone" placeholder="请输入手机号码" :disabled="isLoading"></v-input>
|
|
|
|
|
|
<div class="form-description">用于接收验证码和安全提醒</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Save Section -->
|
|
|
|
|
|
<div class="save-section">
|
|
|
|
|
|
<div class="save-info" :class="{ changed: hasChanges }">
|
|
|
|
|
|
{{ hasChanges ? '您有未保存的更改' : '所有信息已保存' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<v-btn :click="saveProfile" :disabled="!hasChanges || isSaving" :loading="isSaving">
|
|
|
|
|
|
{{ isSaving ? '保存中...' : '保存修改' }}
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
// 初始化用户数据
|
|
|
|
|
|
user = {
|
|
|
|
|
|
id: $G.token?.body()?.uid,
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
nickname: "",
|
|
|
|
|
|
icon: "",
|
|
|
|
|
|
email: "",
|
|
|
|
|
|
phone: "",
|
|
|
|
|
|
status: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 原始用户数据,用于比较变更
|
|
|
|
|
|
originalUser = {}
|
|
|
|
|
|
|
|
|
|
|
|
// UI状态
|
|
|
|
|
|
isLoading = false
|
|
|
|
|
|
isSaving = false
|
|
|
|
|
|
hasChanges = false
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有变更
|
|
|
|
|
|
checkForChanges = () => {
|
|
|
|
|
|
let changes = [
|
|
|
|
|
|
user.username !== originalUser.username,
|
|
|
|
|
|
user.nickname !== originalUser.nickname,
|
|
|
|
|
|
user.icon !== originalUser.icon,
|
|
|
|
|
|
user.email !== originalUser.email,
|
|
|
|
|
|
user.phone !== originalUser.phone
|
|
|
|
|
|
]
|
|
|
|
|
|
hasChanges = changes.some(change => change)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载用户数据
|
|
|
|
|
|
loadUserData = async () => {
|
|
|
|
|
|
isLoading = true
|
|
|
|
|
|
const response = await $axios.get("/api/user/" + user.id).catch(error => {
|
|
|
|
|
|
console.log(error)
|
|
|
|
|
|
$message.error("加载用户数据失败")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (response) {
|
|
|
|
|
|
// 确保所有字段都有值,避免 undefined
|
|
|
|
|
|
user = {
|
|
|
|
|
|
id: response.id,
|
|
|
|
|
|
username: response.username || "",
|
|
|
|
|
|
nickname: response.nickname || "",
|
|
|
|
|
|
icon: response.icon || "",
|
|
|
|
|
|
email: response.email || "",
|
|
|
|
|
|
phone: response.phone || "",
|
|
|
|
|
|
status: response.status || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
// 保存原始数据
|
|
|
|
|
|
originalUser = JSON.parse(JSON.stringify(user))
|
|
|
|
|
|
hasChanges = false
|
|
|
|
|
|
}
|
|
|
|
|
|
isLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存修改
|
|
|
|
|
|
saveProfile = async () => {
|
|
|
|
|
|
checkForChanges()
|
|
|
|
|
|
if (!hasChanges || isSaving) return
|
|
|
|
|
|
|
|
|
|
|
|
isSaving = true
|
|
|
|
|
|
|
|
|
|
|
|
// 准备更新数据
|
|
|
|
|
|
const updateData = {
|
|
|
|
|
|
username: user.username || null,
|
|
|
|
|
|
nickname: user.nickname || null,
|
|
|
|
|
|
icon: user.icon || null,
|
|
|
|
|
|
email: user.email || null,
|
|
|
|
|
|
phone: user.phone || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送更新请求
|
|
|
|
|
|
const response = await $axios.patch("/api/user/" + user.id, updateData).catch(error => {
|
|
|
|
|
|
$message.error("保存失败: " + error.message || "未知错误")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (response) {
|
|
|
|
|
|
// 更新本地数据
|
|
|
|
|
|
user = {
|
|
|
|
|
|
...user,
|
|
|
|
|
|
username: response.username || user.username,
|
|
|
|
|
|
nickname: response.nickname || user.nickname,
|
|
|
|
|
|
icon: response.icon || user.icon,
|
|
|
|
|
|
email: response.email || user.email,
|
|
|
|
|
|
phone: response.phone || user.phone
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新原始数据
|
|
|
|
|
|
originalUser = JSON.parse(JSON.stringify(user))
|
|
|
|
|
|
hasChanges = false
|
|
|
|
|
|
|
|
|
|
|
|
$message.success("个人信息更新成功!")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isSaving = false
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// 页面加载时获取用户数据
|
|
|
|
|
|
$data.loadUserData()
|
|
|
|
|
|
|
|
|
|
|
|
// 监听用户数据变化
|
|
|
|
|
|
$watch(() => {
|
|
|
|
|
|
checkForChanges()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 页面离开前提醒未保存的更改
|
|
|
|
|
|
window.addEventListener('beforeunload', (event) => {
|
|
|
|
|
|
if ($data.hasChanges) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
event.returnValue = '您有未保存的更改,确定要离开页面吗?'
|
|
|
|
|
|
return event.returnValue
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
</html>
|