1886 lines
68 KiB
JavaScript
1886 lines
68 KiB
JavaScript
(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 =
|
||
'<button class="block-backdrop" id="grantBackdrop" type="button" aria-label="Close"></button>' +
|
||
'<section class="block-dialog" role="dialog" aria-modal="true" aria-labelledby="grantDialogTitle">' +
|
||
'<div class="block-dialog-header"><h2 id="grantDialogTitle">Send</h2>' +
|
||
'<button class="block-close" id="grantCloseButton" type="button" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>' +
|
||
'<div class="block-dialog-body"><div class="block-field"><label for="grantSearchInput">User ID</label>' +
|
||
'<div class="block-search-row"><input id="grantSearchInput" type="text" inputmode="numeric" autocomplete="off" placeholder="Enter user ID"/><button id="grantSearchButton" type="button">Search</button></div></div>' +
|
||
'<div class="block-search-result" id="grantSearchResult" aria-live="polite"></div>' +
|
||
'<div class="level-panel" id="grantPanel" hidden><div class="block-section-title" id="grantOptionTitle">Select item</div><div class="grant-option-grid" id="grantOptionGrid"></div><div class="block-section-title" id="grantDurationTitle" hidden>Select duration</div><div class="duration-grid grant-duration-grid" id="grantDurationGrid" hidden></div><button class="confirm-level-button" id="confirmGrantButton" type="button" disabled>Send now</button></div></div></section>';
|
||
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 =
|
||
'<button class="block-backdrop" id="bdLeaderBackdrop" type="button" aria-label="Close"></button>' +
|
||
'<section class="block-dialog" role="dialog" aria-modal="true" aria-labelledby="bdLeaderDialogTitle">' +
|
||
'<div class="block-dialog-header"><h2 id="bdLeaderDialogTitle">Add BD Leader</h2>' +
|
||
'<button class="block-close" id="bdLeaderCloseButton" type="button" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>' +
|
||
'<div class="block-dialog-body"><div class="block-field"><label for="bdLeaderSearchInput">User ID</label>' +
|
||
'<div class="block-search-row"><input id="bdLeaderSearchInput" type="text" inputmode="numeric" autocomplete="off" placeholder="Enter user ID"/><button id="bdLeaderSearchButton" type="button">Search</button></div></div>' +
|
||
'<div class="block-search-result" id="bdLeaderSearchResult" aria-live="polite"></div>' +
|
||
'<div class="level-panel" id="bdLeaderPanel" hidden><div class="block-section-title">Add this user as BD Leader</div>' +
|
||
'<button class="confirm-level-button" id="confirmBDLeaderButton" type="button" disabled>Add BD Leader</button></div></div></section>';
|
||
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 =
|
||
'<button class="block-backdrop" id="countryBackdrop" type="button" aria-label="Close"></button>' +
|
||
'<section class="block-dialog" role="dialog" aria-modal="true" aria-labelledby="countryDialogTitle">' +
|
||
'<div class="block-dialog-header"><h2 id="countryDialogTitle">Transfer User Country</h2>' +
|
||
'<button class="block-close" id="countryCloseButton" type="button" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>' +
|
||
'<div class="block-dialog-body"><div class="block-field"><label for="countrySearchInput">User ID</label>' +
|
||
'<div class="block-search-row"><input id="countrySearchInput" type="text" inputmode="numeric" autocomplete="off" placeholder="Enter user ID"/><button id="countrySearchButton" type="button">Search</button></div></div>' +
|
||
'<div class="block-search-result" id="countrySearchResult" aria-live="polite"></div>' +
|
||
'<div class="level-panel" id="countryPanel" hidden><div class="block-section-title">Select country</div><div class="grant-option-grid" id="countryOptionGrid"></div><button class="confirm-level-button" id="confirmCountryButton" type="button" disabled>Confirm Transfer</button></div></div></section>';
|
||
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();
|
||
}
|
||
})();
|