From 1a3984fb634655dbef3f0517ea51862e491330ad Mon Sep 17 00:00:00 2001 From: 170-carry Date: Thu, 30 Apr 2026 13:29:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B8=81=E5=95=86=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- h5/activity/recharge-agency-list/index.html | 451 ++++++++- h5/coin-seller/index.back.html | 35 + h5/coin-seller/index.html | 52 +- h5/hyapp/recharge-center/index.html | 126 ++- .../recharge-center/original-vue-logic.md | 90 +- h5/hyapp/recharge-center/script.js | 895 ++++++++++++++++-- h5/hyapp/recharge-center/style.css | 513 +++++++++- h5/mifa-pay/app.js | 50 +- 8 files changed, 2054 insertions(+), 158 deletions(-) create mode 100644 h5/coin-seller/index.back.html diff --git a/h5/activity/recharge-agency-list/index.html b/h5/activity/recharge-agency-list/index.html index 9042d54..89844ec 100644 --- a/h5/activity/recharge-agency-list/index.html +++ b/h5/activity/recharge-agency-list/index.html @@ -36,7 +36,7 @@ width: min(100%, var(--page-width)); aspect-ratio: 1080 / 2410; margin: 0 auto; - overflow: hidden; + overflow: visible; background: var(--brown-990); } @@ -45,7 +45,7 @@ width: 100%; height: 100%; background: var(--brown-990); - overflow: hidden; + overflow: visible; } .sr-only { @@ -118,7 +118,141 @@ left: 5.3704%; top: 63.0705%; width: 89.1667%; - height: 11.4108%; + height: auto; + overflow: visible; + } + + .agency-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 12px; + overflow: visible; + } + + .agency-row { + width: 100%; + aspect-ratio: 963 / 275; + display: flex; + align-items: center; + gap: 8px; + padding: 0 24px; + border: 1px solid var(--gold-line); + border-radius: 2.9076% / 10.1818%; + background: var(--panel-brown); + box-shadow: + inset 0 0 0 1px rgba(255, 214, 109, 0.16), + 0 6px 18px rgba(38, 11, 0, 0.35); + } + + .agency-avatar { + width: 52px; + height: 52px; + flex: 0 0 52px; + border-radius: 50%; + border: 1px solid rgba(255, 220, 136, 0.78); + background: rgba(46, 18, 0, 0.55); + object-fit: cover; + } + + .agency-main { + min-width: 0; + flex: 1; + } + + .agency-name { + display: block; + color: #fff3cf; + font-size: 13px; + font-weight: 700; + line-height: 1.15; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .agency-meta { + margin-top: 4px; + display: flex; + align-items: center; + gap: 5px; + color: rgba(255, 238, 195, 0.74); + font-size: 10px; + line-height: 1.15; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .agency-flags { + display: flex; + align-items: center; + gap: 3px; + flex: 0 0 auto; + } + + .agency-flag { + width: 16px; + height: 16px; + border-radius: 50%; + border: 1px solid rgba(255, 238, 195, 0.62); + object-fit: cover; + background: rgba(46, 18, 0, 0.6); + } + + .agency-actions { + flex: 0 0 78px; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + gap: 5px; + } + + .agency-action { + appearance: none; + width: 78px; + height: 24px; + margin: 0; + padding: 0 7px; + border: 1px solid rgba(255, 222, 126, 0.72); + border-radius: 999px; + font-size: 10px; + font-weight: 800; + line-height: 1; + letter-spacing: 0; + white-space: nowrap; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + box-shadow: 0 2px 8px rgba(38, 11, 0, 0.3); + } + + .agency-action:active { + transform: translateY(1px); + } + + .agency-action.whatsapp { + border-color: rgba(159, 255, 185, 0.84); + background: linear-gradient(180deg, #27c66a, #168a49); + color: #ffffff; + } + + .agency-action.profile { + background: linear-gradient(180deg, #fff2a8, #efb43d); + color: #552300; + } + + .agency-state { + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + color: rgba(255, 238, 195, 0.78); + font-size: 13px; + font-weight: 650; + text-align: center; + padding: 0 18px; + aspect-ratio: 963 / 275; border: 1px solid var(--gold-line); border-radius: 2.9076% / 10.1818%; background: var(--panel-brown); @@ -127,13 +261,15 @@ .bottom-frame { position: absolute; left: 1.8056%; - top: 91%; + top: var(--bottom-frame-top, 91%); width: 96.1574%; height: 2.4066%; display: block; max-width: none; pointer-events: none; user-select: none; + transform: scaleY(-1); + transform-origin: center; } @@ -151,7 +287,11 @@ Recharge Agency -
+
+
+
Loading agencies...
+
+
+ diff --git a/h5/coin-seller/index.back.html b/h5/coin-seller/index.back.html new file mode 100644 index 0000000..5cc60d3 --- /dev/null +++ b/h5/coin-seller/index.back.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + Yumi H5 + + + + + + + +
+ + diff --git a/h5/coin-seller/index.html b/h5/coin-seller/index.html index 5cc60d3..5cdbc92 100644 --- a/h5/coin-seller/index.html +++ b/h5/coin-seller/index.html @@ -1,35 +1,19 @@ - - - - - - - - - - - - - Yumi H5 - - - - - - - -
- + + + + + + Document + + + + diff --git a/h5/hyapp/recharge-center/index.html b/h5/hyapp/recharge-center/index.html index 25aaf55..62204de 100644 --- a/h5/hyapp/recharge-center/index.html +++ b/h5/hyapp/recharge-center/index.html @@ -4,7 +4,7 @@ Recharge Center - +
@@ -28,6 +28,49 @@
+
+
+ + U +
+
+
-
+
UID: -
+
+ + + +
+
+
+ +
+
+
+
Coins
+
+ +
+
+ + 0 +
+
+
@@ -59,28 +102,26 @@
-
UID: -
- +
-
@@ -94,13 +135,60 @@

FAQ

How to recharge?

-

Enter the user's ID, choose a package, then select a payment method.

+

Enter the user's ID, enter the coins amount, then confirm recharge.

How to check recharge result?

After payment, return to the app and check the user's wallet balance.

+ + + +
-
loading....
+
loading....
- + diff --git a/h5/hyapp/recharge-center/original-vue-logic.md b/h5/hyapp/recharge-center/original-vue-logic.md index a46cb6c..fc5de4b 100644 --- a/h5/hyapp/recharge-center/original-vue-logic.md +++ b/h5/hyapp/recharge-center/original-vue-logic.md @@ -32,6 +32,44 @@ ## APIs +### Get current user profile + +`GET /team/member/profile` + +Usage in new H5: +- Loads the current logged-in user's account card before the recharge flow. +- This is the same current-user profile source reused by the hyapp center pages. + +Reusable fields: +- `id` +- `account` +- `userNickname` +- `name` +- `userAvatar` +- `ownSpecialId.account` + +### Get current user coin balance + +`GET /wallet/freight/balance` + +Original Vue reference: +- The old recharge agency / coin seller page imports `a4` from the main bundle. +- `a4` maps to `dX()`, which calls `GET /wallet/freight/balance`. +- Original UI stores `body.toString()` and displays it under `my_coins`. + +Usage in new H5: +- Displays the current user's Coins card. +- If the request fails, the page keeps showing `0` and the recharge flow still works. + +Reusable fields: +- `body` as number +- `body.gold` +- `body.goldBalance` +- `body.coin` +- `body.coins` +- `body.balance` +- `body.amount` + ### Search target user `GET /user/user-profile/open-search?account=` @@ -52,6 +90,55 @@ Reusable fields: - `isFreightAgent` - `isSuperFreightAgent` +### Recharge coins to target user + +`POST /wallet/freight/ship` + +Body: + +```json +{ + "acceptUserId": "", + "quantity": 1000 +} +``` + +Original Vue reference: +- The old coin seller page imports `a5` from the main bundle. +- `a5` maps to `mX(payload)`, which calls `POST /wallet/freight/ship`. +- Original submit logic sends `te({ acceptUserId: o, quantity: t })`. +- If the response has `body`, Vue uses it as the updated current coin balance. + +Usage in new H5: +- After searching the target user, the page shows one coins input and a confirm recharge button. +- It no longer shows country selection, payment methods, or recharge packages for this direct transfer flow. + +Reusable fields: +- `acceptUserId`: target user `id` +- `quantity`: entered coins amount +- response `body`: updated current user coin balance when returned + +### Get transfer history + +`GET /wallet/freight/balance/running/water/client/flowByUserId` + +Query params: +- `userId=` +- `type=0 | 1` +- `searchId=` +- `lastId=` + +Original Vue reference: +- The seller records page imports `ad` as `Z` from the main bundle. +- `Z(params)` maps to `hX(params)`, which calls this endpoint. +- Original page uses `type=0` for `income` and `type=1` for `expenditure`. +- For each record, Vue displays `+quantity` when `record.type === 0`, otherwise `-quantity`. +- For user display, Vue reads `record.userAccount` for income records and `record.acceptUserAccount` for expenditure records. + +Usage in new H5: +- The Coins card has a right-side History button. +- History opens in a center modal with Income / Expenditure tabs, user ID search, and load more using `lastId`. + ### Get payment application `GET /order/web/pay/application/{applicationId}` @@ -160,4 +247,5 @@ Reusable fields: - New page lives at `h5/hyapp/recharge-center`. - It keeps the existing hyapp center architecture: static `index.html`, `style.css`, `script.js`. - It uses URL `token` for Java API authorization and sends the same H5 request headers used by the existing hyapp center pages. -- It merges the original `/recharge` and `/recharge-pay-way` flows into one page, so it does not depend on Vue router or Pinia `targetUser`. +- It uses the old coin seller direct-transfer API for the current UI: search target user, enter coins amount, and confirm recharge. +- The older `/recharge-pay-way` country / package / third-party payment flow is documented above for reference but is not rendered in the current H5 page. diff --git a/h5/hyapp/recharge-center/script.js b/h5/hyapp/recharge-center/script.js index 9ca46a4..d7d10cf 100644 --- a/h5/hyapp/recharge-center/script.js +++ b/h5/hyapp/recharge-center/script.js @@ -18,137 +18,245 @@ language_button_aria: "Change language", help: "Help", close: "Close", + my_account: "My account", + profile_card: "User profile", + whatsapp_label: "WhatsApp:", + add_whatsapp: "+ add whatsapp", + add_whatsapp_title: "Add WhatsApp", + whatsapp_placeholder: "Enter WhatsApp", + whatsapp_required: "Please enter WhatsApp", + whatsapp_too_long: "WhatsApp cannot exceed 64 characters", + whatsapp_saved: "WhatsApp saved", + save: "Save", + saving: "Saving...", + balance_title: "Coins", + history: "History", search_title: "Recharge user", search_subtitle: "Search by user ID", search_placeholder: "Enter user ID", search: "Search", + cancel: "Cancel", searching: "Searching...", target_user: "Target user", uid_prefix: "UID:", change: "Change", - select_country: "Select a country", - payment_methods: "Payment methods", - select_package: "Select a package", + coin_recharge_title: "Recharge coins", + coin_recharge_subtitle: "Enter coins amount", + coin_amount_placeholder: "Enter coins amount", + confirm_recharge: "Confirm recharge", loading_data: "Loading...", + loading_mask: "loading....", no_country: "No supported country", no_packages: "No recharge packages", user_not_found: "User info not found", missing_token: "Missing token", failed_to_load: "Failed to load data", + recharging: "Recharging...", + recharge_success: "Recharge success", + please_enter_valid_amount: "Please enter a valid amount", + transaction_records: "Transaction records", + income: "Income", + expenditure: "Expenditure", + enter_user_id: "Enter user ID", + coins: "coins", + no_transaction_records: "No transaction records", + no_selling_records_yet: "No records yet", + load_more: "Load more", payment_not_available: "Payment not available", creating_order: "Creating order...", redirecting_payment: "Redirecting to payment...", order_created: "Order created", faq_title: "FAQ", faq_question_1: "How to recharge?", - faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", + faq_answer_1: "Enter the user's ID, enter the coins amount, then confirm recharge.", faq_question_2: "How to check recharge result?", faq_answer_2: "After payment, return to the app and check the user's wallet balance." }, ar: { - document_title: "Recharge Center", - page_label: "Recharge Center", - back: "Back", - title: "Recharge Center", - language_button_aria: "Change language", - help: "Help", - close: "Close", - search_title: "Recharge user", - search_subtitle: "Search by user ID", - search_placeholder: "Enter user ID", - search: "Search", - searching: "Searching...", - target_user: "Target user", + document_title: "مركز الشحن", + page_label: "مركز الشحن", + back: "رجوع", + title: "مركز الشحن", + language_button_aria: "تغيير اللغة", + help: "مساعدة", + close: "إغلاق", + my_account: "حسابي", + profile_card: "ملف المستخدم", + whatsapp_label: "WhatsApp:", + add_whatsapp: "+ إضافة واتساب", + add_whatsapp_title: "إضافة واتساب", + whatsapp_placeholder: "أدخل واتساب", + whatsapp_required: "يرجى إدخال واتساب", + whatsapp_too_long: "لا يمكن أن يتجاوز واتساب 64 حرفا", + whatsapp_saved: "تم حفظ واتساب", + save: "حفظ", + saving: "جار الحفظ...", + balance_title: "العملات", + history: "السجل", + search_title: "شحن مستخدم", + search_subtitle: "ابحث بواسطة معرف المستخدم", + search_placeholder: "أدخل معرف المستخدم", + search: "بحث", + cancel: "إلغاء", + searching: "جار البحث...", + target_user: "المستخدم المستهدف", uid_prefix: "UID:", - change: "Change", - select_country: "Select a country", - payment_methods: "Payment methods", - select_package: "Select a package", - loading_data: "Loading...", - no_country: "No supported country", - no_packages: "No recharge packages", - user_not_found: "User info not found", - missing_token: "Missing token", - failed_to_load: "Failed to load data", - payment_not_available: "Payment not available", - creating_order: "Creating order...", - redirecting_payment: "Redirecting to payment...", - order_created: "Order created", - faq_title: "FAQ", - faq_question_1: "How to recharge?", - faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", - faq_question_2: "How to check recharge result?", - faq_answer_2: "After payment, return to the app and check the user's wallet balance." + change: "تغيير", + coin_recharge_title: "شحن العملات", + coin_recharge_subtitle: "أدخل عدد العملات", + coin_amount_placeholder: "أدخل عدد العملات", + confirm_recharge: "تأكيد الشحن", + loading_data: "جار التحميل...", + loading_mask: "جار التحميل....", + no_country: "لا توجد دولة مدعومة", + no_packages: "لا توجد باقات شحن", + user_not_found: "لم يتم العثور على المستخدم", + missing_token: "التوكن مفقود", + failed_to_load: "فشل تحميل البيانات", + recharging: "جار الشحن...", + recharge_success: "تم الشحن بنجاح", + please_enter_valid_amount: "يرجى إدخال مبلغ صالح", + transaction_records: "سجلات التحويل", + income: "الوارد", + expenditure: "الصادر", + enter_user_id: "أدخل معرف المستخدم", + coins: "عملات", + no_transaction_records: "لا توجد سجلات تحويل", + no_selling_records_yet: "لا توجد سجلات بعد", + load_more: "تحميل المزيد", + payment_not_available: "الدفع غير متاح", + creating_order: "جار إنشاء الطلب...", + redirecting_payment: "جار الانتقال إلى الدفع...", + order_created: "تم إنشاء الطلب", + faq_title: "الأسئلة الشائعة", + faq_question_1: "كيف أشحن؟", + faq_answer_1: "أدخل معرف المستخدم، ثم أدخل عدد العملات واضغط تأكيد الشحن.", + faq_question_2: "كيف أتحقق من نتيجة الشحن؟", + faq_answer_2: "بعد الشحن، تحقق من رصيد العملات في حساب المستخدم." }, tr: { - document_title: "Recharge Center", - page_label: "Recharge Center", + document_title: "Yukleme Merkezi", + page_label: "Yukleme Merkezi", back: "Geri", - title: "Recharge Center", + title: "Yukleme Merkezi", language_button_aria: "Dili degistir", - help: "Help", + help: "Yardim", close: "Kapat", - search_title: "Recharge user", + my_account: "Hesabim", + profile_card: "Kullanici profili", + whatsapp_label: "WhatsApp:", + add_whatsapp: "+ WhatsApp ekle", + add_whatsapp_title: "WhatsApp ekle", + whatsapp_placeholder: "WhatsApp gir", + whatsapp_required: "Lutfen WhatsApp girin", + whatsapp_too_long: "WhatsApp 64 karakteri gecemez", + whatsapp_saved: "WhatsApp kaydedildi", + save: "Kaydet", + saving: "Kaydediliyor...", + balance_title: "Coinler", + history: "Gecmis", + search_title: "Kullaniciya yukleme", search_subtitle: "Kullanici ID ile ara", search_placeholder: "Kullanici ID gir", search: "Ara", + cancel: "Iptal", searching: "Araniyor...", - target_user: "Target user", + target_user: "Hedef kullanici", uid_prefix: "UID:", change: "Degistir", - select_country: "Ulke sec", - payment_methods: "Payment methods", - select_package: "Paket sec", + coin_recharge_title: "Coin yukle", + coin_recharge_subtitle: "Coin miktari gir", + coin_amount_placeholder: "Coin miktari gir", + confirm_recharge: "Yuklemeyi onayla", loading_data: "Yukleniyor...", + loading_mask: "yukleniyor....", no_country: "Desteklenen ulke yok", - no_packages: "Recharge package yok", + no_packages: "Yukleme paketi yok", user_not_found: "Kullanici bulunamadi", missing_token: "Token eksik", failed_to_load: "Veri yuklenemedi", - payment_not_available: "Payment not available", - creating_order: "Order olusturuluyor...", - redirecting_payment: "Payment sayfasina yonlendiriliyor...", - order_created: "Order created", - faq_title: "FAQ", - faq_question_1: "How to recharge?", - faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", - faq_question_2: "How to check recharge result?", - faq_answer_2: "After payment, return to the app and check the user's wallet balance." + recharging: "Yukleniyor...", + recharge_success: "Yukleme basarili", + please_enter_valid_amount: "Gecerli bir miktar girin", + transaction_records: "Islem kayitlari", + income: "Gelir", + expenditure: "Gider", + enter_user_id: "Kullanici ID gir", + coins: "coin", + no_transaction_records: "Islem kaydi yok", + no_selling_records_yet: "Henuz kayit yok", + load_more: "Daha fazla yukle", + payment_not_available: "Odeme kullanilamiyor", + creating_order: "Siparis olusturuluyor...", + redirecting_payment: "Odeme sayfasina yonlendiriliyor...", + order_created: "Siparis olusturuldu", + faq_title: "SSS", + faq_question_1: "Nasil yukleme yapilir?", + faq_answer_1: "Kullanici ID girin, coin miktarini yazin ve yuklemeyi onaylayin.", + faq_question_2: "Yukleme sonucu nasil kontrol edilir?", + faq_answer_2: "Yuklemeden sonra kullanicinin coin bakiyesini kontrol edin." }, id: { - document_title: "Recharge Center", - page_label: "Recharge Center", + document_title: "Pusat Isi Ulang", + page_label: "Pusat Isi Ulang", back: "Kembali", - title: "Recharge Center", + title: "Pusat Isi Ulang", language_button_aria: "Ubah bahasa", - help: "Help", + help: "Bantuan", close: "Tutup", - search_title: "Recharge user", - search_subtitle: "Cari dengan user ID", - search_placeholder: "Masukkan user ID", + my_account: "Akun saya", + profile_card: "Profil pengguna", + whatsapp_label: "WhatsApp:", + add_whatsapp: "+ tambah whatsapp", + add_whatsapp_title: "Tambah WhatsApp", + whatsapp_placeholder: "Masukkan WhatsApp", + whatsapp_required: "Masukkan WhatsApp", + whatsapp_too_long: "WhatsApp tidak boleh lebih dari 64 karakter", + whatsapp_saved: "WhatsApp tersimpan", + save: "Simpan", + saving: "Menyimpan...", + balance_title: "Koin", + history: "Riwayat", + search_title: "Isi ulang pengguna", + search_subtitle: "Cari dengan ID pengguna", + search_placeholder: "Masukkan ID pengguna", search: "Cari", + cancel: "Batal", searching: "Mencari...", - target_user: "Target user", + target_user: "Pengguna tujuan", uid_prefix: "UID:", change: "Ganti", - select_country: "Pilih negara", - payment_methods: "Payment methods", - select_package: "Pilih paket", + coin_recharge_title: "Isi ulang koin", + coin_recharge_subtitle: "Masukkan jumlah koin", + coin_amount_placeholder: "Masukkan jumlah koin", + confirm_recharge: "Konfirmasi isi ulang", loading_data: "Memuat...", + loading_mask: "memuat....", no_country: "Negara tidak tersedia", - no_packages: "Paket recharge tidak tersedia", + no_packages: "Paket isi ulang tidak tersedia", user_not_found: "User tidak ditemukan", missing_token: "Token tidak ditemukan", failed_to_load: "Gagal memuat data", - payment_not_available: "Payment not available", - creating_order: "Membuat order...", - redirecting_payment: "Membuka payment...", - order_created: "Order created", + recharging: "Mengisi ulang...", + recharge_success: "Isi ulang berhasil", + please_enter_valid_amount: "Masukkan jumlah yang valid", + transaction_records: "Riwayat transaksi", + income: "Masuk", + expenditure: "Keluar", + enter_user_id: "Masukkan ID pengguna", + coins: "koin", + no_transaction_records: "Tidak ada riwayat transaksi", + no_selling_records_yet: "Belum ada riwayat", + load_more: "Muat lagi", + payment_not_available: "Pembayaran tidak tersedia", + creating_order: "Membuat pesanan...", + redirecting_payment: "Membuka pembayaran...", + order_created: "Pesanan dibuat", faq_title: "FAQ", - faq_question_1: "How to recharge?", - faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", - faq_question_2: "How to check recharge result?", - faq_answer_2: "After payment, return to the app and check the user's wallet balance." + faq_question_1: "Bagaimana cara isi ulang?", + faq_answer_1: "Masukkan ID pengguna, isi jumlah koin, lalu konfirmasi isi ulang.", + faq_question_2: "Bagaimana cara memeriksa hasil isi ulang?", + faq_answer_2: "Setelah isi ulang, periksa saldo koin pengguna." } }; @@ -162,10 +270,35 @@ loadingConfig: false, ordering: false, activeOrderKey: "", + rechargeAmount: "", + rechargeLocked: false, + rechargeStatusText: "", + rechargeStatusType: "", + historyOpen: false, + historyLoading: false, + historyTab: 0, + historySearch: "", + historyStatusText: "", + historyStatusType: "", + historyIncome: [], + historyExpenditure: [], + historyHasMore: { + 0: true, + 1: true + }, searchStatusText: "", searchStatusType: "", paymentStatusText: "", paymentStatusType: "", + selfProfile: null, + sellerProfile: null, + whatsapp: "", + whatsappOpen: false, + whatsappDraft: "", + whatsappSaving: false, + whatsappStatusText: "", + whatsappStatusType: "", + coins: 0, profile: null, application: null, appCode: "", @@ -250,6 +383,12 @@ return API_BASE_URL.replace(/\/+$/, ""); } + function goApiBaseURL() { + const override = window.HYAPP_GO_API_BASE_URL || params.get("goApiBase") || params.get("goBase"); + if (override) return String(override).replace(/\/+$/, ""); + return apiBaseURL(); + } + function normalizeAuthorization(token) { const value = String(token || "").trim(); if (!value) return ""; @@ -268,7 +407,7 @@ }; } - async function requestJSON(path, options = {}) { + async function requestBaseJSON(baseURL, path, options = {}) { const token = readRawParam("token"); if (!token) throw new Error(message("missing_token")); @@ -284,7 +423,7 @@ request.body = JSON.stringify(options.body); } - const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), request); + const response = await fetch(new URL(path, `${baseURL}/`).toString(), request); const data = await response.json().catch(() => ({})); if (!response.ok || data.status === false) { const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); @@ -295,10 +434,48 @@ return data.body ?? data.data ?? null; } + async function requestJSON(path, options = {}) { + return requestBaseJSON(apiBaseURL(), path, options); + } + + async function requestGoJSON(path, options = {}) { + return requestBaseJSON(goApiBaseURL(), path, options); + } + function endpointOpenSearch(account) { return `/user/user-profile/open-search?account=${encodeURIComponent(account)}`; } + function endpointSelfProfile() { + return "/team/member/profile"; + } + + function endpointSellerProfile() { + return "/app/h5/recharge-agency/profile"; + } + + function endpointSellerWhatsapp() { + return "/app/h5/recharge-agency/profile/whatsapp"; + } + + function endpointCoinBalance() { + return "/wallet/freight/balance"; + } + + function endpointFreightShip() { + return "/wallet/freight/ship"; + } + + function endpointTransferHistory({ userId, type, searchId = "", lastId = "" }) { + const query = new URLSearchParams({ + userId: String(userId || ""), + type: String(type), + searchId: String(searchId || ""), + lastId: String(lastId || "") + }); + return `/wallet/freight/balance/running/water/client/flowByUserId?${query.toString()}`; + } + function endpointApplication() { return `/order/web/pay/application/${encodeURIComponent(RECHARGE_APPLICATION_ID)}`; } @@ -330,7 +507,7 @@ } function updateModalLock() { - const hasOpen = Array.from(document.querySelectorAll(".help-modal")).some((modal) => !modal.hidden); + const hasOpen = Array.from(document.querySelectorAll(".help-modal, .history-modal, .whatsapp-modal")).some((modal) => !modal.hidden); document.body.classList.toggle("modal-open", hasOpen); } @@ -363,6 +540,45 @@ return number.toFixed(2); } + function pickCoinAmount(body) { + if (typeof body === "number" || typeof body === "string") return body; + if (body && typeof body === "object") { + return body.gold ?? body.goldBalance ?? body.coin ?? body.coins ?? body.balance ?? body.amount ?? 0; + } + return 0; + } + + function pickWhatsapp(body) { + if (typeof body === "string") return body.trim(); + if (body && typeof body === "object") { + return String(body.whatsapp || body.whatsApp || body.contactInfo || "").trim(); + } + return ""; + } + + function formatCoins(value) { + const number = Number(value); + if (!Number.isFinite(number)) return "0"; + return Number.isInteger(number) ? String(number) : String(Number(number.toFixed(2))); + } + + function normalizeRecordList(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?.content)) return body.content; + if (Array.isArray(body?.rows)) return body.rows; + return []; + } + + function formatRecordTime(value) { + if (!value) return "-"; + const date = new Date(Number(value) || value); + if (Number.isNaN(date.getTime())) return String(value); + const pad = (item) => String(item).padStart(2, "0"); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; + } + function pickSpecialAccount(profile) { return profile?.ownSpecialId?.account || profile?.specialId?.account || profile?.specialAccount || profile?.shortAccount || profile?.shortId || ""; } @@ -383,6 +599,13 @@ return profile?.id || profile?.userId || ""; } + function pickRecordUser(record) { + if (Number(record?.type) === 1) { + return record?.acceptUserAccount || record?.acceptUser || record?.targetUser || {}; + } + return record?.userAccount || record?.user || record?.sourceUser || {}; + } + function commodityType(profile = state.profile) { if (profile?.isSuperFreightAgent) return "FREIGHT_GOLD_SUPER"; if (profile?.isFreightAgent) return "FREIGHT_GOLD"; @@ -409,6 +632,40 @@ status.classList.toggle("is-error", type === "error"); } + function setRechargeStatus(text = "", type = "") { + state.rechargeStatusText = text; + state.rechargeStatusType = type; + const status = document.querySelector("#coinRechargeStatus"); + if (!status) return; + status.textContent = text; + status.hidden = !text; + status.classList.toggle("is-error", type === "error"); + } + + function setHistoryStatus(text = "", type = "") { + state.historyStatusText = text; + state.historyStatusType = type; + const status = document.querySelector("#historyStatus"); + if (!status) return; + status.textContent = text; + status.hidden = !text; + status.classList.toggle("is-error", type === "error"); + } + + function setWhatsappStatus(text = "", type = "") { + state.whatsappStatusText = text; + state.whatsappStatusType = type; + const status = document.querySelector("#whatsappStatus"); + if (!status) return; + status.textContent = text; + status.hidden = !text; + status.classList.toggle("is-error", type === "error"); + } + + function sanitizeCoinAmount(value) { + return String(value || "").replace(/\D/g, "").replace(/^0+(?=\d)/, ""); + } + function renderSearch() { const input = document.querySelector("#accountInput"); const button = document.querySelector("#searchButton"); @@ -421,14 +678,14 @@ setSearchStatus(state.searchStatusText, state.searchStatusType); } - function renderAvatar(profile) { - const image = document.querySelector("#targetAvatar"); - const fallback = document.querySelector("#targetAvatarFallback"); + function renderProfileAvatar(profile, imageSelector, fallbackSelector, defaultLetter = "U") { + const image = document.querySelector(imageSelector); + const fallback = document.querySelector(fallbackSelector); if (!image || !fallback) return; const name = pickProfileName(profile); const avatar = profile?.userAvatar || profile?.avatar || ""; - fallback.textContent = String(name || "U").slice(0, 1).toUpperCase() || "U"; + fallback.textContent = String(name || defaultLetter).slice(0, 1).toUpperCase() || defaultLetter; if (!avatar) { image.hidden = true; @@ -448,6 +705,37 @@ image.src = avatar; } + function renderSelfProfile() { + const profile = state.selfProfile || {}; + const nameNode = document.querySelector("#selfName"); + const uidNode = document.querySelector("#selfUid"); + if (nameNode) nameNode.textContent = pickProfileName(profile); + if (uidNode) uidNode.textContent = `${message("uid_prefix")} ${pickDisplayAccount(profile)}`.trim(); + renderProfileAvatar(profile, "#selfAvatar", "#selfAvatarFallback"); + renderSelfWhatsapp(); + } + + function renderSelfWhatsapp() { + const icon = document.querySelector("#selfWhatsappIcon"); + const text = document.querySelector("#selfWhatsappText"); + const button = document.querySelector("#addWhatsappButton"); + const whatsapp = String(state.whatsapp || "").trim(); + if (icon) icon.hidden = !whatsapp; + if (text) { + text.textContent = whatsapp; + text.hidden = !whatsapp; + } + if (button) { + button.hidden = Boolean(whatsapp); + button.textContent = message("add_whatsapp"); + } + } + + function renderBalance() { + const node = document.querySelector("#selfBalance"); + if (node) node.textContent = formatCoins(state.coins); + } + function renderTarget() { const card = document.querySelector("#targetCard"); if (card) card.hidden = !state.profile; @@ -457,7 +745,143 @@ const uidNode = document.querySelector("#targetUid"); if (nameNode) nameNode.textContent = pickProfileName(state.profile); if (uidNode) uidNode.textContent = `${message("uid_prefix")} ${pickDisplayAccount(state.profile)}`.trim(); - renderAvatar(state.profile); + renderProfileAvatar(state.profile, "#targetAvatar", "#targetAvatarFallback"); + } + + function renderCoinRecharge() { + const card = document.querySelector("#coinRechargeCard"); + const input = document.querySelector("#coinAmountInput"); + const button = document.querySelector("#confirmRechargeButton"); + const amount = Number(state.rechargeAmount); + const canSubmit = Boolean(state.profile && Number.isFinite(amount) && amount > 0 && !state.rechargeLocked); + + if (card) card.hidden = !state.profile; + if (input && document.activeElement !== input) input.value = state.rechargeAmount; + if (button) { + button.disabled = !canSubmit; + button.textContent = state.rechargeLocked ? message("recharging") : message("confirm_recharge"); + } + setRechargeStatus(state.rechargeStatusText, state.rechargeStatusType); + } + + function activeHistoryList() { + return state.historyTab === 1 ? state.historyExpenditure : state.historyIncome; + } + + function setActiveHistoryList(records) { + if (state.historyTab === 1) state.historyExpenditure = records; + else state.historyIncome = records; + } + + function createHistoryAvatar(user) { + const avatar = document.createElement("div"); + avatar.className = "history-avatar"; + const url = user?.userAvatar || user?.avatar || ""; + const name = user?.userNickName || user?.userNickname || user?.name || "-"; + if (url) { + const image = document.createElement("img"); + image.src = url; + image.alt = ""; + image.addEventListener("error", () => { + avatar.textContent = String(name).slice(0, 1).toUpperCase() || "U"; + }); + avatar.appendChild(image); + } else { + avatar.textContent = String(name).slice(0, 1).toUpperCase() || "U"; + } + return avatar; + } + + function createHistoryRecord(record) { + const user = pickRecordUser(record); + const item = document.createElement("article"); + item.className = "history-record"; + + const userWrap = document.createElement("div"); + userWrap.className = "history-record-user"; + const copy = document.createElement("div"); + copy.className = "history-record-copy"; + const name = document.createElement("div"); + name.className = "history-record-name"; + name.textContent = user?.userNickName || user?.userNickname || user?.name || "-"; + const account = document.createElement("div"); + account.className = "history-record-account"; + account.textContent = `${message("uid_prefix")} ${user?.account || user?.id || "-"}`.trim(); + copy.append(name, account); + userWrap.append(createHistoryAvatar(user), copy); + + const side = document.createElement("div"); + side.className = "history-record-side"; + const amount = document.createElement("div"); + const isIncome = Number(record?.type) === 0; + amount.className = `history-record-amount ${isIncome ? "is-income" : "is-expense"}`; + amount.textContent = `${isIncome ? "+" : "-"}${formatCoins(record?.quantity)} ${message("coins")}`; + const time = document.createElement("div"); + time.className = "history-record-time"; + time.textContent = formatRecordTime(record?.createTime); + side.append(amount, time); + + item.append(userWrap, side); + return item; + } + + function renderHistoryModal() { + const modal = document.querySelector("#historyModal"); + const input = document.querySelector("#historySearchInput"); + const list = document.querySelector("#historyList"); + const loadMore = document.querySelector("#historyLoadMore"); + if (modal) modal.hidden = !state.historyOpen; + if (input && document.activeElement !== input) input.value = state.historySearch; + + document.querySelectorAll("[data-history-tab]").forEach((button) => { + const active = Number(button.dataset.historyTab) === state.historyTab; + button.classList.toggle("is-active", active); + button.setAttribute("aria-selected", String(active)); + }); + + setHistoryStatus(state.historyStatusText, state.historyStatusType); + + if (list) { + const records = activeHistoryList(); + if (state.historyLoading && !records.length) { + const loading = document.createElement("div"); + loading.className = "history-empty"; + loading.textContent = message("loading_data"); + replaceChildren(list, [loading]); + } else if (!records.length) { + const empty = document.createElement("div"); + empty.className = "history-empty"; + const title = document.createElement("strong"); + title.textContent = message("no_transaction_records"); + const text = document.createElement("span"); + text.textContent = message("no_selling_records_yet"); + empty.append(title, text); + replaceChildren(list, [empty]); + } else { + replaceChildren(list, records.map(createHistoryRecord)); + } + } + + if (loadMore) { + const hasRecords = activeHistoryList().length > 0; + loadMore.hidden = state.historyLoading || !hasRecords || !state.historyHasMore[state.historyTab]; + loadMore.disabled = state.historyLoading; + } + updateModalLock(); + } + + function renderWhatsappModal() { + const modal = document.querySelector("#whatsappModal"); + const input = document.querySelector("#whatsappInput"); + const button = document.querySelector("#saveWhatsappButton"); + if (modal) modal.hidden = !state.whatsappOpen; + if (input && document.activeElement !== input) input.value = state.whatsappDraft; + if (button) { + button.disabled = state.whatsappSaving || !String(state.whatsappDraft || "").trim(); + button.textContent = state.whatsappSaving ? message("saving") : message("save"); + } + setWhatsappStatus(state.whatsappStatusText, state.whatsappStatusType); + updateModalLock(); } function countryName(item) { @@ -575,8 +999,10 @@ button.addEventListener("click", () => placeOrder(channel, goods)); const coin = document.createElement("span"); - coin.className = "coin-mark"; - coin.textContent = "$"; + coin.className = "coin-mark gold-coin-icon"; + const shine = document.createElement("span"); + shine.className = "gold-coin-shine"; + coin.appendChild(shine); const content = document.createElement("span"); content.className = "package-content"; content.textContent = goods?.content || goods?.awardContent || "-"; @@ -637,10 +1063,79 @@ } function renderAll() { + renderSelfProfile(); + renderBalance(); renderSearch(); renderTarget(); - renderCountries(); - renderPayment(); + renderCoinRecharge(); + renderWhatsappModal(); + } + + async function fetchSelfProfile() { + state.selfProfile = await requestJSON(endpointSelfProfile()) || {}; + renderSelfProfile(); + return state.selfProfile; + } + + async function fetchSellerProfile() { + state.sellerProfile = await requestGoJSON(endpointSellerProfile()) || {}; + state.whatsapp = pickWhatsapp(state.sellerProfile); + renderSelfWhatsapp(); + return state.sellerProfile; + } + + async function fetchCoinBalance() { + const body = await requestJSON(endpointCoinBalance()); + state.coins = pickCoinAmount(body); + renderBalance(); + return state.coins; + } + + function resetHistoryCache() { + state.historyIncome = []; + state.historyExpenditure = []; + state.historyHasMore = { + 0: true, + 1: true + }; + state.historyStatusText = ""; + state.historyStatusType = ""; + } + + async function fetchHistoryRecords({ append = false } = {}) { + const userId = pickProfileId(state.selfProfile); + if (!userId) { + setHistoryStatus(message("user_not_found"), "error"); + renderHistoryModal(); + return; + } + + const currentRecords = activeHistoryList(); + const lastId = append && currentRecords.length ? currentRecords[currentRecords.length - 1]?.id || "" : ""; + state.historyLoading = true; + setHistoryStatus(message("loading_data")); + renderHistoryModal(); + + try { + const body = await requestJSON(endpointTransferHistory({ + userId, + type: state.historyTab, + searchId: state.historySearch, + lastId + })); + const records = normalizeRecordList(body); + setActiveHistoryList(append ? currentRecords.concat(records) : records); + state.historyHasMore[state.historyTab] = records.length > 0; + setHistoryStatus(""); + } catch (error) { + console.error("Failed to load transfer history:", error); + const text = error.response?.errorMsg || error.message || message("failed_to_load"); + setHistoryStatus(text, "error"); + showToast(text); + } finally { + state.historyLoading = false; + renderHistoryModal(); + } } async function fetchApplication() { @@ -724,6 +1219,9 @@ state.searchLocked = true; state.profile = null; state.payProfile = null; + state.rechargeAmount = ""; + state.rechargeStatusText = ""; + state.rechargeStatusType = ""; state.countryList = []; state.selectedCountryId = ""; state.commodity = []; @@ -737,9 +1235,11 @@ const profile = Array.isArray(result) ? result[0] : result; if (!profile || !pickProfileId(profile)) throw new Error(message("user_not_found")); state.profile = profile; + state.rechargeAmount = ""; + state.rechargeStatusText = ""; + state.rechargeStatusType = ""; setSearchStatus(""); renderAll(); - await loadRechargeOptions(); } catch (error) { console.error("Recharge target search failed:", error); const text = error.response?.errorMsg || error.message || message("user_not_found"); @@ -776,6 +1276,67 @@ } } + async function rechargeCoins(event) { + if (event) event.preventDefault(); + if (state.rechargeLocked) return; + + const input = document.querySelector("#coinAmountInput"); + const amountValue = sanitizeCoinAmount(input?.value || state.rechargeAmount); + state.rechargeAmount = amountValue; + if (input) input.value = amountValue; + + const quantity = Number(amountValue); + const userId = pickProfileId(state.profile); + if (!userId) { + const text = message("user_not_found"); + setRechargeStatus(text, "error"); + showToast(text); + renderAll(); + return; + } + + if (!Number.isFinite(quantity) || quantity <= 0) { + const text = message("please_enter_valid_amount"); + setRechargeStatus(text, "error"); + showToast(text); + renderAll(); + return; + } + + state.rechargeLocked = true; + setRechargeStatus(message("recharging")); + renderAll(); + + try { + const result = await requestJSON(endpointFreightShip(), { + method: "POST", + body: { + acceptUserId: String(userId), + quantity + } + }); + if (result !== null && result !== undefined) { + state.coins = pickCoinAmount(result); + renderBalance(); + } else { + await fetchCoinBalance(); + } + state.rechargeAmount = ""; + resetHistoryCache(); + setRechargeStatus(message("recharge_success")); + showToast(message("recharge_success")); + if (state.historyOpen) fetchHistoryRecords(); + } catch (error) { + console.error("Recharge coins failed:", error); + const text = error.response?.errorMsg || error.message || message("failed_to_load"); + setRechargeStatus(text, "error"); + showToast(text); + } finally { + state.rechargeLocked = false; + renderAll(); + } + } + function payEnvironment() { const value = String(window.AIRWALLEX_ENV || params.get("payEnv") || window.ENV || "").toLowerCase(); if (value === "production" || value === "prod") return "prod"; @@ -942,6 +1503,9 @@ function changeTarget() { state.profile = null; state.payProfile = null; + state.rechargeAmount = ""; + state.rechargeStatusText = ""; + state.rechargeStatusType = ""; state.countryList = []; state.selectedCountryId = ""; state.regionId = ""; @@ -953,6 +1517,109 @@ window.setTimeout(() => document.querySelector("#accountInput")?.focus(), 60); } + async function openHistoryModal() { + state.historyOpen = true; + renderHistoryModal(); + if (!activeHistoryList().length && !state.historyLoading) { + await fetchHistoryRecords(); + } + } + + function closeHistoryModal() { + state.historyOpen = false; + renderHistoryModal(); + } + + async function switchHistoryTab(type) { + const nextType = Number(type); + if (state.historyTab === nextType || state.historyLoading) return; + state.historyTab = nextType; + state.historyStatusText = ""; + state.historyStatusType = ""; + renderHistoryModal(); + if (!activeHistoryList().length) await fetchHistoryRecords(); + } + + async function searchHistory(event) { + if (event) event.preventDefault(); + state.historySearch = String(document.querySelector("#historySearchInput")?.value || "").trim(); + resetHistoryCache(); + renderHistoryModal(); + await fetchHistoryRecords(); + } + + async function clearHistorySearch() { + if (!state.historySearch && !document.querySelector("#historySearchInput")?.value) return; + state.historySearch = ""; + resetHistoryCache(); + renderHistoryModal(); + await fetchHistoryRecords(); + } + + function openWhatsappModal() { + state.whatsappOpen = true; + state.whatsappDraft = state.whatsapp || ""; + state.whatsappStatusText = ""; + state.whatsappStatusType = ""; + renderWhatsappModal(); + window.setTimeout(() => document.querySelector("#whatsappInput")?.focus(), 60); + } + + function closeWhatsappModal() { + if (state.whatsappSaving) return; + state.whatsappOpen = false; + state.whatsappStatusText = ""; + state.whatsappStatusType = ""; + renderWhatsappModal(); + } + + async function saveWhatsapp(event) { + if (event) event.preventDefault(); + if (state.whatsappSaving) return; + + const input = document.querySelector("#whatsappInput"); + const whatsapp = String(input?.value || state.whatsappDraft || "").trim(); + state.whatsappDraft = whatsapp; + + if (!whatsapp) { + setWhatsappStatus(message("whatsapp_required"), "error"); + renderWhatsappModal(); + return; + } + + if (whatsapp.length > 64) { + setWhatsappStatus(message("whatsapp_too_long"), "error"); + renderWhatsappModal(); + return; + } + + state.whatsappSaving = true; + setWhatsappStatus(message("saving")); + renderWhatsappModal(); + + try { + const profile = await requestGoJSON(endpointSellerWhatsapp(), { + method: "POST", + body: { whatsapp } + }) || {}; + state.sellerProfile = profile; + state.whatsapp = pickWhatsapp(profile) || whatsapp; + state.whatsappOpen = false; + state.whatsappStatusText = ""; + state.whatsappStatusType = ""; + showToast(message("whatsapp_saved")); + } catch (error) { + console.error("Save WhatsApp failed:", error); + const text = error.response?.errorMsg || error.message || message("failed_to_load"); + state.whatsappStatusText = text; + state.whatsappStatusType = "error"; + showToast(text); + } finally { + state.whatsappSaving = false; + renderAll(); + } + } + function closePage() { try { if (window.app && typeof window.app.closePage === "function") { @@ -1024,14 +1691,49 @@ if (!state.query.trim()) setSearchStatus(""); renderSearch(); }); + document.querySelector("#coinAmountInput")?.addEventListener("input", (event) => { + const value = sanitizeCoinAmount(event.target.value); + state.rechargeAmount = value; + if (event.target.value !== value) event.target.value = value; + setRechargeStatus(""); + renderCoinRecharge(); + }); document.querySelector("#searchForm")?.addEventListener("submit", searchTarget); + document.querySelector("#coinRechargeForm")?.addEventListener("submit", rechargeCoins); document.querySelector("[data-change-target]")?.addEventListener("click", changeTarget); document.querySelector("[data-help-open]")?.addEventListener("click", () => setModalOpen("#helpModal", true)); + document.querySelector("[data-history-open]")?.addEventListener("click", () => openHistoryModal()); + document.querySelector("[data-whatsapp-open]")?.addEventListener("click", openWhatsappModal); + document.querySelector("#whatsappInput")?.addEventListener("input", (event) => { + state.whatsappDraft = String(event.target.value || ""); + if (state.whatsappStatusText) setWhatsappStatus(""); + renderWhatsappModal(); + }); + document.querySelector("#whatsappForm")?.addEventListener("submit", saveWhatsapp); + document.querySelector("#historySearchInput")?.addEventListener("input", (event) => { + if (!String(event.target.value || "").trim()) state.historySearch = ""; + }); + document.querySelector("#historySearchForm")?.addEventListener("submit", searchHistory); + document.querySelector("#historyClearButton")?.addEventListener("click", () => clearHistorySearch()); + document.querySelector("#historyLoadMore")?.addEventListener("click", () => fetchHistoryRecords({ append: true })); + document.querySelectorAll("[data-history-tab]").forEach((button) => { + button.addEventListener("click", () => switchHistoryTab(button.dataset.historyTab)); + }); document.querySelectorAll("[data-help-close]").forEach((node) => { node.addEventListener("click", () => setModalOpen("#helpModal", false)); }); + document.querySelectorAll("[data-history-close]").forEach((node) => { + node.addEventListener("click", closeHistoryModal); + }); + document.querySelectorAll("[data-whatsapp-close]").forEach((node) => { + node.addEventListener("click", closeWhatsappModal); + }); document.addEventListener("keydown", (event) => { - if (event.key === "Escape") setModalOpen("#helpModal", false); + if (event.key === "Escape") { + setModalOpen("#helpModal", false); + closeHistoryModal(); + closeWhatsappModal(); + } }); } @@ -1040,14 +1742,27 @@ bindEvents(); applyLanguage(currentLang); renderAll(); - setLoading(false); if (!readRawParam("token")) { setSearchStatus(message("missing_token"), "error"); renderAll(); + setLoading(false); return; } + const initialResults = await Promise.allSettled([ + fetchSelfProfile(), + fetchCoinBalance(), + fetchSellerProfile() + ]); + initialResults.forEach((result) => { + if (result.status === "rejected") { + console.warn("Recharge center initial request failed:", result.reason); + } + }); + renderAll(); + setLoading(false); + if (state.query) { await searchTarget(); } diff --git a/h5/hyapp/recharge-center/style.css b/h5/hyapp/recharge-center/style.css index 9e0880a..255f645 100644 --- a/h5/hyapp/recharge-center/style.css +++ b/h5/hyapp/recharge-center/style.css @@ -189,8 +189,7 @@ button { } .search-card, -.country-card, -.payment-card { +.coin-recharge-card { display: grid; gap: 14px; padding: 16px; @@ -238,6 +237,11 @@ button { gap: 10px; } +.coin-recharge-form { + display: grid; + gap: 12px; +} + .search-form input { width: 100%; min-width: 0; @@ -257,7 +261,46 @@ button { box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12); } +.coin-input-shell { + display: flex; + align-items: center; + gap: 10px; + min-height: 48px; + padding: 0 12px; + border: 1px solid #e4eeee; + border-radius: 8px; + background: #fbfdfd; +} + +.coin-input-shell:focus-within { + border-color: rgba(21, 189, 169, 0.7); + box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12); +} + +.input-coin { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex: 0 0 28px; + border-radius: 50%; +} + +.coin-input-shell input { + width: 100%; + min-width: 0; + height: 46px; + border: 0; + outline: 0; + background: transparent; + color: #24282e; + font-size: 16px; + font-weight: 900; +} + .search-form button, +.coin-recharge-form button, .package-button, .mini-action { min-height: 44px; @@ -274,7 +317,13 @@ button { padding: 0 14px; } +.coin-recharge-form button { + width: 100%; + padding: 0 14px; +} + .search-form button:disabled, +.coin-recharge-form button:disabled, .package-button:disabled, .mini-action:disabled { opacity: 0.55; @@ -304,6 +353,121 @@ button { padding: 16px; } +.balance-card { + display: grid; + gap: 12px; + padding: 16px; + overflow: hidden; + background: + radial-gradient(circle at 88% 10%, rgba(75, 231, 216, 0.18), rgba(75, 231, 216, 0) 34%), + linear-gradient(145deg, #ffffff 0%, #f8fffd 100%); +} + +.balance-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.balance-label { + color: #24282e; + font-size: 15px; + font-weight: 950; + line-height: 1.2; +} + +.balance-icon { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + flex: 0 0 34px; + border-radius: 50%; +} + +.history-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 34px; + padding: 0 10px; + border-radius: 999px; + background: #e7fbf8; + color: #0aa397; + font-size: 12px; + font-weight: 950; +} + +.history-button svg { + width: 17px; + height: 17px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.2; +} + +.gold-coin-icon { + position: relative; + overflow: hidden; + border: 2px solid #f0ad17; + background: + radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.72) 0 9%, rgba(255, 255, 255, 0) 25%), + linear-gradient(145deg, #ffe88a 0%, #ffc928 47%, #f4a800 100%); + box-shadow: + inset 0 -3px 0 rgba(174, 111, 0, 0.16), + inset 0 2px 0 rgba(255, 255, 255, 0.5); +} + +.gold-coin-icon::before { + content: ""; + position: absolute; + inset: 7px; + border: 2px solid rgba(174, 111, 0, 0.24); + border-radius: 50%; +} + +.gold-coin-shine { + position: absolute; + top: 8px; + left: 10px; + width: 8px; + height: 4px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.72); + transform: rotate(-28deg); +} + +.balance-value { + display: flex; + align-items: center; + gap: 9px; + min-width: 0; + color: #181d24; +} + +.balance-coin { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex: 0 0 28px; + border-radius: 50%; +} + +.balance-value strong { + min-width: 0; + overflow-wrap: anywhere; + font-size: 32px; + font-weight: 950; + line-height: 1.05; +} + .avatar-shell { display: flex; align-items: center; @@ -358,6 +522,10 @@ button { white-space: nowrap; } +.self-profile-card .name { + margin-top: 0; +} + .meta { margin-top: 6px; overflow: hidden; @@ -369,6 +537,50 @@ button { white-space: nowrap; } +.whatsapp-meta { + display: flex; + align-items: center; + gap: 8px; +} + +.whatsapp-meta span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.whatsapp-icon, +.whatsapp-input-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + flex: 0 0 22px; + border-radius: 50%; + background: #e7fbf8; + color: #0aa397; +} + +.whatsapp-icon svg, +.whatsapp-input-icon svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.9; +} + +.whatsapp-add-button { + color: #0aa397; + font-size: 13px; + font-weight: 950; + line-height: 1.2; +} + .mini-action { min-width: 78px; min-height: 36px; @@ -377,6 +589,14 @@ button { font-size: 12px; } +.mini-action-icon { + width: 36px; + min-width: 36px; + padding: 0; + font-size: 18px; + line-height: 1; +} + .country-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); @@ -513,9 +733,6 @@ button { width: 32px; height: 32px; border-radius: 50%; - background: linear-gradient(145deg, #ffe694, #ffc93f); - color: #8b5d00; - font-weight: 950; } .package-content, @@ -548,7 +765,9 @@ button { text-align: center; } -.help-modal { +.help-modal, +.history-modal, +.whatsapp-modal { position: fixed; inset: 0; z-index: 40; @@ -629,6 +848,280 @@ button { line-height: 1.4; } +.history-dialog { + width: min(100%, 390px); +} + +.whatsapp-dialog { + width: min(100%, 360px); +} + +.whatsapp-form { + display: grid; + gap: 12px; + padding: 0 16px 18px; +} + +.whatsapp-input-shell { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + min-width: 0; + min-height: 44px; + border: 1px solid #e4eeee; + border-radius: 8px; + padding: 0 12px; + background: #fbfdfd; +} + +.whatsapp-input-shell:focus-within { + border-color: rgba(21, 189, 169, 0.7); + box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12); +} + +.whatsapp-form input { + width: 100%; + min-width: 0; + height: 42px; + border: 0; + outline: 0; + padding: 0; + background: transparent; + color: #24282e; + font-size: 14px; + font-weight: 850; +} + +.whatsapp-form button { + min-height: 44px; + border-radius: 8px; + background: linear-gradient(135deg, #30dfb4, #15bda9); + color: #fff; + font-size: 14px; + font-weight: 950; + box-shadow: 0 8px 18px rgba(21, 189, 169, 0.24); +} + +.whatsapp-form button:disabled { + opacity: 0.55; +} + +.history-body { + display: grid; + gap: 12px; + overflow-y: auto; + padding: 0 16px 18px; + -webkit-overflow-scrolling: touch; +} + +.history-search-form { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 8px; +} + +.history-search-form input { + min-width: 0; + height: 40px; + border: 1px solid #e4eeee; + border-radius: 8px; + outline: 0; + padding: 0 10px; + background: #fbfdfd; + color: #24282e; + font-size: 13px; + font-weight: 800; +} + +.history-search-form input:focus { + border-color: rgba(21, 189, 169, 0.7); + box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12); +} + +.history-search-form button { + min-width: 56px; + min-height: 40px; + padding: 0 10px; + border-radius: 8px; + background: #e7fbf8; + color: #0aa397; + font-size: 12px; + font-weight: 950; +} + +.history-search-form button[type="submit"] { + background: linear-gradient(135deg, #30dfb4, #15bda9); + color: #fff; + box-shadow: 0 8px 18px rgba(21, 189, 169, 0.18); +} + +.history-tabs { + position: relative; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + padding: 4px; + border-radius: 8px; + background: #f3f7f7; +} + +.history-tab { + min-height: 36px; + border-radius: 7px; + color: #7a7f87; + font-size: 13px; + font-weight: 950; +} + +.history-tab.is-active { + background: #fff; + color: #0aa397; + box-shadow: 0 5px 14px rgba(22, 31, 42, 0.07); +} + +.history-status { + padding: 10px 12px; + border-radius: 8px; + background: #f1fbf9; + color: #0b958a; + font-size: 12px; + font-weight: 850; +} + +.history-status.is-error { + background: #fff3f3; + color: var(--danger); +} + +.history-list { + display: grid; + gap: 10px; +} + +.history-record { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-width: 0; + padding: 12px; + border: 1px solid #eef0f2; + border-radius: 8px; + background: #fbfcfc; +} + +.history-record-user { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1; +} + +.history-avatar { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + width: 42px; + height: 42px; + flex: 0 0 42px; + border-radius: 50%; + background: linear-gradient(145deg, #d9fff5, #43e7d8); + color: #0b5f56; + font-size: 16px; + font-weight: 950; +} + +.history-avatar img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.history-record-copy { + min-width: 0; +} + +.history-record-name, +.history-record-account { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.history-record-name { + color: #25282e; + font-size: 14px; + font-weight: 950; +} + +.history-record-account { + margin-top: 5px; + color: var(--muted); + font-size: 12px; + font-weight: 800; +} + +.history-record-side { + display: grid; + justify-items: end; + gap: 5px; + min-width: 88px; +} + +.history-record-amount { + color: #24282e; + font-size: 13px; + font-weight: 950; + text-align: right; +} + +.history-record-amount.is-income { + color: #0aa397; +} + +.history-record-amount.is-expense { + color: #e09a00; +} + +.history-record-time { + color: var(--muted); + direction: ltr; + font-size: 11px; + font-weight: 750; +} + +.history-empty { + display: grid; + justify-items: center; + gap: 6px; + padding: 32px 14px; + border-radius: 8px; + background: #f7fbfa; + color: #8b8d92; + font-size: 13px; + font-weight: 800; + text-align: center; +} + +.history-empty strong { + color: #333840; + font-size: 15px; + font-weight: 950; +} + +.history-load-more { + min-height: 40px; + border-radius: 8px; + background: #e7fbf8; + color: #0aa397; + font-size: 13px; + font-weight: 950; +} + .home-indicator { width: 134px; height: 5px; @@ -772,6 +1265,14 @@ body.modal-open { text-align: right; } +[dir="rtl"] .history-record-side { + justify-items: start; +} + +[dir="rtl"] .history-record-amount { + text-align: left; +} + @keyframes runner-bob { 0%, 100% { diff --git a/h5/mifa-pay/app.js b/h5/mifa-pay/app.js index 9af609e..c71c188 100644 --- a/h5/mifa-pay/app.js +++ b/h5/mifa-pay/app.js @@ -33,8 +33,10 @@ const deps = { const state = { applicationId: defaults.applicationId, productType: defaults.productType, + application: null, userProfile: null, countries: [], + regionId: "", selectedCountryId: "", commodityCard: null, selectedGoodsId: "", @@ -112,6 +114,20 @@ function currentCountryId() { return state.selectedCountryId; } +function currentRegionId() { + return state.regionId; +} + +function currentUserAccount() { + return String( + state.userProfile?.account || + state.userProfile?.actualAccount || + state.userProfile?.ownSpecialId?.account || + state.userProfile?.specialId?.account || + "" + ).trim(); +} + function getCountryLabel(item) { return ( item?.label || @@ -311,8 +327,36 @@ async function refreshMeta() { persistSettings(); els.applicationId.value = state.applicationId; - const countriesResp = await deps.getApi("/order/web/pay/country"); - state.countries = Array.isArray(countriesResp?.body) ? countriesResp.body : []; + const applicationResp = await deps.getApi( + `/order/web/pay/application/${encodeURIComponent(state.applicationId)}` + ); + state.application = applicationResp?.body || {}; + + const profileQuery = new URLSearchParams({ + sysOrigin: state.application?.appCode || state.userProfile?.originSys || "", + type: state.productType, + }); + const account = currentUserAccount(); + if (account) { + profileQuery.set("account", account); + } + if (currentUserId()) { + profileQuery.set("userId", currentUserId()); + } + + const profileResp = await deps.getApi( + `/order/web/pay/user-profile?${profileQuery.toString()}` + ); + const payProfile = profileResp?.body || {}; + if (payProfile.userProfile) { + state.userProfile = { + ...state.userProfile, + ...payProfile.userProfile, + }; + } + state.regionId = payProfile.regionId || ""; + state.countries = Array.isArray(payProfile.countryList) ? payProfile.countryList : []; + if (!state.countries.length) { state.selectedCountryId = ""; state.commodityCard = null; @@ -335,7 +379,7 @@ async function refreshMeta() { state.applicationId )}&payCountryId=${encodeURIComponent(currentCountryId())}&type=${encodeURIComponent( state.productType - )}` + )}®ionId=${encodeURIComponent(currentRegionId())}` ); state.commodityCard = commodityResp?.body || {};