(function () { var ACTION_LABELS = { 'block-user': 'Block User', 'send-frame': 'Send Frame', 'send-car': 'Send Car', 'send-badge': 'Send Badge', 'send-vip': 'Send VIP', 'update-user-level': 'Update User Level', 'add-bd-leader': 'Add BD Leader', 'add-admin': 'Add Admin', 'add-superadmin': 'Add Superadmin', 'transfer-user-country': 'Transfer User Country', }; var DAY_MS = 24 * 60 * 60 * 1000; var PERMANENT_BLOCK_MS = 10 * 365 * DAY_MS; var RESOURCE_TYPES = { 'send-frame': 'avatar_frame', 'send-car': 'vehicle', 'send-badge': 'badge', }; var ACTION_PERMISSION_KEYS = { 'send-frame': 'canGrantAvatarFrame', 'send-car': 'canGrantVehicle', 'send-badge': 'canGrantBadge', 'send-vip': 'canGrantVip', 'update-user-level': 'canUpdateUserLevel', 'add-bd-leader': 'canAddBdLeader', 'add-admin': 'canAddAdmin', 'add-superadmin': 'canAddSuperadmin', 'block-user': 'canBlockUser', 'transfer-user-country': 'canTransferUserCountry', }; var RESOURCE_GRANT_DURATIONS = [ { label: '7 days', ms: 7 * DAY_MS }, { label: '30 days', ms: 30 * DAY_MS }, { label: '90 days', ms: 90 * DAY_MS }, ]; var DEFAULT_RESOURCE_GRANT_DURATION_MS = RESOURCE_GRANT_DURATIONS[0].ms; var VIP_GRANT_LEVELS = [1, 2, 3, 4, 5]; var TRACK_LABELS = { wealth: 'Wealth', charm: 'Charm', game: 'Game', }; var api = window.HyAppAPI && window.HyAppAPI.managerCenter; var pageState = { loading: true, denied: false, overview: null, permissions: {}, resourcesByType: {}, countries: [], }; var blockState = { searchedUser: null, selectedDurationMs: 0, selectedDurationLabel: '', blockedUsers: [], submitting: false, }; var levelState = { searchedUser: null, selectedTrack: 'wealth', selectedLevel: 0, submitting: false, }; var grantState = { action: '', resourceType: '', title: '', searchedUser: null, selectedResourceIds: [], selectedResourceDurationMs: DEFAULT_RESOURCE_GRANT_DURATION_MS, selectedVipLevel: 0, submitting: false, }; var bdLeaderState = { action: '', title: '', searchedUser: null, submitting: false, }; var countryState = { searchedUser: null, selectedCountryCode: '', submitting: false, }; function $(id) { return document.getElementById(id); } function root() { return document.querySelector('.manager-center'); } function t(key, fallback) { return window.HyAppI18n && window.HyAppI18n.t ? window.HyAppI18n.t(key, fallback) : fallback || key; } function toast(message) { if (window.HyAppToast && window.HyAppToast.show) { window.HyAppToast.show(message); return; } window.alert(message); } function showError(error) { var message = error && error.status === 403 ? 'Permission denied.' : (error && error.message) || 'Request failed.'; toast(message); } function commandId(prefix) { return [ prefix || 'manager', Date.now(), Math.random().toString(16).slice(2, 10), ].join('-'); } function userName(user) { return ( user.username || user.name || user.target_username || user.display_user_id || user.target_display_user_id || 'User' ); } function userDisplayId(user) { return user.display_user_id || user.target_display_user_id || ''; } function userId(user) { return user.user_id || user.target_user_id || ''; } function avatarUrl(user) { return user.avatar || user.target_avatar || ''; } function resourceImageUrl(resource) { return ( resource.preview_url || resource.previewUrl || resource.asset_url || resource.assetUrl || resource.icon_url || resource.iconUrl || '' ); } function resourceKindLabel() { if (grantState.action === 'send-frame') return 'Frame'; if (grantState.action === 'send-car') return 'Car'; if (grantState.action === 'send-badge') return 'Badge'; return 'Resource'; } function currentVip(user) { return user.current_vip || user.currentVip || user.vip || null; } function currentVipLevel(user) { var vip = currentVip(user); if (!vip || !vip.active) return 0; return Number(vip.level) || 0; } function currentVipStatusText(user) { var vip = currentVip(user); if (!vip || !vip.active || !vip.level) return 'Current VIP: None'; var label = vip.name || 'VIP ' + vip.level; var expiresAt = Number(vip.expires_at_ms || vip.expiresAtMs || 0); if (expiresAt > 0) { return label + ' active · Expires ' + formatDateTime(expiresAt); } return label + ' active'; } function initial(text) { return String(text || 'M') .trim() .charAt(0) .toUpperCase(); } function createNode(tag, className, text) { var node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function setAvatar(node, user) { var url = avatarUrl(user); node.innerHTML = ''; if (url) { var img = document.createElement('img'); img.src = url; img.alt = ''; node.appendChild(img); return; } node.textContent = initial(userName(user) || userDisplayId(user)); } function textValue(value, fallback) { if (value === undefined || value === null || value === '') { return fallback; } return String(value); } function isLocalPreviewError(error) { var host = window.location && window.location.hostname; var isLocalHost = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === ''; var message = (error && error.message) || ''; return isLocalHost && /failed to fetch|network/i.test(message); } function firstValue(source, keys) { source = source || {}; for (var i = 0; i < keys.length; i += 1) { var key = keys[i]; if ( Object.prototype.hasOwnProperty.call(source, key) && source[key] !== undefined && source[key] !== null && source[key] !== '' ) { return source[key]; } } return undefined; } function firstNestedValue(sources, keys) { for (var i = 0; i < sources.length; i += 1) { var value = firstValue(sources[i], keys); if (value !== undefined) return value; } return undefined; } function formatMoney(value, fallback) { if (value === undefined || value === null || value === '') { return fallback; } if (typeof value === 'string' && value.indexOf('$') >= 0) { return value; } var number = Number(String(value).replace(/,/g, '')); if (!Number.isFinite(number)) return String(value); return ( '$' + number.toLocaleString('en-US', { maximumFractionDigits: number % 1 === 0 ? 0 : 2, minimumFractionDigits: number % 1 === 0 ? 0 : 2, }) ); } function formatPercent(value, fallback) { if (value === undefined || value === null || value === '') { return fallback; } if (typeof value === 'string' && value.indexOf('%') >= 0) { // 服务端已带符号下发;只对正数补 +,零和负数原样透传,避免出现 "+0%"。 var parsed = parseFloat(value); if (!Number.isFinite(parsed) || parsed <= 0) { return value; } return '+' + value.replace(/^\+/, ''); } var number = Number(value); if (!Number.isFinite(number)) return String(value); if (Math.abs(number) > 0 && Math.abs(number) <= 1) { number *= 100; } return (number > 0 ? '+' : '') + Math.round(number) + '%'; } function setText(id, value) { var node = $(id); if (node) node.textContent = value; } function updateManagerAvatar(profile) { var image = $('managerAvatar'); var fallback = $('managerAvatarFallback'); if (!image || !fallback) return; // 没有头像时展示首字母占位,不再使用演示头像假数据。 var url = avatarUrl(profile); if (url) { image.src = url; image.hidden = false; fallback.hidden = true; return; } image.hidden = true; fallback.hidden = false; fallback.textContent = initial( userName(profile) || userDisplayId(profile) ); } function dashboardSources() { var overview = pageState.overview || {}; return [ overview.dashboard, overview.summary, overview.metrics, overview.statistics, overview.wallet, overview, ]; } function renderDashboard() { var overview = pageState.overview || {}; var profile = overview.user || overview.profile || overview.manager || overview.current_user || overview.currentUser || overview; updateManagerAvatar(profile || {}); setText( 'managerName', textValue( firstValue(profile || {}, [ 'username', 'name', 'nickname', 'target_username', ]), '--' ) ); setText( 'managerDisplayId', textValue( userDisplayId(profile || {}) || userId(profile || {}), '--' ) ); var sources = dashboardSources(); // 页面不再内置任何设计假数据:加载中由骨架屏占位,渲染时字段缺失(聚合降级/请求失败/无 API) // 一律显示中性占位符,避免把演示数值误当真实团队工资。 var fallback = { thisMonth: '--', lastMonth: '--', hosts: '--', growth: '--', wallet: '--', bankCards: '--', }; setText( 'managerThisMonthSalary', formatMoney( firstNestedValue(sources, [ 'this_month_salary', 'thisMonthSalary', 'current_month_salary', 'currentMonthSalary', 'team_salary_this_month', 'teamSalaryThisMonth', ]), fallback.thisMonth ) ); setText( 'managerLastMonthSalary', formatMoney( firstNestedValue(sources, [ 'last_month_salary', 'lastMonthSalary', 'previous_month_salary', 'previousMonthSalary', 'team_salary_last_month', 'teamSalaryLastMonth', ]), fallback.lastMonth ) ); setText( 'managerActiveHosts', textValue( firstNestedValue(sources, [ 'active_hosts', 'activeHosts', 'total_active_hosts', 'totalActiveHosts', 'host_count', 'hostCount', ]), fallback.hosts ) ); setText( 'managerGrowth', formatPercent( firstNestedValue(sources, [ 'growth', 'growth_rate', 'growthRate', 'salary_growth', 'salaryGrowth', 'month_growth', 'monthGrowth', ]), fallback.growth ) ); setText( 'managerWallet', formatMoney( firstNestedValue(sources, [ 'wallet', 'wallet_balance', 'walletBalance', 'balance', ]), fallback.wallet ) ); setText( 'managerBankCards', textValue( firstNestedValue(sources, [ 'bank_cards', 'bankCards', 'bank_card_count', 'bankCardCount', ]), fallback.bankCards ) ); } function formatDateTime(ms) { if (!ms) return 'Never'; var date = new Date(Number(ms)); var year = date.getFullYear(); var month = String(date.getMonth() + 1).padStart(2, '0'); var day = String(date.getDate()).padStart(2, '0'); var hour = String(date.getHours()).padStart(2, '0'); var minute = String(date.getMinutes()).padStart(2, '0'); return year + '-' + month + '-' + day + ' ' + hour + ':' + minute; } function setPageLoading(loading) { // 首屏加载态用页面级骨架屏整层覆盖(common/skeleton.js),数据就绪后整层淡出; // is-loading 状态类保留给页面内其他降级样式使用。 pageState.loading = loading; var container = root(); if (container) container.classList.toggle('is-loading', loading); if (!window.HyAppSkeleton) return; if (loading) { window.HyAppSkeleton.show(); } else { window.HyAppSkeleton.hide(); } } function setDenied(denied) { pageState.denied = denied; var container = root(); if (!container) return; container.classList.toggle('is-denied', denied); } function normalizeUser(item) { var vip = item.current_vip || item.currentVip || item.vip || null; return { user_id: item.user_id || item.userId || item.target_user_id || '', display_user_id: item.display_user_id || item.displayUserId || item.target_display_user_id || '', username: item.username || item.name || item.target_username || '', avatar: item.avatar || item.target_avatar || '', country: item.country || '', levels: item.levels || {}, current_vip: vip ? { level: Number(vip.level) || 0, name: vip.name || '', active: Boolean(vip.active), started_at_ms: vip.started_at_ms || vip.startedAtMs || 0, expires_at_ms: vip.expires_at_ms || vip.expiresAtMs || 0, updated_at_ms: vip.updated_at_ms || vip.updatedAtMs || 0, } : null, }; } function normalizeBlock(item) { return { block_id: item.block_id || item.blockId || '', target_user_id: item.target_user_id || item.targetUserId || '', target_display_user_id: item.target_display_user_id || item.targetDisplayUserId || '', target_username: item.target_username || item.targetUsername || '', target_avatar: item.target_avatar || item.targetAvatar || '', country: item.country || '', blocked_until_ms: item.blocked_until_ms || item.blockedUntilMs || item.unblockAtMs || 0, status: item.status || 'active', }; } function normalizePermissions(raw) { raw = raw || {}; return { canGrantAvatarFrame: raw.canGrantAvatarFrame !== undefined ? Boolean(raw.canGrantAvatarFrame) : raw.can_grant_avatar_frame !== undefined ? Boolean(raw.can_grant_avatar_frame) : true, canGrantVehicle: raw.canGrantVehicle !== undefined ? Boolean(raw.canGrantVehicle) : raw.can_grant_vehicle !== undefined ? Boolean(raw.can_grant_vehicle) : true, canGrantBadge: raw.canGrantBadge !== undefined ? Boolean(raw.canGrantBadge) : raw.can_grant_badge !== undefined ? Boolean(raw.can_grant_badge) : true, canGrantVip: raw.canGrantVip !== undefined ? Boolean(raw.canGrantVip) : raw.can_grant_vip !== undefined ? Boolean(raw.can_grant_vip) : true, canUpdateUserLevel: raw.canUpdateUserLevel !== undefined ? Boolean(raw.canUpdateUserLevel) : raw.can_update_user_level !== undefined ? Boolean(raw.can_update_user_level) : true, canAddBdLeader: raw.canAddBdLeader !== undefined ? Boolean(raw.canAddBdLeader) : raw.can_add_bd_leader !== undefined ? Boolean(raw.can_add_bd_leader) : true, canAddAdmin: raw.canAddAdmin !== undefined ? Boolean(raw.canAddAdmin) : raw.can_add_admin !== undefined ? Boolean(raw.can_add_admin) : true, canAddSuperadmin: raw.canAddSuperadmin !== undefined ? Boolean(raw.canAddSuperadmin) : raw.can_add_superadmin !== undefined ? Boolean(raw.can_add_superadmin) : true, canBlockUser: raw.canBlockUser !== undefined ? Boolean(raw.canBlockUser) : raw.can_block_user !== undefined ? Boolean(raw.can_block_user) : true, canTransferUserCountry: raw.canTransferUserCountry !== undefined ? Boolean(raw.canTransferUserCountry) : raw.can_transfer_user_country !== undefined ? Boolean(raw.can_transfer_user_country) : true, }; } function canUseAction(action) { var key = ACTION_PERMISSION_KEYS[action]; if (!key) return true; return pageState.permissions[key] !== false; } function renderActionPermissions() { document.querySelectorAll('[data-action]').forEach(function (button) { var action = button.getAttribute('data-action'); button.hidden = !canUseAction(action); }); } function ensureActionAllowed(action) { if (canUseAction(action)) return true; toast('Permission denied.'); return false; } function apiSearchUser(keyword, scope, action) { if (!api || !api.searchUsers) { return Promise.reject(new Error('API not available.')); } return api .searchUsers(keyword, 20, scope, action) .then(function (data) { var items = (data && data.items) || []; return items.map(normalizeUser); }); } function loadOverview() { if (!api || !api.overview) { // 无 API 注入(本地纯静态预览等)时立即渲染一次,把骨架屏收敛成中性占位符。 pageState.permissions = normalizePermissions({}); renderDashboard(); renderActionPermissions(); setDenied(false); return Promise.resolve(); } setPageLoading(true); return api .overview() .then(function (data) { pageState.overview = data || {}; pageState.permissions = normalizePermissions( pageState.overview.permissions || pageState.overview ); renderDashboard(); renderActionPermissions(); setDenied(false); return canUseAction('block-user') ? refreshBlocks() : Promise.resolve(); }) .catch(function (error) { if (isLocalPreviewError(error)) { pageState.permissions = normalizePermissions({}); renderDashboard(); renderActionPermissions(); setDenied(false); return; } // 接口失败也要渲染一次:把骨架屏收敛成中性占位符,页面不残留加载态。 setDenied(true); renderDashboard(); showError(error); }) .finally(function () { setPageLoading(false); }); } function refreshBlocks() { if (!api || !api.listBlocks) return Promise.resolve(); return api .listBlocks('active', 50) .then(function (data) { blockState.blockedUsers = ((data && data.items) || []).map( normalizeBlock ); renderBlockedList(); }) .catch(showError); } function goBack() { if (window.HyAppBridge && window.HyAppBridge.back) { window.HyAppBridge.back(); return; } if (window.history.length > 1) { window.history.back(); } } function renderUserRow(user, actionText, onAction) { var row = createNode('div', 'searched-user'); var avatar = createNode('div', 'searched-avatar'); setAvatar(avatar, user); var copy = createNode('div', 'blocked-row-main'); copy.appendChild(createNode('div', 'searched-name', userName(user))); copy.appendChild( createNode('div', 'searched-meta', 'UID: ' + userDisplayId(user)) ); if (currentVip(user)) { copy.appendChild( createNode('div', 'searched-vip', currentVipStatusText(user)) ); } var button = createNode('button', 'user-block-button', actionText); button.type = 'button'; button.addEventListener('click', onAction); row.appendChild(avatar); row.appendChild(copy); row.appendChild(button); return row; } function renderSearchResult() { var container = $('blockSearchResult'); if (!container) return; container.innerHTML = ''; if (!blockState.searchedUser) return; container.appendChild( renderUserRow(blockState.searchedUser, 'Block', function () { selectDuration('86400000', '1 day'); var panel = $('blockDurationPanel'); if (panel) panel.hidden = false; }) ); } function setupLevelGrid() { var grid = $('levelGrid'); if (!grid || grid.children.length) return; for (var level = 1; level <= 11; level += 1) { var button = createNode('button', '', 'Lv. ' + level); button.type = 'button'; button.setAttribute('data-level', String(level)); grid.appendChild(button); } } function ensureTrackSelector() { var panel = $('levelPanel'); if (!panel || $('levelTrackGrid')) return; var title = createNode( 'div', 'block-section-title', 'Select level type' ); var grid = createNode('div', 'track-grid'); grid.id = 'levelTrackGrid'; Object.keys(TRACK_LABELS).forEach(function (track) { var button = createNode('button', '', TRACK_LABELS[track]); button.type = 'button'; button.setAttribute('data-track', track); button.addEventListener('click', function () { levelState.selectedTrack = track; renderLevelButtons(); renderLevelSearchResult(); }); grid.appendChild(button); }); panel.insertBefore(grid, panel.firstChild); panel.insertBefore(title, grid); } function currentTrackLevel(user) { var track = levelState.selectedTrack; var levels = (user && user.levels) || {}; return levels[track] && levels[track].level ? levels[track].level : 0; } function renderLevelSearchResult() { var container = $('levelSearchResult'); if (!container) return; container.innerHTML = ''; if (!levelState.searchedUser) return; var user = levelState.searchedUser; var row = createNode('div', 'searched-user'); var avatar = createNode('div', 'searched-avatar'); setAvatar(avatar, user); var copy = createNode('div', 'blocked-row-main'); copy.appendChild(createNode('div', 'searched-name', userName(user))); copy.appendChild( createNode( 'div', 'searched-meta', 'UID: ' + userDisplayId(user) + ' · ' + TRACK_LABELS[levelState.selectedTrack] + ' Lv. ' + currentTrackLevel(user) ) ); var badge = createNode( 'div', 'block-list-count', 'Lv. ' + currentTrackLevel(user) ); row.appendChild(avatar); row.appendChild(copy); row.appendChild(badge); container.appendChild(row); } function renderLevelButtons() { var trackButtons = document.querySelectorAll( '#levelTrackGrid [data-track]' ); trackButtons.forEach(function (button) { button.classList.toggle( 'is-selected', button.getAttribute('data-track') === levelState.selectedTrack ); }); var buttons = document.querySelectorAll('#levelGrid [data-level]'); buttons.forEach(function (button) { var isSelected = Number(button.getAttribute('data-level')) === levelState.selectedLevel; button.classList.toggle('is-selected', isSelected); }); var confirmButton = $('confirmLevelButton'); if (confirmButton) { confirmButton.disabled = !levelState.searchedUser || !levelState.selectedLevel || levelState.submitting; } } function selectLevel(level) { levelState.selectedLevel = Number(level) || 0; renderLevelButtons(); } function renderDurationButtons() { var buttons = document.querySelectorAll( '#durationGrid [data-duration-ms]' ); buttons.forEach(function (button) { var isSelected = Number(button.getAttribute('data-duration-ms')) === blockState.selectedDurationMs; button.classList.toggle('is-selected', isSelected); }); var confirmButton = $('confirmBlockButton'); if (confirmButton) { confirmButton.disabled = !blockState.searchedUser || blockState.submitting; } } function selectDuration(durationMs, label) { blockState.selectedDurationMs = Number(durationMs) || 0; blockState.selectedDurationLabel = label || 'Permanent'; renderDurationButtons(); } function renderBlockedList() { var container = $('blockedList'); var count = $('blockListCount'); if (!container) return; container.innerHTML = ''; if (count) count.textContent = String(blockState.blockedUsers.length); if (!blockState.blockedUsers.length) { container.appendChild( createNode('div', 'blocked-empty', 'No blocked users.') ); return; } blockState.blockedUsers.forEach(function (user) { var row = createNode('div', 'blocked-row'); var avatar = createNode('div', 'blocked-avatar'); setAvatar(avatar, user); var copy = createNode('div', 'blocked-row-main'); copy.appendChild(createNode('div', 'blocked-name', userName(user))); copy.appendChild( createNode('div', 'blocked-meta', 'UID: ' + userDisplayId(user)) ); copy.appendChild( createNode( 'div', 'blocked-time', 'Unblock time: ' + formatDateTime(user.blocked_until_ms) ) ); var unblockButton = createNode( 'button', 'user-unblock-button', 'Unblock' ); unblockButton.type = 'button'; unblockButton.addEventListener('click', function () { unblockUser(user.block_id); }); row.appendChild(avatar); row.appendChild(copy); row.appendChild(unblockButton); container.appendChild(row); }); } function unblockUser(blockId) { if (!blockId || blockState.submitting) return; blockState.submitting = true; api.unblockUser(blockId, { reason: 'manager_center' }) .then(function () { toast('User unblocked.'); return refreshBlocks(); }) .catch(showError) .finally(function () { blockState.submitting = false; }); } function resetBlockSearch() { var input = $('blockSearchInput'); var panel = $('blockDurationPanel'); blockState.searchedUser = null; blockState.selectedDurationMs = 0; blockState.selectedDurationLabel = ''; if (input) input.value = ''; if (panel) panel.hidden = true; renderSearchResult(); renderDurationButtons(); } function openBlockModal() { if (pageState.denied) { toast('Permission denied.'); return; } if (!ensureActionAllowed('block-user')) return; var modal = $('blockModal'); if (!modal) return; resetBlockSearch(); refreshBlocks(); modal.hidden = false; modal.setAttribute('aria-hidden', 'false'); setTimeout(function () { var input = $('blockSearchInput'); if (input) input.focus(); }, 30); } function closeBlockModal() { var modal = $('blockModal'); if (!modal) return; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); } function resetLevelSearch() { var input = $('levelSearchInput'); var panel = $('levelPanel'); levelState.searchedUser = null; levelState.selectedTrack = 'wealth'; levelState.selectedLevel = 0; if (input) input.value = ''; if (panel) panel.hidden = true; renderLevelSearchResult(); renderLevelButtons(); } function openLevelModal() { if (pageState.denied) { toast('Permission denied.'); return; } if (!ensureActionAllowed('update-user-level')) return; var modal = $('levelModal'); if (!modal) return; resetLevelSearch(); modal.hidden = false; modal.setAttribute('aria-hidden', 'false'); setTimeout(function () { var input = $('levelSearchInput'); if (input) input.focus(); }, 30); } function closeLevelModal() { var modal = $('levelModal'); if (!modal) return; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); } function searchBlockUser() { var input = $('blockSearchInput'); var value = input ? input.value.trim() : ''; var panel = $('blockDurationPanel'); if (!value) { toast('Enter user ID.'); return; } // 封禁搜索只校验 Block User 权限;不能借用赠送弹窗状态,否则会按资源/VIP 赠送权限误判。 apiSearchUser(value, '', 'block-user') .then(function (items) { if (!items.length) { blockState.searchedUser = null; toast('No user found.'); return; } blockState.searchedUser = items[0]; blockState.selectedDurationMs = 0; blockState.selectedDurationLabel = ''; if (panel) panel.hidden = true; renderSearchResult(); renderDurationButtons(); }) .catch(showError); } function confirmBlockUser() { if (!blockState.searchedUser) { toast('Search a user first.'); return; } if (blockState.submitting) return; var durationMs = blockState.selectedDurationMs || PERMANENT_BLOCK_MS; blockState.submitting = true; renderDurationButtons(); api.blockUser({ command_id: commandId('manager-block'), target_user_id: userId(blockState.searchedUser), blocked_until_ms: Date.now() + durationMs, reason: 'manager_center', }) .then(function () { toast('User blocked.'); resetBlockSearch(); return refreshBlocks(); }) .catch(showError) .finally(function () { blockState.submitting = false; renderDurationButtons(); }); } function searchLevelUser() { var input = $('levelSearchInput'); var value = input ? input.value.trim() : ''; var panel = $('levelPanel'); if (!value) { toast('Enter user ID.'); return; } // 等级调整搜索按自己的能力开关校验,避免资源赠送关闭时误挡等级操作。 apiSearchUser(value, '', 'update-user-level') .then(function (items) { if (!items.length) { levelState.searchedUser = null; toast('No user found.'); return; } levelState.searchedUser = items[0]; levelState.selectedLevel = 0; if (panel) panel.hidden = false; renderLevelSearchResult(); renderLevelButtons(); }) .catch(showError); } function confirmLevelUpdate() { if (!levelState.searchedUser) { toast('Search a user first.'); return; } if (!levelState.selectedLevel) { toast('Select a user level.'); return; } if (levelState.submitting) return; levelState.submitting = true; renderLevelButtons(); api.updateLevel({ command_id: commandId('manager-level'), target_user_id: userId(levelState.searchedUser), track: levelState.selectedTrack, level: levelState.selectedLevel, reason: 'manager_center', }) .then(function () { toast('User level updated.'); closeLevelModal(); }) .catch(showError) .finally(function () { levelState.submitting = false; renderLevelButtons(); }); } function ensureGrantModal() { if ($('grantModal')) return; var modal = createNode('div', 'block-modal grant-modal'); modal.id = 'grantModal'; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = '' + '' + 'Send' + '' + 'User ID' + 'Search' + '' + 'Select itemSelect durationSend now'; document.querySelector('.manager-center').appendChild(modal); $('grantBackdrop').addEventListener('click', closeGrantModal); $('grantCloseButton').addEventListener('click', closeGrantModal); $('grantSearchButton').addEventListener('click', searchGrantUser); $('grantSearchInput').addEventListener('keydown', function (event) { if (event.key === 'Enter') searchGrantUser(); }); $('confirmGrantButton').addEventListener('click', confirmGrant); } function openGrantModal(action) { if (pageState.denied) { toast('Permission denied.'); return; } if (!ensureActionAllowed(action)) return; ensureGrantModal(); grantState.action = action; grantState.resourceType = RESOURCE_TYPES[action] || ''; grantState.title = ACTION_LABELS[action] || 'Send'; grantState.searchedUser = null; grantState.selectedResourceIds = []; grantState.selectedResourceDurationMs = DEFAULT_RESOURCE_GRANT_DURATION_MS; grantState.selectedVipLevel = 0; $('grantDialogTitle').textContent = grantState.title; $('grantSearchInput').value = ''; $('grantSearchResult').innerHTML = ''; $('grantPanel').hidden = true; $('grantModal').hidden = false; $('grantModal').setAttribute('aria-hidden', 'false'); renderGrantOptions(); if (grantState.resourceType) loadGrantResources(grantState.resourceType); setTimeout(function () { $('grantSearchInput').focus(); }, 30); } function closeGrantModal() { var modal = $('grantModal'); if (!modal) return; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); } function loadGrantResources(resourceType) { if (pageState.resourcesByType[resourceType]) { renderGrantOptions(); return; } api.listResources(resourceType, 20) .then(function (data) { pageState.resourcesByType[resourceType] = (data && data.items) || []; renderGrantOptions(); }) .catch(showError); } function searchGrantUser() { var value = $('grantSearchInput').value.trim(); if (!value) { toast('Enter user ID.'); return; } // 搜索入口也必须携带当前赠送动作;Send VIP 只校验 VIP 赠送权限,不能落回资源赠送权限导致误拒。 apiSearchUser(value, '', grantState.action) .then(function (items) { if (!items.length) { grantState.searchedUser = null; toast('No user found.'); return; } grantState.searchedUser = items[0]; var container = $('grantSearchResult'); container.innerHTML = ''; container.appendChild( renderUserRow( grantState.searchedUser, 'Select', function () { $('grantPanel').hidden = false; renderGrantOptions(); } ) ); $('grantPanel').hidden = false; renderGrantOptions(); }) .catch(showError); } function renderGrantOptions() { var grid = $('grantOptionGrid'); if (!grid) return; grid.innerHTML = ''; grid.classList.toggle( 'is-resource-grid', grantState.action !== 'send-vip' ); var durationTitle = $('grantDurationTitle'); var durationGrid = $('grantDurationGrid'); if (durationTitle) { durationTitle.hidden = false; durationTitle.textContent = grantState.action === 'send-vip' ? 'Duration: 30 days' : 'Select duration'; } if (durationGrid) { durationGrid.innerHTML = ''; durationGrid.hidden = grantState.action === 'send-vip'; } if (grantState.action === 'send-vip') { $('grantOptionTitle').textContent = 'Select VIP level'; var activeVipLevel = currentVipLevel(grantState.searchedUser || {}); if ( grantState.selectedVipLevel && activeVipLevel > grantState.selectedVipLevel ) { grantState.selectedVipLevel = 0; } // VIP grants must stay on the explicit product allow-list so the UI never sends a level the backend has not opened. VIP_GRANT_LEVELS.forEach(function (level) { var button = createNode('button', '', 'VIP ' + level); var blockedByCurrentVip = activeVipLevel > level; button.type = 'button'; button.disabled = blockedByCurrentVip; if (blockedByCurrentVip) { button.title = 'Current VIP is higher'; } button.classList.toggle( 'is-selected', grantState.selectedVipLevel === level ); button.addEventListener('click', function () { if (blockedByCurrentVip) return; grantState.selectedVipLevel = level; renderGrantOptions(); }); grid.appendChild(button); }); if (activeVipLevel > 1) { grid.appendChild( createNode( 'div', 'vip-grant-note', activeVipLevel > Math.max.apply(null, VIP_GRANT_LEVELS) ? 'Current VIP is higher than manager-center gift levels.' : 'Lower VIP levels are disabled because the user already has a higher VIP.' ) ); } } else { $('grantOptionTitle').textContent = 'Select ' + resourceKindLabel(); var resources = pageState.resourcesByType[grantState.resourceType] || []; var selectedResourceSet = grantState.selectedResourceIds.reduce( function (result, resourceId) { result[String(resourceId)] = true; return result; }, {} ); if (!resources.length) { grid.appendChild( createNode('div', 'blocked-empty', 'No resource available.') ); } resources.forEach(function (resource) { var button = createNode('button', 'grant-resource-option'); var imageURL = resourceImageUrl(resource); var resourceID = resource.resource_id; var resourceKey = String(resourceID); button.type = 'button'; button.setAttribute( 'aria-label', 'Select ' + resourceKindLabel() ); button.classList.toggle( 'is-selected', !!selectedResourceSet[resourceKey] ); if (imageURL) { var media = createNode('span', 'grant-resource-media'); var img = document.createElement('img'); img.src = imageURL; img.alt = ''; media.appendChild(img); button.appendChild(media); } else { button.appendChild( createNode('span', 'grant-resource-fallback') ); } button.addEventListener('click', function () { // 资源赠送是素材发放,不是互斥装备选择;同一弹窗允许一次选择多个素材,由后端拆成独立 grant 命令保证可重试。 if (selectedResourceSet[resourceKey]) { grantState.selectedResourceIds = grantState.selectedResourceIds.filter( function (selectedID) { return String(selectedID) !== resourceKey; } ); } else { grantState.selectedResourceIds = grantState.selectedResourceIds.concat(resourceID); } renderGrantOptions(); }); grid.appendChild(button); }); if (grantState.selectedResourceIds.length) { grid.appendChild( createNode( 'div', 'resource-selected-note', grantState.selectedResourceIds.length + ' selected' ) ); } if (durationGrid) { RESOURCE_GRANT_DURATIONS.forEach(function (item) { var durationButton = createNode('button', '', item.label); durationButton.type = 'button'; durationButton.classList.toggle( 'is-selected', grantState.selectedResourceDurationMs === item.ms ); durationButton.addEventListener('click', function () { grantState.selectedResourceDurationMs = item.ms; renderGrantOptions(); }); durationGrid.appendChild(durationButton); }); } } var confirm = $('confirmGrantButton'); if (confirm) { confirm.disabled = !grantState.searchedUser || grantState.submitting || (grantState.action === 'send-vip' ? !grantState.selectedVipLevel || currentVipLevel(grantState.searchedUser) > grantState.selectedVipLevel : !grantState.selectedResourceIds.length || !grantState.selectedResourceDurationMs); } } function confirmGrant() { if (!grantState.searchedUser || grantState.submitting) return; grantState.submitting = true; renderGrantOptions(); var payload = { command_id: commandId('manager-grant'), target_user_id: userId(grantState.searchedUser), reason: 'manager_center', }; var request; if (grantState.action === 'send-vip') { payload.level = grantState.selectedVipLevel; request = api.grantVip(payload); } else { payload.resource_ids = grantState.selectedResourceIds.slice(); payload.duration_ms = grantState.selectedResourceDurationMs; request = api.grantResource(payload); } request .then(function () { toast('Sent successfully.'); closeGrantModal(); }) .catch(showError) .finally(function () { grantState.submitting = false; renderGrantOptions(); }); } function ensureBDLeaderModal() { if ($('bdLeaderModal')) return; var modal = createNode('div', 'block-modal bd-leader-modal'); modal.id = 'bdLeaderModal'; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = '' + '' + 'Add BD Leader' + '' + 'User ID' + 'Search' + '' + 'Add this user as BD Leader' + 'Add BD Leader'; document.querySelector('.manager-center').appendChild(modal); $('bdLeaderBackdrop').addEventListener('click', closeBDLeaderModal); $('bdLeaderCloseButton').addEventListener('click', closeBDLeaderModal); $('bdLeaderSearchButton').addEventListener('click', searchBDLeaderUser); $('bdLeaderSearchInput').addEventListener('keydown', function (event) { if (event.key === 'Enter') searchBDLeaderUser(); }); $('confirmBDLeaderButton').addEventListener('click', confirmBDLeader); } function openBDLeaderModal(action) { if (pageState.denied) { toast('Permission denied.'); return; } if (!ensureActionAllowed(action)) return; ensureBDLeaderModal(); bdLeaderState.action = action; bdLeaderState.title = ACTION_LABELS[action] || 'Add BD Leader'; bdLeaderState.searchedUser = null; bdLeaderState.submitting = false; $('bdLeaderDialogTitle').textContent = bdLeaderState.title; $('confirmBDLeaderButton').textContent = bdLeaderState.title; $('bdLeaderSearchInput').value = ''; $('bdLeaderSearchResult').innerHTML = ''; $('bdLeaderPanel').hidden = true; renderBDLeaderActions(); $('bdLeaderModal').hidden = false; $('bdLeaderModal').setAttribute('aria-hidden', 'false'); setTimeout(function () { $('bdLeaderSearchInput').focus(); }, 30); } function closeBDLeaderModal() { var modal = $('bdLeaderModal'); if (!modal) return; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); } function searchBDLeaderUser() { var value = $('bdLeaderSearchInput').value.trim(); if (!value) { toast('Enter user ID.'); return; } // BD/Admin/Superadmin 共用弹窗,搜索阶段先按当前按钮的能力开关过滤;写入口仍会二次校验真实落库能力。 apiSearchUser(value, '', bdLeaderState.action) .then(function (items) { if (!items.length) { bdLeaderState.searchedUser = null; $('bdLeaderSearchResult').innerHTML = ''; $('bdLeaderPanel').hidden = true; renderBDLeaderActions(); toast('No user found.'); return; } bdLeaderState.searchedUser = items[0]; var container = $('bdLeaderSearchResult'); container.innerHTML = ''; container.appendChild( renderUserRow( bdLeaderState.searchedUser, 'Select', function () { $('bdLeaderPanel').hidden = false; renderBDLeaderActions(); } ) ); $('bdLeaderPanel').hidden = false; renderBDLeaderActions(); }) .catch(showError); } function renderBDLeaderActions() { var confirm = $('confirmBDLeaderButton'); if (!confirm) return; confirm.disabled = !bdLeaderState.searchedUser || bdLeaderState.submitting; } function confirmBDLeader() { if (!bdLeaderState.searchedUser) { toast('Search a user first.'); return; } if (!api || !api.createBDLeader) { toast('API not available.'); return; } if (bdLeaderState.submitting) return; bdLeaderState.submitting = true; renderBDLeaderActions(); api.createBDLeader({ command_id: commandId('manager-bd-leader'), target_user_id: userId(bdLeaderState.searchedUser), reason: 'manager_center', }) .then(function () { toast('BD Leader added.'); closeBDLeaderModal(); }) .catch(showError) .finally(function () { bdLeaderState.submitting = false; renderBDLeaderActions(); }); } function normalizeCountry(item) { item = item || {}; var code = String( item.country_code || item.countryCode || item.code || item.alphaTwo || item.alpha_two || '' ) .trim() .toUpperCase(); return { code: code, name: item.country_display_name || item.countryDisplayName || item.country_name || item.countryName || item.name || code, }; } function countryLabel(country) { if (!country) return ''; return country.name && country.name !== country.code ? country.name + ' · ' + country.code : country.code; } function selectedCountry() { for (var i = 0; i < pageState.countries.length; i += 1) { if ( pageState.countries[i].code === countryState.selectedCountryCode ) { return pageState.countries[i]; } } return null; } function loadCountries() { if (pageState.countries.length) { renderCountryOptions(); return Promise.resolve(pageState.countries); } if (!api || !api.listCountries) { return Promise.reject(new Error('API not available.')); } return api .listCountries() .then(function (data) { var list = (data && (data.countries || data.items)) || []; pageState.countries = list .map(normalizeCountry) .filter(function (country) { return !!country.code; }); renderCountryOptions(); return pageState.countries; }) .catch(showError); } function ensureCountryModal() { if ($('countryModal')) return; var modal = createNode('div', 'block-modal country-modal'); modal.id = 'countryModal'; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = '' + '' + 'Transfer User Country' + '' + 'User ID' + 'Search' + '' + 'Select countryConfirm Transfer'; document.querySelector('.manager-center').appendChild(modal); $('countryBackdrop').addEventListener('click', closeCountryModal); $('countryCloseButton').addEventListener('click', closeCountryModal); $('countrySearchButton').addEventListener('click', searchCountryUser); $('countrySearchInput').addEventListener('keydown', function (event) { if (event.key === 'Enter') searchCountryUser(); }); $('confirmCountryButton').addEventListener( 'click', confirmCountryTransfer ); } function openCountryModal() { if (pageState.denied) { toast('Permission denied.'); return; } if (!ensureActionAllowed('transfer-user-country')) return; ensureCountryModal(); countryState.searchedUser = null; countryState.selectedCountryCode = ''; countryState.submitting = false; $('countrySearchInput').value = ''; $('countrySearchResult').innerHTML = ''; $('countryPanel').hidden = true; $('countryModal').hidden = false; $('countryModal').setAttribute('aria-hidden', 'false'); loadCountries(); renderCountryActions(); setTimeout(function () { $('countrySearchInput').focus(); }, 30); } function closeCountryModal() { var modal = $('countryModal'); if (!modal) return; modal.hidden = true; modal.setAttribute('aria-hidden', 'true'); } function searchCountryUser() { var value = $('countrySearchInput').value.trim(); if (!value) { toast('Enter user ID.'); return; } // 转国家是唯一允许跨国家搜索的入口,必须同时携带 scope=all 和动作,避免额外依赖资源赠送权限。 apiSearchUser(value, 'all', 'transfer-user-country') .then(function (items) { if (!items.length) { countryState.searchedUser = null; $('countrySearchResult').innerHTML = ''; $('countryPanel').hidden = true; renderCountryActions(); toast('No user found.'); return; } countryState.searchedUser = items[0]; var container = $('countrySearchResult'); container.innerHTML = ''; container.appendChild( renderUserRow( countryState.searchedUser, 'Select', function () { $('countryPanel').hidden = false; renderCountryActions(); } ) ); $('countryPanel').hidden = false; renderCountryActions(); }) .catch(showError); } function renderCountryOptions() { var grid = $('countryOptionGrid'); if (!grid) return; grid.innerHTML = ''; if (!pageState.countries.length) { grid.appendChild( createNode('div', 'blocked-empty', 'No country available.') ); renderCountryActions(); return; } pageState.countries.forEach(function (country) { var button = createNode('button', 'country-option'); button.type = 'button'; button.classList.toggle( 'is-selected', countryState.selectedCountryCode === country.code ); button.appendChild(createNode('span', '', countryLabel(country))); button.addEventListener('click', function () { countryState.selectedCountryCode = country.code; renderCountryOptions(); renderCountryActions(); }); grid.appendChild(button); }); renderCountryActions(); } function renderCountryActions() { var confirm = $('confirmCountryButton'); if (!confirm) return; var currentCountry = String( (countryState.searchedUser && countryState.searchedUser.country) || '' ).toUpperCase(); confirm.disabled = !countryState.searchedUser || !countryState.selectedCountryCode || countryState.selectedCountryCode === currentCountry || countryState.submitting; var country = selectedCountry(); confirm.textContent = country ? 'Transfer to ' + countryLabel(country) : 'Confirm Transfer'; } function confirmCountryTransfer() { if (!countryState.searchedUser) { toast('Search a user first.'); return; } if (!countryState.selectedCountryCode) { toast('Select a country.'); return; } if (countryState.submitting) return; countryState.submitting = true; renderCountryActions(); api.transferUserCountry({ command_id: commandId('manager-country'), target_user_id: userId(countryState.searchedUser), country: countryState.selectedCountryCode, reason: 'manager_center_country_transfer', }) .then(function () { toast('User country transferred.'); closeCountryModal(); }) .catch(showError) .finally(function () { countryState.submitting = false; renderCountryActions(); }); } function copyManagerId() { var value = $('managerDisplayId') ? $('managerDisplayId').textContent.trim() : ''; if (!value) return; if (window.navigator && window.navigator.clipboard) { window.navigator.clipboard .writeText(value) .then(function () { toast('Copied.'); }) .catch(function () { toast(value); }); return; } toast(value); } function bindEvents() { var backButton = $('backButton'); if (backButton) backButton.addEventListener('click', goBack); var copyButton = document.querySelector('.manager-copy-button'); if (copyButton) copyButton.addEventListener('click', copyManagerId); document.querySelectorAll('[data-action]').forEach(function (button) { button.addEventListener('click', function () { var action = button.getAttribute('data-action'); if (!ensureActionAllowed(action)) { return; } if (action === 'block-user') { openBlockModal(); return; } if (action === 'update-user-level') { openLevelModal(); return; } if (action === 'transfer-user-country') { openCountryModal(); return; } if ( action === 'add-bd-leader' || action === 'add-admin' || action === 'add-superadmin' ) { openBDLeaderModal(action); return; } openGrantModal(action); }); }); setupLevelGrid(); ensureTrackSelector(); var closeButton = $('blockCloseButton'); var backdrop = $('blockBackdrop'); var searchButton = $('blockSearchButton'); var searchInput = $('blockSearchInput'); var confirmButton = $('confirmBlockButton'); var levelCloseButton = $('levelCloseButton'); var levelBackdrop = $('levelBackdrop'); var levelSearchButton = $('levelSearchButton'); var levelSearchInput = $('levelSearchInput'); var confirmLevelButton = $('confirmLevelButton'); if (closeButton) closeButton.addEventListener('click', closeBlockModal); if (backdrop) backdrop.addEventListener('click', closeBlockModal); if (levelCloseButton) levelCloseButton.addEventListener('click', closeLevelModal); if (levelBackdrop) levelBackdrop.addEventListener('click', closeLevelModal); if (searchButton) searchButton.addEventListener('click', searchBlockUser); if (searchInput) { searchInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') searchBlockUser(); }); } if (levelSearchButton) levelSearchButton.addEventListener('click', searchLevelUser); if (levelSearchInput) { levelSearchInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') searchLevelUser(); }); } if (confirmButton) confirmButton.addEventListener('click', confirmBlockUser); if (confirmLevelButton) { confirmLevelButton.addEventListener('click', confirmLevelUpdate); } document .querySelectorAll('#durationGrid [data-duration-ms]') .forEach(function (button) { button.addEventListener('click', function () { selectDuration( button.getAttribute('data-duration-ms'), button.getAttribute('data-duration-label') ); }); }); document .querySelectorAll('#levelGrid [data-level]') .forEach(function (button) { button.addEventListener('click', function () { selectLevel(button.getAttribute('data-level')); }); }); } function init() { bindEvents(); renderBlockedList(); // 首屏不预渲染仪表盘:骨架屏保持到 loadOverview 的任一分支调用 renderDashboard 收敛。 loadOverview(); if (window.HyAppBridge && window.HyAppBridge.ready) { window.HyAppBridge.ready({ page: 'manager-center' }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();