v3
veypi 3 months ago
parent 1a29442c1c
commit 96acf05fb6

@ -13,8 +13,8 @@ type User struct {
Nickname string `json:"nickname" gorm:"type:varchar(100)" parse:"json"` Nickname string `json:"nickname" gorm:"type:varchar(100)" parse:"json"`
Icon string `json:"icon" parse:"json"` Icon string `json:"icon" parse:"json"`
Email string `json:"email" gorm:"unique;type:varchar(50);default:null" parse:"json"` Email string `json:"email" gorm:"unique;type:varchar(64);null;default:null" parse:"json"`
Phone string `json:"phone" gorm:"type:varchar(30);unique;default:null" parse:"json"` Phone string `json:"phone" gorm:"type:varchar(64);unique;null;default:null" parse:"json"`
Status uint `json:"status" parse:"json"` Status uint `json:"status" parse:"json"`

@ -8,104 +8,447 @@
</head> </head>
<style> <style>
body { body {
max-width: 600px; padding: 24px;
}
.profile-container {
max-width: 1000px;
margin: 0 auto; margin: 0 auto;
border-radius: 8px; }
padding: 20px;
.profile-header {
display: flex; display: flex;
height: 100%; align-items: center;
flex-direction: column; gap: 8px;
justify-content: start; margin-bottom: 24px;
gap: 2rem; }
.profile-title {
font-size: 24px;
font-weight: 600;
color: #333;
}
.refresh-btn {
background: none;
border: none;
color: #666;
font-size: 16px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
}
.refresh-btn:hover {
color: #6c5ce7;
background: #f8f7ff;
}
/* 用户头像卡片 */
.avatar-section {
background: linear-gradient(135deg, #6c5ce7, #5a4fcf);
color: white;
border-radius: 12px;
padding: 32px 24px;
margin-bottom: 24px;
text-align: center;
}
.avatar-container {
position: relative;
display: inline-block;
margin-bottom: 16px;
}
.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: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.section-icon {
width: 24px;
height: 24px;
color: #6c5ce7;
font-size: 18px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
} }
.form-group { .form-group {
margin-bottom: 15px; margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
} }
label { .form-label {
display: block; display: block;
margin-bottom: 5px; font-size: 14px;
font-weight: bold; font-weight: 500;
color: #333;
margin-bottom: 8px;
} }
input[type="text"], .form-input {
input[type="email"],
input[type="tel"] {
width: 100%; width: 100%;
padding: 8px; padding: 12px 16px;
border: 1px solid #ddd; border: 2px solid #e0e0e0;
border-radius: 4px; border-radius: 8px;
box-sizing: border-box; font-size: 16px;
color: #333;
background: white;
transition: all 0.2s ease;
} }
.avatar-preview { .form-input:focus {
width: 100px; outline: none;
height: 100px; border-color: #6c5ce7;
border-radius: 50%; box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.1);
}
.form-input:disabled {
background: #f8f9fa;
color: #666;
cursor: not-allowed;
}
.form-description {
font-size: 12px;
color: #666;
margin-top: 4px;
line-height: 1.4;
}
/* 头像输入特殊样式 */
.avatar-input-group {
display: flex;
gap: 12px;
align-items: flex-start;
}
.avatar-input {
flex: 1;
}
.avatar-preview-small {
width: 48px;
height: 48px;
border-radius: 8px;
object-fit: cover; object-fit: cover;
margin: 0 auto; border: 2px solid #e0e0e0;
margin-bottom: 10px; flex-shrink: 0;
} }
.btn { .avatar-placeholder-small {
background-color: #4CAF50; width: 48px;
height: 48px;
border-radius: 8px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
border: 2px solid #e0e0e0;
flex-shrink: 0;
}
/* 保存按钮区域 */
.save-section {
background: #f8f9fa;
border-radius: 12px;
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #e0e0e0;
}
.save-info {
color: #666;
font-size: 14px;
}
.save-info.changed {
color: #6c5ce7;
font-weight: 500;
}
.save-btn {
padding: 12px 24px;
background: #6c5ce7;
color: white; color: white;
padding: 10px 15px;
border: none; border: none;
border-radius: 4px; border-radius: 8px;
cursor: pointer;
font-size: 16px; font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
opacity: 0.5;
pointer-events: none;
}
.save-btn.enabled {
opacity: 1;
pointer-events: auto;
}
.save-btn:hover.enabled {
background: #5a4fcf;
transform: translateY(-1px);
} }
.btn:hover { .save-btn.saving {
background-color: #45a049; background: #28a745;
cursor: not-allowed;
} }
.error-message { /* 消息提示 */
color: red; .message {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
font-size: 14px; font-size: 14px;
margin-top: 5px; font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateX(400px);
transition: transform 0.3s ease;
z-index: 1000;
}
.message.show {
transform: translateX(0);
}
.message.error {
background: #dc3545;
color: white;
}
.message.success {
background: #28a745;
color: white;
}
/* 加载状态 */
.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: 12px;
z-index: 10;
}
.loading-spinner {
color: #6c5ce7;
font-size: 24px;
}
.section-loading {
position: relative;
}
@media (max-width: 768px) {
.profile-container {
padding: 0 12px;
}
.profile-section {
padding: 20px 16px;
}
.avatar-section {
padding: 24px 16px;
}
.avatar-input-group {
flex-direction: column;
}
.save-section {
flex-direction: column;
gap: 12px;
text-align: center;
}
} }
</style> </style>
<body> <body>
<h1 class="text-2xl text-center">个人信息修改</h1> <div class="profile-container">
<div class="form-group"> <!-- Header -->
<label>用户名</label> <div class="profile-header">
<input type="text" !value="user.username" @input="user.username = $event.target.value" placeholder="请输入用户名"> <h1 class="profile-title">个人信息</h1>
</div> <button class="refresh-btn" @click="loadUserData">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<div class="form-group"> <!-- Avatar Section -->
<label>昵称</label> <div class="avatar-section">
<input type="text" !value="user.nickname" @input="user.nickname = $event.target.value" placeholder="请输入昵称"> <div class="avatar-container">
</div> <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>
<div class="form-group"> <!-- Basic Info Section -->
<label>头像URL</label> <div class="profile-section section-loading" v-if="!isLoading">
<img v-if="user.icon" :src="user.icon" class="avatar-preview"> <div class="section-header">
<input type="text" !value="user.icon" @input="user.icon = $event.target.value" placeholder="请输入头像URL"> <i class="fa-solid fa-user section-icon"></i>
</div> <h2 class="section-title">基本信息</h2>
</div>
<div class="form-group"> <div class="form-group">
<label>电子邮箱</label> <label class="form-label">用户名</label>
<input type="email" !value="user.email" @input="user.email = $event.target.value" placeholder="请输入电子邮箱"> <input type="text" class="form-input" !value="user.username"
</div> @input="updateField('username', $event.target.value)" placeholder="请输入用户名">
<div class="form-description">用户名用于登录,建议使用英文或数字</div>
</div>
<div class="form-group"> <div class="form-group">
<label>手机号码</label> <label class="form-label">昵称</label>
<input type="tel" !value="user.phone" @input="user.phone = $event.target.value" placeholder="请输入手机号码"> <input type="text" class="form-input" !value="user.nickname"
</div> @input="updateField('nickname', $event.target.value)" placeholder="请输入昵称">
<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">
<input type="text" class="form-input" !value="user.icon" @input="updateField('icon', $event.target.value)"
placeholder="请输入头像图片URL">
<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>
<button @click="saveProfile" class="btn">保存修改</button> <!-- Contact Info Section -->
<div class="profile-section section-loading" v-if="!isLoading">
<div class="section-header">
<i class="fa-solid fa-address-book section-icon"></i>
<h2 class="section-title">联系方式</h2>
</div>
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div> <div class="form-group">
<div v-if="successMessage" style="color: green; margin-top: 10px;">{{ successMessage }}</div> <label class="form-label">电子邮箱</label>
<input type="email" class="form-input" !value="user.email" @input="updateField('email', $event.target.value)"
placeholder="请输入电子邮箱">
<div class="form-description">用于接收重要通知和找回密码</div>
</div>
<div class="form-group">
<label class="form-label">手机号码</label>
<input type="tel" class="form-input" !value="user.phone" @input="updateField('phone', $event.target.value)"
placeholder="请输入手机号码">
<div class="form-description">用于接收验证码和安全提醒</div>
</div>
</div>
<!-- Loading State -->
<div class="profile-section" v-if="isLoading">
<div class="loading-overlay">
<i class="fa-solid fa-spinner fa-spin loading-spinner"></i>
</div>
<div style="height: 200px;"></div>
</div>
<!-- Save Section -->
<div class="save-section" v-if="!isLoading">
<div class="save-info" :class="{ changed: hasChanges }">
{{ hasChanges ? '您有未保存的更改' : '所有信息已保存' }}
</div>
<button class="save-btn" :class="{ enabled: hasChanges && !isSaving, saving: isSaving }" @click="saveProfile"
:disabled="!hasChanges || isSaving">
{{ isSaving ? '保存中...' : '保存修改' }}
</button>
</div>
<!-- Messages -->
<div class="message error" :class="{ show: showErrorMessage }">
<i class="fa-solid fa-exclamation-circle"></i>
{{ errorMessage }}
</div>
<div class="message success" :class="{ show: showSuccessMessage }">
<i class="fa-solid fa-check-circle"></i>
{{ successMessage }}
</div>
</div>
</body> </body>
<script setup> <script setup>
// 初始化用户数据 // 初始化用户数据
user = { user = {
id: $G.token?.body()?.uid, id: $G.token?.body()?.uid,
username: '', username: '',
@ -114,17 +457,60 @@
email: "", email: "",
phone: "", phone: "",
status: 0 status: 0
}; }
// 原始用户数据,用于比较变更
originalUser = {}
errorMessage = ""; // UI状态
successMessage = ""; errorMessage = ""
isLoading = false; successMessage = ""
isLoading = false
isSaving = false
hasChanges = false
showErrorMessage = false
showSuccessMessage = false
// 更新字段
updateField = (field, value) => {
user[field] = value
checkForChanges()
}
// 检查是否有变更
checkForChanges = () => {
hasChanges = (
user.username !== originalUser.username ||
user.nickname !== originalUser.nickname ||
user.icon !== originalUser.icon ||
user.email !== originalUser.email ||
user.phone !== originalUser.phone
)
}
// 显示错误消息
showError = (message) => {
errorMessage = message
showErrorMessage = true
setTimeout(() => {
showErrorMessage = false
}, 5000)
}
// 显示成功消息
showSuccess = (message) => {
successMessage = message
showSuccessMessage = true
setTimeout(() => {
showSuccessMessage = false
}, 3000)
}
// 加载用户数据 // 加载用户数据
loadUserData = async () => { loadUserData = async () => {
try { try {
isLoading = true; isLoading = true
const response = await $axios.get("/api/user/" + user.id); const response = await $axios.get("/api/user/" + user.id)
if (response) { if (response) {
user = { user = {
id: response.id, id: response.id,
@ -134,21 +520,26 @@
email: response.email || "", email: response.email || "",
phone: response.phone || "", phone: response.phone || "",
status: response.status || 0 status: response.status || 0
}; }
// 保存原始数据
originalUser = JSON.parse(JSON.stringify(user))
hasChanges = false
} }
} catch (error) { } catch (error) {
errorMessage = "加载用户数据失败: " + error.message; showError("加载用户数据失败: " + error.message)
} finally { } finally {
isLoading = false; isLoading = false
} }
}; }
// 保存修改 // 保存修改
saveProfile = async () => { saveProfile = async () => {
try { try {
errorMessage = ""; if (!hasChanges || isSaving) return
successMessage = "";
isLoading = true; errorMessage = ""
successMessage = ""
isSaving = true
// 准备更新数据 // 准备更新数据
const updateData = { const updateData = {
@ -157,13 +548,12 @@
icon: user.icon || null, icon: user.icon || null,
email: user.email || null, email: user.email || null,
phone: user.phone || null phone: user.phone || null
}; }
// 发送更新请求 // 发送更新请求
const response = await $axios.patch("/api/user/" + user.id, updateData); const response = await $axios.patch("/api/user/" + user.id, updateData)
if (response) { if (response) {
successMessage = "个人信息更新成功!";
// 更新本地数据 // 更新本地数据
user = { user = {
...user, ...user,
@ -172,18 +562,41 @@
icon: response.icon || user.icon, icon: response.icon || user.icon,
email: response.email || user.email, email: response.email || user.email,
phone: response.phone || user.phone phone: response.phone || user.phone
}; }
// 更新原始数据
originalUser = JSON.parse(JSON.stringify(user))
hasChanges = false
showSuccess("个人信息更新成功!")
} }
} catch (error) { } catch (error) {
errorMessage = "保存失败: " + error; showError("保存失败: " + error)
} finally { } finally {
isLoading = false; isSaving = false
} }
}; }
</script> </script>
<script> <script>
loadUserData(); // 页面加载时获取用户数据
$data.loadUserData()
// 监听用户数据变化
$watch(() => {
if ($data.hasChanges) {
console.log('Profile has unsaved changes')
}
})
// 页面离开前提醒未保存的更改
window.addEventListener('beforeunload', (event) => {
if ($data.hasChanges) {
event.preventDefault()
event.returnValue = '您有未保存的更改,确定要离开页面吗?'
return event.returnValue
}
})
</script> </script>
</html> </html>

Loading…
Cancel
Save