diff --git a/activity/tasks/assets/back.png b/activity/tasks/assets/back.png new file mode 100644 index 0000000..d2935f3 --- /dev/null +++ b/activity/tasks/assets/back.png @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/activity/tasks/assets/button-glow.png b/activity/tasks/assets/button-glow.png new file mode 100644 index 0000000..4591774 Binary files /dev/null and b/activity/tasks/assets/button-glow.png differ diff --git a/activity/tasks/assets/coin.png b/activity/tasks/assets/coin.png new file mode 100644 index 0000000..f61a28e Binary files /dev/null and b/activity/tasks/assets/coin.png differ diff --git a/activity/tasks/assets/figma-game-icon.png b/activity/tasks/assets/figma-game-icon.png new file mode 100644 index 0000000..34568c3 Binary files /dev/null and b/activity/tasks/assets/figma-game-icon.png differ diff --git a/activity/tasks/assets/figma-task-full.png b/activity/tasks/assets/figma-task-full.png new file mode 100644 index 0000000..f166f06 Binary files /dev/null and b/activity/tasks/assets/figma-task-full.png differ diff --git a/activity/tasks/assets/task-bg.png b/activity/tasks/assets/task-bg.png new file mode 100644 index 0000000..96c9acb Binary files /dev/null and b/activity/tasks/assets/task-bg.png differ diff --git a/activity/tasks/assets/task-h5-preview-wide.png b/activity/tasks/assets/task-h5-preview-wide.png new file mode 100644 index 0000000..c0ebc6e Binary files /dev/null and b/activity/tasks/assets/task-h5-preview-wide.png differ diff --git a/activity/tasks/assets/task-h5-preview.png b/activity/tasks/assets/task-h5-preview.png new file mode 100644 index 0000000..0537359 Binary files /dev/null and b/activity/tasks/assets/task-h5-preview.png differ diff --git a/activity/tasks/index.html b/activity/tasks/index.html new file mode 100644 index 0000000..e22f520 --- /dev/null +++ b/activity/tasks/index.html @@ -0,0 +1,84 @@ + + + + + + Task + + + + +
+
+ + +
+
+

Daily Tasks

+

+ Resets daily at midnight +

+
+
+
+ +
+
+

+ Exclusive For Newcomers +

+

+ Limited to one time +

+
+
+
+ + +
+
+ + + + + + + + diff --git a/activity/tasks/script.js b/activity/tasks/script.js new file mode 100644 index 0000000..0aa4611 --- /dev/null +++ b/activity/tasks/script.js @@ -0,0 +1,594 @@ +(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' }); + } +})(); diff --git a/activity/tasks/style.css b/activity/tasks/style.css new file mode 100644 index 0000000..a0a8d99 --- /dev/null +++ b/activity/tasks/style.css @@ -0,0 +1,325 @@ +* { + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +:root { + --app-scale: 1; + --task-width: 100%; + --task-bg: #130a2b; + --task-white: #ffffff; + --task-muted: rgba(255, 255, 255, 0.62); + --task-line: rgba(255, 255, 255, 0.16); + --task-purple: rgba(217, 0, 255, 0.32); + --task-gold: #ffd155; + --task-gold-border: #fff09b; + --task-red-shadow: #be1e21; +} + +html, +body { + margin: 0; + width: 100%; + min-height: 100%; + overflow-x: hidden; + background: var(--task-bg); + color: var(--task-white); + font-family: + 'Source Han Sans SC', Arial, 'Helvetica Neue', Helvetica, sans-serif; +} + +button { + border: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; + outline: none; +} + +.app-viewport { + position: relative; + width: var(--task-width); + min-height: 100vh; + margin: 0; +} + +.task-page { + position: relative; + width: 100%; + min-height: 100vh; + overflow-x: hidden; + padding: 44px 0 36px; + background: var(--task-bg); +} + +.task-page::before { + content: ''; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background: + linear-gradient(180deg, rgba(19, 10, 43, 0.08), rgba(19, 10, 43, 0.12)), + url('./assets/task-bg.png') center top / 100% 100% no-repeat; +} + +.task-nav { + position: relative; + z-index: 2; + width: 100%; + height: 44px; +} + +.task-nav h1 { + position: absolute; + left: 50%; + top: 13px; + width: 180px; + margin: 0 0 0 -90px; + color: var(--task-white); + font-size: 18px; + font-weight: 500; + line-height: 18px; + text-align: center; + text-transform: capitalize; +} + +.back-button { + position: absolute; + left: 16px; + top: 10px; + width: 24px; + height: 24px; +} + +.back-button span { + position: absolute; + left: 7px; + top: 4px; + width: 14px; + height: 14px; + border-left: 2px solid var(--task-white); + border-bottom: 2px solid var(--task-white); + transform: rotate(45deg); +} + +.task-section { + position: relative; + z-index: 1; + margin-top: 16px; +} + +.task-section-exclusive { + margin-top: 18px; +} + +.section-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + min-height: 20px; + padding: 0 16px; +} + +.section-head h2 { + margin: 0; + color: var(--task-white); + font-size: 16px; + font-weight: 800; + line-height: 16px; + text-transform: capitalize; + text-shadow: + 0 2px 0 var(--task-red-shadow), + 0 0 4px rgba(244, 203, 255, 0.55); +} + +.section-head p { + margin: 2px 0 0; + color: var(--task-white); + font-size: 12px; + font-weight: 400; + line-height: 12px; + text-align: right; + white-space: nowrap; +} + +.task-list { + margin-top: 12px; +} + +.task-row { + position: relative; + display: grid; + grid-template-columns: 42px minmax(0, 1fr) 52px; + column-gap: 9px; + align-items: center; + min-height: 65px; + padding: 0 16px; +} + +.task-row::after { + content: ''; + position: absolute; + left: 12px; + right: 12px; + bottom: 0; + height: 1px; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0), + var(--task-line) 50%, + rgba(255, 255, 255, 0) + ); +} + +.task-row:last-child::after { + opacity: 0.55; +} + +.task-icon { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + overflow: hidden; + border-radius: 50%; + background: var(--task-purple); +} + +.task-icon img, +.task-icon svg { + display: block; + width: 29px; + height: 29px; +} + +.task-icon.is-image { + border-radius: 8px; + background: rgba(255, 255, 255, 0.1); +} + +.task-icon.is-image img { + width: 38px; + height: 38px; + border-radius: 8px; + object-fit: cover; +} + +.task-main { + min-width: 0; + padding-top: 1px; +} + +.task-title { + display: block; + max-width: 205px; + overflow: hidden; + color: var(--task-white); + font-size: 12px; + font-weight: 400; + line-height: 14px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-progress { + display: block; + margin-top: 2px; + color: var(--task-muted); + font-size: 10px; + font-weight: 400; + line-height: 10px; +} + +.task-reward { + display: inline-flex; + align-items: center; + gap: 2px; + margin-top: 7px; + color: var(--task-gold); + font-size: 12px; + font-weight: 400; + line-height: 12px; +} + +.task-reward img { + width: 12px; + height: 12px; +} + +.task-action { + position: relative; + width: 52px; + height: 20px; + border: 1px solid var(--task-gold-border); + background: + linear-gradient( + 175deg, + rgba(70, 42, 120, 0.72) 0%, + rgba(59, 36, 100, 0) 72% + ), + #130828; + color: var(--task-white); + font-size: 12px; + font-weight: 400; + line-height: 18px; + text-align: center; +} + +.task-action.is-claimable { + border-color: rgba(255, 240, 155, 0.95); + background: + linear-gradient( + 90deg, + rgba(129, 53, 159, 0.15), + rgba(255, 0, 199, 0.62) + ), + url('./assets/button-glow.png') center / 98px 28px no-repeat, + #130828; +} + +.task-action.is-claimed, +.task-action:disabled { + cursor: default; +} + +.task-action.is-claimed { + border-color: rgba(255, 255, 255, 0.72); + background: + linear-gradient(175deg, rgba(70, 42, 120, 0.4), rgba(59, 36, 100, 0)), + #3a3348; + color: rgba(255, 255, 255, 0.86); +} + +.task-action.is-loading { + color: rgba(255, 255, 255, 0.72); +} + +.task-state { + position: relative; + z-index: 1; + margin: 64px 24px 0; + padding: 14px 16px; + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(19, 8, 40, 0.62); + color: rgba(255, 255, 255, 0.86); + font-size: 13px; + line-height: 18px; + text-align: center; +} + +.task-list .task-state { + margin: 4px 16px 0; +} + +@media (max-width: 374px) { + .app-viewport { + margin-left: 0; + margin-right: 0; + } +} diff --git a/common/api.js b/common/api.js index a65cd5a..bb402ed 100644 --- a/common/api.js +++ b/common/api.js @@ -27,7 +27,9 @@ var default_api = 'https://api.global-interaction.com/'; function decodeBase64URLJSON(value) { try { - var normalized = String(value || '').replace(/-/g, '+').replace(/_/g, '/'); + var normalized = String(value || '') + .replace(/-/g, '+') + .replace(/_/g, '/'); while (normalized.length % 4) normalized += '='; return JSON.parse(window.atob(normalized)); } catch (_) { @@ -120,7 +122,8 @@ var default_api = 'https://api.global-interaction.com/'; return memoryAccessToken; if (memoryAccessToken && isExpiredToken(memoryAccessToken)) { memoryAccessToken = ''; - if (shouldPersist()) window.localStorage.removeItem(ACCESS_TOKEN_KEY); + if (shouldPersist()) + window.localStorage.removeItem(ACCESS_TOKEN_KEY); } var queryToken = normalizeToken(readQuery('token')) || @@ -636,6 +639,20 @@ var default_api = 'https://api.global-interaction.com/'; }, }; + // 任务页 API 只封装 H5 需要的两个 App 合约;登录态、app_code、env 和错误 envelope 仍由 request 统一处理。 + // 领取奖励的 command_id 必须来自点击动作本身,页面脚本负责生成并在同一次重试中复用,避免重复点击造成多次发奖。 + var taskAPI = { + tabs: function () { + return request('/api/v1/tasks/tabs', { method: 'GET' }); + }, + claim: function (payload) { + return request('/api/v1/tasks/claim', { + method: 'POST', + body: payload || {}, + }); + }, + }; + var weeklyStarAPI = { current: function () { return request('/api/v1/activities/weekly-star/current', { @@ -734,6 +751,7 @@ var default_api = 'https://api.global-interaction.com/'; managerCenter: managerCenterAPI, superadminCenter: superadminCenterAPI, level: levelAPI, + task: taskAPI, weeklyStar: weeklyStarAPI, game: gameAPI, }; diff --git a/common/jsbridge.js b/common/jsbridge.js index 3be34f9..3bf35fc 100644 --- a/common/jsbridge.js +++ b/common/jsbridge.js @@ -62,6 +62,9 @@ board_type: String(boardType || ''), }); }, + openTaskAction: function (payload) { + return post('taskAction', payload || {}); + }, ready: function (payload) { return post('ready', payload || {}); }, diff --git a/common/locales/ar.json b/common/locales/ar.json index 96211f3..5f3befb 100644 --- a/common/locales/ar.json +++ b/common/locales/ar.json @@ -18,7 +18,7 @@ "diceMatch.apiUnavailable": "واجهة لعبة النرد غير متاحة", "diceMatch.selectStake": "اختر قيمة الرهان", "diceMatch.requestFailed": "فشل الطلب", - "diceMatch.configFeeNote": "الرسوم {fee}% · المجمع {pool}%", + "diceMatch.configFeeNote": "الرسوم {fee}%", "roomReward.pageTitle": "فعالية مكافآت الغرفة", "roomReward.pageLabel": "فعالية مكافآت الغرفة", "roomReward.currentTarget": "هدف المكافأة الحالي", @@ -458,5 +458,26 @@ "recharge.status.pending": "قيد الانتظار", "recharge.status.redirected": "بانتظار الدفع", "recharge.status.credited": "تمت الإضافة", - "recharge.status.failed": "فشل" + "recharge.status.failed": "فشل", + "tasks.pageTitle": "المهام", + "tasks.pageLabel": "مركز المهام", + "tasks.navLabel": "تنقل المهام", + "tasks.back": "رجوع", + "tasks.title": "المهام", + "tasks.dailyTitle": "المهام اليومية", + "tasks.dailyReset": "تتم إعادة التعيين يوميا عند منتصف الليل", + "tasks.exclusiveTitle": "حصري للوافدين الجدد", + "tasks.exclusiveLimit": "مرة واحدة فقط", + "tasks.go": "اذهب", + "tasks.claim": "استلام", + "tasks.claiming": "جار الاستلام...", + "tasks.claimed": "تم الاستلام", + "tasks.empty": "لا توجد مهام بعد.", + "tasks.loading": "جار التحميل...", + "tasks.untitled": "مهمة", + "tasks.apiUnavailable": "واجهة المهام غير متاحة.", + "tasks.loadFailed": "فشل التحميل.", + "tasks.claimSuccess": "تم استلام المكافأة.", + "tasks.claimFailed": "فشل الاستلام.", + "tasks.noAction": "لم يتم إعداد الانتقال." } diff --git a/common/locales/en.json b/common/locales/en.json index d28306d..5ea502d 100644 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -18,7 +18,7 @@ "diceMatch.apiUnavailable": "Dice API unavailable", "diceMatch.selectStake": "Select a stake", "diceMatch.requestFailed": "Request failed", - "diceMatch.configFeeNote": "Fee {fee}% · Pool {pool}%", + "diceMatch.configFeeNote": "Fee {fee}%", "roomReward.pageTitle": "Room Reward Event", "roomReward.pageLabel": "Room Reward Event", "roomReward.currentTarget": "Current reward target", @@ -464,5 +464,26 @@ "recharge.status.pending": "Pending", "recharge.status.redirected": "Waiting for payment", "recharge.status.credited": "Credited", - "recharge.status.failed": "Failed" + "recharge.status.failed": "Failed", + "tasks.pageTitle": "Task", + "tasks.pageLabel": "Task center", + "tasks.navLabel": "Task navigation", + "tasks.back": "Back", + "tasks.title": "Task", + "tasks.dailyTitle": "Daily Tasks", + "tasks.dailyReset": "Resets daily at midnight", + "tasks.exclusiveTitle": "Exclusive For Newcomers", + "tasks.exclusiveLimit": "Limited to one time", + "tasks.go": "Go", + "tasks.claim": "Claim", + "tasks.claiming": "Claiming...", + "tasks.claimed": "Claimed", + "tasks.empty": "No tasks yet.", + "tasks.loading": "Loading...", + "tasks.untitled": "Task", + "tasks.apiUnavailable": "Task API unavailable.", + "tasks.loadFailed": "Load failed.", + "tasks.claimSuccess": "Reward claimed.", + "tasks.claimFailed": "Claim failed.", + "tasks.noAction": "No action configured." } diff --git a/common/locales/es.json b/common/locales/es.json index f28b61b..4b01621 100644 --- a/common/locales/es.json +++ b/common/locales/es.json @@ -18,7 +18,7 @@ "diceMatch.apiUnavailable": "API del juego de dados no disponible", "diceMatch.selectStake": "Selecciona una apuesta", "diceMatch.requestFailed": "Error de solicitud", - "diceMatch.configFeeNote": "Comisión {fee}% · Bolsa {pool}%", + "diceMatch.configFeeNote": "Comisión {fee}%", "roomReward.pageTitle": "Evento de recompensas de sala", "roomReward.pageLabel": "Evento de recompensas de sala", "roomReward.currentTarget": "Objetivo de recompensa actual", @@ -458,5 +458,26 @@ "recharge.status.pending": "Pendiente", "recharge.status.redirected": "Esperando pago", "recharge.status.credited": "Acreditado", - "recharge.status.failed": "Fallido" + "recharge.status.failed": "Fallido", + "tasks.pageTitle": "Tareas", + "tasks.pageLabel": "Centro de tareas", + "tasks.navLabel": "Navegación de tareas", + "tasks.back": "Atrás", + "tasks.title": "Tareas", + "tasks.dailyTitle": "Tareas diarias", + "tasks.dailyReset": "Se reinicia cada día a medianoche", + "tasks.exclusiveTitle": "Exclusivo para nuevos usuarios", + "tasks.exclusiveLimit": "Limitado a una vez", + "tasks.go": "Ir", + "tasks.claim": "Reclamar", + "tasks.claiming": "Reclamando...", + "tasks.claimed": "Reclamado", + "tasks.empty": "Aún no hay tareas.", + "tasks.loading": "Cargando...", + "tasks.untitled": "Tarea", + "tasks.apiUnavailable": "API de tareas no disponible.", + "tasks.loadFailed": "Error al cargar.", + "tasks.claimSuccess": "Recompensa reclamada.", + "tasks.claimFailed": "Error al reclamar.", + "tasks.noAction": "No hay acción configurada." } diff --git a/common/locales/id.json b/common/locales/id.json index 5f390e1..faae215 100644 --- a/common/locales/id.json +++ b/common/locales/id.json @@ -447,5 +447,26 @@ "recharge.status.pending": "Menunggu", "recharge.status.redirected": "Menunggu pembayaran", "recharge.status.credited": "Masuk", - "recharge.status.failed": "Gagal" + "recharge.status.failed": "Gagal", + "tasks.pageTitle": "Tugas", + "tasks.pageLabel": "Pusat tugas", + "tasks.navLabel": "Navigasi tugas", + "tasks.back": "Kembali", + "tasks.title": "Tugas", + "tasks.dailyTitle": "Tugas Harian", + "tasks.dailyReset": "Reset setiap tengah malam", + "tasks.exclusiveTitle": "Khusus pengguna baru", + "tasks.exclusiveLimit": "Terbatas satu kali", + "tasks.go": "Pergi", + "tasks.claim": "Klaim", + "tasks.claiming": "Mengklaim...", + "tasks.claimed": "Diklaim", + "tasks.empty": "Belum ada tugas.", + "tasks.loading": "Memuat...", + "tasks.untitled": "Tugas", + "tasks.apiUnavailable": "API tugas tidak tersedia.", + "tasks.loadFailed": "Gagal memuat.", + "tasks.claimSuccess": "Hadiah diklaim.", + "tasks.claimFailed": "Gagal klaim.", + "tasks.noAction": "Aksi belum dikonfigurasi." } diff --git a/common/locales/tr.json b/common/locales/tr.json index 683eba2..f2ba698 100644 --- a/common/locales/tr.json +++ b/common/locales/tr.json @@ -447,5 +447,26 @@ "recharge.status.pending": "Bekliyor", "recharge.status.redirected": "Ödeme bekleniyor", "recharge.status.credited": "Yüklendi", - "recharge.status.failed": "Başarısız" + "recharge.status.failed": "Başarısız", + "tasks.pageTitle": "Görev", + "tasks.pageLabel": "Görev merkezi", + "tasks.navLabel": "Görev navigasyonu", + "tasks.back": "Geri", + "tasks.title": "Görev", + "tasks.dailyTitle": "Günlük Görevler", + "tasks.dailyReset": "Her gün gece yarısı yenilenir", + "tasks.exclusiveTitle": "Yeni kullanıcıya özel", + "tasks.exclusiveLimit": "Bir kez ile sınırlı", + "tasks.go": "Git", + "tasks.claim": "Al", + "tasks.claiming": "Alınıyor...", + "tasks.claimed": "Alındı", + "tasks.empty": "Henüz görev yok.", + "tasks.loading": "Yükleniyor...", + "tasks.untitled": "Görev", + "tasks.apiUnavailable": "Görev API kullanılamıyor.", + "tasks.loadFailed": "Yükleme başarısız.", + "tasks.claimSuccess": "Ödül alındı.", + "tasks.claimFailed": "Alma başarısız.", + "tasks.noAction": "Eylem yapılandırılmadı." } diff --git a/common/locales/zh.json b/common/locales/zh.json index bc2c983..81470f9 100644 --- a/common/locales/zh.json +++ b/common/locales/zh.json @@ -453,5 +453,26 @@ "recharge.status.pending": "待支付", "recharge.status.redirected": "等待支付", "recharge.status.credited": "已到账", - "recharge.status.failed": "失败" + "recharge.status.failed": "失败", + "tasks.pageTitle": "任务", + "tasks.pageLabel": "任务中心", + "tasks.navLabel": "任务导航", + "tasks.back": "返回", + "tasks.title": "任务", + "tasks.dailyTitle": "每日任务", + "tasks.dailyReset": "每日0点重置", + "tasks.exclusiveTitle": "新人专属", + "tasks.exclusiveLimit": "限一次", + "tasks.go": "去", + "tasks.claim": "领取", + "tasks.claiming": "领取中...", + "tasks.claimed": "已领取", + "tasks.empty": "暂无任务", + "tasks.loading": "加载中...", + "tasks.untitled": "任务", + "tasks.apiUnavailable": "任务接口不可用", + "tasks.loadFailed": "加载失败", + "tasks.claimSuccess": "领取成功", + "tasks.claimFailed": "领取失败", + "tasks.noAction": "未配置跳转" } diff --git a/game/touzi/index.html b/game/touzi/index.html index 102ffa5..118e76a 100644 --- a/game/touzi/index.html +++ b/game/touzi/index.html @@ -77,6 +77,11 @@ animation: screen-impact-shake 520ms ease-out 420ms both; } + .dice-frame.is-self-win-compare { + animation: you-win-screen-smash 780ms + cubic-bezier(0.14, 0.84, 0.18, 1) 430ms both; + } + .game-content { position: absolute; inset: 0; @@ -182,12 +187,14 @@ position: relative; display: flex; align-items: center; - justify-content: center; - width: 102px; + justify-content: flex-end; + width: max-content; + min-width: 102px; + max-width: 176px; height: 32px; - flex: 0 0 102px; + flex: 0 1 auto; gap: 4px; - overflow: hidden; + overflow: visible; padding: 0 7px; /* border: 1px solid rgba(108, 113, 194, 0.96); border-radius: 4px; */ @@ -218,16 +225,16 @@ .top-coin span { display: block; - min-width: 0; - max-width: 48px; - flex: 0 1 auto; - overflow: hidden; + min-width: max-content; + max-width: none; + flex: 0 0 auto; + overflow: visible; line-height: 20px; } .top-coin.is-compact span { - max-width: 38px; - font-size: 16px; + max-width: none; + font-size: 18px; } .top-coin img { @@ -933,6 +940,108 @@ opacity: 1; } + .you-win-smash-layer, + .you-win-impact { + position: absolute; + visibility: hidden; + opacity: 0; + pointer-events: none; + } + + .you-win-smash-layer { + inset: 0; + z-index: 23; + overflow: hidden; + } + + .you-win-smash-layer::before { + position: absolute; + inset: 0; + content: ''; + background: + radial-gradient( + circle at 50% 39%, + rgba(255, 248, 179, 0.72) 0%, + rgba(255, 126, 20, 0.3) 18%, + rgba(255, 73, 0, 0.08) 35%, + rgba(255, 73, 0, 0) 58% + ), + rgba(255, 255, 255, 0.2); + opacity: 0; + } + + .you-win-smash-floor { + position: absolute; + top: 362px; + left: 50%; + width: 312px; + height: 92px; + border-radius: 50%; + background: radial-gradient( + ellipse at center, + rgba(255, 215, 75, 0.68) 0%, + rgba(255, 103, 15, 0.34) 30%, + rgba(120, 16, 0, 0.28) 54%, + rgba(0, 0, 0, 0) 76% + ); + opacity: 0; + filter: blur(1px) drop-shadow(0 0 18px rgba(255, 118, 22, 0.76)); + transform: translateX(-50%) scale(0.38, 0.2); + transform-origin: 50% 50%; + } + + .dice-frame.is-self-win-compare .you-win-smash-layer { + visibility: visible; + opacity: 1; + } + + .dice-frame.is-self-win-compare .you-win-smash-layer::before { + animation: you-win-screen-flash 760ms ease-out 430ms both; + } + + .dice-frame.is-self-win-compare .you-win-smash-floor { + animation: you-win-floor-impact 900ms ease-out 430ms both; + } + + .you-win-impact { + top: 300px; + left: 0; + z-index: 25; + display: flex; + align-items: center; + justify-content: center; + width: 375px; + height: 92px; + color: transparent; + font-size: 56px; + font-weight: 1000; + line-height: 62px; + text-align: center; + background: linear-gradient( + 180deg, + #ffffff 0%, + #fff5b6 19%, + #ffcf44 48%, + #ff7a00 74%, + #9b2d00 100% + ); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-stroke: 2px #7d2700; + filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9)) + drop-shadow(0 0 22px rgba(255, 205, 46, 0.9)) + drop-shadow(0 0 36px rgba(255, 77, 0, 0.78)); + transform-origin: 50% 78%; + will-change: opacity, transform, filter; + } + + .dice-frame.is-self-win-compare .you-win-impact { + visibility: visible; + opacity: 1; + animation: you-win-drop-impact 980ms + cubic-bezier(0.13, 0.92, 0.18, 1) both; + } + .countdown-number { min-width: 132px; color: transparent; @@ -1264,6 +1373,10 @@ .dice-frame.is-matching .vs-badge, .dice-frame.is-matching .vs-badge::after, + .dice-frame.is-self-win-compare, + .dice-frame.is-self-win-compare .you-win-smash-layer::before, + .dice-frame.is-self-win-compare .you-win-smash-floor, + .dice-frame.is-self-win-compare .you-win-impact, .dice-frame.is-matching, .dice-frame.is-searching .center-search-bg, .dice-frame.is-searching .search-indicator img, @@ -1343,6 +1456,105 @@ } } + @keyframes you-win-drop-impact { + 0% { + opacity: 0; + filter: brightness(1.9) drop-shadow(0 0 36px #ff79ff); + transform: translateY(-650px) scale(3.9) rotate(-7deg); + } + 34% { + opacity: 1; + filter: brightness(1.75) drop-shadow(0 0 32px #4cc9ff); + transform: translateY(-122px) scale(2.05) rotate(4deg); + } + 58% { + opacity: 1; + filter: brightness(1.55) drop-shadow(0 0 30px #ffd557); + transform: translateY(28px) scale(0.78) rotate(1deg); + } + 72% { + filter: brightness(1.42) drop-shadow(0 0 24px #ff79ff); + transform: translateY(-16px) scale(1.16) rotate(-0.8deg); + } + 86% { + filter: brightness(1.18) drop-shadow(0 0 16px #4cc9ff); + transform: translateY(4px) scale(0.96) rotate(0.3deg); + } + 100% { + opacity: 1; + filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9)) + drop-shadow(0 0 22px rgba(255, 205, 46, 0.9)) + drop-shadow(0 0 36px rgba(255, 77, 0, 0.78)); + transform: translateY(0) scale(1) rotate(0); + } + } + + @keyframes you-win-screen-smash { + 0%, + 100% { + transform: translate3d(0, 0, 0) scale(var(--dice-scale)); + filter: none; + } + 12% { + transform: translate3d(0, 14px, 0) scale(var(--dice-scale)); + filter: brightness(1.22) contrast(1.08); + } + 24% { + transform: translate3d(-12px, -8px, 0) + scale(var(--dice-scale)); + } + 36% { + transform: translate3d(10px, 7px, 0) + scale(var(--dice-scale)); + } + 50% { + transform: translate3d(-7px, 4px, 0) + scale(var(--dice-scale)); + } + 66% { + transform: translate3d(5px, -3px, 0) + scale(var(--dice-scale)); + filter: brightness(1.08) contrast(1.03); + } + 82% { + transform: translate3d(-2px, 1px, 0) + scale(var(--dice-scale)); + } + } + + @keyframes you-win-screen-flash { + 0% { + opacity: 0; + } + 8% { + opacity: 1; + } + 22% { + opacity: 0.46; + } + 100% { + opacity: 0; + } + } + + @keyframes you-win-floor-impact { + 0% { + opacity: 0; + transform: translateX(-50%) scale(0.38, 0.2); + } + 16% { + opacity: 1; + transform: translateX(-50%) scale(1.05, 0.52); + } + 58% { + opacity: 0.62; + } + 100% { + opacity: 0; + transform: translateX(-50%) scale(1.32, 0.72); + } + } + @keyframes search-orbit { 0% { transform: rotate(0deg) @@ -2042,6 +2254,20 @@ data-role="phase-text" aria-live="polite" > + +
= 100000 + number >= 1000000 ); topCoin.setAttribute('title', fullLabel); topCoin.setAttribute( @@ -2711,16 +2919,17 @@ selectedButton || document.querySelector('.stake') ); if (config) { - var fee = Number(config.fee_bps || 0) / 100; - var pool = Number(config.pool_bps || 0) / 100; + var totalFee = + (Number(config.fee_bps || 0) + + Number(config.pool_bps || 0)) / + 100; setText( document.querySelector('.fee-note'), i18nFormat( 'diceMatch.configFeeNote', - 'Fee {fee}% · Pool {pool}%', + 'Fee {fee}%', { - fee: fee, - pool: pool, + fee: totalFee, } ) ); @@ -2734,6 +2943,7 @@ 'is-matching', 'is-countdown', 'is-victory', + 'is-self-win-compare', 'has-scores' ); setCountdownNumber(''); @@ -2867,6 +3077,22 @@ ); } + function isSelfWinner(match) { + var self = findSelfParticipant(match); + var winner = findWinnerParticipant(match); + if (!self) return false; + if (winner && sameParticipant(winner, self)) return true; + return self.result === 'win'; + } + + function setSelfWinCompare(visible) { + frame.classList.remove('is-self-win-compare'); + if (!visible) return; + // 每一局 Compare 都要重新播放“砸下”效果,因此先读一次布局强制浏览器提交移除状态。 + void frame.offsetWidth; + frame.classList.add('is-self-win-compare'); + } + function renderMatchParticipants(match) { var self = findSelfParticipant(match); var opponent = findOpponentParticipant(match, self); @@ -3064,12 +3290,18 @@ stopRollEffects(); renderMatchParticipants(match); frame.classList.add('has-scores'); + var drawReroll = isDrawReroll(match); + var selfWinCompare = + !drawReroll && isSelfWinner(match); setPhaseText( - isDrawReroll(match) - ? i18nText('diceMatch.draw', 'Draw') - : i18nText('diceMatch.compare', 'Compare') + selfWinCompare + ? '' + : drawReroll + ? i18nText('diceMatch.draw', 'Draw') + : i18nText('diceMatch.compare', 'Compare') ); - if (isDrawReroll(match)) { + setSelfWinCompare(selfWinCompare); + if (drawReroll) { return delay(2000).then(function () { if (sequence !== roundSequence) return; rollAndAnimate(sequence);