(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, }; })();