You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/page/profile.html

599 lines
13 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!doctype html>
<html>
<head>
<title>个人信息修改</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<style>
body {
padding: 24px;
}
.profile-container {
max-width: 1000px;
margin: 0 auto;
}
.profile-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}
.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 {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
color: #333;
background: white;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: #6c5ce7;
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;
border: 2px solid #e0e0e0;
flex-shrink: 0;
}
.avatar-placeholder-small {
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;
border: none;
border-radius: 8px;
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);
}
.save-btn.saving {
background: #28a745;
cursor: not-allowed;
}
/* 消息提示 */
.message {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
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>
<body>
<div class="profile-container">
<!-- Header -->
<div class="profile-header">
<h1 class="profile-title">个人信息</h1>
<button class="refresh-btn" @click="loadUserData">
<i class="fa-solid fa-rotate-right"></i>
</button>
</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" v-if="!isLoading">
<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>
<input type="text" class="form-input" v:value="user.username" placeholder="请输入用户名">
<div class="form-description">用户名用于登录,建议使用英文或数字</div>
</div>
<div class="form-group">
<label class="form-label">昵称</label>
<input type="text" class="form-input" v:value="user.nickname" 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" v:value="user.icon" 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>
<!-- 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 class="form-group">
<label class="form-label">电子邮箱</label>
<input type="email" class="form-input" v:value="user.email" placeholder="请输入电子邮箱">
<div class="form-description">用于接收重要通知和找回密码</div>
</div>
<div class="form-group">
<label class="form-label">手机号码</label>
<input type="tel" class="form-input" v:value="user.phone" 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>
<script setup>
// 初始化用户数据
user = {
id: $G.token?.body()?.uid,
username: '',
nickname: "",
icon: "",
email: "",
phone: "",
status: 0
}
// 原始用户数据,用于比较变更
originalUser = {}
// UI状态
errorMessage = ""
successMessage = ""
isLoading = false
isSaving = false
hasChanges = false
showErrorMessage = false
showSuccessMessage = false
// 更新字段
updateField = (field, value) => {
user[field] = value
}
// 检查是否有变更
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)
}
// 显示错误消息
showError = (message) => {
errorMessage = message
showErrorMessage = true
setTimeout(() => {
showErrorMessage = false
}, 5000)
}
// 显示成功消息
showSuccess = (message) => {
successMessage = message
showSuccessMessage = true
setTimeout(() => {
showSuccessMessage = false
}, 3000)
}
// 加载用户数据
loadUserData = async () => {
isLoading = true
const response = await $axios.get("/api/user/" + user.id).catch(error => {
console.log(error)
})
if (response) {
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
}
try {
} catch (error) {
showError("加载用户数据失败: " + error.message)
} finally {
isLoading = false
}
}
// 保存修改
saveProfile = async () => {
try {
checkForChanges()
if (!hasChanges || isSaving) return
errorMessage = ""
successMessage = ""
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)
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
showSuccess("个人信息更新成功!")
}
} catch (error) {
showError("保存失败: " + error)
} finally {
isSaving = false
}
}
</script>
<script>
// 页面加载时获取用户数据
$data.loadUserData()
// 监听用户数据变化
$watch(() => {
checkForChanges()
})
// 页面离开前提醒未保存的更改
window.addEventListener('beforeunload', (event) => {
if ($data.hasChanges) {
event.preventDefault()
event.returnValue = '您有未保存的更改,确定要离开页面吗?'
return event.returnValue
}
})
</script>
</html>