Files
Echo-Tavern/static/js/user_profile.js
T

327 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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}`));
})();