(function () { 'use strict'; var page = document.getElementById('coinSellerPage'); var region = document.getElementById('coinSellerRegion'); var loadingView = document.getElementById('sellerLoading'); var emptyView = document.getElementById('sellerEmpty'); var errorView = document.getElementById('sellerError'); var retryButton = document.getElementById('sellerRetry'); var skeletonList = document.getElementById('sellerSkeletonList'); var sellerList = document.getElementById('sellerList'); var sellerTemplate = document.getElementById('sellerCardTemplate'); var params = (window.HyAppParams && window.HyAppParams.current && window.HyAppParams.current.raw) || new URLSearchParams(window.location.search); var requestSequence = 0; var currentItems = []; var currentView = 'loading'; if (!page || !region || !sellerList || !sellerTemplate) return; function t(key, fallback) { return window.HyAppI18n && window.HyAppI18n.t ? window.HyAppI18n.t(key, fallback) : fallback; } function format(message, values) { return String(message || '').replace( /\{([a-zA-Z0-9_]+)\}/g, function (_, key) { return Object.prototype.hasOwnProperty.call(values || {}, key) ? String(values[key]) : ''; } ); } function stringValue(value) { return value === undefined || value === null ? '' : String(value).trim(); } function safeMediaURL(value) { var raw = stringValue(value); if (!raw) return ''; try { var parsed = new URL(raw, window.location.href); return parsed.protocol === 'https:' || parsed.protocol === 'http:' ? parsed.href : ''; } catch (_) { return ''; } } function safeWhatsAppURL(value) { var raw = stringValue(value); if (!raw) return ''; try { var parsed = new URL(raw, window.location.href); if (parsed.protocol === 'whatsapp:') return parsed.href; if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { return ''; } var host = parsed.hostname.toLowerCase(); return host === 'wa.me' || host === 'api.whatsapp.com' ? parsed.href : ''; } catch (_) { return ''; } } function isVisualFixture() { return params.get('mock') === '1'; } function visualFixturePayload() { var requestedCount = Number(params.get('mock_count') || 5); var count = Number.isFinite(requestedCount) ? Math.max(0, Math.min(20, Math.floor(requestedCount))) : 5; var avatar = safeMediaURL(params.get('mock_avatar')); var contactEnabled = params.get('mock_contact') !== '0'; var items = []; // 该分支只服务显式 mock=1 的设计验收。生产请求失败时绝不进入这里, // 否则示例币商会掩盖鉴权、区域或上游故障并被用户误认为真实联系人。 for (var index = 0; index < count; index += 1) { items.push({ user_id: String(8800000 + index), display_user_id: String(123456789 + index), username: params.get('mock_name') || 'Namenamenamenamename', avatar: avatar, country_code: 'AE', country_name: 'United Arab Emirates', country_display_name: 'United Arab Emirates', whatsapp_phone: contactEnabled ? '+971500000000' : '', whatsapp_url: contactEnabled ? 'https://wa.me/971500000000' : '', }); } return { items: items, total: items.length }; } function normalizePayload(payload) { if (!payload || !Array.isArray(payload.items)) { throw new Error('coin_seller_response_invalid'); } return payload.items.map(function (item) { var source = item && typeof item === 'object' ? item : {}; return { userId: stringValue(source.user_id), displayUserId: stringValue(source.display_user_id), username: stringValue(source.username), avatar: stringValue(source.avatar), countryCode: stringValue(source.country_code).toUpperCase(), countryName: stringValue( source.country_display_name || source.country_name ), whatsappPhone: stringValue(source.whatsapp_phone), whatsappURL: stringValue(source.whatsapp_url), }; }); } function sellerName(item) { return ( item.username || item.displayUserId || item.userId || t('coin_seller_fami.unknown_seller', 'Seller') ); } function firstVisibleCharacter(value) { var text = stringValue(value); if (!text) return ''; if (window.Intl && window.Intl.Segmenter) { var segments = new window.Intl.Segmenter(undefined, { granularity: 'grapheme', }).segment(text); var first = segments[Symbol.iterator]().next(); return first.done ? '' : first.value.segment; } return Array.from(text)[0] || ''; } function flagURL(countryCode) { var code = stringValue(countryCode).toUpperCase(); if (!/^[A-Z]{2}$/.test(code)) return ''; if (code === 'AE') return './assets/fami/layers/flag-ae.webp'; // Figma 只提供 AE 叶子素材;其他国家必须按接口 country_code 请求真实国旗, // 不能错误复用 AE 图。失败时保留 ISO code 作为有意义的单卡降级。 return 'https://flagcdn.com/w80/' + code.toLowerCase() + '.png'; } function bindAvatar(card, item, name) { var image = card.querySelector('.seller-avatar'); var fallback = card.querySelector('.seller-avatar-fallback'); var source = safeMediaURL(item.avatar); fallback.textContent = firstVisibleCharacter(name); image.alt = format( t('coin_seller_fami.avatar_label', '{name} avatar'), { name: name } ); image.addEventListener( 'load', function () { image.classList.add('is-loaded'); }, { once: true } ); image.addEventListener( 'error', function () { image.classList.remove('is-loaded'); image.removeAttribute('src'); }, { once: true } ); if (source) image.src = source; } function bindFlag(card, item) { var image = card.querySelector('.seller-flag'); var fallback = card.querySelector('.seller-flag-code'); var code = item.countryCode; var source = flagURL(code); var country = item.countryName || code; fallback.textContent = code; image.alt = country ? format(t('coin_seller_fami.country_flag', '{country} flag'), { country: country, }) : ''; image.addEventListener( 'load', function () { image.classList.add('is-loaded'); }, { once: true } ); image.addEventListener( 'error', function () { image.classList.remove('is-loaded'); image.removeAttribute('src'); }, { once: true } ); if (source) image.src = source; } function bindContact(card, item, name) { var contact = card.querySelector('.seller-contact'); var label = contact.querySelector('span'); var target = safeWhatsAppURL(item.whatsappURL); var contactText = t('coin_seller_fami.contact', 'Contact'); label.textContent = contactText; if (target) { contact.href = target; contact.target = '_blank'; contact.removeAttribute('aria-disabled'); contact.removeAttribute('tabindex'); contact.setAttribute( 'aria-label', format(t('coin_seller_fami.contact_seller', 'Contact {name}'), { name: name, }) ); return; } // 空联系方式来自服务端的稳定 schema。控件必须真实禁用,而不是仍然 // 导航到空页或由客户端根据电话号码猜测一个 WhatsApp 地址。 contact.removeAttribute('href'); contact.removeAttribute('target'); contact.setAttribute('aria-disabled', 'true'); contact.setAttribute('tabindex', '-1'); contact.setAttribute( 'aria-label', t('coin_seller_fami.contact_unavailable', 'Contact unavailable') ); contact.addEventListener('click', function (event) { event.preventDefault(); }); } function createSellerCard(item, index) { var fragment = sellerTemplate.content.cloneNode(true); var card = fragment.querySelector('.seller-card'); var name = sellerName(item); var visibleID = item.displayUserId || item.userId; card.dataset.userId = item.userId; card.dataset.index = String(index); card.querySelector('.seller-name').textContent = name; card.querySelector('.seller-display-id').textContent = format( t('coin_seller_fami.seller_id', 'ID: {id}'), { id: visibleID } ); bindAvatar(card, item, name); bindFlag(card, item); bindContact(card, item, name); return fragment; } function renderSellers(items) { var fragment = document.createDocumentFragment(); sellerList.replaceChildren(); items.forEach(function (item, index) { fragment.appendChild(createSellerCard(item, index)); }); sellerList.appendChild(fragment); } function createSkeletonCard() { var item = document.createElement('li'); item.className = 'seller-skeleton-card'; item.innerHTML = '' + '' + '' + '' + ''; return item; } function renderSkeletons() { var fragment = document.createDocumentFragment(); for (var index = 0; index < 5; index += 1) { fragment.appendChild(createSkeletonCard()); } skeletonList.replaceChildren(fragment); } function setView(view) { currentView = view; loadingView.hidden = view !== 'loading'; emptyView.hidden = view !== 'empty'; errorView.hidden = view !== 'error'; sellerList.hidden = view !== 'list'; region.setAttribute('aria-busy', String(view === 'loading')); } function requestCoinSellers() { if (isVisualFixture()) { return Promise.resolve(visualFixturePayload()); } if ( !window.HyAppAPI || !window.HyAppAPI.wallet || !window.HyAppAPI.wallet.listCoinSellers ) { return Promise.reject(new Error('coin_seller_api_unavailable')); } return window.HyAppAPI.wallet.listCoinSellers(); } function loadCoinSellers() { var sequence = ++requestSequence; currentItems = []; setView('loading'); requestCoinSellers() .then(function (payload) { // Retry 或后续刷新可能先于旧请求完成;只允许最新序列提交 UI, // 防止慢失败覆盖已经成功的新列表。 if (sequence !== requestSequence) return; currentItems = normalizePayload(payload); if (!currentItems.length) { setView('empty'); return; } renderSellers(currentItems); setView('list'); }) .catch(function (error) { if (sequence !== requestSequence) return; currentItems = []; setView('error'); if (window.console && window.console.warn) { window.console.warn('[coin-seller:fami]', error); } }); } retryButton.addEventListener('click', loadCoinSellers); window.addEventListener('hyapp:i18n-ready', function () { // DOM 模板里的动态 ID 与可访问标签不由 data-i18n 自动插值;语言切换后 // 重新渲染当前成功列表即可保持视觉与读屏文案一致,不重新发业务请求。 if (currentView === 'list' && currentItems.length) { renderSellers(currentItems); } }); renderSkeletons(); loadCoinSellers(); })();