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 = `${langText}`; } } 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 丝路青年", sub: "编程基础,循环入门" }, 2: { title: "Level B 红色之旅", sub: "变量入门,循环进阶,变量进阶" }, 3: { title: "Level C 数智时代", sub: "变量进阶,单条件分支入门,双条件分支入门,复杂路线规划" }, 4: { title: "Level D 国宝秘境", sub: "条件分支进阶,循环嵌套入门,循环嵌套进阶,变量、循环与条件综合应用" }, 5: { title: "Level E 雪域征途", sub: "循环嵌套进阶,无参数函数入门,单参数函数入门" }, 6: { title: "Level F 红色之旅", sub: "综合运用与项目实战" } }; 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) { coverEl.src = `images/level${lv}_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 = '

加载关卡失败,请稍后重试。

'; 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 = `
${text}
晋升关卡
`; } 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'; }); }