Files
aifo-portal-web/assets/js/challenge-floor-render.js
steven 4d30803b60 Initial commit: AIFO official portal web.
Add static site, shared layout, and Node dev server with standard .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 15:29:50 +08:00

1815 lines
62 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Challenge floor: reads window.CHALLENGE_FLOOR_BOOT { currencyRates, programData, challengeFeeDiscount? },
* drives UI via appState (programType, instantMode, aifoliteMode, currency, feeDiscount).
* List price = round(fee × rate); sale price = round(fee × rate × feeDiscount) to 2 decimal places (cents); integers display without “.00”.
*/
(function ($) {
var boot = window.CHALLENGE_FLOOR_BOOT;
if (!boot || !boot.programData) {
console.warn("CHALLENGE_FLOOR_BOOT missing; load challenge-floor-program-data.js first.");
return;
}
var programData = boot.programData;
var currencyRates = boot.currencyRates;
var defaultFeeDiscount =
boot && boot.challengeFeeDiscount != null && !isNaN(Number(boot.challengeFeeDiscount))
? Number(boot.challengeFeeDiscount)
: 0.5;
var appState = {
programType: "2step",
instantMode: "NoCommission",
aifoliteMode: "lite",
accountSize: "10k",
expandedParam: null,
currency: "USD",
feeDiscount: defaultFeeDiscount,
};
if (typeof window.CHALLENGE_FLOOR_APP_STATE === "object" && window.CHALLENGE_FLOOR_APP_STATE) {
$.extend(appState, window.CHALLENGE_FLOOR_APP_STATE);
}
if (appState.feeDiscount == null || (typeof appState.feeDiscount === "number" && isNaN(appState.feeDiscount))) {
appState.feeDiscount = defaultFeeDiscount;
} else {
appState.feeDiscount = normalizeFeeDiscount(appState.feeDiscount);
}
/** Theme URL for ladder SVGs etc.; set via CHALLENGE_FLOOR_BOOT.assetBase or global assets_url (header.php). */
var CHALLENGE_FLOOR_ASSET_BASE = (function () {
if (boot && boot.assetBase) {
return String(boot.assetBase).replace(/\/?$/, "/");
}
if (typeof assets_url !== "undefined" && assets_url) {
return String(assets_url).replace(/\/?$/, "/") + "assets/images/challenge/";
}
return "../assets/images/challenge/";
})();
var parameterDescriptions = {
"Challenge Fee": "One-time fee to participate in the challenge program",
"Daily Drawdown":
"Maximum percentage loss allowed in a single trading day based on the previous days closing balance or equity.",
"Max Drawdown":
"The Maximum Loss Limit is the maximum allowable loss calculated based on the highest equity value your account reaches during the evaluation or trading phase.",
"Profit Target": "The percentage gain required to successfully complete each phase of the challenge.",
"Enhanced Payout Target":
"Higher target threshold used to unlock enhanced payout benefits in this program.",
"Enhanced Payout":
"≥ 6% profit target for applying for 95% profit split and advance settlement.",
"Challenge Window":
"After account activation, users are granted a 24-hour trading window. The countdown begins upon successful challenge activation.",
"Max Floating Loss":
"The total floating loss of all open positions must not exceed 1% of the initial account balance.",
"Consistency Score":
"A profit-distribution metric that measures how evenly your profits are generated over time.",
"withdrawable profit":
"The Profit Target refers to the required percentage of withdrawable profit that the account must achieve before a payout can be requested.",
"Profit split": "Percentage of profits you keep. The firm keeps the remainder.",
"Payout period": "Minimum time required before you can request a withdrawal of profits.",
"Trading days": "Minimum trading days required to pass the phase.",
"News Trading": "",
};
var parameterDescriptionsLite = {
"Challenge Fee": "One-time fee to participate in the challenge program.",
"Profit Target": "The percentage gain required to successfully complete each phase of the challenge.",
"Enhanced Payout":
"≥ 6% profit target for applying for 95% profit split and advance settlement.",
"Challenge Window":
"After account activation, users are granted a 24-hour trading window. The countdown begins upon successful challenge activation.",
"Max Floating Loss":
"The total floating loss of all open positions must not exceed 1% of the initial account balance.",
"Max Drawdown":
"The maximum loss limit follows a static drawdown model. If account equity falls below 98% of the initial balance, the account will be immediately terminated.",
"Profit split": "Percentage of profits you keep. The firm keeps the remainder.",
};
var parameterNames = [
"Challenge Fee",
"Daily Drawdown",
"Max Drawdown",
"Profit Target",
"Profit split",
"Trading days",
"Payout period",
];
var parameterNames_instant = [
"Challenge Fee",
"Daily Drawdown",
"Max Drawdown",
"Consistency Score",
"Profit split",
"Payout period",
];
var parameterNames_lite = [
"Challenge Fee",
"Profit Target",
"Enhanced Payout",
"Challenge Window",
"Max Floating Loss",
"Max Drawdown",
"Profit split",
];
var PARAM_TO_FIELD = {
"Challenge Fee": "fee",
"Daily Drawdown": "daily",
"Max Drawdown": "max",
"Profit Target": "target",
"withdrawable profit": "target",
"Consistency Score": "consistency",
"Enhanced Payout Target": "enhanced_target",
"Enhanced Payout": "enhanced_target",
"Challenge Window": "window",
"Max Floating Loss": "floating",
"Profit split": "split",
"Profit Split": "split",
"Trading days": "tradingDays",
"Payout period": "payout",
};
var ICON_BY_PARAM = {
"Challenge Fee": "Component 26.svg",
"Daily Drawdown": "Component 27.svg",
"Max Drawdown": "Component 28.svg",
"Profit Target": "Component 29.svg",
"Enhanced Payout Target": "Component 29.svg",
"Enhanced Payout": "Component 29.svg",
"Challenge Window": "Component 29.svg",
"Max Floating Loss": "Component 28.svg",
"Consistency Score": "challenge_icon_consistency_score.png",
"withdrawable profit": "Component 29.svg",
"Profit split": "Component 30.svg",
"Profit Split": "Component 30.svg",
"Trading days": "Component 32.svg",
"Payout period": "Component 31.svg",
"News Trading": "Component 32.svg",
};
function isAifoliteEliteMode() {
return appState.programType === "aifolite" && appState.aifoliteMode === "aifo";
}
function getParameterDescription(paramName) {
if (appState.programType === "aifolite" && !isAifoliteEliteMode() && parameterDescriptionsLite[paramName]) {
return parameterDescriptionsLite[paramName];
}
if (
(appState.programType === "instant" || isAifoliteEliteMode()) &&
paramName === "Consistency Score"
) {
return (
"A profit-distribution metric that measures how evenly your profits are generated over time, requiring that no single day contributes 15% or more of total profit and that the two most profitable days combined contribute less than 25% of total profit."
);
}
return parameterDescriptions[paramName] || "";
}
function normalizeInstantMode(mode) {
if (!programData.instant) return "NoCommission";
if (mode && programData.instant[mode]) return mode;
if (programData.instant.NoCommission) return "NoCommission";
if (programData.instant.pro) return "pro";
if (programData.instant.aifo) return "aifo";
var keys = Object.keys(programData.instant);
return keys.length ? keys[0] : "NoCommission";
}
function normalizeAifoliteMode(mode) {
if (mode === "aifo" && programData.instant && programData.instant.aifo) return "aifo";
return "lite";
}
function getCurrentProgramConfig() {
var cur = appState.currency;
var p = appState.programType;
if (p === "instant") {
var mode = normalizeInstantMode(appState.instantMode);
appState.instantMode = mode;
if (!programData.instant[mode] || !programData.instant[mode][cur]) {
return null;
}
return programData.instant[mode][cur];
}
if (p === "aifolite") {
var aMode = normalizeAifoliteMode(appState.aifoliteMode);
appState.aifoliteMode = aMode;
if (aMode === "aifo") {
if (!programData.instant || !programData.instant.aifo || !programData.instant.aifo[cur]) {
return null;
}
return programData.instant.aifo[cur];
}
if (!programData.aifolite || !programData.aifolite[cur]) return null;
return programData.aifolite[cur];
}
if (!programData[p] || !programData[p][cur]) return null;
return programData[p][cur];
}
function ensureValidCurrency() {
var cfg = getCurrentProgramConfig();
if (cfg) return cfg;
var codes = Object.keys(currencyRates);
for (var i = 0; i < codes.length; i++) {
appState.currency = codes[i];
if (getCurrentProgramConfig()) return getCurrentProgramConfig();
}
return null;
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formatFee(currency, feeDigits) {
var meta = currencyRates[currency] || currencyRates.USD;
var sym = meta.symbol;
var rate = meta.rate != null ? meta.rate : 1;
var n = parseFloat(String(feeDigits).replace(/,/g, ""));
if (isNaN(n)) return sym + feeDigits;
var v = n * rate;
return sym + v.toLocaleString("en-US", { maximumFractionDigits: 0 });
}
function parseFeeNumber(feeDigits) {
var n = parseFloat(String(feeDigits).replace(/,/g, ""));
return isNaN(n) ? NaN : n;
}
var FEE_SALE_DECIMAL_PLACES = 2;
var FEE_AMOUNT_EPS = 1e-9;
function roundToCurrencyDecimals(value, decimalPlaces) {
if (value == null || isNaN(value)) return NaN;
var d =
decimalPlaces == null ? FEE_SALE_DECIMAL_PLACES : Math.max(0, Math.floor(Number(decimalPlaces)));
if (d === 0) return Math.round(Number(value));
var m = Math.pow(10, d);
return Math.round(Number(value) * m) / m;
}
function isEffectivelyIntegerAmount(amount) {
if (amount == null || isNaN(amount)) return false;
var r = Math.round(amount);
return Math.abs(amount - r) < FEE_AMOUNT_EPS * Math.max(1, Math.abs(amount));
}
function stripTrailingZerosFromLocalizedNumber(str) {
if (str.indexOf(".") < 0) return str;
return str.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
}
function formatFeeCurrencyAmount(currency, amount) {
var meta = currencyRates[currency] || currencyRates.USD;
var sym = meta.symbol;
if (amount == null || isNaN(amount)) return sym + "—";
var neg = amount < 0;
var x = neg ? -amount : amount;
var core;
if (isEffectivelyIntegerAmount(x)) {
core = Math.round(x).toLocaleString("en-US", { maximumFractionDigits: 0 });
} else {
core = stripTrailingZerosFromLocalizedNumber(
x.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 14 })
);
}
return (neg ? "-" : "") + sym + core;
}
function normalizeFeeDiscount(mult) {
var d = mult == null || isNaN(Number(mult)) ? 1 : Number(mult);
if (d <= 0) return 0.01;
if (d > 1) return 1;
return d;
}
function feeDisplayAmounts(currency, feeDigits, discountMult) {
var meta = currencyRates[currency] || currencyRates.USD;
var rate = meta.rate != null ? meta.rate : 1;
var d = normalizeFeeDiscount(discountMult);
var n = parseFeeNumber(feeDigits);
if (isNaN(n)) return { listNum: NaN, saleNum: NaN };
var listNum = Math.round(n * rate);
var rawSale = n * rate * d;
var saleNum = roundToCurrencyDecimals(rawSale, FEE_SALE_DECIMAL_PLACES);
return { listNum: listNum, saleNum: saleNum };
}
function formatFeeRoundedAmount(currency, roundedInt) {
var meta = currencyRates[currency] || currencyRates.USD;
var sym = meta.symbol;
if (roundedInt == null || isNaN(roundedInt)) return sym + "—";
return sym + roundedInt.toLocaleString("en-US", { maximumFractionDigits: 0 });
}
function formatAccountSize(sizeKey) {
var n = parseInt(String(sizeKey).replace(/k$/i, ""), 10);
if (isNaN(n)) return sizeKey;
return "$" + n.toLocaleString("en-US") + ",000";
}
/** Mobile size pills: "10k" → "10K" (matches design, not currency-prefixed). */
function formatMobileSizeShort(sizeKey) {
var m = String(sizeKey).match(/^(\d+)k$/i);
if (m) return m[1] + "K";
return String(sizeKey).toUpperCase();
}
function normalizeRow(raw) {
var o = $.extend({}, raw);
o.tradingDays = o.miniday != null ? o.miniday : o.tradingDays != null ? o.tradingDays : "—";
return o;
}
function getActiveParameterNames() {
if (appState.programType === "aifolite") {
return isAifoliteEliteMode() ? parameterNames_instant.slice() : parameterNames_lite.slice();
}
if (appState.programType === "instant") return parameterNames_instant.slice();
return parameterNames.slice();
}
function profitTargetRowClass() {
if (appState.programType === "3step") return " phase-target-row phase-target-row--3";
if (appState.programType === "2step") return " phase-target-row phase-target-row--2";
if (appState.programType === "1step") return " phase-target-row phase-target-row--1";
return "";
}
function getLadderDefinitions() {
return getActiveParameterNames()
.filter(function (name) {
return name !== "Challenge Fee";
})
.map(function (paramName, idx) {
return {
paramName: paramName,
label: paramName,
icon: ICON_BY_PARAM[paramName] || "Component 29.svg",
tooltip: getParameterDescription(paramName),
rowClass: paramName === "Profit Target" ? profitTargetRowClass() : "",
ladderIndex: idx,
ladderKey: String(paramName)
.replace(/\s+/g, "-")
.replace(/[^a-zA-Z0-9-]/g, "")
.toLowerCase(),
};
});
}
function getCellRaw(row, paramName) {
var key = PARAM_TO_FIELD[paramName];
if (!key) return "";
if (key === "tradingDays") return row.tradingDays != null ? String(row.tradingDays) : "—";
var v = row[key];
return v == null ? "" : String(v);
}
function cellAllowsHtml(paramName) {
return (
paramName === "Profit Target" ||
paramName === "Consistency Score" ||
paramName === "withdrawable profit"
);
}
function metricHelpMarkup(label, tooltip, opts) {
opts = opts || {};
var mobileCard = !!opts.mobileCard;
var btnClass = "metric-help-btn" + (mobileCard ? " metric-help-btn--sr-only" : "");
var btnTab = mobileCard ? "-1" : "0";
var btnAriaHidden = mobileCard ? ' aria-hidden="true"' : "";
return (
'<span class="metric-help">' +
'<span class="' +
btnClass +
'" role="button" tabindex="' +
btnTab +
'"' +
btnAriaHidden +
' aria-label="' +
escapeHtml(label + " info") +
'">' +
'<img class="metric-help-btn__icon" src="' +
CHALLENGE_FLOOR_ASSET_BASE +
'challenge_metric_help.svg" alt="" width="16" height="16" decoding="async" />' +
"</span>" +
'<span class="metric-tooltip" role="tooltip">' +
'<span class="metric-tooltip__surface">' +
escapeHtml(tooltip) +
"</span></span></span>"
);
}
var LADDER_HEAD = { label: "Account Size", icon: "Component 33.svg" };
var LADDER_FOOT = {
line1: "One-time",
line2: "Challenge Fee",
icon: "Component 34.svg",
};
function renderMetricsFootItem() {
return (
'<div class="metric-item metric-item--no-help metric-item--ladder-foot metric-item--ladder-foot-split challenge-ladder__foot" data-ladder-anchor="metric-item--ladder-foot">' +
'<span class="metric-label metric-label--ladder-foot">' +
'<span class="ladder-foot-row-primary">' +
'<img src="' +
CHALLENGE_FLOOR_ASSET_BASE +
LADDER_FOOT.icon +
'" class="chall-icon" alt="">' +
'<span class="ladder-foot-line ladder-foot-line--primary">' +
escapeHtml(LADDER_FOOT.line1) +
"</span>" +
"</span>" +
'<span class="ladder-foot-line ladder-foot-line--secondary">' +
escapeHtml(LADDER_FOOT.line2) +
"</span>" +
"</span>" +
"</div>"
);
}
function renderMetricsAnchorItem(label, iconFile, anchorClass, sectionClass) {
return (
'<div class="metric-item metric-item--no-help challenge-ladder__' +
sectionClass +
" " +
anchorClass +
'" data-ladder-anchor="' +
escapeHtml(anchorClass) +
'">' +
'<img src="' +
CHALLENGE_FLOOR_ASSET_BASE +
iconFile +
'" class="chall-icon" alt="">' +
'<span class="metric-label">' +
escapeHtml(label) +
"</span>" +
"</div>"
);
}
function renderMetricsColumn(rows) {
var body = rows
.map(function (m) {
var rowCls = m.rowClass || "";
var dataAttrs =
' data-ladder-row="' +
m.ladderIndex +
'" data-ladder-key="' +
escapeHtml(m.ladderKey) +
'"';
return (
'<div class="metric-item' +
rowCls +
'"' +
dataAttrs +
">" +
'<img src="' +
CHALLENGE_FLOOR_ASSET_BASE +
m.icon +
'" class="chall-icon" alt="">' +
'<span class="metric-label">' +
escapeHtml(m.label) +
"</span>" +
metricHelpMarkup(m.label, m.tooltip) +
"</div>"
);
})
.join("");
return (
renderMetricsAnchorItem(LADDER_HEAD.label, LADDER_HEAD.icon, "metric-item--ladder-head", "head") +
'<div class="metrics-col__body challenge-ladder__body">' +
body +
"</div>" +
'<div class="challenge-ladder__foot-block">' +
renderMetricsFootItem() +
'<div class="challenge-ladder__foot-tail" aria-hidden="true"></div>' +
"</div>"
);
}
function priceRowHtml(acc) {
var highlight = acc.highlight ? " highlight" : "";
var rowModifier = acc.best ? " price-row--best" : "";
var bestIcon = acc.best
? '<img src="' +
CHALLENGE_FLOOR_ASSET_BASE +
'challenge_icon_best_value.svg" alt="" class="price-row__best-icon" width="18" height="18" decoding="async" />'
: "";
var oldPart = acc.oldPrice
? '<span class="old-price">' + escapeHtml(acc.oldPrice) + "</span>"
: "";
return (
'<div class="price-row' +
rowModifier +
'">' +
bestIcon +
'<span class="price' +
highlight +
'">' +
escapeHtml(acc.priceFormatted) +
"</span>" +
oldPart +
"</div>"
);
}
function renderMobileMetricRows(acc, rows) {
return rows
.map(function (m) {
var raw = getCellRaw(acc, m.paramName);
var inner = cellAllowsHtml(m.paramName) ? raw : escapeHtml(raw);
var rowCls = m.rowClass || "";
var dataAttrs =
' data-ladder-row="' +
m.ladderIndex +
'" data-ladder-key="' +
escapeHtml(m.ladderKey) +
'"';
return (
'<div class="card-mobile-metric-row' +
rowCls +
'"' +
dataAttrs +
">" +
'<span class="card-mobile-metric-left">' +
'<img src="' +
CHALLENGE_FLOOR_ASSET_BASE +
m.icon +
'" class="chall-icon" alt="">' +
'<span class="metric-label-dotted" tabindex="0" role="button" aria-label="' +
escapeHtml(m.label + " info") +
'">' +
escapeHtml(m.label) +
"</span>" +
metricHelpMarkup(m.label, m.tooltip, { mobileCard: true }) +
"</span>" +
'<span class="card-mobile-metric-value">' +
inner +
"</span>" +
"</div>"
);
})
.join("");
}
function renderDesktopAccountCard(acc, rows) {
var best = acc.best ? " best" : "";
var tag = acc.best ? '<div class="best-tag">BEST VALUE</div>' : "";
var discount =
acc.discountTag ?
'<img src="' +
CHALLENGE_FLOOR_ASSET_BASE +
'dis_10.png" alt="" class="account-discount-flag">' :
"";
var values = rows
.map(function (m) {
var raw = getCellRaw(acc, m.paramName);
var inner = cellAllowsHtml(m.paramName) ? raw : escapeHtml(raw);
var rowCls = m.rowClass || "";
var dataAttrs =
' data-ladder-row="' +
m.ladderIndex +
'" data-ladder-key="' +
escapeHtml(m.ladderKey) +
'"';
return '<div class="value-item' + rowCls + '"' + dataAttrs + ">" + inner + "</div>";
})
.join("");
var pid = acc.product_id != null ? acc.product_id : "";
var vid = acc.variant_id != null ? acc.variant_id : "";
return (
'<div class="col-md-6 col-xl mb-3">' +
'<article class="account-card challenge-ladder' +
best +
'">' +
tag +
'<div class="account-head challenge-ladder__head"><div class="label">Account</div><div class="size">' +
discount +
escapeHtml(acc.sizeLabel) +
"</div></div>" +
'<div class="values challenge-ladder__body">' +
values +
"</div>" +
'<div class="price-area challenge-ladder__foot">' +
priceRowHtml(acc) +
'<button class="btn-start" type="button" data-product-id="' +
escapeHtml(String(pid)) +
'" data-variant-id="' +
escapeHtml(String(vid)) +
'">Start Challenge</button></div>' +
"</article></div>"
);
}
function renderMobileAccountCard(acc, rows, cardIndex) {
var best = acc.best ? " best" : "";
var tag = acc.best ? '<div class="best-tag">BEST VALUE</div>' : "";
var discount =
acc.discountTag ?
'<img src="' +
CHALLENGE_FLOOR_ASSET_BASE +
'dis_10.png" alt="" class="account-discount-flag">' :
"";
var pid = acc.product_id != null ? acc.product_id : "";
var vid = acc.variant_id != null ? acc.variant_id : "";
var idxAttr =
cardIndex != null && !isNaN(Number(cardIndex))
? ' data-challenge-card-index="' + String(Number(cardIndex)) + '"'
: "";
return (
'<div class="col-md-6 col-xl mb-3"' +
idxAttr +
">" +
'<article class="account-card account-card--mobile' +
best +
'">' +
tag +
'<div class="account-head"><div class="label">Account</div><div class="size">' +
discount +
escapeHtml(acc.sizeLabel) +
"</div></div>" +
'<div class="price-area price-area--mobile">' +
'<button class="btn-start" type="button" data-product-id="' +
escapeHtml(String(pid)) +
'" data-variant-id="' +
escapeHtml(String(vid)) +
'">Start Challenge</button>' +
priceRowHtml(acc) +
"</div>" +
'<div class="card-mobile-metrics">' +
renderMobileMetricRows(acc, rows) +
"</div>" +
"</article></div>"
);
}
function buildAccountsFromConfig(cfg) {
if (!cfg || !cfg.availableSizes) return [];
var best = cfg.best_value_lable || [];
var discount = cfg.discount_lable || [];
return cfg.availableSizes
.map(function (sizeKey) {
var raw = cfg.data[sizeKey];
if (!raw) return null;
var row = normalizeRow(raw);
row.sizeKey = sizeKey;
row.sizeLabel = formatAccountSize(sizeKey);
row.best = best.indexOf(sizeKey) >= 0;
row.discountTag = discount.indexOf(sizeKey) >= 0;
var amounts = feeDisplayAmounts(appState.currency, row.fee, appState.feeDiscount);
if (isNaN(amounts.saleNum)) {
row.priceFormatted = formatFee(appState.currency, row.fee);
row.oldPrice = "";
} else {
row.priceFormatted = formatFeeCurrencyAmount(appState.currency, amounts.saleNum);
var listVal = amounts.listNum;
var saleVal = amounts.saleNum;
var showOld =
!isNaN(listVal) &&
!isNaN(saleVal) &&
Math.abs(listVal - saleVal) > FEE_AMOUNT_EPS * Math.max(1, Math.abs(listVal));
row.oldPrice = showOld ? formatFeeRoundedAmount(appState.currency, listVal) : "";
}
row.highlight = row.best;
row.product_id = raw.product_id;
row.variant_id = raw.variant_id;
return row;
})
.filter(Boolean);
}
function buildAifolite24hNoteHtml() {
return (
'<div class="row no-gutters challenge-floor-24h-note-wrap">' +
'<div class="col-12">' +
'<div class="challenge-floor-24h-note" role="note">' +
'<p class="challenge-floor-24h-note__title">Note:</p>' +
"<p class=\"challenge-floor-24h-note__p\">" +
"The challenge begins immediately after purchase. To ensure you have the maximum time to complete the challenge. Please take consider market closing time and holidays." +
"</p>" +
"<p class=\"challenge-floor-24h-note__p\">" +
"The instrument used in the first executed trade will be considered the only permitted trading instrument for the account. Any subsequent trading of other instruments will be deemed a violation." +
"</p>" +
"<p class=\"challenge-floor-24h-note__p\">" +
"Please refer to the FAQs for more information." +
"</p>" +
"</div></div></div>"
);
}
function buildFloorHtml() {
var cfg = ensureValidCurrency();
if (!cfg) {
return '<div class="text-center py-4 text-muted">No program data for this selection.</div>';
}
var rows = getLadderDefinitions();
var accounts = buildAccountsFromConfig(cfg);
if (!accounts.length) {
return '<div class="text-center py-4 text-muted">No tiers available.</div>';
}
var metricsCol = renderMetricsColumn(rows);
var desktopCards = accounts.map(function (a) {
return renderDesktopAccountCard(a, rows);
}).join("");
var mobileCards = accounts
.map(function (a, i) {
return renderMobileAccountCard(a, rows, i);
})
.join("");
var mobileSizeTabs =
'<div class="challenge-mobile-size-tabs d-lg-none js-challenge-mobile-size-tabs" role="tablist" aria-label="Account size">' +
accounts
.map(function (a, i) {
var activeCls = i === 0 ? " active" : "";
var dot = a.best ? '<span class="challenge-mobile-size-tab__best-dot" aria-hidden="true"></span>' : "";
return (
'<button type="button" class="challenge-mobile-size-tab' +
activeCls +
'" role="tab" aria-selected="' +
(i === 0 ? "true" : "false") +
'" data-size-index="' +
i +
'" data-size-key="' +
escapeHtml(a.sizeKey) +
'">' +
dot +
'<span class="challenge-mobile-size-tab__label">' +
escapeHtml(formatMobileSizeShort(a.sizeKey)) +
"</span></button>"
);
})
.join("") +
"</div>";
var rowLayoutExtras = "";
if (accounts.length === 1) {
rowLayoutExtras = " challenge-floor-row--single-card";
} else if (accounts.length === 5) {
rowLayoutExtras = " challenge-floor-row--five-cards-pc";
} else if (accounts.length === 4) {
rowLayoutExtras = " challenge-floor-row--four-cards-pc";
} else if (
(appState.programType === "instant" && appState.instantMode === "aifo") ||
isAifoliteEliteMode()
) {
rowLayoutExtras = " challenge-floor-row--instant-aifo-two-pc";
} else if (appState.programType === "aifolite") {
rowLayoutExtras = " challenge-floor-row--aifolite-24h-pc";
}
var mainRow =
'<div class="row no-gutters' +
rowLayoutExtras +
'">' +
'<div class="col-lg-2 d-none d-lg-block pr-3 metrics-col challenge-ladder">' +
metricsCol +
"</div>" +
'<div class="col-lg-10">' +
'<div class="challenge-cards-scroll js-challenge-cards-scroll d-none d-lg-block">' +
'<div class="row card-wrap challenge-cards-inner">' +
desktopCards +
"</div></div>" +
mobileSizeTabs +
'<div class="challenge-cards-scroll js-challenge-cards-scroll d-lg-none">' +
'<div class="row card-wrap challenge-cards-inner">' +
mobileCards +
"</div></div>" +
"</div></div>";
var note =
appState.programType === "aifolite" && !isAifoliteEliteMode() ? buildAifolite24hNoteHtml() : "";
return mainRow + note;
}
function bindChallengeCardsScroll($ctx) {
$ctx.find(".js-challenge-cards-scroll").each(function () {
var $el = $(this);
$el.off("scroll.challengeCards").on("scroll.challengeCards", function () {
$el.addClass("is-scrolling");
window.clearTimeout($el.data("challengeScrollTimer"));
$el.data(
"challengeScrollTimer",
window.setTimeout(function () {
$el.removeClass("is-scrolling");
}, 900)
);
});
});
}
/** 992px1200px:与 CSS 卡片固定宽区间一致 */
function isMidDesktopCardsViewport() {
return window.matchMedia("(min-width: 992px) and (max-width: 1199.98px)").matches;
}
var challengeCardsDragDocBound = false;
var challengeCardsDragActive = null;
function unbindChallengeCardsDragScroll($ctx) {
$ctx.find(".js-challenge-cards-scroll").each(function () {
$(this)
.off(".challengeCardsDrag")
.removeClass("challenge-cards-scroll--drag-enabled is-drag-scrolling")
.css("cursor", "");
});
challengeCardsDragActive = null;
if (challengeCardsDragDocBound) {
$(document).off(".challengeCardsDrag");
challengeCardsDragDocBound = false;
}
}
function bindChallengeCardsDragScroll($ctx) {
unbindChallengeCardsDragScroll($ctx);
if (!isMidDesktopCardsViewport()) return;
function onDocMouseMove(e) {
if (!challengeCardsDragActive) return;
var drag = challengeCardsDragActive;
var dx = e.pageX - drag.startX;
if (Math.abs(dx) > 4) drag.moved = true;
drag.el.scrollLeft = drag.startScroll - dx;
}
function onDocMouseUp() {
if (!challengeCardsDragActive) return;
var drag = challengeCardsDragActive;
drag.$el.removeClass("is-drag-scrolling");
if (drag.moved) {
drag.$el.data("challengeCardsDragMoved", true);
window.setTimeout(function () {
drag.$el.removeData("challengeCardsDragMoved");
}, 0);
}
challengeCardsDragActive = null;
}
if (!challengeCardsDragDocBound) {
$(document).on("mousemove.challengeCardsDrag", onDocMouseMove);
$(document).on("mouseup.challengeCardsDrag", onDocMouseUp);
challengeCardsDragDocBound = true;
}
$ctx.find(".js-challenge-cards-scroll").each(function () {
var el = this;
var $el = $(el);
$el.addClass("challenge-cards-scroll--drag-enabled");
$el.on("mousedown.challengeCardsDrag", function (e) {
if (e.button !== 0) return;
if (
$(e.target).closest(
"button, a, input, select, textarea, label, .metric-help-btn, .metric-label-dotted"
).length
) {
return;
}
challengeCardsDragActive = {
el: el,
$el: $el,
startX: e.pageX,
startScroll: el.scrollLeft,
moved: false,
};
$el.addClass("is-drag-scrolling");
e.preventDefault();
});
$el.on("click.challengeCardsDrag", "button, a", function (e) {
if ($el.data("challengeCardsDragMoved")) {
e.preventDefault();
e.stopImmediatePropagation();
}
});
});
}
function syncMobileSizeTabFromScroll($mount) {
var $scroll = $mount.find(".js-challenge-cards-scroll.d-lg-none");
var $tabs = $mount.find(".js-challenge-mobile-size-tabs");
if (!$scroll.length || !$tabs.length) return;
var $cards = $scroll.find(".challenge-cards-inner > [class*='col-']");
if (!$cards.length) return;
var scrollEl = $scroll[0];
var scRect = scrollEl.getBoundingClientRect();
var mid = scRect.left + scRect.width / 2;
var bestI = 0;
var bestD = Infinity;
$cards.each(function (idx) {
var r = this.getBoundingClientRect();
var c = r.left + r.width / 2;
var d = Math.abs(c - mid);
if (d < bestD) {
bestD = d;
bestI = idx;
}
});
$tabs.find(".challenge-mobile-size-tab").each(function (idx) {
var on = idx === bestI;
$(this).toggleClass("active", on);
$(this).attr("aria-selected", on ? "true" : "false");
});
}
function bindMobileSizeTabSync($mount) {
var $scroll = $mount.find(".js-challenge-cards-scroll.d-lg-none");
var $tabs = $mount.find(".js-challenge-mobile-size-tabs");
if (!$scroll.length || !$tabs.length) return;
function sync() {
syncMobileSizeTabFromScroll($mount);
}
$scroll.off("scroll.mobileSizeTabs").on("scroll.mobileSizeTabs", function () {
window.requestAnimationFrame(sync);
});
$tabs
.off("click.mobileSizeTabs")
.on("click.mobileSizeTabs", ".challenge-mobile-size-tab", function () {
var idx = parseInt(String($(this).attr("data-size-index")), 10);
if (isNaN(idx)) return;
var card = $scroll.find(".challenge-cards-inner > [class*='col-']").get(idx);
if (card) {
card.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
}
});
$(window).off("resize.mobileSizeTabsChallenge").on("resize.mobileSizeTabsChallenge", sync);
}
/** 防止阶梯行被负 margin / 过大 padding 顶出左列(9921200 + overflow:hidden 时会整列看似空白) */
function clampMetricsColLadderInView($metricsCol) {
if (!$metricsCol.length || !$metricsCol[0]) return;
var colTop = $metricsCol[0].getBoundingClientRect().top;
var slack = 2;
var $body = $metricsCol.find(".metrics-col__body");
if ($body.length) {
var pt = parseFloat($body.css("padding-top")) || 0;
if (pt > 240) {
$body.css("padding-top", "");
}
}
$metricsCol.find(".metric-item").each(function () {
var rect = this.getBoundingClientRect();
if (rect.height < 1) return;
if (rect.top < colTop - slack) {
var $el = $(this);
var mt = parseFloat($el.css("margin-top")) || 0;
$el.css("margin-top", mt + Math.ceil(colTop - rect.top) + "px");
}
});
}
/** PC:按设计稿对齐——Account Size↔金额、参数行↔value、Fee↔price-row、左右列底↔卡片底;Profit Target 行高逻辑不变。 */
function syncChallengeDesktopLadderHeights() {
if (isCompactChallengeFloorViewport()) return;
var $mount = $("#challenge-floor-mount");
var $row = $mount.children(".row.no-gutters").first();
if (!$row.length) return;
var $metricsCol = $row.find(".metrics-col");
var $head = $metricsCol.find(".metric-item--ladder-head");
var $foot = $metricsCol.find(".metric-item--ladder-foot");
var $footTail = $metricsCol.find(".challenge-ladder__foot-tail");
var $metrics = $metricsCol.find(".metrics-col__body .metric-item");
var $scroll = $row.find(".js-challenge-cards-scroll.d-none.d-lg-block");
if (!$metricsCol.length || !$scroll.length) return;
if (!$scroll.is(":visible")) return;
var $cards = $scroll.find("article.account-card");
var $card = $cards.first();
if (!$card.length) return;
var $accountHead = $card.find("> .account-head");
var $size = $accountHead.find(".size").first();
var $priceRow = $card.find("> .price-area > .price-row").first();
var $valueCols = $scroll.find("article.account-card > .values");
if (!$valueCols.length) return;
$metricsCol.css({ marginTop: "", paddingBottom: "" });
$footTail.css({ height: "", minHeight: "" });
$metricsCol.find(".metrics-col__body").css("padding-top", "");
$metrics.css("margin-top", "");
$head.add($foot).css({
minHeight: "",
height: "",
marginTop: "",
paddingTop: "",
paddingBottom: "",
});
if ($head.length && $accountHead.length && $size.length) {
var headBoxH = Math.ceil($accountHead.outerHeight());
if (headBoxH < 1) headBoxH = 1;
var sizeH = Math.ceil($size.outerHeight());
if (sizeH < 1) sizeH = 1;
$head.css({
boxSizing: "border-box",
display: "flex",
alignItems: "center",
minHeight: headBoxH,
height: headBoxH,
paddingTop: "0",
paddingBottom: "0",
marginTop: "0",
});
if ($head[0]) void $head[0].offsetHeight;
var headTopDelta = Math.round(
$accountHead[0].getBoundingClientRect().top - $head[0].getBoundingClientRect().top
);
if (headTopDelta !== 0) {
$head.css("margin-top", headTopDelta + "px");
}
if ($head[0]) void $head[0].offsetHeight;
var ladderHeadTop = $head[0].getBoundingClientRect().top;
var sizeTop = $size[0].getBoundingClientRect().top;
var padTop = Math.max(0, Math.round(sizeTop - ladderHeadTop));
var padBottom = Math.max(0, headBoxH - padTop - sizeH);
$head.css({
paddingTop: padTop + "px",
paddingBottom: padBottom + "px",
});
function nudgeHeadPaddingToSize() {
if (!$head[0] || !$size[0]) return;
var sizeRect = $size[0].getBoundingClientRect();
var sizeMidY = (sizeRect.top + sizeRect.bottom) / 2;
var $label = $head.find(".metric-label").first();
var alignEl = $label.length ? $label[0] : $head[0];
var alignRect = alignEl.getBoundingClientRect();
var alignMidY = (alignRect.top + alignRect.bottom) / 2;
var centerDelta = Math.round(sizeMidY - alignMidY);
if (centerDelta === 0) return;
var headPadT = parseFloat($head.css("padding-top")) || 0;
var headPadB = parseFloat($head.css("padding-bottom")) || 0;
if (centerDelta > 0 && headPadB >= centerDelta) {
$head.css({
paddingTop: headPadT + centerDelta + "px",
paddingBottom: headPadB - centerDelta + "px",
});
} else if (centerDelta < 0 && headPadT >= -centerDelta) {
$head.css({
paddingTop: headPadT + centerDelta + "px",
paddingBottom: headPadB - centerDelta + "px",
});
}
}
if ($head[0]) void $head[0].offsetHeight;
nudgeHeadPaddingToSize();
if ($head[0]) void $head[0].offsetHeight;
var headBottomFix = Math.round(
$accountHead[0].getBoundingClientRect().bottom - $head[0].getBoundingClientRect().bottom
);
if (headBottomFix !== 0) {
var headPadB = parseFloat($head.css("padding-bottom")) || 0;
$head.css("padding-bottom", Math.max(0, headPadB + headBottomFix) + "px");
if ($head[0]) void $head[0].offsetHeight;
nudgeHeadPaddingToSize();
}
}
var $metricsBody = $metricsCol.find(".metrics-col__body");
var $firstValCell = $valueCols.first().children(".value-item").first();
if ($metricsBody.length && $firstValCell.length && $head.length && $accountHead.length) {
var cardBodyGap = Math.round(
$firstValCell[0].getBoundingClientRect().top -
$accountHead[0].getBoundingClientRect().bottom
);
if (cardBodyGap < 0) cardBodyGap = 0;
$metricsBody.css("padding-top", cardBodyGap + "px");
if ($metrics.length && $head[0]) {
if ($metrics[0]) void $metrics[0].offsetHeight;
var leftBodyGap = Math.round(
$metrics.first()[0].getBoundingClientRect().top - $head[0].getBoundingClientRect().bottom
);
if (leftBodyGap !== cardBodyGap) {
$metricsBody.css("padding-top", cardBodyGap + (cardBodyGap - leftBodyGap) + "px");
}
}
}
var nValues = $valueCols.first().children(".value-item").length;
var rowCount = Math.min($metrics.length, nValues);
if (rowCount >= 1) {
$metrics.css({ minHeight: "", height: "" });
$valueCols.children(".value-item").css({ minHeight: "", height: "" });
var r;
for (r = 0; r < rowCount; r++) {
var $mRow = $metrics.eq(r);
var isPhaseTarget = $mRow.hasClass("phase-target-row");
var maxH = Math.ceil($mRow.outerHeight());
$valueCols.each(function () {
var $cell = $(this).children(".value-item").eq(r);
if ($cell.length) {
var h = Math.ceil($cell.outerHeight());
if (h > maxH) maxH = h;
}
});
if (maxH < 1) maxH = 1;
if (isPhaseTarget) {
$mRow.css({ minHeight: "", height: "" });
$valueCols.each(function () {
$(this).children(".value-item").eq(r).css({ minHeight: "", height: "" });
});
if ($mRow[0]) void $mRow[0].offsetHeight;
maxH = Math.ceil($mRow.outerHeight());
$valueCols.each(function () {
var $cell = $(this).children(".value-item").eq(r);
if ($cell.length) {
var phaseH = Math.ceil($cell.outerHeight());
if (phaseH > maxH) maxH = phaseH;
}
});
if (maxH < 1) maxH = 1;
$mRow.css({ minHeight: maxH, height: "" });
$valueCols.each(function () {
$(this).children(".value-item").eq(r).css({ minHeight: maxH, height: "" });
});
} else {
$mRow.css({ minHeight: maxH, height: maxH });
$valueCols.each(function () {
$(this).children(".value-item").eq(r).css({ minHeight: maxH, height: maxH });
});
}
}
/* 逐行:左侧 label 顶边与首张卡 value 顶边严格对齐 */
for (r = 0; r < rowCount; r++) {
var $mRowAlign = $metrics.eq(r);
var $vRef = $valueCols.first().children(".value-item").eq(r);
if (!$mRowAlign.length || !$vRef.length) continue;
if ($mRowAlign[0]) void $mRowAlign[0].offsetHeight;
var rowDelta = Math.round(
$vRef[0].getBoundingClientRect().top - $mRowAlign[0].getBoundingClientRect().top
);
if (rowDelta !== 0) {
var rowMt = parseFloat($mRowAlign.css("margin-top")) || 0;
$mRowAlign.css("margin-top", rowMt + rowDelta + "px");
}
}
}
if ($foot.length && $priceRow.length) {
$foot.css({ minHeight: "", height: "auto", marginTop: "" });
var $oneTimeLine = $foot.find(".ladder-foot-line--primary").first();
var $priceEl = $priceRow.find(".price").first();
if (!$priceEl.length) {
$priceEl = $priceRow;
}
if ($oneTimeLine.length && $priceEl.length) {
if ($foot[0]) void $foot[0].offsetHeight;
var oneTimeRect = $oneTimeLine[0].getBoundingClientRect();
var priceRect = $priceEl[0].getBoundingClientRect();
var oneTimeMidY = (oneTimeRect.top + oneTimeRect.bottom) / 2;
var priceMidY = (priceRect.top + priceRect.bottom) / 2;
var push = Math.round(priceMidY - oneTimeMidY);
if (push !== 0) {
$foot.css("margin-top", push + "px");
}
}
}
/* 9921200pxfoot-tail 补齐至卡片底,消除左下角空白 */
if ($footTail.length) {
if (isMidDesktopCardsViewport() && $foot.length) {
var cardBottomMid = 0;
$cards.each(function () {
var b = this.getBoundingClientRect().bottom;
if (b > cardBottomMid) cardBottomMid = b;
});
if (cardBottomMid > 0) {
if ($foot[0]) void $foot[0].offsetHeight;
var footBottomMid = $foot[0].getBoundingClientRect().bottom;
var tailH = Math.round(cardBottomMid - footBottomMid);
if (tailH < 0) tailH = 0;
$footTail.css({
display: "block",
minHeight: tailH,
height: tailH > 0 ? tailH : "",
});
}
} else {
$footTail.css({ minHeight: "", height: "" });
}
}
clampMetricsColLadderInView($metricsCol);
syncMetricsColGradientPanel($metricsCol, $cards);
}
/** PC:左侧渐变背景高度 ≈ 卡片高度,顶部略高出卡片顶(不撑满整列) */
function syncMetricsColGradientPanel($metricsCol, $cards) {
if (!$metricsCol.length || !$cards.length) return;
$metricsCol.css({ "--metrics-col-bg-h": "", "--metrics-col-bg-top": "" });
var tallestH = 0;
var cardTop = Infinity;
var cardBottom = 0;
$cards.each(function () {
var rect = this.getBoundingClientRect();
if (rect.height > tallestH) tallestH = Math.ceil(rect.height);
if (rect.top < cardTop) cardTop = rect.top;
if (rect.bottom > cardBottom) cardBottom = rect.bottom;
});
if (tallestH < 1 || !isFinite(cardTop)) return;
if ($metricsCol[0]) void $metricsCol[0].offsetHeight;
var metricsTop = $metricsCol[0].getBoundingClientRect().top;
var overhang = 4;
var $floor = $metricsCol.closest(".challenge-floor");
if ($floor.length) {
var v = getComputedStyle($floor[0]).getPropertyValue("--challenge-metrics-bg-overhang-top");
if (v) overhang = parseFloat(v.trim()) || 4;
}
var bgTop = Math.round(cardTop - metricsTop - overhang);
/* 992~1200px:渐变底边与拉伸后的左列底(≈ 卡片底)对齐 */
if (isMidDesktopCardsViewport()) {
var metricsH = Math.ceil($metricsCol[0].getBoundingClientRect().height);
var bgH = metricsH - bgTop;
if (cardBottom > 0) {
var cardAlignedH = Math.round(cardBottom - metricsTop - bgTop);
if (cardAlignedH > bgH) bgH = cardAlignedH;
}
if (bgH < tallestH + overhang) bgH = tallestH + overhang;
$metricsCol.css({
"--metrics-col-bg-h": bgH + "px",
"--metrics-col-bg-top": bgTop + "px",
});
return;
}
$metricsCol.css({
"--metrics-col-bg-h": tallestH + overhang + "px",
"--metrics-col-bg-top": bgTop + "px",
});
}
var challengeLadderResizeTimer = null;
function scheduleChallengeDesktopLadderSync() {
window.clearTimeout(challengeLadderResizeTimer);
challengeLadderResizeTimer = window.setTimeout(function () {
syncChallengeDesktopLadderHeights();
}, 120);
}
$(window).on("resize.challengeFloorLadder", scheduleChallengeDesktopLadderSync);
function renderCurrencyBar() {
var html = Object.keys(currencyRates)
.map(function (code) {
var active = code === appState.currency ? " active" : "";
return (
'<button type="button" class="challenge-currency-btn' +
active +
'" data-currency="' +
code +
'">' +
code +
"</button>"
);
})
.join("");
$("#challengeCurrencyTabs").html(html);
}
function updateInstantSubtabsVisibility() {
var show = appState.programType === "instant";
$("#challengeInstantSubtabs").toggleClass("d-none", !show);
if (show) {
$("#challengeInstantSubtabs .challenge-instant-subtab").removeClass("active");
$(
'#challengeInstantSubtabs .challenge-instant-subtab[data-instant-mode="' +
appState.instantMode +
'"]'
).addClass("active");
}
}
function updateAifoliteSubtabsVisibility() {
var show = appState.programType === "aifolite";
$("#challengeAifoliteSubtabs").toggleClass("d-none", !show);
if (show) {
appState.aifoliteMode = normalizeAifoliteMode(appState.aifoliteMode);
$("#challengeAifoliteSubtabs .challenge-instant-subtab").removeClass("active");
$(
'#challengeAifoliteSubtabs .challenge-instant-subtab[data-aifolite-mode="' +
appState.aifoliteMode +
'"]'
).addClass("active");
}
}
function updateAddonsVisibility() {
var hide = appState.programType === "instant" || appState.programType === "aifolite";
$(".challenge-floor-addons").toggleClass("d-none", hide);
}
function renderChallengeFloor() {
ensureValidCurrency();
closeAllMetricTooltips();
var $mount = $("#challenge-floor-mount");
$mount.html(buildFloorHtml());
bindChallengeCardsScroll($mount);
bindChallengeCardsDragScroll($mount);
bindMobileSizeTabSync($mount);
window.requestAnimationFrame(function () {
syncChallengeDesktopLadderHeights();
window.requestAnimationFrame(function () {
syncChallengeDesktopLadderHeights();
syncMobileSizeTabFromScroll($mount);
if (isMidDesktopCardsViewport()) {
window.setTimeout(syncChallengeDesktopLadderHeights, 280);
}
});
});
}
function tabIdToProgramType(tabId) {
if (tabId === "24h") return "aifolite";
return tabId;
}
function programTypeToTabId(p) {
if (p === "aifolite") return "24h";
return p;
}
function syncTabsFromAppState() {
var tab = programTypeToTabId(appState.programType);
$(".challenge-floor .tab-btn[data-tab]").removeClass("active");
$('.challenge-floor .tab-btn[data-tab="' + tab + '"]').addClass("active");
updateInstantSubtabsVisibility();
updateAifoliteSubtabsVisibility();
updateAddonsVisibility();
}
$(document).on("click", ".challenge-floor .tab-btn[data-tab]", function () {
var tab = $(this).data("tab");
appState.programType = tabIdToProgramType(tab);
if (appState.programType === "instant") {
appState.instantMode = normalizeInstantMode(appState.instantMode);
}
if (appState.programType === "aifolite") {
appState.aifoliteMode = normalizeAifoliteMode(appState.aifoliteMode);
}
syncTabsFromAppState();
renderCurrencyBar();
renderChallengeFloor();
});
$(document).on("click", "#challengeInstantSubtabs .challenge-instant-subtab", function () {
appState.instantMode = normalizeInstantMode($(this).data("instant-mode"));
syncTabsFromAppState();
renderCurrencyBar();
renderChallengeFloor();
});
$(document).on("click", "#challengeAifoliteSubtabs .challenge-instant-subtab", function () {
appState.aifoliteMode = normalizeAifoliteMode($(this).data("aifolite-mode"));
syncTabsFromAppState();
renderCurrencyBar();
renderChallengeFloor();
});
$(document).on("click", ".challenge-currency-btn", function () {
appState.currency = $(this).data("currency");
renderCurrencyBar();
renderChallengeFloor();
});
/** Viewport width at/above this uses CSS absolute tooltip (desktop ladder/cards). */
var METRIC_TOOLTIP_PC_MIN_PX = 992;
var metricTooltipSuppressDocClose = false;
/** 手机窄屏(<992):使用卡片内指标 + 横向滑动楼层;992+ 含竖屏平板走 PC 楼层 */
function isCompactChallengeFloorViewport() {
return window.matchMedia("(max-width: 991.98px)").matches;
}
function isMobileMetricTooltipViewport() {
return isCompactChallengeFloorViewport();
}
function isMobileCardMetricHelp($wrap) {
return $wrap && $wrap.length && $wrap.closest(".card-mobile-metrics").length > 0;
}
function getMetricTooltipForWrap($wrap) {
var $portaled = $wrap.data("metricTooltipPortaled");
if ($portaled && $portaled.length && $.contains(document.documentElement, $portaled[0])) {
return $portaled;
}
return $wrap.children(".metric-tooltip").first();
}
function restoreMetricTooltipDom($wrap) {
if (!$wrap || !$wrap.length) return;
var $tip = getMetricTooltipForWrap($wrap);
$wrap.removeClass("metric-help--tooltip-fixed");
$wrap.removeData("metricTooltipPortaled");
if (!$tip.length) return;
$tip.removeClass("metric-tooltip--mobile-portal metric-tooltip--positioned metric-tooltip--flip-below");
$tip[0].style.removeProperty("--metric-tooltip-callout-x");
$tip.css({
position: "",
left: "",
top: "",
right: "",
bottom: "",
width: "",
maxWidth: "",
transform: "",
margin: "",
});
if ($tip.parent()[0] === document.body) {
$wrap.append($tip);
}
}
function closeAllMetricTooltips() {
$(".challenge-floor .metric-help.show").each(function () {
var $w = $(this);
$w.removeClass("show");
restoreMetricTooltipDom($w);
});
$("body > .metric-tooltip.metric-tooltip--mobile-portal").remove();
}
function portalMetricTooltipToBody($wrap) {
var $tip = $wrap.children(".metric-tooltip").first();
if (!$tip.length) return $();
if ($tip.parent()[0] !== document.body) {
$wrap.data("metricTooltipPortaled", $tip);
document.body.appendChild($tip[0]);
}
$tip.addClass("metric-tooltip--mobile-portal");
$tip.removeClass("metric-tooltip--positioned");
return $tip;
}
function clearMobileMetricTooltipPosition($wrap) {
restoreMetricTooltipDom($wrap);
}
function metricHelpToggleFromWrap($wrap) {
var isMobileCard = isMobileCardMetricHelp($wrap);
var isMobileViewport = isMobileMetricTooltipViewport();
var willOpen = !$wrap.hasClass("show");
$(".challenge-floor .metric-help")
.not($wrap)
.each(function () {
var $other = $(this);
$other.removeClass("show");
restoreMetricTooltipDom($other);
});
if (willOpen) {
if (isMobileCard && isMobileViewport) {
metricTooltipSuppressDocClose = true;
window.setTimeout(function () {
metricTooltipSuppressDocClose = false;
}, 0);
$wrap.addClass("metric-help--tooltip-fixed show");
portalMetricTooltipToBody($wrap);
positionMobileMetricTooltip($wrap);
schedulePositionMobileMetricTooltip($wrap);
} else {
$wrap.addClass("show");
}
} else {
$wrap.removeClass("show");
if (isMobileCard) {
restoreMetricTooltipDom($wrap);
}
}
}
function positionMobileMetricTooltip($wrap) {
if (!$wrap || !$wrap.length || !$wrap.hasClass("show")) return;
if (!isMobileCardMetricHelp($wrap)) return;
if (!isMobileMetricTooltipViewport()) {
restoreMetricTooltipDom($wrap);
return;
}
var $tip = getMetricTooltipForWrap($wrap);
if (!$tip.length) {
$tip = portalMetricTooltipToBody($wrap);
}
if (!$tip.length) return;
var $left = $wrap.closest(".card-mobile-metric-left");
var $anchor = $left.length ? $left.find(".metric-label-dotted").first() : $wrap.find(".metric-help-btn");
if (!$anchor.length) return;
var tipEl = $tip[0];
var $card = $wrap.closest(".account-card");
var cardEl = $card.length ? $card[0] : null;
$wrap.addClass("metric-help--tooltip-fixed");
tipEl.classList.remove("metric-tooltip--flip-below", "metric-tooltip--positioned");
var edgePad = 12;
var vw = window.innerWidth;
var vh = window.innerHeight;
var margin = 12;
var gapToAnchor = 8;
var arrowH = 7;
var cardRect = cardEl ? cardEl.getBoundingClientRect() : null;
var maxW = cardRect
? Math.max(120, cardRect.width - edgePad * 2)
: Math.min(320, vw - margin * 2);
$tip.css({
position: "fixed",
transform: "none",
margin: 0,
right: "auto",
bottom: "auto",
width: maxW + "px",
maxWidth: maxW + "px",
});
var anchor = $anchor[0].getBoundingClientRect();
var tipRect = tipEl.getBoundingClientRect();
var tipW = tipRect.width;
var tipH = tipRect.height;
var minLeft = cardRect ? cardRect.left + edgePad : margin;
var maxLeft = cardRect ? cardRect.right - edgePad - tipW : vw - margin - tipW;
var anchorCx = anchor.left + anchor.width / 2;
var left = anchorCx - tipW / 2;
if (maxLeft < minLeft) {
left = minLeft;
} else {
left = Math.max(minLeft, Math.min(left, maxLeft));
}
var tail = gapToAnchor + arrowH;
var top = anchor.top - tipH - tail;
var flipBelow = false;
if (top < margin) {
top = anchor.bottom + tail;
flipBelow = true;
}
if (top + tipH > vh - margin) {
top = Math.max(margin, vh - margin - tipH);
}
if (flipBelow) {
tipEl.classList.add("metric-tooltip--flip-below");
}
$tip.css({ left: left + "px", top: top + "px" });
tipRect = tipEl.getBoundingClientRect();
tipW = tipRect.width;
var arrowCenterX = anchorCx - tipRect.left;
var minCx = 18;
var maxCx = tipW - minCx;
arrowCenterX = Math.max(minCx, Math.min(maxCx, arrowCenterX));
tipEl.style.setProperty("--metric-tooltip-callout-x", arrowCenterX + "px");
$tip.addClass("metric-tooltip--positioned");
}
function schedulePositionMobileMetricTooltip($wrap) {
if (!$wrap || !$wrap.length || !$wrap.hasClass("show")) return;
requestAnimationFrame(function () {
requestAnimationFrame(function () {
positionMobileMetricTooltip($wrap);
});
});
}
var metricTooltipRepositionTimer = null;
function repositionOpenMetricTooltips() {
clearTimeout(metricTooltipRepositionTimer);
metricTooltipRepositionTimer = setTimeout(function () {
$(".challenge-floor .metric-help.show").each(function () {
positionMobileMetricTooltip($(this));
});
}, 40);
}
$(document).on("click", ".challenge-floor .metric-help-btn", function (e) {
e.preventDefault();
e.stopPropagation();
if ($(this).attr("aria-hidden") === "true") return;
metricHelpToggleFromWrap($(this).closest(".metric-help"));
});
$(document).on("keydown", ".challenge-floor .metric-help-btn", function (e) {
if (e.key !== "Enter" && e.key !== " ") return;
e.preventDefault();
if ($(this).attr("aria-hidden") === "true") return;
metricHelpToggleFromWrap($(this).closest(".metric-help"));
});
function metricHelpWrapFromMobileMetricLeft($left) {
var $label = $left.find(".metric-label-dotted").first();
if (!$label.length) return $();
return $label.siblings(".metric-help");
}
$(document).on("click", ".challenge-floor .card-mobile-metrics .metric-label-dotted", function (e) {
e.preventDefault();
e.stopPropagation();
var $wrap = $(this).siblings(".metric-help");
if (!$wrap.length) return;
metricHelpToggleFromWrap($wrap);
});
$(document).on("click", ".challenge-floor .card-mobile-metrics .card-mobile-metric-left", function (e) {
if ($(e.target).closest(".metric-label-dotted").length) return;
var $wrap = metricHelpWrapFromMobileMetricLeft($(this));
if (!$wrap.length) return;
e.preventDefault();
e.stopPropagation();
metricHelpToggleFromWrap($wrap);
});
$(document).on("keydown", ".challenge-floor .card-mobile-metrics .metric-label-dotted", function (e) {
if (e.key !== "Enter" && e.key !== " ") return;
e.preventDefault();
var $wrap = $(this).siblings(".metric-help");
if (!$wrap.length) return;
metricHelpToggleFromWrap($wrap);
});
$(document).on("click", function (e) {
if (metricTooltipSuppressDocClose) return;
var $t = $(e.target);
if ($t.closest(".metric-tooltip--mobile-portal").length) return;
if ($t.closest(".challenge-floor .metric-help").length) return;
if ($t.closest(".challenge-floor .card-mobile-metrics .card-mobile-metric-left").length) return;
$(".challenge-floor .metric-help.show").each(function () {
var $w = $(this);
$w.removeClass("show");
restoreMetricTooltipDom($w);
});
});
var CHALLENGE_CHECKOUT_COUPON = "AIFO50";
var CHALLENGE_CHECKOUT_BASE = "https://dashboard.aifo.com/checkout";
function buildChallengeCheckoutUrl(productId, variantId) {
return (
CHALLENGE_CHECKOUT_BASE +
"?product_id=" +
encodeURIComponent(productId) +
"&variant_id=" +
encodeURIComponent(variantId)
);
}
function copyChallengeCouponCode(code) {
var text = String(code || "");
if (!text) return Promise.resolve();
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
return navigator.clipboard.writeText(text);
}
return new Promise(function (resolve, reject) {
var $ta = $("<textarea>").val(text).css({
position: "fixed",
left: "-9999px",
top: "0",
opacity: "0",
});
$("body").append($ta);
$ta[0].focus();
$ta[0].select();
try {
if (document.execCommand("copy")) resolve();
else reject(new Error("copy failed"));
} catch (err) {
reject(err);
} finally {
$ta.remove();
}
});
}
function openChallengeCouponModal(checkoutUrl) {
var $modal = $("#challengeCouponModal");
if (!$modal.length) {
if (checkoutUrl) window.open(checkoutUrl, "_blank");
return;
}
$("#challengeCouponModalCode").text(CHALLENGE_CHECKOUT_COUPON);
$modal.data("checkout-url", checkoutUrl || "");
$modal.removeAttr("hidden").attr("aria-hidden", "false").addClass("is-open");
window.setTimeout(function () {
$("#challengeCouponModalOk").trigger("focus");
}, 0);
}
function closeChallengeCouponModal() {
var $modal = $("#challengeCouponModal");
$modal.removeClass("is-open").attr("aria-hidden", "true").attr("hidden", "hidden");
$modal.removeData("checkout-url");
}
function proceedChallengeCheckoutFromModal() {
var $modal = $("#challengeCouponModal");
var url = $modal.data("checkout-url");
closeChallengeCouponModal();
if (url) window.open(url, "_blank");
}
$(document).on("click", ".challenge-floor .btn-start", function () {
var pid = $(this).data("product-id");
var vid = $(this).data("variant-id");
if (!pid || !vid) return;
var checkoutUrl = buildChallengeCheckoutUrl(pid, vid);
copyChallengeCouponCode(CHALLENGE_CHECKOUT_COUPON)
.catch(function () {
/* 复制失败仍展示弹窗,用户可手动输入优惠码 */
})
.finally(function () {
openChallengeCouponModal(checkoutUrl);
});
});
$(document).on("click", "#challengeCouponModalOk", function () {
proceedChallengeCheckoutFromModal();
});
$(document).on("click", "[data-coupon-modal-dismiss]", function () {
closeChallengeCouponModal();
});
$(document).on("keydown.challengeCouponModal", function (e) {
if (e.key !== "Escape") return;
var $modal = $("#challengeCouponModal");
if (!$modal.length || !$modal.hasClass("is-open")) return;
closeChallengeCouponModal();
});
$(function () {
renderCurrencyBar();
syncTabsFromAppState();
renderChallengeFloor();
document.addEventListener("scroll", repositionOpenMetricTooltips, true);
$(window).on("resize orientationchange", repositionOpenMetricTooltips);
$(window).on("resize orientationchange", function () {
var $mount = $("#challenge-floor-mount");
window.requestAnimationFrame(function () {
bindChallengeCardsDragScroll($mount);
syncChallengeDesktopLadderHeights();
syncMobileSizeTabFromScroll($mount);
});
});
});
window.ChallengeFloorState = {
getAppState: function () {
return $.extend({}, appState);
},
setFeeDiscount: function (d) {
appState.feeDiscount = normalizeFeeDiscount(d);
renderChallengeFloor();
},
setCurrency: function (c) {
appState.currency = c;
renderCurrencyBar();
renderChallengeFloor();
},
setProgram: function (p, subMode) {
appState.programType = p;
if (p === "instant") {
appState.instantMode = normalizeInstantMode(subMode || appState.instantMode);
} else if (p === "aifolite") {
appState.aifoliteMode = normalizeAifoliteMode(subMode || appState.aifoliteMode);
}
syncTabsFromAppState();
renderCurrencyBar();
renderChallengeFloor();
},
refresh: renderChallengeFloor,
syncDesktopLadder: syncChallengeDesktopLadderHeights,
};
})(jQuery);