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

369 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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 =
'<img class="seller-skeleton-skin" src="./assets/fami/layers/seller-card-skin.webp" alt="" draggable="false">' +
'<span class="seller-skeleton-avatar"></span>' +
'<span class="seller-skeleton-line seller-skeleton-name"></span>' +
'<span class="seller-skeleton-line seller-skeleton-id"></span>' +
'<span class="seller-skeleton-button"></span>';
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();
})();