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

619 lines
22 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 () {
var ui = window.FamiCenterUI;
var fallbackAvatar =
'../../products/fami/centers/assets/fami/agency-avatar.png';
var state = {
loading: true,
profile: null,
owner: null,
balance: 0,
stats: null,
hosts: [],
dailyHosts: {},
activeAsset: 'diamond',
activeDate: '',
view: 'summary',
inviteProfile: null,
inviting: false,
statsRequestSequence: 0,
dailyRequestSequence: 0,
};
var dates = [];
var contactController = null;
var summaryDefinitions = [
{
key: 'total_earnings',
label: 'fami.total_earnings',
fallback: 'Total Earnings',
},
{
key: 'gift_income',
label: 'fami.gift_income',
fallback: 'Gift Income',
},
{
key: 'share_income',
label: 'fami.share_income',
fallback: 'Share Income',
},
{
key: 'total_hosts',
label: 'fami.total_hosts',
fallback: 'Total Hosts',
},
{
key: 'gifted_host_count',
label: 'fami.gifted_hosts',
fallback: 'Gifted Hosts',
},
];
var mockHosts = Array.from({ length: 5 }, function (_, index) {
return {
membership_id: 'membership-' + index,
host_user_id: 'host-' + index,
display_user_id: '12345' + (index + 10),
username: 'host ' + (index + 1),
avatar: fallbackAvatar,
diamond_earnings: 123214,
};
});
var mock = {
user: {
user_id: 'agency-owner-1',
display_user_id: '123456789',
username: 'konghuimingc',
avatar: fallbackAvatar,
contact_info: '+65 1234 5678',
},
overview: {
agency: {
agency_id: '123456789',
name: 'konghuimingc',
avatar: fallbackAvatar,
created_at_ms: Date.UTC(2099, 8, 9),
},
},
coins: {
balances: [{ asset_type: 'COIN', available_amount: 1000000000 }],
},
stats: {
summary: {
total_earnings: 999999,
gift_income: 999999,
share_income: 999999,
total_hosts: 999999,
gifted_host_count: 999999,
},
hosts: mockHosts,
},
};
function profileForOverview(payload, owner) {
var data = ui.unwrap(payload) || {};
var agency = data.agency || {};
return {
id: String(agency.agency_id || ''),
displayId: (owner && owner.displayId) || '',
name: String(agency.name || 'Agency'),
avatar: String(agency.avatar || ''),
createdAt: agency.created_at_ms || 0,
};
}
function normalizeHost(item) {
item = item || {};
return {
id: String(item.host_user_id || ''),
membershipId: String(item.membership_id || ''),
displayId: String(item.display_user_id || ''),
name: String(item.username || 'Host'),
avatar: String(item.avatar || ''),
diamondEarnings: ui.number(item.diamond_earnings),
};
}
function summaryValues() {
var summary = (state.stats && state.stats.summary) || {};
return summaryDefinitions.map(function (definition) {
return ui.number(summary[definition.key]);
});
}
function applyStats(payload, date) {
var stats = ui.unwrap(payload) || { summary: {}, hosts: [] };
var hosts = Array.isArray(stats.hosts)
? stats.hosts.map(normalizeHost)
: [];
state.stats = stats;
state.hosts = hosts;
if (date) state.dailyHosts[date] = hosts;
}
function hostValue(host, index) {
var date = dates[index] && dates[index].value;
var daily = state.dailyHosts[date] || [];
var item = daily.find(function (candidate) {
return candidate.id === host.id;
});
return item ? item.diamondEarnings : 0;
}
function renderProfile() {
var profile = state.profile || {};
var owner = state.owner || {};
ui.setAvatar(ui.$('agencyAvatar'), profile.avatar, fallbackAvatar);
ui.$('agencyName').textContent = profile.name || '-';
ui.$('agencyUserId').textContent = ui
.t('fami.user_id', 'User ID:{id}')
.replace('{id}', profile.displayId || '-');
var welcomeDate = profile.createdAt
? new Date(profile.createdAt)
: null;
var welcome = '';
if (welcomeDate && !Number.isNaN(welcomeDate.getTime())) {
// Figma 固定使用点分隔日期;字段值仍来自 agency.created_at_ms不由客户端推断公会成立时间。
welcome = [
welcomeDate.getFullYear(),
String(welcomeDate.getMonth() + 1).padStart(2, '0'),
String(welcomeDate.getDate()).padStart(2, '0'),
].join('.');
}
if (ui.isMock()) welcome = '2099.99.99';
ui.$('agencyWelcome').textContent = ui
.t('fami.welcome_date', 'Welcome {date}')
.replace('{date}', welcome || '-');
ui.$('contactName').textContent = owner.name || profile.name || '-';
ui.$('contactUserId').textContent = ui
.t('fami.user_id', 'User ID:{id}')
.replace('{id}', owner.displayId || profile.displayId || '-');
if (contactController)
contactController.setContact(owner.contact || '');
}
function renderDates() {
dates = ui.dateWindow(9);
if (!state.activeDate && dates.length)
state.activeDate = dates[0].value;
ui.renderDateStrip(
ui.$('dateStrip'),
dates.slice(0, 7),
state.activeDate,
selectDate
);
}
function renderMetrics() {
var grid = ui.$('metricGrid');
grid.replaceChildren();
summaryValues().forEach(function (value, index) {
var card = document.createElement('div');
card.className = 'agency-metric';
var valueRow = document.createElement('div');
valueRow.className = 'agency-metric__value';
if (index < 3) {
var icon = document.createElement('img');
icon.src =
'../../products/fami/centers/assets/fami/diamond.png';
icon.alt = '';
valueRow.appendChild(icon);
}
var number = document.createElement('strong');
number.textContent = ui.formatNumber(value).replace(/,/g, '');
valueRow.appendChild(number);
var definition = summaryDefinitions[index];
var label = document.createElement('span');
label.textContent = ui.isMock()
? ui.t('fami.total_return', 'Total Return')
: ui.t(definition.label, definition.fallback);
card.appendChild(valueRow);
card.appendChild(label);
grid.appendChild(card);
});
}
function renderTabs() {
document.querySelectorAll('.agency-tab').forEach(function (tab) {
var active = tab.getAttribute('data-type') === state.activeAsset;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', String(active));
});
}
function renderHostTable() {
var hostTable = ui.$('hostTable');
// mock 模式固定复现 Figma 已输入的搜索态,但不能让占位 ID 把设计稿中的示例行过滤掉;真实入口仍按用户提交值筛选。
var query = ui.isMock()
? ''
: String(ui.$('hostSearchInput').value || '')
.trim()
.toLowerCase();
var hosts = state.hosts.filter(function (host) {
if (!query) return true;
return (
host.displayId.toLowerCase().includes(query) ||
host.name.toLowerCase().includes(query)
);
});
var grid = document.createElement('div');
grid.className = 'agency-host-table__grid';
var header = document.createElement('div');
header.className = 'agency-host-table__header';
dates.slice(0, 9).forEach(function (date) {
var head = document.createElement('span');
head.className = 'agency-host-table__head';
head.textContent = ui.isMock()
? ui.t('fami.today', 'Today')
: date.label;
head.setAttribute('aria-label', date.label + ' ' + date.value);
header.appendChild(head);
});
var rows = document.createElement('div');
rows.className = 'agency-host-table__rows';
hosts.forEach(function (host) {
var avatar = document.createElement('img');
avatar.className = 'agency-host-table__avatar';
avatar.alt = '';
ui.setAvatar(avatar, host.avatar, fallbackAvatar);
rows.appendChild(avatar);
dates.slice(0, 8).forEach(function (_, index) {
var value = document.createElement('span');
value.className = 'agency-host-table__value';
value.textContent = ui
.formatNumber(hostValue(host, index))
.replace(/,/g, '');
rows.appendChild(value);
});
});
grid.appendChild(header);
grid.appendChild(rows);
hostTable.replaceChildren(grid);
}
function renderView() {
ui.$('summaryView').hidden = state.view !== 'summary';
ui.$('hostView').hidden = state.view !== 'hosts';
if (state.view === 'hosts') renderHostTable();
}
function render() {
renderProfile();
ui.$('goldBalance').textContent = ui.formatNumber(state.balance);
renderTabs();
renderDates();
renderMetrics();
renderView();
}
function applyPayloads(
userPayload,
overviewPayload,
coinPayload,
statsPayload
) {
state.owner = ui.profileFrom(userPayload, 'Agency');
state.profile = profileForOverview(overviewPayload, state.owner);
state.balance = ui.walletAvailable(coinPayload, 'COIN');
applyStats(statsPayload, state.activeDate);
state.loading = false;
render();
}
function selectDate(value) {
state.activeDate = value;
renderDates();
if (ui.isMock()) {
applyStats(mock.stats, value);
renderMetrics();
return;
}
var api = window.HyAppAPI && window.HyAppAPI.agencyCenter;
if (!api || !api.stats) return;
var sequence = ++state.statsRequestSequence;
// 汇总卡必须与当前选中 UTC 自然日一致;迟到响应只写入其日期缓存,不覆盖后来选择。
api.stats({ start_date: value, end_date: value })
.then(function (payload) {
if (sequence !== state.statsRequestSequence) return;
applyStats(payload, value);
renderMetrics();
})
.catch(function (error) {
if (sequence !== state.statsRequestSequence) return;
ui.toast(
(error && error.message) ||
ui.t(
'agency_center.load_failed',
'Load failed. Try again later.'
)
);
});
}
function loadDailyMatrix() {
if (ui.isMock()) {
dates.slice(0, 8).forEach(function (date) {
state.dailyHosts[date.value] = mockHosts.map(normalizeHost);
});
renderHostTable();
return;
}
var api = window.HyAppAPI && window.HyAppAPI.agencyCenter;
if (!api || !api.stats) return;
var sequence = ++state.dailyRequestSequence;
// 矩阵每列是一个自然日,现有 stats 契约按区间聚合;并行读取 8 个单日且独立容错,绝不把某日聚合值复制到其他日期。
Promise.all(
dates.slice(0, 8).map(function (date) {
return ui.safe('agency-stats-' + date.value, function () {
return api.stats({
start_date: date.value,
end_date: date.value,
});
});
})
).then(function (payloads) {
if (sequence !== state.dailyRequestSequence) return;
payloads.forEach(function (payload, index) {
var day = dates[index] && dates[index].value;
var data = ui.unwrap(payload) || {};
state.dailyHosts[day] = Array.isArray(data.hosts)
? data.hosts.map(normalizeHost)
: [];
});
renderHostTable();
});
}
function load() {
dates = ui.dateWindow(9);
if (!state.activeDate && dates.length)
state.activeDate = dates[0].value;
if (ui.isMock()) {
applyPayloads(mock.user, mock.overview, mock.coins, mock.stats);
return;
}
var api = window.HyAppAPI;
if (!api || !api.agencyCenter || !api.agencyCenter.stats) {
state.loading = false;
render();
ui.toast(
ui.t('agency_center.api_not_ready', 'API module is not ready.')
);
return;
}
// 公会资料、负责人、COIN 余额和日统计来自明确接口;旧 salary 字段只代表 USD 工资,不能继续冒充 Gold Coins。
Promise.all([
ui.safe('agency-owner', function () {
return api.user.me();
}),
ui.safe('agency-overview', function () {
return api.agencyCenter.overview();
}),
ui.safe('agency-coins', function () {
return api.wallet.balances('COIN');
}),
ui.safe('agency-stats', function () {
return api.agencyCenter.stats({
start_date: state.activeDate,
end_date: state.activeDate,
});
}),
]).then(function (results) {
applyPayloads(results[0], results[1], results[2], results[3]);
if (!results.some(Boolean)) {
ui.toast(
ui.t(
'agency_center.load_failed',
'Load failed. Try again later.'
)
);
}
});
}
function openWithdraw() {
// 资金页必须由产品上下文构造 URL保证无 app_code 参数的旧客户端仍使用 Fami 租户。
window.location.href = window.HyGuild.product.buildURL('../wallet/', {
identity: 'AGENCY',
});
}
function setInviteStatus(message, success) {
var node = ui.$('inviteStatus');
node.textContent = message || '';
node.hidden = !message;
node.classList.toggle('is-success', !!success);
}
function normalizeInviteProfile(payload, fallbackId) {
var profile = ui.profileFrom(payload, 'Host');
if (!profile.id) return null;
if (!profile.displayId) profile.displayId = fallbackId;
return profile;
}
function renderInviteResult() {
var root = ui.$('inviteResult');
root.replaceChildren();
if (!state.inviteProfile) return;
var row = document.createElement('div');
row.className = 'fami-search-result';
var avatar = document.createElement('img');
avatar.alt = '';
ui.setAvatar(avatar, state.inviteProfile.avatar, fallbackAvatar);
var copy = document.createElement('div');
copy.className = 'fami-search-result__copy';
var name = document.createElement('strong');
name.textContent = state.inviteProfile.name || 'Host';
var id = document.createElement('span');
id.textContent = 'ID: ' + state.inviteProfile.displayId;
var action = document.createElement('button');
action.className = 'fami-search-result__action';
action.type = 'button';
action.disabled = state.inviting;
action.textContent = state.inviting
? ui.t('agency_center.pending', 'Pending')
: ui.t('agency_center.invite', 'Invite');
action.addEventListener('click', inviteHost);
copy.appendChild(name);
copy.appendChild(id);
row.appendChild(avatar);
row.appendChild(copy);
row.appendChild(action);
root.appendChild(row);
}
function searchInvite(event) {
event.preventDefault();
var value = String(ui.$('inviteSearchInput').value || '').trim();
state.inviteProfile = null;
renderInviteResult();
if (!value) return;
if (ui.isMock()) {
state.inviteProfile = ui.profileFrom(mock.user, 'Host');
state.inviteProfile.id = 'mock-host-' + value;
state.inviteProfile.displayId = value;
renderInviteResult();
return;
}
var api = window.HyAppAPI && window.HyAppAPI.user;
if (!api || !api.resolveDisplayUserID) {
setInviteStatus(
ui.t('agency_center.api_not_ready', 'API module is not ready.')
);
return;
}
ui.$('inviteSearchButton').disabled = true;
setInviteStatus(ui.t('agency_center.searching', 'Searching...'));
api.resolveDisplayUserID(value)
.then(function (payload) {
state.inviteProfile = normalizeInviteProfile(payload, value);
setInviteStatus(
state.inviteProfile
? ''
: ui.t('agency_center.no_users_found', 'No users found')
);
renderInviteResult();
})
.catch(function (error) {
setInviteStatus(
(error && error.message) ||
ui.t(
'agency_center.search_failed',
'Search failed. Try again later.'
)
);
})
.finally(function () {
ui.$('inviteSearchButton').disabled = false;
});
}
function inviteHost() {
if (!state.inviteProfile || state.inviting) return;
if (ui.isMock()) {
setInviteStatus(
ui.t(
'agency_center.invite_sent',
'Invite sent. Waiting for confirmation.'
),
true
);
return;
}
var api = window.HyAppAPI && window.HyAppAPI.agencyCenter;
if (!api || !api.inviteHost) return;
state.inviting = true;
renderInviteResult();
// 邀请使用唯一 command_id 交给服务端幂等;结果未确认前禁用按钮,避免 WebView 连点产生多份待确认邀请。
api.inviteHost({
command_id: ui.commandId('agency-invite-host'),
target_user_id: state.inviteProfile.id,
})
.then(function () {
setInviteStatus(
ui.t(
'agency_center.invite_sent',
'Invite sent. Waiting for confirmation.'
),
true
);
state.inviteProfile = null;
renderInviteResult();
})
.catch(function (error) {
setInviteStatus(
(error && error.message) ||
ui.t(
'agency_center.invite_failed',
'Invite failed. Try again later.'
)
);
})
.finally(function () {
state.inviting = false;
renderInviteResult();
});
}
function openInvite() {
ui.$('hostSearchInput').value = ui.isMock() ? '1235456' : '';
state.view = 'hosts';
renderView();
loadDailyMatrix();
ui.openModal('inviteModal');
ui.$('inviteSearchInput').value = '';
state.inviteProfile = null;
setInviteStatus('');
renderInviteResult();
}
function init() {
contactController = ui.bindContact();
ui.$('backButton').addEventListener('click', function () {
if (state.view === 'hosts') {
state.view = 'summary';
renderView();
return;
}
ui.goBack();
});
ui.$('withdrawButton').addEventListener('click', openWithdraw);
ui.$('addHostButton').addEventListener('click', openInvite);
ui.$('inviteSearchForm').addEventListener('submit', searchInvite);
ui.$('hostSearchForm').addEventListener('submit', function (event) {
event.preventDefault();
renderHostTable();
});
document
.querySelectorAll('[data-invite-close]')
.forEach(function (node) {
node.addEventListener('click', function () {
ui.closeModal('inviteModal');
});
});
document
.querySelectorAll('.agency-tab:not([disabled])')
.forEach(function (tab) {
tab.addEventListener('click', function () {
state.activeAsset = tab.getAttribute('data-type');
renderTabs();
});
});
window.addEventListener('hyapp:i18n-ready', render);
render();
load();
if (window.HyAppBridge) {
window.HyAppBridge.ready({
page: 'agency-center',
app_code: 'fami',
mock: ui.isMock(),
});
}
}
document.addEventListener('DOMContentLoaded', init);
})();