4d30803b60
Add static site, shared layout, and Node dev server with standard .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
1815 lines
62 KiB
JavaScript
1815 lines
62 KiB
JavaScript
/**
|
||
* 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 day’s 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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
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)
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 992px~1200px:与 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 顶出左列(992~1200 + 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");
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 992~1200px:foot-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);
|