- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
510 lines
32 KiB
HTML
510 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>个人中心 - 简记memo</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f8fafc; }
|
|
.nav-link.active { background: rgba(79, 158, 222, 0.1); color: #4F9EDE; }
|
|
.menu-card { transition: all 0.2s; cursor: pointer; }
|
|
.menu-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
|
.stat-card { transition: all 0.2s; }
|
|
.stat-card:hover { transform: translateY(-2px); }
|
|
.avatar-option { cursor: pointer; transition: all 0.2s; border: 2px solid transparent; }
|
|
.avatar-option:hover { transform: scale(1.05); }
|
|
.avatar-option.selected { border-color: #4F9EDE; box-shadow: 0 0 0 3px rgba(79, 158, 222, 0.2); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- 公共头部 -->
|
|
<div id="webHeader"></div>
|
|
|
|
<div style="margin-top: 56px; min-height: calc(100vh - 56px - 40px); padding: 24px;">
|
|
<div style="max-width: 1200px; margin: 0 auto;">
|
|
<!-- 左右布局 -->
|
|
<div style="display: grid; grid-template-columns: 380px 1fr; gap: 24px;">
|
|
<!-- 左侧:用户信息 -->
|
|
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
<!-- 用户信息卡片 -->
|
|
<div style="background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 12px; padding: 32px 24px; color: #fff; text-align: center;">
|
|
<div id="userAvatarContainer" style="width: 100px; height: 100px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; font-size: 40px; font-weight: 600; cursor: pointer; position: relative; overflow: hidden;" onclick="showAvatarModal()">
|
|
U
|
|
<div style="position: absolute; bottom: 0; left: 0; right: 0; height: 28px; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 12px;">更换</div>
|
|
</div>
|
|
<h2 style="font-size: 22px; font-weight: 700; margin-bottom: 6px;" id="userNickname">用户</h2>
|
|
<p style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;" id="userEmail">未绑定邮箱</p>
|
|
<p style="font-size: 13px; opacity: 0.8;" id="userSignature">还没有个性签名</p>
|
|
<button onclick="showEditModal()" style="margin-top: 20px; padding: 10px 24px; background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; font-size: 13px; color: #fff; cursor: pointer;">
|
|
编辑资料
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 快捷入口 -->
|
|
<div style="background: #fff; border-radius: 12px; padding: 20px;">
|
|
<h3 style="font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 16px;">快捷入口</h3>
|
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
<div onclick="navigateTo('/web/calendar')" class="menu-card" style="display: flex; align-items: center; gap: 12px; padding: 14px; border-radius: 10px; background: #f8fafc;">
|
|
<div style="width: 40px; height: 40px; background: linear-gradient(135deg, rgba(79, 158, 222, 0.1) 0%, rgba(79, 158, 222, 0.05) 100%); border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
|
<svg style="width: 20px; height: 20px; fill: none; stroke: #4F9EDE;" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">日程管理</div>
|
|
<div style="font-size: 12px; color: #64748b;">管理每日待办任务</div>
|
|
</div>
|
|
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
</div>
|
|
<div onclick="navigateTo('/web/growth')" class="menu-card" style="display: flex; align-items: center; gap: 12px; padding: 14px; border-radius: 10px; background: #f8fafc;">
|
|
<div style="width: 40px; height: 40px; background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(245, 158, 11, 0.05) 100%); border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
|
<svg style="width: 20px; height: 20px; fill: none; stroke: #f59e0b;" viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">成长中心</div>
|
|
<div style="font-size: 12px; color: #64748b;">查看等级与成就</div>
|
|
</div>
|
|
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 设置 -->
|
|
<div style="background: #fff; border-radius: 12px; padding: 8px;">
|
|
<div onclick="navigateTo('/about')" style="display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; cursor: pointer; border-radius: 8px; transition: all 0.15s;" onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='transparent'">
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<svg style="width: 18px; height: 18px; fill: none; stroke: #64748b;" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
|
<span style="font-size: 14px; color: #1e293b;">关于我们</span>
|
|
</div>
|
|
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
</div>
|
|
<div onclick="navigateTo('/privacy')" style="display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; cursor: pointer; border-radius: 8px; transition: all 0.15s;" onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='transparent'">
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<svg style="width: 18px; height: 18px; fill: none; stroke: #64748b;" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
<span style="font-size: 14px; color: #1e293b;">隐私政策</span>
|
|
</div>
|
|
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧:统计数据 -->
|
|
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
<!-- 统计卡片 -->
|
|
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;">
|
|
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
|
<div style="font-size: 32px; font-weight: 700; color: #4F9EDE;" id="statTodoTotal">0</div>
|
|
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">总待办</div>
|
|
</div>
|
|
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
|
<div style="font-size: 32px; font-weight: 700; color: #22c55e;" id="statTodoDone">0</div>
|
|
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">已完成</div>
|
|
</div>
|
|
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
|
<div style="font-size: 32px; font-weight: 700; color: #f59e0b;" id="statLevel">1</div>
|
|
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">当前等级</div>
|
|
</div>
|
|
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
|
<div style="font-size: 32px; font-weight: 700; color: #8b5cf6;" id="statExp">0</div>
|
|
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">经验值</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 成长进度 -->
|
|
<div style="background: #fff; border-radius: 12px; padding: 24px;">
|
|
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 20px;">成长进度</h3>
|
|
<div style="display: flex; align-items: center; gap: 20px;">
|
|
<div style="width: 80px; height: 80px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 24px; font-weight: 700;">
|
|
Lv.<span id="growthLevel">1</span>
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
|
|
<span style="font-size: 15px; font-weight: 600; color: #1e293b;">当前等级</span>
|
|
<span style="font-size: 13px; color: #64748b;" id="growthExpText">0 / 100 经验</span>
|
|
</div>
|
|
<div style="background: #e2e8f0; border-radius: 4px; height: 10px; overflow: hidden;">
|
|
<div id="growthExpBar" style="height: 100%; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 4px; transition: width 0.5s; width: 0%;"></div>
|
|
</div>
|
|
<p style="font-size: 12px; color: #94a3b8; margin-top: 8px;" id="growthRemain">还需 100 经验升级</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 最近成就 -->
|
|
<div style="background: #fff; border-radius: 12px; padding: 24px; flex: 1;">
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
|
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b;">我的成就</h3>
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<select id="profileAchFilter" onchange="filterProfileAchievements()" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; color: #475569; background: #fff; cursor: pointer;">
|
|
<option value="all">全部分类</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div id="recentAchievements" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px;">
|
|
<!-- 动态渲染 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 公共底部 -->
|
|
<div id="webFooter"></div>
|
|
</div>
|
|
|
|
<!-- 编辑资料弹窗 -->
|
|
<div id="editModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; padding: 24px;">
|
|
<div style="background: #fff; border-radius: 16px; padding: 28px; width: 100%; max-width: 400px;">
|
|
<h3 style="font-size: 18px; font-weight: 600; color: #1e293b; margin-bottom: 24px;">编辑资料</h3>
|
|
<div style="margin-bottom: 20px;">
|
|
<label style="display: block; font-size: 13px; font-weight: 500; color: #475569; margin-bottom: 8px;">昵称</label>
|
|
<input type="text" id="editNickname" placeholder="请输入昵称" style="width: 100%; padding: 12px 16px; border: 1px solid #e2e8f0; border-radius: 10px; font-size: 14px; outline: none;" onfocus="this.style.borderColor='#4F9EDE'" onblur="this.style.borderColor='#e2e8f0'">
|
|
</div>
|
|
<div style="margin-bottom: 28px;">
|
|
<label style="display: block; font-size: 13px; font-weight: 500; color: #475569; margin-bottom: 8px;">签名</label>
|
|
<textarea id="editSignature" placeholder="请输入个性签名" rows="3" style="width: 100%; padding: 12px 16px; border: 1px solid #e2e8f0; border-radius: 10px; font-size: 14px; resize: none; outline: none;" onfocus="this.style.borderColor='#4F9EDE'" onblur="this.style.borderColor='#e2e8f0'"></textarea>
|
|
</div>
|
|
<div style="display: flex; justify-content: flex-end; gap: 12px;">
|
|
<button onclick="closeEditModal()" style="padding: 10px 20px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; color: #475569; cursor: pointer; background: #fff;">取消</button>
|
|
<button onclick="saveProfile()" style="padding: 10px 20px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; font-size: 14px; color: #fff; cursor: pointer;">保存</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 头像选择弹窗 -->
|
|
<div id="avatarModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; padding: 24px;">
|
|
<div style="background: #fff; border-radius: 16px; padding: 28px; width: 100%; max-width: 480px;">
|
|
<h3 style="font-size: 18px; font-weight: 600; color: #1e293b; margin-bottom: 24px;">选择头像</h3>
|
|
<div id="avatarGrid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px;">
|
|
<!-- 动态生成头像选项 -->
|
|
</div>
|
|
<div style="display: flex; justify-content: flex-end; gap: 12px;">
|
|
<button onclick="closeAvatarModal()" style="padding: 10px 20px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; color: #475569; cursor: pointer; background: #fff;">取消</button>
|
|
<button onclick="saveAvatar()" style="padding: 10px 20px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; font-size: 14px; color: #fff; cursor: pointer;">确认</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
|
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
|
<script src="/static/js/config.js"></script>
|
|
<script src="/static/js/toast.js"></script>
|
|
<script src="/static/js/auth.js"></script>
|
|
<script src="/static/js/web-components.js"></script>
|
|
<script>
|
|
// 默认头像列表
|
|
const DEFAULT_AVATARS = [
|
|
{ url: '/static/avatar/ginger_cat.jpg', name: 'ginger_cat', label: '橘猫' },
|
|
{ url: '/static/avatar/ragdoll.jpg', name: 'ragdoll', label: '布偶猫' },
|
|
{ url: '/static/avatar/hamster.jpg', name: 'hamster', label: '小仓鼠' },
|
|
{ url: '/static/avatar/dragon.jpg', name: 'dragon', label: '龙' },
|
|
{ url: '/static/avatar/pony.jpg', name: 'pony', label: '小马' },
|
|
{ url: '/static/avatar/lamb.jpg', name: 'lamb', label: '小羊' },
|
|
{ url: '/static/avatar/corgi.jpg', name: 'corgi', label: '柯基' },
|
|
{ url: '/static/avatar/piglet.jpg', name: 'piglet', label: '小猪' },
|
|
];
|
|
|
|
let selectedAvatarUrl = '';
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// 初始化公共头部底部
|
|
initWebPage('profile');
|
|
// 加载页面数据
|
|
loadUserInfo();
|
|
loadStats();
|
|
loadGrowthInfo();
|
|
renderAvatarGrid();
|
|
});
|
|
|
|
function navigateTo(url) {
|
|
window.location.href = url;
|
|
}
|
|
|
|
function loadUserInfo() {
|
|
const token = localStorage.getItem('user_token');
|
|
const nickname = localStorage.getItem('user_nickname') || '用户';
|
|
const avatar = localStorage.getItem('user_avatar');
|
|
const email = localStorage.getItem('user_email');
|
|
|
|
document.getElementById('userNickname').textContent = nickname;
|
|
// 优先显示localStorage中的邮箱
|
|
if (email) {
|
|
document.getElementById('userEmail').textContent = email;
|
|
}
|
|
|
|
const avatarContainer = document.getElementById('userAvatarContainer');
|
|
if (avatar) {
|
|
avatarContainer.innerHTML = `<img src="${avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;"><div style="position: absolute; bottom: 0; left: 0; right: 0; height: 28px; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 12px;">更换</div>`;
|
|
selectedAvatarUrl = avatar;
|
|
} else {
|
|
avatarContainer.textContent = nickname.charAt(0).toUpperCase();
|
|
}
|
|
|
|
fetch('/api/web/user/info', { headers: { 'token': token } })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.code === 200) {
|
|
const user = data.data;
|
|
document.getElementById('userNickname').textContent = user.nickname || '用户';
|
|
// 接口返回的邮箱优先
|
|
if (user.email) {
|
|
document.getElementById('userEmail').textContent = user.email;
|
|
localStorage.setItem('user_email', user.email);
|
|
}
|
|
if (user.signature) {
|
|
document.getElementById('userSignature').textContent = user.signature;
|
|
}
|
|
if (user.avatar) {
|
|
const container = document.getElementById('userAvatarContainer');
|
|
container.innerHTML = `<img src="${user.avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;"><div style="position: absolute; bottom: 0; left: 0; right: 0; height: 28px; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 12px;">更换</div>`;
|
|
localStorage.setItem('user_avatar', user.avatar);
|
|
selectedAvatarUrl = user.avatar;
|
|
}
|
|
localStorage.setItem('user_nickname', user.nickname || '用户');
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadStats() {
|
|
const token = localStorage.getItem('user_token');
|
|
|
|
// 加载成长信息
|
|
fetch('/api/web/growth/info', { headers: { 'token': token } })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.code === 200) {
|
|
document.getElementById('statLevel').textContent = data.data.level || 1;
|
|
document.getElementById('statExp').textContent = data.data.total_exp || 0;
|
|
}
|
|
});
|
|
|
|
// 加载待办统计
|
|
fetch('/api/web/todo/list', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'token': token },
|
|
body: JSON.stringify({})
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.code === 200) {
|
|
const todos = data.data || [];
|
|
const total = todos.length;
|
|
const done = todos.filter(t => t.done === 1 || t.status === 1).length;
|
|
document.getElementById('statTodoTotal').textContent = total;
|
|
document.getElementById('statTodoDone').textContent = done;
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadGrowthInfo() {
|
|
const token = localStorage.getItem('user_token');
|
|
|
|
fetch('/api/web/growth/info', { headers: { 'token': token } })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.code === 200) {
|
|
const g = data.data;
|
|
document.getElementById('growthLevel').textContent = g.level || 1;
|
|
|
|
// 直接使用后端返回的字段
|
|
const currentExp = g.currentExp || 0;
|
|
const expToNext = g.nextLevelExp || 100;
|
|
const progress = g.levelProgress || 0;
|
|
|
|
document.getElementById('growthExpText').textContent = currentExp + ' / ' + expToNext + ' 经验';
|
|
document.getElementById('growthRemain').textContent = '还需 ' + expToNext + ' 经验升级';
|
|
document.getElementById('growthExpBar').style.width = Math.min(progress, 100) + '%';
|
|
|
|
// 更新统计数据
|
|
document.getElementById('statLevel').textContent = g.level || 1;
|
|
document.getElementById('statExp').textContent = g.experience || 0;
|
|
|
|
// 渲染成就
|
|
renderProfileAchievements(g.achievements?.list || []);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 存储成就数据用于筛选
|
|
let profileAchievementsData = [];
|
|
let profileAchCategories = [];
|
|
|
|
// 渲染个人中心成就
|
|
function renderProfileAchievements(unlockedList) {
|
|
const token = localStorage.getItem('user_token');
|
|
|
|
// 获取成就分类
|
|
fetch('/api/web/growth/rules', { headers: { 'token': token } })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.code === 200) {
|
|
const sections = data.data.sections;
|
|
const achSection = sections.find(s => s.type === 'achievement_system');
|
|
if (achSection && achSection.categories) {
|
|
profileAchCategories = achSection.categories;
|
|
// 更新分类下拉框 - 使用小写字段名
|
|
const select = document.getElementById('profileAchFilter');
|
|
if (select) {
|
|
select.innerHTML = '<option value="all">全部分类</option>' +
|
|
profileAchCategories.map(c => `<option value="${c.key || c.Key}">${c.name || c.Name}</option>`).join('');
|
|
}
|
|
}
|
|
|
|
// 按稀有度排序(传说 > 史诗 > 稀有 > 高级 > 中级 > 初级)
|
|
const rarityOrder = { '传说': 6, '史诗': 5, '稀有': 4, '高级': 3, '中级': 2, '初级': 1 };
|
|
profileAchievementsData = (unlockedList || []).sort((a, b) => {
|
|
return (rarityOrder[b.rarityName || b.rarity] || 0) - (rarityOrder[a.rarityName || a.rarity] || 0);
|
|
});
|
|
|
|
renderProfileAchList('all');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('加载成就分类失败:', err);
|
|
// 即使没有分类,也尝试渲染成就
|
|
profileAchievementsData = unlockedList || [];
|
|
renderProfileAchList('all');
|
|
});
|
|
}
|
|
|
|
function filterProfileAchievements() {
|
|
const filter = document.getElementById('profileAchFilter').value;
|
|
renderProfileAchList(filter);
|
|
}
|
|
|
|
function renderProfileAchList(filter) {
|
|
const container = document.getElementById('recentAchievements');
|
|
|
|
let filtered = profileAchievementsData;
|
|
if (filter !== 'all') {
|
|
filtered = profileAchievementsData.filter(ach => ach.category === filter);
|
|
}
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #94a3b8; grid-column: 1/-1;"><span style="font-size: 48px; display: block; margin-bottom: 12px;">🏆</span><p>' + (filter === 'all' ? '暂无成就,继续努力吧!' : '该分类暂无成就') + '</p></div>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
filtered.forEach(ach => {
|
|
const color = ach.rarityColor || '#4F9EDE';
|
|
const icon = ach.icon || '🏆';
|
|
const name = ach.name || '未知成就';
|
|
const desc = ach.description || ach.desc || '';
|
|
const rarityName = ach.rarityName || ach.rarity || '';
|
|
|
|
html += `
|
|
<div style="text-align: center; padding: 16px; border-radius: 12px; background: linear-gradient(135deg, rgba(79,158,222,0.08) 0%, rgba(79,158,222,0.02) 100%);">
|
|
<div style="width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; background: ${color}20;">
|
|
<span style="font-size: 20px;">${icon}</span>
|
|
</div>
|
|
<h4 style="font-size: 13px; font-weight: 600; color: ${color}; margin-bottom: 2px;">${name}</h4>
|
|
${desc ? '<p style="font-size: 11px; color: #64748b; margin-bottom: 4px;">' + desc + '</p>' : ''}
|
|
${rarityName ? '<span style="display: inline-block; padding: 2px 8px; background: ' + color + '15; color: ' + color + '; font-size: 10px; border-radius: 4px;">' + rarityName + '</span>' : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderAvatarGrid() {
|
|
const container = document.getElementById('avatarGrid');
|
|
let html = '';
|
|
DEFAULT_AVATARS.forEach(avatar => {
|
|
const isSelected = selectedAvatarUrl === avatar.url;
|
|
html += `
|
|
<div class="avatar-option ${isSelected ? 'selected' : ''}" onclick="selectAvatar('${avatar.url}', this)" style="border-radius: 50%; overflow: hidden; width: 80px; height: 80px; margin: 0 auto;">
|
|
<img src="${avatar.url}" style="width: 100%; height: 100%; object-fit: cover;" alt="${avatar.label}">
|
|
</div>
|
|
`;
|
|
});
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function selectAvatar(url, element) {
|
|
selectedAvatarUrl = url;
|
|
document.querySelectorAll('.avatar-option').forEach(el => el.classList.remove('selected'));
|
|
element.classList.add('selected');
|
|
}
|
|
|
|
function showAvatarModal() {
|
|
renderAvatarGrid();
|
|
document.getElementById('avatarModal').style.display = 'flex';
|
|
}
|
|
|
|
function closeAvatarModal() {
|
|
document.getElementById('avatarModal').style.display = 'none';
|
|
}
|
|
|
|
function saveAvatar() {
|
|
if (!selectedAvatarUrl) {
|
|
$toast.error('请选择头像');
|
|
return;
|
|
}
|
|
|
|
const token = localStorage.getItem('user_token');
|
|
fetch('/api/web/user/edit', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'token': token },
|
|
body: JSON.stringify({ avatar: selectedAvatarUrl })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.code === 200) {
|
|
$toast.success('头像更换成功');
|
|
localStorage.setItem('user_avatar', selectedAvatarUrl);
|
|
loadUserInfo();
|
|
closeAvatarModal();
|
|
} else {
|
|
$toast.error(data.msg || '更换失败');
|
|
}
|
|
});
|
|
}
|
|
|
|
function showEditModal() {
|
|
const nickname = document.getElementById('userNickname').textContent;
|
|
const signature = document.getElementById('userSignature').textContent;
|
|
document.getElementById('editNickname').value = nickname;
|
|
document.getElementById('editSignature').value = signature === '还没有个性签名' ? '' : signature;
|
|
document.getElementById('editModal').style.display = 'flex';
|
|
}
|
|
|
|
function closeEditModal() {
|
|
document.getElementById('editModal').style.display = 'none';
|
|
}
|
|
|
|
function saveProfile() {
|
|
const token = localStorage.getItem('user_token');
|
|
const nickname = document.getElementById('editNickname').value;
|
|
const signature = document.getElementById('editSignature').value;
|
|
|
|
if (!nickname) {
|
|
$toast.error('请输入昵称');
|
|
return;
|
|
}
|
|
|
|
fetch('/api/web/user/edit', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'token': token },
|
|
body: JSON.stringify({ nickname, signature })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.code === 200) {
|
|
$toast.success('保存成功');
|
|
closeEditModal();
|
|
document.getElementById('userNickname').textContent = nickname;
|
|
document.getElementById('userSignature').textContent = signature || '还没有个性签名';
|
|
localStorage.setItem('user_nickname', nickname);
|
|
} else {
|
|
$toast.error(data.msg || '保存失败');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|