986 lines
33 KiB
JavaScript
986 lines
33 KiB
JavaScript
(function () {
|
||
const supportedLanguages = ["en", "ar", "tr", "id"];
|
||
const languageLabels = {
|
||
en: "EN",
|
||
ar: "AR",
|
||
tr: "TR",
|
||
id: "ID"
|
||
};
|
||
const API_BASE_URL = "https://jvapi.haiyihy.com";
|
||
const GO_API_PATH_PREFIX = "/go";
|
||
const LOCAL_GO_API_BASE_URL = "http://127.0.0.1:1100";
|
||
const defaultPropsType = "RIDE";
|
||
|
||
const fallbackMessages = {
|
||
en: {
|
||
document_title: "Manager Center",
|
||
page_label: "Manager Center",
|
||
back: "Back",
|
||
title: "Manager Center",
|
||
language_button_aria: "Change language",
|
||
close: "Close",
|
||
current_manager: "Current Manager",
|
||
user: "User",
|
||
user_search_placeholder: "Enter user ID or account",
|
||
search: "Search",
|
||
searching: "Searching...",
|
||
target_user: "Target User",
|
||
gift_props: "Gift Props",
|
||
gift_type: "Gift Type",
|
||
prop_type_ride: "Ride",
|
||
prop_type_noble_vip: "Noble VIP",
|
||
prop_type_avatar_frame: "Avatar Frame",
|
||
prop_type_badge: "Badge",
|
||
prop_type_chat_bubble: "Chat Bubble",
|
||
prop_type_data_card: "Profile Card",
|
||
no_giftable_props: "No giftable props",
|
||
send: "Send",
|
||
sending: "Sending...",
|
||
send_success: "Sent successfully",
|
||
ban_user: "Ban User",
|
||
ban_reason: "Ban reason",
|
||
ban: "Ban",
|
||
submitting: "Submitting...",
|
||
ban_success: "User banned",
|
||
unban_user: "Unban User",
|
||
unban_reason: "Unban reason",
|
||
unban: "Unban",
|
||
unban_success: "User unbanned",
|
||
can_unban: "Can unban",
|
||
cannot_unban: "Cannot unban",
|
||
loading: "Loading...",
|
||
uid_label: "UID",
|
||
days: "{count} days",
|
||
recharged: "Recharged",
|
||
not_recharged: "Not recharged",
|
||
recharge_unknown: "Recharge unknown",
|
||
can_ban: "Can ban",
|
||
cannot_ban: "Cannot ban",
|
||
missing_token: "Missing token",
|
||
not_manager: "Current account is not a manager",
|
||
user_not_found: "User not found",
|
||
user_recharged: "This user has recharge records and cannot be banned",
|
||
user_already_banned: "This user is already banned",
|
||
user_not_banned: "This user is not banned",
|
||
prop_not_giftable: "This prop is not available for manager gifting",
|
||
vip_not_giftable: "This noble prop cannot be gifted",
|
||
request_failed: "Request failed",
|
||
enter_user: "Enter user ID or account"
|
||
},
|
||
ar: {
|
||
document_title: "مركز المدير",
|
||
page_label: "مركز المدير",
|
||
back: "رجوع",
|
||
title: "مركز المدير",
|
||
language_button_aria: "تغيير اللغة",
|
||
close: "إغلاق",
|
||
current_manager: "المدير الحالي",
|
||
user: "المستخدم",
|
||
user_search_placeholder: "أدخل معرّف المستخدم أو الحساب",
|
||
search: "بحث",
|
||
searching: "جار البحث...",
|
||
target_user: "المستخدم المستهدف",
|
||
gift_props: "إرسال العناصر",
|
||
gift_type: "نوع الهدية",
|
||
prop_type_ride: "مركبة",
|
||
prop_type_noble_vip: "VIP نبيل",
|
||
prop_type_avatar_frame: "إطار الصورة",
|
||
prop_type_badge: "شارة",
|
||
prop_type_chat_bubble: "فقاعة الدردشة",
|
||
prop_type_data_card: "بطاقة الملف",
|
||
no_giftable_props: "لا توجد عناصر قابلة للإهداء",
|
||
send: "إرسال",
|
||
sending: "جار الإرسال...",
|
||
send_success: "تم الإرسال بنجاح",
|
||
ban_user: "حظر المستخدم",
|
||
ban_reason: "سبب الحظر",
|
||
ban: "حظر",
|
||
submitting: "جار الإرسال...",
|
||
ban_success: "تم حظر المستخدم",
|
||
unban_user: "إلغاء حظر المستخدم",
|
||
unban_reason: "سبب إلغاء الحظر",
|
||
unban: "إلغاء الحظر",
|
||
unban_success: "تم إلغاء حظر المستخدم",
|
||
can_unban: "يمكن إلغاء الحظر",
|
||
cannot_unban: "لا يمكن إلغاء الحظر",
|
||
loading: "جار التحميل...",
|
||
uid_label: "UID",
|
||
days: "{count} يوم",
|
||
recharged: "تم الشحن",
|
||
not_recharged: "لم يتم الشحن",
|
||
recharge_unknown: "حالة الشحن غير معروفة",
|
||
can_ban: "يمكن الحظر",
|
||
cannot_ban: "لا يمكن الحظر",
|
||
missing_token: "رمز الدخول مفقود",
|
||
not_manager: "الحساب الحالي ليس مديرًا",
|
||
user_not_found: "المستخدم غير موجود",
|
||
user_recharged: "لدى هذا المستخدم سجلات شحن ولا يمكن حظره",
|
||
user_already_banned: "هذا المستخدم محظور بالفعل",
|
||
user_not_banned: "هذا المستخدم غير محظور",
|
||
prop_not_giftable: "هذا العنصر غير متاح كهديّة من المدير",
|
||
vip_not_giftable: "لا يمكن إهداء عنصر VIP هذا",
|
||
request_failed: "فشل الطلب",
|
||
enter_user: "أدخل معرّف المستخدم أو الحساب"
|
||
},
|
||
tr: {
|
||
document_title: "Yönetici Merkezi",
|
||
page_label: "Yönetici Merkezi",
|
||
back: "Geri",
|
||
title: "Yönetici Merkezi",
|
||
language_button_aria: "Dili değiştir",
|
||
close: "Kapat",
|
||
current_manager: "Mevcut Yönetici",
|
||
user: "Kullanıcı",
|
||
user_search_placeholder: "Kullanıcı ID veya hesap gir",
|
||
search: "Ara",
|
||
searching: "Aranıyor...",
|
||
target_user: "Hedef Kullanıcı",
|
||
gift_props: "Eşya Gönder",
|
||
gift_type: "Hediye Türü",
|
||
prop_type_ride: "Araç",
|
||
prop_type_noble_vip: "Noble VIP",
|
||
prop_type_avatar_frame: "Avatar Çerçevesi",
|
||
prop_type_badge: "Rozet",
|
||
prop_type_chat_bubble: "Sohbet Balonu",
|
||
prop_type_data_card: "Profil Kartı",
|
||
no_giftable_props: "Gönderilebilir eşya yok",
|
||
send: "Gönder",
|
||
sending: "Gönderiliyor...",
|
||
send_success: "Başarıyla gönderildi",
|
||
ban_user: "Kullanıcıyı Yasakla",
|
||
ban_reason: "Yasaklama nedeni",
|
||
ban: "Yasakla",
|
||
submitting: "Gönderiliyor...",
|
||
ban_success: "Kullanıcı yasaklandı",
|
||
unban_user: "Yasağı Kaldır",
|
||
unban_reason: "Yasağı kaldırma nedeni",
|
||
unban: "Yasağı kaldır",
|
||
unban_success: "Kullanıcının yasağı kaldırıldı",
|
||
can_unban: "Yasak kaldırılabilir",
|
||
cannot_unban: "Yasak kaldırılamaz",
|
||
loading: "Yükleniyor...",
|
||
uid_label: "UID",
|
||
days: "{count} gün",
|
||
recharged: "Yükleme yaptı",
|
||
not_recharged: "Yükleme yapmadı",
|
||
recharge_unknown: "Yükleme durumu bilinmiyor",
|
||
can_ban: "Yasaklanabilir",
|
||
cannot_ban: "Yasaklanamaz",
|
||
missing_token: "Token eksik",
|
||
not_manager: "Mevcut hesap yönetici değil",
|
||
user_not_found: "Kullanıcı bulunamadı",
|
||
user_recharged: "Bu kullanıcının yükleme kaydı var, yasaklanamaz",
|
||
user_already_banned: "Bu kullanıcı zaten yasaklı",
|
||
user_not_banned: "Bu kullanıcı yasaklı değil",
|
||
prop_not_giftable: "Bu eşya yönetici hediyesi için açık değil",
|
||
vip_not_giftable: "Bu noble eşyası gönderilemez",
|
||
request_failed: "İstek başarısız",
|
||
enter_user: "Kullanıcı ID veya hesap gir"
|
||
},
|
||
id: {
|
||
document_title: "Pusat Manajer",
|
||
page_label: "Pusat Manajer",
|
||
back: "Kembali",
|
||
title: "Pusat Manajer",
|
||
language_button_aria: "Ubah bahasa",
|
||
close: "Tutup",
|
||
current_manager: "Manajer Saat Ini",
|
||
user: "Pengguna",
|
||
user_search_placeholder: "Masukkan ID pengguna atau akun",
|
||
search: "Cari",
|
||
searching: "Mencari...",
|
||
target_user: "Pengguna Target",
|
||
gift_props: "Kirim Properti",
|
||
gift_type: "Jenis Hadiah",
|
||
prop_type_ride: "Kendaraan",
|
||
prop_type_noble_vip: "Noble VIP",
|
||
prop_type_avatar_frame: "Bingkai Avatar",
|
||
prop_type_badge: "Lencana",
|
||
prop_type_chat_bubble: "Gelembung Chat",
|
||
prop_type_data_card: "Kartu Profil",
|
||
no_giftable_props: "Tidak ada properti yang dapat dikirim",
|
||
send: "Kirim",
|
||
sending: "Mengirim...",
|
||
send_success: "Berhasil dikirim",
|
||
ban_user: "Blokir Pengguna",
|
||
ban_reason: "Alasan blokir",
|
||
ban: "Blokir",
|
||
submitting: "Mengirim...",
|
||
ban_success: "Pengguna diblokir",
|
||
unban_user: "Buka Blokir Pengguna",
|
||
unban_reason: "Alasan buka blokir",
|
||
unban: "Buka blokir",
|
||
unban_success: "Blokir pengguna dibuka",
|
||
can_unban: "Dapat dibuka",
|
||
cannot_unban: "Tidak dapat dibuka",
|
||
loading: "Memuat...",
|
||
uid_label: "UID",
|
||
days: "{count} hari",
|
||
recharged: "Sudah isi ulang",
|
||
not_recharged: "Belum isi ulang",
|
||
recharge_unknown: "Status isi ulang tidak diketahui",
|
||
can_ban: "Dapat diblokir",
|
||
cannot_ban: "Tidak dapat diblokir",
|
||
missing_token: "Token tidak ditemukan",
|
||
not_manager: "Akun saat ini bukan manajer",
|
||
user_not_found: "Pengguna tidak ditemukan",
|
||
user_recharged: "Pengguna ini memiliki riwayat isi ulang dan tidak dapat diblokir",
|
||
user_already_banned: "Pengguna ini sudah diblokir",
|
||
user_not_banned: "Pengguna ini tidak diblokir",
|
||
prop_not_giftable: "Properti ini tidak tersedia untuk hadiah manajer",
|
||
vip_not_giftable: "Properti noble ini tidak dapat dikirim",
|
||
request_failed: "Permintaan gagal",
|
||
enter_user: "Masukkan ID pengguna atau akun"
|
||
}
|
||
};
|
||
|
||
const state = {
|
||
manager: null,
|
||
target: null,
|
||
propsType: defaultPropsType,
|
||
props: [],
|
||
selectedPropsId: ""
|
||
};
|
||
|
||
const params = readURLParams();
|
||
let currentLang = normalizeLanguage(params.get("lang"));
|
||
let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
|
||
|
||
function readURLParams() {
|
||
const result = new URLSearchParams(window.location.search);
|
||
const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
|
||
const hashParams = new URLSearchParams(hashQuery);
|
||
hashParams.forEach((value, key) => {
|
||
if (!result.has(key)) result.set(key, value);
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function readRawParam(name) {
|
||
const queries = [window.location.search.replace(/^\?/, "")];
|
||
if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]);
|
||
|
||
for (const query of queries) {
|
||
for (const part of query.split("&")) {
|
||
if (!part) continue;
|
||
const equalIndex = part.indexOf("=");
|
||
const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part;
|
||
const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : "";
|
||
let decodedKey = "";
|
||
try {
|
||
decodedKey = decodeURIComponent(rawKey);
|
||
} catch (error) {
|
||
continue;
|
||
}
|
||
if (decodedKey !== name) continue;
|
||
try {
|
||
return decodeURIComponent(rawValue.replace(/\+/g, "%2B"));
|
||
} catch (error) {
|
||
return rawValue;
|
||
}
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function normalizeLanguage(lang) {
|
||
const value = String(lang || "en").toLowerCase();
|
||
return supportedLanguages.includes(value) ? value : "en";
|
||
}
|
||
|
||
async function loadMessages(lang) {
|
||
const normalizedLang = normalizeLanguage(lang);
|
||
if (window.location.protocol === "file:") return fallbackMessages[normalizedLang];
|
||
|
||
try {
|
||
const response = await fetch(`./locales/${normalizedLang}.json?v=20260511-0107`, { cache: "no-store" });
|
||
if (response.ok) return await response.json();
|
||
} catch (error) {
|
||
return fallbackMessages[normalizedLang];
|
||
}
|
||
|
||
return fallbackMessages[normalizedLang];
|
||
}
|
||
|
||
function localizedValue(messages, key) {
|
||
return messages?.[key] || fallbackMessages[currentLang]?.[key] || fallbackMessages.en[key] || "";
|
||
}
|
||
|
||
function message(key, fallback = "") {
|
||
return localizedValue(currentMessages, key) || fallback || key;
|
||
}
|
||
|
||
function renderMessages(messages) {
|
||
document.querySelectorAll("[data-i18n]").forEach((node) => {
|
||
const value = localizedValue(messages, node.dataset.i18n);
|
||
if (value) node.textContent = value;
|
||
});
|
||
|
||
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
|
||
const value = localizedValue(messages, node.dataset.i18nAria);
|
||
if (value) node.setAttribute("aria-label", value);
|
||
});
|
||
|
||
document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => {
|
||
const value = localizedValue(messages, node.dataset.i18nPlaceholder);
|
||
if (value) node.setAttribute("placeholder", value);
|
||
});
|
||
}
|
||
|
||
function setLanguageMenuOpen(open) {
|
||
const languageButton = $(".language-button");
|
||
const languageMenu = $(".language-menu");
|
||
if (!languageButton || !languageMenu) return;
|
||
|
||
languageMenu.hidden = !open;
|
||
languageButton.setAttribute("aria-expanded", String(open));
|
||
}
|
||
|
||
function renderLanguageControls(messages) {
|
||
const languageButton = $(".language-button");
|
||
if (languageButton) {
|
||
languageButton.textContent = languageLabels[currentLang];
|
||
languageButton.setAttribute("aria-label", localizedValue(messages, "language_button_aria") || "Change language");
|
||
}
|
||
|
||
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
|
||
const isActive = button.dataset.lang === currentLang;
|
||
button.classList.toggle("is-active", isActive);
|
||
button.setAttribute("aria-pressed", String(isActive));
|
||
});
|
||
}
|
||
|
||
function setPropsTypeMenuOpen(open) {
|
||
const root = $("#propsTypeSelect");
|
||
const button = $("#propsTypeButton");
|
||
const menu = $("#propsTypeMenu");
|
||
if (!root || !button || !menu) return;
|
||
|
||
root.classList.toggle("is-open", open);
|
||
menu.hidden = !open;
|
||
button.setAttribute("aria-expanded", String(open));
|
||
}
|
||
|
||
function renderPropsTypeSelect() {
|
||
const root = $("#propsTypeSelect");
|
||
const label = $("#propsTypeSelectedLabel");
|
||
if (root) root.dataset.value = state.propsType;
|
||
if (label) label.textContent = propTypeLabel(state.propsType);
|
||
|
||
document.querySelectorAll("#propsTypeMenu [data-value]").forEach((button) => {
|
||
const isSelected = button.dataset.value === state.propsType;
|
||
button.classList.toggle("is-selected", isSelected);
|
||
button.setAttribute("aria-selected", String(isSelected));
|
||
});
|
||
}
|
||
|
||
async function choosePropsType(value) {
|
||
const nextValue = String(value || defaultPropsType);
|
||
setPropsTypeMenuOpen(false);
|
||
if (nextValue === state.propsType) {
|
||
renderPropsTypeSelect();
|
||
return;
|
||
}
|
||
state.propsType = nextValue;
|
||
renderPropsTypeSelect();
|
||
try {
|
||
await loadProps();
|
||
} catch (error) {
|
||
setStatus($("#propsStatus"), friendlyError(error), "error");
|
||
}
|
||
}
|
||
|
||
async function applyLanguage(lang) {
|
||
currentLang = normalizeLanguage(lang);
|
||
const messages = await loadMessages(currentLang);
|
||
currentMessages = messages;
|
||
|
||
document.documentElement.lang = currentLang;
|
||
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
|
||
document.title = message("document_title", "Manager Center");
|
||
renderMessages(messages);
|
||
renderLanguageControls(messages);
|
||
renderPropsTypeSelect();
|
||
renderManager(state.manager);
|
||
renderTarget();
|
||
renderProps();
|
||
}
|
||
|
||
function $(selector) {
|
||
return document.querySelector(selector);
|
||
}
|
||
|
||
function apiBaseURL() {
|
||
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
|
||
if (override) return String(override).replace(/\/+$/, "");
|
||
return API_BASE_URL.replace(/\/+$/, "");
|
||
}
|
||
|
||
function isLocalDocumentsFile() {
|
||
if (window.location.protocol !== "file:") return false;
|
||
let pathname = window.location.pathname || "";
|
||
try {
|
||
pathname = decodeURIComponent(pathname);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
return pathname.startsWith("/Users/hy/Documents/");
|
||
}
|
||
|
||
function goApiBaseURL() {
|
||
const override = window.HYAPP_GO_API_BASE_URL || params.get("goApiBase") || params.get("goBase");
|
||
if (override) return String(override).replace(/\/+$/, "");
|
||
if (isLocalDocumentsFile()) return `${LOCAL_GO_API_BASE_URL}${GO_API_PATH_PREFIX}`;
|
||
return `${apiBaseURL()}${GO_API_PATH_PREFIX}`;
|
||
}
|
||
|
||
function buildRequestURL(baseURL, path) {
|
||
const base = `${String(baseURL || API_BASE_URL).replace(/\/+$/, "")}/`;
|
||
const target = String(path || "").replace(/^\/+/, "");
|
||
return new URL(target, base).toString();
|
||
}
|
||
|
||
function normalizeAuthorization(token) {
|
||
const value = String(token || "").trim();
|
||
if (!value) return "";
|
||
return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`;
|
||
}
|
||
|
||
function authHeadersFromToken(token) {
|
||
const sysOrigin = params.get("sysOrigin") || params.get("origin") || "ATYOU";
|
||
return {
|
||
Authorization: normalizeAuthorization(token),
|
||
"req-lang": currentLang,
|
||
"req-client": "H5",
|
||
"req-version": "V2",
|
||
"req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai",
|
||
"req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=Web;channel=Web",
|
||
"req-sys-origin": `origin=${sysOrigin};originChild=${sysOrigin}`
|
||
};
|
||
}
|
||
|
||
async function requestGoJSON(path, options = {}) {
|
||
const token = readRawParam("token");
|
||
if (!token) {
|
||
const error = new Error(message("missing_token"));
|
||
error.code = "missing_token";
|
||
throw error;
|
||
}
|
||
|
||
const headers = authHeadersFromToken(token);
|
||
const request = {
|
||
method: options.method || "GET",
|
||
cache: "no-store",
|
||
headers
|
||
};
|
||
if (options.body !== undefined) {
|
||
headers["Content-Type"] = "application/json";
|
||
request.body = JSON.stringify(options.body);
|
||
}
|
||
|
||
const response = await fetch(buildRequestURL(goApiBaseURL(), path), request);
|
||
const data = await response.json().catch(() => ({}));
|
||
if (!response.ok || data.status === false) {
|
||
const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
|
||
error.code = data.code || data.errorCodeName || "";
|
||
error.response = data;
|
||
throw error;
|
||
}
|
||
return data.body ?? data.data ?? null;
|
||
}
|
||
|
||
function endpointProfile() {
|
||
return "/app/h5/manager-center/profile";
|
||
}
|
||
|
||
function endpointSearchUser(account) {
|
||
return `/app/h5/manager-center/users/search?account=${encodeURIComponent(account)}`;
|
||
}
|
||
|
||
function endpointProps(type) {
|
||
return `/app/h5/manager-center/props?type=${encodeURIComponent(type || "")}`;
|
||
}
|
||
|
||
function endpointSendProps() {
|
||
return "/app/h5/manager-center/props/send";
|
||
}
|
||
|
||
function endpointBanUser() {
|
||
return "/app/h5/manager-center/users/ban";
|
||
}
|
||
|
||
function endpointUnbanUser() {
|
||
return "/app/h5/manager-center/users/unban";
|
||
}
|
||
|
||
function setLoading(isLoading) {
|
||
const root = $(".manager-center");
|
||
const mask = $("#loadingMask");
|
||
if (root) root.dataset.loading = isLoading ? "true" : "false";
|
||
if (mask) mask.hidden = !isLoading;
|
||
}
|
||
|
||
function setStatus(node, text, type = "") {
|
||
if (!node) return;
|
||
node.textContent = text || "";
|
||
node.hidden = !text;
|
||
node.classList.toggle("is-error", type === "error");
|
||
}
|
||
|
||
function showToast(text) {
|
||
const toast = $("#toast");
|
||
if (!toast) return;
|
||
toast.textContent = text || "";
|
||
toast.hidden = !text;
|
||
window.clearTimeout(showToast.timer);
|
||
showToast.timer = window.setTimeout(() => {
|
||
toast.hidden = true;
|
||
}, 2300);
|
||
}
|
||
|
||
function syncModalState() {
|
||
document.body.classList.toggle("is-modal-open", Boolean(document.querySelector(".modal-mask:not([hidden])")));
|
||
}
|
||
|
||
function setModalOpen(selector, open) {
|
||
const modal = $(selector);
|
||
if (!modal) return;
|
||
modal.hidden = !open;
|
||
if (open) {
|
||
setLanguageMenuOpen(false);
|
||
setPropsTypeMenuOpen(false);
|
||
}
|
||
syncModalState();
|
||
}
|
||
|
||
function closeModals() {
|
||
document.querySelectorAll(".modal-mask").forEach((modal) => {
|
||
modal.hidden = true;
|
||
});
|
||
setPropsTypeMenuOpen(false);
|
||
syncModalState();
|
||
}
|
||
|
||
function openGiftModal() {
|
||
if (!state.target) {
|
||
showToast(message("enter_user"));
|
||
return;
|
||
}
|
||
setModalOpen("#giftModal", true);
|
||
updateActionState();
|
||
}
|
||
|
||
function openBanModal() {
|
||
if (!state.target) {
|
||
showToast(message("enter_user"));
|
||
return;
|
||
}
|
||
setStatus($("#banStatus"), "");
|
||
setModalOpen("#banModal", true);
|
||
updateActionState();
|
||
}
|
||
|
||
function openUnbanModal() {
|
||
if (!state.target) {
|
||
showToast(message("enter_user"));
|
||
return;
|
||
}
|
||
setStatus($("#unbanStatus"), "");
|
||
setModalOpen("#unbanModal", true);
|
||
updateActionState();
|
||
}
|
||
|
||
function closePage() {
|
||
try {
|
||
if (window.app && typeof window.app.closePage === "function") {
|
||
window.app.closePage();
|
||
return;
|
||
}
|
||
if (window.FlutterPageControl && typeof window.FlutterPageControl.postMessage === "function") {
|
||
window.FlutterPageControl.postMessage("close_page");
|
||
return;
|
||
}
|
||
if (window.history.length > 1) window.history.back();
|
||
} catch (error) {
|
||
console.error("close manager center failed:", error);
|
||
if (window.history.length > 1) window.history.back();
|
||
}
|
||
}
|
||
|
||
function firstLetter(name, fallback) {
|
||
const value = String(name || fallback || "U").trim();
|
||
return value ? value.slice(0, 1).toUpperCase() : "U";
|
||
}
|
||
|
||
function renderAvatar(imgSelector, fallbackSelector, url, name, fallback) {
|
||
const img = $(imgSelector);
|
||
const fallbackNode = $(fallbackSelector);
|
||
if (!img || !fallbackNode) return;
|
||
const src = String(url || "").trim();
|
||
fallbackNode.textContent = firstLetter(name, fallback);
|
||
if (src) {
|
||
img.src = src;
|
||
img.hidden = false;
|
||
fallbackNode.hidden = true;
|
||
} else {
|
||
img.removeAttribute("src");
|
||
img.hidden = true;
|
||
fallbackNode.hidden = false;
|
||
}
|
||
}
|
||
|
||
function displayName(user) {
|
||
return user?.userNickname || user?.nickname || user?.name || "-";
|
||
}
|
||
|
||
function displayUserID(user) {
|
||
return String(user?.userId || user?.id || "-");
|
||
}
|
||
|
||
function displayShortID(user) {
|
||
return String(user?.actualAccount || user?.account || user?.shortId || user?.userId || user?.id || "-");
|
||
}
|
||
|
||
function canBanTarget(target) {
|
||
return Boolean(target) && target.canBan !== false && !target.hasRecharge && !canUnbanTarget(target);
|
||
}
|
||
|
||
function canUnbanTarget(target) {
|
||
const status = String(target?.accountStatus || "").trim().toUpperCase();
|
||
return Boolean(target) && (status === "FREEZE" || status === "ARCHIVE" || status === "ARCHIVE_DEVICE");
|
||
}
|
||
|
||
function userIDText(user) {
|
||
return `${message("uid_label", "UID")}: ${displayShortID(user)}`;
|
||
}
|
||
|
||
function propTypeLabel(type) {
|
||
const key = `prop_type_${String(type || "").toLowerCase()}`;
|
||
return message(key, type || "-");
|
||
}
|
||
|
||
function formatDays(days) {
|
||
return message("days", "{count} days").replace("{count}", String(days || 0));
|
||
}
|
||
|
||
function giftDays(item) {
|
||
const days = Number(item?.days);
|
||
if (Number.isFinite(days) && days > 0) return days;
|
||
return String(item?.type || state.propsType).toUpperCase() === "NOBLE_VIP" ? 3 : 7;
|
||
}
|
||
|
||
function renderManager(manager) {
|
||
$("#managerName").textContent = displayName(manager);
|
||
$("#managerUid").textContent = userIDText(manager);
|
||
renderAvatar("#managerAvatar", "#managerAvatarFallback", manager?.userAvatar, displayName(manager), "M");
|
||
}
|
||
|
||
function renderTarget() {
|
||
const card = $("#targetCard");
|
||
const actionCard = $("#actionCard");
|
||
if (!state.target) {
|
||
if (card) card.hidden = true;
|
||
if (actionCard) actionCard.hidden = true;
|
||
closeModals();
|
||
updateActionState();
|
||
return;
|
||
}
|
||
|
||
const target = state.target;
|
||
if (card) card.hidden = false;
|
||
if (actionCard) actionCard.hidden = false;
|
||
$("#targetName").textContent = displayName(target);
|
||
$("#targetUid").textContent = userIDText(target);
|
||
$("#targetAccountStatus").textContent = target.accountStatus || "-";
|
||
const canBan = canBanTarget(target);
|
||
const banEligibility = $("#banEligibility");
|
||
if (banEligibility) {
|
||
banEligibility.textContent = canBan ? message("can_ban") : message("cannot_ban");
|
||
banEligibility.classList.toggle("is-danger", !canBan);
|
||
}
|
||
const canUnban = canUnbanTarget(target);
|
||
const unbanEligibility = $("#unbanEligibility");
|
||
if (unbanEligibility) {
|
||
unbanEligibility.textContent = canUnban ? message("can_unban") : message("cannot_unban");
|
||
unbanEligibility.classList.toggle("is-danger", !canUnban);
|
||
}
|
||
renderAvatar("#targetAvatar", "#targetAvatarFallback", target.userAvatar, displayName(target), "U");
|
||
updateActionState();
|
||
}
|
||
|
||
function renderProps() {
|
||
const list = $("#propsList");
|
||
if (!list) return;
|
||
list.replaceChildren();
|
||
if (!state.props.length) {
|
||
setStatus($("#propsStatus"), message("no_giftable_props"));
|
||
updateActionState();
|
||
return;
|
||
}
|
||
setStatus($("#propsStatus"), "");
|
||
|
||
state.props.forEach((item) => {
|
||
const button = document.createElement("button");
|
||
button.type = "button";
|
||
button.className = "prop-option";
|
||
button.dataset.propsId = String(item.id);
|
||
if (String(item.id) === String(state.selectedPropsId)) {
|
||
button.classList.add("is-selected");
|
||
}
|
||
|
||
const cover = document.createElement("span");
|
||
cover.className = "prop-cover";
|
||
if (item.cover || item.sourceUrl) {
|
||
const img = document.createElement("img");
|
||
img.alt = "";
|
||
img.src = item.cover || item.sourceUrl;
|
||
cover.appendChild(img);
|
||
} else {
|
||
const fallback = document.createElement("span");
|
||
fallback.textContent = firstLetter(item.name, "G");
|
||
cover.appendChild(fallback);
|
||
}
|
||
|
||
const copy = document.createElement("span");
|
||
copy.className = "prop-copy";
|
||
const name = document.createElement("span");
|
||
name.className = "prop-name";
|
||
name.textContent = item.name || `ID ${item.id}`;
|
||
const meta = document.createElement("span");
|
||
meta.className = "prop-meta";
|
||
meta.textContent = item.vipLevel ? `${propTypeLabel(item.type)} · VIP ${item.vipLevel}` : propTypeLabel(item.type);
|
||
copy.append(name, meta);
|
||
|
||
const days = document.createElement("span");
|
||
days.className = "prop-days";
|
||
days.textContent = formatDays(giftDays(item));
|
||
|
||
button.append(cover, copy, days);
|
||
button.addEventListener("click", () => {
|
||
state.selectedPropsId = String(item.id);
|
||
renderProps();
|
||
updateActionState();
|
||
});
|
||
list.appendChild(button);
|
||
});
|
||
updateActionState();
|
||
}
|
||
|
||
function updateActionState() {
|
||
const giftEntryButton = $("#giftEntryButton");
|
||
const banEntryButton = $("#banEntryButton");
|
||
const unbanEntryButton = $("#unbanEntryButton");
|
||
const sendButton = $("#sendPropsButton");
|
||
const banButton = $("#banButton");
|
||
const unbanButton = $("#unbanButton");
|
||
if (giftEntryButton) giftEntryButton.disabled = !state.target;
|
||
if (banEntryButton) banEntryButton.disabled = !state.target;
|
||
if (unbanEntryButton) unbanEntryButton.disabled = !state.target;
|
||
if (sendButton) sendButton.disabled = !state.target || !state.selectedPropsId;
|
||
if (banButton) banButton.disabled = !canBanTarget(state.target);
|
||
if (unbanButton) unbanButton.disabled = !canUnbanTarget(state.target);
|
||
}
|
||
|
||
async function loadProfile() {
|
||
const body = await requestGoJSON(endpointProfile());
|
||
state.manager = body?.manager || body || {};
|
||
renderManager(state.manager);
|
||
}
|
||
|
||
async function loadProps() {
|
||
state.props = [];
|
||
state.selectedPropsId = "";
|
||
renderProps();
|
||
setStatus($("#propsStatus"), message("loading"));
|
||
|
||
const body = await requestGoJSON(endpointProps(state.propsType));
|
||
state.props = Array.isArray(body?.records) ? body.records : [];
|
||
state.selectedPropsId = state.props.length ? String(state.props[0].id) : "";
|
||
renderProps();
|
||
}
|
||
|
||
async function searchUser(event) {
|
||
event.preventDefault();
|
||
const input = $("#accountInput");
|
||
const account = String(input?.value || "").trim();
|
||
if (!account) {
|
||
setStatus($("#searchStatus"), message("enter_user"), "error");
|
||
return;
|
||
}
|
||
|
||
$("#searchButton").disabled = true;
|
||
setStatus($("#searchStatus"), message("searching"));
|
||
try {
|
||
const body = await requestGoJSON(endpointSearchUser(account));
|
||
state.target = body?.user || body || null;
|
||
renderTarget();
|
||
setStatus($("#searchStatus"), "");
|
||
} catch (error) {
|
||
state.target = null;
|
||
renderTarget();
|
||
setStatus($("#searchStatus"), friendlyError(error), "error");
|
||
} finally {
|
||
$("#searchButton").disabled = false;
|
||
}
|
||
}
|
||
|
||
async function sendProps() {
|
||
if (!state.target || !state.selectedPropsId) return;
|
||
|
||
const button = $("#sendPropsButton");
|
||
button.disabled = true;
|
||
try {
|
||
await requestGoJSON(endpointSendProps(), {
|
||
method: "POST",
|
||
body: {
|
||
acceptUserId: displayUserID(state.target),
|
||
propsId: state.selectedPropsId
|
||
}
|
||
});
|
||
setModalOpen("#giftModal", false);
|
||
showToast(message("send_success"));
|
||
} catch (error) {
|
||
showToast(friendlyError(error));
|
||
} finally {
|
||
updateActionState();
|
||
}
|
||
}
|
||
|
||
async function banUser() {
|
||
if (!canBanTarget(state.target)) return;
|
||
|
||
const button = $("#banButton");
|
||
button.disabled = true;
|
||
setStatus($("#banStatus"), message("submitting"));
|
||
try {
|
||
await requestGoJSON(endpointBanUser(), {
|
||
method: "POST",
|
||
body: {
|
||
userId: displayUserID(state.target),
|
||
reason: $("#banReason").value || ""
|
||
}
|
||
});
|
||
state.target.accountStatus = "ARCHIVE";
|
||
renderTarget();
|
||
setModalOpen("#banModal", false);
|
||
setStatus($("#banStatus"), "");
|
||
showToast(message("ban_success"));
|
||
} catch (error) {
|
||
setStatus($("#banStatus"), friendlyError(error), "error");
|
||
} finally {
|
||
updateActionState();
|
||
}
|
||
}
|
||
|
||
async function unbanUser() {
|
||
if (!canUnbanTarget(state.target)) return;
|
||
|
||
const button = $("#unbanButton");
|
||
button.disabled = true;
|
||
setStatus($("#unbanStatus"), message("submitting"));
|
||
try {
|
||
await requestGoJSON(endpointUnbanUser(), {
|
||
method: "POST",
|
||
body: {
|
||
userId: displayUserID(state.target),
|
||
reason: $("#unbanReason").value || ""
|
||
}
|
||
});
|
||
state.target.accountStatus = "NORMAL";
|
||
renderTarget();
|
||
setModalOpen("#unbanModal", false);
|
||
setStatus($("#unbanStatus"), "");
|
||
showToast(message("unban_success"));
|
||
} catch (error) {
|
||
setStatus($("#unbanStatus"), friendlyError(error), "error");
|
||
} finally {
|
||
updateActionState();
|
||
}
|
||
}
|
||
|
||
function friendlyError(error) {
|
||
const code = error?.code || error?.response?.code || error?.response?.errorCodeName || "";
|
||
if (code === "missing_token") return message("missing_token");
|
||
if (code === "not_manager") return message("not_manager");
|
||
if (code === "user_not_found") return message("user_not_found");
|
||
if (code === "user_recharged") return message("user_recharged");
|
||
if (code === "user_already_banned") return message("user_already_banned");
|
||
if (code === "user_not_banned") return message("user_not_banned");
|
||
if (code === "prop_not_giftable") return message("prop_not_giftable");
|
||
if (code === "vip_not_giftable") return message("vip_not_giftable");
|
||
return error?.message || message("request_failed");
|
||
}
|
||
|
||
function bindEvents() {
|
||
$(".back-button")?.addEventListener("click", closePage);
|
||
$(".language-button")?.addEventListener("click", (event) => {
|
||
event.stopPropagation();
|
||
setLanguageMenuOpen(Boolean($(".language-menu")?.hidden));
|
||
});
|
||
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
|
||
button.addEventListener("click", async (event) => {
|
||
event.stopPropagation();
|
||
setLanguageMenuOpen(false);
|
||
await applyLanguage(button.dataset.lang);
|
||
});
|
||
});
|
||
document.addEventListener("click", () => {
|
||
setLanguageMenuOpen(false);
|
||
setPropsTypeMenuOpen(false);
|
||
});
|
||
$("#giftEntryButton")?.addEventListener("click", openGiftModal);
|
||
$("#banEntryButton")?.addEventListener("click", openBanModal);
|
||
$("#unbanEntryButton")?.addEventListener("click", openUnbanModal);
|
||
document.querySelectorAll("[data-modal-close]").forEach((button) => {
|
||
button.addEventListener("click", closeModals);
|
||
});
|
||
document.querySelectorAll(".modal-mask").forEach((modal) => {
|
||
modal.addEventListener("click", (event) => {
|
||
if (event.target === modal) closeModals();
|
||
});
|
||
});
|
||
$("#propsTypeButton")?.addEventListener("click", (event) => {
|
||
event.stopPropagation();
|
||
setLanguageMenuOpen(false);
|
||
setPropsTypeMenuOpen(Boolean($("#propsTypeMenu")?.hidden));
|
||
});
|
||
document.querySelectorAll("#propsTypeMenu [data-value]").forEach((button) => {
|
||
button.addEventListener("click", async (event) => {
|
||
event.stopPropagation();
|
||
await choosePropsType(button.dataset.value);
|
||
});
|
||
});
|
||
document.addEventListener("keydown", (event) => {
|
||
if (event.key !== "Escape") return;
|
||
setLanguageMenuOpen(false);
|
||
setPropsTypeMenuOpen(false);
|
||
closeModals();
|
||
});
|
||
$("#searchForm")?.addEventListener("submit", searchUser);
|
||
$("#sendPropsButton")?.addEventListener("click", sendProps);
|
||
$("#banButton")?.addEventListener("click", banUser);
|
||
$("#unbanButton")?.addEventListener("click", unbanUser);
|
||
}
|
||
|
||
async function init() {
|
||
bindEvents();
|
||
renderPropsTypeSelect();
|
||
await applyLanguage(currentLang);
|
||
if (!readRawParam("token")) {
|
||
setLoading(false);
|
||
setStatus($("#searchStatus"), message("missing_token"), "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await Promise.all([loadProfile(), loadProps()]);
|
||
} catch (error) {
|
||
showToast(friendlyError(error));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
init();
|
||
})();
|