(function () { var viewport = document.getElementById('appViewport'); var page = document.getElementById('page'); var dailyRoot = document.getElementById('dailyTasks'); var exclusiveRoot = document.getElementById('exclusiveTasks'); var stateRoot = document.getElementById('taskState'); var backButton = document.getElementById('backButton'); var COMMAND_PREFIX = 'task_claim_command:'; var COIN_IMAGE = './assets/coin.png'; var GAME_IMAGE = './assets/figma-game-icon.png'; var state = { loading: true, error: '', sections: [], claiming: {}, }; var mockData = { server_time_ms: Date.now(), next_refresh_at_ms: Date.now() + 6 * 60 * 60 * 1000, sections: [ { section: 'daily', items: [ mockTask( 'daily-mic', 'daily', 'Use the microphone for 1 min', 'mic_online_minute', 0, 1000000, 1100, 'in_progress', false, 'mic' ), mockTask( 'daily-game', 'daily', 'Use the microphone for 1 min', 'game_spend_coin', 1000000, 1000000, 1100, 'completed', true, 'gamepad' ), mockTask( 'daily-gift', 'daily', 'Use the microphone for 1 min', 'gift_spend_coin', 1000000, 1000000, 1100, 'claimed', false, 'gift' ), ], }, { section: 'exclusive', items: [ mockTask( 'exclusive-game-preview', 'exclusive', 'Use the microphone for 1 min', 'game_spend_coin', 0, 1000000, 1100, 'in_progress', false, 'game_preview' ), mockTask( 'exclusive-game', 'exclusive', 'Use the microphone for 1 min', 'game_spend_coin', 1000000, 1000000, 1100, 'completed', true, 'gamepad' ), mockTask( 'exclusive-gift', 'exclusive', 'Use the microphone for 1 min', 'gift_spend_coin', 1000000, 1000000, 1100, 'claimed', false, 'gift' ), ], }, ], }; function t(key, fallback) { if (window.HyAppI18n && window.HyAppI18n.t) { return window.HyAppI18n.t(key, fallback); } return fallback || key; } function mockTask( id, type, title, metric, progress, target, reward, status, claimable, iconKey ) { return { task_id: id, task_type: type, task_day: type === 'daily' ? new Date().toISOString().slice(0, 10) : 'lifetime', title: title, metric_type: metric, progress_value: progress, target_value: target, reward_coin_amount: reward, status: status, claimable: claimable, icon_key: iconKey, icon_url: iconKey === 'game_preview' ? GAME_IMAGE : '', action_type: iconKey === 'gift' ? 'gift_panel' : iconKey === 'gamepad' ? 'room_random_game' : 'wallet', action_param: iconKey === 'gamepad' ? 'dice' : '', action_payload: iconKey === 'gamepad' ? { game_id: 'dice' } : {}, }; } function isMockMode() { var params = new URLSearchParams(window.location.search || ''); return params.get('mock') === '1' || params.get('mock') === 'true'; } function escapeHTML(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeAttr(value) { return escapeHTML(value).replace(/`/g, '`'); } function clampProgress(item) { var target = Math.max( 0, Number(item.target_value || item.targetValue || 0) ); var progress = Math.max( 0, Number(item.progress_value || item.progressValue || 0) ); return { target: target, progress: target > 0 ? Math.min(progress, target) : progress, }; } function formatProgress(item) { var values = clampProgress(item); return String(values.progress) + '/' + String(values.target); } function formatReward(value) { var amount = Number(value || 0); if (!Number.isFinite(amount)) return '0'; if (amount >= 1000000) return trimNumber(amount / 1000000) + 'M'; if (amount >= 1000) return trimNumber(amount / 1000) + 'k'; return String(amount); } function trimNumber(value) { var rounded = Math.round(value * 10) / 10; return String(rounded).replace(/\.0$/, ''); } function claimKey(item) { return [ item.task_id || item.taskId, item.task_type || item.taskType, item.task_day || item.taskDay, ].join('|'); } function commandStorageKey(item) { return COMMAND_PREFIX + claimKey(item); } function commandID(item) { var storageKey = commandStorageKey(item); var cached = window.sessionStorage.getItem(storageKey); if (cached) return cached; // 领取命令以任务主键和页面时间组成,保存在 sessionStorage;如果请求超时但服务端已入账,用户重试会继续用同一 command_id 命中后端幂等。 var next = 'h5-task-claim-' + Date.now() + '-' + Math.random().toString(16).slice(2) + '-' + String(item.task_id || item.taskId || ''); window.sessionStorage.setItem(storageKey, next); return next; } function clearCommandID(item) { window.sessionStorage.removeItem(commandStorageKey(item)); } function normalizeSections(payload) { var sections = payload && Array.isArray(payload.sections) ? payload.sections : []; var byName = sections.reduce(function (map, section) { map[String(section.section || '')] = Array.isArray(section.items) ? section.items : []; return map; }, {}); return [ { section: 'daily', items: byName.daily || [] }, { section: 'exclusive', items: byName.exclusive || [] }, ]; } function iconKind(item) { var key = String(item.icon_key || item.iconKey || '').toLowerCase(); var metric = String( item.metric_type || item.metricType || '' ).toLowerCase(); if (key.indexOf('mic') >= 0 || metric.indexOf('mic') >= 0) return 'mic'; if (key.indexOf('gift') >= 0 || metric.indexOf('gift') >= 0) return 'gift'; if (key.indexOf('game') >= 0 || metric.indexOf('game') >= 0) return 'gamepad'; if (key.indexOf('cp') >= 0 || metric.indexOf('cp') >= 0) return 'cp'; if (key.indexOf('mood') >= 0 || metric.indexOf('mood') >= 0) return 'mood'; return 'coin'; } function iconHTML(item) { var iconURL = item.icon_url || item.iconUrl || ''; if (iconURL) { // 后台 icon_url 是可配置资源,可能因为地址过期、跨域或资源未上传而加载失败;失败时用内置图标兜底,不能让任务列表留下空方块。 return [ '', ].join(''); } return ( '' + svgForIcon(iconKind(item)) + '' ); } function svgForIcon(kind) { if (kind === 'mic') { return ''; } if (kind === 'gift') { return ''; } if (kind === 'cp') { return ''; } if (kind === 'mood') { return ''; } if (kind === 'coin') { return ''; } return ''; } function actionState(item) { if (item.status === 'claimed') { return { label: t('tasks.claimed', 'Claimed'), className: 'is-claimed', disabled: true, }; } if (item.claimable) { return { label: t('tasks.claim', 'Claim'), className: 'is-claimable', disabled: false, }; } return { label: t('tasks.go', 'Go'), className: 'is-go', disabled: false, }; } function renderSection(root, items) { if (!items.length) { root.innerHTML = '
' + escapeHTML(t('tasks.empty', 'No tasks yet.')) + '
'; return; } root.innerHTML = items.map(renderTaskRow).join(''); } function renderTaskRow(item) { var action = actionState(item); var key = claimKey(item); var loading = Boolean(state.claiming[key]); var label = loading ? t('tasks.claiming', 'Claiming...') : action.label; return [ '
', iconHTML(item), '
', '', escapeHTML( item.title || item.description || t('tasks.untitled', 'Task') ), '', '', escapeHTML(formatProgress(item)), '', '', escapeHTML( formatReward(item.reward_coin_amount || item.rewardCoinAmount) ), '', '
', '', '
', ].join(''); } function render() { var sections = normalizeSections({ sections: state.sections }); var daily = sections[0].items; var exclusive = sections[1].items; renderSection(dailyRoot, daily); renderSection(exclusiveRoot, exclusive); bindIconFallbacks(); renderState(); updateScale(); } function bindIconFallbacks() { Array.prototype.slice .call(document.querySelectorAll('[data-icon-fallback] img')) .forEach(function (image) { image.addEventListener( 'error', function () { var wrapper = image.parentElement; if (!wrapper) return; wrapper.classList.remove('is-image'); wrapper.innerHTML = svgForIcon( wrapper.getAttribute('data-icon-fallback') || 'coin' ); }, { once: true } ); }); } function renderState() { if (state.loading) { stateRoot.hidden = false; stateRoot.textContent = t('tasks.loading', 'Loading...'); return; } if (state.error) { stateRoot.hidden = false; stateRoot.textContent = state.error; return; } stateRoot.hidden = true; stateRoot.textContent = ''; } function loadTasks() { state.loading = true; state.error = ''; renderState(); if (isMockMode()) { applyPayload(mockData); return; } if (!window.HyAppAPI || !window.HyAppAPI.task) { state.loading = false; state.error = t('tasks.apiUnavailable', 'Task API unavailable.'); render(); return; } window.HyAppAPI.task .tabs() .then(applyPayload) .catch(function (error) { state.loading = false; state.error = error.message || t('tasks.loadFailed', 'Load failed.'); state.sections = []; render(); }); } function applyPayload(payload) { var sections = normalizeSections(payload); state.sections = sections.map(function (section) { return { section: section.section, items: section.items, }; }); state.loading = false; state.error = ''; render(); } function findTask(key) { var sections = normalizeSections({ sections: state.sections }); for ( var sectionIndex = 0; sectionIndex < sections.length; sectionIndex += 1 ) { var items = sections[sectionIndex].items; for (var itemIndex = 0; itemIndex < items.length; itemIndex += 1) { if (claimKey(items[itemIndex]) === key) return items[itemIndex]; } } return null; } function claimTask(item) { var key = claimKey(item); state.claiming[key] = true; render(); if (isMockMode()) { // mock 只服务本地视觉和交互验收,不触发真实发奖;真实页面仍必须通过后端 claim 接口刷新钱包和任务状态。 item.status = 'claimed'; item.claimable = false; state.claiming[key] = false; showToast(t('tasks.claimSuccess', 'Reward claimed.')); render(); return; } // H5 只提交任务列表返回的 task_day/task_type/task_id,避免页面自行推算每日刷新周期;服务端负责判断当天是否仍可领取。 window.HyAppAPI.task .claim({ task_id: item.task_id || item.taskId, task_type: item.task_type || item.taskType, task_day: item.task_day || item.taskDay, command_id: commandID(item), }) .then(function () { clearCommandID(item); showToast(t('tasks.claimSuccess', 'Reward claimed.')); return window.HyAppAPI.task.tabs(); }) .then(applyPayload) .catch(function (error) { state.claiming[key] = false; showToast( error.message || t('tasks.claimFailed', 'Claim failed.') ); render(); }); } function goTask(item) { var actionType = item.action_type || item.actionType || 'none'; if (!actionType || actionType === 'none') { showToast(t('tasks.noAction', 'No action configured.')); return; } var payload = { task_id: item.task_id || item.taskId || '', task_type: item.task_type || item.taskType || '', task_day: item.task_day || item.taskDay || '', action_type: actionType, action_param: item.action_param || item.actionParam || '', action_payload: item.action_payload || item.actionPayload || {}, }; // H5 不猜测原生房间/钱包/礼物页面的具体路由,只把后台配置的跳转元数据原样交给 App;App 侧可按 action_type 做统一分发。 if (window.HyAppBridge && window.HyAppBridge.openTaskAction) { window.HyAppBridge.openTaskAction(payload); return; } if (window.HyAppBridge && window.HyAppBridge.post) { window.HyAppBridge.post('taskAction', payload); return; } window.dispatchEvent( new CustomEvent('hyapp:task-action', { detail: payload, }) ); } function showToast(message) { if (window.HyAppToast && window.HyAppToast.show) { window.HyAppToast.show(message); return; } window.dispatchEvent( new CustomEvent('hyapp:toast', { detail: { message: message }, }) ); } function onTaskAction(event) { var button = event.target.closest('[data-task-action]'); if (!button) return; var item = findTask(button.getAttribute('data-task-action')); if (!item) return; if (item.claimable) { claimTask(item); return; } goTask(item); } function updateScale() { document.documentElement.style.setProperty('--app-scale', '1'); if (!viewport || !page) return; var height = Math.max(page.scrollHeight, window.innerHeight || 0); viewport.style.minHeight = height + 'px'; document.body.style.minHeight = height + 'px'; } function bindEvents() { page.addEventListener('click', onTaskAction); window.addEventListener('resize', updateScale); if (backButton) { backButton.addEventListener('click', function () { if (window.HyAppBridge && window.HyAppBridge.back) { window.HyAppBridge.back(); return; } window.history.back(); }); } } bindEvents(); loadTasks(); if (window.HyAppBridge && window.HyAppBridge.ready) { window.HyAppBridge.ready({ page: 'tasks' }); } })();