2026-07-17 11:54:43 +08:00

403 lines
13 KiB
JavaScript

(function () {
var params = new URLSearchParams(window.location.search);
function $(id) {
return document.getElementById(id);
}
function t(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function toast(message) {
if (message && window.HyAppToast && window.HyAppToast.show) {
window.HyAppToast.show(message);
}
}
function unwrap(payload) {
if (!payload || typeof payload !== 'object') return payload;
return Object.prototype.hasOwnProperty.call(payload, 'data')
? payload.data
: payload;
}
function first(source, keys, fallback) {
var target = source || {};
for (var index = 0; index < keys.length; index += 1) {
var value = target[keys[index]];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return fallback === undefined ? '' : fallback;
}
function number(value) {
var result = Number(value);
return Number.isFinite(result) ? result : 0;
}
function formatNumber(value, maximumFractionDigits) {
return number(value).toLocaleString('en-US', {
maximumFractionDigits:
maximumFractionDigits === undefined ? 0 : maximumFractionDigits,
});
}
function walletAvailable(payload, assetType) {
var data = unwrap(payload) || {};
var balances = Array.isArray(data) ? data : data.balances || [];
var expected = String(assetType || '').toUpperCase();
var balance = balances.find(function (item) {
return (
String((item && item.asset_type) || '').toUpperCase() ===
expected
);
});
return balance ? number(balance.available_amount) : 0;
}
function formatDate(value, options) {
if (!value) return '';
var date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return '';
var language =
window.HyAppI18n && window.HyAppI18n.lang
? window.HyAppI18n.lang()
: 'en';
var locale = {
en: 'en-US',
ar: 'ar',
tr: 'tr-TR',
es: 'es-ES',
zh: 'zh-CN',
id: 'id-ID',
}[language];
try {
return date.toLocaleString(
locale || 'en-US',
options || {
month: 'short',
day: 'numeric',
}
);
} catch (_) {
return date.toLocaleString('en-US', options || {});
}
}
function dateKey(value) {
var date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return '';
return [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0'),
].join('-');
}
function dateWindow(count) {
var result = [];
var total = Math.max(1, Number(count || 7));
var now = new Date();
// stats 服务按 UTC 自然日聚合;日期窗也必须从 UTC 零点递减,否则 UTC+ 时区会在本地午夜后提前请求“明天”的收益。
var today = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
);
for (var index = 0; index < total; index += 1) {
var date = new Date(today.getTime() - index * 86400000);
var value = [
date.getUTCFullYear(),
String(date.getUTCMonth() + 1).padStart(2, '0'),
String(date.getUTCDate()).padStart(2, '0'),
].join('-');
result.push({
value: value,
date: date,
label:
index === 0
? t('fami.today', 'Today')
: formatDate(date, {
month: 'numeric',
day: 'numeric',
timeZone: 'UTC',
}),
});
}
return result;
}
function renderDateStrip(node, dates, activeValue, onSelect) {
if (!node) return;
node.replaceChildren();
dates.forEach(function (item) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'fami-date-strip__item';
button.classList.toggle('is-active', item.value === activeValue);
// 视觉回归 mock 复现 Figma 的 Today 占位;真实入口展示实际日期,避免多个同名按钮让用户无法判断筛选日。
button.textContent = isMock()
? t('fami.today', 'Today')
: item.label;
button.setAttribute('data-date', item.value);
button.setAttribute('aria-label', item.label + ' ' + item.value);
button.setAttribute(
'aria-pressed',
String(item.value === activeValue)
);
button.addEventListener('click', function () {
onSelect(item.value);
});
node.appendChild(button);
});
}
function setAvatar(image, source, fallback) {
if (!image) return;
var explicitSource = String(source || '').trim();
// Figma 人像只属于显式 mock 验收数据。线上资料没有头像时保留真实空态,
// 不能让设计稿示例人物被误认为当前用户或币商。
var resolved =
explicitSource || (isMock() ? String(fallback || '').trim() : '');
image.onerror = function () {
image.onerror = null;
var mockFallback = isMock() ? String(fallback || '').trim() : '';
if (mockFallback && image.src !== mockFallback) {
image.src = mockFallback;
return;
}
image.removeAttribute('src');
image.classList.add('is-empty');
};
image.classList.toggle('is-empty', !resolved);
if (resolved) {
image.src = resolved;
} else {
image.removeAttribute('src');
}
}
function contactFrom(payload) {
var data = unwrap(payload) || {};
var source = data.profile || data.user || data;
return String(
first(source, [
'contact_info',
'contactInfo',
'whatsapp_phone',
'whatsappPhone',
'contact',
]) || ''
).trim();
}
function profileFrom(payload, fallbackName) {
var data = unwrap(payload) || {};
var source = data.profile || data.user || data;
return {
id: String(first(source, ['user_id', 'userId', 'id']) || ''),
displayId: String(
first(source, [
'display_user_id',
'displayUserId',
'default_display_user_id',
'uid',
'user_id',
'id',
]) || ''
),
name: String(
first(source, [
'username',
'nickname',
'display_name',
'name',
]) ||
fallbackName ||
''
),
avatar: String(
first(source, ['avatar', 'avatar_url', 'avatarUrl']) || ''
),
contact: contactFrom(source),
createdAt: first(source, [
'created_at_ms',
'created_at',
'createdAt',
'joined_at_ms',
'joined_at',
'joinedAt',
]),
};
}
function commandId(prefix) {
var random = Math.random().toString(36).slice(2, 10);
return [prefix || 'fami', Date.now().toString(36), random].join('-');
}
function isMock() {
return params.get('mock') === '1';
}
function safe(label, factory) {
return Promise.resolve()
.then(factory)
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn('[fami:' + label + ']', error);
}
return null;
});
}
function goBack(fallbackURL) {
if (window.HyAppBridge && window.HyAppBridge.back) {
window.HyAppBridge.back();
return;
}
if (window.history.length > 1) {
window.history.back();
return;
}
if (fallbackURL) window.location.href = fallbackURL;
}
function openModal(modal) {
var node = typeof modal === 'string' ? $(modal) : modal;
if (!node) return;
node.hidden = false;
node.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
}
function closeModal(modal) {
var node = typeof modal === 'string' ? $(modal) : modal;
if (!node) return;
node.hidden = true;
node.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
}
function bindContact(options) {
var openButton = $('contactCard');
var modal = $('contactModal');
var form = $('contactForm');
var input = $('contactInput');
var save = $('contactSaveButton');
var status = $('contactStatus');
if (!openButton || !modal || !form || !input || !save || !status) {
return { setContact: function () {} };
}
var current = '';
function setStatus(message, success) {
status.textContent = message || '';
status.hidden = !message;
status.classList.toggle('is-success', !!success);
}
function setContact(value) {
current = String(value || '').trim();
}
function close() {
closeModal(modal);
setStatus('');
}
openButton.addEventListener('click', function () {
input.value = current;
setStatus('');
openModal(modal);
window.setTimeout(function () {
input.focus();
}, 0);
});
modal.querySelectorAll('[data-contact-close]').forEach(function (node) {
node.addEventListener('click', close);
});
form.addEventListener('submit', function (event) {
event.preventDefault();
var value = String(input.value || '').trim();
if (!value) {
setStatus(
t('contact_card.required', 'Enter WhatsApp contact.')
);
return;
}
var api = window.HyAppAPI && window.HyAppAPI.user;
if (isMock()) {
setContact(value);
close();
toast(t('contact_card.saved', 'Saved.'));
if (options && options.onSaved) options.onSaved(current);
return;
}
if (!api || !api.updateContact) {
setStatus(
t(
'contact_card.api_unavailable',
'Contact API unavailable.'
)
);
return;
}
// 联系方式属于用户主资料而不是中心页本地状态;只有服务端确认写入后才关闭弹层,防止 UI 显示成功但下一页仍读到旧联系方式。
save.disabled = true;
save.textContent = t('contact_card.saving', 'Saving...');
setStatus('');
api.updateContact({ contact_info: value })
.then(function (response) {
setContact(contactFrom(response) || value);
close();
toast(t('contact_card.saved', 'Saved.'));
if (options && options.onSaved) options.onSaved(current);
})
.catch(function (error) {
setStatus(
(error && error.message) ||
t('contact_card.save_failed', 'Save failed.')
);
})
.finally(function () {
save.disabled = false;
save.textContent = t('contact_card.save', 'Save');
});
});
return { setContact: setContact };
}
window.FamiCenterUI = {
$: $,
t: t,
toast: toast,
unwrap: unwrap,
first: first,
number: number,
formatNumber: formatNumber,
walletAvailable: walletAvailable,
formatDate: formatDate,
dateKey: dateKey,
dateWindow: dateWindow,
renderDateStrip: renderDateStrip,
setAvatar: setAvatar,
contactFrom: contactFrom,
profileFrom: profileFrom,
commandId: commandId,
isMock: isMock,
params: params,
safe: safe,
goBack: goBack,
openModal: openModal,
closeModal: closeModal,
bindContact: bindContact,
};
})();