任务和游戏
9
activity/tasks/assets/back.png
Normal file
@ -0,0 +1,9 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon_20_返回">
|
||||
<g id="Vector">
|
||||
</g>
|
||||
<g id="Vector_2">
|
||||
</g>
|
||||
<path id="Vector_3" d="M16 4L8 12L16 20" stroke="var(--stroke-0, white)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
BIN
activity/tasks/assets/button-glow.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
activity/tasks/assets/coin.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
activity/tasks/assets/figma-game-icon.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
activity/tasks/assets/figma-task-full.png
Normal file
|
After Width: | Height: | Size: 397 KiB |
BIN
activity/tasks/assets/task-bg.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
activity/tasks/assets/task-h5-preview-wide.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
activity/tasks/assets/task-h5-preview.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
84
activity/tasks/index.html
Normal file
@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title data-i18n="tasks.pageTitle">Task</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="../../common/theme.css?v=20260611-task-h5"
|
||||
/>
|
||||
<link rel="stylesheet" href="./style.css?v=20260611-task-h5" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-viewport" id="appViewport">
|
||||
<main
|
||||
class="task-page"
|
||||
id="page"
|
||||
aria-label="Task center"
|
||||
data-i18n-aria="tasks.pageLabel"
|
||||
>
|
||||
<nav
|
||||
class="task-nav"
|
||||
aria-label="Task navigation"
|
||||
data-i18n-aria="tasks.navLabel"
|
||||
>
|
||||
<button
|
||||
class="back-button"
|
||||
id="backButton"
|
||||
type="button"
|
||||
aria-label="Back"
|
||||
data-i18n-aria="tasks.back"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
</button>
|
||||
<h1 data-i18n="tasks.title">Task</h1>
|
||||
</nav>
|
||||
|
||||
<section class="task-section" data-section-block="daily">
|
||||
<header class="section-head">
|
||||
<h2 data-i18n="tasks.dailyTitle">Daily Tasks</h2>
|
||||
<p data-refresh-text data-i18n="tasks.dailyReset">
|
||||
Resets daily at midnight
|
||||
</p>
|
||||
</header>
|
||||
<div
|
||||
class="task-list"
|
||||
id="dailyTasks"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="task-section task-section-exclusive"
|
||||
data-section-block="exclusive"
|
||||
>
|
||||
<header class="section-head">
|
||||
<h2 data-i18n="tasks.exclusiveTitle">
|
||||
Exclusive For Newcomers
|
||||
</h2>
|
||||
<p data-i18n="tasks.exclusiveLimit">
|
||||
Limited to one time
|
||||
</p>
|
||||
</header>
|
||||
<div
|
||||
class="task-list"
|
||||
id="exclusiveTasks"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<div class="task-state" id="taskState" hidden></div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="../../common/i18n.js?v=20260611-task-h5"></script>
|
||||
<script src="../../common/api.js?v=20260611-task-h5"></script>
|
||||
<script src="../../common/jsbridge.js?v=20260611-task-h5"></script>
|
||||
<script src="../../common/toast.js?v=20260611-task-h5"></script>
|
||||
<script src="./script.js?v=20260611-task-h5"></script>
|
||||
</body>
|
||||
</html>
|
||||
594
activity/tasks/script.js
Normal file
@ -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, '"')
|
||||
.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 [
|
||||
'<span class="task-icon is-image" data-icon-fallback="',
|
||||
escapeAttr(iconKind(item)),
|
||||
'"><img src="',
|
||||
escapeAttr(iconURL),
|
||||
'" alt="" /></span>',
|
||||
].join('');
|
||||
}
|
||||
return (
|
||||
'<span class="task-icon">' + svgForIcon(iconKind(item)) + '</span>'
|
||||
);
|
||||
}
|
||||
|
||||
function svgForIcon(kind) {
|
||||
if (kind === 'mic') {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M12 3a4 4 0 0 0-4 4v5a4 4 0 0 0 8 0V7a4 4 0 0 0-4-4Zm-6 8.5a1 1 0 0 1 2 0V12a4 4 0 0 0 8 0v-.5a1 1 0 1 1 2 0V12a6 6 0 0 1-5 5.92V20h3a1 1 0 1 1 0 2H8a1 1 0 1 1 0-2h3v-2.08A6 6 0 0 1 6 12v-.5Z"/></svg>';
|
||||
}
|
||||
if (kind === 'gift') {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M8.2 4.2A3 3 0 0 1 12 5a3 3 0 0 1 3.8-.8A3.1 3.1 0 0 1 17 8h2a1 1 0 0 1 1 1v3h-7V8h-2v4H4V9a1 1 0 0 1 1-1h2a3.1 3.1 0 0 1 1.2-3.8ZM9 6a1 1 0 0 0 .2 2H11a2 2 0 0 0-2-2Zm4 2h1.8A1 1 0 1 0 13 6a2 2 0 0 0 0 2ZM5 14h6v6H6a1 1 0 0 1-1-1v-5Zm8 6v-6h6v5a1 1 0 0 1-1 1h-5Z"/></svg>';
|
||||
}
|
||||
if (kind === 'cp') {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M7.5 6A3.5 3.5 0 0 1 12 9.35 3.5 3.5 0 1 1 16.5 6c2.45 0 4.5 1.9 4.5 4.35 0 4.25-6.05 8.05-8.35 9.35a1.3 1.3 0 0 1-1.3 0C9.05 18.4 3 14.6 3 10.35 3 7.9 5.05 6 7.5 6Z"/></svg>';
|
||||
}
|
||||
if (kind === 'mood') {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-3 7.2a1.2 1.2 0 1 1 0-2.4 1.2 1.2 0 0 1 0 2.4Zm6 0a1.2 1.2 0 1 1 0-2.4 1.2 1.2 0 0 1 0 2.4Zm-6.3 3.35a1 1 0 0 1 1.4.1 2.5 2.5 0 0 0 3.8 0 1 1 0 0 1 1.5 1.3 4.5 4.5 0 0 1-6.8 0 1 1 0 0 1 .1-1.4Z"/></svg>';
|
||||
}
|
||||
if (kind === 'coin') {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M12 3c4.42 0 8 2.02 8 4.5V16c0 2.76-3.58 5-8 5s-8-2.24-8-5V7.5C4 5.02 7.58 3 12 3Zm0 2C8.14 5 6 6.47 6 7.5S8.14 10 12 10s6-1.47 6-2.5S15.86 5 12 5Zm6 5.3c-1.45 1.05-3.6 1.7-6 1.7s-4.55-.65-6-1.7V12c0 1.2 2.33 3 6 3s6-1.8 6-3v-1.7Zm0 5c-1.45 1.05-3.6 1.7-6 1.7s-4.55-.65-6-1.7V16c0 1.2 2.33 3 6 3s6-1.8 6-3v-.7Z"/></svg>';
|
||||
}
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M8 9h8a4 4 0 0 1 0 8h-.5a2 2 0 0 1-1.6-.8l-.9-1.2h-2l-.9 1.2a2 2 0 0 1-1.6.8H8a4 4 0 0 1 0-8Zm.5 3H7v1.5h1.5V15H10v-1.5h1.5V12H10v-1.5H8.5V12Zm6.5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm2.5 2.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"/></svg>';
|
||||
}
|
||||
|
||||
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 =
|
||||
'<div class="task-state">' +
|
||||
escapeHTML(t('tasks.empty', 'No tasks yet.')) +
|
||||
'</div>';
|
||||
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 [
|
||||
'<article class="task-row" data-task-key="',
|
||||
escapeAttr(key),
|
||||
'">',
|
||||
iconHTML(item),
|
||||
'<div class="task-main">',
|
||||
'<strong class="task-title">',
|
||||
escapeHTML(
|
||||
item.title || item.description || t('tasks.untitled', 'Task')
|
||||
),
|
||||
'</strong>',
|
||||
'<span class="task-progress">',
|
||||
escapeHTML(formatProgress(item)),
|
||||
'</span>',
|
||||
'<span class="task-reward"><img src="',
|
||||
COIN_IMAGE,
|
||||
'" alt="" aria-hidden="true" /><span>',
|
||||
escapeHTML(
|
||||
formatReward(item.reward_coin_amount || item.rewardCoinAmount)
|
||||
),
|
||||
'</span></span>',
|
||||
'</div>',
|
||||
'<button class="task-action ',
|
||||
action.className,
|
||||
loading ? ' is-loading' : '',
|
||||
'" type="button" data-task-action="',
|
||||
escapeAttr(key),
|
||||
'"',
|
||||
action.disabled || loading ? ' disabled' : '',
|
||||
'>',
|
||||
escapeHTML(label),
|
||||
'</button>',
|
||||
'</article>',
|
||||
].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' });
|
||||
}
|
||||
})();
|
||||
325
activity/tasks/style.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -62,6 +62,9 @@
|
||||
board_type: String(boardType || ''),
|
||||
});
|
||||
},
|
||||
openTaskAction: function (payload) {
|
||||
return post('taskAction', payload || {});
|
||||
},
|
||||
ready: function (payload) {
|
||||
return post('ready', payload || {});
|
||||
},
|
||||
|
||||
@ -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": "لم يتم إعداد الانتقال."
|
||||
}
|
||||
|
||||
@ -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."
|
||||
}
|
||||
|
||||
@ -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."
|
||||
}
|
||||
|
||||
@ -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."
|
||||
}
|
||||
|
||||
@ -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ı."
|
||||
}
|
||||
|
||||
@ -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": "未配置跳转"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
></div>
|
||||
<div
|
||||
class="you-win-smash-layer"
|
||||
data-role="you-win-smash"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="you-win-smash-floor"></span>
|
||||
</div>
|
||||
<div
|
||||
class="you-win-impact"
|
||||
data-role="you-win-impact"
|
||||
aria-hidden="true"
|
||||
>
|
||||
YOU WIN
|
||||
</div>
|
||||
<div
|
||||
class="countdown-layer"
|
||||
data-role="countdown-layer"
|
||||
@ -2459,30 +2685,12 @@
|
||||
return Math.trunc(number).toLocaleString('en-US');
|
||||
}
|
||||
|
||||
function trimCompactDecimal(value) {
|
||||
return String(value).replace(/\.0$/, '');
|
||||
}
|
||||
|
||||
function formatTopCoin(value) {
|
||||
var number = Number(value || 0);
|
||||
if (!Number.isFinite(number)) number = 0;
|
||||
number = Math.max(0, Math.trunc(number));
|
||||
if (number < 100000) return formatCoin(number);
|
||||
if (number < 1000000)
|
||||
return Math.floor(number / 1000) + 'K';
|
||||
if (number < 10000000)
|
||||
return (
|
||||
trimCompactDecimal(
|
||||
(Math.floor(number / 100000) / 10).toFixed(1)
|
||||
) + 'M'
|
||||
);
|
||||
if (number < 1000000000)
|
||||
return Math.floor(number / 1000000) + 'M';
|
||||
return (
|
||||
trimCompactDecimal(
|
||||
(Math.floor(number / 100000000) / 10).toFixed(1)
|
||||
) + 'B'
|
||||
);
|
||||
if (number < 1000000) return formatCoin(number);
|
||||
return Math.floor(number / 1000) + 'K';
|
||||
}
|
||||
|
||||
function setCoin(value) {
|
||||
@ -2495,7 +2703,7 @@
|
||||
if (topCoin) {
|
||||
topCoin.classList.toggle(
|
||||
'is-compact',
|
||||
number >= 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);
|
||||
|
||||