经理中心

This commit is contained in:
zhx 2026-05-11 18:45:50 +08:00
parent 12ebb29f02
commit d82aec3805
7 changed files with 826 additions and 84 deletions

View File

@ -1,21 +1,29 @@
<!doctype html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>经理中心</title>
<link rel="stylesheet" href="./style.css?v=20260511-0102" />
<title>Manager Center</title>
<link rel="stylesheet" href="./style.css?v=20260511-0107" />
</head>
<body>
<div class="manager-center" data-loading="true">
<div class="manager-center" aria-label="Manager Center" data-i18n-aria="page_label" data-loading="true">
<nav class="title-bar">
<button class="back-button" type="button" aria-label="返回">
<button class="back-button" type="button" aria-label="Back" data-i18n-aria="back">
<svg viewBox="0 0 32 32" aria-hidden="true">
<path d="M20 8 12 16l8 8" />
</svg>
</button>
<h1>经理中心</h1>
<span class="title-spacer" aria-hidden="true"></span>
<h1 data-i18n="title">Manager Center</h1>
<div class="language-switcher">
<button class="language-button" type="button" aria-label="Change language" data-i18n-aria="language_button_aria" aria-expanded="false">EN</button>
<div class="language-menu" hidden>
<button type="button" data-lang="en">EN</button>
<button type="button" data-lang="ar">AR</button>
<button type="button" data-lang="tr">TR</button>
<button type="button" data-lang="id">ID</button>
</div>
</div>
</nav>
<main class="content">
@ -25,7 +33,7 @@
<span id="managerAvatarFallback">M</span>
</div>
<div class="profile-copy">
<div class="eyebrow">当前经理</div>
<div class="eyebrow" data-i18n="current_manager">Current Manager</div>
<div class="name" id="managerName">-</div>
<div class="meta" id="managerUid">UID: -</div>
</div>
@ -33,11 +41,11 @@
<section class="card search-card">
<div class="section-head">
<h2>用户</h2>
<h2 data-i18n="user">User</h2>
</div>
<form class="search-form" id="searchForm" autocomplete="off">
<input id="accountInput" type="text" inputmode="text" placeholder="输入用户 ID 或账号" />
<button id="searchButton" type="submit">搜索</button>
<input id="accountInput" type="text" inputmode="text" placeholder="Enter user ID or account" data-i18n-placeholder="user_search_placeholder" />
<button id="searchButton" type="submit" data-i18n="search">Search</button>
</form>
<div class="form-status" id="searchStatus" role="status" hidden></div>
</section>
@ -48,50 +56,64 @@
<span id="targetAvatarFallback">U</span>
</div>
<div class="profile-copy">
<div class="eyebrow">目标用户</div>
<div class="eyebrow" data-i18n="target_user">Target User</div>
<div class="name" id="targetName">-</div>
<div class="meta" id="targetUid">UID: -</div>
<div class="pill-row">
<span class="status-pill" id="targetAccountStatus">-</span>
<span class="status-pill" id="targetRechargeStatus">-</span>
</div>
</div>
</section>
<section class="card gift-card">
<div class="section-head">
<h2>赠送道具</h2>
<h2 data-i18n="gift_props">Gift Props</h2>
</div>
<label class="field-label" id="propsTypeLabelText" data-i18n="gift_type">Gift Type</label>
<div class="custom-select" id="propsTypeSelect" data-value="RIDE">
<button
class="custom-select-button"
id="propsTypeButton"
type="button"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="propsTypeLabelText propsTypeSelectedLabel"
>
<span id="propsTypeSelectedLabel">Ride</span>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m7 9 5 5 5-5" />
</svg>
</button>
<div class="custom-select-menu" id="propsTypeMenu" role="listbox" aria-labelledby="propsTypeLabelText" hidden>
<button type="button" role="option" data-value="RIDE" data-i18n="prop_type_ride">Ride</button>
<button type="button" role="option" data-value="NOBLE_VIP" data-i18n="prop_type_noble_vip">Noble VIP</button>
<button type="button" role="option" data-value="AVATAR_FRAME" data-i18n="prop_type_avatar_frame">Avatar Frame</button>
<button type="button" role="option" data-value="CHAT_BUBBLE" data-i18n="prop_type_chat_bubble">Chat Bubble</button>
<button type="button" role="option" data-value="DATA_CARD" data-i18n="prop_type_data_card">Profile Card</button>
</div>
</div>
<label class="field-label" for="propsTypeSelect">礼物类型</label>
<select id="propsTypeSelect">
<option value="RIDE">座驾</option>
<option value="NOBLE_VIP">贵族</option>
<option value="AVATAR_FRAME">头像框</option>
<option value="CHAT_BUBBLE">聊天气泡</option>
<option value="DATA_CARD">资料卡</option>
</select>
<div class="props-status" id="propsStatus" role="status" hidden></div>
<div class="props-list" id="propsList"></div>
<button class="primary-action" id="sendPropsButton" type="button" disabled>赠送</button>
<button class="primary-action" id="sendPropsButton" type="button" disabled data-i18n="send">Send</button>
</section>
<section class="card ban-card" id="banCard" hidden>
<div class="section-head">
<h2>封禁用户</h2>
<h2 data-i18n="ban_user">Ban User</h2>
<span class="danger-chip" id="banEligibility">-</span>
</div>
<textarea id="banReason" maxlength="200" rows="3" placeholder="封禁原因"></textarea>
<button class="danger-action" id="banButton" type="button" disabled>封禁</button>
<textarea id="banReason" maxlength="200" rows="3" placeholder="Ban reason" data-i18n-placeholder="ban_reason"></textarea>
<button class="danger-action" id="banButton" type="button" disabled data-i18n="ban">Ban</button>
<div class="form-status" id="banStatus" role="status" hidden></div>
</section>
</main>
<div class="loading-mask" id="loadingMask" role="status" aria-live="polite">
<div class="loader" aria-hidden="true"></div>
<span>加载中...</span>
<span data-i18n="loading">Loading...</span>
</div>
<div class="toast" id="toast" role="status" hidden></div>
</div>
<script src="./script.js?v=20260511-0104" defer></script>
<script src="./script.js?v=20260511-0108" defer></script>
</body>
</html>

View File

@ -0,0 +1,47 @@
{
"document_title": "مركز المدير",
"page_label": "مركز المدير",
"back": "رجوع",
"title": "مركز المدير",
"language_button_aria": "تغيير اللغة",
"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_chat_bubble": "فقاعة الدردشة",
"prop_type_data_card": "بطاقة الملف",
"no_giftable_props": "لا توجد عناصر قابلة للإهداء",
"send": "إرسال",
"sending": "جار الإرسال...",
"confirm_send": "هل تريد إرسال هذا العنصر؟",
"send_success": "تم الإرسال بنجاح",
"ban_user": "حظر المستخدم",
"ban_reason": "سبب الحظر",
"ban": "حظر",
"confirm_ban": "هل تريد حظر هذا المستخدم؟",
"submitting": "جار الإرسال...",
"ban_success": "تم حظر المستخدم",
"loading": "جار التحميل...",
"uid_label": "UID",
"days": "{count} يوم",
"recharged": "تم الشحن",
"not_recharged": "لم يتم الشحن",
"recharge_unknown": "حالة الشحن غير معروفة",
"can_ban": "يمكن الحظر",
"cannot_ban": "لا يمكن الحظر",
"missing_token": "رمز الدخول مفقود",
"not_manager": "الحساب الحالي ليس مديرًا",
"user_not_found": "المستخدم غير موجود",
"user_recharged": "لدى هذا المستخدم سجلات شحن ولا يمكن حظره",
"prop_not_giftable": "هذا العنصر غير متاح كهديّة من المدير",
"vip_not_giftable": "لا يمكن إهداء عنصر VIP هذا",
"request_failed": "فشل الطلب",
"enter_user": "أدخل معرّف المستخدم أو الحساب"
}

View File

@ -0,0 +1,47 @@
{
"document_title": "Manager Center",
"page_label": "Manager Center",
"back": "Back",
"title": "Manager Center",
"language_button_aria": "Change language",
"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_chat_bubble": "Chat Bubble",
"prop_type_data_card": "Profile Card",
"no_giftable_props": "No giftable props",
"send": "Send",
"sending": "Sending...",
"confirm_send": "Send this prop?",
"send_success": "Sent successfully",
"ban_user": "Ban User",
"ban_reason": "Ban reason",
"ban": "Ban",
"confirm_ban": "Ban this user?",
"submitting": "Submitting...",
"ban_success": "User banned",
"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",
"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"
}

View File

@ -0,0 +1,47 @@
{
"document_title": "Pusat Manajer",
"page_label": "Pusat Manajer",
"back": "Kembali",
"title": "Pusat Manajer",
"language_button_aria": "Ubah bahasa",
"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_chat_bubble": "Gelembung Chat",
"prop_type_data_card": "Kartu Profil",
"no_giftable_props": "Tidak ada properti yang dapat dikirim",
"send": "Kirim",
"sending": "Mengirim...",
"confirm_send": "Kirim properti ini?",
"send_success": "Berhasil dikirim",
"ban_user": "Blokir Pengguna",
"ban_reason": "Alasan blokir",
"ban": "Blokir",
"confirm_ban": "Blokir pengguna ini?",
"submitting": "Mengirim...",
"ban_success": "Pengguna diblokir",
"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",
"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"
}

View File

@ -0,0 +1,47 @@
{
"document_title": "Yönetici Merkezi",
"page_label": "Yönetici Merkezi",
"back": "Geri",
"title": "Yönetici Merkezi",
"language_button_aria": "Dili değiştir",
"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_chat_bubble": "Sohbet Balonu",
"prop_type_data_card": "Profil Kartı",
"no_giftable_props": "Gönderilebilir eşya yok",
"send": "Gönder",
"sending": "Gönderiliyor...",
"confirm_send": "Bu eşyayı göndermek istiyor musun?",
"send_success": "Başarıyla gönderildi",
"ban_user": "Kullanıcıyı Yasakla",
"ban_reason": "Yasaklama nedeni",
"ban": "Yasakla",
"confirm_ban": "Bu kullanıcıyı yasaklamak istiyor musun?",
"submitting": "Gönderiliyor...",
"ban_success": "Kullanıcı yasaklandı",
"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",
"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"
}

View File

@ -1,9 +1,207 @@
(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",
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_chat_bubble: "Chat Bubble",
prop_type_data_card: "Profile Card",
no_giftable_props: "No giftable props",
send: "Send",
sending: "Sending...",
confirm_send: "Send this prop?",
send_success: "Sent successfully",
ban_user: "Ban User",
ban_reason: "Ban reason",
ban: "Ban",
confirm_ban: "Ban this user?",
submitting: "Submitting...",
ban_success: "User banned",
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",
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: "تغيير اللغة",
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_chat_bubble: "فقاعة الدردشة",
prop_type_data_card: "بطاقة الملف",
no_giftable_props: "لا توجد عناصر قابلة للإهداء",
send: "إرسال",
sending: "جار الإرسال...",
confirm_send: "هل تريد إرسال هذا العنصر؟",
send_success: "تم الإرسال بنجاح",
ban_user: "حظر المستخدم",
ban_reason: "سبب الحظر",
ban: "حظر",
confirm_ban: "هل تريد حظر هذا المستخدم؟",
submitting: "جار الإرسال...",
ban_success: "تم حظر المستخدم",
loading: "جار التحميل...",
uid_label: "UID",
days: "{count} يوم",
recharged: "تم الشحن",
not_recharged: "لم يتم الشحن",
recharge_unknown: "حالة الشحن غير معروفة",
can_ban: "يمكن الحظر",
cannot_ban: "لا يمكن الحظر",
missing_token: "رمز الدخول مفقود",
not_manager: "الحساب الحالي ليس مديرًا",
user_not_found: "المستخدم غير موجود",
user_recharged: "لدى هذا المستخدم سجلات شحن ولا يمكن حظره",
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",
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_chat_bubble: "Sohbet Balonu",
prop_type_data_card: "Profil Kartı",
no_giftable_props: "Gönderilebilir eşya yok",
send: "Gönder",
sending: "Gönderiliyor...",
confirm_send: "Bu eşyayı göndermek istiyor musun?",
send_success: "Başarıyla gönderildi",
ban_user: "Kullanıcıyı Yasakla",
ban_reason: "Yasaklama nedeni",
ban: "Yasakla",
confirm_ban: "Bu kullanıcıyı yasaklamak istiyor musun?",
submitting: "Gönderiliyor...",
ban_success: "Kullanıcı yasaklandı",
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",
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",
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_chat_bubble: "Gelembung Chat",
prop_type_data_card: "Kartu Profil",
no_giftable_props: "Tidak ada properti yang dapat dikirim",
send: "Kirim",
sending: "Mengirim...",
confirm_send: "Kirim properti ini?",
send_success: "Berhasil dikirim",
ban_user: "Blokir Pengguna",
ban_reason: "Alasan blokir",
ban: "Blokir",
confirm_ban: "Blokir pengguna ini?",
submitting: "Mengirim...",
ban_success: "Pengguna diblokir",
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",
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,
@ -13,6 +211,8 @@
};
const params = readURLParams();
let currentLang = normalizeLanguage(params.get("lang"));
let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en;
function readURLParams() {
const result = new URLSearchParams(window.location.search);
@ -51,6 +251,129 @@
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-0105`, { 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);
}
@ -95,7 +418,7 @@
const sysOrigin = params.get("sysOrigin") || params.get("origin") || "ATYOU";
return {
Authorization: normalizeAuthorization(token),
"req-lang": params.get("lang") || "zh-CN",
"req-lang": currentLang,
"req-client": "H5",
"req-version": "V2",
"req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai",
@ -107,7 +430,7 @@
async function requestGoJSON(path, options = {}) {
const token = readRawParam("token");
if (!token) {
const error = new Error("缺少登录信息");
const error = new Error(message("missing_token"));
error.code = "missing_token";
throw error;
}
@ -226,17 +549,30 @@
return String(user?.userId || user?.id || "-");
}
function displayAccount(user) {
return user?.account ? ` (${user.account})` : "";
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;
}
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 renderManager(manager) {
$("#managerName").textContent = displayName(manager);
$("#managerUid").textContent = `UID: ${displayUserID(manager)}${displayAccount(manager)}`;
$("#managerUid").textContent = userIDText(manager);
renderAvatar("#managerAvatar", "#managerAvatarFallback", manager?.userAvatar, displayName(manager), "M");
}
@ -254,14 +590,11 @@
card.hidden = false;
banCard.hidden = false;
$("#targetName").textContent = displayName(target);
$("#targetUid").textContent = `UID: ${displayUserID(target)}${displayAccount(target)}`;
$("#targetUid").textContent = userIDText(target);
$("#targetAccountStatus").textContent = target.accountStatus || "-";
const rechargeStatus = $("#targetRechargeStatus");
const canBan = canBanTarget(target);
rechargeStatus.textContent = target.hasRecharge ? "已充值" : (canBan ? "未充值" : "充值状态未知");
rechargeStatus.classList.toggle("is-danger", Boolean(target.hasRecharge) || !canBan);
const banEligibility = $("#banEligibility");
banEligibility.textContent = canBan ? "可封禁" : "不可封禁";
banEligibility.textContent = canBan ? message("can_ban") : message("cannot_ban");
banEligibility.classList.toggle("is-danger", !canBan);
renderAvatar("#targetAvatar", "#targetAvatarFallback", target.userAvatar, displayName(target), "U");
updateActionState();
@ -272,7 +605,7 @@
if (!list) return;
list.replaceChildren();
if (!state.props.length) {
setStatus($("#propsStatus"), "没有可赠送道具");
setStatus($("#propsStatus"), message("no_giftable_props"));
updateActionState();
return;
}
@ -307,12 +640,12 @@
name.textContent = item.name || `ID ${item.id}`;
const meta = document.createElement("span");
meta.className = "prop-meta";
meta.textContent = item.vipLevel ? `${item.type} · VIP ${item.vipLevel}` : item.type;
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 = `${item.days || 0}`;
days.textContent = formatDays(item.days);
button.append(cover, copy, days);
button.addEventListener("click", () => {
@ -339,10 +672,10 @@
}
async function loadProps() {
setStatus($("#propsStatus"), "加载中...");
state.props = [];
state.selectedPropsId = "";
renderProps();
setStatus($("#propsStatus"), message("loading"));
const body = await requestGoJSON(endpointProps(state.propsType));
state.props = Array.isArray(body?.records) ? body.records : [];
@ -355,12 +688,12 @@
const input = $("#accountInput");
const account = String(input?.value || "").trim();
if (!account) {
setStatus($("#searchStatus"), "请输入用户 ID 或账号", "error");
setStatus($("#searchStatus"), message("enter_user"), "error");
return;
}
$("#searchButton").disabled = true;
setStatus($("#searchStatus"), "搜索中...");
setStatus($("#searchStatus"), message("searching"));
try {
const body = await requestGoJSON(endpointSearchUser(account));
state.target = body?.user || body || null;
@ -377,7 +710,7 @@
async function sendProps() {
if (!state.target || !state.selectedPropsId) return;
if (!window.confirm("确认赠送该道具?")) return;
if (!window.confirm(message("confirm_send"))) return;
const button = $("#sendPropsButton");
button.disabled = true;
@ -389,7 +722,7 @@
propsId: state.selectedPropsId
}
});
showToast("赠送成功");
showToast(message("send_success"));
} catch (error) {
showToast(friendlyError(error));
} finally {
@ -399,11 +732,11 @@
async function banUser() {
if (!canBanTarget(state.target)) return;
if (!window.confirm("确认封禁该用户?")) return;
if (!window.confirm(message("confirm_ban"))) return;
const button = $("#banButton");
button.disabled = true;
setStatus($("#banStatus"), "提交中...");
setStatus($("#banStatus"), message("submitting"));
try {
await requestGoJSON(endpointBanUser(), {
method: "POST",
@ -415,7 +748,7 @@
state.target.accountStatus = "ARCHIVE";
renderTarget();
setStatus($("#banStatus"), "");
showToast("封禁成功");
showToast(message("ban_success"));
} catch (error) {
setStatus($("#banStatus"), friendlyError(error), "error");
} finally {
@ -425,36 +758,60 @@
function friendlyError(error) {
const code = error?.code || error?.response?.code || error?.response?.errorCodeName || "";
if (code === "missing_token") return "缺少登录信息";
if (code === "not_manager") return "当前账号不是经理";
if (code === "user_not_found") return "用户不存在";
if (code === "user_recharged") return "该用户已有充值记录,不能封禁";
if (code === "prop_not_giftable") return "该道具未开放经理赠送";
if (code === "vip_not_giftable") return "该贵族道具不可赠送";
return error?.message || "请求失败";
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 === "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);
$("#searchForm")?.addEventListener("submit", searchUser);
$("#propsTypeSelect")?.addEventListener("change", async (event) => {
state.propsType = event.target.value || defaultPropsType;
try {
await loadProps();
} catch (error) {
setStatus($("#propsStatus"), friendlyError(error), "error");
}
$(".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);
});
$("#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);
});
$("#searchForm")?.addEventListener("submit", searchUser);
$("#sendPropsButton")?.addEventListener("click", sendProps);
$("#banButton")?.addEventListener("click", banUser);
}
async function init() {
bindEvents();
$("#propsTypeSelect").value = state.propsType;
renderPropsTypeSelect();
await applyLanguage(currentLang);
if (!readRawParam("token")) {
setLoading(false);
setStatus($("#searchStatus"), "缺少登录信息", "error");
setStatus($("#searchStatus"), message("missing_token"), "error");
return;
}

View File

@ -36,7 +36,6 @@ body {
button,
input,
select,
textarea {
font: inherit;
}
@ -110,6 +109,58 @@ button:disabled {
stroke-linejoin: round;
}
.language-switcher {
position: relative;
justify-self: end;
}
.language-button {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 30px;
border: 1px solid rgba(35, 40, 46, 0.12);
border-radius: 15px;
background: rgba(255, 255, 255, 0.76);
color: var(--text);
font-size: 13px;
font-weight: 900;
box-shadow: 0 4px 14px rgba(32, 36, 42, 0.08);
}
.language-menu {
position: absolute;
z-index: 8;
top: calc(100% + 7px);
right: 0;
display: grid;
gap: 2px;
min-width: 58px;
padding: 5px;
border: 1px solid rgba(35, 40, 46, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 10px 26px rgba(36, 46, 60, 0.14);
}
.language-menu button {
min-height: 28px;
border-radius: 6px;
color: var(--text);
font-size: 12px;
font-weight: 900;
}
.language-menu button.is-active {
background: #e7f8f4;
color: var(--primary-dark);
}
.language-menu[hidden] {
display: none;
}
.content {
position: relative;
z-index: 1;
@ -245,7 +296,6 @@ button:disabled {
}
input,
select,
textarea {
width: 100%;
border: 1px solid var(--line);
@ -256,8 +306,11 @@ textarea {
}
input,
select {
button.custom-select-button {
height: 44px;
}
input {
padding: 0 12px;
}
@ -268,7 +321,6 @@ textarea {
}
input:focus,
select:focus,
textarea:focus {
border-color: var(--primary);
background: #fff;
@ -312,22 +364,120 @@ textarea:focus {
font-weight: 800;
}
.custom-select {
position: relative;
}
.custom-select-button {
display: grid;
grid-template-columns: minmax(0, 1fr) 24px;
align-items: center;
width: 100%;
border: 1px solid var(--primary);
border-radius: 8px;
padding: 0 12px 0 14px;
background: #fff;
color: var(--text);
text-align: left;
box-shadow: 0 0 0 5px rgba(24, 184, 159, 0.1);
}
.custom-select-button span {
overflow: hidden;
font-size: 16px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.custom-select-button svg {
width: 22px;
height: 22px;
fill: none;
justify-self: end;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2;
transition: transform 0.16s ease;
}
.custom-select.is-open .custom-select-button svg {
transform: rotate(180deg);
}
.custom-select-menu {
position: absolute;
z-index: 12;
top: calc(100% + 8px);
right: 0;
left: 0;
display: grid;
gap: 4px;
max-height: 220px;
overflow-y: auto;
border: 1px solid rgba(35, 40, 46, 0.1);
border-radius: 8px;
padding: 6px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 16px 36px rgba(32, 36, 42, 0.18);
}
.custom-select-menu button {
min-height: 36px;
border-radius: 6px;
padding: 0 11px;
background: transparent;
color: var(--text);
font-size: 14px;
font-weight: 800;
text-align: left;
}
.custom-select-menu button.is-selected {
background: #e7f8f4;
color: var(--primary-dark);
}
.custom-select-menu button:active {
background: #dff4ef;
}
.custom-select-menu[hidden] {
display: none;
}
.props-list {
display: grid;
gap: 8px;
margin-top: 12px;
gap: 6px;
max-height: 330px;
margin-top: 10px;
overflow-y: auto;
padding-right: 2px;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(24, 184, 159, 0.45) transparent;
}
.props-list::-webkit-scrollbar {
width: 5px;
}
.props-list::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(24, 184, 159, 0.42);
}
.prop-option {
display: grid;
grid-template-columns: 52px 1fr auto;
grid-template-columns: 42px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
gap: 8px;
width: 100%;
min-height: 68px;
min-height: 54px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px;
padding: 6px 8px;
background: #fff;
color: inherit;
text-align: left;
@ -339,10 +489,10 @@ textarea:focus {
}
.prop-cover {
width: 52px;
height: 52px;
width: 42px;
height: 42px;
overflow: hidden;
border-radius: 8px;
border-radius: 7px;
background: #eef2f5;
}
@ -364,29 +514,32 @@ textarea:focus {
}
.prop-copy {
display: grid;
gap: 2px;
min-width: 0;
}
.prop-name {
overflow: hidden;
font-size: 15px;
font-size: 14px;
font-weight: 900;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.prop-meta {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
font-size: 11px;
line-height: 14px;
}
.prop-days {
border-radius: 999px;
padding: 5px 8px;
padding: 4px 8px;
background: #f2f4f7;
color: #596273;
font-size: 12px;
font-size: 11px;
font-weight: 900;
}
@ -447,6 +600,28 @@ textarea:focus {
text-align: center;
}
[dir="rtl"] .back-button svg {
transform: rotate(180deg);
}
[dir="rtl"] .language-menu {
right: auto;
left: 0;
}
[dir="rtl"] .profile-copy,
[dir="rtl"] input,
[dir="rtl"] textarea,
[dir="rtl"] .custom-select-button,
[dir="rtl"] .custom-select-menu button,
[dir="rtl"] .prop-option {
text-align: right;
}
[dir="rtl"] .custom-select-button {
padding: 0 14px 0 12px;
}
@keyframes spin {
to {
transform: rotate(360deg);