From 6944cd64f8d07d4a9426f5d68eb8b347ec67ad35 Mon Sep 17 00:00:00 2001 From: 170-carry Date: Wed, 29 Apr 2026 00:14:49 +0800 Subject: [PATCH] chore: publish pending h5 updates --- h5/agency-center/index.back.html | 165 ++ h5/agency-center/index.html | 169 +-- h5/bd-center/index.back.html | 97 ++ h5/bd-center/index.html | 111 +- h5/bd-leader-center/index.back.html | 140 ++ h5/bd-leader-center/index.html | 154 +- h5/hyapp/agency-center/host-list.css | 466 ++++++ h5/hyapp/agency-center/host-list.html | 108 ++ h5/hyapp/agency-center/host-list.js | 866 +++++++++++ h5/hyapp/agency-center/index.html | 235 +++ h5/hyapp/agency-center/original-vue-logic.md | 336 +++++ h5/hyapp/agency-center/script.js | 1134 ++++++++++++++ h5/hyapp/agency-center/style.css | 1404 ++++++++++++++++++ h5/hyapp/host-center/locales/ar.json | 1 + h5/hyapp/host-center/locales/en.json | 1 + h5/hyapp/host-center/locales/id.json | 1 + h5/hyapp/host-center/locales/tr.json | 1 + h5/hyapp/host-center/policy.html | 8 +- h5/hyapp/host-center/policy.js | 25 +- h5/hyapp/host-center/style.css | 5 +- h5/hyapp/hosts-bill/index.html | 145 ++ h5/hyapp/hosts-bill/script.js | 940 ++++++++++++ h5/hyapp/hosts-bill/style.css | 561 +++++++ 23 files changed, 6668 insertions(+), 405 deletions(-) create mode 100644 h5/agency-center/index.back.html create mode 100644 h5/bd-center/index.back.html create mode 100644 h5/bd-leader-center/index.back.html create mode 100644 h5/hyapp/agency-center/host-list.css create mode 100644 h5/hyapp/agency-center/host-list.html create mode 100644 h5/hyapp/agency-center/host-list.js create mode 100644 h5/hyapp/agency-center/index.html create mode 100644 h5/hyapp/agency-center/original-vue-logic.md create mode 100644 h5/hyapp/agency-center/script.js create mode 100644 h5/hyapp/agency-center/style.css create mode 100644 h5/hyapp/hosts-bill/index.html create mode 100644 h5/hyapp/hosts-bill/script.js create mode 100644 h5/hyapp/hosts-bill/style.css diff --git a/h5/agency-center/index.back.html b/h5/agency-center/index.back.html new file mode 100644 index 0000000..dda5b99 --- /dev/null +++ b/h5/agency-center/index.back.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + Agency Center + + + + +
+ + +
+
+
+ My available salary + +
+
$0.00
+
+ +
+
My task as a host today is:
+
+ +
+
Minutes
+
0/120 +
+
+ +
+
+ + +
+
+ + + + + + + + \ No newline at end of file diff --git a/h5/agency-center/index.html b/h5/agency-center/index.html index dda5b99..666ba46 100644 --- a/h5/agency-center/index.html +++ b/h5/agency-center/index.html @@ -1,165 +1,14 @@ - - - + + - - - - - - - - - - Agency Center - + + + Document - -
- - -
-
-
- My available salary - -
-
$0.00
-
- -
-
My task as a host today is:
-
- -
-
Minutes
-
0/120 -
-
- -
-
- - -
-
- - - - - + - \ No newline at end of file diff --git a/h5/bd-center/index.back.html b/h5/bd-center/index.back.html new file mode 100644 index 0000000..e53ccf0 --- /dev/null +++ b/h5/bd-center/index.back.html @@ -0,0 +1,97 @@ + + + + + + + + + + + + + BD Center + + + +
+ + +
+
+
+ My available salary + +
+
$0
+
+ + +
+
+ + + + + diff --git a/h5/bd-center/index.html b/h5/bd-center/index.html index c17605d..f5caf9d 100644 --- a/h5/bd-center/index.html +++ b/h5/bd-center/index.html @@ -1,97 +1,14 @@ - - - - - - - - - - - - - BD Center - - - -
- - -
-
-
- My available salary - -
-
$0
-
- - -
-
- - - - - + + + + + + Document + + + + + \ No newline at end of file diff --git a/h5/bd-leader-center/index.back.html b/h5/bd-leader-center/index.back.html new file mode 100644 index 0000000..a1b4538 --- /dev/null +++ b/h5/bd-leader-center/index.back.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + BD Leader Center + + + +
+
+ +

BD Leader Center

+ +
+ +
+
+
+
+
My available salary
+ +
+
+ + $0 +
+
+
+ + +
+
+ + + + + + + diff --git a/h5/bd-leader-center/index.html b/h5/bd-leader-center/index.html index a1b4538..d1a8e1d 100644 --- a/h5/bd-leader-center/index.html +++ b/h5/bd-leader-center/index.html @@ -1,140 +1,14 @@ - - - - - - - - - - - - - BD Leader Center - - - -
-
- -

BD Leader Center

- -
- -
-
-
-
-
My available salary
- -
-
- - $0 -
-
-
- - -
-
- - - - - - - + + + + + + Document + + + + + \ No newline at end of file diff --git a/h5/hyapp/agency-center/host-list.css b/h5/hyapp/agency-center/host-list.css new file mode 100644 index 0000000..525ad1a --- /dev/null +++ b/h5/hyapp/agency-center/host-list.css @@ -0,0 +1,466 @@ +.host-list-page { + overflow-x: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +.host-list-page[data-loading="true"] .title-bar, +.host-list-page[data-loading="true"] .content, +.host-list-page[data-loading="true"] .home-indicator { + visibility: hidden; +} + +.host-list-content { + gap: 12px; + padding-top: 8px; +} + +.team-overview-card { + display: grid; + grid-template-columns: 54px minmax(0, 1fr) auto; + align-items: center; + gap: 14px; + min-height: 96px; + padding: 17px 18px; +} + +.team-overview-icon { + display: flex; + align-items: center; + justify-content: center; + width: 54px; + height: 54px; + border-radius: 50%; + background: #e7fbf8; + color: #12a99c; +} + +.team-overview-icon svg { + width: 30px; + height: 30px; +} + +.team-overview-icon path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.35; +} + +.team-overview-copy { + min-width: 0; +} + +.team-overview-title { + overflow: hidden; + color: #24282e; + font-size: 18px; + font-weight: 950; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.team-overview-text { + display: -webkit-box; + margin-top: 6px; + overflow: hidden; + color: #8f9196; + font-size: 13px; + font-weight: 800; + line-height: 1.25; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.team-overview-count { + display: grid; + justify-items: end; + gap: 4px; + color: #8f9196; + font-size: 11px; + font-weight: 850; + line-height: 1.1; + text-transform: uppercase; +} + +.team-overview-count strong { + color: #24282e; + font-size: 24px; + font-weight: 950; + line-height: 1; +} + +.team-search-card { + padding: 12px; +} + +.team-search-form { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 8px; +} + +.team-search-control { + display: flex; + align-items: center; + min-width: 0; + height: 42px; + padding: 0 12px; + border: 1px solid #edf0f1; + border-radius: 8px; + background: #f8faf9; +} + +.team-search-control svg { + width: 20px; + height: 20px; + flex: 0 0 20px; + margin-right: 9px; + color: #7f858b; +} + +.team-search-control path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.2; +} + +.team-search-control input { + width: 100%; + min-width: 0; + border: 0; + outline: 0; + background: transparent; + color: #25282e; + font: inherit; + font-size: 14px; + font-weight: 800; +} + +.team-search-control input::placeholder { + color: #a7aab0; +} + +.team-search-button, +.team-cancel-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 64px; + height: 42px; + padding: 0 12px; + border-radius: 8px; + font-size: 13px; + font-weight: 950; + white-space: nowrap; +} + +.team-search-button { + background: var(--green); + color: #fff; + box-shadow: 0 6px 14px rgba(37, 216, 120, 0.18); +} + +.team-cancel-button { + background: #f2f4f5; + color: #4d525a; +} + +.team-member-list { + display: grid; + gap: 10px; +} + +.host-member-card { + display: grid; + grid-template-columns: 28px minmax(0, 1fr) auto 38px; + align-items: center; + gap: 10px; + min-height: 82px; + padding: 13px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.96); + box-shadow: var(--shadow); +} + +.host-member-card.is-self { + grid-template-columns: 28px minmax(0, 1fr) auto; +} + +.member-index { + color: #30343a; + font-size: 16px; + font-weight: 950; + text-align: center; +} + +.member-user { + display: flex; + align-items: center; + min-width: 0; +} + +.member-avatar { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + flex: 0 0 48px; + overflow: hidden; + border-radius: 50%; + background: linear-gradient(145deg, #55f2df 0%, #d9fff5 100%); + color: #13745f; + font-size: 19px; + font-weight: 950; +} + +.member-avatar img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.member-copy { + min-width: 0; + margin-left: 11px; +} + +.member-name { + overflow: hidden; + color: #24282e; + font-size: 15px; + font-weight: 950; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-account { + margin-top: 5px; + overflow: hidden; + color: #8f9196; + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-stats { + display: grid; + grid-template-columns: repeat(2, minmax(46px, auto)); + gap: 8px; +} + +.member-stat { + display: grid; + justify-items: center; + gap: 4px; + min-width: 0; +} + +.member-stat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + background: #eefbf8; + color: #17b99a; +} + +.member-stat-icon svg { + width: 16px; + height: 16px; +} + +.member-stat-icon path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.3; +} + +.member-stat strong { + max-width: 64px; + overflow: hidden; + color: #24282e; + font-size: 12px; + font-weight: 950; + line-height: 1.15; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-remove-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + background: #f6f7f7; + color: #70747b; +} + +.member-remove-button svg { + width: 20px; + height: 20px; +} + +.member-remove-button path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.15; +} + +.team-list-status { + display: flex; + align-items: center; + justify-content: center; + min-height: 54px; + color: #8f9196; + font-size: 13px; + font-weight: 850; + text-align: center; +} + +.team-list-status:empty { + display: none; +} + +.team-load-sentinel { + height: 1px; +} + +.team-confirm-modal[hidden] { + display: none; +} + +.team-confirm-modal { + position: fixed; + z-index: 28; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 18px; +} + +.team-confirm-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 21, 28, 0.38); +} + +.team-confirm-dialog { + position: relative; + width: min(360px, 100%); + padding: 20px 18px 18px; + border-radius: 14px; + background: #fff; + box-shadow: 0 18px 44px rgba(23, 31, 42, 0.2); +} + +.team-confirm-dialog h2 { + margin: 0; + color: #24282e; + font-size: 19px; + font-weight: 950; + line-height: 1.2; + text-align: center; +} + +.team-confirm-dialog p { + margin: 12px 0 0; + color: #7d8188; + font-size: 14px; + font-weight: 800; + line-height: 1.45; + text-align: center; +} + +.team-confirm-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: 18px; +} + +.team-confirm-cancel, +.team-confirm-submit { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 42px; + border-radius: 8px; + font-size: 14px; + font-weight: 950; +} + +.team-confirm-cancel { + border: 1px solid #e7eaec; + background: #fff; + color: #555a62; +} + +.team-confirm-submit { + background: var(--green); + color: #fff; +} + +.team-confirm-submit:disabled { + opacity: 0.62; +} + +html[dir="rtl"] .team-search-control svg { + margin-right: 0; + margin-left: 9px; +} + +html[dir="rtl"] .member-copy { + margin-right: 11px; + margin-left: 0; +} + +@media (max-width: 370px) { + .team-overview-card { + grid-template-columns: 48px minmax(0, 1fr); + } + + .team-overview-count { + grid-column: 2; + justify-self: start; + } + + .team-search-form { + grid-template-columns: minmax(0, 1fr) auto; + } + + .team-cancel-button { + grid-column: 2; + } + + .host-member-card { + grid-template-columns: 26px minmax(0, 1fr) 36px; + } + + .member-stats { + grid-column: 2 / 4; + justify-content: start; + } +} diff --git a/h5/hyapp/agency-center/host-list.html b/h5/hyapp/agency-center/host-list.html new file mode 100644 index 0000000..94900b3 --- /dev/null +++ b/h5/hyapp/agency-center/host-list.html @@ -0,0 +1,108 @@ + + + + + + Host List + + + + +
+ + + + + + +
+
+ +
+
Host List
+
Manage hosts in your organization
+
+
+ - + Members +
+
+ +
+
+
+ + +
+ + +
+
+ +
+
+ +
+ + + + + + +
+ +
loading....
+
+
+ + + diff --git a/h5/hyapp/agency-center/host-list.js b/h5/hyapp/agency-center/host-list.js new file mode 100644 index 0000000..fcbc70b --- /dev/null +++ b/h5/hyapp/agency-center/host-list.js @@ -0,0 +1,866 @@ +(function () { + const API_BASE_URL = "https://jvapi.haiyihy.com"; + const PAGE_SIZE = 20; + const supportedLanguages = ["en", "ar", "tr", "id"]; + const languageLabels = { + en: "EN", + ar: "AR", + tr: "TR", + id: "ID" + }; + + const translations = { + en: { + document_title: "Host List", + page_label: "Host List", + back: "Back", + title: "Host List", + host_list: "Host List", + team_member_subtitle: "Manage hosts in your organization", + members: "Members", + language_button_aria: "Change language", + search: "Search", + cancel: "Cancel", + confirm: "Confirm", + tips: "Tips", + enter_host_id: "Enter host ID", + uid_prefix: "UID:", + target: "Target", + work_time: "Work Time", + loading: "Loading", + loading_more: "Loading more", + no_more_data: "No more data", + no_members: "No hosts", + no_search_result: "No matching hosts", + remove_member: "Remove host", + confirm_remove_member_start: "Confirm remove", + confirm_remove_member_end: "from your organization?", + member_removed: "Host removed", + missing_token: "Missing token", + missing_token_text: "Open this page from the app or add a token in the URL.", + missing_team: "Missing team information", + failed_to_load_data: "Failed to load data", + failed_to_delete_member: "Failed to remove host", + operation_failed: "Operation failed", + unknown_user: "Unknown user" + }, + ar: { + document_title: "قائمة المضيفين", + page_label: "قائمة المضيفين", + back: "رجوع", + title: "قائمة المضيفين", + host_list: "قائمة المضيفين", + team_member_subtitle: "إدارة المضيفين في منظمتك", + members: "الأعضاء", + language_button_aria: "تغيير اللغة", + search: "بحث", + cancel: "إلغاء", + confirm: "تأكيد", + tips: "تنبيه", + enter_host_id: "أدخل معرف المضيف", + uid_prefix: "UID:", + target: "الهدف", + work_time: "وقت العمل", + loading: "جار التحميل", + loading_more: "جار تحميل المزيد", + no_more_data: "لا توجد بيانات أخرى", + no_members: "لا يوجد مضيفون", + no_search_result: "لا توجد نتائج مطابقة", + remove_member: "إزالة المضيف", + confirm_remove_member_start: "تأكيد إزالة", + confirm_remove_member_end: "من منظمتك؟", + member_removed: "تمت إزالة المضيف", + missing_token: "رمز الدخول مفقود", + missing_token_text: "افتح هذه الصفحة من التطبيق أو أضف token في الرابط.", + missing_team: "معلومات الفريق مفقودة", + failed_to_load_data: "فشل تحميل البيانات", + failed_to_delete_member: "فشلت إزالة المضيف", + operation_failed: "فشلت العملية", + unknown_user: "مستخدم غير معروف" + }, + tr: { + document_title: "Host Listesi", + page_label: "Host Listesi", + back: "Geri", + title: "Host Listesi", + host_list: "Host Listesi", + team_member_subtitle: "Organizasyonunuzdaki hostlari yonetin", + members: "Uyeler", + language_button_aria: "Dili degistir", + search: "Ara", + cancel: "Iptal", + confirm: "Onayla", + tips: "Ipuclari", + enter_host_id: "Host ID girin", + uid_prefix: "UID:", + target: "Hedef", + work_time: "Calisma Suresi", + loading: "Yukleniyor", + loading_more: "Daha fazla yukleniyor", + no_more_data: "Baska veri yok", + no_members: "Host yok", + no_search_result: "Eslesen host yok", + remove_member: "Hostu kaldir", + confirm_remove_member_start: "Kaldirmayi onayla:", + confirm_remove_member_end: "organizasyonunuzdan?", + member_removed: "Host kaldirildi", + missing_token: "Token eksik", + missing_token_text: "Bu sayfayi uygulamadan acin veya URL'ye token ekleyin.", + missing_team: "Takim bilgisi eksik", + failed_to_load_data: "Veri yuklenemedi", + failed_to_delete_member: "Host kaldirilamadi", + operation_failed: "Islem basarisiz", + unknown_user: "Bilinmeyen kullanici" + }, + id: { + document_title: "Daftar Host", + page_label: "Daftar Host", + back: "Kembali", + title: "Daftar Host", + host_list: "Daftar Host", + team_member_subtitle: "Kelola host di organisasi Anda", + members: "Anggota", + language_button_aria: "Ubah bahasa", + search: "Cari", + cancel: "Batal", + confirm: "Konfirmasi", + tips: "Tips", + enter_host_id: "Masukkan ID host", + uid_prefix: "UID:", + target: "Target", + work_time: "Waktu Kerja", + loading: "Memuat", + loading_more: "Memuat lagi", + no_more_data: "Tidak ada data lagi", + no_members: "Tidak ada host", + no_search_result: "Tidak ada host yang cocok", + remove_member: "Hapus host", + confirm_remove_member_start: "Konfirmasi hapus", + confirm_remove_member_end: "dari organisasi Anda?", + member_removed: "Host dihapus", + missing_token: "Token tidak ditemukan", + missing_token_text: "Buka halaman ini dari aplikasi atau tambahkan token di URL.", + missing_team: "Informasi tim tidak ditemukan", + failed_to_load_data: "Gagal memuat data", + failed_to_delete_member: "Gagal menghapus host", + operation_failed: "Operasi gagal", + unknown_user: "Pengguna tidak dikenal" + } + }; + + const params = readURLParams(); + let currentLang = normalizeLanguage(params.get("lang")); + let currentMessages = translations[currentLang] || translations.en; + let currentTeamId = params.get("teamId") || params.get("id") || ""; + let currentTotalMembers = parseOptionalNumber(params.get("members")); + let currentProfile = null; + let members = []; + let displayMembers = []; + let lastId = ""; + let noMoreData = false; + let loadingList = false; + let loadingMore = false; + let searchActive = false; + let pendingRemoveMember = null; + let loadObserver = null; + + 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"; + } + + function t(key, fallback) { + return currentMessages[key] || translations.en[key] || fallback || key; + } + + function parseOptionalNumber(value) { + if (value === null || value === undefined || value === "") return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; + } + + function apiBaseURL() { + const override = window.HYAPP_API_BASE_URL || params.get("apiBase"); + if (override) return String(override).replace(/\/+$/, ""); + return API_BASE_URL.replace(/\/+$/, ""); + } + + function normalizeAuthorization(token) { + const value = String(token || "").trim(); + if (!value) return ""; + return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`; + } + + function authHeadersFromToken(token) { + 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=ATYOU;originChild=ATYOU" + }; + } + + async function requestJSON(path, options = {}) { + const token = readRawParam("token"); + if (!token) throw new Error(t("missing_token", "Missing token")); + + const headers = authHeadersFromToken(token); + const method = options.method || "GET"; + const hasBody = options.body !== undefined; + if (hasBody) headers["Content-Type"] = "application/json"; + + const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), { + method, + cache: "no-store", + headers, + body: hasBody ? JSON.stringify(options.body) : undefined + }); + 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.response = data; + throw error; + } + return data.body ?? data.data ?? null; + } + + function buildPageURL(target) { + const url = new URL(target, window.location.href); + const nextParams = new URLSearchParams(url.search); + + params.forEach((value, key) => { + if (!nextParams.has(key)) nextParams.set(key, value); + }); + + const token = readRawParam("token"); + if (token) nextParams.set("token", token); + if (currentLang) nextParams.set("lang", currentLang); + if (currentTeamId) nextParams.set("teamId", currentTeamId); + + url.search = nextParams.toString(); + return url.toString(); + } + + function setLoading(isLoading) { + const root = document.querySelector(".host-list-page"); + const loadingMask = document.querySelector("#loadingMask"); + if (root) root.dataset.loading = isLoading ? "true" : "false"; + if (loadingMask) { + loadingMask.hidden = !isLoading; + loadingMask.setAttribute("aria-busy", String(isLoading)); + } + } + + function renderLanguageControls() { + const languageButton = document.querySelector(".language-button"); + if (languageButton) { + languageButton.textContent = languageLabels[currentLang]; + languageButton.setAttribute("aria-label", t("language_button_aria")); + } + + 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 renderMessages() { + document.querySelectorAll("[data-i18n]").forEach((node) => { + const value = currentMessages[node.dataset.i18n]; + if (value) node.textContent = value; + }); + + document.querySelectorAll("[data-i18n-aria]").forEach((node) => { + const value = currentMessages[node.dataset.i18nAria]; + if (value) node.setAttribute("aria-label", value); + }); + + document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => { + const value = currentMessages[node.dataset.i18nPlaceholder]; + if (value) node.setAttribute("placeholder", value); + }); + } + + function applyLanguage(lang) { + currentLang = normalizeLanguage(lang); + currentMessages = translations[currentLang] || translations.en; + document.documentElement.lang = currentLang; + document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; + document.title = t("document_title"); + renderMessages(); + renderLanguageControls(); + renderTotalMembers(); + renderList(); + renderRemoveConfirm(); + } + + function setLanguageMenuOpen(open) { + const languageButton = document.querySelector(".language-button"); + const languageMenu = document.querySelector(".language-menu"); + if (!languageButton || !languageMenu) return; + languageMenu.hidden = !open; + languageButton.setAttribute("aria-expanded", String(open)); + } + + function pickTeamInfo(body) { + return body?.teamProfile || body || null; + } + + function pickTeamId(teamInfo) { + return teamInfo?.id || teamInfo?.teamId || teamInfo?.teamProfile?.id || ""; + } + + function pickMemberCount(body) { + if (typeof body === "number") return body; + if (body && typeof body === "object") { + return body.memberQuantity ?? body.count ?? body.total ?? body.memberCount ?? null; + } + return null; + } + + function pickRecords(body) { + if (Array.isArray(body)) return body; + if (Array.isArray(body?.records)) return body.records; + if (Array.isArray(body?.list)) return body.list; + if (Array.isArray(body?.items)) return body.items; + return []; + } + + function pickMemberProfile(member) { + return member?.userProfile || member?.memberProfile || member?.profile || member?.user || {}; + } + + function pickAccount(profile, member) { + return profile?.ownSpecialId?.account || profile?.account || member?.account || member?.userAccount || member?.userId || member?.id || "-"; + } + + function pickMemberName(profile, member) { + return profile?.userNickname || profile?.name || member?.userNickname || member?.name || t("unknown_user"); + } + + function collectUserIdentifiers(profile = {}) { + return [ + profile?.id, + profile?.account, + profile?.ownSpecialId?.account + ] + .filter((value) => value !== null && value !== undefined && value !== "") + .map((value) => String(value)); + } + + function collectMemberUserIdentifiers(member = {}) { + const profile = pickMemberProfile(member); + return [ + profile?.id, + profile?.account, + profile?.ownSpecialId?.account, + member?.userId, + member?.userAccount, + member?.account + ] + .filter((value) => value !== null && value !== undefined && value !== "") + .map((value) => String(value)); + } + + function isCurrentUserMember(member) { + const currentIds = new Set(collectUserIdentifiers(currentProfile)); + if (!currentIds.size) return false; + return collectMemberUserIdentifiers(member).some((value) => currentIds.has(value)); + } + + function formatCountValue(value) { + if (value === null || value === undefined || value === "") return "-"; + const number = Number(value); + if (Number.isFinite(number) && Number.isInteger(number)) return String(number); + if (Number.isFinite(number)) return String(Number(number.toFixed(2))); + return String(value); + } + + function normalizeMember(member) { + const profile = pickMemberProfile(member); + const name = pickMemberName(profile, member); + return { + raw: member, + id: member?.id || member?.memberId || member?.userId || profile?.id || "", + name, + avatar: profile?.userAvatar || member?.userAvatar || member?.avatar || "", + account: pickAccount(profile, member), + target: member?.targetFormat ?? member?.target ?? member?.giftTarget ?? member?.giftValue ?? "-", + time: member?.timeHours ?? member?.onlineTime ?? member?.workTime ?? "-", + isSelf: isCurrentUserMember(member) + }; + } + + function createSvg(paths) { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("aria-hidden", "true"); + paths.forEach((pathValue) => { + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", pathValue); + svg.appendChild(path); + }); + return svg; + } + + function createMemberAvatar(member) { + const avatar = document.createElement("div"); + avatar.className = "member-avatar"; + + const fallback = document.createElement("span"); + fallback.textContent = (member.name || t("unknown_user")).slice(0, 1).toUpperCase(); + avatar.appendChild(fallback); + + if (member.avatar) { + const image = document.createElement("img"); + image.src = member.avatar; + image.alt = member.name; + image.addEventListener("error", () => { + image.remove(); + }); + avatar.appendChild(image); + } + + return avatar; + } + + function createStat(iconPaths, label, value) { + const stat = document.createElement("div"); + stat.className = "member-stat"; + stat.setAttribute("aria-label", `${label}: ${value}`); + + const icon = document.createElement("span"); + icon.className = "member-stat-icon"; + icon.appendChild(createSvg(iconPaths)); + + const text = document.createElement("strong"); + text.textContent = formatCountValue(value); + text.title = `${label}: ${formatCountValue(value)}`; + + stat.appendChild(icon); + stat.appendChild(text); + return stat; + } + + function createMemberCard(member, index) { + const normalized = normalizeMember(member); + const card = document.createElement("article"); + card.className = "host-member-card"; + if (normalized.isSelf) card.classList.add("is-self"); + + const indexNode = document.createElement("div"); + indexNode.className = "member-index"; + indexNode.textContent = String(index + 1); + + const user = document.createElement("div"); + user.className = "member-user"; + user.appendChild(createMemberAvatar(normalized)); + + const copy = document.createElement("div"); + copy.className = "member-copy"; + + const name = document.createElement("div"); + name.className = "member-name"; + name.textContent = normalized.name; + + const account = document.createElement("div"); + account.className = "member-account"; + account.textContent = `${t("uid_prefix")} ${normalized.account}`.trim(); + + copy.appendChild(name); + copy.appendChild(account); + user.appendChild(copy); + + const stats = document.createElement("div"); + stats.className = "member-stats"; + stats.appendChild(createStat(["M12 3 14.4 8.1 20 8.8 15.8 12.7 16.9 18.2 12 15.4 7.1 18.2 8.2 12.7 4 8.8 9.6 8.1Z"], t("target"), normalized.target)); + stats.appendChild(createStat(["M12 6v6l4 2M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"], t("work_time"), normalized.time)); + + card.appendChild(indexNode); + card.appendChild(user); + card.appendChild(stats); + if (!normalized.isSelf) { + const remove = document.createElement("button"); + remove.type = "button"; + remove.className = "member-remove-button"; + remove.setAttribute("aria-label", t("remove_member")); + remove.appendChild(createSvg(["M4 7h16M10 11v6M14 11v6M6 7l1 14h10l1-14M9 7l1-3h4l1 3"])); + remove.addEventListener("click", () => { + setRemoveConfirmOpen(true, normalized); + }); + card.appendChild(remove); + } + return card; + } + + function replaceNodeChildren(node, children) { + while (node.firstChild) node.removeChild(node.firstChild); + children.forEach((child) => node.appendChild(child)); + } + + function setListStatus(message) { + const status = document.querySelector("#listStatus"); + if (status) status.textContent = message || ""; + } + + function renderTotalMembers() { + const totalNode = document.querySelector("#memberTotal"); + if (!totalNode) return; + const number = Number(currentTotalMembers); + totalNode.textContent = Number.isFinite(number) ? String(Math.max(0, Math.trunc(number))) : "-"; + } + + function renderSearchState() { + const searchButton = document.querySelector("#memberSearchButton"); + const cancelButton = document.querySelector("#memberCancelButton"); + if (searchButton) searchButton.hidden = false; + if (cancelButton) cancelButton.hidden = !searchActive; + } + + function renderList() { + const list = document.querySelector("#memberList"); + if (!list) return; + const normalizedItems = Array.isArray(displayMembers) ? displayMembers : []; + replaceNodeChildren(list, normalizedItems.map((member, index) => createMemberCard(member, index))); + renderSearchState(); + + if (loadingList) { + setListStatus(`${t("loading")}...`); + return; + } + + if (loadingMore) { + setListStatus(`${t("loading_more")}...`); + return; + } + + if (!normalizedItems.length) { + setListStatus(searchActive ? t("no_search_result") : t("no_members")); + return; + } + + if (!searchActive && noMoreData) { + setListStatus(t("no_more_data")); + return; + } + + setListStatus(""); + } + + function renderRemoveConfirm() { + const text = document.querySelector("#removeConfirmText"); + if (!text) return; + if (!pendingRemoveMember) { + text.textContent = ""; + return; + } + text.textContent = `${t("confirm_remove_member_start")} ${pendingRemoveMember.account || pendingRemoveMember.name} ${t("confirm_remove_member_end")}`; + } + + function setRemoveConfirmOpen(open, member = null) { + const modal = document.querySelector("#removeConfirmModal"); + if (!modal) return; + pendingRemoveMember = open ? member : null; + modal.hidden = !open; + document.body.classList.toggle("daily-modal-open", open); + renderRemoveConfirm(); + } + + function showToast(message) { + const toast = document.querySelector("#toast"); + if (!toast) return; + toast.textContent = message; + toast.hidden = false; + window.clearTimeout(showToast.timer); + showToast.timer = window.setTimeout(() => { + toast.hidden = true; + }, 2200); + } + + async function ensureTeamId() { + if (currentTeamId) return currentTeamId; + const body = await requestJSON("/team/entry"); + currentTeamId = pickTeamId(pickTeamInfo(body)); + return currentTeamId; + } + + async function fetchCurrentProfile() { + currentProfile = await requestJSON("/team/member/profile"); + return currentProfile; + } + + async function fetchMemberCount(teamId) { + if (!teamId) return; + try { + const query = new URLSearchParams({ id: String(teamId) }); + const body = await requestJSON(`/team/members/count?${query.toString()}`); + currentTotalMembers = pickMemberCount(body); + renderTotalMembers(); + } catch (error) { + console.error("Failed to load host count:", error); + renderTotalMembers(); + } + } + + async function fetchMembers(loadMore = false) { + if (!currentTeamId || loadingList || loadingMore || searchActive) return; + if (loadMore && noMoreData) return; + + try { + if (loadMore) { + loadingMore = true; + } else { + loadingList = true; + lastId = ""; + noMoreData = false; + members = []; + displayMembers = []; + } + renderList(); + + const query = new URLSearchParams({ id: String(currentTeamId) }); + if (loadMore && lastId) query.set("lastId", String(lastId)); + const body = await requestJSON(`/team/members?${query.toString()}`); + const nextMembers = pickRecords(body); + + if (loadMore) { + members = members.concat(nextMembers); + } else { + members = nextMembers; + } + + displayMembers = members.slice(); + if (nextMembers.length > 0) { + lastId = nextMembers[nextMembers.length - 1]?.id || lastId; + } + if (nextMembers.length < PAGE_SIZE) noMoreData = true; + } catch (error) { + console.error("Failed to load hosts:", error); + showToast(error.message || t("failed_to_load_data")); + } finally { + loadingList = false; + loadingMore = false; + renderList(); + } + } + + async function searchMembers() { + const input = document.querySelector("#memberSearchInput"); + const account = String(input?.value || "").trim(); + if (!account) { + cancelSearch(); + return; + } + + searchActive = true; + loadingList = true; + displayMembers = []; + renderList(); + + try { + const query = new URLSearchParams({ account }); + const body = await requestJSON(`/team/search-members?${query.toString()}`); + displayMembers = pickRecords(body); + } catch (error) { + console.error("Failed to search hosts:", error); + showToast(error.message || t("failed_to_load_data")); + displayMembers = []; + } finally { + loadingList = false; + renderList(); + } + } + + function cancelSearch() { + const input = document.querySelector("#memberSearchInput"); + if (input) input.value = ""; + searchActive = false; + displayMembers = members.slice(); + renderList(); + } + + async function removeCurrentMember() { + if (!pendingRemoveMember?.id) return; + if (pendingRemoveMember.isSelf) return; + const button = document.querySelector("#confirmRemoveButton"); + if (button) button.disabled = true; + + try { + await requestJSON("/team-member/client/removeMemberTeam", { + method: "POST", + body: { + ids: [pendingRemoveMember.id] + } + }); + + const removedId = String(pendingRemoveMember.id); + members = members.filter((member) => String(normalizeMember(member).id) !== removedId); + displayMembers = displayMembers.filter((member) => String(normalizeMember(member).id) !== removedId); + if (Number.isFinite(Number(currentTotalMembers))) { + currentTotalMembers = Math.max(0, Number(currentTotalMembers) - 1); + } + renderTotalMembers(); + renderList(); + setRemoveConfirmOpen(false); + showToast(t("member_removed")); + } catch (error) { + console.error("Failed to remove host:", error); + showToast(error.message || t("failed_to_delete_member")); + } finally { + if (button) button.disabled = false; + } + } + + function goBack() { + if (window.history.length > 1) { + window.history.back(); + return; + } + window.location.href = buildPageURL("./index.html"); + } + + function setupLoadObserver() { + const sentinel = document.querySelector("#loadMoreSentinel"); + const root = document.querySelector(".host-list-page"); + if (!sentinel || !root || !("IntersectionObserver" in window)) return; + + if (loadObserver) loadObserver.disconnect(); + loadObserver = new IntersectionObserver((entries) => { + if (entries[0]?.isIntersecting && !searchActive && !loadingList && !loadingMore && !noMoreData) { + fetchMembers(true); + } + }, { + root, + rootMargin: "140px 0px" + }); + loadObserver.observe(sentinel); + } + + function bindEvents() { + document.querySelector(".back-button")?.addEventListener("click", goBack); + + document.querySelector(".language-button")?.addEventListener("click", (event) => { + event.stopPropagation(); + const languageMenu = document.querySelector(".language-menu"); + setLanguageMenuOpen(Boolean(languageMenu?.hidden)); + }); + + document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { + button.addEventListener("click", (event) => { + event.stopPropagation(); + setLanguageMenuOpen(false); + applyLanguage(button.dataset.lang); + }); + }); + + document.querySelector("#teamSearchForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + searchMembers(); + }); + + document.querySelector("#memberCancelButton")?.addEventListener("click", cancelSearch); + document.querySelector("#confirmRemoveButton")?.addEventListener("click", removeCurrentMember); + + document.querySelectorAll("[data-close-remove]").forEach((node) => { + node.addEventListener("click", () => { + setRemoveConfirmOpen(false); + }); + }); + + const root = document.querySelector(".host-list-page"); + root?.addEventListener("scroll", () => { + if (searchActive || loadingList || loadingMore || noMoreData) return; + const distanceToBottom = root.scrollHeight - root.scrollTop - root.clientHeight; + if (distanceToBottom < 140) fetchMembers(true); + }, { passive: true }); + + document.addEventListener("click", () => { + setLanguageMenuOpen(false); + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + setRemoveConfirmOpen(false); + setLanguageMenuOpen(false); + } + }); + } + + async function initializePage() { + setLoading(true); + applyLanguage(currentLang); + bindEvents(); + setupLoadObserver(); + renderTotalMembers(); + + if (!readRawParam("token")) { + setLoading(false); + setListStatus(t("missing_token_text")); + return; + } + + try { + await ensureTeamId(); + if (!currentTeamId) throw new Error(t("missing_team")); + await fetchCurrentProfile(); + + await Promise.allSettled([ + fetchMemberCount(currentTeamId), + fetchMembers(false) + ]).then((results) => { + results.forEach((result) => { + if (result.status === "rejected") console.warn("Host list request failed:", result.reason); + }); + }); + } catch (error) { + console.error("Failed to initialize host list:", error); + showToast(error.message || t("failed_to_load_data")); + setListStatus(error.message || t("failed_to_load_data")); + } finally { + setLoading(false); + } + } + + initializePage(); +})(); diff --git a/h5/hyapp/agency-center/index.html b/h5/hyapp/agency-center/index.html new file mode 100644 index 0000000..634cac8 --- /dev/null +++ b/h5/hyapp/agency-center/index.html @@ -0,0 +1,235 @@ + + + + + + Agency Center + + + +
+ + + + + + +
+
+
+ + +
+
+
Agency
+
UID: -
+
BD: -
+
+ +
+ + + +
+
+
Work Time (min)
+
0
+
+ +
+
Target
+ +
+
+ +
+
+
+ Salary + +
+ + Withdraw/Exchange + + +
+ + + 0.00 + + +
+ +
+ + + + Host List + + + - + + + +
+ +
+ + + + Salary Bills + + + +
+ +
+ + + + Agency Policy + + + + + +
+
+ + + + + + + + + + +
+ +
loading....
+
+
+ + + diff --git a/h5/hyapp/agency-center/original-vue-logic.md b/h5/hyapp/agency-center/original-vue-logic.md new file mode 100644 index 0000000..24fe08b --- /dev/null +++ b/h5/hyapp/agency-center/original-vue-logic.md @@ -0,0 +1,336 @@ +# 原 Vue Agency Center 逻辑清单 + +来源: +- `chatapp-h5/h5/js/index--RUKA0EA-1776148658686.js` +- `chatapp-h5/h5/js/pageConfig-BUpLPO7w-1776148658686.js` +- `chatapp-h5/h5/js/teamBill-BWXvrc1S-1776148658686.js` +- `chatapp-h5/h5/js/index-CIAVq8iD-1776148658686.js` +- 旧静态复刻:`chatapp-h5/h5/agency-center/agency-center.js` + +## 页面初始化 + +- 初始化 i18n:读取当前 locale,并调用语言切换逻辑。 +- 初始化 Vue Router。 +- 初始化 identity store,并设置身份为 `AGENCY`。 +- 清空 payee store。 +- 使用 `usePageConfig("AGENCY_CENTER", { needsTeamInfo: true, forceRefresh: true, onDataLoaded })`。 +- 页面 mounted 后触发: + - `identity = "AGENCY"` + - 获取银行卡列表 + - 获取提现/KYC 信息 + - 获取主播工资提现是否可用 + - 清空收款人缓存 +- `usePageConfig` 完成基础数据加载后执行: + - 获取团队成员数量 + - 获取团队账单概要 + - 获取当前用户成员工作汇总 + +## App 连接与请求头 + +- 判断是否在 App/WebView 环境。 +- 如果已有连接且请求头缓存有效,复用缓存请求头。 +- 如果未连接,调用 App bridge 获取 access origin 参数。 +- 解析 App 返回参数后写入 HTTP 请求头。 +- 关键请求头: + - `Authorization` + - `Req-Lang` + - `Req-App-Intel` + - `Req-Imei` + - `Req-Sys-Origin` +- 连接失败时可能跳转 `/not_app`。 +- 新 `hyapp` 页面可沿用 Host Center 的 URL token 模式:从 `?token=` 或 hash query 读取 token,再转为 `Authorization: Bearer ` 请求 Java 接口。 + +## 用户与团队信息 + +### GET `/team/member/profile` + +- 用途:获取当前用户信息。 +- 显式查询参数:无。 +- 依赖:`Authorization` 请求头。 +- 原 Vue 展示或缓存字段: + - `id` + - `account` + - `userNickname` + - `name` + - `userAvatar` + - `ownSpecialId.account` +- `usePageConfig` 中写入 `userInfo`: + - `id = body.account` + - `account = body.account` + - `userNickname = body.userNickname` + - `userAvatar = body.userAvatar` +- 旧静态复刻中保留完整 `body` 到 `state.userProfile`,并缓存到 `localStorage.userProfile`。 + +### GET `/team/entry` + +- 用途:获取当前用户所属团队入口信息。 +- 显式查询参数:无。 +- 依赖:`Authorization` 请求头。 +- 原 Vue 使用 `body.teamProfile || body` 作为团队信息。 +- 团队 ID 来源:`teamProfile.id || body.id`。 +- 旧静态复刻缓存到 `localStorage.teamInfo`。 +- `team/entry` 失败或空返回时,团队信息置空。 + +### GET `/team/bd-profile` + +- 用途:获取当前用户所在团队关联的 BD 用户资料。 +- 显式查询参数:无。 +- 依赖:`Authorization` 请求头。 +- 后端实现:当前登录用户 -> `TeamMember.teamId` -> `BusinessDevelopmentTeamService.getBdUserIdByTeamId(teamId)` -> `UserProfileDTO`。 +- 原 Vue agency-center 未直接调用该接口;该接口由 Java 后端已有的 `TeamRestController#getTeamBdProfile` 提供。 + +## 身份与权限 + +### GET `/app/h5/identity` + +- 用途:获取 H5 身份和路由权限。 +- 显式查询参数:无。 +- 原逻辑根据返回字段生成 roles: + - `freightAgent` + - `agent` + - `bdLeader` + - `bd` + - `anchor` + - `admin` +- 主角色优先级: + - `guest` + - `anchor` + - `agent` + - `bd` + - `bdLeader` + - `freightAgent` + - `admin` +- 默认跳转页: + - `admin` -> `/admin-center` + - `freightAgent` -> `/coin-seller` + - `agent` -> `/agency-center` + - `bdLeader` -> `/bd-leader-center` + - `bd` -> `/bd-center` + - `anchor` -> `/host-center` + - `guest` -> `/apply` +- Agency Center 允许 `agent` 访问;旧权限配置里 `freightAgent` 也允许访问 `/agency-center`。 +- 如果当前路由不在允许列表中,跳转到主角色默认页。 + +## 工资/余额 + +### GET `/wallet/bank/balance` + +- 用途:获取可用工资余额。 +- 显式查询参数:无。 +- 原 Vue `usePageConfig` 写入 `salaryInfo.currentSalary`。 +- 展示字段:`$${salaryInfo.currentSalary}`。 +- 原逻辑只在 `body` 是 number 时使用 `body.toFixed(2)`,否则展示 `0`。 +- 请求失败时展示 `0`,并 toast:`Failed to load balance. Please try again.` +- 新页面如果展示 Agency 可用工资,优先复用这个数据源。 + +## 今日任务/主播任务 + +### GET `/team/member/work/statistics-now` + +- 用途:获取当前用户今日工作统计。 +- 查询参数: + - `dataType=DAILY` + - `memberId=<当前用户 id/account>` +- 原 Vue `taskInfo`: + - `anchorType = "Chat"` + - `minutesProgress = min(ownTime + otherTime, 120)` + - `minutesTotal = 120` + - `giftTask = acceptGiftValue || 0` +- 原 Agency Center 组件展示三个格子: + - `Host type` + - `Minutes` + - `Gift Task` +- 旧静态复刻里实际只展示 `Minutes`,`Host type` 和 `Gift Task` HTML 被注释。 + +## 成员工作汇总 + +### GET `/team/member-work` + +- 用途:获取当前用户工作目标和结算汇总。 +- 查询参数: + - `userId=<当前用户 id>` +- 原函数名:`fetchMemberWorkTotal`。 +- 原逻辑: + - 读取 `body.targets || []` + - 跳过 `targets[0]` + - 再请求或读取 identity 判断身份 + - 如果是 `agent`:累加 `target.settlementResult.ownSalary + target.settlementResult.memberSalary` + - 如果是 `anchor`:累加 `target.settlementResult.memberSalary` + - 写入 `memberWorkTotal = total.toFixed(2)` +- 注意:Agency Center 主模板里没有明确展示 `memberWorkTotal`,这是页面加载时保留的汇总逻辑。 + +## 团队成员数量 + +### GET `/team/members/count` + +- 用途:获取团队成员数量。 +- 查询参数: + - `id=` +- 团队 ID 来源:`/team/entry` 的 `teamProfile.id || id`。 +- 返回使用: + - `body.memberQuantity` +- 原点击 Team Member 时会把成员数量带到 URL: + - `/team-member?source=agency-center&members=` +- 请求失败时旧静态复刻 fallback 为 `87`,Vue 构建产物 catch 中也有相同 fallback。 + +## 团队账单概要 + +### GET `/team/bill` + +- 用途:获取团队账单列表,Agency Center 首页只取第一条作为概要。 +- 查询参数: + - `id=` +- 原逻辑: + - 读取 `body[0]` + - `billBelong` 形如 `YYYYMM`,展示为 `YYYY.MM` + - `hostSalary = settleResult.memberSalary.toFixed(2) || "–"` + - `agentSalary = settleResult.ownSalary.toFixed(2) || "–"` + - `total = settleResult.totalSalary.toFixed(2) || "–"` +- 状态映射: + - `SETTLED` -> `Completed` + - `UNPAID` -> `In progress` + - `HANG_UP` -> `Out of account` + - `PAY_OUT` -> `Completed` +- 状态颜色逻辑: + - `In progress`:蓝色 + - `Pending`:红色 + - `Completed`:绿色 + - `Out of account`:灰色 +- 注意:Agency Center 首页初始化了 `teamBill`,但主模板只提供 Team Bill 入口,不直接展示账单卡片。 + +## 团队政策 + +### GET `/team/release/policy` + +- 用途:Platform Policy 页面使用,不在 Agency Center 首页直接渲染。 +- 查询参数: + - `id=` +- 原 `PlatformPolicy` 通过 URL `from=agencycenter` 判断是否展示代理字段。 +- 当 `from=agencycenter` 时,政策卡片展示: + - `time/hours` + - `target` + - `host_salary` + - `agent_salary` + - `total_salary` +- 线上 Java 接口需要参数名 `id`,不能用 `teamId`。 + +## 银行卡与提现信息 + +### GET `/wallet/bank-card/list` + +- 用途:获取银行卡/收款方式列表。 +- 显式查询参数:无。 +- 原逻辑: + - `bankCards = body` + - `defaultBankCard = body.find(item => item.use) || null` + +### GET `/wallet/withdraw-info/list` + +- 用途:获取提现/KYC 信息。 +- 显式查询参数:无。 +- 原逻辑: + - 如果 `body.length > 0`,取第一条 + - `passportFrontUrl` 按 JSON 字符串解析成 `previewUrls` + - 合并成 `withdrawInfo` +- 点击提现时如果 `withdrawInfo` 缺失,会弹出补全信息弹窗。 + +### GET `/wallet/bank/host-salary/withdraw/enabled` + +- 用途:控制是否展示 `Cash withdraw` 入口。 +- 显式查询参数:无。 +- 原 Agency Center 仍然调用 `host-salary` 提现开关。 +- 返回使用: + - `withdrawEnabled = Boolean(body)` + +## 点击提现逻辑 + +- 计算值:`missingWithdrawInfo = withdrawInfo == null || defaultBankCard == null` +- 点击 `Cash withdraw`: + - 如果缺提现信息或默认银行卡,打开补全信息弹窗。 + - 否则跳转 `/cash-withdraw`。 +- 补全信息弹窗: + - 标题:`prompt` + - 主文案:`fill_info_tips` + - 辅助文案:`card_issue_tips` + - 按钮:`to_improve` + - 按钮跳转 `/agency-setting` + +## 页面展示结构 + +- 顶部: + - `GeneralHeader` + - 标题:`agency_center` + - 右侧消息图标,点击跳转 `/message` + - 原 Agency Center 没有未读红点逻辑。 +- 消息页: + - `Message` 页面 mounted 后请求 `/team/user/apply/record?teamId=&status=WAIT`。 + - 返回的 WAIT 记录作为待处理入团申请列表展示。 + - 可在新 Agency Center 中复用该 WAIT 记录数量作为消息红点依据;原 Vue 没有单独的 unread 字段或 unread 接口。 + - 原 Message 记录格式: + - `createUserProfile.userAvatar` + - `createUserProfile.userNickname` + - `createUserProfile.account` + - 操作按钮:`Agree`、`Refuse` + - 同意/拒绝接口:`POST /team/process/user/apply`,body 为 `{ id, status }`,`status` 可取 `AGREE` 或 `REJECT`。 +- 工资卡片: + - 标题:`my_available_salary` + - 右侧按钮:`my_salary_details` + - 金额:`$${salaryInfo.currentSalary}` +- 今日任务卡片: + - 标题:`today_host_task` + - 原 Vue 展示 `Host type`、`Minutes`、`Gift Task` + - 旧静态复刻只展示 `Minutes` +- 菜单卡片: + - `Platform policy` + - `Exchange coins` + - `Transfer` + - `Cash withdraw`,受提现开关控制 + - `Bank Accounts` + - `Team Member` + - `Team Bill` + - `Invite Members` + +## 点击跳转 + +- 消息:`/message` +- 工资详情:`/history-salary` +- 平台政策:`/platform-policy?from=agencycenter` +- 兑换金币:`/exchange-gold-coins` +- 转账:`/transfer` +- 提现:`/cash-withdraw` +- 银行卡/收款信息:`/agency-setting` +- 团队成员:`/team-member?source=agency-center&members=` +- 团队账单:`/team-bill` +- 邀请成员:`/invite-members` +- 补全提现信息:`/agency-setting` + +## 可复用到新 `h5/hyapp/agency-center` 的数据源 + +- 当前用户信息:`GET /team/member/profile` + - 可展示头像、昵称、UID、靓号。 +- 当前团队信息:`GET /team/entry` + - 可取 agency/team 名称、teamId,并作为团队成员、账单、政策接口的前置数据。 +- 当前团队关联 BD:`GET /team/bd-profile` + - 可展示 BD 的头像/昵称/账号等用户资料;旧 Vue agency-center 未直接调用,但后端已有接口。 +- 当前余额/工资:`GET /wallet/bank/balance` + - 可作为 Agency Center 的 Salary。 +- 今日工作数据:`GET /team/member/work/statistics-now?dataType=DAILY&memberId=<当前用户 id/account>` + - 可展示 Minutes,是否保留 Host type/Gift Task 需要按新设计决定。 +- 团队成员数量:`GET /team/members/count?id=` + - 可展示 Team Member 入口右侧数量,或用于跳转参数。 +- 团队账单概要:`GET /team/bill?id=` + - 可展示最近账单、Agent salary、Host salary、Total。 +- 提现入口开关:`GET /wallet/bank/host-salary/withdraw/enabled` + - 可决定是否展示 Cash withdraw。 +- 收款/提现信息:`GET /wallet/bank-card/list`、`GET /wallet/withdraw-info/list` + - 可决定点击提现时直接进入还是提示补充信息。 +- 身份权限:`GET /app/h5/identity` + - 可判断是否为 `agent`,非代理时应避免展示代理中心主内容。 + +## 新页面实现注意点 + +- 新页面应放在 `h5/hyapp/agency-center/`,不要改旧 Vue 构建产物和旧 `h5/agency-center`。 +- 视觉风格参考 `h5/hyapp/host-center`:浅绿色背景、圆角白色卡片、移动端固定宽度体验、URL token 请求。 +- 先完成数据加载再决定展示代理中心或非代理状态,避免接口完成前闪跳。 +- 如果需要非代理兜底页,应基于 `/app/h5/identity` 的 `agent` 字段判断。 +- 如果继续使用政策接口,Java 线上接口参数必须是 `id=`。 diff --git a/h5/hyapp/agency-center/script.js b/h5/hyapp/agency-center/script.js new file mode 100644 index 0000000..cc8f47b --- /dev/null +++ b/h5/hyapp/agency-center/script.js @@ -0,0 +1,1134 @@ +(function () { + const API_BASE_URL = "https://jvapi.haiyihy.com"; + const supportedLanguages = ["en", "ar", "tr", "id"]; + const languageLabels = { + en: "EN", + ar: "AR", + tr: "TR", + id: "ID" + }; + + const translations = { + en: { + document_title: "Agency Center", + page_label: "Agency Center", + back: "Back", + title: "Agency Center", + language_button_aria: "Change language", + uid_prefix: "UID:", + bd_prefix: "BD:", + salary: "Salary", + withdraw_exchange: "Withdraw/Exchange", + open_withdraw_exchange: "Open withdraw and exchange page", + host_list: "Host List", + open_host_list: "Open host list", + team_bill: "Salary Bills", + open_team_bill: "Open salary bills", + streaming_duration: "Work Time (min)", + target: "Target", + daily_details: "Daily data", + gift_points: "Gift points", + online_minutes: "Online minutes", + no_daily_data: "No daily data", + open_daily_data: "Open daily data", + close: "Close", + open_messages: "Open messages", + users_message: "Messages", + messages_loading: "Loading...", + no_messages: "No messages", + unknown_user: "Unknown user", + agree: "Agree", + refuse: "Refuse", + operation_failed: "Operation failed", + approved_application: "Approved {name}'s application.", + rejected_application: "Rejected {name}'s application.", + platform_policy: "Agency Policy", + how_to_invite_member: "How to invite host", + open_invite_help: "Open invite host guide", + invite_download_app: "Invite users to download APP.", + invited_users_login: "Invited users need to log in to the APP.", + join_team_steps: "Me > Host Center > Apply to join the team.", + enter_team_id: "Enter the team ID, click send to join.", + completed: "Completed", + in_progress: "In progress", + out_of_account: "Out of account", + pending: "Pending", + not_agency_title: "Agency access unavailable", + not_agency_text: "This account is not an agency account.", + missing_token_text: "Open this page from the app or add a token in the URL.", + apply_now: "Apply", + missing_token: "Missing token", + load_failed: "Failed to load data" + }, + ar: { + document_title: "مركز الوكالة", + page_label: "مركز الوكالة", + back: "رجوع", + title: "مركز الوكالة", + language_button_aria: "تغيير اللغة", + uid_prefix: "UID:", + bd_prefix: "BD:", + salary: "الراتب", + withdraw_exchange: "سحب/استبدال", + open_withdraw_exchange: "فتح صفحة السحب والاستبدال", + host_list: "قائمة المضيفين", + open_host_list: "فتح قائمة المضيفين", + team_bill: "Salary Bills", + open_team_bill: "Open salary bills", + streaming_duration: "وقت العمل (دقيقة)", + target: "الهدف", + daily_details: "البيانات اليومية", + gift_points: "نقاط الهدايا", + online_minutes: "دقائق الاتصال", + no_daily_data: "لا توجد بيانات يومية", + open_daily_data: "فتح البيانات اليومية", + close: "إغلاق", + open_messages: "فتح الرسائل", + users_message: "الرسائل", + messages_loading: "جار التحميل...", + no_messages: "لا توجد رسائل", + unknown_user: "مستخدم غير معروف", + agree: "موافقة", + refuse: "رفض", + operation_failed: "فشلت العملية", + approved_application: "تمت الموافقة على طلب {name}.", + rejected_application: "تم رفض طلب {name}.", + platform_policy: "سياسة الوكالة", + how_to_invite_member: "كيفية دعوة مضيف", + open_invite_help: "فتح دليل دعوة مضيف", + invite_download_app: "ادع المستخدمين لتنزيل التطبيق.", + invited_users_login: "يجب على المستخدمين المدعوين تسجيل الدخول إلى التطبيق.", + join_team_steps: "أنا > مركز المضيف > طلب الانضمام إلى الفريق.", + enter_team_id: "أدخل معرف الفريق، ثم اضغط إرسال للانضمام.", + completed: "مكتمل", + in_progress: "قيد التنفيذ", + out_of_account: "خارج الحساب", + pending: "قيد الانتظار", + not_agency_title: "الوصول للوكالة غير متاح", + not_agency_text: "هذا الحساب ليس حساب وكالة.", + missing_token_text: "افتح هذه الصفحة من التطبيق أو أضف token في الرابط.", + apply_now: "تقديم", + missing_token: "رمز الدخول مفقود", + load_failed: "فشل تحميل البيانات" + }, + tr: { + document_title: "Ajans Merkezi", + page_label: "Ajans Merkezi", + back: "Geri", + title: "Ajans Merkezi", + language_button_aria: "Dili degistir", + uid_prefix: "UID:", + bd_prefix: "BD:", + salary: "Maas", + withdraw_exchange: "Cek/Degistir", + open_withdraw_exchange: "Cekme ve degisim sayfasini ac", + host_list: "Host Listesi", + open_host_list: "Host listesini ac", + team_bill: "Salary Bills", + open_team_bill: "Open salary bills", + streaming_duration: "Calisma Suresi (dk)", + target: "Hedef", + daily_details: "Gunluk veri", + gift_points: "Hediye puani", + online_minutes: "Online dakika", + no_daily_data: "Gunluk veri yok", + open_daily_data: "Gunluk veriyi ac", + close: "Kapat", + open_messages: "Mesajlari ac", + users_message: "Mesajlar", + messages_loading: "Yukleniyor...", + no_messages: "Mesaj yok", + unknown_user: "Bilinmeyen kullanici", + agree: "Onayla", + refuse: "Reddet", + operation_failed: "Islem basarisiz", + approved_application: "{name} basvurusu onaylandi.", + rejected_application: "{name} basvurusu reddedildi.", + platform_policy: "Ajans Politikasi", + how_to_invite_member: "Host nasil davet edilir", + open_invite_help: "Host davet rehberini ac", + invite_download_app: "Kullanicilari APP indirmeye davet et.", + invited_users_login: "Davet edilen kullanicilarin APP'e giris yapmasi gerekiyor.", + join_team_steps: "Ben > Host Merkezi > Takima katilmak icin basvur.", + enter_team_id: "Takim ID'sini girin, katilmak icin gonder'e tiklayin.", + completed: "Tamamlandi", + in_progress: "Devam ediyor", + out_of_account: "Hesap disi", + pending: "Beklemede", + not_agency_title: "Ajans erisimi yok", + not_agency_text: "Bu hesap ajans hesabi degil.", + missing_token_text: "Bu sayfayi uygulamadan acin veya URL'ye token ekleyin.", + apply_now: "Basvur", + missing_token: "Token eksik", + load_failed: "Veri yuklenemedi" + }, + id: { + document_title: "Pusat Agensi", + page_label: "Pusat Agensi", + back: "Kembali", + title: "Pusat Agensi", + language_button_aria: "Ubah bahasa", + uid_prefix: "UID:", + bd_prefix: "BD:", + salary: "Gaji", + withdraw_exchange: "Tarik/Tukar", + open_withdraw_exchange: "Buka halaman tarik dan tukar", + host_list: "Daftar Host", + open_host_list: "Buka daftar host", + team_bill: "Salary Bills", + open_team_bill: "Open salary bills", + streaming_duration: "Waktu Kerja (mnt)", + target: "Target", + daily_details: "Data harian", + gift_points: "Poin hadiah", + online_minutes: "Menit online", + no_daily_data: "Tidak ada data harian", + open_daily_data: "Buka data harian", + close: "Tutup", + open_messages: "Buka pesan", + users_message: "Pesan", + messages_loading: "Memuat...", + no_messages: "Tidak ada pesan", + unknown_user: "Pengguna tidak dikenal", + agree: "Setuju", + refuse: "Tolak", + operation_failed: "Operasi gagal", + approved_application: "Pengajuan {name} disetujui.", + rejected_application: "Pengajuan {name} ditolak.", + platform_policy: "Kebijakan agensi", + how_to_invite_member: "Cara mengundang host", + open_invite_help: "Buka panduan undang host", + invite_download_app: "Undang pengguna untuk mengunduh APP.", + invited_users_login: "Pengguna yang diundang harus login ke APP.", + join_team_steps: "Saya > Host Center > Ajukan bergabung ke tim.", + enter_team_id: "Masukkan ID tim, lalu ketuk kirim untuk bergabung.", + completed: "Selesai", + in_progress: "Berjalan", + out_of_account: "Di luar akun", + pending: "Tertunda", + not_agency_title: "Akses agensi tidak tersedia", + not_agency_text: "Akun ini bukan akun agensi.", + missing_token_text: "Buka halaman ini dari aplikasi atau tambahkan token di URL.", + apply_now: "Ajukan", + missing_token: "Token tidak ditemukan", + load_failed: "Gagal memuat data" + } + }; + + const params = readURLParams(); + let currentLang = normalizeLanguage(params.get("lang")); + let currentMessages = translations[currentLang] || translations.en; + let currentProfile = null; + let currentTeamInfo = null; + let currentBdProfile = null; + let currentIdentity = null; + let currentHasAgencyAccess = true; + let currentAccessReason = ""; + let currentStreamingDuration = "0"; + let currentGiftValue = "0"; + let currentDailyTargets = []; + let currentUnreadMessageCount = 0; + let currentMessageItems = []; + let currentMessagesLoading = false; + let currentTeamMemberCount = null; + + 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"; + } + + function t(key, fallback) { + return currentMessages[key] || translations.en[key] || fallback || key; + } + + function formatMessageTemplate(key, values = {}) { + return t(key).replace(/\{(\w+)\}/g, (match, name) => values[name] ?? match); + } + + function apiBaseURL() { + const override = window.HYAPP_API_BASE_URL || params.get("apiBase"); + if (override) return String(override).replace(/\/+$/, ""); + return API_BASE_URL.replace(/\/+$/, ""); + } + + function normalizeAuthorization(token) { + const value = String(token || "").trim(); + if (!value) return ""; + return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`; + } + + function authHeadersFromToken(token) { + 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=ATYOU;originChild=ATYOU" + }; + } + + async function requestJSON(path, options = {}) { + const token = readRawParam("token"); + if (!token) throw new Error(t("missing_token", "Missing token")); + + const headers = authHeadersFromToken(token); + const method = options.method || "GET"; + const hasBody = options.body !== undefined; + if (hasBody) headers["Content-Type"] = "application/json"; + + const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), { + method, + cache: "no-store", + headers, + body: hasBody ? JSON.stringify(options.body) : undefined + }); + 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.response = data; + throw error; + } + return data.body ?? data.data ?? null; + } + + function isTruthyIdentityValue(value) { + if (typeof value === "boolean") return value; + if (typeof value === "number") return value === 1; + if (typeof value === "string") return ["true", "1", "yes"].includes(value.toLowerCase()); + return false; + } + + function hasAgencyAccess(identity) { + if (!identity) return false; + return isTruthyIdentityValue(identity.agent) || + isTruthyIdentityValue(identity.freightAgent) || + isTruthyIdentityValue(identity.admin); + } + + function setLoading(isLoading) { + const root = document.querySelector(".agency-center"); + const loadingMask = document.querySelector("#loadingMask"); + if (root) root.dataset.loading = isLoading ? "true" : "false"; + if (loadingMask) { + loadingMask.hidden = !isLoading; + loadingMask.setAttribute("aria-busy", String(isLoading)); + } + } + + function setAgencyView(hasAccess, reason) { + currentHasAgencyAccess = hasAccess; + currentAccessReason = reason || ""; + + const root = document.querySelector(".agency-center"); + if (root) root.dataset.view = hasAccess ? "agency" : "no-access"; + + document.querySelectorAll(".agent-only").forEach((node) => { + node.hidden = !hasAccess; + }); + + const accessCard = document.querySelector("#accessCard"); + if (accessCard) accessCard.hidden = hasAccess; + + const accessText = document.querySelector("#accessText"); + if (accessText && reason === "missing-token") accessText.textContent = t("missing_token_text"); + if (accessText && reason !== "missing-token") accessText.textContent = t("not_agency_text"); + } + + function formatMoney(value) { + const number = Number(value); + return Number.isFinite(number) ? number.toFixed(2) : "0.00"; + } + + function pickCurrentSalary(body) { + if (typeof body === "number") return body; + if (body && typeof body === "object") { + return body.currentSalary ?? body.availableBalance ?? body.balance ?? 0; + } + return 0; + } + + function pickProfileName(profile) { + return profile?.userNickname || profile?.name || "Agency"; + } + + function pickProfileAccount(profile) { + return profile?.ownSpecialId?.account || profile?.account || profile?.id || "-"; + } + + function pickTeamInfo(body) { + return body?.teamProfile || body || null; + } + + function pickTeamId(teamInfo) { + return teamInfo?.id || teamInfo?.teamId || teamInfo?.teamProfile?.id || ""; + } + + function pickBdLabel(profile) { + if (!profile) return "-"; + const name = profile.userNickname || profile.name || "BD"; + const account = profile.ownSpecialId?.account || profile.account || profile.id || ""; + if (!account || String(account) === String(name)) return name; + return `${name} (${account})`; + } + + function renderProfile(profile = currentProfile) { + currentProfile = profile; + const displayName = pickProfileName(profile); + const displayAccount = pickProfileAccount(profile); + const nameNode = document.querySelector("#profileName"); + const uidNode = document.querySelector("#profileUid"); + const avatarImage = document.querySelector("#profileAvatar"); + const avatarFallback = document.querySelector("#profileAvatarFallback"); + + if (nameNode) nameNode.textContent = displayName; + if (uidNode) uidNode.textContent = `${t("uid_prefix")} ${displayAccount}`.trim(); + + if (avatarFallback) { + avatarFallback.textContent = displayName.slice(0, 1).toUpperCase() || "A"; + avatarFallback.hidden = false; + } + + if (!avatarImage) return; + const avatar = profile?.userAvatar ? String(profile.userAvatar) : ""; + avatarImage.alt = displayName; + if (!avatar) { + avatarImage.hidden = true; + avatarImage.removeAttribute("src"); + return; + } + + avatarImage.hidden = false; + if (avatarFallback) avatarFallback.hidden = true; + avatarImage.onerror = () => { + avatarImage.hidden = true; + avatarImage.removeAttribute("src"); + if (avatarFallback) avatarFallback.hidden = false; + }; + avatarImage.src = avatar; + } + + function renderTeamInfo(teamInfo = currentTeamInfo) { + currentTeamInfo = teamInfo; + syncLinks(); + } + + function renderTeamMemberCount(count = currentTeamMemberCount) { + currentTeamMemberCount = count; + const countNode = document.querySelector("#teamMemberCount"); + if (!countNode) return; + + const number = Number(count); + if (Number.isFinite(number)) { + countNode.textContent = String(Math.max(0, Math.trunc(number))); + return; + } + + countNode.textContent = "-"; + } + + function renderMessageUnread(count = currentUnreadMessageCount) { + currentUnreadMessageCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0; + const dot = document.querySelector("#messageUnreadDot"); + const messageButton = document.querySelector(".profile-message-button"); + if (dot) dot.hidden = currentUnreadMessageCount <= 0; + if (messageButton) messageButton.dataset.unread = currentUnreadMessageCount > 0 ? "true" : "false"; + } + + function pickMessageRecords(body) { + if (Array.isArray(body)) return body; + if (Array.isArray(body?.records)) return body.records; + if (Array.isArray(body?.list)) return body.list; + return []; + } + + function normalizeMessageRecord(record) { + const profile = record?.createUserProfile || record?.userProfile || record?.user || {}; + const name = profile.userNickname || profile.name || record?.name || record?.userNickname || t("unknown_user"); + return { + id: record?.id, + reason: record?.reason || "", + teamId: record?.teamId || "", + status: record?.status || "WAIT", + createTime: record?.createTime || "", + userId: profile.id || record?.userId || record?.createUserId || "", + account: profile.ownSpecialId?.account || profile.account || record?.account || record?.userAccount || "", + name, + avatar: profile.userAvatar || record?.avatar || record?.userAvatar || "" + }; + } + + function createMessageAvatar(message) { + const avatar = document.createElement("div"); + avatar.className = "message-avatar"; + + const fallback = document.createElement("span"); + fallback.textContent = (message.name || t("unknown_user")).slice(0, 1).toUpperCase(); + avatar.appendChild(fallback); + + if (message.avatar) { + const image = document.createElement("img"); + image.src = message.avatar; + image.alt = message.name; + image.addEventListener("error", () => { + image.remove(); + }); + avatar.appendChild(image); + } + + return avatar; + } + + function createMessageCard(message) { + const card = document.createElement("div"); + card.className = "message-card"; + + const user = document.createElement("div"); + user.className = "message-user"; + user.appendChild(createMessageAvatar(message)); + + const copy = document.createElement("div"); + copy.className = "message-copy"; + + const name = document.createElement("div"); + name.className = "message-name"; + name.textContent = message.name || t("unknown_user"); + + const idLine = document.createElement("div"); + idLine.className = "message-id"; + idLine.textContent = `ID : ${message.account || message.userId || "-"}`; + + copy.appendChild(name); + copy.appendChild(idLine); + user.appendChild(copy); + + const actions = document.createElement("div"); + actions.className = "message-actions"; + + const agree = document.createElement("button"); + agree.type = "button"; + agree.className = "message-action agree-btn"; + agree.textContent = t("agree"); + agree.addEventListener("click", () => processMessageApplication(message, "AGREE")); + + const refuse = document.createElement("button"); + refuse.type = "button"; + refuse.className = "message-action refuse-btn"; + refuse.textContent = t("refuse"); + refuse.addEventListener("click", () => processMessageApplication(message, "REJECT")); + + actions.appendChild(agree); + actions.appendChild(refuse); + + card.appendChild(user); + card.appendChild(actions); + return card; + } + + function renderMessageList(items = currentMessageItems) { + currentMessageItems = Array.isArray(items) ? items : []; + const list = document.querySelector("#messageList"); + if (!list) return; + + if (currentMessagesLoading) { + const loading = document.createElement("div"); + loading.className = "message-empty"; + loading.textContent = t("messages_loading"); + replaceNodeChildren(list, [loading]); + return; + } + + if (!currentMessageItems.length) { + const empty = document.createElement("div"); + empty.className = "message-empty"; + empty.textContent = t("no_messages"); + replaceNodeChildren(list, [empty]); + return; + } + + replaceNodeChildren(list, currentMessageItems.map(createMessageCard)); + } + + function renderBdProfile(profile = currentBdProfile) { + currentBdProfile = profile; + const bdName = document.querySelector("#bdName"); + if (bdName) bdName.textContent = `${t("bd_prefix")} ${pickBdLabel(profile)}`.trim(); + } + + function renderSalary(value) { + const salaryNode = document.querySelector("#currentSalary"); + if (salaryNode) salaryNode.textContent = formatMoney(value); + } + + function formatStreamingDuration(value) { + const numericValue = Number(value); + if (!Number.isFinite(numericValue)) return "0"; + const safeValue = Math.max(0, numericValue); + return Number.isInteger(safeValue) ? String(safeValue) : String(Number(safeValue.toFixed(2))); + } + + function pickFirstMemberWorkTarget(body) { + const targets = Array.isArray(body?.targets) ? body.targets : []; + return targets[0] || body?.target || null; + } + + function pickStreamingDuration(body) { + const workTarget = pickFirstMemberWorkTarget(body); + return body?.onlineTime ?? body?.target?.onlineTime ?? workTarget?.target?.onlineTime ?? workTarget?.onlineTime ?? 0; + } + + function renderStreamingDuration(value) { + currentStreamingDuration = formatStreamingDuration(value); + const durationNode = document.querySelector("#streamingDuration"); + if (durationNode) durationNode.textContent = currentStreamingDuration; + } + + function formatCountValue(value) { + if (value === null || value === undefined || value === "") return "-"; + const number = Number(value); + if (Number.isFinite(number) && Number.isInteger(number)) return String(number); + if (Number.isFinite(number)) return String(Number(number.toFixed(2))); + return String(value); + } + + function renderCurrentGiftValue(value) { + currentGiftValue = formatCountValue(value === null || value === undefined || value === "" ? 0 : value); + const giftNode = document.querySelector("#currentGiftValue"); + const dailyGiftNode = document.querySelector("#dailyCurrentGift"); + if (giftNode) giftNode.textContent = currentGiftValue; + if (dailyGiftNode) dailyGiftNode.textContent = currentGiftValue; + } + + function pickGiftValue(workTarget) { + const target = workTarget?.target || workTarget || {}; + return target.acceptGiftValue ?? target.giftValue ?? target.giftScore ?? target.currentGiftValue ?? null; + } + + function pickDailyTargets(workTarget) { + const target = workTarget?.target || workTarget || {}; + return Array.isArray(target.dailyTargets) ? target.dailyTargets : []; + } + + function formatDateNumber(value) { + const text = String(value || ""); + if (/^\d{8}$/.test(text)) { + return `${text.slice(0, 4)}.${text.slice(4, 6)}.${text.slice(6, 8)}`; + } + return text || "-"; + } + + function createDailyCell(label, value) { + const cell = document.createElement("div"); + cell.className = "daily-cell"; + + const labelNode = document.createElement("span"); + labelNode.textContent = label; + + const valueNode = document.createElement("strong"); + valueNode.textContent = value; + + cell.appendChild(labelNode); + cell.appendChild(valueNode); + return cell; + } + + function createDailyRow(item) { + const row = document.createElement("div"); + row.className = "daily-row"; + + const date = document.createElement("div"); + date.className = "daily-date"; + date.textContent = formatDateNumber(item?.dateNumber); + + const values = document.createElement("div"); + values.className = "daily-values"; + values.appendChild(createDailyCell(t("gift_points"), formatCountValue(item?.acceptGiftValue ?? 0))); + values.appendChild(createDailyCell(t("online_minutes"), formatCountValue(item?.onlineTime ?? 0))); + + row.appendChild(date); + row.appendChild(values); + return row; + } + + function replaceNodeChildren(node, children) { + while (node.firstChild) node.removeChild(node.firstChild); + children.forEach((child) => node.appendChild(child)); + } + + function renderDailyTargets(targets = currentDailyTargets) { + currentDailyTargets = Array.isArray(targets) ? targets : []; + const list = document.querySelector("#dailyList"); + if (!list) return; + + if (!currentDailyTargets.length) { + const empty = document.createElement("div"); + empty.className = "daily-empty"; + empty.textContent = t("no_daily_data"); + replaceNodeChildren(list, [empty]); + return; + } + + replaceNodeChildren(list, currentDailyTargets.map(createDailyRow)); + } + + function renderMemberWorkSummary(memberWork) { + const workTarget = pickFirstMemberWorkTarget(memberWork); + renderStreamingDuration(pickStreamingDuration(memberWork)); + renderCurrentGiftValue(pickGiftValue(workTarget) ?? 0); + renderDailyTargets(pickDailyTargets(workTarget)); + } + + function setDailyModalOpen(open) { + const modal = document.querySelector("#dailyModal"); + if (!modal) return; + modal.hidden = !open; + if (open) { + const inviteModal = document.querySelector("#inviteModal"); + if (inviteModal) inviteModal.hidden = true; + const messageModal = document.querySelector("#messageModal"); + if (messageModal) messageModal.hidden = true; + renderDailyTargets(); + } + syncModalBodyLock(); + } + + function setInviteModalOpen(open) { + const modal = document.querySelector("#inviteModal"); + if (!modal) return; + modal.hidden = !open; + if (open) { + const dailyModal = document.querySelector("#dailyModal"); + if (dailyModal) dailyModal.hidden = true; + const messageModal = document.querySelector("#messageModal"); + if (messageModal) messageModal.hidden = true; + } + syncModalBodyLock(); + } + + function setMessagesModalOpen(open) { + const modal = document.querySelector("#messageModal"); + if (!modal) return; + modal.hidden = !open; + if (open) { + const dailyModal = document.querySelector("#dailyModal"); + const inviteModal = document.querySelector("#inviteModal"); + if (dailyModal) dailyModal.hidden = true; + if (inviteModal) inviteModal.hidden = true; + renderMessageList(); + } + syncModalBodyLock(); + } + + function syncModalBodyLock() { + const hasOpenModal = Array.from(document.querySelectorAll(".daily-modal, .message-modal")).some((modal) => !modal.hidden); + document.body.classList.toggle("daily-modal-open", hasOpenModal); + } + + function buildPageURL(target, options = {}) { + const url = new URL(target, window.location.href); + const nextParams = new URLSearchParams(url.search); + + params.forEach((value, key) => { + if (!nextParams.has(key)) nextParams.set(key, value); + }); + + const token = readRawParam("token"); + if (token) nextParams.set("token", token); + if (currentLang) nextParams.set("lang", currentLang); + if (options.teamId) nextParams.set("teamId", String(options.teamId)); + if (options.members !== undefined && options.members !== null && options.members !== "") { + nextParams.set("members", String(options.members)); + } + + url.search = nextParams.toString(); + return url.toString(); + } + + function syncLinks() { + const teamId = pickTeamId(currentTeamInfo); + + document.querySelectorAll("[data-route]").forEach((link) => { + const options = {}; + if (link.dataset.policy === "team" && teamId) options.teamId = teamId; + if (link.dataset.teamMember === "true") { + if (teamId) options.teamId = teamId; + if (currentTeamMemberCount !== null && currentTeamMemberCount !== undefined) { + options.members = currentTeamMemberCount; + } + } + if (link.dataset.teamBill === "true" && teamId) options.teamId = teamId; + link.setAttribute("href", buildPageURL(link.dataset.route, options)); + }); + } + + function showToast(message) { + const toast = document.querySelector("#toast"); + if (!toast) return; + toast.textContent = message; + toast.hidden = false; + window.clearTimeout(showToast.timer); + showToast.timer = window.setTimeout(() => { + toast.hidden = true; + }, 2200); + } + + 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 agency center page failed:", error); + if (window.history.length > 1) window.history.back(); + } + } + + function setLanguageMenuOpen(open) { + const languageButton = document.querySelector(".language-button"); + const languageMenu = document.querySelector(".language-menu"); + if (!languageButton || !languageMenu) return; + languageMenu.hidden = !open; + languageButton.setAttribute("aria-expanded", String(open)); + } + + function renderLanguageControls() { + const languageButton = document.querySelector(".language-button"); + if (languageButton) { + languageButton.textContent = languageLabels[currentLang]; + languageButton.setAttribute("aria-label", t("language_button_aria")); + } + + 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 renderMessages() { + document.querySelectorAll("[data-i18n]").forEach((node) => { + const value = currentMessages[node.dataset.i18n]; + if (value) node.textContent = value; + }); + + document.querySelectorAll("[data-i18n-aria]").forEach((node) => { + const value = currentMessages[node.dataset.i18nAria]; + if (value) node.setAttribute("aria-label", value); + }); + } + + function applyLanguage(lang) { + currentLang = normalizeLanguage(lang); + currentMessages = translations[currentLang] || translations.en; + document.documentElement.lang = currentLang; + document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; + document.title = t("document_title"); + renderMessages(); + renderLanguageControls(); + renderProfile(); + renderTeamInfo(); + renderBdProfile(); + renderStreamingDuration(currentStreamingDuration); + renderCurrentGiftValue(currentGiftValue); + renderDailyTargets(); + renderMessageUnread(); + renderMessageList(); + renderTeamMemberCount(); + syncLinks(); + if (!currentHasAgencyAccess) setAgencyView(false, currentAccessReason); + } + + async function fetchIdentity() { + currentIdentity = await requestJSON("/app/h5/identity"); + return currentIdentity; + } + + async function fetchUserProfile() { + const profile = await requestJSON("/team/member/profile"); + renderProfile(profile); + return profile; + } + + async function fetchTeamInfo() { + const body = await requestJSON("/team/entry"); + const teamInfo = pickTeamInfo(body); + renderTeamInfo(teamInfo); + return teamInfo; + } + + async function fetchBdProfile() { + const profile = await requestJSON("/team/bd-profile"); + renderBdProfile(profile); + return profile; + } + + async function fetchBankBalance() { + const body = await requestJSON("/wallet/bank/balance"); + renderSalary(pickCurrentSalary(body)); + } + + function pickTeamMemberCount(body) { + if (typeof body === "number") return body; + if (body && typeof body === "object") { + return body.memberQuantity ?? body.count ?? body.total ?? body.memberCount ?? null; + } + return null; + } + + async function fetchTeamMemberCount(teamId) { + if (!teamId) { + renderTeamMemberCount(null); + return null; + } + + try { + const query = new URLSearchParams({ id: String(teamId) }); + const body = await requestJSON(`/team/members/count?${query.toString()}`); + const count = pickTeamMemberCount(body); + renderTeamMemberCount(count); + syncLinks(); + return count; + } catch (error) { + console.error("Failed to load team member count:", error); + renderTeamMemberCount(null); + syncLinks(); + return null; + } + } + + async function fetchMemberWork(memberId) { + if (!memberId) { + console.warn("Missing memberId for /team/member-work"); + renderMemberWorkSummary(null); + return null; + } + + try { + const url = new URL("/team/member-work", `${apiBaseURL()}/`); + url.searchParams.set("userId", String(memberId)); + const memberWork = await requestJSON(`${url.pathname}${url.search}`); + renderMemberWorkSummary(memberWork); + return memberWork; + } catch (error) { + console.error("Failed to load member work:", error); + renderMemberWorkSummary(null); + return null; + } + } + + async function fetchMessageList(teamId) { + if (!teamId) { + currentMessageItems = []; + renderMessageUnread(0); + renderMessageList(); + return []; + } + + currentMessagesLoading = true; + renderMessageList(); + try { + const query = new URLSearchParams({ teamId: String(teamId), status: "WAIT" }); + const body = await requestJSON(`/team/user/apply/record?${query.toString()}`); + currentMessageItems = pickMessageRecords(body).map(normalizeMessageRecord); + renderMessageUnread(currentMessageItems.length); + return currentMessageItems; + } catch (error) { + console.error("Failed to load messages:", error); + currentMessageItems = []; + renderMessageUnread(0); + return []; + } finally { + currentMessagesLoading = false; + renderMessageList(); + } + } + + async function processMessageApplication(message, status) { + if (!message?.id) return; + + try { + await requestJSON("/team/process/user/apply", { + method: "POST", + body: { + id: message.id, + status + } + }); + + currentMessageItems = currentMessageItems.filter((item) => item.id !== message.id); + renderMessageUnread(currentMessageItems.length); + renderMessageList(); + + const messageKey = status === "AGREE" ? "approved_application" : "rejected_application"; + showToast(formatMessageTemplate(messageKey, { name: message.name || t("unknown_user") })); + } catch (error) { + console.error("Failed to process message application:", error); + showToast(error.message || t("operation_failed")); + } + } + + function bindEvents() { + document.querySelector(".back-button")?.addEventListener("click", closePage); + + document.querySelector(".language-button")?.addEventListener("click", (event) => { + event.stopPropagation(); + const languageMenu = document.querySelector(".language-menu"); + setLanguageMenuOpen(Boolean(languageMenu?.hidden)); + }); + + document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { + button.addEventListener("click", (event) => { + event.stopPropagation(); + setLanguageMenuOpen(false); + applyLanguage(button.dataset.lang); + }); + }); + + document.querySelector(".gift-value")?.addEventListener("click", () => { + setDailyModalOpen(true); + }); + + document.querySelector(".profile-message-button")?.addEventListener("click", () => { + setMessagesModalOpen(true); + fetchMessageList(pickTeamId(currentTeamInfo)); + }); + + document.querySelector(".invite-help-button")?.addEventListener("click", () => { + setInviteModalOpen(true); + }); + + document.querySelectorAll("[data-close-daily]").forEach((node) => { + node.addEventListener("click", () => { + setDailyModalOpen(false); + }); + }); + + document.querySelectorAll("[data-close-invite]").forEach((node) => { + node.addEventListener("click", () => { + setInviteModalOpen(false); + }); + }); + + document.querySelectorAll("[data-close-messages]").forEach((node) => { + node.addEventListener("click", () => { + setMessagesModalOpen(false); + }); + }); + + document.addEventListener("click", () => { + setLanguageMenuOpen(false); + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + setDailyModalOpen(false); + setInviteModalOpen(false); + setMessagesModalOpen(false); + } + }); + } + + async function initializePage() { + setLoading(true); + applyLanguage(currentLang); + bindEvents(); + syncLinks(); + + const token = readRawParam("token"); + if (!token) { + setAgencyView(false, "missing-token"); + setLoading(false); + return; + } + + try { + const firstWave = [ + fetchIdentity(), + fetchUserProfile() + ]; + const firstResults = await Promise.allSettled(firstWave); + firstResults.forEach((result) => { + if (result.status === "rejected") console.warn("Agency center data request failed:", result.reason); + }); + + const hasAccess = hasAgencyAccess(currentIdentity); + setAgencyView(hasAccess); + + if (hasAccess) { + const agencyResults = await Promise.allSettled([ + fetchTeamInfo(), + fetchBdProfile(), + fetchBankBalance() + ]); + agencyResults.forEach((result) => { + if (result.status === "rejected") console.warn("Agency center data request failed:", result.reason); + }); + + const memberId = currentProfile?.id || currentProfile?.account; + const teamId = pickTeamId(currentTeamInfo); + const dependentResults = await Promise.allSettled([ + fetchMemberWork(memberId), + fetchMessageList(teamId), + fetchTeamMemberCount(teamId) + ]); + dependentResults.forEach((result) => { + if (result.status === "rejected") console.warn("Agency center dependent request failed:", result.reason); + }); + } + } catch (error) { + console.error("Failed to initialize agency center:", error); + showToast(error.message || t("load_failed")); + setAgencyView(hasAgencyAccess(currentIdentity)); + } finally { + setLoading(false); + } + } + + initializePage(); +})(); diff --git a/h5/hyapp/agency-center/style.css b/h5/hyapp/agency-center/style.css new file mode 100644 index 0000000..5cbf60c --- /dev/null +++ b/h5/hyapp/agency-center/style.css @@ -0,0 +1,1404 @@ +:root { + --page-bg: #f6f7f7; + --card-bg: rgba(255, 255, 255, 0.96); + --text: #25282e; + --muted: #8f9196; + --line: #eef0f2; + --mint: #43e7d8; + --green: #25d878; + --gold: #e3b55b; + --shadow: 0 8px 22px rgba(36, 46, 60, 0.055); +} + +* { + box-sizing: border-box; +} + +html, +body { + width: 100%; + min-height: 100%; + margin: 0; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: transparent; + -webkit-text-size-adjust: 100%; +} + +body { + display: flex; + justify-content: center; + background: #111; + color: var(--text); + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +button { + border: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +[hidden] { + display: none !important; +} + +.agency-center { + position: relative; + width: 100%; + max-width: 430px; + min-height: 100vh; + overflow: hidden; + background: var(--page-bg); +} + +.agency-center[data-loading="true"] .title-bar, +.agency-center[data-loading="true"] .content, +.agency-center[data-loading="true"] .home-indicator { + visibility: hidden; +} + +.hero-bg { + position: absolute; + inset: 0 0 auto; + height: 215px; + background: + radial-gradient(circle at 72% 4%, rgba(209, 255, 248, 0.98) 0, rgba(209, 255, 248, 0.88) 23%, rgba(209, 255, 248, 0) 48%), + radial-gradient(circle at 14% 20%, rgba(185, 250, 244, 0.78) 0, rgba(185, 250, 244, 0) 35%), + linear-gradient(151deg, #ccf7ef 0%, #ebfffa 58%, rgba(242, 255, 252, 0.7) 100%); + clip-path: polygon(0 0, 100% 0, 100% 76%, 70% 47%, 40% 58%, 0 78%); +} + +.status-bar, +.title-bar, +.content, +.home-indicator { + position: relative; + z-index: 1; +} + +.status-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 47px; + padding: 18px 29px 0 38px; + color: #11161c; + direction: ltr; + font-size: 15px; + font-weight: 800; +} + +.status-icons { + display: flex; + align-items: center; + gap: 6px; +} + +.cellular { + display: flex; + align-items: flex-end; + gap: 2px; + width: 18px; + height: 12px; +} + +.cellular i { + width: 3px; + border-radius: 2px; + background: #11161c; +} + +.cellular i:nth-child(1) { + height: 4px; +} + +.cellular i:nth-child(2) { + height: 6px; +} + +.cellular i:nth-child(3) { + height: 9px; +} + +.cellular i:nth-child(4) { + height: 11px; +} + +.wifi { + position: relative; + width: 18px; + height: 13px; +} + +.wifi i { + position: absolute; + left: 50%; + border: 2px solid #11161c; + border-bottom: 0; + border-left-color: transparent; + border-right-color: transparent; + border-radius: 12px 12px 0 0; + transform: translateX(-50%); +} + +.wifi i:nth-child(1) { + top: 1px; + width: 16px; + height: 8px; +} + +.wifi i:nth-child(2) { + top: 5px; + width: 10px; + height: 5px; +} + +.wifi i:nth-child(3) { + top: 10px; + width: 4px; + height: 4px; + border: 0; + border-radius: 50%; + background: #11161c; +} + +.battery { + position: relative; + width: 24px; + height: 12px; + border: 2px solid #11161c; + border-radius: 3px; +} + +.battery::after { + content: ""; + position: absolute; + top: 3px; + right: -4px; + width: 2px; + height: 4px; + border-radius: 0 2px 2px 0; + background: #11161c; +} + +.battery i { + display: block; + width: 16px; + height: 100%; + background: #11161c; +} + +.title-bar { + display: grid; + grid-template-columns: 52px 1fr 52px; + align-items: center; + z-index: 3; + height: 60px; + padding: 0 14px; + direction: ltr; +} + +.title-bar h1 { + margin: 0; + color: #24282e; + font-size: 22px; + font-weight: 900; + line-height: 1; + text-align: center; +} + +.back-button { + display: flex; + align-items: center; + justify-content: flex-start; + width: 44px; + height: 44px; +} + +.back-button svg { + width: 28px; + height: 28px; +} + +.back-button path { + fill: none; + stroke: #24282e; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 3; +} + +.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.68); + color: #25282e; + font-size: 13px; + font-weight: 900; + box-shadow: 0 4px 12px rgba(43, 118, 107, 0.08); +} + +.language-menu { + position: absolute; + z-index: 4; + 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: #25282e; + font-size: 12px; + font-weight: 900; +} + +.language-menu button.is-active { + background: #e6fbf8; + color: #0e8f82; +} + +.content { + display: flex; + flex-direction: column; + gap: 15px; + padding: 8px 14px 90px; +} + +.card { + width: 100%; + border-radius: 8px; + background: var(--card-bg); + box-shadow: var(--shadow); +} + +.profile-card { + display: flex; + align-items: center; + min-height: 103px; + padding: 16px 24px; + gap: 0; +} + +.avatar-shell { + position: relative; + width: 64px; + height: 64px; + flex: 0 0 64px; +} + +.avatar-image, +.avatar-fallback { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + border-radius: 50%; +} + +.avatar-image { + object-fit: cover; +} + +.avatar-fallback { + background: linear-gradient(145deg, #55f2df 0%, #d9fff5 100%); + color: #13745f; + font-size: 25px; + font-weight: 950; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.62); +} + +.profile-copy { + flex: 1 1 auto; + min-width: 0; + margin-left: 20px; +} + +.profile-message-button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + flex: 0 0 42px; + margin-left: 14px; + border-radius: 50%; + background: #f2fbf9; + color: #4c5058; + box-shadow: inset 0 0 0 1px rgba(34, 44, 55, 0.07); +} + +.profile-message-button svg { + width: 25px; + height: 25px; +} + +.profile-message-button path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.2; +} + +.message-unread-dot { + position: absolute; + top: 7px; + right: 7px; + width: 9px; + height: 9px; + border: 2px solid #fff; + border-radius: 50%; + background: #ff3b30; +} + +.name { + overflow: hidden; + color: #25282e; + font-size: 20px; + font-weight: 900; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.meta, +.agency-line { + overflow: hidden; + color: var(--muted); + font-size: 13px; + font-weight: 750; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.meta { + margin-top: 6px; +} + +.agency-line { + margin-top: 6px; +} + +.access-card { + display: grid; + grid-template-columns: 48px minmax(0, 1fr) auto; + align-items: center; + gap: 14px; + min-height: 92px; + padding: 18px; +} + +.access-icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 50%; + background: #e9fbf6; +} + +.access-icon svg { + width: 27px; + height: 27px; +} + +.access-icon path { + fill: none; + stroke: #1fb879; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.3; +} + +.access-copy { + min-width: 0; +} + +.access-title { + color: #24282e; + font-size: 16px; + font-weight: 900; + line-height: 1.2; +} + +.access-text { + margin-top: 5px; + color: var(--muted); + font-size: 13px; + font-weight: 750; + line-height: 1.35; +} + +.access-action { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 36px; + padding: 0 14px; + border-radius: 8px; + background: var(--green); + color: #fff; + font-size: 13px; + font-weight: 900; + white-space: nowrap; +} + +.metrics-card { + display: grid; + grid-template-columns: 1fr 1px 1fr; + min-height: 111px; + padding: 23px 24px 20px; +} + +.metric { + min-width: 0; +} + +.metric:first-child { + padding-right: 20px; +} + +.gift-metric { + padding-left: 25px; +} + +.vertical-line { + width: 1px; + background: var(--line); +} + +.label { + min-height: 36px; + color: #8b8d92; + font-size: 16px; + font-weight: 750; + line-height: 1.12; +} + +.value { + margin-top: 4px; + color: #292d33; + font-size: 25px; + font-weight: 900; + line-height: 1; +} + +.gift-value { + display: flex; + align-items: center; + margin-top: 7px; + color: #252a30; + font-size: 24px; + font-weight: 900; + line-height: 1; +} + +.gift-value .amount-text { + min-width: 0; + overflow-wrap: anywhere; +} + +.salary-card { + display: flex; + flex-direction: column; + justify-content: center; + min-height: 113px; + padding: 18px 24px; +} + +.salary-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.salary-title { + display: flex; + align-items: center; + min-width: 0; + color: #24282e; + font-size: 17px; + font-weight: 900; +} + +.salary-title svg { + width: 17px; + height: 17px; + margin-left: 7px; +} + +.salary-title circle, +.salary-title path { + fill: none; + stroke: #272b31; + stroke-linecap: round; + stroke-width: 2; +} + +.salary-link { + display: flex; + align-items: center; + flex: 0 0 auto; + color: var(--mint); + font-size: 13px; + font-weight: 900; +} + +.salary-link .chevron { + margin-left: 5px; + color: var(--mint); + font-size: 20px; +} + +.salary-value { + display: flex; + align-items: center; + max-width: 100%; + margin-top: 14px; + color: #23272d; + font-size: 34px; + font-weight: 950; + line-height: 1.05; +} + +.salary-currency { + flex: 0 0 auto; + margin-right: 6px; + color: var(--gold); + font-size: 28px; + font-weight: 950; + line-height: 1; +} + +.amount-text { + min-width: 0; + overflow-wrap: anywhere; +} + +.chevron { + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 8px; + color: #8b8e94; + font-size: 25px; + font-weight: 500; + line-height: 0.8; +} + +.salary-value .chevron { + margin-left: 9px; + color: #b4b6ba; + font-size: 24px; +} + +.policy-entry-card { + padding: 0 16px; +} + +.policy-entry-link { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 64px; + text-align: left; + cursor: pointer; +} + +.policy-entry-divider { + height: 1px; + margin-left: 42px; + background: var(--line); +} + +.policy-entry-left { + display: flex; + align-items: center; + min-width: 0; +} + +.policy-entry-left svg { + width: 24px; + height: 24px; + flex: 0 0 24px; + margin-right: 18px; +} + +.policy-entry-left svg path { + fill: none; + stroke: #55585f; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.2; +} + +.question-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex: 0 0 24px; + margin-right: 18px; + border: 2px solid #55585f; + border-radius: 50%; + color: #55585f; + font-size: 15px; + font-weight: 950; + line-height: 1; +} + +.policy-entry-left span:not(.question-icon) { + min-width: 0; + overflow: hidden; + color: #464951; + font-size: 16px; + font-weight: 850; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.policy-entry-link > .chevron { + flex: 0 0 auto; + margin-left: 12px; + color: #8b8d92; + font-size: 24px; +} + +.team-member-entry-card { + margin-top: -1px; +} + +.team-member-entry-right { + display: inline-flex; + align-items: center; + flex: 0 0 auto; + margin-left: 12px; + color: #8b8d92; + font-size: 14px; + font-weight: 900; + line-height: 1; +} + +.team-member-entry-right .chevron { + margin-left: 7px; + color: #8b8d92; + font-size: 24px; +} + +.daily-modal[hidden] { + display: none; +} + +.daily-modal { + position: fixed; + z-index: 20; + inset: 0; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.daily-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 21, 28, 0.38); +} + +.daily-sheet { + position: relative; + display: flex; + flex-direction: column; + width: min(430px, 100%); + max-height: 80vh; + max-height: 80dvh; + overflow: hidden; + border-radius: 16px 16px 0 0; + background: #fff; + box-shadow: 0 -14px 34px rgba(23, 31, 42, 0.16); +} + +.daily-sheet-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 58px; + padding: 16px 18px 12px; + border-bottom: 1px solid #eef0f2; +} + +.daily-sheet-head h2 { + margin: 0; + color: #24282e; + font-size: 18px; + font-weight: 950; + line-height: 1.2; +} + +.daily-close { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 50%; + background: #f4f6f7; + color: #50545b; + font-size: 25px; + font-weight: 500; + line-height: 1; +} + +.daily-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 13px 18px; + border-bottom: 1px solid #f0f2f3; + color: #8b8d92; + font-size: 14px; + font-weight: 850; +} + +.daily-summary strong { + color: #24282e; + font-size: 20px; + font-weight: 950; + overflow-wrap: anywhere; +} + +.daily-list { + display: grid; + flex: 1 1 auto; + gap: 10px; + min-height: 0; + overflow-y: auto; + padding: 14px 18px max(18px, env(safe-area-inset-bottom)); + -webkit-overflow-scrolling: touch; +} + +.daily-row { + display: grid; + gap: 10px; + padding: 12px; + border: 1px solid #eef0f2; + border-radius: 8px; + background: #fbfcfc; +} + +.daily-date { + color: #24282e; + font-size: 15px; + font-weight: 950; + line-height: 1.2; +} + +.daily-values { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.daily-cell { + min-width: 0; +} + +.daily-cell span { + display: block; + overflow: hidden; + color: #96989d; + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.daily-cell strong { + display: block; + margin-top: 4px; + overflow-wrap: anywhere; + color: #292d33; + font-size: 16px; + font-weight: 950; + line-height: 1.2; +} + +.daily-empty { + display: flex; + align-items: center; + justify-content: center; + min-height: 92px; + border: 1px dashed #e5e8ea; + border-radius: 8px; + color: #8b8d92; + font-size: 14px; + font-weight: 850; + text-align: center; +} + +.daily-modal-open { + overflow: hidden; +} + +.invite-steps { + display: grid; + flex: 1 1 auto; + gap: 12px; + min-height: 0; + overflow-y: auto; + padding: 16px 18px max(20px, env(safe-area-inset-bottom)); + -webkit-overflow-scrolling: touch; +} + +.invite-step { + display: grid; + grid-template-columns: 30px minmax(0, 1fr); + gap: 12px; + align-items: start; + padding: 13px 12px; + border: 1px solid #eef0f2; + border-radius: 8px; + background: #fbfcfc; + color: #3d4148; + font-size: 14px; + font-weight: 800; + line-height: 1.42; +} + +.invite-step-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + background: #e6fbf8; + color: #10a89b; + font-size: 14px; + font-weight: 950; + line-height: 1; +} + +.message-modal[hidden] { + display: none; +} + +.message-modal { + position: fixed; + z-index: 24; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 18px; +} + +.message-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 21, 28, 0.38); +} + +.message-dialog { + position: relative; + display: flex; + flex-direction: column; + width: min(394px, 100%); + max-height: 78vh; + max-height: 78dvh; + overflow: hidden; + border-radius: 14px; + background: #fff; + box-shadow: 0 18px 44px rgba(23, 31, 42, 0.2); +} + +.message-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 58px; + padding: 16px 18px 12px; + border-bottom: 1px solid #eef0f2; +} + +.message-head h2 { + margin: 0; + color: #24282e; + font-size: 18px; + font-weight: 950; + line-height: 1.2; +} + +.message-list { + display: grid; + flex: 1 1 auto; + gap: 12px; + min-height: 0; + overflow-y: auto; + padding: 14px 14px 16px; + -webkit-overflow-scrolling: touch; +} + +.message-card { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + padding: 12px; + border: 1px solid #eef0f2; + border-radius: 8px; + background: #fbfcfc; +} + +.message-user { + display: flex; + align-items: center; + min-width: 0; +} + +.message-avatar { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + flex: 0 0 50px; + overflow: hidden; + border-radius: 50%; + background: linear-gradient(145deg, #55f2df 0%, #d9fff5 100%); + color: #13745f; + font-size: 20px; + font-weight: 950; +} + +.message-avatar img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.message-copy { + min-width: 0; + margin-left: 12px; +} + +.message-name { + overflow: hidden; + color: #24282e; + font-size: 15px; + font-weight: 950; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.message-id { + margin-top: 5px; + overflow: hidden; + color: #8d9096; + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.message-actions { + display: flex; + gap: 6px; +} + +.message-action { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 54px; + min-height: 34px; + padding: 0 10px; + border-radius: 8px; + font-size: 12px; + font-weight: 950; + white-space: nowrap; +} + +.agree-btn { + background: var(--green); + color: #fff; +} + +.refuse-btn { + background: #f1f3f4; + color: #4d525a; +} + +.message-empty { + display: flex; + align-items: center; + justify-content: center; + min-height: 136px; + border: 1px dashed #e5e8ea; + border-radius: 8px; + color: #8b8d92; + font-size: 14px; + font-weight: 850; + text-align: center; +} + +.home-indicator { + position: absolute; + right: 0; + bottom: max(8px, env(safe-area-inset-bottom)); + left: 0; + display: flex; + justify-content: center; + pointer-events: none; +} + +.home-indicator::before { + content: ""; + width: 156px; + height: 5px; + border-radius: 999px; + background: #050505; +} + +.toast { + position: fixed; + z-index: 40; + left: 50%; + bottom: 72px; + width: max-content; + max-width: min(350px, calc(100% - 44px)); + padding: 10px 14px; + border-radius: 8px; + background: rgba(19, 25, 32, 0.88); + color: #fff; + font-size: 13px; + font-weight: 800; + line-height: 1.35; + text-align: center; + transform: translateX(-50%); +} + +.loading-mask { + position: absolute; + z-index: 30; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 18px; + min-height: 100vh; + background: + radial-gradient(circle at 50% 35%, rgba(230, 255, 243, 0.98), rgba(246, 247, 247, 0.98) 44%, rgba(246, 247, 247, 0.96) 100%); +} + +.runner-stick { + position: relative; + width: 92px; + height: 82px; + color: #1ecf70; + filter: drop-shadow(0 8px 14px rgba(30, 207, 112, 0.22)); + animation: runner-bob 0.42s ease-in-out infinite; +} + +.runner-head, +.runner-body, +.runner-arm, +.runner-leg, +.runner-ground { + position: absolute; + display: block; +} + +.runner-head { + top: 5px; + left: 39px; + width: 18px; + height: 18px; + border: 4px solid currentColor; + border-radius: 50%; +} + +.runner-body { + top: 26px; + left: 47px; + width: 5px; + height: 28px; + border-radius: 999px; + background: currentColor; + transform-origin: 50% 3px; + animation: runner-body 0.42s ease-in-out infinite; +} + +.runner-arm, +.runner-leg { + left: 47px; + width: 5px; + border-radius: 999px; + background: currentColor; + transform-origin: 50% 2px; +} + +.runner-arm { + top: 31px; + height: 27px; +} + +.runner-arm-front { + animation: runner-arm-front 0.42s ease-in-out infinite; +} + +.runner-arm-back { + animation: runner-arm-back 0.42s ease-in-out infinite; +} + +.runner-leg { + top: 51px; + height: 33px; +} + +.runner-leg-front { + animation: runner-leg-front 0.42s ease-in-out infinite; +} + +.runner-leg-back { + animation: runner-leg-back 0.42s ease-in-out infinite; +} + +.runner-ground { + right: 6px; + bottom: 2px; + width: 70px; + height: 4px; + overflow: hidden; + border-radius: 999px; + background: rgba(30, 207, 112, 0.18); +} + +.runner-ground::before, +.runner-ground::after { + content: ""; + position: absolute; + top: 0; + width: 24px; + height: 4px; + border-radius: 999px; + background: currentColor; + animation: runner-ground 0.6s linear infinite; +} + +.runner-ground::after { + animation-delay: 0.3s; +} + +.loading-copy { + color: #139a55; + font-size: 15px; + font-weight: 900; + letter-spacing: 0; +} + +@keyframes runner-bob { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +@keyframes runner-body { + 0%, + 100% { + transform: rotate(-7deg); + } + 50% { + transform: rotate(7deg); + } +} + +@keyframes runner-arm-front { + 0%, + 100% { + transform: rotate(62deg); + } + 50% { + transform: rotate(-58deg); + } +} + +@keyframes runner-arm-back { + 0%, + 100% { + transform: rotate(-58deg); + } + 50% { + transform: rotate(62deg); + } +} + +@keyframes runner-leg-front { + 0%, + 100% { + transform: rotate(-58deg); + } + 50% { + transform: rotate(54deg); + } +} + +@keyframes runner-leg-back { + 0%, + 100% { + transform: rotate(54deg); + } + 50% { + transform: rotate(-58deg); + } +} + +@keyframes runner-ground { + 0% { + transform: translateX(78px); + } + 100% { + transform: translateX(-30px); + } +} + +@media (max-width: 360px) { + .access-card { + grid-template-columns: 44px minmax(0, 1fr); + } + + .access-action { + grid-column: 2; + justify-self: start; + } + + .metrics-card, + .daily-values { + grid-template-columns: 1fr; + } + + .vertical-line { + display: none; + } + + .metric:first-child, + .gift-metric { + padding-right: 0; + padding-left: 0; + } + + .message-card { + grid-template-columns: 1fr; + } + + .message-actions { + justify-content: flex-end; + } +} + +html[dir="rtl"] .profile-copy { + margin-right: 20px; + margin-left: 0; +} + +html[dir="rtl"] .profile-message-button { + margin-right: 14px; + margin-left: 0; +} + +html[dir="rtl"] .message-unread-dot { + right: auto; + left: 7px; +} + +html[dir="rtl"] .message-copy { + margin-right: 12px; + margin-left: 0; +} + +html[dir="rtl"] .salary-title svg { + margin-right: 7px; + margin-left: 0; +} + +html[dir="rtl"] .metric:first-child { + padding-right: 0; + padding-left: 20px; +} + +html[dir="rtl"] .gift-metric { + padding-right: 25px; + padding-left: 0; +} + +html[dir="rtl"] .policy-entry-left svg { + margin-right: 0; + margin-left: 18px; +} + +html[dir="rtl"] .question-icon { + margin-right: 0; + margin-left: 18px; +} + +html[dir="rtl"] .policy-entry-link { + text-align: right; +} + +html[dir="rtl"] .policy-entry-divider { + margin-right: 42px; + margin-left: 0; +} + +html[dir="rtl"] .chevron { + margin-right: 8px; + margin-left: 0; + transform: rotate(180deg); +} diff --git a/h5/hyapp/host-center/locales/ar.json b/h5/hyapp/host-center/locales/ar.json index 9618490..a7b2b3a 100644 --- a/h5/hyapp/host-center/locales/ar.json +++ b/h5/hyapp/host-center/locales/ar.json @@ -22,6 +22,7 @@ "open_withdraw_exchange": "فتح صفحة السحب والاستبدال", "open_anchor_policy": "فتح سياسة المضيف", "anchor_policy": "سياسة المضيف", + "platform_policy": "سياسة الوكالة", "policy_loading": "جار تحميل السياسة...", "policy_empty": "لا توجد بيانات سياسة", "policy_missing_token": "رمز الدخول مفقود", diff --git a/h5/hyapp/host-center/locales/en.json b/h5/hyapp/host-center/locales/en.json index af3ec0c..03db105 100644 --- a/h5/hyapp/host-center/locales/en.json +++ b/h5/hyapp/host-center/locales/en.json @@ -22,6 +22,7 @@ "open_withdraw_exchange": "Open withdraw and exchange page", "open_anchor_policy": "Open host policy", "anchor_policy": "Host Policy", + "platform_policy": "Agency Policy", "policy_loading": "Loading policy...", "policy_empty": "No policy data", "policy_missing_token": "Missing token", diff --git a/h5/hyapp/host-center/locales/id.json b/h5/hyapp/host-center/locales/id.json index 9cf3e79..b145d5f 100644 --- a/h5/hyapp/host-center/locales/id.json +++ b/h5/hyapp/host-center/locales/id.json @@ -22,6 +22,7 @@ "open_withdraw_exchange": "Buka halaman tarik dan tukar", "open_anchor_policy": "Buka kebijakan host", "anchor_policy": "Kebijakan Host", + "platform_policy": "Kebijakan Agensi", "policy_loading": "Memuat kebijakan...", "policy_empty": "Tidak ada data kebijakan", "policy_missing_token": "Token tidak ditemukan", diff --git a/h5/hyapp/host-center/locales/tr.json b/h5/hyapp/host-center/locales/tr.json index c98ec34..21f254d 100644 --- a/h5/hyapp/host-center/locales/tr.json +++ b/h5/hyapp/host-center/locales/tr.json @@ -22,6 +22,7 @@ "open_withdraw_exchange": "Çekme ve değişim sayfasını aç", "open_anchor_policy": "Host politikasını aç", "anchor_policy": "Host Politikası", + "platform_policy": "Ajans Politikası", "policy_loading": "Politika yükleniyor...", "policy_empty": "Politika verisi yok", "policy_missing_token": "Token eksik", diff --git a/h5/hyapp/host-center/policy.html b/h5/hyapp/host-center/policy.html index ee4be09..868cc3e 100644 --- a/h5/hyapp/host-center/policy.html +++ b/h5/hyapp/host-center/policy.html @@ -4,7 +4,7 @@ Host Policy - +
@@ -25,7 +25,7 @@ -

Host Policy

+

Host Policy

- + diff --git a/h5/hyapp/host-center/policy.js b/h5/hyapp/host-center/policy.js index 9109468..2d68c89 100644 --- a/h5/hyapp/host-center/policy.js +++ b/h5/hyapp/host-center/policy.js @@ -14,6 +14,7 @@ back: "Back", language_button_aria: "Change language", anchor_policy: "Host Policy", + platform_policy: "Agency Policy", policy_loading: "Loading policy...", policy_empty: "No policy data", policy_missing_token: "Missing token", @@ -32,6 +33,7 @@ back: "رجوع", language_button_aria: "تغيير اللغة", anchor_policy: "سياسة المضيف", + platform_policy: "سياسة الوكالة", policy_loading: "جار تحميل السياسة...", policy_empty: "لا توجد بيانات سياسة", policy_missing_token: "رمز الدخول مفقود", @@ -50,6 +52,7 @@ back: "Geri", language_button_aria: "Dili değiştir", anchor_policy: "Host Politikası", + platform_policy: "Ajans Politikası", policy_loading: "Politika yükleniyor...", policy_empty: "Politika verisi yok", policy_missing_token: "Token eksik", @@ -68,6 +71,7 @@ back: "Kembali", language_button_aria: "Ubah bahasa", anchor_policy: "Kebijakan Host", + platform_policy: "Kebijakan Agensi", policy_loading: "Memuat kebijakan...", policy_empty: "Tidak ada data kebijakan", policy_missing_token: "Token tidak ditemukan", @@ -89,6 +93,14 @@ let currentPolicy = null; let policyStatusKey = "policy_loading"; + function isAgencyPolicy() { + return params.get("from") === "agencycenter"; + } + + function policyTitleKey() { + return isAgencyPolicy() ? "platform_policy" : "anchor_policy"; + } + function readURLParams() { const result = new URLSearchParams(window.location.search); const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : ""; @@ -136,7 +148,7 @@ if (window.location.protocol === "file:") return fallbackMessages[normalizedLang]; try { - const response = await fetch(`./locales/${normalizedLang}.json?v=20260428-2030`, { cache: "no-store" }); + const response = await fetch(`./locales/${normalizedLang}.json?v=20260428-2100`, { cache: "no-store" }); if (response.ok) return await response.json(); } catch (error) { return fallbackMessages[normalizedLang]; @@ -159,6 +171,12 @@ const value = messages[node.dataset.i18nAria]; if (value) node.setAttribute("aria-label", value); }); + + const title = messages[policyTitleKey()] || messages.anchor_policy || fallbackMessages.en.anchor_policy; + document.querySelectorAll("[data-policy-title]").forEach((node) => { + node.textContent = title; + }); + document.querySelector(".policy-page")?.setAttribute("aria-label", title); } function apiBaseURL() { @@ -381,7 +399,8 @@ if (token) nextParams.set("token", token); if (currentLang) nextParams.set("lang", currentLang); const query = nextParams.toString(); - window.location.href = `./index.html${query ? `?${query}` : ""}`; + const fallbackPath = isAgencyPolicy() ? "../agency-center/index.html" : "./index.html"; + window.location.href = `${fallbackPath}${query ? `?${query}` : ""}`; }); } @@ -415,7 +434,7 @@ document.documentElement.lang = currentLang; document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; - document.title = messages.anchor_policy || messages.document_title || "Host Policy"; + document.title = messages[policyTitleKey()] || messages.anchor_policy || messages.document_title || "Host Policy"; renderMessages(messages); renderLanguageControls(messages); if (currentPolicy) { diff --git a/h5/hyapp/host-center/style.css b/h5/hyapp/host-center/style.css index 7f36629..5a4bc2d 100644 --- a/h5/hyapp/host-center/style.css +++ b/h5/hyapp/host-center/style.css @@ -630,8 +630,11 @@ a { } .income-card { + display: flex; + flex-direction: column; + justify-content: center; min-height: 143px; - padding: 19px 24px 18px; + padding: 18px 24px; } .income-head { diff --git a/h5/hyapp/hosts-bill/index.html b/h5/hyapp/hosts-bill/index.html new file mode 100644 index 0000000..bb3bd6e --- /dev/null +++ b/h5/hyapp/hosts-bill/index.html @@ -0,0 +1,145 @@ + + + + + + Salary Bills + + + + +
+ + + + + + +
+
+ +
+
+ History team income + +
+
$0.00
+
+
+ +
+
+ Total bills + 0 +
+
+ Completed + 0 +
+
+ +
+

Work report

+
+
+
+
+ + + + + + + + +
+ +
loading....
+
+
+ + + diff --git a/h5/hyapp/hosts-bill/script.js b/h5/hyapp/hosts-bill/script.js new file mode 100644 index 0000000..7be1ba2 --- /dev/null +++ b/h5/hyapp/hosts-bill/script.js @@ -0,0 +1,940 @@ +(function () { + const API_BASE_URL = "https://jvapi.haiyihy.com"; + const supportedLanguages = ["en", "ar", "tr", "id"]; + const languageLabels = { + en: "EN", + ar: "AR", + tr: "TR", + id: "ID" + }; + + const translations = { + en: { + document_title: "Salary Bills", + page_label: "Salary Bills", + back: "Back", + title: "Salary Bills", + language_button_aria: "Change language", + history_team_income: "History team income", + bill_summary: "Bill summary", + total_bills: "Total bills", + completed_bills: "Completed", + work_report: "Work report", + host_salary: "Host salary", + agent_salary: "Agent salary", + total: "Total", + more: "More", + team_member: "Team member", + enter_host_id: "Enter host ID", + search: "Search", + cancel: "Cancel", + close: "Close", + help: "Help", + open_help: "Open help", + work_report_description: "Work report shows each team bill cycle and salary summary.", + status_description: "Status describes the current settlement state.", + status_completed: "Completed: salary has been settled.", + status_in_progress: "In progress: the bill is still running.", + status_out_of_account: "Out of account: the bill is suspended or unavailable.", + completed: "Completed", + in_progress: "In progress", + out_of_account: "Out of account", + pending: "Pending", + target: "Target", + days: "Days", + uid_prefix: "UID:", + gift_points: "Gift points", + loading: "Loading", + no_bills: "No bills", + no_members: "No hosts", + no_search_result: "No matching hosts", + missing_token: "Missing token", + missing_token_text: "Open this page from the app or add a token in the URL.", + missing_team: "Missing team information", + failed_to_load_data: "Failed to load data", + unknown_user: "Unknown user" + }, + ar: { + document_title: "Salary Bills", + page_label: "Salary Bills", + back: "رجوع", + title: "Salary Bills", + language_button_aria: "تغيير اللغة", + history_team_income: "دخل الفريق السابق", + bill_summary: "ملخص الفواتير", + total_bills: "إجمالي الفواتير", + completed_bills: "مكتمل", + work_report: "تقرير العمل", + host_salary: "راتب المضيف", + agent_salary: "راتب الوكالة", + total: "الإجمالي", + more: "المزيد", + team_member: "عضو الفريق", + enter_host_id: "أدخل معرف المضيف", + search: "بحث", + cancel: "إلغاء", + close: "إغلاق", + help: "مساعدة", + open_help: "فتح المساعدة", + work_report_description: "يعرض تقرير العمل كل دورة فاتورة للفريق وملخص الراتب.", + status_description: "توضح الحالة وضع التسوية الحالي.", + status_completed: "مكتمل: تم تسوية الراتب.", + status_in_progress: "قيد التنفيذ: لا تزال الفاتورة قيد التشغيل.", + status_out_of_account: "خارج الحساب: الفاتورة موقوفة أو غير متاحة.", + completed: "مكتمل", + in_progress: "قيد التنفيذ", + out_of_account: "خارج الحساب", + pending: "قيد الانتظار", + target: "الهدف", + days: "الأيام", + uid_prefix: "UID:", + gift_points: "نقاط الهدايا", + loading: "جار التحميل", + no_bills: "لا توجد فواتير", + no_members: "لا يوجد مضيفون", + no_search_result: "لا توجد نتائج مطابقة", + missing_token: "رمز الدخول مفقود", + missing_token_text: "افتح هذه الصفحة من التطبيق أو أضف token في الرابط.", + missing_team: "معلومات الفريق مفقودة", + failed_to_load_data: "فشل تحميل البيانات", + unknown_user: "مستخدم غير معروف" + }, + tr: { + document_title: "Salary Bills", + page_label: "Salary Bills", + back: "Geri", + title: "Salary Bills", + language_button_aria: "Dili degistir", + history_team_income: "Gecmis takim geliri", + bill_summary: "Fatura ozeti", + total_bills: "Toplam fatura", + completed_bills: "Tamamlandi", + work_report: "Calisma raporu", + host_salary: "Host maasi", + agent_salary: "Ajans maasi", + total: "Toplam", + more: "Daha fazla", + team_member: "Takim uyesi", + enter_host_id: "Host ID girin", + search: "Ara", + cancel: "Iptal", + close: "Kapat", + help: "Yardim", + open_help: "Yardimi ac", + work_report_description: "Calisma raporu her takim fatura dongusunu ve maas ozetini gosterir.", + status_description: "Durum mevcut odeme surecini aciklar.", + status_completed: "Tamamlandi: maas odendi.", + status_in_progress: "Devam ediyor: fatura hala calisiyor.", + status_out_of_account: "Hesap disi: fatura askida veya kullanilamiyor.", + completed: "Tamamlandi", + in_progress: "Devam ediyor", + out_of_account: "Hesap disi", + pending: "Beklemede", + target: "Hedef", + days: "Gun", + uid_prefix: "UID:", + gift_points: "Hediye puani", + loading: "Yukleniyor", + no_bills: "Fatura yok", + no_members: "Host yok", + no_search_result: "Eslesen host yok", + missing_token: "Token eksik", + missing_token_text: "Bu sayfayi uygulamadan acin veya URL'ye token ekleyin.", + missing_team: "Takim bilgisi eksik", + failed_to_load_data: "Veri yuklenemedi", + unknown_user: "Bilinmeyen kullanici" + }, + id: { + document_title: "Salary Bills", + page_label: "Salary Bills", + back: "Kembali", + title: "Salary Bills", + language_button_aria: "Ubah bahasa", + history_team_income: "Riwayat pendapatan tim", + bill_summary: "Ringkasan tagihan", + total_bills: "Total tagihan", + completed_bills: "Selesai", + work_report: "Laporan kerja", + host_salary: "Gaji host", + agent_salary: "Gaji agensi", + total: "Total", + more: "Lainnya", + team_member: "Anggota tim", + enter_host_id: "Masukkan ID host", + search: "Cari", + cancel: "Batal", + close: "Tutup", + help: "Bantuan", + open_help: "Buka bantuan", + work_report_description: "Laporan kerja menampilkan setiap siklus tagihan tim dan ringkasan gaji.", + status_description: "Status menjelaskan kondisi penyelesaian saat ini.", + status_completed: "Selesai: gaji telah diselesaikan.", + status_in_progress: "Berjalan: tagihan masih berjalan.", + status_out_of_account: "Di luar akun: tagihan ditangguhkan atau tidak tersedia.", + completed: "Selesai", + in_progress: "Berjalan", + out_of_account: "Di luar akun", + pending: "Tertunda", + target: "Target", + days: "Hari", + uid_prefix: "UID:", + gift_points: "Poin hadiah", + loading: "Memuat", + no_bills: "Tidak ada tagihan", + no_members: "Tidak ada host", + no_search_result: "Tidak ada host yang cocok", + missing_token: "Token tidak ditemukan", + missing_token_text: "Buka halaman ini dari aplikasi atau tambahkan token di URL.", + missing_team: "Informasi tim tidak ditemukan", + failed_to_load_data: "Gagal memuat data", + unknown_user: "Pengguna tidak dikenal" + } + }; + + const params = readURLParams(); + let currentLang = normalizeLanguage(params.get("lang")); + let currentMessages = translations[currentLang] || translations.en; + let currentTeamId = params.get("teamId") || params.get("id") || ""; + let bills = []; + let currentPolicy = []; + let selectedBill = null; + let selectedMembers = []; + let displayMembers = []; + let memberSearchActive = false; + let detailLoading = false; + + 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"; + } + + function t(key, fallback) { + return currentMessages[key] || translations.en[key] || fallback || key; + } + + function apiBaseURL() { + const override = window.HYAPP_API_BASE_URL || params.get("apiBase"); + if (override) return String(override).replace(/\/+$/, ""); + return API_BASE_URL.replace(/\/+$/, ""); + } + + function normalizeAuthorization(token) { + const value = String(token || "").trim(); + if (!value) return ""; + return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`; + } + + function authHeadersFromToken(token) { + 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=ATYOU;originChild=ATYOU" + }; + } + + async function requestJSON(path, options = {}) { + const token = readRawParam("token"); + if (!token) throw new Error(t("missing_token", "Missing token")); + + const headers = authHeadersFromToken(token); + const method = options.method || "GET"; + const hasBody = options.body !== undefined; + if (hasBody) headers["Content-Type"] = "application/json"; + + const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), { + method, + cache: "no-store", + headers, + body: hasBody ? JSON.stringify(options.body) : undefined + }); + 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.response = data; + throw error; + } + return data.body ?? data.data ?? null; + } + + function buildPageURL(target) { + const url = new URL(target, window.location.href); + const nextParams = new URLSearchParams(url.search); + + params.forEach((value, key) => { + if (!nextParams.has(key)) nextParams.set(key, value); + }); + + const token = readRawParam("token"); + if (token) nextParams.set("token", token); + if (currentLang) nextParams.set("lang", currentLang); + if (currentTeamId) nextParams.set("teamId", currentTeamId); + + url.search = nextParams.toString(); + return url.toString(); + } + + function setLoading(isLoading) { + const root = document.querySelector(".hosts-bill-page"); + const loadingMask = document.querySelector("#loadingMask"); + if (root) root.dataset.loading = isLoading ? "true" : "false"; + if (loadingMask) { + loadingMask.hidden = !isLoading; + loadingMask.setAttribute("aria-busy", String(isLoading)); + } + } + + function renderLanguageControls() { + const languageButton = document.querySelector(".language-button"); + if (languageButton) { + languageButton.textContent = languageLabels[currentLang]; + languageButton.setAttribute("aria-label", t("language_button_aria")); + } + + 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 renderMessages() { + document.querySelectorAll("[data-i18n]").forEach((node) => { + const value = currentMessages[node.dataset.i18n]; + if (value) node.textContent = value; + }); + + document.querySelectorAll("[data-i18n-aria]").forEach((node) => { + const value = currentMessages[node.dataset.i18nAria]; + if (value) node.setAttribute("aria-label", value); + }); + + document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => { + const value = currentMessages[node.dataset.i18nPlaceholder]; + if (value) node.setAttribute("placeholder", value); + }); + } + + function applyLanguage(lang) { + currentLang = normalizeLanguage(lang); + currentMessages = translations[currentLang] || translations.en; + document.documentElement.lang = currentLang; + document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; + document.title = t("document_title"); + renderMessages(); + renderLanguageControls(); + renderSummary(); + renderBills(); + renderBillDetail(); + } + + function setLanguageMenuOpen(open) { + const languageButton = document.querySelector(".language-button"); + const languageMenu = document.querySelector(".language-menu"); + if (!languageButton || !languageMenu) return; + languageMenu.hidden = !open; + languageButton.setAttribute("aria-expanded", String(open)); + } + + function pickTeamInfo(body) { + return body?.teamProfile || body || null; + } + + function pickTeamId(teamInfo) { + return teamInfo?.id || teamInfo?.teamId || teamInfo?.teamProfile?.id || ""; + } + + function pickRecords(body) { + if (Array.isArray(body)) return body; + if (Array.isArray(body?.records)) return body.records; + if (Array.isArray(body?.list)) return body.list; + if (Array.isArray(body?.items)) return body.items; + return []; + } + + function pickPolicyRecords(body) { + if (Array.isArray(body)) return body; + if (Array.isArray(body?.policy)) return body.policy; + if (Array.isArray(body?.policies)) return body.policies; + return pickRecords(body); + } + + function formatMoney(value) { + const number = Number(value); + return Number.isFinite(number) ? `$${number.toFixed(2)}` : "$0.00"; + } + + function formatPlainMoney(value) { + const number = Number(value); + return Number.isFinite(number) ? number.toFixed(2) : "0.00"; + } + + function formatCount(value) { + if (value === null || value === undefined || value === "") return "0"; + const number = Number(value); + if (!Number.isFinite(number)) return String(value); + return Math.trunc(number).toLocaleString("en-US"); + } + + function formatGift(value) { + if (value === null || value === undefined || value === "") return "0"; + const number = Number(value); + if (!Number.isFinite(number)) return String(value); + return Math.trunc(number).toLocaleString("en-US"); + } + + function normalizeStatus(status) { + switch (String(status || "").toUpperCase()) { + case "SETTLED": + case "PAY_OUT": + case "COMPLETED": + return { key: "completed", className: "status-completed" }; + case "HANG_UP": + case "OUT_OF_ACCOUNT": + case "OUT OF ACCOUNT": + return { key: "out_of_account", className: "status-out-of-account" }; + case "PENDING": + return { key: "pending", className: "status-pending" }; + case "UNPAID": + case "IN_PROGRESS": + case "IN PROGRESS": + default: + return { key: "in_progress", className: "status-in-progress" }; + } + } + + function statusText(status) { + const info = normalizeStatus(status); + return t(info.key, status || "-"); + } + + function setStatusBadge(node, status) { + if (!node) return; + const info = normalizeStatus(status); + node.className = `bill-status-badge ${info.className}`; + node.textContent = t(info.key); + } + + function billTitle(bill) { + return bill?.billTitle || bill?.billDayTitle || String(bill?.billBelong || "-"); + } + + function billTotal(bill) { + return Number(bill?.settleResult?.totalSalary || 0); + } + + function billMemberSalary(bill) { + return Number(bill?.settleResult?.memberSalary || 0); + } + + function billAgentSalary(bill) { + return Number(bill?.settleResult?.ownSalary || 0); + } + + function isCompletedBill(bill) { + const status = String(bill?.status || "").toUpperCase(); + return status === "SETTLED" || status === "PAY_OUT" || status === "COMPLETED"; + } + + function calculateHistoryIncome() { + return bills.reduce((sum, bill) => { + if (!isCompletedBill(bill)) return sum; + return sum + billTotal(bill); + }, 0); + } + + function replaceNodeChildren(node, children) { + while (node.firstChild) node.removeChild(node.firstChild); + children.forEach((child) => node.appendChild(child)); + } + + function createSvg(paths) { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("aria-hidden", "true"); + paths.forEach((pathValue) => { + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", pathValue); + svg.appendChild(path); + }); + return svg; + } + + function renderSummary() { + const historyIncome = document.querySelector("#historyIncome"); + const totalBills = document.querySelector("#totalBills"); + const completedBills = document.querySelector("#completedBills"); + + if (historyIncome) historyIncome.textContent = formatMoney(calculateHistoryIncome()); + if (totalBills) totalBills.textContent = formatCount(bills.length); + if (completedBills) completedBills.textContent = formatCount(bills.filter(isCompletedBill).length); + } + + function createMoneyItem(label, value) { + const item = document.createElement("div"); + item.className = "bill-money-item"; + + const labelNode = document.createElement("span"); + labelNode.textContent = label; + + const valueNode = document.createElement("strong"); + valueNode.textContent = formatMoney(value); + + item.appendChild(labelNode); + item.appendChild(valueNode); + return item; + } + + function createBillCard(bill) { + const card = document.createElement("article"); + card.className = "bill-card"; + + const top = document.createElement("div"); + top.className = "bill-card-top"; + + const date = document.createElement("div"); + date.className = "bill-date"; + date.textContent = billTitle(bill); + + const badge = document.createElement("span"); + setStatusBadge(badge, bill?.status); + + top.appendChild(date); + top.appendChild(badge); + + const moneyGrid = document.createElement("div"); + moneyGrid.className = "bill-money-grid"; + moneyGrid.appendChild(createMoneyItem(t("host_salary"), billMemberSalary(bill))); + moneyGrid.appendChild(createMoneyItem(t("agent_salary"), billAgentSalary(bill))); + moneyGrid.appendChild(createMoneyItem(t("total"), billTotal(bill))); + + const more = document.createElement("button"); + more.type = "button"; + more.className = "bill-more-button"; + more.textContent = t("more"); + more.addEventListener("click", () => { + openBillDetail(bill); + }); + + card.appendChild(top); + card.appendChild(moneyGrid); + card.appendChild(more); + return card; + } + + function setListStatus(message) { + const status = document.querySelector("#listStatus"); + if (status) status.textContent = message || ""; + } + + function renderBills() { + renderSummary(); + const list = document.querySelector("#billList"); + if (!list) return; + replaceNodeChildren(list, bills.map(createBillCard)); + setListStatus(bills.length ? "" : t("no_bills")); + } + + function pickMemberProfile(member) { + return member?.memberProfile || member?.userProfile || member?.profile || member?.user || {}; + } + + function pickMemberTarget(member) { + return member?.target || member?.memberTarget || {}; + } + + function pickMemberName(profile, member) { + return profile?.userNickname || profile?.name || member?.userNickname || member?.name || t("unknown_user"); + } + + function pickMemberAccount(profile, member) { + return profile?.ownSpecialId?.account || profile?.account || member?.account || member?.userAccount || profile?.id || member?.userId || "-"; + } + + function memberIdentifiers(member) { + const profile = pickMemberProfile(member); + return [ + profile?.id, + profile?.account, + profile?.ownSpecialId?.account, + member?.userId, + member?.account, + member?.userAccount + ] + .filter((value) => value !== null && value !== undefined && value !== "") + .map((value) => String(value)); + } + + function calculateTargetLevel(acceptGiftValue) { + if (!Array.isArray(currentPolicy) || !currentPolicy.length) return "N/A"; + const giftValue = Number(acceptGiftValue); + if (!Number.isFinite(giftValue)) return "N/A"; + + const sortedPolicy = currentPolicy + .slice() + .filter((item) => item && item.target !== null && item.target !== undefined) + .sort((left, right) => Number(right.target) - Number(left.target)); + + for (const policy of sortedPolicy) { + if (giftValue >= Number(policy.target)) return policy.level ?? "N/A"; + } + + const levels = sortedPolicy + .map((policy) => Number(policy.level)) + .filter((level) => Number.isFinite(level)); + return levels.length ? Math.min(...levels) : "N/A"; + } + + function createMemberAvatar(name, avatarURL) { + const avatar = document.createElement("div"); + avatar.className = "bill-member-avatar"; + + const fallback = document.createElement("span"); + fallback.textContent = (name || t("unknown_user")).slice(0, 1).toUpperCase(); + avatar.appendChild(fallback); + + if (avatarURL) { + const image = document.createElement("img"); + image.src = avatarURL; + image.alt = name; + image.addEventListener("error", () => { + image.remove(); + }); + avatar.appendChild(image); + } + + return avatar; + } + + function createMemberCard(member) { + const profile = pickMemberProfile(member); + const target = pickMemberTarget(member); + const name = pickMemberName(profile, member); + const account = pickMemberAccount(profile, member); + const giftValue = target?.acceptGiftValue || 0; + const effectiveDay = target?.effectiveDay || 0; + + const card = document.createElement("article"); + card.className = "bill-member-card"; + + const user = document.createElement("div"); + user.className = "bill-member-user"; + user.appendChild(createMemberAvatar(name, profile?.userAvatar || member?.userAvatar || member?.avatar || "")); + + const copy = document.createElement("div"); + copy.className = "bill-member-copy"; + + const nameNode = document.createElement("div"); + nameNode.className = "bill-member-name"; + nameNode.textContent = name; + + const accountNode = document.createElement("div"); + accountNode.className = "bill-member-account"; + accountNode.textContent = `${t("uid_prefix")} ${account}`.trim(); + + copy.appendChild(nameNode); + copy.appendChild(accountNode); + user.appendChild(copy); + + const side = document.createElement("div"); + side.className = "bill-member-side"; + + const level = document.createElement("span"); + level.className = "bill-member-target"; + level.textContent = `${t("target")}: ${calculateTargetLevel(giftValue)}`; + + const gift = document.createElement("div"); + gift.className = "bill-member-gift"; + gift.textContent = formatGift(giftValue); + gift.title = `${t("gift_points")}: ${formatGift(giftValue)}`; + + const days = document.createElement("div"); + days.className = "bill-member-days"; + days.textContent = `${t("days")}: ${effectiveDay}`; + + side.appendChild(level); + side.appendChild(gift); + side.appendChild(days); + + card.appendChild(user); + card.appendChild(side); + return card; + } + + function setMemberStatus(message) { + const status = document.querySelector("#memberStatus"); + if (status) status.textContent = message || ""; + } + + function renderSearchState() { + const cancelButton = document.querySelector("#memberCancelButton"); + if (cancelButton) cancelButton.hidden = !memberSearchActive; + } + + function renderBillDetail() { + const date = document.querySelector("#detailBillDate"); + const total = document.querySelector("#detailBillTotal"); + const status = document.querySelector("#detailBillStatus"); + const list = document.querySelector("#billMemberList"); + + if (date) date.textContent = selectedBill ? billTitle(selectedBill) : "-"; + if (total) total.textContent = selectedBill ? formatMoney(billTotal(selectedBill)) : "$0.00"; + if (status && selectedBill) setStatusBadge(status, selectedBill.status); + if (list) replaceNodeChildren(list, displayMembers.map(createMemberCard)); + + renderSearchState(); + + if (detailLoading) { + setMemberStatus(`${t("loading")}...`); + return; + } + + if (!displayMembers.length) { + setMemberStatus(memberSearchActive ? t("no_search_result") : t("no_members")); + return; + } + + setMemberStatus(""); + } + + async function ensureTeamId() { + if (currentTeamId) return currentTeamId; + const body = await requestJSON("/team/entry"); + currentTeamId = pickTeamId(pickTeamInfo(body)); + return currentTeamId; + } + + async function fetchBills() { + const query = new URLSearchParams({ id: String(currentTeamId) }); + const body = await requestJSON(`/team/bill?${query.toString()}`); + bills = pickRecords(body); + renderBills(); + } + + async function fetchPolicy() { + const query = new URLSearchParams({ id: String(currentTeamId) }); + const body = await requestJSON(`/team/release/policy?${query.toString()}`); + currentPolicy = pickPolicyRecords(body); + } + + async function fetchBillMembers(billId) { + const query = new URLSearchParams({ id: String(billId) }); + const body = await requestJSON(`/team/bill/work-members?${query.toString()}`); + return pickRecords(body); + } + + async function openBillDetail(bill) { + selectedBill = bill; + selectedMembers = []; + displayMembers = []; + memberSearchActive = false; + detailLoading = true; + const input = document.querySelector("#memberSearchInput"); + if (input) input.value = ""; + + const modal = document.querySelector("#billDetailModal"); + if (modal) modal.hidden = false; + document.body.classList.add("daily-modal-open"); + renderBillDetail(); + + try { + selectedMembers = await fetchBillMembers(bill.id); + displayMembers = selectedMembers.slice(); + } catch (error) { + console.error("Failed to load bill members:", error); + showToast(error.message || t("failed_to_load_data")); + selectedMembers = []; + displayMembers = []; + } finally { + detailLoading = false; + renderBillDetail(); + } + } + + function setBillDetailOpen(open) { + const modal = document.querySelector("#billDetailModal"); + if (!modal) return; + modal.hidden = !open; + document.body.classList.toggle("daily-modal-open", open || isHelpOpen()); + if (!open) { + selectedBill = null; + selectedMembers = []; + displayMembers = []; + memberSearchActive = false; + detailLoading = false; + } + } + + function isHelpOpen() { + return !document.querySelector("#helpModal")?.hidden; + } + + function setHelpOpen(open) { + const modal = document.querySelector("#helpModal"); + if (!modal) return; + modal.hidden = !open; + document.body.classList.toggle("daily-modal-open", open || !document.querySelector("#billDetailModal")?.hidden); + } + + function searchMembers() { + const input = document.querySelector("#memberSearchInput"); + const query = String(input?.value || "").trim(); + if (!query) { + cancelMemberSearch(); + return; + } + + memberSearchActive = true; + displayMembers = selectedMembers.filter((member) => memberIdentifiers(member).some((id) => id === query)); + renderBillDetail(); + } + + function cancelMemberSearch() { + const input = document.querySelector("#memberSearchInput"); + if (input) input.value = ""; + memberSearchActive = false; + displayMembers = selectedMembers.slice(); + renderBillDetail(); + } + + function showToast(message) { + const toast = document.querySelector("#toast"); + if (!toast) return; + toast.textContent = message; + toast.hidden = false; + window.clearTimeout(showToast.timer); + showToast.timer = window.setTimeout(() => { + toast.hidden = true; + }, 2200); + } + + function goBack() { + if (window.history.length > 1) { + window.history.back(); + return; + } + window.location.href = buildPageURL("../agency-center/index.html"); + } + + function bindEvents() { + document.querySelector(".back-button")?.addEventListener("click", goBack); + + document.querySelector(".language-button")?.addEventListener("click", (event) => { + event.stopPropagation(); + const languageMenu = document.querySelector(".language-menu"); + setLanguageMenuOpen(Boolean(languageMenu?.hidden)); + }); + + document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { + button.addEventListener("click", (event) => { + event.stopPropagation(); + setLanguageMenuOpen(false); + applyLanguage(button.dataset.lang); + }); + }); + + document.querySelector(".bill-help-button")?.addEventListener("click", () => { + setHelpOpen(true); + }); + + document.querySelector("#memberSearchForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + searchMembers(); + }); + + document.querySelector("#memberCancelButton")?.addEventListener("click", cancelMemberSearch); + + document.querySelectorAll("[data-close-detail]").forEach((node) => { + node.addEventListener("click", () => setBillDetailOpen(false)); + }); + + document.querySelectorAll("[data-close-help]").forEach((node) => { + node.addEventListener("click", () => setHelpOpen(false)); + }); + + document.addEventListener("click", () => { + setLanguageMenuOpen(false); + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + setLanguageMenuOpen(false); + setBillDetailOpen(false); + setHelpOpen(false); + } + }); + } + + async function initializePage() { + setLoading(true); + applyLanguage(currentLang); + bindEvents(); + + if (!readRawParam("token")) { + setLoading(false); + setListStatus(t("missing_token_text")); + return; + } + + try { + await ensureTeamId(); + if (!currentTeamId) throw new Error(t("missing_team")); + + const results = await Promise.allSettled([ + fetchBills(), + fetchPolicy() + ]); + results.forEach((result) => { + if (result.status === "rejected") console.warn("Hosts bill request failed:", result.reason); + }); + } catch (error) { + console.error("Failed to initialize hosts bill:", error); + showToast(error.message || t("failed_to_load_data")); + setListStatus(error.message || t("failed_to_load_data")); + } finally { + setLoading(false); + } + } + + initializePage(); +})(); diff --git a/h5/hyapp/hosts-bill/style.css b/h5/hyapp/hosts-bill/style.css new file mode 100644 index 0000000..e2bb09f --- /dev/null +++ b/h5/hyapp/hosts-bill/style.css @@ -0,0 +1,561 @@ +.hosts-bill-page { + overflow-x: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +.hosts-bill-content { + gap: 12px; + padding-top: 8px; +} + +.bill-summary-card { + display: grid; + grid-template-columns: 54px minmax(0, 1fr); + align-items: center; + gap: 14px; + min-height: 112px; + padding: 18px; +} + +.bill-summary-icon { + display: flex; + align-items: center; + justify-content: center; + width: 54px; + height: 54px; + border-radius: 50%; + background: #e7fbf8; + color: #12a99c; +} + +.bill-summary-icon svg { + width: 30px; + height: 30px; +} + +.bill-summary-icon path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.25; +} + +.bill-summary-copy { + min-width: 0; +} + +.bill-summary-head { + display: flex; + align-items: center; + gap: 7px; + color: #24282e; + font-size: 16px; + font-weight: 900; + line-height: 1.2; +} + +.bill-help-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex: 0 0 24px; + color: #30343a; +} + +.bill-help-button svg { + width: 18px; + height: 18px; +} + +.bill-help-button circle, +.bill-help-button path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.8; +} + +.bill-summary-value { + margin-top: 11px; + overflow-wrap: anywhere; + color: #23272d; + font-size: 32px; + font-weight: 950; + line-height: 1.05; +} + +.bill-stat-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.bill-stat-card { + display: grid; + gap: 8px; + min-height: 76px; + padding: 15px 16px; +} + +.bill-stat-card span { + color: #8f9196; + font-size: 13px; + font-weight: 850; + line-height: 1.15; +} + +.bill-stat-card strong { + color: #24282e; + font-size: 24px; + font-weight: 950; + line-height: 1; +} + +.work-report-head { + display: flex; + align-items: center; + justify-content: space-between; + margin: 4px 2px -2px; +} + +.work-report-head h2 { + margin: 0; + color: #24282e; + font-size: 16px; + font-weight: 950; + line-height: 1.2; +} + +.bill-list { + display: grid; + gap: 10px; +} + +.bill-card { + position: relative; + display: grid; + gap: 13px; + padding: 15px 16px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.96); + box-shadow: var(--shadow); +} + +.bill-card-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.bill-date { + min-width: 0; + overflow: hidden; + color: #24282e; + font-size: 17px; + font-weight: 950; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + max-width: 128px; + min-height: 24px; + padding: 0 8px; + border-radius: 5px; + color: #3d73ff; + background: #ebf1fd; + font-size: 12px; + font-weight: 950; + line-height: 1.1; + text-align: center; +} + +.bill-status-badge.status-completed { + color: #13a046; + background: #e8faed; +} + +.bill-status-badge.status-out-of-account { + color: #7a7d83; + background: #e9e9e9; +} + +.bill-status-badge.status-pending { + color: #ff3d40; + background: #feebef; +} + +.bill-money-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.bill-money-item { + min-width: 0; +} + +.bill-money-item span { + display: block; + overflow: hidden; + color: #8f9196; + font-size: 12px; + font-weight: 850; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-money-item strong { + display: block; + margin-top: 5px; + overflow: hidden; + color: #24282e; + font-size: 15px; + font-weight: 950; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-more-button { + display: inline-flex; + align-items: center; + justify-content: center; + justify-self: end; + min-height: 34px; + padding: 0 13px; + border-radius: 8px; + background: #f2fbf9; + color: #10a890; + font-size: 13px; + font-weight: 950; +} + +.bill-list-status { + display: flex; + align-items: center; + justify-content: center; + min-height: 52px; + color: #8f9196; + font-size: 13px; + font-weight: 850; + text-align: center; +} + +.bill-list-status:empty { + display: none; +} + +.bill-detail-sheet { + gap: 10px; + padding-bottom: max(14px, env(safe-area-inset-bottom)); +} + +.bill-detail-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 18px; +} + +.bill-detail-meta div { + display: grid; + gap: 5px; + min-width: 0; +} + +.bill-detail-meta span:first-child { + overflow: hidden; + color: #777c84; + font-size: 15px; + font-weight: 850; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-detail-meta strong { + color: #24282e; + font-size: 20px; + font-weight: 950; + line-height: 1; +} + +.bill-search-form { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 8px; + padding: 0 18px; +} + +.bill-search-control { + display: flex; + align-items: center; + min-width: 0; + height: 42px; + padding: 0 12px; + border: 1px solid #edf0f1; + border-radius: 8px; + background: #f8faf9; +} + +.bill-search-control svg { + width: 20px; + height: 20px; + flex: 0 0 20px; + margin-right: 9px; + color: #7f858b; +} + +.bill-search-control path { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.2; +} + +.bill-search-control input { + width: 100%; + min-width: 0; + border: 0; + outline: 0; + background: transparent; + color: #25282e; + font: inherit; + font-size: 14px; + font-weight: 800; +} + +.bill-search-control input::placeholder { + color: #a7aab0; +} + +.bill-search-button, +.bill-cancel-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 64px; + height: 42px; + padding: 0 12px; + border-radius: 8px; + font-size: 13px; + font-weight: 950; + white-space: nowrap; +} + +.bill-search-button { + background: var(--green); + color: #fff; + box-shadow: 0 6px 14px rgba(37, 216, 120, 0.18); +} + +.bill-cancel-button { + background: #f2f4f5; + color: #4d525a; +} + +.bill-member-list { + display: grid; + gap: 9px; + overflow-y: auto; + padding: 0 18px; +} + +.bill-member-card { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + min-height: 76px; + padding: 12px; + border-radius: 10px; + background: #fff; + box-shadow: 0 2px 10px rgba(28, 38, 50, 0.08); +} + +.bill-member-user { + display: flex; + align-items: center; + min-width: 0; +} + +.bill-member-avatar { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + flex: 0 0 48px; + overflow: hidden; + border-radius: 50%; + background: linear-gradient(145deg, #55f2df 0%, #d9fff5 100%); + color: #13745f; + font-size: 19px; + font-weight: 950; +} + +.bill-member-avatar img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.bill-member-copy { + min-width: 0; + margin-left: 11px; +} + +.bill-member-name { + overflow: hidden; + color: #24282e; + font-size: 15px; + font-weight: 950; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-member-account { + margin-top: 5px; + overflow: hidden; + color: #8f9196; + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-member-side { + display: grid; + justify-items: end; + gap: 5px; + min-width: 88px; +} + +.bill-member-target { + display: inline-flex; + max-width: 118px; + min-height: 23px; + align-items: center; + justify-content: center; + overflow: hidden; + padding: 0 7px; + border-radius: 5px; + background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%); + color: #fff; + font-size: 12px; + font-weight: 900; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-member-gift { + max-width: 118px; + overflow: hidden; + color: #ffb627; + font-size: 13px; + font-weight: 950; + line-height: 1.15; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bill-member-days { + color: #8f9196; + font-size: 11px; + font-weight: 850; + line-height: 1.1; +} + +.bill-member-status { + min-height: 42px; + padding: 0 18px; +} + +.bill-help-sheet { + max-height: 80vh; + max-height: 80dvh; +} + +.bill-help-content { + overflow-y: auto; + padding: 0 20px 22px; + color: #555a62; + font-size: 14px; + font-weight: 800; + line-height: 1.45; +} + +.bill-help-content p { + margin: 0 0 12px; +} + +html[dir="rtl"] .bill-search-control svg { + margin-right: 0; + margin-left: 9px; +} + +html[dir="rtl"] .bill-member-copy { + margin-right: 11px; + margin-left: 0; +} + +@media (max-width: 370px) { + .bill-summary-card { + grid-template-columns: 48px minmax(0, 1fr); + padding: 16px; + } + + .bill-summary-icon { + width: 48px; + height: 48px; + } + + .bill-money-grid { + grid-template-columns: 1fr; + } + + .bill-search-form { + grid-template-columns: minmax(0, 1fr) auto; + } + + .bill-cancel-button { + grid-column: 2; + } + + .bill-member-card { + grid-template-columns: 1fr; + } + + .bill-member-side { + justify-items: start; + min-width: 0; + margin-left: 59px; + } + + html[dir="rtl"] .bill-member-side { + margin-right: 59px; + margin-left: 0; + } +}