327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
(function () {
|
||
const body = document.body;
|
||
const currentUser = (() => {
|
||
try {
|
||
return JSON.parse(body.dataset.user || 'null');
|
||
} catch {
|
||
return null;
|
||
}
|
||
})();
|
||
|
||
const profileUsername = String(body.dataset.profileUsername || '').trim().toLowerCase();
|
||
const subtitleEl = document.getElementById('profile-subtitle');
|
||
const heroEl = document.getElementById('profile-hero');
|
||
const metaEl = document.getElementById('profile-meta');
|
||
const achievementSummaryEl = document.getElementById('achievement-summary');
|
||
const achievementDetailLinkEl = document.getElementById('achievement-detail-link');
|
||
const achievementsListEl = document.getElementById('achievements-list');
|
||
const recentRoomsEl = document.getElementById('recent-rooms');
|
||
const invitePanelEl = document.getElementById('invite-panel');
|
||
const statusEl = document.getElementById('status');
|
||
|
||
const setStatus = (text) => {
|
||
statusEl.textContent = text || '';
|
||
};
|
||
|
||
const getJson = async (url) => {
|
||
const res = await fetch(url);
|
||
return res.json();
|
||
};
|
||
|
||
const postJson = async (url, payload) => {
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
return res.json();
|
||
};
|
||
|
||
const levelLabel = (level) => {
|
||
const map = {
|
||
bronze: '青铜',
|
||
silver: '白银',
|
||
gold: '黄金',
|
||
platinum: '铂金',
|
||
diamond: '钻石',
|
||
};
|
||
return map[String(level || '').toLowerCase()] || '青铜';
|
||
};
|
||
|
||
const renderAchievements = (bundle) => {
|
||
const summary = bundle?.summary || {};
|
||
const items = Array.isArray(bundle?.items) ? bundle.items : [];
|
||
|
||
if (achievementSummaryEl) {
|
||
const total = Number(summary.total_unlocked || 0);
|
||
const systemCount = Number(summary.system_unlocked || 0);
|
||
const cardCount = Number(summary.card_unlocked || 0);
|
||
const best = levelLabel(summary.highest_level || '');
|
||
achievementSummaryEl.textContent = total
|
||
? `已解锁 ${total} 项成就(系统 ${systemCount} / 卡片 ${cardCount}),最高等级:${best}`
|
||
: '暂未解锁成就,完成对局后会自动点亮。';
|
||
}
|
||
|
||
if (!achievementsListEl) return;
|
||
achievementsListEl.textContent = '';
|
||
if (!items.length) {
|
||
const li = document.createElement('li');
|
||
li.textContent = '暂无成就';
|
||
achievementsListEl.appendChild(li);
|
||
return;
|
||
}
|
||
|
||
items.slice(0, 24).forEach((item) => {
|
||
const li = document.createElement('li');
|
||
const scope = item.source === 'card' ? `卡片·${item.card_title || '未知剧本'}` : '系统';
|
||
const level = levelLabel(item.level);
|
||
const title = item.title || item.name || '未命名成就';
|
||
li.textContent = `[${scope}] ${title} · ${level} · 要求 ${item.threshold || 1} · ${item.unlocked_at || '-'}`;
|
||
achievementsListEl.appendChild(li);
|
||
});
|
||
};
|
||
|
||
const renderRecentRooms = (rooms) => {
|
||
recentRoomsEl.textContent = '';
|
||
if (!rooms.length) {
|
||
const li = document.createElement('li');
|
||
li.textContent = '暂无对局记录';
|
||
recentRoomsEl.appendChild(li);
|
||
return;
|
||
}
|
||
rooms.forEach((row) => {
|
||
const li = document.createElement('li');
|
||
const statusLabel = row.status === 'running' ? '进行中' : row.status === 'lobby' ? '准备中' : '已结束';
|
||
li.textContent = `${row.room_id} · ${row.card_title || '未知剧本'} · ${statusLabel}`;
|
||
if (row.status !== 'ended') {
|
||
const action = document.createElement('a');
|
||
action.className = 'link-btn';
|
||
action.style.marginLeft = '0.5rem';
|
||
action.href = `/room/${row.room_id}?player_id=${row.player_id}`;
|
||
action.textContent = '进入';
|
||
li.appendChild(action);
|
||
}
|
||
recentRoomsEl.appendChild(li);
|
||
});
|
||
};
|
||
|
||
const renderInvitePanel = async (profile) => {
|
||
invitePanelEl.textContent = '';
|
||
const target = profile.user || {};
|
||
const isSelf = Boolean(currentUser && String(currentUser.username || '') === String(target.username || ''));
|
||
if (!currentUser) {
|
||
invitePanelEl.textContent = '登录后可邀请该用户一起游戏。';
|
||
return;
|
||
}
|
||
if (isSelf) {
|
||
invitePanelEl.textContent = '这是你自己的主页。可在积分中心查看余额与流水。';
|
||
return;
|
||
}
|
||
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'editor-form-grid';
|
||
|
||
let availableRooms = [];
|
||
try {
|
||
const myRoomsResp = await getJson('/api/me/rooms');
|
||
availableRooms = (myRoomsResp.rooms || []).filter((row) => row.status === 'lobby' || row.status === 'running');
|
||
} catch {
|
||
availableRooms = [];
|
||
}
|
||
|
||
if (availableRooms.length) {
|
||
const roomLabel = document.createElement('label');
|
||
roomLabel.textContent = '选择你的房间';
|
||
roomLabel.htmlFor = 'invite-room-select';
|
||
wrap.appendChild(roomLabel);
|
||
|
||
const select = document.createElement('select');
|
||
select.id = 'invite-room-select';
|
||
availableRooms.forEach((row) => {
|
||
const option = document.createElement('option');
|
||
option.value = row.room_id;
|
||
const statusLabel = row.status === 'running' ? '进行中' : '准备中';
|
||
option.textContent = `${row.room_id} · ${row.card_title || '未知剧本'} · ${statusLabel}`;
|
||
select.appendChild(option);
|
||
});
|
||
wrap.appendChild(select);
|
||
|
||
const messageInput = document.createElement('input');
|
||
messageInput.maxLength = 120;
|
||
messageInput.placeholder = '可选留言,例如:来一起开黑';
|
||
wrap.appendChild(messageInput);
|
||
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'secondary-btn';
|
||
btn.textContent = `邀请 @${target.username || ''}`;
|
||
btn.addEventListener('click', async () => {
|
||
btn.disabled = true;
|
||
setStatus('正在发送邀请...');
|
||
try {
|
||
const data = await postJson('/api/invites', {
|
||
to_username: target.username,
|
||
room_id: select.value,
|
||
message: String(messageInput.value || '').trim(),
|
||
});
|
||
if (!data.ok) {
|
||
setStatus(data.error || '邀请失败');
|
||
return;
|
||
}
|
||
setStatus('邀请已发送。');
|
||
} catch (error) {
|
||
setStatus(`网络错误: ${error}`);
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
wrap.appendChild(btn);
|
||
} else {
|
||
const hint = document.createElement('p');
|
||
hint.className = 'helper-text';
|
||
hint.textContent = '你当前没有可邀请的活跃房间,请先在大厅创建或加入房间。';
|
||
wrap.appendChild(hint);
|
||
}
|
||
|
||
const targetUserId = String(target.user_id || '').trim();
|
||
if (targetUserId) {
|
||
const tipTitle = document.createElement('h3');
|
||
tipTitle.style.margin = '0.8rem 0 0.2rem';
|
||
tipTitle.textContent = '积分打赏';
|
||
wrap.appendChild(tipTitle);
|
||
|
||
const tipAmount = document.createElement('input');
|
||
tipAmount.type = 'number';
|
||
tipAmount.min = '1';
|
||
tipAmount.max = '100000';
|
||
tipAmount.placeholder = '输入打赏积分,例如 66';
|
||
wrap.appendChild(tipAmount);
|
||
|
||
const tipMessage = document.createElement('input');
|
||
tipMessage.maxLength = 120;
|
||
tipMessage.placeholder = '打赏留言(可选)';
|
||
wrap.appendChild(tipMessage);
|
||
|
||
const tipBtn = document.createElement('button');
|
||
tipBtn.type = 'button';
|
||
tipBtn.className = 'ghost-btn';
|
||
tipBtn.textContent = `打赏 @${target.username || ''}`;
|
||
tipBtn.addEventListener('click', async () => {
|
||
const amount = Number(tipAmount.value);
|
||
if (!Number.isFinite(amount) || amount <= 0) {
|
||
setStatus('请输入有效的打赏积分。');
|
||
return;
|
||
}
|
||
tipBtn.disabled = true;
|
||
setStatus('正在打赏...');
|
||
try {
|
||
const data = await postJson('/api/store/tip', {
|
||
to_user_id: targetUserId,
|
||
amount: Math.round(amount),
|
||
message: String(tipMessage.value || '').trim(),
|
||
});
|
||
if (!data.ok) {
|
||
setStatus(data.error || '打赏失败');
|
||
return;
|
||
}
|
||
tipAmount.value = '';
|
||
tipMessage.value = '';
|
||
setStatus('打赏成功。');
|
||
} catch (error) {
|
||
setStatus(`网络错误: ${error}`);
|
||
} finally {
|
||
tipBtn.disabled = false;
|
||
}
|
||
});
|
||
wrap.appendChild(tipBtn);
|
||
}
|
||
|
||
invitePanelEl.appendChild(wrap);
|
||
};
|
||
|
||
const renderProfile = async (profile) => {
|
||
const user = profile.user || {};
|
||
const presence = profile.presence || {};
|
||
const stats = profile.stats || {};
|
||
const points = (profile.points || {}).account || null;
|
||
const badges = profile.badges || {};
|
||
const badgeRows = Array.isArray(badges.badges) ? badges.badges : [];
|
||
const activeBadgeId = String(badges.active_badge || '').trim();
|
||
const activeBadge = badgeRows.find((row) => row.id === activeBadgeId) || null;
|
||
const onlineText = presence.is_online ? '在线' : '离线';
|
||
subtitleEl.textContent = `@${user.username || ''} · ${onlineText}`;
|
||
|
||
heroEl.textContent = '';
|
||
const badge = document.createElement('div');
|
||
badge.className = `user-badge ${user.avatar_url ? 'has-avatar' : ''}`;
|
||
badge.style.setProperty('--avatar-color', user.avatar_color || '#0070f2');
|
||
if (user.avatar_url) {
|
||
badge.style.setProperty('--avatar-url', `url('${String(user.avatar_url).replace(/'/g, "\\'")}')`);
|
||
}
|
||
const icon = document.createElement('i');
|
||
badge.appendChild(icon);
|
||
const text = document.createElement('span');
|
||
text.textContent = `@${user.username || '未知用户'}`;
|
||
badge.appendChild(text);
|
||
heroEl.appendChild(badge);
|
||
|
||
const bio = document.createElement('p');
|
||
bio.className = 'helper-text';
|
||
bio.style.marginTop = '0.6rem';
|
||
bio.textContent = user.bio || '这个用户还没有填写个性签名。';
|
||
heroEl.appendChild(bio);
|
||
|
||
const rows = [
|
||
`在线状态:${onlineText}`,
|
||
`当前房间:${presence.active_room_id || '-'}`,
|
||
`最后活跃:${presence.last_seen || '-'}`,
|
||
`排行榜名次:${stats.rank || '-'}`,
|
||
`总胜场:${stats.wins || 0}`,
|
||
`参与局数:${stats.played_games || 0}`,
|
||
`胜率:${Number(stats.win_rate || 0).toFixed(1)}%`,
|
||
`总回合:${stats.total_rounds || 0}`,
|
||
`职业徽章:${activeBadge ? activeBadge.name : '未选择'}`,
|
||
];
|
||
if (activeBadge) {
|
||
const badgeDesc = String(activeBadge.description || '').trim();
|
||
const effects = [];
|
||
if (activeBadge.effect_item) effects.push(`开局物品 ${activeBadge.effect_item}`);
|
||
if (activeBadge.effect_clue) effects.push(`开局线索 ${activeBadge.effect_clue}`);
|
||
if (activeBadge.special_choice) effects.push(`特殊行动 ${activeBadge.special_choice}`);
|
||
const intro = badgeDesc || '暂无介绍';
|
||
rows.push(`徽章介绍:${intro}${effects.length ? `(${effects.join(';')})` : ''}`);
|
||
}
|
||
if (points && typeof points === 'object') {
|
||
rows.push(`积分余额:${Number(points.balance || 0)} / 可用 ${Number(points.available_balance || 0)}`);
|
||
}
|
||
metaEl.textContent = '';
|
||
rows.forEach((row) => {
|
||
const div = document.createElement('div');
|
||
div.textContent = row;
|
||
metaEl.appendChild(div);
|
||
});
|
||
|
||
renderAchievements(profile.achievements || {});
|
||
if (achievementDetailLinkEl && user.username) {
|
||
achievementDetailLinkEl.href = `/users/${encodeURIComponent(user.username)}/achievements`;
|
||
}
|
||
renderRecentRooms(profile.recent_rooms || []);
|
||
await renderInvitePanel(profile);
|
||
};
|
||
|
||
const loadProfile = async () => {
|
||
if (!profileUsername) {
|
||
setStatus('用户不存在');
|
||
return;
|
||
}
|
||
const data = await getJson(`/api/users/${encodeURIComponent(profileUsername)}/profile`);
|
||
if (!data.ok) {
|
||
setStatus(data.error || '加载用户主页失败');
|
||
return;
|
||
}
|
||
await renderProfile(data.profile || {});
|
||
};
|
||
|
||
loadProfile().catch((error) => setStatus(`网络错误: ${error}`));
|
||
})();
|