403 lines
13 KiB
JavaScript
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,
|
|
};
|
|
})();
|