Files
001code-html--cocos/scratch-gui/src/playground/programming-learning.js
2026-09-10 15:15:39 +08:00

714 lines
24 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.
import CONFIG from './config.js';
import { initDisableDevtool } from './devtool-control.js';
// ============= 业务参数 =============
let gamelvid = '1'; // 具体关卡等级,初始为 1
let gamelvdata = {};
// 添加缓存对象
const levelDataCache = {};
const permission = localStorage.getItem('permission');
let currentProcessedLevelData = []; // Store processed level data for secure checking
gamelvdata = sessionStorage.getItem('gamelvdata')
if (gamelvdata) {
gamelvdata = JSON.parse(gamelvdata)
} else {
gamelvdata = {
gamemode: 1,
gamelv: 1,
gameid: 1,
currentlv: 1,
ismatch: false,
matchid: { cuntry: 1, province: 1, city: 1, district: 1 },
gameNumber: 10,
};
}
document.addEventListener('DOMContentLoaded', function () {
let selectGameDom = document.querySelector('.navbar-middle');
selectGameDom.addEventListener('click', function () {
localStorage.removeItem('levelHistory')
window.location.href = 'welcome.html';
})
// Update Language Name based on gamemode
function updateLanguageDisplay() {
const langNameEl = document.querySelector('.lang-name');
if (langNameEl && gamelvdata) {
const mode = parseInt(gamelvdata.gamemode);
let langText = '';
const iconSrc = 'images/stat-icon-graphical.svg';
if (mode === 1) {
langText = '图形化';
} else if (mode === 2) {
langText = 'Python';
} else if (mode === 3) {
langText = 'C++';
}
langNameEl.innerHTML = `<img src="${iconSrc}" class="lang-icon" alt=""><span>${langText}</span>`;
}
}
updateLanguageDisplay();
// 初始化禁用开发者工具
initDisableDevtool({
ondevtoolopen: () => {
window.location.href = localStorage.getItem('returnUrl') || '/welcome.html';
}
});
// 3. 弹窗交互绑定
function bindSharedModalEvents() {
// 通用关闭逻辑 (增强:支持点击内部图标)
document.addEventListener('click', (e) => {
const closeBtn = e.target.closest('.u-dialog-close-button') || e.target.closest('.modal-close-btn') || e.target.closest('.modal-close');
if (closeBtn) {
const modal = closeBtn.closest('.modal-overlay');
if (modal) modal.style.display = 'none';
}
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
}
});
// 登出按钮事件
const logoutBtn = document.getElementById('logout-button');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
['access_token', 'refresh_token', 'player_id'].forEach(key => {
localStorage.removeItem(key);
});
window.location.href = '/login.html';
});
}
}
// 执行共有逻辑初始化
bindSharedModalEvents();
// ============== 共有逻辑结束 ==============
const listItemsModes = document.querySelectorAll('.level-card-actions .pill');
// New UI doesn't have shape toggles, so we just handle the data update.
listItemsModes.forEach((item, index) => {
item.addEventListener('click', function () {
if (index !== 0 && (!permission || parseInt(permission) === 0)) {
showMessage('error', '您没有权限解锁此等级');
return;
}
listItemsModes.forEach(other => other.classList.remove('pill-active'));
this.classList.add('pill-active');
// Update Level Card Text
const levelInfo = {
1: { title: "Level A <span style='font-size: 0.7em'>丝路青年</span>", sub: "编程基础,循环入门", cover: 1 },
2: { title: "Level B <span style='font-size: 0.7em'>丝路青年</span>", sub: "变量入门,循环进阶,变量进阶", cover: 1 },
3: { title: "Level C <span style='font-size: 0.7em'>红色之旅</span>", sub: "变量进阶,单条件分支入门,双条件分支入门,复杂路线规划", cover: 2 },
4: { title: "Level D <span style='font-size: 0.7em'>红色之旅</span>", sub: "条件分支进阶,循环嵌套入门,循环嵌套进阶,变量、循环与条件综合应用", cover: 2 },
5: { title: "Level E <span style='font-size: 0.7em'>数智时代</span>", sub: "循环嵌套进阶,无参数函数入门,单参数函数入门", cover: 3 },
6: { title: "Level F <span style='font-size: 0.7em'>数智时代</span>", sub: "综合运用与项目实战", cover: 3 }
};
const lv = index + 1;
const titleEl = document.querySelector('.level-card-head');
const subEl = document.querySelector('.level-card-sub');
if (levelInfo[lv]) {
if (titleEl) titleEl.innerHTML = levelInfo[lv].title;
if (subEl) subEl.textContent = levelInfo[lv].sub;
}
// Update Cover Image
const coverEl = document.querySelector('.level-card-cover');
if (coverEl) {
const coverId = (levelInfo[lv] && levelInfo[lv].cover) || lv;
coverEl.src = `images/level${coverId}_cover.png`;
}
gamelvid = (index + 1).toString();
gamelvdata.gamelv = index + 1;
// 记住当前 Level从关卡返回后恢复
try {
sessionStorage.setItem('gamelvdata', JSON.stringify(gamelvdata));
sessionStorage.setItem('programmingLearningSelectedLevel', String(lv));
} catch (e) {}
console.log('当前 gamelvid:', gamelvid);
if (levelDataCache[gamelvid]) {
console.log('使用缓存数据:', gamelvid);
renderGameModeItems(levelDataCache[gamelvid]);
} else {
fetchAndRenderGameModes();
}
});
});
// 恢复上次选中的 Level默认 Level A
if (listItemsModes.length > 0) {
let savedLv = parseInt(
(gamelvdata && gamelvdata.gamelv) ||
sessionStorage.getItem('programmingLearningSelectedLevel') ||
'1',
10
);
if (!isFinite(savedLv) || savedLv < 1) savedLv = 1;
if (savedLv > listItemsModes.length) savedLv = listItemsModes.length;
// 无权限时只能回 Level A
if (savedLv !== 1 && (!permission || parseInt(permission) === 0)) {
savedLv = 1;
}
listItemsModes[savedLv - 1].click();
}
/********************************************************
* 2) Bind Level Item Click
********************************************************/
addLevelItemClick();
});
/********************************************************
* 3) 拉取数据并渲染关卡列表
********************************************************/
function fetchAndRenderGameModes() {
const playerId = localStorage.getItem('player_id');
resetLevelItems();
const requestGamelvid = gamelvid; // Capture current ID for this request
fetch(`${CONFIG.DataServerBaseUrl}/api/get-base-game-records/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({
"user_id": playerId,
"gamemode": gamelvdata.gamemode,
"gamelv": gamelvdata.gamelv,
})
})
.then(response => response.json())
.then(data => {
// Must check if user hasn't switched away while waiting
if (requestGamelvid !== gamelvid) {
console.log(`忽略旧请求结果: req=${requestGamelvid}, curr=${gamelvid}`);
return;
}
if (data.success) {
// Use the captured ID (requestGamelvid) to get data
const levelData = data.data[requestGamelvid];
if (Array.isArray(levelData)) {
levelDataCache[requestGamelvid] = levelData;
console.log('缓存关卡数据:', requestGamelvid);
renderGameModeItems(levelData);
} else {
console.error('无效的关卡数据格式:', levelData);
showMessage('error', '关卡数据格式错误');
}
} else {
showMessage('error', data.message);
}
})
.catch(err => {
console.error('请求出错:', err);
// const container = document.getElementById('levels-container');
// if (container) container.innerHTML = '<p style="text-align:center; padding: 20px;">加载关卡失败,请稍后重试。</p>';
showMessage('error', '加载关卡失败,请稍后重试');
})
.finally(() => {
// if (loadingEl) loadingEl.style.display = 'none';
// if (levelsContainer) levelsContainer.style.display = ''; // Restore original display (grid/flex)
});
}
function renderGameModeItems(data) {
let trialLevelCount = 3;
const playerId = localStorage.getItem('player_id');
const fetchTrialLevels = async () => {
if (!playerId || (permission && parseInt(permission) !== 0)) {
return;
}
try {
const response = await fetch(`${CONFIG.DataServerBaseUrl}/api/get-user-trial-levels/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({ user_id: playerId })
});
const result = await response.json();
if (result.success && result.data) {
trialLevelCount = Math.floor(result.data.trial_level_count / 10);
console.log('用户试用关卡数量:', trialLevelCount);
localStorage.setItem('trial_count', result.data.trial_level_count);
processItemsWithTrialCount();
} else {
console.error('获取试用关卡数量失败:', result.message);
}
} catch (error) {
console.error('请求试用关卡数量出错:', error);
}
};
const processItemsWithTrialCount = () => {
const items = data.map((item, index) => {
if (!permission || parseInt(permission) === 0) {
item.islock = index >= trialLevelCount;
}
if (!CONFIG.lvlock_normal_learn) {
item.islock = false;
}
return item;
});
currentProcessedLevelData = items;
renderItems(items);
};
const renderItems = (items) => {
// Target the new list items in .levels-row
const listItems = document.querySelectorAll('.levels-row .level-item');
listItems.forEach((li, index) => {
if (index < items.length) {
const itemData = items[index];
// Update data-gamelv-id
li.dataset.gamelvId = itemData.gameid;
// Elements
const btn = li.querySelector('.level-btn');
const imgEl = li.querySelector('.level-bg');
const numEl = li.querySelector('.level-number');
const titleEl = li.querySelector('.level-title');
// Update Text
if (numEl) numEl.textContent = itemData.islock ? '' : (itemData.titleNumber || (index + 1));
if (titleEl) {
const text = itemData.titleText || '';
// Add promotion badge for specific levels ONLY if unlocked
if (!itemData.islock && (parseInt(itemData.gameid) === 29 || parseInt(itemData.gameid) === 30)) {
titleEl.innerHTML = `<div>${text}</div><div style="margin-top: 5px;"><span class="promotion-badge">晋升关卡</span></div>`;
} else {
titleEl.textContent = text;
}
}
// Update Lock State and Image
if (itemData.islock) {
if (btn) {
btn.classList.add('locked');
btn.classList.remove('unlocked');
}
if (imgEl) {
imgEl.src = 'images/level_btn_locked.png';
imgEl.alt = 'Locked';
}
// Add custom tooltip logic if needed (borrowed from old code)
addLockedTooltip(li, itemData.titleText);
} else {
if (btn) {
btn.classList.remove('locked');
btn.classList.add('unlocked');
}
if (imgEl) {
imgEl.src = 'images/level_btn_unlocked.svg';
imgEl.alt = itemData.titleText || 'Level';
}
removeLockedTooltip(li);
}
// 5. 进度条逻辑
const shapeGray = li.querySelector('.u-grey-5');
const shapeProgress = li.querySelector('.u-gradient');
if (shapeGray && shapeProgress) {
const total = itemData.total || 0;
const finish = itemData.finish || 0;
if (total > 0 && !itemData.islock) {
shapeGray.style.display = 'block';
const ratio = total > 0 ? (finish / total) : 0;
const progressWidth = (ratio * 100).toFixed(0) + '%';
shapeProgress.style.width = progressWidth;
let gradient;
if (ratio < 0.3) gradient = 'linear-gradient(to right, #ff6474, #e01c4c)';
else if (ratio < 1) gradient = 'linear-gradient(to right, #fcc75a, #ee6622)';
else gradient = 'linear-gradient(to right, #00ff00, #009245)';
shapeProgress.style.backgroundImage = gradient;
shapeProgress.setAttribute('title', `${finish}/${total}`);
} else {
shapeGray.style.display = 'none';
}
}
li.style.display = 'flex'; // Use flex as per new CSS? .level-item is flex column
} else {
li.style.display = 'none';
}
});
};
if (!permission || parseInt(permission) === 0) {
fetchTrialLevels();
} else {
processItemsWithTrialCount();
}
}
/********************************************************
* 4) Click Event
********************************************************/
function addLevelItemClick() {
const levelsContainer = document.getElementById('levels-container');
if (!levelsContainer) return;
let startX = 0;
let startY = 0;
levelsContainer.addEventListener('mousedown', function (event) {
startX = event.clientX;
startY = event.clientY;
});
levelsContainer.addEventListener('click', function (event) {
const diffX = Math.abs(event.clientX - startX);
const diffY = Math.abs(event.clientY - startY);
// 如果移动超过5px认为是拖拽选择操作不触发点击
if (diffX > 5 || diffY > 5) {
return;
}
const li = event.target.closest('.level-item');
if (!li) return;
const gameid = li.dataset.gamelvId;
console.log('你点击了关卡 gamelv-id:', gameid);
const allListItems = document.querySelectorAll('.levels-row .level-item');
const index = Array.from(allListItems).indexOf(li);
if (!currentProcessedLevelData || !currentProcessedLevelData[index]) {
console.log('关卡数据尚未加载,阻止点击');
return;
}
const isLockedInMemory = currentProcessedLevelData[index].islock;
if (isLockedInMemory) {
console.log('此关卡已锁定,无法进入。');
if ((permission && parseInt(permission) !== 0)) {
showMessage('error', '请先完成前面的关卡');
} else {
showMessage('error', '仅向权益用户开放');
}
return;
}
// Checking for promotion levels
if (gameid === '29' || gameid === '30') {
showPromotionDialog(gameid);
return;
}
enterGame(gameid);
});
}
function enterGame(gameid) {
let gamelvdata = sessionStorage.getItem('gamelvdata');
if (gamelvdata) {
gamelvdata = JSON.parse(gamelvdata);
} else {
gamelvdata = {
gamemode: 1,
gamelv: 1,
gameid: 1,
currentlv: 1,
ismatch: false,
matchid: { cuntry: 1, province: 1, city: 1, district: 1 },
gameNumber: 10,
};
}
gamelvdata.gamelv = parseInt(gamelvid);
gamelvdata.gameid = parseInt(gameid);
sessionStorage.setItem('gamelvdata', JSON.stringify(gamelvdata));
sessionStorage.setItem('lastpage', 'programming-learning.html');
localStorage.setItem('platformMode', 'normal');
window.location.href = 'editor.html';
}
// ========== Permissions & Modals UI Logic ==========
function initPermissionControl() {
const permission = localStorage.getItem('permission');
const vipBadge = document.querySelector('.navbar-link-vip');
if (vipBadge) {
if (permission && parseInt(permission) !== 0) {
vipBadge.style.display = 'inline-flex';
} else {
vipBadge.style.display = 'none';
}
}
}
window.addEventListener('DOMContentLoaded', () => {
initPermissionControl();
setQRCodeImage();
initializePermissionCode();
initPromotionDialog();
// Close buttons for new simple modals
document.querySelectorAll('.modal-close').forEach(btn => {
btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal-overlay');
if (modal) modal.style.display = 'none';
});
});
const activationLink = document.querySelector('a[href="#carousel_ac25"]');
const activationModal = document.getElementById('carousel_ac25');
if (activationLink && activationModal) {
activationLink.addEventListener('click', (e) => {
e.preventDefault();
activationModal.style.display = 'flex';
});
}
const tryUseBtn = document.getElementById('tryuse');
if (tryUseBtn) {
tryUseBtn.addEventListener('click', (e) => {
e.preventDefault();
if (activationModal) activationModal.style.display = 'none';
});
}
});
document.getElementById('logout-button').addEventListener('click', () => {
['access_token', 'refresh_token', 'player_id'].forEach(key => {
localStorage.removeItem(key);
});
window.location.href = '/login.html';
});
function showMessage(type, message) {
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
// Add basic toast styles since we lost nicepage
toastContainer.style.position = 'fixed';
toastContainer.style.top = '20px';
toastContainer.style.left = '50%';
toastContainer.style.transform = 'translateX(-50%)';
toastContainer.style.zIndex = '9999';
document.body.appendChild(toastContainer);
}
const toast = document.createElement('div');
toast.classList.add('toast', type);
toast.textContent = message;
// Basic Toast Style
toast.style.background = type === 'error' ? '#ff4757' : '#2ed573';
// toast.style.color = 'white';
// toast.style.padding = '10px 20px';
// toast.style.borderRadius = '5px';
// toast.style.marginBottom = '10px';
// toast.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
toastContainer.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
function showPromotionDialog(gameid) {
const dialog = document.getElementById('promotion-dialog');
if (dialog) {
dialog.style.display = 'flex';
dialog.dataset.currentGameId = gameid;
}
}
function hidePromotionDialog(enterLevel = false) {
const dialog = document.getElementById('promotion-dialog');
if (dialog) {
const gameid = dialog.dataset.currentGameId;
dialog.style.display = 'none';
if (enterLevel && gameid) {
enterGame(gameid);
}
}
}
function initPromotionDialog() {
const closeButton = document.querySelector('.u-btn-promotion-close');
if (closeButton) {
closeButton.addEventListener('click', function () {
hidePromotionDialog(true);
});
}
const closeIcon = document.querySelector('.u-icon-promotion-close');
if (closeIcon) {
closeIcon.addEventListener('click', function () {
hidePromotionDialog(false);
});
}
}
function setQRCodeImage() {
const qrcodeUrl = sessionStorage.getItem('qrcode_url');
// Target the new modal structure
const qrcodeElement = document.querySelector('#carousel_ac25 .modal-qr');
if (qrcodeElement) {
if (qrcodeElement.tagName === 'IMG') {
qrcodeElement.src = qrcodeUrl || '../images/modal-activate-qr.png';
} else {
qrcodeElement.style.backgroundImage = `url("${qrcodeUrl || '../images/modal-activate-qr.png'}")`;
}
}
}
const initializePermissionCode = () => {
const permissionCodeSendButton = document.getElementById('permissionCodeSendButton');
const closeModal = document.getElementById('closeNoPermissionModal');
if (!permissionCodeSendButton) return;
permissionCodeSendButton.addEventListener('click', async (e) => {
e.preventDefault();
const permissionCode = document.getElementById('permissionCode').value.trim();
const username = localStorage.getItem('username');
permissionCodeSendButton.disabled = true;
try {
const result = await api.redeemActivationCode(permissionCode, username);
if (result.success) {
showMessage('success', '您已激活账号,即将刷新页面');
const user_permission = result.user_permission;
localStorage.setItem('permission', user_permission);
if (closeModal) closeModal.click();
document.getElementById('permissionCode').value = "";
window.location.reload();
} else {
showMessage('error', result.message);
}
} catch (error) {
console.error('Error activating account:', error);
showMessage('error', '服务器错误,请稍后再试');
} finally {
permissionCodeSendButton.disabled = false;
}
});
};
const api = {
async redeemActivationCode(code, username) {
const response = await fetch(`${CONFIG.DataServerBaseUrl}/api/redeem_activation_code/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ code, username })
});
return await response.json();
}
};
// Tooltip logic adapted for new UI
function addLockedTooltip(listItem, titleText) {
removeLockedTooltip(listItem);
const imageElement = listItem.querySelector('.level-bg'); // Target the image
if (!imageElement || !titleText) return;
const tooltip = document.createElement('div');
tooltip.className = 'locked-tooltip'; // Make sure this class is defined in CSS or add inline styles
tooltip.textContent = titleText;
tooltip.style.position = 'fixed';
tooltip.style.background = 'rgba(0,0,0,0.8)';
tooltip.style.color = 'white';
tooltip.style.padding = '5px 10px';
tooltip.style.borderRadius = '4px';
tooltip.style.pointerEvents = 'none';
tooltip.style.fontSize = '12px';
tooltip.style.zIndex = '10000';
tooltip.style.display = 'none';
document.body.appendChild(tooltip);
const mouseEnterHandler = () => {
const rect = listItem.getBoundingClientRect(); // Tooltip relative to ITEM, not just image
tooltip.style.left = (rect.left + rect.width / 2) + 'px';
tooltip.style.top = (rect.top - 30) + 'px';
tooltip.style.transform = 'translateX(-50%)';
tooltip.style.display = 'block';
};
const mouseLeaveHandler = () => {
tooltip.style.display = 'none';
};
listItem.addEventListener('mouseenter', mouseEnterHandler);
listItem.addEventListener('mouseleave', mouseLeaveHandler);
listItem._tooltipHandlers = {
mouseEnter: mouseEnterHandler,
mouseLeave: mouseLeaveHandler,
tooltip: tooltip
};
}
function removeLockedTooltip(listItem) {
if (!listItem || !listItem._tooltipHandlers) return;
const handlers = listItem._tooltipHandlers;
listItem.removeEventListener('mouseenter', handlers.mouseEnter);
listItem.removeEventListener('mouseleave', handlers.mouseLeave);
if (handlers.tooltip && handlers.tooltip.parentNode) {
handlers.tooltip.parentNode.removeChild(handlers.tooltip);
}
delete listItem._tooltipHandlers;
}
// Helper to reset items to skeleton state
function resetLevelItems() {
// 必须清空缓存的当前关卡数据防止在数据未加载完成时用户点击使用了上一个Level的数据判断
currentProcessedLevelData = [];
const listItems = document.querySelectorAll('.levels-row .level-item');
listItems.forEach(li => {
// 1. 重置为显示状态因为我们要显示Skeleton
li.style.display = 'flex';
// 2. 也是最重要的,重置为包含 "locked" 样式
const btn = li.querySelector('.level-btn');
if (btn) {
btn.classList.add('locked');
btn.classList.remove('unlocked');
}
// 3. 重置图标
const imgEl = li.querySelector('.level-bg');
if (imgEl) {
imgEl.src = 'images/level_btn_locked.png';
}
// 4. 清空数字
const numEl = li.querySelector('.level-number');
if (numEl) numEl.textContent = '';
// 5. 清空标题
const titleEl = li.querySelector('.level-title');
if (titleEl) titleEl.textContent = '';
// 6. 隐藏进度条
const shapeGray = li.querySelector('.u-grey-5');
if (shapeGray) shapeGray.style.display = 'none';
});
}