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

460 lines
11 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" />
<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>