1
794
h5/app-invite/app.js
Normal file
@ -0,0 +1,794 @@
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
|
||||
const DEFAULT_INVITE_API_BASE = "https://jvapi.haiyihy.com/";
|
||||
const DEFAULT_AVATAR = "./assets/avatar-sample.png";
|
||||
const DEFAULT_TASK_NOTES = {
|
||||
transport: "(Only the highest-level bonuses may be awarded)",
|
||||
quota: "(You'll receive a reward upon reaching each level)",
|
||||
};
|
||||
|
||||
const DEFAULTS = {
|
||||
view: "invite",
|
||||
inviteLink: "http://yourappbalbalanal",
|
||||
inviteCode: "123455678",
|
||||
invitees: "168",
|
||||
eligibleVoters: "01",
|
||||
rechargeTotal: "01",
|
||||
myRewards: "12354",
|
||||
friendName: "Name",
|
||||
downloadUrl: "https://example.com/download",
|
||||
endAt: "",
|
||||
inviterBindReward: 2000,
|
||||
rechargeReward: 1000,
|
||||
giftReward: 1000,
|
||||
};
|
||||
|
||||
const FALLBACK_TASKS = {
|
||||
transport: [
|
||||
{
|
||||
title: "Milestone 1",
|
||||
desc: "Total invitee recharge reaches 49,999 coins",
|
||||
buttonLabel: "Incomplete",
|
||||
disabled: true,
|
||||
claimed: false,
|
||||
achieved: false,
|
||||
ruleId: 0,
|
||||
},
|
||||
{
|
||||
title: "Milestone 2",
|
||||
desc: "Total invitee recharge reaches 99,999 coins",
|
||||
buttonLabel: "Incomplete",
|
||||
disabled: true,
|
||||
claimed: false,
|
||||
achieved: false,
|
||||
ruleId: 0,
|
||||
},
|
||||
{
|
||||
title: "Milestone 3",
|
||||
desc: "Total invitee recharge reaches 149,999 coins",
|
||||
buttonLabel: "Incomplete",
|
||||
disabled: true,
|
||||
claimed: false,
|
||||
achieved: false,
|
||||
ruleId: 0,
|
||||
},
|
||||
],
|
||||
quota: [
|
||||
{
|
||||
title: "Milestone 1",
|
||||
desc: "Invite 10 friends who have already made a payment",
|
||||
buttonLabel: "Claim",
|
||||
disabled: false,
|
||||
claimed: false,
|
||||
achieved: true,
|
||||
ruleId: 0,
|
||||
},
|
||||
{
|
||||
title: "Milestone 2",
|
||||
desc: "Invite 20 friends who have already made a payment",
|
||||
buttonLabel: "Incomplete",
|
||||
disabled: true,
|
||||
claimed: false,
|
||||
achieved: false,
|
||||
ruleId: 0,
|
||||
},
|
||||
{
|
||||
title: "Milestone 3",
|
||||
desc: "Invite 30 friends who have already made a payment",
|
||||
buttonLabel: "Claim",
|
||||
disabled: false,
|
||||
claimed: false,
|
||||
achieved: true,
|
||||
ruleId: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_RANKS = Array.from({ length: 30 }, (_, index) => ({
|
||||
rank: index + 1,
|
||||
name:
|
||||
[
|
||||
"hanneem",
|
||||
"layan",
|
||||
"amira",
|
||||
"sami",
|
||||
"noura",
|
||||
"huda",
|
||||
"rami",
|
||||
"selim",
|
||||
"mira",
|
||||
"yael",
|
||||
][index % 10],
|
||||
score: `${123 - Math.floor(index / 3)} people`,
|
||||
}));
|
||||
|
||||
const state = {
|
||||
view: normalizeView(query.get("view") || DEFAULTS.view),
|
||||
apiBase: resolveAPIBase(),
|
||||
authorization: resolveAuthorization(),
|
||||
landingCode: trimValue(query.get("code") || query.get("inviteCode")),
|
||||
inviteLink: query.get("inviteLink") || DEFAULTS.inviteLink,
|
||||
inviteCode: query.get("inviteCode") || DEFAULTS.inviteCode,
|
||||
invitees: query.get("invitees") || DEFAULTS.invitees,
|
||||
eligibleVoters: query.get("eligible") || DEFAULTS.eligibleVoters,
|
||||
rechargeTotal: query.get("recharge") || DEFAULTS.rechargeTotal,
|
||||
myRewards: query.get("rewards") || DEFAULTS.myRewards,
|
||||
friendName: query.get("friendName") || DEFAULTS.friendName,
|
||||
inviterAvatar: DEFAULT_AVATAR,
|
||||
downloadUrl: query.get("downloadUrl") || DEFAULTS.downloadUrl,
|
||||
inviterBindReward: query.get("inviterReward") || DEFAULTS.inviterBindReward,
|
||||
rechargeReward: query.get("rechargeReward") || DEFAULTS.rechargeReward,
|
||||
giftReward: query.get("giftReward") || DEFAULTS.giftReward,
|
||||
taskSet: normalizeTaskSet(query.get("taskSet") || "quota"),
|
||||
tasks: {
|
||||
transport: cloneTasks(FALLBACK_TASKS.transport),
|
||||
quota: cloneTasks(FALLBACK_TASKS.quota),
|
||||
},
|
||||
countdownTimer: null,
|
||||
pendingClaimRuleId: 0,
|
||||
};
|
||||
|
||||
const els = {
|
||||
views: [...document.querySelectorAll("[data-view]")],
|
||||
inviteLinkText: document.getElementById("inviteLinkText"),
|
||||
inviteCodeText: document.getElementById("inviteCodeText"),
|
||||
statInvitees: document.getElementById("statInvitees"),
|
||||
statEligible: document.getElementById("statEligible"),
|
||||
statRecharge: document.getElementById("statRecharge"),
|
||||
statRewards: document.getElementById("statRewards"),
|
||||
taskList: document.getElementById("taskList"),
|
||||
taskNote: document.getElementById("taskNote"),
|
||||
taskTabs: [...document.querySelectorAll("[data-task-set]")],
|
||||
navButtons: [...document.querySelectorAll("[data-nav-view]")],
|
||||
copyButtons: [...document.querySelectorAll("[data-copy-target]")],
|
||||
rankList: document.getElementById("rankList"),
|
||||
giftTitle: document.getElementById("giftTitle"),
|
||||
giftAvatarImage: document.getElementById("giftAvatarImage"),
|
||||
downloadButton: document.getElementById("downloadButton"),
|
||||
inviteRewardAmounts: [
|
||||
...document.querySelectorAll('.reward-panel .reward-amount'),
|
||||
],
|
||||
giftRewardAmount: document.querySelector('.gift-panel .reward-amount'),
|
||||
toast: document.getElementById("pageToast"),
|
||||
countdown: {
|
||||
days: document.querySelector('[data-countdown="days"]'),
|
||||
hours: document.querySelector('[data-countdown="hours"]'),
|
||||
minutes: document.querySelector('[data-countdown="minutes"]'),
|
||||
seconds: document.querySelector('[data-countdown="seconds"]'),
|
||||
},
|
||||
};
|
||||
|
||||
function normalizeView(value) {
|
||||
if (value === "rank" || value === "gift") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return "invite";
|
||||
}
|
||||
|
||||
function normalizeTaskSet(value) {
|
||||
return value === "transport" ? "transport" : "quota";
|
||||
}
|
||||
|
||||
function trimValue(value) {
|
||||
return value == null ? "" : String(value).trim();
|
||||
}
|
||||
|
||||
function normalizeBaseURL(base) {
|
||||
const value = trimValue(base);
|
||||
if (!value) {
|
||||
return DEFAULT_INVITE_API_BASE;
|
||||
}
|
||||
|
||||
return value.endsWith("/") ? value : `${value}/`;
|
||||
}
|
||||
|
||||
function resolveAPIBase() {
|
||||
const queryBase = trimValue(query.get("apiBase"));
|
||||
if (queryBase) {
|
||||
return normalizeBaseURL(queryBase);
|
||||
}
|
||||
|
||||
if (typeof window.__APP_INVITE_API_BASE__ === "string") {
|
||||
return normalizeBaseURL(window.__APP_INVITE_API_BASE__);
|
||||
}
|
||||
|
||||
return normalizeBaseURL(DEFAULT_INVITE_API_BASE);
|
||||
}
|
||||
|
||||
function resolveAuthorization() {
|
||||
const candidates = [
|
||||
query.get("authorization"),
|
||||
query.get("Authorization"),
|
||||
query.get("token"),
|
||||
query.get("auth"),
|
||||
typeof window.__APP_INVITE_AUTHORIZATION__ === "string"
|
||||
? window.__APP_INVITE_AUTHORIZATION__
|
||||
: "",
|
||||
typeof window.__APP_INVITE_TOKEN__ === "string" ? window.__APP_INVITE_TOKEN__ : "",
|
||||
];
|
||||
|
||||
const value = candidates.find((item) => trimValue(item));
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const normalized = trimValue(value);
|
||||
if (/^bearer\s+/i.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return `Bearer ${normalized}`;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, (match) => {
|
||||
const entityMap = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
return entityMap[match] || match;
|
||||
});
|
||||
}
|
||||
|
||||
function cloneTasks(list) {
|
||||
return list.map((item) => ({ ...item }));
|
||||
}
|
||||
|
||||
function showToast(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
els.toast.textContent = text;
|
||||
els.toast.classList.add("show");
|
||||
|
||||
window.clearTimeout(showToast.timer);
|
||||
showToast.timer = window.setTimeout(() => {
|
||||
els.toast.classList.remove("show");
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
function loadImage(image) {
|
||||
if (!image || image.getAttribute("src")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const src = image.getAttribute("data-src");
|
||||
if (src) {
|
||||
image.setAttribute("src", src);
|
||||
}
|
||||
}
|
||||
|
||||
function setImageSource(image, src, fallbackSrc = "") {
|
||||
if (!(image instanceof HTMLImageElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSrc = trimValue(src) || fallbackSrc;
|
||||
if (!nextSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
image.setAttribute("src", nextSrc);
|
||||
image.setAttribute("data-src", nextSrc);
|
||||
}
|
||||
|
||||
function hydrateViewAssets(view) {
|
||||
const targets = {
|
||||
invite: [
|
||||
document.getElementById("inviteHeroImage"),
|
||||
...document.querySelectorAll('.view[data-view="invite"] .lazy-image'),
|
||||
],
|
||||
rank: [
|
||||
...document.querySelectorAll('.view[data-view="rank"] [data-badge-src]'),
|
||||
...document.querySelectorAll('.view[data-view="rank"] [data-avatar-src]'),
|
||||
],
|
||||
gift: [
|
||||
document.getElementById("giftBgImage"),
|
||||
document.getElementById("giftAvatarImage"),
|
||||
document.getElementById("phoneHomeImage"),
|
||||
document.getElementById("phoneGiftImage"),
|
||||
...document.querySelectorAll('.view[data-view="gift"] .lazy-image'),
|
||||
],
|
||||
};
|
||||
|
||||
(targets[view] || []).forEach((item) => {
|
||||
if (item instanceof HTMLImageElement) {
|
||||
loadImage(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
state.view = normalizeView(view);
|
||||
els.views.forEach((section) => {
|
||||
section.classList.toggle("active", section.getAttribute("data-view") === state.view);
|
||||
});
|
||||
|
||||
hydrateViewAssets(state.view);
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
if (state.view === DEFAULTS.view) {
|
||||
url.searchParams.delete("view");
|
||||
} else {
|
||||
url.searchParams.set("view", state.view);
|
||||
}
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
|
||||
function formatMetric(value) {
|
||||
const raw = trimValue(value);
|
||||
if (!raw) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
||||
return new Intl.NumberFormat("en-US").format(Number(raw));
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
function formatSignedReward(value) {
|
||||
const raw = trimValue(value);
|
||||
if (!raw) {
|
||||
return "+0";
|
||||
}
|
||||
|
||||
if (raw.startsWith("+")) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
return `+${formatMetric(raw)}`;
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
els.inviteLinkText.textContent = trimValue(state.inviteLink) || "--";
|
||||
els.inviteCodeText.textContent = trimValue(state.inviteCode) || "--";
|
||||
els.statInvitees.textContent = formatMetric(state.invitees);
|
||||
els.statEligible.textContent = formatMetric(state.eligibleVoters);
|
||||
els.statRecharge.textContent = formatMetric(state.rechargeTotal);
|
||||
els.statRewards.textContent = formatMetric(state.myRewards);
|
||||
els.giftTitle.innerHTML = `Your friend [${escapeHtml(state.friendName)}]<br />has invited you to join`;
|
||||
setImageSource(els.giftAvatarImage, state.inviterAvatar, DEFAULT_AVATAR);
|
||||
|
||||
if (els.inviteRewardAmounts[0]) {
|
||||
els.inviteRewardAmounts[0].textContent = formatSignedReward(state.inviterBindReward);
|
||||
}
|
||||
if (els.inviteRewardAmounts[1]) {
|
||||
els.inviteRewardAmounts[1].textContent = formatSignedReward(state.rechargeReward);
|
||||
}
|
||||
if (els.giftRewardAmount) {
|
||||
els.giftRewardAmount.textContent = formatSignedReward(state.giftReward);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTasks() {
|
||||
const current = state.tasks[state.taskSet] || cloneTasks(FALLBACK_TASKS[state.taskSet]);
|
||||
els.taskTabs.forEach((tab) => {
|
||||
const active = tab.getAttribute("data-task-set") === state.taskSet;
|
||||
tab.classList.toggle("active", active);
|
||||
tab.classList.toggle("passive", !active);
|
||||
});
|
||||
|
||||
els.taskList.innerHTML = current
|
||||
.map((item) => {
|
||||
const isLoading = Number(item.ruleId) > 0 && state.pendingClaimRuleId === Number(item.ruleId);
|
||||
const disabled = item.disabled || isLoading || Number(item.ruleId) <= 0;
|
||||
const buttonLabel = isLoading ? "Loading..." : item.buttonLabel;
|
||||
let buttonStateClass = " is-pending";
|
||||
|
||||
if (isLoading) {
|
||||
buttonStateClass = " is-loading";
|
||||
} else if (item.claimed) {
|
||||
buttonStateClass = " is-claimed";
|
||||
} else if (item.achieved && !item.claimed) {
|
||||
buttonStateClass = " is-claimable";
|
||||
}
|
||||
|
||||
return `
|
||||
<article class="task-card">
|
||||
<div>
|
||||
<p class="task-title">${escapeHtml(item.title)}</p>
|
||||
<p class="task-desc">${escapeHtml(item.desc)}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="claim-button${buttonStateClass}${disabled ? " disabled" : ""}"
|
||||
data-rule-id="${Number(item.ruleId) || 0}"
|
||||
${disabled ? "disabled" : ""}
|
||||
>
|
||||
${escapeHtml(buttonLabel)}
|
||||
</button>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
els.taskNote.textContent = DEFAULT_TASK_NOTES[state.taskSet] || "";
|
||||
}
|
||||
|
||||
function renderRankList() {
|
||||
els.rankList.innerHTML = DEFAULT_RANKS.map((item) => {
|
||||
const badgeMap = {
|
||||
1: "./assets/rank-top1.png",
|
||||
2: "./assets/rank-top2.png",
|
||||
3: "./assets/rank-top3.png",
|
||||
};
|
||||
|
||||
const badge = badgeMap[item.rank];
|
||||
return `
|
||||
<article
|
||||
class="rank-row"
|
||||
style="background-image:url('./assets/rank-row.svg')"
|
||||
>
|
||||
<div class="rank-badge">
|
||||
${
|
||||
badge
|
||||
? `<img src="${badge}" alt="Rank ${item.rank}" />`
|
||||
: escapeHtml(String(item.rank))
|
||||
}
|
||||
</div>
|
||||
<div class="avatar">
|
||||
<img src="./assets/avatar-sample.png" alt="${escapeHtml(item.name)}" />
|
||||
</div>
|
||||
<div class="rank-name">${escapeHtml(item.name)}</div>
|
||||
<div class="rank-score">${escapeHtml(item.score)}</div>
|
||||
</article>
|
||||
`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function parseEndAt() {
|
||||
const raw = query.get("endAt") || DEFAULTS.endAt;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamp = Date.parse(raw);
|
||||
if (Number.isNaN(timestamp)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
function pad(number) {
|
||||
return String(number).padStart(2, "0");
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
const endAt = parseEndAt();
|
||||
if (!endAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = Math.max(0, endAt - Date.now());
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
els.countdown.days.textContent = pad(days);
|
||||
els.countdown.hours.textContent = pad(hours);
|
||||
els.countdown.minutes.textContent = pad(minutes);
|
||||
els.countdown.seconds.textContent = pad(secs);
|
||||
}
|
||||
|
||||
function setupCountdown() {
|
||||
const endAt = parseEndAt();
|
||||
if (!endAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
state.countdownTimer = window.setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const input = document.createElement("textarea");
|
||||
input.value = text;
|
||||
input.setAttribute("readonly", "");
|
||||
input.style.position = "absolute";
|
||||
input.style.left = "-9999px";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
|
||||
function extractErrorMessage(payload) {
|
||||
const candidates = [
|
||||
payload?.errorMsg,
|
||||
payload?.message,
|
||||
payload?.errorCodeName,
|
||||
payload?.code,
|
||||
];
|
||||
|
||||
return candidates.find((item) => trimValue(item)) || "";
|
||||
}
|
||||
|
||||
async function requestJSON(path, options = {}) {
|
||||
const { method = "GET", headers = {}, body, searchParams } = options;
|
||||
const url = new URL(path, state.apiBase);
|
||||
|
||||
Object.entries(searchParams || {}).forEach(([key, value]) => {
|
||||
const raw = trimValue(value);
|
||||
if (raw) {
|
||||
url.searchParams.set(key, raw);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...headers,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = contentType.includes("application/json") ? await response.json() : null;
|
||||
|
||||
if (!response.ok || payload?.status === false) {
|
||||
const message = extractErrorMessage(payload) || `Request failed (${response.status})`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return payload?.body || payload?.data || payload || null;
|
||||
}
|
||||
|
||||
function normalizeTaskList(list, taskSet) {
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
return cloneTasks(FALLBACK_TASKS[taskSet]);
|
||||
}
|
||||
|
||||
return list.map((item, index) => {
|
||||
const threshold = Number(item?.threshold || 0);
|
||||
const goldAmount = Number(item?.goldAmount || 0);
|
||||
const achieved = Boolean(item?.achieved);
|
||||
const claimed = Boolean(item?.claimed);
|
||||
const desc =
|
||||
taskSet === "quota"
|
||||
? `Invite ${formatMetric(threshold)} valid friends to claim ${formatMetric(goldAmount)} coins`
|
||||
: `Reach ${formatMetric(threshold)} recharge coins to claim ${formatMetric(goldAmount)} coins`;
|
||||
|
||||
return {
|
||||
title: `Milestone ${index + 1}`,
|
||||
desc,
|
||||
buttonLabel: claimed ? "Claimed" : achieved ? "Claim" : "Incomplete",
|
||||
disabled: claimed || !achieved,
|
||||
claimed,
|
||||
achieved,
|
||||
ruleId: Number(item?.ruleId) || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function markTaskClaimed(ruleId) {
|
||||
["quota", "transport"].forEach((taskSet) => {
|
||||
state.tasks[taskSet] = (state.tasks[taskSet] || []).map((item) => {
|
||||
if (Number(item.ruleId) !== Number(ruleId)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
claimed: true,
|
||||
disabled: true,
|
||||
buttonLabel: "Claimed",
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyHomeData(body) {
|
||||
if (!body || typeof body !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = body.stats || {};
|
||||
state.inviteLink = trimValue(body.inviteLink) || state.inviteLink;
|
||||
state.inviteCode = trimValue(body.inviteCode) || state.inviteCode;
|
||||
state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl;
|
||||
state.invitees = stats.totalInvitedUsers ?? state.invitees;
|
||||
state.eligibleVoters = stats.totalValidUsers ?? state.eligibleVoters;
|
||||
state.rechargeTotal = stats.totalRechargeCoins ?? state.rechargeTotal;
|
||||
state.myRewards = stats.rewardGoldCoins ?? state.myRewards;
|
||||
|
||||
const inviterBindReward = body.inviterBindReward?.goldAmount;
|
||||
if (inviterBindReward != null) {
|
||||
state.inviterBindReward = inviterBindReward;
|
||||
}
|
||||
|
||||
const rechargeReward =
|
||||
body.inviterTotalRechargeTasks?.[0]?.goldAmount ??
|
||||
body.inviteeRechargeRules?.[0]?.goldAmount;
|
||||
if (rechargeReward != null) {
|
||||
state.rechargeReward = rechargeReward;
|
||||
}
|
||||
|
||||
state.tasks.quota = normalizeTaskList(body.inviterValidCountTasks, "quota");
|
||||
state.tasks.transport = normalizeTaskList(body.inviterTotalRechargeTasks, "transport");
|
||||
|
||||
const shareTitle = trimValue(body.shareTitle);
|
||||
if (shareTitle) {
|
||||
document.title = shareTitle;
|
||||
}
|
||||
}
|
||||
|
||||
function applyLandingData(body) {
|
||||
if (!body || typeof body !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
state.friendName = trimValue(body.inviterNickname) || state.friendName;
|
||||
state.inviterAvatar = trimValue(body.inviterAvatar) || DEFAULT_AVATAR;
|
||||
state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl;
|
||||
state.giftReward = body.inviteeReward?.goldAmount ?? state.giftReward;
|
||||
|
||||
const inviteCode = trimValue(body.inviteCode);
|
||||
if (inviteCode) {
|
||||
state.landingCode = inviteCode;
|
||||
}
|
||||
|
||||
const shareTitle = trimValue(body.shareTitle);
|
||||
if (shareTitle) {
|
||||
document.title = shareTitle;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHomeData() {
|
||||
if (!state.authorization) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = await requestJSON("/go/app/h5/invite-campaign/home", {
|
||||
headers: {
|
||||
Authorization: state.authorization,
|
||||
},
|
||||
});
|
||||
applyHomeData(body);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function fetchLandingData() {
|
||||
if (!state.landingCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = await requestJSON("/go/public/h5/invite-campaign/landing", {
|
||||
searchParams: {
|
||||
code: state.landingCode,
|
||||
},
|
||||
});
|
||||
applyLandingData(body);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function refreshPageData() {
|
||||
try {
|
||||
if (state.view === "gift") {
|
||||
await fetchLandingData();
|
||||
} else {
|
||||
await fetchHomeData();
|
||||
}
|
||||
} catch (error) {
|
||||
const message = trimValue(error?.message);
|
||||
if (message) {
|
||||
showToast(message);
|
||||
}
|
||||
} finally {
|
||||
renderStats();
|
||||
renderTasks();
|
||||
}
|
||||
}
|
||||
|
||||
async function claimTask(ruleId) {
|
||||
if (!ruleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.authorization) {
|
||||
showToast("Token is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
state.pendingClaimRuleId = ruleId;
|
||||
renderTasks();
|
||||
|
||||
try {
|
||||
const body = await requestJSON("/go/app/h5/invite-campaign/tasks/claim", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: state.authorization,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ ruleId }),
|
||||
});
|
||||
|
||||
markTaskClaimed(ruleId);
|
||||
showToast(body?.alreadyClaimed ? "Already claimed" : "Claim succeeded");
|
||||
await fetchHomeData().catch(() => {});
|
||||
} catch (error) {
|
||||
showToast(trimValue(error?.message) || "Claim failed");
|
||||
} finally {
|
||||
state.pendingClaimRuleId = 0;
|
||||
renderStats();
|
||||
renderTasks();
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
els.taskTabs.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
state.taskSet = normalizeTaskSet(button.getAttribute("data-task-set"));
|
||||
renderTasks();
|
||||
});
|
||||
});
|
||||
|
||||
els.navButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
setView(button.getAttribute("data-nav-view") || "invite");
|
||||
});
|
||||
});
|
||||
|
||||
els.copyButtons.forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const target = button.getAttribute("data-copy-target");
|
||||
const value = target === "invite-code" ? state.inviteCode : state.inviteLink;
|
||||
|
||||
try {
|
||||
await copyText(trimValue(value));
|
||||
showToast(target === "invite-code" ? "Invitation code copied" : "Invitation link copied");
|
||||
} catch (error) {
|
||||
showToast("Copy failed");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
els.taskList.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-rule-id]");
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ruleId = Number(button.getAttribute("data-rule-id") || 0);
|
||||
if (!ruleId || button.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
claimTask(ruleId);
|
||||
});
|
||||
|
||||
els.downloadButton.addEventListener("click", () => {
|
||||
if (!state.downloadUrl) {
|
||||
showToast("Download link is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = state.downloadUrl;
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
renderStats();
|
||||
renderTasks();
|
||||
renderRankList();
|
||||
bindEvents();
|
||||
setView(state.view);
|
||||
setupCountdown();
|
||||
await refreshPageData();
|
||||
}
|
||||
|
||||
init();
|
||||
BIN
h5/app-invite/assets/avatar-sample.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
h5/app-invite/assets/button-active-bg.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
h5/app-invite/assets/button-inactive-bg.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
h5/app-invite/assets/claim-active-art.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
h5/app-invite/assets/claim-active-core.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
3
h5/app-invite/assets/claim-active-mask.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 204.5 52.25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector 1" d="M10.501 52.25H194.515L194.425 48.5L204.5 42L203.75 9.25L193.432 7L193.264 0L10.501 0.75V5.75L0 9.25V42.25L10.501 47.75V52.25Z" fill="var(--fill-0, #0B3008)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 365 B |
BIN
h5/app-invite/assets/claim-active.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
h5/app-invite/assets/claim-inactive.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
h5/app-invite/assets/copy-button.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
h5/app-invite/assets/copy-core.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
21
h5/app-invite/assets/copy-mask.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 760.5 125.5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector 1" filter="url(#filter0_i_0_6)">
|
||||
<path d="M738.498 16V0H22.0683L18.0013 15.1769L0 20V103.873L21.0015 109.5V125.5H740.998V105H760.5V16H738.498Z" fill="url(#paint0_linear_0_6)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_0_6" x="0" y="0" width="760.5" height="125.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="6.9"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.84 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_6"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_0_6" x1="380.499" y1="104.5" x2="380.499" y2="0.500003" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#206C1A"/>
|
||||
<stop offset="1" stop-color="#3CA733"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
h5/app-invite/assets/gift-bg.png
Normal file
|
After Width: | Height: | Size: 8.2 MiB |
BIN
h5/app-invite/assets/gift-cta-core.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
21
h5/app-invite/assets/gift-cta-mask.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 1002.21 165.552" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector 1" filter="url(#filter0_i_0_7)">
|
||||
<path d="M973.215 21.1063V0H29.0822L23.7227 20.0205L0 26.3828V137.023L27.6765 144.446V165.552H976.509V138.51H1002.21V21.1063H973.215Z" fill="url(#paint0_linear_0_7)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_0_7" x="0" y="0" width="1002.21" height="165.552" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="6.9"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.84 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_7"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_0_7" x1="501.434" y1="137.85" x2="501.434" y2="0.659574" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#206C1A"/>
|
||||
<stop offset="1" stop-color="#3CA733"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
h5/app-invite/assets/gift-cta.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
h5/app-invite/assets/hero-title-combined.png
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
h5/app-invite/assets/hero-title-full.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
h5/app-invite/assets/hero-title-top-shift4.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
h5/app-invite/assets/hero-title-top.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
h5/app-invite/assets/invite-hero-bg.png
Normal file
|
After Width: | Height: | Size: 8.2 MiB |
BIN
h5/app-invite/assets/monthly-tasks-button.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
h5/app-invite/assets/panel-bottom.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
h5/app-invite/assets/panel-middle.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
h5/app-invite/assets/panel-top.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
h5/app-invite/assets/phone-gift.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
h5/app-invite/assets/phone-home.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
h5/app-invite/assets/rank-button.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
3
h5/app-invite/assets/rank-row.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 880 149" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rectangle 52" d="M0 19C0 8.50659 8.50659 0 19 0H861C871.493 0 880 8.50659 880 19V130C880 140.493 871.493 149 861 149H19C8.50657 149 0 140.493 0 130V19Z" fill="var(--fill-0, #042207)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 374 B |
BIN
h5/app-invite/assets/rank-title-button.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
h5/app-invite/assets/rank-top1.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
h5/app-invite/assets/rank-top2.png
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
h5/app-invite/assets/rank-top3.png
Normal file
|
After Width: | Height: | Size: 344 KiB |
BIN
h5/app-invite/assets/reward-bag.png
Normal file
|
After Width: | Height: | Size: 836 KiB |
BIN
h5/app-invite/assets/tab-active-core.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
3
h5/app-invite/assets/tab-active-mask.png
Normal file
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 409 104.5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector 1" d="M21.002 104.5H389.029L388.85 97L409 84L407.5 18.5L386.864 14L386.529 0L21.002 1.5V11.5L0 18.5V84.5L21.002 95.5V104.5Z" fill="var(--fill-0, #0B3008)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
3
h5/app-invite/assets/tab-inactive-core.png
Normal file
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 409 104.5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector 1" d="M21.002 104.5H389.029L388.85 97L409 84L407.5 18.5L386.864 14L386.529 0L21.002 1.5V11.5L0 18.5V84.5L21.002 95.5V104.5Z" fill="var(--fill-0, #0B3008)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
BIN
h5/app-invite/assets/tab-quota-active.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
h5/app-invite/assets/tab-quota-inactive.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
h5/app-invite/assets/tab-transport-active.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
h5/app-invite/assets/tab-transport-inactive.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
3
h5/app-invite/assets/task-row-bg.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 880 169" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rectangle 52" d="M0 19C0 8.50659 8.50659 0 19 0H861C871.493 0 880 8.50659 880 19V150C880 160.493 871.493 169 861 169H19C8.50657 169 0 160.493 0 150V19Z" fill="var(--fill-0, #042207)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 374 B |
BIN
h5/app-invite/assets/title-image.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
h5/app-invite/assets/title-top-core.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
3
h5/app-invite/assets/title-top-mask.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 975 143" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect id="Rectangle 61" width="975" height="143" fill="var(--fill-0, #D9D9D9)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
BIN
h5/app-invite/assets/任务标题背景按钮.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
h5/app-invite/assets/按钮未选中.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
h5/app-invite/assets/按钮选中.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
96
h5/app-invite/dev-test-server.mjs
Normal file
@ -0,0 +1,96 @@
|
||||
import { createServer } from "node:http";
|
||||
import { createReadStream, statSync } from "node:fs";
|
||||
import { extname, join, normalize } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const rootDir = fileURLToPath(new URL(".", import.meta.url));
|
||||
const port = Number(process.env.INVITE_TEST_PORT || 65248);
|
||||
const apiTarget = String(process.env.INVITE_API_TARGET || "http://127.0.0.1:2900").replace(/\/$/, "");
|
||||
|
||||
const mimeTypes = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
function sendText(response, status, text) {
|
||||
response.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
||||
response.end(text);
|
||||
}
|
||||
|
||||
function serveStatic(request, response) {
|
||||
const requestPath = new URL(request.url, "http://localhost").pathname;
|
||||
const pathname = requestPath === "/" ? "/dev-test.html" : requestPath;
|
||||
const filePath = normalize(join(rootDir, pathname));
|
||||
if (!filePath.startsWith(rootDir)) {
|
||||
sendText(response, 403, "Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = statSync(filePath);
|
||||
if (!stat.isFile()) {
|
||||
sendText(response, 404, "Not found");
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, {
|
||||
"content-type": mimeTypes[extname(filePath)] || "application/octet-stream",
|
||||
"content-length": stat.size,
|
||||
});
|
||||
createReadStream(filePath).pipe(response);
|
||||
} catch {
|
||||
sendText(response, 404, "Not found");
|
||||
}
|
||||
}
|
||||
|
||||
async function proxyAPI(request, response) {
|
||||
const incomingURL = new URL(request.url, "http://localhost");
|
||||
const targetURL = new URL(incomingURL.pathname.replace(/^\/api/, "") + incomingURL.search, apiTarget);
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(request.headers)) {
|
||||
if (value !== undefined && key.toLowerCase() !== "host") {
|
||||
headers.set(key, Array.isArray(value) ? value.join(",") : value);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const upstream = await fetch(targetURL, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: request.method === "GET" || request.method === "HEAD" ? undefined : request,
|
||||
duplex: "half",
|
||||
});
|
||||
response.writeHead(upstream.status, {
|
||||
"content-type": upstream.headers.get("content-type") || "application/octet-stream",
|
||||
});
|
||||
if (upstream.body) {
|
||||
for await (const chunk of upstream.body) {
|
||||
response.write(chunk);
|
||||
}
|
||||
}
|
||||
response.end();
|
||||
} catch (error) {
|
||||
response.writeHead(502, { "content-type": "application/json; charset=utf-8" });
|
||||
response.end(JSON.stringify({
|
||||
status: false,
|
||||
code: "proxy_error",
|
||||
message: error instanceof Error ? error.message : "proxy error",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
createServer((request, response) => {
|
||||
if (request.url?.startsWith("/api/") || request.url === "/api") {
|
||||
proxyAPI(request, response);
|
||||
return;
|
||||
}
|
||||
serveStatic(request, response);
|
||||
}).listen(port, "127.0.0.1", () => {
|
||||
console.log(`Invite dev test page: http://127.0.0.1:${port}/dev-test.html`);
|
||||
console.log(`Proxy target: ${apiTarget}`);
|
||||
});
|
||||
799
h5/app-invite/dev-test.html
Normal file
@ -0,0 +1,799 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>邀请活动本地接口测试</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f5f7f4;
|
||||
--panel: #ffffff;
|
||||
--line: #d8dfd2;
|
||||
--text: #1f2b1d;
|
||||
--muted: #64705f;
|
||||
--brand: #176d36;
|
||||
--brand-soft: #e7f4ea;
|
||||
--danger: #a62b21;
|
||||
--danger-soft: #fff0ee;
|
||||
--code: #102414;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family:
|
||||
ui-sans-serif, -apple-system, BlinkMacSystemFont, "PingFang SC",
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 76px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
background: var(--code);
|
||||
color: #dff5dd;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status {
|
||||
min-width: 210px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 360px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: #fbfcfa;
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.task-list,
|
||||
.rank-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task,
|
||||
.rank {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: #fbfcfa;
|
||||
}
|
||||
|
||||
.task-head,
|
||||
.rank {
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-head {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-meta,
|
||||
.small {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
object-fit: cover;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.rank-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logs {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout,
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<h1 class="title">邀请活动本地接口测试</h1>
|
||||
<p class="subtitle">
|
||||
默认通过本页同源的 <code>/api</code> 代理到本地 Go 服务,避免浏览器 CORS 限制。
|
||||
</p>
|
||||
</div>
|
||||
<div class="status" id="statusText">等待操作</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="panel">
|
||||
<h2>测试参数</h2>
|
||||
<div class="grid">
|
||||
<label>
|
||||
API Base
|
||||
<input id="apiBaseInput" value="/api" />
|
||||
</label>
|
||||
<label>
|
||||
邀请人 token
|
||||
<textarea id="inviterTokenInput" placeholder="用于 /app/h5/invite-campaign/home、rank、claim"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
被邀请人 token
|
||||
<textarea id="inviteeTokenInput" placeholder="用于 bind-code"></textarea>
|
||||
</label>
|
||||
<div class="row">
|
||||
<label>
|
||||
邀请码
|
||||
<input id="inviteCodeInput" placeholder="查首页后自动填入" />
|
||||
</label>
|
||||
<label>
|
||||
排行 limit
|
||||
<input id="rankLimitInput" type="number" value="50" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>
|
||||
被邀请人 userId
|
||||
<input id="inviteeUserIdInput" type="number" placeholder="充值回调用" />
|
||||
</label>
|
||||
<label>
|
||||
sysOrigin
|
||||
<input id="sysOriginInput" placeholder="查首页后自动填入" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>
|
||||
充值金币
|
||||
<input id="rechargeCoinsInput" type="number" value="100000" />
|
||||
</label>
|
||||
<label>
|
||||
订单号
|
||||
<input id="orderIdInput" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" id="saveButton" class="secondary">保存参数</button>
|
||||
<button type="button" id="resetButton" class="danger">清空日志</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="grid">
|
||||
<div class="panel">
|
||||
<h2>流程操作</h2>
|
||||
<div class="actions">
|
||||
<button type="button" data-action="health">1. 健康检查</button>
|
||||
<button type="button" data-action="home">2. 邀请人首页</button>
|
||||
<button type="button" data-action="rank">3. 排行榜</button>
|
||||
<button type="button" data-action="landing">4. 落地页</button>
|
||||
<button type="button" data-action="bind">5. 被邀请人绑码</button>
|
||||
<button type="button" data-action="recharge">6. 模拟充值回调</button>
|
||||
<button type="button" data-action="refresh">7. 刷新首页+榜单</button>
|
||||
</div>
|
||||
<p class="small">
|
||||
推荐顺序:填邀请人 token -> 查首页 -> 填被邀请人 token 和 userId -> 绑码 -> 充值回调 -> 刷新 -> 点任务 Claim。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>首页结果</h2>
|
||||
<div class="metric-grid" id="metrics"></div>
|
||||
<div class="grid" style="margin-top: 14px">
|
||||
<label>
|
||||
邀请链接
|
||||
<input id="inviteLinkOutput" readonly />
|
||||
</label>
|
||||
<div class="small" id="countdownText">月度倒计时未加载</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>任务领取</h2>
|
||||
<div class="task-list" id="taskList"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>排行榜</h2>
|
||||
<div class="rank-list" id="rankList"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>接口日志</h2>
|
||||
<div class="logs" id="logs"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const storageKey = "appInviteDevTest";
|
||||
const state = {
|
||||
home: null,
|
||||
rank: null,
|
||||
landing: null,
|
||||
countdownTimer: 0,
|
||||
};
|
||||
|
||||
const els = {
|
||||
statusText: document.getElementById("statusText"),
|
||||
apiBaseInput: document.getElementById("apiBaseInput"),
|
||||
inviterTokenInput: document.getElementById("inviterTokenInput"),
|
||||
inviteeTokenInput: document.getElementById("inviteeTokenInput"),
|
||||
inviteCodeInput: document.getElementById("inviteCodeInput"),
|
||||
rankLimitInput: document.getElementById("rankLimitInput"),
|
||||
inviteeUserIdInput: document.getElementById("inviteeUserIdInput"),
|
||||
sysOriginInput: document.getElementById("sysOriginInput"),
|
||||
rechargeCoinsInput: document.getElementById("rechargeCoinsInput"),
|
||||
orderIdInput: document.getElementById("orderIdInput"),
|
||||
inviteLinkOutput: document.getElementById("inviteLinkOutput"),
|
||||
countdownText: document.getElementById("countdownText"),
|
||||
metrics: document.getElementById("metrics"),
|
||||
taskList: document.getElementById("taskList"),
|
||||
rankList: document.getElementById("rankList"),
|
||||
logs: document.getElementById("logs"),
|
||||
};
|
||||
|
||||
function loadSavedParams() {
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || "{}");
|
||||
Object.entries(saved).forEach(([key, value]) => {
|
||||
if (els[key]) {
|
||||
els[key].value = value;
|
||||
}
|
||||
});
|
||||
if (!els.orderIdInput.value) {
|
||||
els.orderIdInput.value = nextOrderId();
|
||||
}
|
||||
}
|
||||
|
||||
function saveParams() {
|
||||
const payload = {};
|
||||
[
|
||||
"apiBaseInput",
|
||||
"inviterTokenInput",
|
||||
"inviteeTokenInput",
|
||||
"inviteCodeInput",
|
||||
"rankLimitInput",
|
||||
"inviteeUserIdInput",
|
||||
"sysOriginInput",
|
||||
"rechargeCoinsInput",
|
||||
].forEach((key) => {
|
||||
payload[key] = els[key].value;
|
||||
});
|
||||
localStorage.setItem(storageKey, JSON.stringify(payload));
|
||||
setStatus("参数已保存");
|
||||
}
|
||||
|
||||
function nextOrderId() {
|
||||
return "invite-test-" + Date.now();
|
||||
}
|
||||
|
||||
function normalizeBase(base) {
|
||||
const value = String(base || "").trim() || "/api";
|
||||
return value.endsWith("/") ? value.slice(0, -1) : value;
|
||||
}
|
||||
|
||||
function authHeader(token) {
|
||||
const value = String(token || "").trim();
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
return /^bearer\s+/i.test(value) ? value : "Bearer " + value;
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
els.statusText.textContent = text;
|
||||
}
|
||||
|
||||
function formatJSON(value) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function unwrap(payload) {
|
||||
return payload && (payload.body || payload.data || payload);
|
||||
}
|
||||
|
||||
function addLog(title, payload, ok = true) {
|
||||
const entry = document.createElement("div");
|
||||
entry.className = "log-entry";
|
||||
entry.innerHTML = `
|
||||
<div class="log-title">
|
||||
<strong>${escapeHtml(title)}</strong>
|
||||
<span>${ok ? "OK" : "ERROR"} · ${new Date().toLocaleTimeString()}</span>
|
||||
</div>
|
||||
<pre>${escapeHtml(formatJSON(payload))}</pre>
|
||||
`;
|
||||
els.logs.prepend(entry);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, (match) => {
|
||||
return {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
}[match];
|
||||
});
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const base = normalizeBase(els.apiBaseInput.value);
|
||||
const response = await fetch(base + path, {
|
||||
method: options.method || "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(options.headers || {}),
|
||||
},
|
||||
body: options.body,
|
||||
});
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = contentType.includes("application/json")
|
||||
? await response.json()
|
||||
: await response.text();
|
||||
if (!response.ok || payload?.status === false) {
|
||||
const message = payload?.errorMsg || payload?.message || response.statusText;
|
||||
throw Object.assign(new Error(message), { payload, status: response.status });
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function runAction(action) {
|
||||
setStatus("请求中:" + action);
|
||||
saveParams();
|
||||
try {
|
||||
if (action === "health") {
|
||||
const payload = await request("/health");
|
||||
addLog("health", payload);
|
||||
}
|
||||
if (action === "home") {
|
||||
await fetchHome();
|
||||
}
|
||||
if (action === "rank") {
|
||||
await fetchRank();
|
||||
}
|
||||
if (action === "landing") {
|
||||
await fetchLanding();
|
||||
}
|
||||
if (action === "bind") {
|
||||
await bindCode();
|
||||
}
|
||||
if (action === "recharge") {
|
||||
await rechargeSuccess();
|
||||
}
|
||||
if (action === "refresh") {
|
||||
await fetchHome();
|
||||
await fetchRank();
|
||||
}
|
||||
setStatus("完成:" + action);
|
||||
} catch (error) {
|
||||
addLog(action, error.payload || { message: error.message, status: error.status }, false);
|
||||
setStatus("失败:" + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHome() {
|
||||
const token = authHeader(els.inviterTokenInput.value);
|
||||
if (!token) {
|
||||
throw new Error("请先填写邀请人 token");
|
||||
}
|
||||
const payload = await request("/app/h5/invite-campaign/home", {
|
||||
headers: { Authorization: token },
|
||||
});
|
||||
state.home = unwrap(payload);
|
||||
addLog("home", payload);
|
||||
applyHome(state.home);
|
||||
}
|
||||
|
||||
async function fetchRank() {
|
||||
const token = authHeader(els.inviterTokenInput.value);
|
||||
if (!token) {
|
||||
throw new Error("请先填写邀请人 token");
|
||||
}
|
||||
const limit = Number(els.rankLimitInput.value || 50);
|
||||
const payload = await request("/app/h5/invite-campaign/rank?limit=" + encodeURIComponent(limit), {
|
||||
headers: { Authorization: token },
|
||||
});
|
||||
state.rank = unwrap(payload);
|
||||
addLog("rank", payload);
|
||||
renderRank(state.rank);
|
||||
}
|
||||
|
||||
async function fetchLanding() {
|
||||
const code = els.inviteCodeInput.value.trim();
|
||||
if (!code) {
|
||||
throw new Error("请先填写邀请码,或先查邀请人首页自动填入");
|
||||
}
|
||||
const payload = await request("/public/h5/invite-campaign/landing?code=" + encodeURIComponent(code));
|
||||
state.landing = unwrap(payload);
|
||||
addLog("landing", payload);
|
||||
}
|
||||
|
||||
async function bindCode() {
|
||||
const token = authHeader(els.inviteeTokenInput.value);
|
||||
const code = els.inviteCodeInput.value.trim();
|
||||
if (!token) {
|
||||
throw new Error("请先填写被邀请人 token");
|
||||
}
|
||||
if (!code) {
|
||||
throw new Error("请先填写邀请码");
|
||||
}
|
||||
const payload = await request("/app/h5/invite-campaign/bind-code", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ inviteCode: code }),
|
||||
});
|
||||
addLog("bind-code", payload);
|
||||
}
|
||||
|
||||
async function rechargeSuccess() {
|
||||
const sysOrigin = els.sysOriginInput.value.trim();
|
||||
const userId = Number(els.inviteeUserIdInput.value || 0);
|
||||
const rechargeCoins = Number(els.rechargeCoinsInput.value || 0);
|
||||
const orderId = els.orderIdInput.value.trim() || nextOrderId();
|
||||
if (!sysOrigin) {
|
||||
throw new Error("请先填写 sysOrigin,或先查首页自动填入");
|
||||
}
|
||||
if (!userId) {
|
||||
throw new Error("请填写被邀请人 userId");
|
||||
}
|
||||
if (!rechargeCoins) {
|
||||
throw new Error("请填写充值金币数量");
|
||||
}
|
||||
els.orderIdInput.value = orderId;
|
||||
const payload = await request("/internal/invite-campaign/recharge-success", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
orderId,
|
||||
userId,
|
||||
rechargeCoins,
|
||||
sysOrigin,
|
||||
payTime: Date.now(),
|
||||
}),
|
||||
});
|
||||
addLog("recharge-success", payload);
|
||||
els.orderIdInput.value = nextOrderId();
|
||||
}
|
||||
|
||||
async function claimTask(ruleId) {
|
||||
const token = authHeader(els.inviterTokenInput.value);
|
||||
if (!token) {
|
||||
throw new Error("请先填写邀请人 token");
|
||||
}
|
||||
const payload = await request("/app/h5/invite-campaign/tasks/claim", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ ruleId }),
|
||||
});
|
||||
addLog("claim rule " + ruleId, payload);
|
||||
await fetchHome();
|
||||
}
|
||||
|
||||
function applyHome(home) {
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
els.inviteCodeInput.value = home.inviteCode || els.inviteCodeInput.value;
|
||||
els.sysOriginInput.value = home.sysOrigin || els.sysOriginInput.value;
|
||||
els.inviteLinkOutput.value = home.inviteLink || "";
|
||||
renderMetrics(home);
|
||||
renderTasks(home);
|
||||
startCountdown(home);
|
||||
}
|
||||
|
||||
function renderMetrics(home) {
|
||||
const stats = home.stats || {};
|
||||
const monthly = home.monthlyProgress || {};
|
||||
const values = [
|
||||
["累计邀请", stats.totalInvitedUsers],
|
||||
["累计有效", stats.totalValidUsers],
|
||||
["累计充值", stats.totalRechargeCoins],
|
||||
["累计奖励", stats.rewardGoldCoins],
|
||||
["本月邀请", monthly.inviteCount],
|
||||
["本月有效", monthly.validUserCount],
|
||||
["本月充值", monthly.totalRechargeCoins],
|
||||
["本月奖励", monthly.rewardGoldCoins],
|
||||
];
|
||||
els.metrics.innerHTML = values
|
||||
.map(([label, value]) => `
|
||||
<div class="metric">
|
||||
<span>${escapeHtml(label)}</span>
|
||||
<strong>${escapeHtml(formatNumber(value))}</strong>
|
||||
</div>
|
||||
`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderTasks(home) {
|
||||
const groups = [
|
||||
["邀请人数任务", home.inviterValidCountTasks || []],
|
||||
["总充值任务", home.inviterTotalRechargeTasks || []],
|
||||
];
|
||||
els.taskList.innerHTML = groups
|
||||
.map(([title, tasks]) => `
|
||||
<div class="task">
|
||||
<strong>${escapeHtml(title)}</strong>
|
||||
${tasks.map(renderTask).join("") || '<span class="small">暂无任务</span>'}
|
||||
</div>
|
||||
`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderTask(task) {
|
||||
const canClaim = task.achieved && !task.claimed;
|
||||
return `
|
||||
<div class="task-head">
|
||||
<div>
|
||||
<strong>Rule #${escapeHtml(task.ruleId)}</strong>
|
||||
<div class="task-meta">
|
||||
进度 ${escapeHtml(formatNumber(task.progress))} / ${escapeHtml(formatNumber(task.threshold))}
|
||||
· 奖励 ${escapeHtml(formatNumber(task.goldAmount))} 金币
|
||||
· ${task.claimed ? "已领取" : task.achieved ? "可领取" : "未达标"}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" ${canClaim ? "" : "disabled"} data-claim="${escapeHtml(task.ruleId)}">
|
||||
${task.claimed ? "Claimed" : "Claim"}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRank(rank) {
|
||||
const items = rank?.items || [];
|
||||
els.rankList.innerHTML = items.length
|
||||
? items.map((item) => `
|
||||
<div class="rank">
|
||||
<div class="rank-user">
|
||||
<strong>#${escapeHtml(item.rank)}</strong>
|
||||
<img class="avatar" src="${escapeHtml(item.avatar || "")}" alt="" />
|
||||
<div>
|
||||
<div class="rank-name">${escapeHtml(item.nickname || "User " + item.userId)}</div>
|
||||
<div class="small">userId: ${escapeHtml(item.userId)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small">
|
||||
邀请 ${escapeHtml(formatNumber(item.inviteCount))} 人 · 奖励 ${escapeHtml(formatNumber(item.rewardGoldCoins))}
|
||||
</div>
|
||||
</div>
|
||||
`).join("")
|
||||
: '<span class="small">暂无排行榜数据</span>';
|
||||
}
|
||||
|
||||
function startCountdown(home) {
|
||||
window.clearInterval(state.countdownTimer);
|
||||
const endAt = Number(home.periodEndAtMs || 0);
|
||||
const serverTime = Number(home.serverTimeMs || Date.now());
|
||||
const offset = serverTime - Date.now();
|
||||
const tick = () => {
|
||||
const diff = Math.max(0, endAt - (Date.now() + offset));
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
els.countdownText.textContent =
|
||||
"当前月 " + (home.monthKey || "--") + ",距离重置:" +
|
||||
days + "天 " + pad(hours) + ":" + pad(minutes) + ":" + pad(secs) +
|
||||
",periodEndAtMs=" + endAt;
|
||||
};
|
||||
tick();
|
||||
state.countdownTimer = window.setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
function pad(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const number = Number(value || 0);
|
||||
return Number.isFinite(number) ? number.toLocaleString("en-US") : "--";
|
||||
}
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const actionButton = event.target.closest("[data-action]");
|
||||
if (actionButton) {
|
||||
runAction(actionButton.dataset.action);
|
||||
return;
|
||||
}
|
||||
const claimButton = event.target.closest("[data-claim]");
|
||||
if (claimButton) {
|
||||
claimTask(Number(claimButton.dataset.claim)).catch((error) => {
|
||||
addLog("claim", error.payload || { message: error.message }, false);
|
||||
setStatus("领取失败:" + error.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("saveButton").addEventListener("click", saveParams);
|
||||
document.getElementById("resetButton").addEventListener("click", () => {
|
||||
els.logs.innerHTML = "";
|
||||
setStatus("日志已清空");
|
||||
});
|
||||
|
||||
loadSavedParams();
|
||||
renderMetrics({ stats: {}, monthlyProgress: {} });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1033
h5/app-invite/index.html
Normal file
BIN
h5/assets/loading-ranking.gif
Normal file
|
After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 458 KiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 451 KiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 482 KiB After Width: | Height: | Size: 2.8 MiB |
407
h5/mifa-pay/app.js
Normal file
@ -0,0 +1,407 @@
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const storedAppId = window.localStorage.getItem("mifa-pay-application-id") || "";
|
||||
const LOGO_SRC = "./mifapay-logo.png";
|
||||
|
||||
function getPathApplicationId() {
|
||||
const segments = window.location.pathname.split("/").filter(Boolean);
|
||||
const pageIndex = segments.lastIndexOf("mifa-pay");
|
||||
if (pageIndex === -1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return segments[pageIndex + 1] || "";
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
applicationId:
|
||||
getPathApplicationId() ||
|
||||
query.get("applicationId") ||
|
||||
query.get("appId") ||
|
||||
storedAppId ||
|
||||
"2048200000000000701",
|
||||
productType: query.get("type") || "GOLD",
|
||||
};
|
||||
|
||||
const deps = {
|
||||
getApi: null,
|
||||
postApi: null,
|
||||
fetchCurrentUser: null,
|
||||
getCachedUserProfile: null,
|
||||
connectApp: null,
|
||||
};
|
||||
|
||||
const state = {
|
||||
applicationId: defaults.applicationId,
|
||||
productType: defaults.productType,
|
||||
userProfile: null,
|
||||
countries: [],
|
||||
selectedCountryId: "",
|
||||
commodityCard: null,
|
||||
selectedGoodsId: "",
|
||||
selectedGoods: null,
|
||||
pendingChannelCode: "",
|
||||
dependencyPromise: null,
|
||||
};
|
||||
|
||||
const els = {
|
||||
applicationId: document.getElementById("applicationId"),
|
||||
productList: document.getElementById("productList"),
|
||||
channelList: document.getElementById("channelList"),
|
||||
pageStatus: document.getElementById("pageStatus"),
|
||||
};
|
||||
|
||||
function setStatus(text = "") {
|
||||
if (!text) {
|
||||
els.pageStatus.textContent = "";
|
||||
els.pageStatus.className = "status";
|
||||
return;
|
||||
}
|
||||
|
||||
els.pageStatus.textContent = text;
|
||||
els.pageStatus.className = "status show";
|
||||
}
|
||||
|
||||
function persistSettings() {
|
||||
window.localStorage.setItem("mifa-pay-application-id", state.applicationId);
|
||||
}
|
||||
|
||||
function extractErrorMessage(error) {
|
||||
return (
|
||||
error?.response?.errorMsg ||
|
||||
error?.response?.body?.errorMsg ||
|
||||
error?.errorMsg ||
|
||||
error?.message ||
|
||||
"Unknown error"
|
||||
);
|
||||
}
|
||||
|
||||
function readStoredUserProfile() {
|
||||
const raw = window.localStorage.getItem("userProfile");
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === "object" ? parsed : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveUserProfile(response = null) {
|
||||
if (response?.body && typeof response.body === "object") {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
if (typeof deps.getCachedUserProfile === "function") {
|
||||
const cached = deps.getCachedUserProfile();
|
||||
if (cached && typeof cached === "object") {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
return readStoredUserProfile();
|
||||
}
|
||||
|
||||
function currentUserId() {
|
||||
return String(state.userProfile?.id || "").trim();
|
||||
}
|
||||
|
||||
function currentCountryId() {
|
||||
return state.selectedCountryId;
|
||||
}
|
||||
|
||||
function getCountryLabel(item) {
|
||||
return (
|
||||
item?.label ||
|
||||
item?.countryName ||
|
||||
item?.country?.aliasName ||
|
||||
item?.country?.countryName ||
|
||||
String(item?.id || "")
|
||||
);
|
||||
}
|
||||
|
||||
function getGoodsList() {
|
||||
return state.commodityCard?.commodity || [];
|
||||
}
|
||||
|
||||
function getChannelList() {
|
||||
return state.commodityCard?.channels || [];
|
||||
}
|
||||
|
||||
function getChannelCode(item) {
|
||||
return item?.details?.channelCode || item?.channel?.channelCode || "";
|
||||
}
|
||||
|
||||
function getAmountText(item) {
|
||||
const content = item?.content || item?.obtainGoldsQuantity || "-";
|
||||
return `${content} ${item?.type || ""}`.trim();
|
||||
}
|
||||
|
||||
function getPriceText(item) {
|
||||
const currency = item?.currency || state.commodityCard?.payCountry?.currency || "USD";
|
||||
return `${item?.amount || item?.amountUsd || "-"} ${currency}`;
|
||||
}
|
||||
|
||||
function ensureSelectedGoods() {
|
||||
const goods = getGoodsList();
|
||||
if (!goods.length) {
|
||||
state.selectedGoodsId = "";
|
||||
state.selectedGoods = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const matched =
|
||||
goods.find((item) => String(item.id) === state.selectedGoodsId) ||
|
||||
goods.find((item) => item.type === state.productType) ||
|
||||
goods[0];
|
||||
state.selectedGoods = matched;
|
||||
state.selectedGoodsId = String(matched.id);
|
||||
}
|
||||
|
||||
function renderProducts() {
|
||||
const goods = getGoodsList();
|
||||
ensureSelectedGoods();
|
||||
|
||||
if (!goods.length) {
|
||||
els.productList.innerHTML = '<div class="empty">No amount available.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
els.productList.innerHTML = goods
|
||||
.map((item) => {
|
||||
const selected = String(item.id) === state.selectedGoodsId;
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="amount-item${selected ? " selected" : ""}"
|
||||
data-goods-id="${item.id}"
|
||||
>
|
||||
<span class="check"></span>
|
||||
<span class="amount-copy">
|
||||
<span class="amount-value">${item.content || item.obtainGoldsQuantity || "-"}</span>
|
||||
<span class="amount-meta">${item.type || "Recharge"}</span>
|
||||
</span>
|
||||
<span class="price-copy">
|
||||
<strong>${item.amount || item.amountUsd || "-"}</strong>
|
||||
<span>USD ${item.amountUsd || "-"}</span>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
els.productList.querySelectorAll("[data-goods-id]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const goodsId = button.getAttribute("data-goods-id");
|
||||
state.selectedGoodsId = goodsId;
|
||||
state.selectedGoods =
|
||||
getGoodsList().find((item) => String(item.id) === goodsId) || null;
|
||||
renderProducts();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderChannels() {
|
||||
const channels = getChannelList();
|
||||
if (!channels.length) {
|
||||
els.channelList.innerHTML = '<div class="empty">No payment method available.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
els.channelList.innerHTML = channels
|
||||
.map((item) => {
|
||||
const code = getChannelCode(item);
|
||||
const pending = state.pendingChannelCode === code;
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="channel-button"
|
||||
data-channel-code="${code}"
|
||||
${pending ? "disabled" : ""}
|
||||
>
|
||||
<span class="channel-logo">
|
||||
<img src="${LOGO_SRC}" alt="MiFaPay" />
|
||||
</span>
|
||||
<span class="channel-name">${pending ? "Processing..." : "Pay with MiFaPay"}</span>
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
els.channelList.querySelectorAll("[data-channel-code]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const channelCode = button.getAttribute("data-channel-code");
|
||||
try {
|
||||
await createOrder(channelCode);
|
||||
} catch (error) {
|
||||
state.pendingChannelCode = "";
|
||||
renderChannels();
|
||||
setStatus(extractErrorMessage(error));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function selectCountryByProfile() {
|
||||
const userCountryCode = state.userProfile?.countryCode;
|
||||
if (!userCountryCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matched = state.countries.find((item) =>
|
||||
getCountryLabel(item).toUpperCase().includes(userCountryCode.toUpperCase())
|
||||
);
|
||||
|
||||
if (!matched) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.selectedCountryId = String(matched.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureDependencies() {
|
||||
if (!state.dependencyPromise) {
|
||||
state.dependencyPromise = Promise.all([
|
||||
import("../js/index-CIAVq8iD-1776148658686.js"),
|
||||
import("../js/appConnector-B5Pbaops-1776148658686.js"),
|
||||
]).then(([indexModule, connectorModule]) => {
|
||||
deps.getApi = indexModule.L;
|
||||
deps.postApi = indexModule.W;
|
||||
deps.fetchCurrentUser = indexModule.au;
|
||||
deps.getCachedUserProfile = indexModule.at;
|
||||
deps.connectApp = connectorModule.c;
|
||||
return deps;
|
||||
});
|
||||
}
|
||||
|
||||
return state.dependencyPromise;
|
||||
}
|
||||
|
||||
async function connectSession() {
|
||||
await ensureDependencies();
|
||||
|
||||
const connection = await deps.connectApp();
|
||||
if (!connection?.success) {
|
||||
throw new Error(connection?.error || "Failed to initialize app session");
|
||||
}
|
||||
|
||||
const cachedUserProfile = resolveUserProfile();
|
||||
if (cachedUserProfile?.id) {
|
||||
state.userProfile = cachedUserProfile;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deps.fetchCurrentUser();
|
||||
const userProfile = resolveUserProfile(response);
|
||||
if (!userProfile?.id) {
|
||||
throw new Error("Please open this page inside the app");
|
||||
}
|
||||
|
||||
state.userProfile = userProfile;
|
||||
}
|
||||
|
||||
async function refreshMeta() {
|
||||
if (!state.applicationId) {
|
||||
throw new Error("Missing applicationId");
|
||||
}
|
||||
|
||||
persistSettings();
|
||||
els.applicationId.value = state.applicationId;
|
||||
|
||||
const countriesResp = await deps.getApi("/order/web/pay/country");
|
||||
state.countries = Array.isArray(countriesResp?.body) ? countriesResp.body : [];
|
||||
if (!state.countries.length) {
|
||||
state.selectedCountryId = "";
|
||||
state.commodityCard = null;
|
||||
renderProducts();
|
||||
renderChannels();
|
||||
throw new Error("No supported country");
|
||||
}
|
||||
|
||||
if (
|
||||
!state.selectedCountryId ||
|
||||
!state.countries.some((item) => String(item.id) === String(state.selectedCountryId))
|
||||
) {
|
||||
if (!selectCountryByProfile()) {
|
||||
state.selectedCountryId = String(state.countries[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
const commodityResp = await deps.getApi(
|
||||
`/order/web/pay/commodity?applicationId=${encodeURIComponent(
|
||||
state.applicationId
|
||||
)}&payCountryId=${encodeURIComponent(currentCountryId())}&type=${encodeURIComponent(
|
||||
state.productType
|
||||
)}`
|
||||
);
|
||||
|
||||
state.commodityCard = commodityResp?.body || {};
|
||||
renderProducts();
|
||||
renderChannels();
|
||||
setStatus("");
|
||||
}
|
||||
|
||||
function safeDecodeUrl(url) {
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(url);
|
||||
} catch (error) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrder(channelCode) {
|
||||
if (!state.selectedGoods) {
|
||||
throw new Error("Select an amount first");
|
||||
}
|
||||
|
||||
if (!currentUserId()) {
|
||||
throw new Error("Account not available");
|
||||
}
|
||||
|
||||
state.pendingChannelCode = channelCode;
|
||||
setStatus("");
|
||||
renderChannels();
|
||||
|
||||
const payload = {
|
||||
applicationId: Number(state.applicationId),
|
||||
goodsId: Number(state.selectedGoodsId),
|
||||
payCountryId: Number(currentCountryId()),
|
||||
userId: Number(currentUserId()),
|
||||
channelCode,
|
||||
newVersion: true,
|
||||
};
|
||||
|
||||
const response = await deps.postApi("/order/web/pay/recharge", payload);
|
||||
const requestUrl = safeDecodeUrl(response?.body?.requestUrl || "");
|
||||
if (!requestUrl) {
|
||||
throw new Error("Missing payment url");
|
||||
}
|
||||
|
||||
window.location.href = requestUrl;
|
||||
}
|
||||
|
||||
async function reloadPageData() {
|
||||
await ensureDependencies();
|
||||
await connectSession();
|
||||
await refreshMeta();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
renderProducts();
|
||||
renderChannels();
|
||||
|
||||
try {
|
||||
await reloadPageData();
|
||||
} catch (error) {
|
||||
setStatus(extractErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
249
h5/mifa-pay/index.html
Normal file
@ -0,0 +1,249 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Recharge</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #08131f;
|
||||
--surface: rgba(15, 28, 42, 0.92);
|
||||
--surface-strong: #102030;
|
||||
--line: rgba(214, 170, 82, 0.18);
|
||||
--line-strong: rgba(214, 170, 82, 0.42);
|
||||
--text: #f6f1e6;
|
||||
--muted: #bcae8d;
|
||||
--gold: #d6aa52;
|
||||
--gold-strong: #c49335;
|
||||
--danger: #ff9f9f;
|
||||
--shadow: 0 16px 36px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"SF Pro Display", "SF Pro Text", "PingFang SC", "Microsoft YaHei",
|
||||
sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(214, 170, 82, 0.1), transparent 24%),
|
||||
linear-gradient(180deg, #091321 0%, var(--bg) 100%);
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: min(100%, 480px);
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 18px;
|
||||
font-size: 30px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
color: var(--gold);
|
||||
text-align: center;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.amount-item,
|
||||
.channel-button {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(214, 170, 82, 0.04), transparent 60%),
|
||||
var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.amount-item:active,
|
||||
.channel-button:active {
|
||||
transform: scale(0.988);
|
||||
}
|
||||
|
||||
.amount-item.selected {
|
||||
border-color: var(--line-strong);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(214, 170, 82, 0.12),
|
||||
0 16px 36px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.amount-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.amount-meta {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.price-copy {
|
||||
text-align: right;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.price-copy strong {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.price-copy span {
|
||||
font-size: 11px;
|
||||
color: rgba(246, 241, 230, 0.58);
|
||||
}
|
||||
|
||||
.check {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(214, 170, 82, 0.3);
|
||||
}
|
||||
|
||||
.amount-item.selected .check {
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, #efcf82, var(--gold-strong));
|
||||
}
|
||||
|
||||
.amount-item.selected .check::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
border-left: 2px solid #0c1520;
|
||||
border-bottom: 2px solid #0c1520;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.channel-block {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.channel-button {
|
||||
justify-content: flex-start;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.channel-logo {
|
||||
width: 78px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.channel-logo img {
|
||||
display: block;
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.channel-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: none;
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 159, 159, 0.18);
|
||||
background: rgba(255, 159, 159, 0.08);
|
||||
color: var(--danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.status.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px dashed var(--line);
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>Recharge</h1>
|
||||
<div id="productList" class="list"></div>
|
||||
<div id="channelList" class="list channel-block"></div>
|
||||
<div id="pageStatus" class="status" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<input id="applicationId" type="hidden" />
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
h5/mifa-pay/mifapay-logo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
120
h5/yumi-invite-agency/index.html
Normal file
@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Invite User To Become Agent</title>
|
||||
<link rel="stylesheet" href="./yumi-invite-agency.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="fullPage" id="pageRoot">
|
||||
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div>
|
||||
<div class="header-container">
|
||||
<div class="side-area">
|
||||
<button class="back-btn" id="backButton" type="button">
|
||||
<img class="flip-img" id="backButtonIcon" src="" alt="" style="width: 100%; aspect-ratio: 1 / 1; display: block" />
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="header-title" id="pageTitle" style="color: black">Invite User To Become Agent</h1>
|
||||
<div class="actions-area">
|
||||
<div class="language-entry" id="languageEntry">
|
||||
<div class="language-name" id="languageName">English</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; position: relative">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
||||
<div
|
||||
class="gradient-border-wrapper"
|
||||
style="
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 4px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 0 8px; display: flex; align-items: center; height: max-content">
|
||||
<input
|
||||
id="searchInput"
|
||||
type="text"
|
||||
name=""
|
||||
placeholder="Enter User ID"
|
||||
autocomplete="off"
|
||||
style="
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 8px;
|
||||
"
|
||||
/>
|
||||
<button
|
||||
id="clearButton"
|
||||
type="button"
|
||||
hidden
|
||||
style="background: none; border: none; color: #9ca3af; cursor: pointer; padding: 0 4px"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button style="font-weight: 600" class="invite-btn" id="searchButton" type="button" disabled>Search</button>
|
||||
</div>
|
||||
|
||||
<div id="resultList" style="margin: 16px 0; display: flex; flex-direction: column; gap: 12px"></div>
|
||||
<div id="noData" class="no-data">please input user id</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="userCardTemplate">
|
||||
<div
|
||||
class="gradient-border-wrapper user-card"
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border-radius: 20px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 10px; transition: all 0.2s">
|
||||
<div class="status-badge" style="position: absolute; z-index: 3; top: -2px; right: -2px"></div>
|
||||
<div style="font-weight: 600">Information:</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px">
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px">
|
||||
<div style="width: 3em; display: flex; align-items: center; justify-content: center">
|
||||
<img class="avatar-image" src="" alt="" style="width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 50%" />
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch">
|
||||
<div class="nickname" style="margin-bottom: 4px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"></div>
|
||||
<div class="account" style="font-size: 0.9em; color: rgba(0, 0, 0, 0.4); font-weight: 500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: auto; min-width: 0; align-self: stretch; display: flex; justify-content: center; align-items: center">
|
||||
<div class="pending-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./yumi-invite-agency.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
456
h5/yumi-invite-agency/yumi-invite-agency.css
Normal file
@ -0,0 +1,456 @@
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fullPage {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background-color: #fff;
|
||||
background-image: url("../assets/secondBg-CSqvFWr0-1776148661447.png");
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.fullPage input::placeholder {
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.invite-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.invite-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.result-action-btn {
|
||||
padding: 4px 10px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-action-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status-bar-placeholder {
|
||||
display: none;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.status-bar-placeholder.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.side-area,
|
||||
.actions-area {
|
||||
min-width: 1.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions-area {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.back-btn,
|
||||
.help-btn {
|
||||
width: 1.5em;
|
||||
border: none;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.back-btn:active,
|
||||
.help-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.language-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #f59e0b !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: var(--gradient-border-width, 1.5px);
|
||||
background: linear-gradient(
|
||||
var(--gradient-direction, to bottom),
|
||||
var(--gradient-start-color, #ffc4c4),
|
||||
var(--gradient-end-color, #fff6a6)
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gradient-border-content {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(
|
||||
--gradient-background,
|
||||
linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%)
|
||||
);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 20px 25px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px rgba(0, 0, 0, 0.04);
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 24px 20px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: iconPulse 0.6s ease-out;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.modal-error .icon-circle-error {
|
||||
background-color: #fee2e2;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.modal-error .icon-symbol-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-error .modal-btn-error {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-warning .icon-circle-warning {
|
||||
background-color: #fef3c7;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.modal-warning .icon-symbol-warning {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.modal-warning .modal-btn-warning {
|
||||
background-color: #d97706;
|
||||
}
|
||||
|
||||
.modal-success .icon-circle-success {
|
||||
background-color: #d1fae5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
.modal-success .icon-symbol-success {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.modal-success .modal-btn-success {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.modal-info .icon-circle-info {
|
||||
background-color: #dbeafe;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.modal-info .icon-symbol-info {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-info .modal-btn-info {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-simple .modal-message {
|
||||
font-size: 15px;
|
||||
color: #374151;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-simple .modal-btn {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-enter {
|
||||
animation: modalEnter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.modal-leave {
|
||||
animation: modalLeave 0.2s ease-in;
|
||||
}
|
||||
|
||||
[dir="rtl"] .flip-img {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
[dir="rtl"] .avatar {
|
||||
margin-right: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .user-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-entry,
|
||||
[dir="rtl"] .header-title {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
@keyframes modalEnter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.7) translateY(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalLeave {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.fullPage {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.fullPage {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal {
|
||||
max-width: 280px;
|
||||
padding: 20px 16px 16px;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
694
h5/yumi-invite-agency/yumi-invite-agency.js
Normal file
@ -0,0 +1,694 @@
|
||||
(function () {
|
||||
const API_BASE = "https://jvapi.haiyihy.com";
|
||||
const BACK_ICON =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
|
||||
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png";
|
||||
|
||||
const TEXT = {
|
||||
pageTitle: "Invite User To Become Agent",
|
||||
language: "English",
|
||||
placeholder: "Enter User ID",
|
||||
search: "Search",
|
||||
invite: "Invite",
|
||||
idlePrompt: "please input user id",
|
||||
noUsersFound: "No users found",
|
||||
information: "Information:",
|
||||
userIdPrefix: "User ID:",
|
||||
pending: "Pending",
|
||||
success: "Success",
|
||||
cancel: "Cancel",
|
||||
invitationSubmitted: "Invitation submitted",
|
||||
applicationCancelled: "Application cancelled",
|
||||
somethingWentWrong: "Something went wrong",
|
||||
cancellationFailed: "Cancellation failed, try again",
|
||||
informationTitle: "Information",
|
||||
successTitle: "Success",
|
||||
errorTitle: "Error",
|
||||
ok: "OK",
|
||||
};
|
||||
|
||||
const state = {
|
||||
query: "",
|
||||
invitedList: [],
|
||||
searchResults: [],
|
||||
showSearchResults: false,
|
||||
searching: false,
|
||||
hasSearched: false,
|
||||
};
|
||||
|
||||
const dom = {
|
||||
statusBarPlaceholder: document.getElementById("statusBarPlaceholder"),
|
||||
backButton: document.getElementById("backButton"),
|
||||
backButtonIcon: document.getElementById("backButtonIcon"),
|
||||
pageTitle: document.getElementById("pageTitle"),
|
||||
languageEntry: document.getElementById("languageEntry"),
|
||||
languageName: document.getElementById("languageName"),
|
||||
searchInput: document.getElementById("searchInput"),
|
||||
clearButton: document.getElementById("clearButton"),
|
||||
searchButton: document.getElementById("searchButton"),
|
||||
resultList: document.getElementById("resultList"),
|
||||
noData: document.getElementById("noData"),
|
||||
userCardTemplate: document.getElementById("userCardTemplate"),
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
document.title = TEXT.pageTitle;
|
||||
document.documentElement.lang = "en";
|
||||
document.documentElement.dir = resolveDocumentDirection();
|
||||
|
||||
dom.backButtonIcon.src = BACK_ICON;
|
||||
dom.pageTitle.textContent = TEXT.pageTitle;
|
||||
dom.languageName.textContent = TEXT.language;
|
||||
dom.searchInput.placeholder = TEXT.placeholder;
|
||||
dom.searchButton.textContent = TEXT.search;
|
||||
dom.noData.textContent = TEXT.idlePrompt;
|
||||
|
||||
syncStatusBarPlaceholder();
|
||||
bindEvents();
|
||||
render();
|
||||
loadInvitedList();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
dom.backButton.addEventListener("click", handleBack);
|
||||
dom.languageEntry.addEventListener("click", handleLanguageClick);
|
||||
dom.searchInput.addEventListener("input", handleSearchInput);
|
||||
dom.clearButton.addEventListener("click", clearSearch);
|
||||
dom.searchButton.addEventListener("click", triggerSearchNow);
|
||||
}
|
||||
|
||||
function resolveDocumentDirection() {
|
||||
const lang = (new URLSearchParams(window.location.search).get("lang") || "").toLowerCase();
|
||||
return lang.startsWith("ar") ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
function syncStatusBarPlaceholder() {
|
||||
const hasAppBridge =
|
||||
Boolean(window.app) ||
|
||||
Boolean(window.FlutterPageControl) ||
|
||||
Boolean(window.webkit && window.webkit.messageHandlers);
|
||||
const isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent || "");
|
||||
dom.statusBarPlaceholder.classList.toggle("is-visible", hasAppBridge && isIOS);
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
function handleLanguageClick() {
|
||||
window.location.href = "/language";
|
||||
}
|
||||
|
||||
function handleSearchInput(event) {
|
||||
state.query = event.target.value;
|
||||
dom.clearButton.hidden = !state.query;
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
state.query = "";
|
||||
state.searchResults = [];
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
dom.searchInput.value = "";
|
||||
dom.clearButton.hidden = true;
|
||||
render();
|
||||
}
|
||||
|
||||
async function triggerSearchNow() {
|
||||
if (state.searching) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = state.query.trim();
|
||||
|
||||
if (!query) {
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.searching = true;
|
||||
state.showSearchResults = true;
|
||||
state.hasSearched = true;
|
||||
state.searchResults = [];
|
||||
render();
|
||||
|
||||
try {
|
||||
const payload = await searchUser(query);
|
||||
state.searchResults = payload ? [mergeWithInvitationStatus(payload.userProfile)] : [];
|
||||
} catch (error) {
|
||||
state.searchResults = [];
|
||||
showToast(extractErrorMessage(error), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
state.searching = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInvitedList() {
|
||||
try {
|
||||
const response = await request("GET", "/team/bd/invite-message-own");
|
||||
state.invitedList = response && response.status && Array.isArray(response.body) ? response.body : [];
|
||||
state.searchResults = state.searchResults.map(function (item) {
|
||||
return mergeWithInvitationStatus(item.userProfile || {});
|
||||
});
|
||||
render();
|
||||
} catch (error) {
|
||||
state.invitedList = [];
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function searchUser(query) {
|
||||
try {
|
||||
const response = await request(
|
||||
"GET",
|
||||
"/user/user-profile/open-search?account=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (response && response.status && response.body) {
|
||||
return { userProfile: response.body };
|
||||
}
|
||||
} catch (error) {
|
||||
if (!/^\d+$/.test(query)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackResponse = await request(
|
||||
"GET",
|
||||
"/user/user-profile?userId=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (fallbackResponse && fallbackResponse.status && fallbackResponse.body) {
|
||||
return { userProfile: fallbackResponse.body };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeWithInvitationStatus(userProfile) {
|
||||
const matchedInvitation = state.invitedList.find(function (item) {
|
||||
return (
|
||||
item &&
|
||||
item.userProfile &&
|
||||
userProfile &&
|
||||
item.userProfile.id === userProfile.id
|
||||
);
|
||||
});
|
||||
|
||||
return Object.assign({}, matchedInvitation || {}, {
|
||||
userProfile: userProfile || {},
|
||||
});
|
||||
}
|
||||
|
||||
async function inviteUser(item) {
|
||||
if (!item || !item.userProfile || !item.userProfile.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request("POST", "/team/bd/invite-agent", {
|
||||
body: {
|
||||
inviteUserId: item.userProfile.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.invitationSubmitted, {
|
||||
title: TEXT.successTitle,
|
||||
type: "success",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.somethingWentWrong), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelInvitation(invitationId) {
|
||||
try {
|
||||
const response = await request("POST", "/team/bd/invite-agent-cancel/" + invitationId);
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.applicationCancelled, {
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.cancellationFailed), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderSearchButton();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function renderSearchButton() {
|
||||
dom.searchButton.disabled = !state.query.trim() || state.searching;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const currentList = state.showSearchResults ? state.searchResults : state.invitedList;
|
||||
dom.resultList.innerHTML = "";
|
||||
|
||||
currentList.forEach(function (item) {
|
||||
dom.resultList.appendChild(createUserCard(item, state.showSearchResults ? "search" : "invited"));
|
||||
});
|
||||
|
||||
if (currentList.length > 0 || (state.searching && state.showSearchResults)) {
|
||||
dom.noData.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
dom.noData.hidden = false;
|
||||
dom.noData.textContent =
|
||||
state.showSearchResults && state.hasSearched ? TEXT.noUsersFound : TEXT.idlePrompt;
|
||||
}
|
||||
|
||||
function createUserCard(item, mode) {
|
||||
const fragment = dom.userCardTemplate.content.cloneNode(true);
|
||||
const statusBadge = fragment.querySelector(".status-badge");
|
||||
const avatarImage = fragment.querySelector(".avatar-image");
|
||||
const nickname = fragment.querySelector(".nickname");
|
||||
const account = fragment.querySelector(".account");
|
||||
const pendingActions = fragment.querySelector(".pending-actions");
|
||||
const userProfile = item.userProfile || {};
|
||||
|
||||
avatarImage.src = userProfile.userAvatar || DEFAULT_AVATAR;
|
||||
avatarImage.alt = userProfile.userNickname || "";
|
||||
avatarImage.onerror = handleAvatarError;
|
||||
nickname.textContent = userProfile.userNickname || "";
|
||||
account.textContent = TEXT.userIdPrefix + " " + (userProfile.account || userProfile.id || "");
|
||||
|
||||
if (item.status === 0) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #759cff 5.66%, #3d73ff 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.pending +
|
||||
"</div>";
|
||||
} else if (item.status === 1) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #75ff98 5.66%, #3dff54 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.success +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
if (mode === "search") {
|
||||
const inviteButton = document.createElement("button");
|
||||
inviteButton.type = "button";
|
||||
inviteButton.className = "result-action-btn";
|
||||
inviteButton.textContent = TEXT.invite;
|
||||
inviteButton.disabled = item.status === 0 || item.status === 1;
|
||||
inviteButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
inviteUser(item);
|
||||
});
|
||||
pendingActions.appendChild(inviteButton);
|
||||
} else if (item.status === 0) {
|
||||
const cancelButton = document.createElement("div");
|
||||
cancelButton.textContent = TEXT.cancel;
|
||||
cancelButton.style.padding = "4px 8px";
|
||||
cancelButton.style.borderRadius = "32px";
|
||||
cancelButton.style.backgroundColor = "red";
|
||||
cancelButton.style.color = "white";
|
||||
cancelButton.style.fontWeight = "600";
|
||||
cancelButton.style.zIndex = "5";
|
||||
cancelButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
cancelInvitation(item.id);
|
||||
});
|
||||
pendingActions.appendChild(cancelButton);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function handleAvatarError(event) {
|
||||
event.target.onerror = null;
|
||||
event.target.src = DEFAULT_AVATAR;
|
||||
}
|
||||
|
||||
function extractErrorMessage(error, fallback) {
|
||||
return (
|
||||
(error &&
|
||||
error.response &&
|
||||
(error.response.errorMsg || error.response.message || error.response.error)) ||
|
||||
(error && error.message) ||
|
||||
fallback ||
|
||||
TEXT.somethingWentWrong
|
||||
);
|
||||
}
|
||||
|
||||
function showToast(message, options) {
|
||||
const config = Object.assign(
|
||||
{
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
confirmText: TEXT.ok,
|
||||
},
|
||||
options || {}
|
||||
);
|
||||
|
||||
const iconMap = {
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
success: "✓",
|
||||
info: "i",
|
||||
simple: "",
|
||||
};
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "modal-overlay";
|
||||
overlay.innerHTML =
|
||||
'<div class="modal modal-enter modal-' +
|
||||
config.type +
|
||||
'">' +
|
||||
(config.type === "simple"
|
||||
? ""
|
||||
: '<div class="modal-icon"><div class="icon-circle icon-circle-' +
|
||||
config.type +
|
||||
'"><span class="icon-symbol icon-symbol-' +
|
||||
config.type +
|
||||
'">' +
|
||||
iconMap[config.type] +
|
||||
"</span></div></div>") +
|
||||
(config.type === "simple" || !config.title
|
||||
? ""
|
||||
: '<div class="modal-title">' + escapeHtml(config.title) + "</div>") +
|
||||
'<div class="modal-message">' +
|
||||
escapeHtml(message) +
|
||||
"</div>" +
|
||||
'<div class="modal-actions"><button class="modal-btn modal-btn-' +
|
||||
config.type +
|
||||
'">' +
|
||||
escapeHtml(config.confirmText) +
|
||||
"</button></div>" +
|
||||
"</div>";
|
||||
|
||||
const modal = overlay.querySelector(".modal");
|
||||
const closeButton = overlay.querySelector(".modal-btn");
|
||||
const close = function () {
|
||||
modal.classList.remove("modal-enter");
|
||||
modal.classList.add("modal-leave");
|
||||
window.setTimeout(function () {
|
||||
overlay.remove();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
closeButton.addEventListener("click", close);
|
||||
overlay.addEventListener("click", function (event) {
|
||||
if (event.target === overlay) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
async function request(method, path, options) {
|
||||
const config = options || {};
|
||||
const headers = buildRequestHeaders(Boolean(config.body), config.headers || {});
|
||||
const response = await fetch(API_BASE + path, {
|
||||
method: method,
|
||||
credentials: "include",
|
||||
headers: headers,
|
||||
body: config.body ? JSON.stringify(config.body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = {};
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch (error) {
|
||||
payload = {
|
||||
status: response.ok,
|
||||
body: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || payload.status === false) {
|
||||
const requestError = new Error(
|
||||
payload.errorMsg || payload.message || response.statusText || TEXT.somethingWentWrong
|
||||
);
|
||||
requestError.response = payload;
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildRequestHeaders(hasBody, overrideHeaders) {
|
||||
const discovered = discoverAppHeaders();
|
||||
const headers = Object.assign({}, overrideHeaders || {});
|
||||
|
||||
if (hasBody) {
|
||||
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
||||
}
|
||||
|
||||
headers.Accept = headers.Accept || "application/json, text/plain, */*";
|
||||
|
||||
if (discovered.authorization) {
|
||||
headers.Authorization = headers.Authorization || discovered.authorization;
|
||||
}
|
||||
|
||||
if (discovered.reqLang) {
|
||||
headers["Req-Lang"] = headers["Req-Lang"] || discovered.reqLang;
|
||||
}
|
||||
|
||||
if (discovered.appVersion) {
|
||||
headers["App-Version"] = headers["App-Version"] || discovered.appVersion;
|
||||
}
|
||||
|
||||
if (discovered.buildVersion) {
|
||||
headers["Build-Version"] = headers["Build-Version"] || discovered.buildVersion;
|
||||
}
|
||||
|
||||
if (discovered.appChannel) {
|
||||
headers["App-Channel"] = headers["App-Channel"] || discovered.appChannel;
|
||||
}
|
||||
|
||||
if (discovered.reqImei) {
|
||||
headers["Req-Imei"] = headers["Req-Imei"] || discovered.reqImei;
|
||||
}
|
||||
|
||||
if (discovered.sysOrigin) {
|
||||
headers["Sys-Origin"] = headers["Sys-Origin"] || discovered.sysOrigin;
|
||||
}
|
||||
|
||||
if (discovered.sysOriginChild) {
|
||||
headers["Sys-Origin-Child"] =
|
||||
headers["Sys-Origin-Child"] || discovered.sysOriginChild;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function discoverAppHeaders() {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const collected = [];
|
||||
|
||||
collectCandidate(collected, window.__APP_HEADERS__);
|
||||
collectCandidate(collected, window.__HEADER_INFO__);
|
||||
collectCandidate(collected, window.headerInfo);
|
||||
collectCandidate(collected, parseHeaderObjectFromQuery(query));
|
||||
|
||||
collectFromStorage(collected, window.localStorage);
|
||||
collectFromStorage(collected, window.sessionStorage);
|
||||
|
||||
return collected.reduce(function (result, candidate) {
|
||||
return Object.assign(result, candidate);
|
||||
}, {
|
||||
reqLang: query.get("lang") || document.documentElement.lang || "en",
|
||||
});
|
||||
}
|
||||
|
||||
function parseHeaderObjectFromQuery(query) {
|
||||
const candidate = {};
|
||||
mapHeaderValue(candidate, "authorization", query.get("authorization"));
|
||||
mapHeaderValue(candidate, "authorization", query.get("token"));
|
||||
mapHeaderValue(candidate, "reqLang", query.get("lang"));
|
||||
mapHeaderValue(candidate, "appVersion", query.get("appVersion"));
|
||||
mapHeaderValue(candidate, "buildVersion", query.get("buildVersion"));
|
||||
mapHeaderValue(candidate, "appChannel", query.get("appChannel"));
|
||||
mapHeaderValue(candidate, "reqImei", query.get("reqImei"));
|
||||
mapHeaderValue(candidate, "sysOrigin", query.get("sysOrigin"));
|
||||
mapHeaderValue(candidate, "sysOriginChild", query.get("sysOriginChild"));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function collectFromStorage(collected, storage) {
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferredKeys = [
|
||||
"authorization",
|
||||
"Authorization",
|
||||
"token",
|
||||
"accessToken",
|
||||
"headerInfo",
|
||||
"headersFromApp",
|
||||
"appHeaders",
|
||||
"persist:root",
|
||||
"user",
|
||||
"userInfo",
|
||||
];
|
||||
|
||||
preferredKeys.forEach(function (key) {
|
||||
try {
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index < storage.length; index += 1) {
|
||||
try {
|
||||
const key = storage.key(index);
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectCandidate(collected, value) {
|
||||
const normalized = normalizeHeaderCandidate(value, 0);
|
||||
if (normalized && Object.keys(normalized).length > 0) {
|
||||
collected.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHeaderCandidate(value, depth) {
|
||||
if (depth > 4 || value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed[0] === "{" || trimmed[0] === "[") {
|
||||
try {
|
||||
return normalizeHeaderCandidate(JSON.parse(trimmed), depth + 1);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (/bearer\s+/i.test(trimmed)) {
|
||||
return {
|
||||
authorization: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce(function (result, entry) {
|
||||
const normalized = normalizeHeaderCandidate(entry, depth + 1);
|
||||
return normalized ? Object.assign(result, normalized) : result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
const aliasMap = {
|
||||
authorization: ["authorization", "Authorization", "token", "accessToken"],
|
||||
reqLang: ["reqLang", "Req-Lang", "lang", "language"],
|
||||
appVersion: ["appVersion", "App-Version"],
|
||||
buildVersion: ["buildVersion", "Build-Version"],
|
||||
appChannel: ["appChannel", "App-Channel"],
|
||||
reqImei: ["reqImei", "Req-Imei"],
|
||||
sysOrigin: ["sysOrigin", "Sys-Origin"],
|
||||
sysOriginChild: ["sysOriginChild", "Sys-Origin-Child"],
|
||||
};
|
||||
|
||||
Object.keys(aliasMap).forEach(function (targetKey) {
|
||||
aliasMap[targetKey].forEach(function (sourceKey) {
|
||||
mapHeaderValue(normalized, targetKey, value[sourceKey]);
|
||||
});
|
||||
});
|
||||
|
||||
["headerInfo", "headers", "auth", "data", "state"].forEach(function (nestedKey) {
|
||||
const nestedValue = normalizeHeaderCandidate(value[nestedKey], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
if (
|
||||
key === "headerInfo" ||
|
||||
key === "headers" ||
|
||||
key === "auth" ||
|
||||
key === "data" ||
|
||||
key === "state"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value[key] === "object") {
|
||||
const nestedValue = normalizeHeaderCandidate(value[key], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mapHeaderValue(target, key, value) {
|
||||
if (value == null || value === "" || target[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
target[key] = String(value);
|
||||
}
|
||||
})();
|
||||
119
h5/yumi-invite-bd-leader/index.html
Normal file
@ -0,0 +1,119 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Invite User To Become BD Leader</title>
|
||||
<link rel="stylesheet" href="./yumi-invite-bd-leader.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="fullPage" id="pageRoot">
|
||||
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div>
|
||||
<div class="header-container">
|
||||
<div class="side-area">
|
||||
<button class="back-btn" id="backButton" type="button">
|
||||
<img class="flip-img" id="backButtonIcon" src="" alt="" style="width: 100%; aspect-ratio: 1 / 1; display: block" />
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="header-title" id="pageTitle" style="color: black">Invite User To Become BD Leader</h1>
|
||||
<div class="actions-area">
|
||||
<div class="language-entry" id="languageEntry">
|
||||
<div class="language-name" id="languageName">English</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; position: relative">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
||||
<div
|
||||
class="gradient-border-wrapper"
|
||||
style="
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 4px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 0 8px; display: flex; align-items: center; height: max-content">
|
||||
<input
|
||||
id="searchInput"
|
||||
type="text"
|
||||
placeholder="Enter User ID"
|
||||
autocomplete="off"
|
||||
style="
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 8px;
|
||||
"
|
||||
/>
|
||||
<button
|
||||
id="clearButton"
|
||||
type="button"
|
||||
hidden
|
||||
style="background: none; border: none; color: #9ca3af; cursor: pointer; padding: 0 4px"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button style="font-weight: 600" class="invite-btn" id="searchButton" type="button" disabled>Search</button>
|
||||
</div>
|
||||
|
||||
<div id="resultList" style="margin: 16px 0; display: flex; flex-direction: column; gap: 12px"></div>
|
||||
<div id="noData" class="no-data">please input user id</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="userCardTemplate">
|
||||
<div
|
||||
class="gradient-border-wrapper user-card"
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border-radius: 20px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 10px; transition: all 0.2s">
|
||||
<div class="status-badge" style="position: absolute; z-index: 3; top: -2px; right: -2px"></div>
|
||||
<div style="font-weight: 600">Information:</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px">
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px">
|
||||
<div style="width: 3em; display: flex; align-items: center; justify-content: center">
|
||||
<img class="avatar-image" src="" alt="" style="width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 50%" />
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch">
|
||||
<div class="nickname" style="margin-bottom: 4px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"></div>
|
||||
<div class="account" style="font-size: 0.9em; color: rgba(0, 0, 0, 0.4); font-weight: 500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: auto; min-width: 0; align-self: stretch; display: flex; justify-content: center; align-items: center">
|
||||
<div class="pending-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./yumi-invite-bd-leader.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
456
h5/yumi-invite-bd-leader/yumi-invite-bd-leader.css
Normal file
@ -0,0 +1,456 @@
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fullPage {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background-color: #fff;
|
||||
background-image: url("../assets/secondBg-CSqvFWr0-1776148661447.png");
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.fullPage input::placeholder {
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.invite-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.invite-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.result-action-btn {
|
||||
padding: 4px 10px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-action-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status-bar-placeholder {
|
||||
display: none;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.status-bar-placeholder.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.side-area,
|
||||
.actions-area {
|
||||
min-width: 1.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions-area {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.back-btn,
|
||||
.help-btn {
|
||||
width: 1.5em;
|
||||
border: none;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.back-btn:active,
|
||||
.help-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.language-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #f59e0b !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: var(--gradient-border-width, 1.5px);
|
||||
background: linear-gradient(
|
||||
var(--gradient-direction, to bottom),
|
||||
var(--gradient-start-color, #ffc4c4),
|
||||
var(--gradient-end-color, #fff6a6)
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gradient-border-content {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(
|
||||
--gradient-background,
|
||||
linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%)
|
||||
);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 20px 25px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px rgba(0, 0, 0, 0.04);
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 24px 20px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: iconPulse 0.6s ease-out;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.modal-error .icon-circle-error {
|
||||
background-color: #fee2e2;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.modal-error .icon-symbol-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-error .modal-btn-error {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-warning .icon-circle-warning {
|
||||
background-color: #fef3c7;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.modal-warning .icon-symbol-warning {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.modal-warning .modal-btn-warning {
|
||||
background-color: #d97706;
|
||||
}
|
||||
|
||||
.modal-success .icon-circle-success {
|
||||
background-color: #d1fae5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
.modal-success .icon-symbol-success {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.modal-success .modal-btn-success {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.modal-info .icon-circle-info {
|
||||
background-color: #dbeafe;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.modal-info .icon-symbol-info {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-info .modal-btn-info {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-simple .modal-message {
|
||||
font-size: 15px;
|
||||
color: #374151;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-simple .modal-btn {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-enter {
|
||||
animation: modalEnter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.modal-leave {
|
||||
animation: modalLeave 0.2s ease-in;
|
||||
}
|
||||
|
||||
[dir="rtl"] .flip-img {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
[dir="rtl"] .avatar {
|
||||
margin-right: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .user-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-entry,
|
||||
[dir="rtl"] .header-title {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
@keyframes modalEnter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.7) translateY(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalLeave {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.fullPage {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.fullPage {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal {
|
||||
max-width: 280px;
|
||||
padding: 20px 16px 16px;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
699
h5/yumi-invite-bd-leader/yumi-invite-bd-leader.js
Normal file
@ -0,0 +1,699 @@
|
||||
(function () {
|
||||
const API_BASE = "https://jvapi.haiyihy.com";
|
||||
const BACK_ICON =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
|
||||
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png";
|
||||
const ENDPOINTS = {
|
||||
invitedList: "/team/bd/invite-bdleader-message-own",
|
||||
invite: "/team/bd/invite-bdleader",
|
||||
cancelPrefix: "/team/bd/invite-bdleader-cancel/",
|
||||
};
|
||||
|
||||
const TEXT = {
|
||||
pageTitle: "Invite User To Become BD Leader",
|
||||
language: "English",
|
||||
placeholder: "Enter User ID",
|
||||
search: "Search",
|
||||
invite: "Invite",
|
||||
idlePrompt: "please input user id",
|
||||
noUsersFound: "No users found",
|
||||
information: "Information:",
|
||||
userIdPrefix: "User ID:",
|
||||
pending: "Pending",
|
||||
success: "Success",
|
||||
cancel: "Cancel",
|
||||
invitationSubmitted: "Invitation submitted",
|
||||
applicationCancelled: "Application cancelled",
|
||||
somethingWentWrong: "Something went wrong",
|
||||
cancellationFailed: "Cancellation failed, try again",
|
||||
informationTitle: "Information",
|
||||
successTitle: "Success",
|
||||
errorTitle: "Error",
|
||||
ok: "OK",
|
||||
};
|
||||
|
||||
const state = {
|
||||
query: "",
|
||||
invitedList: [],
|
||||
searchResults: [],
|
||||
showSearchResults: false,
|
||||
searching: false,
|
||||
hasSearched: false,
|
||||
};
|
||||
|
||||
const dom = {
|
||||
statusBarPlaceholder: document.getElementById("statusBarPlaceholder"),
|
||||
backButton: document.getElementById("backButton"),
|
||||
backButtonIcon: document.getElementById("backButtonIcon"),
|
||||
pageTitle: document.getElementById("pageTitle"),
|
||||
languageEntry: document.getElementById("languageEntry"),
|
||||
languageName: document.getElementById("languageName"),
|
||||
searchInput: document.getElementById("searchInput"),
|
||||
clearButton: document.getElementById("clearButton"),
|
||||
searchButton: document.getElementById("searchButton"),
|
||||
resultList: document.getElementById("resultList"),
|
||||
noData: document.getElementById("noData"),
|
||||
userCardTemplate: document.getElementById("userCardTemplate"),
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
document.title = TEXT.pageTitle;
|
||||
document.documentElement.lang = "en";
|
||||
document.documentElement.dir = resolveDocumentDirection();
|
||||
|
||||
dom.backButtonIcon.src = BACK_ICON;
|
||||
dom.pageTitle.textContent = TEXT.pageTitle;
|
||||
dom.languageName.textContent = TEXT.language;
|
||||
dom.searchInput.placeholder = TEXT.placeholder;
|
||||
dom.searchButton.textContent = TEXT.search;
|
||||
dom.noData.textContent = TEXT.idlePrompt;
|
||||
|
||||
syncStatusBarPlaceholder();
|
||||
bindEvents();
|
||||
render();
|
||||
loadInvitedList();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
dom.backButton.addEventListener("click", handleBack);
|
||||
dom.languageEntry.addEventListener("click", handleLanguageClick);
|
||||
dom.searchInput.addEventListener("input", handleSearchInput);
|
||||
dom.clearButton.addEventListener("click", clearSearch);
|
||||
dom.searchButton.addEventListener("click", triggerSearchNow);
|
||||
}
|
||||
|
||||
function resolveDocumentDirection() {
|
||||
const lang = (new URLSearchParams(window.location.search).get("lang") || "").toLowerCase();
|
||||
return lang.startsWith("ar") ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
function syncStatusBarPlaceholder() {
|
||||
const hasAppBridge =
|
||||
Boolean(window.app) ||
|
||||
Boolean(window.FlutterPageControl) ||
|
||||
Boolean(window.webkit && window.webkit.messageHandlers);
|
||||
const isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent || "");
|
||||
dom.statusBarPlaceholder.classList.toggle("is-visible", hasAppBridge && isIOS);
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
function handleLanguageClick() {
|
||||
window.location.href = "/language";
|
||||
}
|
||||
|
||||
function handleSearchInput(event) {
|
||||
state.query = event.target.value;
|
||||
dom.clearButton.hidden = !state.query;
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
state.query = "";
|
||||
state.searchResults = [];
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
dom.searchInput.value = "";
|
||||
dom.clearButton.hidden = true;
|
||||
render();
|
||||
}
|
||||
|
||||
async function triggerSearchNow() {
|
||||
if (state.searching) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = state.query.trim();
|
||||
|
||||
if (!query) {
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.searching = true;
|
||||
state.showSearchResults = true;
|
||||
state.hasSearched = true;
|
||||
state.searchResults = [];
|
||||
render();
|
||||
|
||||
try {
|
||||
const payload = await searchUser(query);
|
||||
state.searchResults = payload ? [mergeWithInvitationStatus(payload.userProfile)] : [];
|
||||
} catch (error) {
|
||||
state.searchResults = [];
|
||||
showToast(extractErrorMessage(error), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
state.searching = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInvitedList() {
|
||||
try {
|
||||
const response = await request("GET", ENDPOINTS.invitedList);
|
||||
state.invitedList = response && response.status && Array.isArray(response.body) ? response.body : [];
|
||||
state.searchResults = state.searchResults.map(function (item) {
|
||||
return mergeWithInvitationStatus(item.userProfile || {});
|
||||
});
|
||||
render();
|
||||
} catch (error) {
|
||||
state.invitedList = [];
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function searchUser(query) {
|
||||
try {
|
||||
const response = await request(
|
||||
"GET",
|
||||
"/user/user-profile/open-search?account=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (response && response.status && response.body) {
|
||||
return { userProfile: response.body };
|
||||
}
|
||||
} catch (error) {
|
||||
if (!/^\d+$/.test(query)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackResponse = await request(
|
||||
"GET",
|
||||
"/user/user-profile?userId=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (fallbackResponse && fallbackResponse.status && fallbackResponse.body) {
|
||||
return { userProfile: fallbackResponse.body };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeWithInvitationStatus(userProfile) {
|
||||
const matchedInvitation = state.invitedList.find(function (item) {
|
||||
return (
|
||||
item &&
|
||||
item.userProfile &&
|
||||
userProfile &&
|
||||
item.userProfile.id === userProfile.id
|
||||
);
|
||||
});
|
||||
|
||||
return Object.assign({}, matchedInvitation || {}, {
|
||||
userProfile: userProfile || {},
|
||||
});
|
||||
}
|
||||
|
||||
async function inviteUser(item) {
|
||||
if (!item || !item.userProfile || !item.userProfile.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request("POST", ENDPOINTS.invite, {
|
||||
body: {
|
||||
inviteUserId: item.userProfile.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.invitationSubmitted, {
|
||||
title: TEXT.successTitle,
|
||||
type: "success",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.somethingWentWrong), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelInvitation(invitationId) {
|
||||
try {
|
||||
const response = await request("POST", ENDPOINTS.cancelPrefix + invitationId);
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.applicationCancelled, {
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.cancellationFailed), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderSearchButton();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function renderSearchButton() {
|
||||
dom.searchButton.disabled = !state.query.trim() || state.searching;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const currentList = state.showSearchResults ? state.searchResults : state.invitedList;
|
||||
dom.resultList.innerHTML = "";
|
||||
|
||||
currentList.forEach(function (item) {
|
||||
dom.resultList.appendChild(createUserCard(item, state.showSearchResults ? "search" : "invited"));
|
||||
});
|
||||
|
||||
if (currentList.length > 0 || (state.searching && state.showSearchResults)) {
|
||||
dom.noData.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
dom.noData.hidden = false;
|
||||
dom.noData.textContent =
|
||||
state.showSearchResults && state.hasSearched ? TEXT.noUsersFound : TEXT.idlePrompt;
|
||||
}
|
||||
|
||||
function createUserCard(item, mode) {
|
||||
const fragment = dom.userCardTemplate.content.cloneNode(true);
|
||||
const statusBadge = fragment.querySelector(".status-badge");
|
||||
const avatarImage = fragment.querySelector(".avatar-image");
|
||||
const nickname = fragment.querySelector(".nickname");
|
||||
const account = fragment.querySelector(".account");
|
||||
const pendingActions = fragment.querySelector(".pending-actions");
|
||||
const userProfile = item.userProfile || {};
|
||||
|
||||
avatarImage.src = userProfile.userAvatar || DEFAULT_AVATAR;
|
||||
avatarImage.alt = userProfile.userNickname || "";
|
||||
avatarImage.onerror = handleAvatarError;
|
||||
nickname.textContent = userProfile.userNickname || "";
|
||||
account.textContent = TEXT.userIdPrefix + " " + (userProfile.account || userProfile.id || "");
|
||||
|
||||
if (item.status === 0) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #759cff 5.66%, #3d73ff 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.pending +
|
||||
"</div>";
|
||||
} else if (item.status === 1) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #75ff98 5.66%, #3dff54 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.success +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
if (mode === "search") {
|
||||
const inviteButton = document.createElement("button");
|
||||
inviteButton.type = "button";
|
||||
inviteButton.className = "result-action-btn";
|
||||
inviteButton.textContent = TEXT.invite;
|
||||
inviteButton.disabled = item.status === 0 || item.status === 1;
|
||||
inviteButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
inviteUser(item);
|
||||
});
|
||||
pendingActions.appendChild(inviteButton);
|
||||
} else if (item.status === 0) {
|
||||
const cancelButton = document.createElement("div");
|
||||
cancelButton.textContent = TEXT.cancel;
|
||||
cancelButton.style.padding = "4px 8px";
|
||||
cancelButton.style.borderRadius = "32px";
|
||||
cancelButton.style.backgroundColor = "red";
|
||||
cancelButton.style.color = "white";
|
||||
cancelButton.style.fontWeight = "600";
|
||||
cancelButton.style.zIndex = "5";
|
||||
cancelButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
cancelInvitation(item.id);
|
||||
});
|
||||
pendingActions.appendChild(cancelButton);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function handleAvatarError(event) {
|
||||
event.target.onerror = null;
|
||||
event.target.src = DEFAULT_AVATAR;
|
||||
}
|
||||
|
||||
function extractErrorMessage(error, fallback) {
|
||||
return (
|
||||
(error &&
|
||||
error.response &&
|
||||
(error.response.errorMsg || error.response.message || error.response.error)) ||
|
||||
(error && error.message) ||
|
||||
fallback ||
|
||||
TEXT.somethingWentWrong
|
||||
);
|
||||
}
|
||||
|
||||
function showToast(message, options) {
|
||||
const config = Object.assign(
|
||||
{
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
confirmText: TEXT.ok,
|
||||
},
|
||||
options || {}
|
||||
);
|
||||
|
||||
const iconMap = {
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
success: "✓",
|
||||
info: "i",
|
||||
simple: "",
|
||||
};
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "modal-overlay";
|
||||
overlay.innerHTML =
|
||||
'<div class="modal modal-enter modal-' +
|
||||
config.type +
|
||||
'">' +
|
||||
(config.type === "simple"
|
||||
? ""
|
||||
: '<div class="modal-icon"><div class="icon-circle icon-circle-' +
|
||||
config.type +
|
||||
'"><span class="icon-symbol icon-symbol-' +
|
||||
config.type +
|
||||
'">' +
|
||||
iconMap[config.type] +
|
||||
"</span></div></div>") +
|
||||
(config.type === "simple" || !config.title
|
||||
? ""
|
||||
: '<div class="modal-title">' + escapeHtml(config.title) + "</div>") +
|
||||
'<div class="modal-message">' +
|
||||
escapeHtml(message) +
|
||||
"</div>" +
|
||||
'<div class="modal-actions"><button class="modal-btn modal-btn-' +
|
||||
config.type +
|
||||
'">' +
|
||||
escapeHtml(config.confirmText) +
|
||||
"</button></div>" +
|
||||
"</div>";
|
||||
|
||||
const modal = overlay.querySelector(".modal");
|
||||
const closeButton = overlay.querySelector(".modal-btn");
|
||||
const close = function () {
|
||||
modal.classList.remove("modal-enter");
|
||||
modal.classList.add("modal-leave");
|
||||
window.setTimeout(function () {
|
||||
overlay.remove();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
closeButton.addEventListener("click", close);
|
||||
overlay.addEventListener("click", function (event) {
|
||||
if (event.target === overlay) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
async function request(method, path, options) {
|
||||
const config = options || {};
|
||||
const headers = buildRequestHeaders(Boolean(config.body), config.headers || {});
|
||||
const response = await fetch(API_BASE + path, {
|
||||
method: method,
|
||||
credentials: "include",
|
||||
headers: headers,
|
||||
body: config.body ? JSON.stringify(config.body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = {};
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch (error) {
|
||||
payload = {
|
||||
status: response.ok,
|
||||
body: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || payload.status === false) {
|
||||
const requestError = new Error(
|
||||
payload.errorMsg || payload.message || response.statusText || TEXT.somethingWentWrong
|
||||
);
|
||||
requestError.response = payload;
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildRequestHeaders(hasBody, overrideHeaders) {
|
||||
const discovered = discoverAppHeaders();
|
||||
const headers = Object.assign({}, overrideHeaders || {});
|
||||
|
||||
if (hasBody) {
|
||||
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
||||
}
|
||||
|
||||
headers.Accept = headers.Accept || "application/json, text/plain, */*";
|
||||
|
||||
if (discovered.authorization) {
|
||||
headers.Authorization = headers.Authorization || discovered.authorization;
|
||||
}
|
||||
|
||||
if (discovered.reqLang) {
|
||||
headers["Req-Lang"] = headers["Req-Lang"] || discovered.reqLang;
|
||||
}
|
||||
|
||||
if (discovered.appVersion) {
|
||||
headers["App-Version"] = headers["App-Version"] || discovered.appVersion;
|
||||
}
|
||||
|
||||
if (discovered.buildVersion) {
|
||||
headers["Build-Version"] = headers["Build-Version"] || discovered.buildVersion;
|
||||
}
|
||||
|
||||
if (discovered.appChannel) {
|
||||
headers["App-Channel"] = headers["App-Channel"] || discovered.appChannel;
|
||||
}
|
||||
|
||||
if (discovered.reqImei) {
|
||||
headers["Req-Imei"] = headers["Req-Imei"] || discovered.reqImei;
|
||||
}
|
||||
|
||||
if (discovered.sysOrigin) {
|
||||
headers["Sys-Origin"] = headers["Sys-Origin"] || discovered.sysOrigin;
|
||||
}
|
||||
|
||||
if (discovered.sysOriginChild) {
|
||||
headers["Sys-Origin-Child"] =
|
||||
headers["Sys-Origin-Child"] || discovered.sysOriginChild;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function discoverAppHeaders() {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const collected = [];
|
||||
|
||||
collectCandidate(collected, window.__APP_HEADERS__);
|
||||
collectCandidate(collected, window.__HEADER_INFO__);
|
||||
collectCandidate(collected, window.headerInfo);
|
||||
collectCandidate(collected, parseHeaderObjectFromQuery(query));
|
||||
|
||||
collectFromStorage(collected, window.localStorage);
|
||||
collectFromStorage(collected, window.sessionStorage);
|
||||
|
||||
return collected.reduce(function (result, candidate) {
|
||||
return Object.assign(result, candidate);
|
||||
}, {
|
||||
reqLang: query.get("lang") || document.documentElement.lang || "en",
|
||||
});
|
||||
}
|
||||
|
||||
function parseHeaderObjectFromQuery(query) {
|
||||
const candidate = {};
|
||||
mapHeaderValue(candidate, "authorization", query.get("authorization"));
|
||||
mapHeaderValue(candidate, "authorization", query.get("token"));
|
||||
mapHeaderValue(candidate, "reqLang", query.get("lang"));
|
||||
mapHeaderValue(candidate, "appVersion", query.get("appVersion"));
|
||||
mapHeaderValue(candidate, "buildVersion", query.get("buildVersion"));
|
||||
mapHeaderValue(candidate, "appChannel", query.get("appChannel"));
|
||||
mapHeaderValue(candidate, "reqImei", query.get("reqImei"));
|
||||
mapHeaderValue(candidate, "sysOrigin", query.get("sysOrigin"));
|
||||
mapHeaderValue(candidate, "sysOriginChild", query.get("sysOriginChild"));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function collectFromStorage(collected, storage) {
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferredKeys = [
|
||||
"authorization",
|
||||
"Authorization",
|
||||
"token",
|
||||
"accessToken",
|
||||
"headerInfo",
|
||||
"headersFromApp",
|
||||
"appHeaders",
|
||||
"persist:root",
|
||||
"user",
|
||||
"userInfo",
|
||||
];
|
||||
|
||||
preferredKeys.forEach(function (key) {
|
||||
try {
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index < storage.length; index += 1) {
|
||||
try {
|
||||
const key = storage.key(index);
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectCandidate(collected, value) {
|
||||
const normalized = normalizeHeaderCandidate(value, 0);
|
||||
if (normalized && Object.keys(normalized).length > 0) {
|
||||
collected.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHeaderCandidate(value, depth) {
|
||||
if (depth > 4 || value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed[0] === "{" || trimmed[0] === "[") {
|
||||
try {
|
||||
return normalizeHeaderCandidate(JSON.parse(trimmed), depth + 1);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (/bearer\\s+/i.test(trimmed)) {
|
||||
return {
|
||||
authorization: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce(function (result, entry) {
|
||||
const normalized = normalizeHeaderCandidate(entry, depth + 1);
|
||||
return normalized ? Object.assign(result, normalized) : result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
const aliasMap = {
|
||||
authorization: ["authorization", "Authorization", "token", "accessToken"],
|
||||
reqLang: ["reqLang", "Req-Lang", "lang", "language"],
|
||||
appVersion: ["appVersion", "App-Version"],
|
||||
buildVersion: ["buildVersion", "Build-Version"],
|
||||
appChannel: ["appChannel", "App-Channel"],
|
||||
reqImei: ["reqImei", "Req-Imei"],
|
||||
sysOrigin: ["sysOrigin", "Sys-Origin"],
|
||||
sysOriginChild: ["sysOriginChild", "Sys-Origin-Child"],
|
||||
};
|
||||
|
||||
Object.keys(aliasMap).forEach(function (targetKey) {
|
||||
aliasMap[targetKey].forEach(function (sourceKey) {
|
||||
mapHeaderValue(normalized, targetKey, value[sourceKey]);
|
||||
});
|
||||
});
|
||||
|
||||
["headerInfo", "headers", "auth", "data", "state"].forEach(function (nestedKey) {
|
||||
const nestedValue = normalizeHeaderCandidate(value[nestedKey], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
if (
|
||||
key === "headerInfo" ||
|
||||
key === "headers" ||
|
||||
key === "auth" ||
|
||||
key === "data" ||
|
||||
key === "state"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value[key] === "object") {
|
||||
const nestedValue = normalizeHeaderCandidate(value[key], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mapHeaderValue(target, key, value) {
|
||||
if (value == null || value === "" || target[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
target[key] = String(value);
|
||||
}
|
||||
})();
|
||||
119
h5/yumi-invite-bd/index.html
Normal file
@ -0,0 +1,119 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Invite User To Become BD</title>
|
||||
<link rel="stylesheet" href="./yumi-invite-bd.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="fullPage" id="pageRoot">
|
||||
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div>
|
||||
<div class="header-container">
|
||||
<div class="side-area">
|
||||
<button class="back-btn" id="backButton" type="button">
|
||||
<img class="flip-img" id="backButtonIcon" src="" alt="" style="width: 100%; aspect-ratio: 1 / 1; display: block" />
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="header-title" id="pageTitle" style="color: black">Invite User To Become BD</h1>
|
||||
<div class="actions-area">
|
||||
<div class="language-entry" id="languageEntry">
|
||||
<div class="language-name" id="languageName">English</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; position: relative">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
||||
<div
|
||||
class="gradient-border-wrapper"
|
||||
style="
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 4px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 0 8px; display: flex; align-items: center; height: max-content">
|
||||
<input
|
||||
id="searchInput"
|
||||
type="text"
|
||||
placeholder="Enter User ID"
|
||||
autocomplete="off"
|
||||
style="
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 8px;
|
||||
"
|
||||
/>
|
||||
<button
|
||||
id="clearButton"
|
||||
type="button"
|
||||
hidden
|
||||
style="background: none; border: none; color: #9ca3af; cursor: pointer; padding: 0 4px"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button style="font-weight: 600" class="invite-btn" id="searchButton" type="button" disabled>Search</button>
|
||||
</div>
|
||||
|
||||
<div id="resultList" style="margin: 16px 0; display: flex; flex-direction: column; gap: 12px"></div>
|
||||
<div id="noData" class="no-data">please input user id</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="userCardTemplate">
|
||||
<div
|
||||
class="gradient-border-wrapper user-card"
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border-radius: 20px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 10px; transition: all 0.2s">
|
||||
<div class="status-badge" style="position: absolute; z-index: 3; top: -2px; right: -2px"></div>
|
||||
<div style="font-weight: 600">Information:</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px">
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px">
|
||||
<div style="width: 3em; display: flex; align-items: center; justify-content: center">
|
||||
<img class="avatar-image" src="" alt="" style="width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 50%" />
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch">
|
||||
<div class="nickname" style="margin-bottom: 4px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"></div>
|
||||
<div class="account" style="font-size: 0.9em; color: rgba(0, 0, 0, 0.4); font-weight: 500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: auto; min-width: 0; align-self: stretch; display: flex; justify-content: center; align-items: center">
|
||||
<div class="pending-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./yumi-invite-bd.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
456
h5/yumi-invite-bd/yumi-invite-bd.css
Normal file
@ -0,0 +1,456 @@
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fullPage {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background-color: #fff;
|
||||
background-image: url("../assets/secondBg-CSqvFWr0-1776148661447.png");
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.fullPage input::placeholder {
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.invite-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.invite-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.result-action-btn {
|
||||
padding: 4px 10px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-action-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status-bar-placeholder {
|
||||
display: none;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.status-bar-placeholder.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.side-area,
|
||||
.actions-area {
|
||||
min-width: 1.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions-area {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.back-btn,
|
||||
.help-btn {
|
||||
width: 1.5em;
|
||||
border: none;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.back-btn:active,
|
||||
.help-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.language-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #f59e0b !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: var(--gradient-border-width, 1.5px);
|
||||
background: linear-gradient(
|
||||
var(--gradient-direction, to bottom),
|
||||
var(--gradient-start-color, #ffc4c4),
|
||||
var(--gradient-end-color, #fff6a6)
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gradient-border-content {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(
|
||||
--gradient-background,
|
||||
linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%)
|
||||
);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 20px 25px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px rgba(0, 0, 0, 0.04);
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 24px 20px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: iconPulse 0.6s ease-out;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.modal-error .icon-circle-error {
|
||||
background-color: #fee2e2;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.modal-error .icon-symbol-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-error .modal-btn-error {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-warning .icon-circle-warning {
|
||||
background-color: #fef3c7;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.modal-warning .icon-symbol-warning {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.modal-warning .modal-btn-warning {
|
||||
background-color: #d97706;
|
||||
}
|
||||
|
||||
.modal-success .icon-circle-success {
|
||||
background-color: #d1fae5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
.modal-success .icon-symbol-success {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.modal-success .modal-btn-success {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.modal-info .icon-circle-info {
|
||||
background-color: #dbeafe;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.modal-info .icon-symbol-info {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-info .modal-btn-info {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-simple .modal-message {
|
||||
font-size: 15px;
|
||||
color: #374151;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-simple .modal-btn {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-enter {
|
||||
animation: modalEnter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.modal-leave {
|
||||
animation: modalLeave 0.2s ease-in;
|
||||
}
|
||||
|
||||
[dir="rtl"] .flip-img {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
[dir="rtl"] .avatar {
|
||||
margin-right: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .user-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-entry,
|
||||
[dir="rtl"] .header-title {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
@keyframes modalEnter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.7) translateY(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalLeave {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.fullPage {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.fullPage {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal {
|
||||
max-width: 280px;
|
||||
padding: 20px 16px 16px;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
699
h5/yumi-invite-bd/yumi-invite-bd.js
Normal file
@ -0,0 +1,699 @@
|
||||
(function () {
|
||||
const API_BASE = "https://jvapi.haiyihy.com";
|
||||
const BACK_ICON =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
|
||||
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png";
|
||||
const ENDPOINTS = {
|
||||
invitedList: "/team/bd/invite-bd-message-own",
|
||||
invite: "/team/bd/invite-bd",
|
||||
cancelPrefix: "/team/bd/invite-bd-cancel/",
|
||||
};
|
||||
|
||||
const TEXT = {
|
||||
pageTitle: "Invite User To Become BD",
|
||||
language: "English",
|
||||
placeholder: "Enter User ID",
|
||||
search: "Search",
|
||||
invite: "Invite",
|
||||
idlePrompt: "please input user id",
|
||||
noUsersFound: "No users found",
|
||||
information: "Information:",
|
||||
userIdPrefix: "User ID:",
|
||||
pending: "Pending",
|
||||
success: "Success",
|
||||
cancel: "Cancel",
|
||||
invitationSubmitted: "Invitation submitted",
|
||||
applicationCancelled: "Application cancelled",
|
||||
somethingWentWrong: "Something went wrong",
|
||||
cancellationFailed: "Cancellation failed, try again",
|
||||
informationTitle: "Information",
|
||||
successTitle: "Success",
|
||||
errorTitle: "Error",
|
||||
ok: "OK",
|
||||
};
|
||||
|
||||
const state = {
|
||||
query: "",
|
||||
invitedList: [],
|
||||
searchResults: [],
|
||||
showSearchResults: false,
|
||||
searching: false,
|
||||
hasSearched: false,
|
||||
};
|
||||
|
||||
const dom = {
|
||||
statusBarPlaceholder: document.getElementById("statusBarPlaceholder"),
|
||||
backButton: document.getElementById("backButton"),
|
||||
backButtonIcon: document.getElementById("backButtonIcon"),
|
||||
pageTitle: document.getElementById("pageTitle"),
|
||||
languageEntry: document.getElementById("languageEntry"),
|
||||
languageName: document.getElementById("languageName"),
|
||||
searchInput: document.getElementById("searchInput"),
|
||||
clearButton: document.getElementById("clearButton"),
|
||||
searchButton: document.getElementById("searchButton"),
|
||||
resultList: document.getElementById("resultList"),
|
||||
noData: document.getElementById("noData"),
|
||||
userCardTemplate: document.getElementById("userCardTemplate"),
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
document.title = TEXT.pageTitle;
|
||||
document.documentElement.lang = "en";
|
||||
document.documentElement.dir = resolveDocumentDirection();
|
||||
|
||||
dom.backButtonIcon.src = BACK_ICON;
|
||||
dom.pageTitle.textContent = TEXT.pageTitle;
|
||||
dom.languageName.textContent = TEXT.language;
|
||||
dom.searchInput.placeholder = TEXT.placeholder;
|
||||
dom.searchButton.textContent = TEXT.search;
|
||||
dom.noData.textContent = TEXT.idlePrompt;
|
||||
|
||||
syncStatusBarPlaceholder();
|
||||
bindEvents();
|
||||
render();
|
||||
loadInvitedList();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
dom.backButton.addEventListener("click", handleBack);
|
||||
dom.languageEntry.addEventListener("click", handleLanguageClick);
|
||||
dom.searchInput.addEventListener("input", handleSearchInput);
|
||||
dom.clearButton.addEventListener("click", clearSearch);
|
||||
dom.searchButton.addEventListener("click", triggerSearchNow);
|
||||
}
|
||||
|
||||
function resolveDocumentDirection() {
|
||||
const lang = (new URLSearchParams(window.location.search).get("lang") || "").toLowerCase();
|
||||
return lang.startsWith("ar") ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
function syncStatusBarPlaceholder() {
|
||||
const hasAppBridge =
|
||||
Boolean(window.app) ||
|
||||
Boolean(window.FlutterPageControl) ||
|
||||
Boolean(window.webkit && window.webkit.messageHandlers);
|
||||
const isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent || "");
|
||||
dom.statusBarPlaceholder.classList.toggle("is-visible", hasAppBridge && isIOS);
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
function handleLanguageClick() {
|
||||
window.location.href = "/language";
|
||||
}
|
||||
|
||||
function handleSearchInput(event) {
|
||||
state.query = event.target.value;
|
||||
dom.clearButton.hidden = !state.query;
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
state.query = "";
|
||||
state.searchResults = [];
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
dom.searchInput.value = "";
|
||||
dom.clearButton.hidden = true;
|
||||
render();
|
||||
}
|
||||
|
||||
async function triggerSearchNow() {
|
||||
if (state.searching) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = state.query.trim();
|
||||
|
||||
if (!query) {
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.searching = true;
|
||||
state.showSearchResults = true;
|
||||
state.hasSearched = true;
|
||||
state.searchResults = [];
|
||||
render();
|
||||
|
||||
try {
|
||||
const payload = await searchUser(query);
|
||||
state.searchResults = payload ? [mergeWithInvitationStatus(payload.userProfile)] : [];
|
||||
} catch (error) {
|
||||
state.searchResults = [];
|
||||
showToast(extractErrorMessage(error), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
state.searching = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInvitedList() {
|
||||
try {
|
||||
const response = await request("GET", ENDPOINTS.invitedList);
|
||||
state.invitedList = response && response.status && Array.isArray(response.body) ? response.body : [];
|
||||
state.searchResults = state.searchResults.map(function (item) {
|
||||
return mergeWithInvitationStatus(item.userProfile || {});
|
||||
});
|
||||
render();
|
||||
} catch (error) {
|
||||
state.invitedList = [];
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function searchUser(query) {
|
||||
try {
|
||||
const response = await request(
|
||||
"GET",
|
||||
"/user/user-profile/open-search?account=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (response && response.status && response.body) {
|
||||
return { userProfile: response.body };
|
||||
}
|
||||
} catch (error) {
|
||||
if (!/^\d+$/.test(query)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackResponse = await request(
|
||||
"GET",
|
||||
"/user/user-profile?userId=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (fallbackResponse && fallbackResponse.status && fallbackResponse.body) {
|
||||
return { userProfile: fallbackResponse.body };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeWithInvitationStatus(userProfile) {
|
||||
const matchedInvitation = state.invitedList.find(function (item) {
|
||||
return (
|
||||
item &&
|
||||
item.userProfile &&
|
||||
userProfile &&
|
||||
item.userProfile.id === userProfile.id
|
||||
);
|
||||
});
|
||||
|
||||
return Object.assign({}, matchedInvitation || {}, {
|
||||
userProfile: userProfile || {},
|
||||
});
|
||||
}
|
||||
|
||||
async function inviteUser(item) {
|
||||
if (!item || !item.userProfile || !item.userProfile.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request("POST", ENDPOINTS.invite, {
|
||||
body: {
|
||||
inviteUserId: item.userProfile.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.invitationSubmitted, {
|
||||
title: TEXT.successTitle,
|
||||
type: "success",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.somethingWentWrong), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelInvitation(invitationId) {
|
||||
try {
|
||||
const response = await request("POST", ENDPOINTS.cancelPrefix + invitationId);
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.applicationCancelled, {
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.cancellationFailed), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderSearchButton();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function renderSearchButton() {
|
||||
dom.searchButton.disabled = !state.query.trim() || state.searching;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const currentList = state.showSearchResults ? state.searchResults : state.invitedList;
|
||||
dom.resultList.innerHTML = "";
|
||||
|
||||
currentList.forEach(function (item) {
|
||||
dom.resultList.appendChild(createUserCard(item, state.showSearchResults ? "search" : "invited"));
|
||||
});
|
||||
|
||||
if (currentList.length > 0 || (state.searching && state.showSearchResults)) {
|
||||
dom.noData.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
dom.noData.hidden = false;
|
||||
dom.noData.textContent =
|
||||
state.showSearchResults && state.hasSearched ? TEXT.noUsersFound : TEXT.idlePrompt;
|
||||
}
|
||||
|
||||
function createUserCard(item, mode) {
|
||||
const fragment = dom.userCardTemplate.content.cloneNode(true);
|
||||
const statusBadge = fragment.querySelector(".status-badge");
|
||||
const avatarImage = fragment.querySelector(".avatar-image");
|
||||
const nickname = fragment.querySelector(".nickname");
|
||||
const account = fragment.querySelector(".account");
|
||||
const pendingActions = fragment.querySelector(".pending-actions");
|
||||
const userProfile = item.userProfile || {};
|
||||
|
||||
avatarImage.src = userProfile.userAvatar || DEFAULT_AVATAR;
|
||||
avatarImage.alt = userProfile.userNickname || "";
|
||||
avatarImage.onerror = handleAvatarError;
|
||||
nickname.textContent = userProfile.userNickname || "";
|
||||
account.textContent = TEXT.userIdPrefix + " " + (userProfile.account || userProfile.id || "");
|
||||
|
||||
if (item.status === 0) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #759cff 5.66%, #3d73ff 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.pending +
|
||||
"</div>";
|
||||
} else if (item.status === 1) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #75ff98 5.66%, #3dff54 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.success +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
if (mode === "search") {
|
||||
const inviteButton = document.createElement("button");
|
||||
inviteButton.type = "button";
|
||||
inviteButton.className = "result-action-btn";
|
||||
inviteButton.textContent = TEXT.invite;
|
||||
inviteButton.disabled = item.status === 0 || item.status === 1;
|
||||
inviteButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
inviteUser(item);
|
||||
});
|
||||
pendingActions.appendChild(inviteButton);
|
||||
} else if (item.status === 0) {
|
||||
const cancelButton = document.createElement("div");
|
||||
cancelButton.textContent = TEXT.cancel;
|
||||
cancelButton.style.padding = "4px 8px";
|
||||
cancelButton.style.borderRadius = "32px";
|
||||
cancelButton.style.backgroundColor = "red";
|
||||
cancelButton.style.color = "white";
|
||||
cancelButton.style.fontWeight = "600";
|
||||
cancelButton.style.zIndex = "5";
|
||||
cancelButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
cancelInvitation(item.id);
|
||||
});
|
||||
pendingActions.appendChild(cancelButton);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function handleAvatarError(event) {
|
||||
event.target.onerror = null;
|
||||
event.target.src = DEFAULT_AVATAR;
|
||||
}
|
||||
|
||||
function extractErrorMessage(error, fallback) {
|
||||
return (
|
||||
(error &&
|
||||
error.response &&
|
||||
(error.response.errorMsg || error.response.message || error.response.error)) ||
|
||||
(error && error.message) ||
|
||||
fallback ||
|
||||
TEXT.somethingWentWrong
|
||||
);
|
||||
}
|
||||
|
||||
function showToast(message, options) {
|
||||
const config = Object.assign(
|
||||
{
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
confirmText: TEXT.ok,
|
||||
},
|
||||
options || {}
|
||||
);
|
||||
|
||||
const iconMap = {
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
success: "✓",
|
||||
info: "i",
|
||||
simple: "",
|
||||
};
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "modal-overlay";
|
||||
overlay.innerHTML =
|
||||
'<div class="modal modal-enter modal-' +
|
||||
config.type +
|
||||
'">' +
|
||||
(config.type === "simple"
|
||||
? ""
|
||||
: '<div class="modal-icon"><div class="icon-circle icon-circle-' +
|
||||
config.type +
|
||||
'"><span class="icon-symbol icon-symbol-' +
|
||||
config.type +
|
||||
'">' +
|
||||
iconMap[config.type] +
|
||||
"</span></div></div>") +
|
||||
(config.type === "simple" || !config.title
|
||||
? ""
|
||||
: '<div class="modal-title">' + escapeHtml(config.title) + "</div>") +
|
||||
'<div class="modal-message">' +
|
||||
escapeHtml(message) +
|
||||
"</div>" +
|
||||
'<div class="modal-actions"><button class="modal-btn modal-btn-' +
|
||||
config.type +
|
||||
'">' +
|
||||
escapeHtml(config.confirmText) +
|
||||
"</button></div>" +
|
||||
"</div>";
|
||||
|
||||
const modal = overlay.querySelector(".modal");
|
||||
const closeButton = overlay.querySelector(".modal-btn");
|
||||
const close = function () {
|
||||
modal.classList.remove("modal-enter");
|
||||
modal.classList.add("modal-leave");
|
||||
window.setTimeout(function () {
|
||||
overlay.remove();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
closeButton.addEventListener("click", close);
|
||||
overlay.addEventListener("click", function (event) {
|
||||
if (event.target === overlay) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
async function request(method, path, options) {
|
||||
const config = options || {};
|
||||
const headers = buildRequestHeaders(Boolean(config.body), config.headers || {});
|
||||
const response = await fetch(API_BASE + path, {
|
||||
method: method,
|
||||
credentials: "include",
|
||||
headers: headers,
|
||||
body: config.body ? JSON.stringify(config.body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = {};
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch (error) {
|
||||
payload = {
|
||||
status: response.ok,
|
||||
body: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || payload.status === false) {
|
||||
const requestError = new Error(
|
||||
payload.errorMsg || payload.message || response.statusText || TEXT.somethingWentWrong
|
||||
);
|
||||
requestError.response = payload;
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildRequestHeaders(hasBody, overrideHeaders) {
|
||||
const discovered = discoverAppHeaders();
|
||||
const headers = Object.assign({}, overrideHeaders || {});
|
||||
|
||||
if (hasBody) {
|
||||
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
||||
}
|
||||
|
||||
headers.Accept = headers.Accept || "application/json, text/plain, */*";
|
||||
|
||||
if (discovered.authorization) {
|
||||
headers.Authorization = headers.Authorization || discovered.authorization;
|
||||
}
|
||||
|
||||
if (discovered.reqLang) {
|
||||
headers["Req-Lang"] = headers["Req-Lang"] || discovered.reqLang;
|
||||
}
|
||||
|
||||
if (discovered.appVersion) {
|
||||
headers["App-Version"] = headers["App-Version"] || discovered.appVersion;
|
||||
}
|
||||
|
||||
if (discovered.buildVersion) {
|
||||
headers["Build-Version"] = headers["Build-Version"] || discovered.buildVersion;
|
||||
}
|
||||
|
||||
if (discovered.appChannel) {
|
||||
headers["App-Channel"] = headers["App-Channel"] || discovered.appChannel;
|
||||
}
|
||||
|
||||
if (discovered.reqImei) {
|
||||
headers["Req-Imei"] = headers["Req-Imei"] || discovered.reqImei;
|
||||
}
|
||||
|
||||
if (discovered.sysOrigin) {
|
||||
headers["Sys-Origin"] = headers["Sys-Origin"] || discovered.sysOrigin;
|
||||
}
|
||||
|
||||
if (discovered.sysOriginChild) {
|
||||
headers["Sys-Origin-Child"] =
|
||||
headers["Sys-Origin-Child"] || discovered.sysOriginChild;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function discoverAppHeaders() {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const collected = [];
|
||||
|
||||
collectCandidate(collected, window.__APP_HEADERS__);
|
||||
collectCandidate(collected, window.__HEADER_INFO__);
|
||||
collectCandidate(collected, window.headerInfo);
|
||||
collectCandidate(collected, parseHeaderObjectFromQuery(query));
|
||||
|
||||
collectFromStorage(collected, window.localStorage);
|
||||
collectFromStorage(collected, window.sessionStorage);
|
||||
|
||||
return collected.reduce(function (result, candidate) {
|
||||
return Object.assign(result, candidate);
|
||||
}, {
|
||||
reqLang: query.get("lang") || document.documentElement.lang || "en",
|
||||
});
|
||||
}
|
||||
|
||||
function parseHeaderObjectFromQuery(query) {
|
||||
const candidate = {};
|
||||
mapHeaderValue(candidate, "authorization", query.get("authorization"));
|
||||
mapHeaderValue(candidate, "authorization", query.get("token"));
|
||||
mapHeaderValue(candidate, "reqLang", query.get("lang"));
|
||||
mapHeaderValue(candidate, "appVersion", query.get("appVersion"));
|
||||
mapHeaderValue(candidate, "buildVersion", query.get("buildVersion"));
|
||||
mapHeaderValue(candidate, "appChannel", query.get("appChannel"));
|
||||
mapHeaderValue(candidate, "reqImei", query.get("reqImei"));
|
||||
mapHeaderValue(candidate, "sysOrigin", query.get("sysOrigin"));
|
||||
mapHeaderValue(candidate, "sysOriginChild", query.get("sysOriginChild"));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function collectFromStorage(collected, storage) {
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferredKeys = [
|
||||
"authorization",
|
||||
"Authorization",
|
||||
"token",
|
||||
"accessToken",
|
||||
"headerInfo",
|
||||
"headersFromApp",
|
||||
"appHeaders",
|
||||
"persist:root",
|
||||
"user",
|
||||
"userInfo",
|
||||
];
|
||||
|
||||
preferredKeys.forEach(function (key) {
|
||||
try {
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index < storage.length; index += 1) {
|
||||
try {
|
||||
const key = storage.key(index);
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectCandidate(collected, value) {
|
||||
const normalized = normalizeHeaderCandidate(value, 0);
|
||||
if (normalized && Object.keys(normalized).length > 0) {
|
||||
collected.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHeaderCandidate(value, depth) {
|
||||
if (depth > 4 || value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed[0] === "{" || trimmed[0] === "[") {
|
||||
try {
|
||||
return normalizeHeaderCandidate(JSON.parse(trimmed), depth + 1);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (/bearer\\s+/i.test(trimmed)) {
|
||||
return {
|
||||
authorization: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce(function (result, entry) {
|
||||
const normalized = normalizeHeaderCandidate(entry, depth + 1);
|
||||
return normalized ? Object.assign(result, normalized) : result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
const aliasMap = {
|
||||
authorization: ["authorization", "Authorization", "token", "accessToken"],
|
||||
reqLang: ["reqLang", "Req-Lang", "lang", "language"],
|
||||
appVersion: ["appVersion", "App-Version"],
|
||||
buildVersion: ["buildVersion", "Build-Version"],
|
||||
appChannel: ["appChannel", "App-Channel"],
|
||||
reqImei: ["reqImei", "Req-Imei"],
|
||||
sysOrigin: ["sysOrigin", "Sys-Origin"],
|
||||
sysOriginChild: ["sysOriginChild", "Sys-Origin-Child"],
|
||||
};
|
||||
|
||||
Object.keys(aliasMap).forEach(function (targetKey) {
|
||||
aliasMap[targetKey].forEach(function (sourceKey) {
|
||||
mapHeaderValue(normalized, targetKey, value[sourceKey]);
|
||||
});
|
||||
});
|
||||
|
||||
["headerInfo", "headers", "auth", "data", "state"].forEach(function (nestedKey) {
|
||||
const nestedValue = normalizeHeaderCandidate(value[nestedKey], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
if (
|
||||
key === "headerInfo" ||
|
||||
key === "headers" ||
|
||||
key === "auth" ||
|
||||
key === "data" ||
|
||||
key === "state"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value[key] === "object") {
|
||||
const nestedValue = normalizeHeaderCandidate(value[key], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mapHeaderValue(target, key, value) {
|
||||
if (value == null || value === "" || target[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
target[key] = String(value);
|
||||
}
|
||||
})();
|
||||
119
h5/yumi-invite-recharge-agency/index.html
Normal file
@ -0,0 +1,119 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Invite User To Become Recharge Agent</title>
|
||||
<link rel="stylesheet" href="./yumi-invite-recharge-agency.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="fullPage" id="pageRoot">
|
||||
<div class="status-bar-placeholder" id="statusBarPlaceholder"></div>
|
||||
<div class="header-container">
|
||||
<div class="side-area">
|
||||
<button class="back-btn" id="backButton" type="button">
|
||||
<img class="flip-img" id="backButtonIcon" src="" alt="" style="width: 100%; aspect-ratio: 1 / 1; display: block" />
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="header-title" id="pageTitle" style="color: black">Invite User To Become Recharge Agent</h1>
|
||||
<div class="actions-area">
|
||||
<div class="language-entry" id="languageEntry">
|
||||
<div class="language-name" id="languageName">English</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; position: relative">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
||||
<div
|
||||
class="gradient-border-wrapper"
|
||||
style="
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 4px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 0 8px; display: flex; align-items: center; height: max-content">
|
||||
<input
|
||||
id="searchInput"
|
||||
type="text"
|
||||
placeholder="Enter User ID"
|
||||
autocomplete="off"
|
||||
style="
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 8px;
|
||||
"
|
||||
/>
|
||||
<button
|
||||
id="clearButton"
|
||||
type="button"
|
||||
hidden
|
||||
style="background: none; border: none; color: #9ca3af; cursor: pointer; padding: 0 4px"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button style="font-weight: 600" class="invite-btn" id="searchButton" type="button" disabled>Search</button>
|
||||
</div>
|
||||
|
||||
<div id="resultList" style="margin: 16px 0; display: flex; flex-direction: column; gap: 12px"></div>
|
||||
<div id="noData" class="no-data">please input user id</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="userCardTemplate">
|
||||
<div
|
||||
class="gradient-border-wrapper user-card"
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border-radius: 20px;
|
||||
--gradient-border-width: 1.5px;
|
||||
--gradient-direction: to bottom;
|
||||
--gradient-start-color: #ffc4c4;
|
||||
--gradient-end-color: #fff6a6;
|
||||
--gradient-background: linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%);
|
||||
"
|
||||
>
|
||||
<div class="gradient-border-content">
|
||||
<div style="padding: 10px; transition: all 0.2s">
|
||||
<div class="status-badge" style="position: absolute; z-index: 3; top: -2px; right: -2px"></div>
|
||||
<div style="font-weight: 600">Information:</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px">
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch; display: flex; align-items: center; gap: 4px">
|
||||
<div style="width: 3em; display: flex; align-items: center; justify-content: center">
|
||||
<img class="avatar-image" src="" alt="" style="width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 50%" />
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0; align-self: stretch">
|
||||
<div class="nickname" style="margin-bottom: 4px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"></div>
|
||||
<div class="account" style="font-size: 0.9em; color: rgba(0, 0, 0, 0.4); font-weight: 500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: auto; min-width: 0; align-self: stretch; display: flex; justify-content: center; align-items: center">
|
||||
<div class="pending-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./yumi-invite-recharge-agency.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
456
h5/yumi-invite-recharge-agency/yumi-invite-recharge-agency.css
Normal file
@ -0,0 +1,456 @@
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fullPage {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background-color: #fff;
|
||||
background-image: url("../assets/secondBg-CSqvFWr0-1776148661447.png");
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.fullPage input::placeholder {
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.invite-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.invite-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.result-action-btn {
|
||||
padding: 4px 10px;
|
||||
border-radius: 32px;
|
||||
background: linear-gradient(269deg, #feb219 7.04%, #ff9326 96.78%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-action-btn:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status-bar-placeholder {
|
||||
display: none;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.status-bar-placeholder.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.side-area,
|
||||
.actions-area {
|
||||
min-width: 1.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions-area {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.back-btn,
|
||||
.help-btn {
|
||||
width: 1.5em;
|
||||
border: none;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.back-btn:active,
|
||||
.help-btn:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.language-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #f59e0b !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-border-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: var(--gradient-border-width, 1.5px);
|
||||
background: linear-gradient(
|
||||
var(--gradient-direction, to bottom),
|
||||
var(--gradient-start-color, #ffc4c4),
|
||||
var(--gradient-end-color, #fff6a6)
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gradient-border-content {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(
|
||||
--gradient-background,
|
||||
linear-gradient(120deg, rgba(255, 216, 0, 0.1) 39.13%, rgba(255, 149, 0, 0.1) 119.28%)
|
||||
);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 20px 25px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px rgba(0, 0, 0, 0.04);
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 24px 20px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: iconPulse 0.6s ease-out;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.modal-error .icon-circle-error {
|
||||
background-color: #fee2e2;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.modal-error .icon-symbol-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-error .modal-btn-error {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.modal-warning .icon-circle-warning {
|
||||
background-color: #fef3c7;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.modal-warning .icon-symbol-warning {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.modal-warning .modal-btn-warning {
|
||||
background-color: #d97706;
|
||||
}
|
||||
|
||||
.modal-success .icon-circle-success {
|
||||
background-color: #d1fae5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
.modal-success .icon-symbol-success {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.modal-success .modal-btn-success {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.modal-info .icon-circle-info {
|
||||
background-color: #dbeafe;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.modal-info .icon-symbol-info {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-info .modal-btn-info {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-simple .modal-message {
|
||||
font-size: 15px;
|
||||
color: #374151;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-simple .modal-btn {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.modal-enter {
|
||||
animation: modalEnter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.modal-leave {
|
||||
animation: modalLeave 0.2s ease-in;
|
||||
}
|
||||
|
||||
[dir="rtl"] .flip-img {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
[dir="rtl"] .avatar {
|
||||
margin-right: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .user-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-entry,
|
||||
[dir="rtl"] .header-title {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
@keyframes modalEnter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.7) translateY(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalLeave {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 360px) {
|
||||
.fullPage {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.fullPage {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.fullPage {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal {
|
||||
max-width: 280px;
|
||||
padding: 20px 16px 16px;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.icon-symbol {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
699
h5/yumi-invite-recharge-agency/yumi-invite-recharge-agency.js
Normal file
@ -0,0 +1,699 @@
|
||||
(function () {
|
||||
const API_BASE = "https://jvapi.haiyihy.com";
|
||||
const BACK_ICON =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGXSURBVHgB7dvBTcNAFIThB5xTQFwADVBBmqEnUgZQAAXAjRqgACgAj2wfECbJZXZnrPmkVRT5lF9ODn4vVRERERERETEaxnNb8cd+PA/jeZ3PY02xqG7KA+Ic6/eds5vfPxfRdelb4uxXrt0VmXqgU3Hgo8iUA52LA9SvF1yVpkviHOdDpRhIJg6oBZKKA0qB5OKASiDJOKAQSDYO9A4kHQd6BpKPA70CWcSBHoFs4kDrQFZxoGUguzjQKpBlHGgRyDYOsANZxwFmIPs4wAq0iTjACvRUG4gDjEeumDRsIg4wAn3VhjDmYt81jWOGf64vo5q3MsAaHOLDH2oa7q2xicQKhLvopTYQiTl63kQk9mzePlKL5QXrSK22O2wjtVx/sYzUej/ILlKPBSqrSL02zGwi9VzBs4jUe0dRPpLCEqd0JJUtV9lISmvAkpHU9qTlIikukktFUt20vzQSHu++F5HqGvDi3PgIgQ5FpL5p/zme+/l1za7IHP6rcSoSfdPeIRCsRcIPtM18rSUMJoeKiIiIiIjw9QNKh3OJhFQBoAAAAABJRU5ErkJggg==";
|
||||
const DEFAULT_AVATAR = "../assets/defaultAvatar-CdxWBK1k-1776148661448.png";
|
||||
const ENDPOINTS = {
|
||||
invitedList: "/team/bd/invite-recharge-agent-message-own",
|
||||
invite: "/team/bd/invite-recharge-agent",
|
||||
cancelPrefix: "/team/bd/invite-recharge-agent-cancel/",
|
||||
};
|
||||
|
||||
const TEXT = {
|
||||
pageTitle: "Invite User To Become Recharge Agent",
|
||||
language: "English",
|
||||
placeholder: "Enter User ID",
|
||||
search: "Search",
|
||||
invite: "Invite",
|
||||
idlePrompt: "please input user id",
|
||||
noUsersFound: "No users found",
|
||||
information: "Information:",
|
||||
userIdPrefix: "User ID:",
|
||||
pending: "Pending",
|
||||
success: "Success",
|
||||
cancel: "Cancel",
|
||||
invitationSubmitted: "Invitation submitted",
|
||||
applicationCancelled: "Application cancelled",
|
||||
somethingWentWrong: "Something went wrong",
|
||||
cancellationFailed: "Cancellation failed, try again",
|
||||
informationTitle: "Information",
|
||||
successTitle: "Success",
|
||||
errorTitle: "Error",
|
||||
ok: "OK",
|
||||
};
|
||||
|
||||
const state = {
|
||||
query: "",
|
||||
invitedList: [],
|
||||
searchResults: [],
|
||||
showSearchResults: false,
|
||||
searching: false,
|
||||
hasSearched: false,
|
||||
};
|
||||
|
||||
const dom = {
|
||||
statusBarPlaceholder: document.getElementById("statusBarPlaceholder"),
|
||||
backButton: document.getElementById("backButton"),
|
||||
backButtonIcon: document.getElementById("backButtonIcon"),
|
||||
pageTitle: document.getElementById("pageTitle"),
|
||||
languageEntry: document.getElementById("languageEntry"),
|
||||
languageName: document.getElementById("languageName"),
|
||||
searchInput: document.getElementById("searchInput"),
|
||||
clearButton: document.getElementById("clearButton"),
|
||||
searchButton: document.getElementById("searchButton"),
|
||||
resultList: document.getElementById("resultList"),
|
||||
noData: document.getElementById("noData"),
|
||||
userCardTemplate: document.getElementById("userCardTemplate"),
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
document.title = TEXT.pageTitle;
|
||||
document.documentElement.lang = "en";
|
||||
document.documentElement.dir = resolveDocumentDirection();
|
||||
|
||||
dom.backButtonIcon.src = BACK_ICON;
|
||||
dom.pageTitle.textContent = TEXT.pageTitle;
|
||||
dom.languageName.textContent = TEXT.language;
|
||||
dom.searchInput.placeholder = TEXT.placeholder;
|
||||
dom.searchButton.textContent = TEXT.search;
|
||||
dom.noData.textContent = TEXT.idlePrompt;
|
||||
|
||||
syncStatusBarPlaceholder();
|
||||
bindEvents();
|
||||
render();
|
||||
loadInvitedList();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
dom.backButton.addEventListener("click", handleBack);
|
||||
dom.languageEntry.addEventListener("click", handleLanguageClick);
|
||||
dom.searchInput.addEventListener("input", handleSearchInput);
|
||||
dom.clearButton.addEventListener("click", clearSearch);
|
||||
dom.searchButton.addEventListener("click", triggerSearchNow);
|
||||
}
|
||||
|
||||
function resolveDocumentDirection() {
|
||||
const lang = (new URLSearchParams(window.location.search).get("lang") || "").toLowerCase();
|
||||
return lang.startsWith("ar") ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
function syncStatusBarPlaceholder() {
|
||||
const hasAppBridge =
|
||||
Boolean(window.app) ||
|
||||
Boolean(window.FlutterPageControl) ||
|
||||
Boolean(window.webkit && window.webkit.messageHandlers);
|
||||
const isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent || "");
|
||||
dom.statusBarPlaceholder.classList.toggle("is-visible", hasAppBridge && isIOS);
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
function handleLanguageClick() {
|
||||
window.location.href = "/language";
|
||||
}
|
||||
|
||||
function handleSearchInput(event) {
|
||||
state.query = event.target.value;
|
||||
dom.clearButton.hidden = !state.query;
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
state.query = "";
|
||||
state.searchResults = [];
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
dom.searchInput.value = "";
|
||||
dom.clearButton.hidden = true;
|
||||
render();
|
||||
}
|
||||
|
||||
async function triggerSearchNow() {
|
||||
if (state.searching) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = state.query.trim();
|
||||
|
||||
if (!query) {
|
||||
state.showSearchResults = false;
|
||||
state.hasSearched = false;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.searching = true;
|
||||
state.showSearchResults = true;
|
||||
state.hasSearched = true;
|
||||
state.searchResults = [];
|
||||
render();
|
||||
|
||||
try {
|
||||
const payload = await searchUser(query);
|
||||
state.searchResults = payload ? [mergeWithInvitationStatus(payload.userProfile)] : [];
|
||||
} catch (error) {
|
||||
state.searchResults = [];
|
||||
showToast(extractErrorMessage(error), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
state.searching = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInvitedList() {
|
||||
try {
|
||||
const response = await request("GET", ENDPOINTS.invitedList);
|
||||
state.invitedList = response && response.status && Array.isArray(response.body) ? response.body : [];
|
||||
state.searchResults = state.searchResults.map(function (item) {
|
||||
return mergeWithInvitationStatus(item.userProfile || {});
|
||||
});
|
||||
render();
|
||||
} catch (error) {
|
||||
state.invitedList = [];
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function searchUser(query) {
|
||||
try {
|
||||
const response = await request(
|
||||
"GET",
|
||||
"/user/user-profile/open-search?account=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (response && response.status && response.body) {
|
||||
return { userProfile: response.body };
|
||||
}
|
||||
} catch (error) {
|
||||
if (!/^\d+$/.test(query)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackResponse = await request(
|
||||
"GET",
|
||||
"/user/user-profile?userId=" + encodeURIComponent(query)
|
||||
);
|
||||
|
||||
if (fallbackResponse && fallbackResponse.status && fallbackResponse.body) {
|
||||
return { userProfile: fallbackResponse.body };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeWithInvitationStatus(userProfile) {
|
||||
const matchedInvitation = state.invitedList.find(function (item) {
|
||||
return (
|
||||
item &&
|
||||
item.userProfile &&
|
||||
userProfile &&
|
||||
item.userProfile.id === userProfile.id
|
||||
);
|
||||
});
|
||||
|
||||
return Object.assign({}, matchedInvitation || {}, {
|
||||
userProfile: userProfile || {},
|
||||
});
|
||||
}
|
||||
|
||||
async function inviteUser(item) {
|
||||
if (!item || !item.userProfile || !item.userProfile.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request("POST", ENDPOINTS.invite, {
|
||||
body: {
|
||||
inviteUserId: item.userProfile.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.invitationSubmitted, {
|
||||
title: TEXT.successTitle,
|
||||
type: "success",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.somethingWentWrong), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelInvitation(invitationId) {
|
||||
try {
|
||||
const response = await request("POST", ENDPOINTS.cancelPrefix + invitationId);
|
||||
|
||||
if (response && response.status) {
|
||||
showToast(TEXT.applicationCancelled, {
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
});
|
||||
await loadInvitedList();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(extractErrorMessage(error, TEXT.cancellationFailed), {
|
||||
title: TEXT.errorTitle,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderSearchButton();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function renderSearchButton() {
|
||||
dom.searchButton.disabled = !state.query.trim() || state.searching;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const currentList = state.showSearchResults ? state.searchResults : state.invitedList;
|
||||
dom.resultList.innerHTML = "";
|
||||
|
||||
currentList.forEach(function (item) {
|
||||
dom.resultList.appendChild(createUserCard(item, state.showSearchResults ? "search" : "invited"));
|
||||
});
|
||||
|
||||
if (currentList.length > 0 || (state.searching && state.showSearchResults)) {
|
||||
dom.noData.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
dom.noData.hidden = false;
|
||||
dom.noData.textContent =
|
||||
state.showSearchResults && state.hasSearched ? TEXT.noUsersFound : TEXT.idlePrompt;
|
||||
}
|
||||
|
||||
function createUserCard(item, mode) {
|
||||
const fragment = dom.userCardTemplate.content.cloneNode(true);
|
||||
const statusBadge = fragment.querySelector(".status-badge");
|
||||
const avatarImage = fragment.querySelector(".avatar-image");
|
||||
const nickname = fragment.querySelector(".nickname");
|
||||
const account = fragment.querySelector(".account");
|
||||
const pendingActions = fragment.querySelector(".pending-actions");
|
||||
const userProfile = item.userProfile || {};
|
||||
|
||||
avatarImage.src = userProfile.userAvatar || DEFAULT_AVATAR;
|
||||
avatarImage.alt = userProfile.userNickname || "";
|
||||
avatarImage.onerror = handleAvatarError;
|
||||
nickname.textContent = userProfile.userNickname || "";
|
||||
account.textContent = TEXT.userIdPrefix + " " + (userProfile.account || userProfile.id || "");
|
||||
|
||||
if (item.status === 0) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #759cff 5.66%, #3d73ff 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.pending +
|
||||
"</div>";
|
||||
} else if (item.status === 1) {
|
||||
statusBadge.innerHTML =
|
||||
'<div style="background-image: linear-gradient(112deg, #75ff98 5.66%, #3dff54 42.49%); padding: 4px 8px; border-radius: 0 12px; color: white; font-weight: 600">' +
|
||||
TEXT.success +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
if (mode === "search") {
|
||||
const inviteButton = document.createElement("button");
|
||||
inviteButton.type = "button";
|
||||
inviteButton.className = "result-action-btn";
|
||||
inviteButton.textContent = TEXT.invite;
|
||||
inviteButton.disabled = item.status === 0 || item.status === 1;
|
||||
inviteButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
inviteUser(item);
|
||||
});
|
||||
pendingActions.appendChild(inviteButton);
|
||||
} else if (item.status === 0) {
|
||||
const cancelButton = document.createElement("div");
|
||||
cancelButton.textContent = TEXT.cancel;
|
||||
cancelButton.style.padding = "4px 8px";
|
||||
cancelButton.style.borderRadius = "32px";
|
||||
cancelButton.style.backgroundColor = "red";
|
||||
cancelButton.style.color = "white";
|
||||
cancelButton.style.fontWeight = "600";
|
||||
cancelButton.style.zIndex = "5";
|
||||
cancelButton.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
cancelInvitation(item.id);
|
||||
});
|
||||
pendingActions.appendChild(cancelButton);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function handleAvatarError(event) {
|
||||
event.target.onerror = null;
|
||||
event.target.src = DEFAULT_AVATAR;
|
||||
}
|
||||
|
||||
function extractErrorMessage(error, fallback) {
|
||||
return (
|
||||
(error &&
|
||||
error.response &&
|
||||
(error.response.errorMsg || error.response.message || error.response.error)) ||
|
||||
(error && error.message) ||
|
||||
fallback ||
|
||||
TEXT.somethingWentWrong
|
||||
);
|
||||
}
|
||||
|
||||
function showToast(message, options) {
|
||||
const config = Object.assign(
|
||||
{
|
||||
title: TEXT.informationTitle,
|
||||
type: "info",
|
||||
confirmText: TEXT.ok,
|
||||
},
|
||||
options || {}
|
||||
);
|
||||
|
||||
const iconMap = {
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
success: "✓",
|
||||
info: "i",
|
||||
simple: "",
|
||||
};
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "modal-overlay";
|
||||
overlay.innerHTML =
|
||||
'<div class="modal modal-enter modal-' +
|
||||
config.type +
|
||||
'">' +
|
||||
(config.type === "simple"
|
||||
? ""
|
||||
: '<div class="modal-icon"><div class="icon-circle icon-circle-' +
|
||||
config.type +
|
||||
'"><span class="icon-symbol icon-symbol-' +
|
||||
config.type +
|
||||
'">' +
|
||||
iconMap[config.type] +
|
||||
"</span></div></div>") +
|
||||
(config.type === "simple" || !config.title
|
||||
? ""
|
||||
: '<div class="modal-title">' + escapeHtml(config.title) + "</div>") +
|
||||
'<div class="modal-message">' +
|
||||
escapeHtml(message) +
|
||||
"</div>" +
|
||||
'<div class="modal-actions"><button class="modal-btn modal-btn-' +
|
||||
config.type +
|
||||
'">' +
|
||||
escapeHtml(config.confirmText) +
|
||||
"</button></div>" +
|
||||
"</div>";
|
||||
|
||||
const modal = overlay.querySelector(".modal");
|
||||
const closeButton = overlay.querySelector(".modal-btn");
|
||||
const close = function () {
|
||||
modal.classList.remove("modal-enter");
|
||||
modal.classList.add("modal-leave");
|
||||
window.setTimeout(function () {
|
||||
overlay.remove();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
closeButton.addEventListener("click", close);
|
||||
overlay.addEventListener("click", function (event) {
|
||||
if (event.target === overlay) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
async function request(method, path, options) {
|
||||
const config = options || {};
|
||||
const headers = buildRequestHeaders(Boolean(config.body), config.headers || {});
|
||||
const response = await fetch(API_BASE + path, {
|
||||
method: method,
|
||||
credentials: "include",
|
||||
headers: headers,
|
||||
body: config.body ? JSON.stringify(config.body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = {};
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch (error) {
|
||||
payload = {
|
||||
status: response.ok,
|
||||
body: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || payload.status === false) {
|
||||
const requestError = new Error(
|
||||
payload.errorMsg || payload.message || response.statusText || TEXT.somethingWentWrong
|
||||
);
|
||||
requestError.response = payload;
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildRequestHeaders(hasBody, overrideHeaders) {
|
||||
const discovered = discoverAppHeaders();
|
||||
const headers = Object.assign({}, overrideHeaders || {});
|
||||
|
||||
if (hasBody) {
|
||||
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
||||
}
|
||||
|
||||
headers.Accept = headers.Accept || "application/json, text/plain, */*";
|
||||
|
||||
if (discovered.authorization) {
|
||||
headers.Authorization = headers.Authorization || discovered.authorization;
|
||||
}
|
||||
|
||||
if (discovered.reqLang) {
|
||||
headers["Req-Lang"] = headers["Req-Lang"] || discovered.reqLang;
|
||||
}
|
||||
|
||||
if (discovered.appVersion) {
|
||||
headers["App-Version"] = headers["App-Version"] || discovered.appVersion;
|
||||
}
|
||||
|
||||
if (discovered.buildVersion) {
|
||||
headers["Build-Version"] = headers["Build-Version"] || discovered.buildVersion;
|
||||
}
|
||||
|
||||
if (discovered.appChannel) {
|
||||
headers["App-Channel"] = headers["App-Channel"] || discovered.appChannel;
|
||||
}
|
||||
|
||||
if (discovered.reqImei) {
|
||||
headers["Req-Imei"] = headers["Req-Imei"] || discovered.reqImei;
|
||||
}
|
||||
|
||||
if (discovered.sysOrigin) {
|
||||
headers["Sys-Origin"] = headers["Sys-Origin"] || discovered.sysOrigin;
|
||||
}
|
||||
|
||||
if (discovered.sysOriginChild) {
|
||||
headers["Sys-Origin-Child"] =
|
||||
headers["Sys-Origin-Child"] || discovered.sysOriginChild;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function discoverAppHeaders() {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const collected = [];
|
||||
|
||||
collectCandidate(collected, window.__APP_HEADERS__);
|
||||
collectCandidate(collected, window.__HEADER_INFO__);
|
||||
collectCandidate(collected, window.headerInfo);
|
||||
collectCandidate(collected, parseHeaderObjectFromQuery(query));
|
||||
|
||||
collectFromStorage(collected, window.localStorage);
|
||||
collectFromStorage(collected, window.sessionStorage);
|
||||
|
||||
return collected.reduce(function (result, candidate) {
|
||||
return Object.assign(result, candidate);
|
||||
}, {
|
||||
reqLang: query.get("lang") || document.documentElement.lang || "en",
|
||||
});
|
||||
}
|
||||
|
||||
function parseHeaderObjectFromQuery(query) {
|
||||
const candidate = {};
|
||||
mapHeaderValue(candidate, "authorization", query.get("authorization"));
|
||||
mapHeaderValue(candidate, "authorization", query.get("token"));
|
||||
mapHeaderValue(candidate, "reqLang", query.get("lang"));
|
||||
mapHeaderValue(candidate, "appVersion", query.get("appVersion"));
|
||||
mapHeaderValue(candidate, "buildVersion", query.get("buildVersion"));
|
||||
mapHeaderValue(candidate, "appChannel", query.get("appChannel"));
|
||||
mapHeaderValue(candidate, "reqImei", query.get("reqImei"));
|
||||
mapHeaderValue(candidate, "sysOrigin", query.get("sysOrigin"));
|
||||
mapHeaderValue(candidate, "sysOriginChild", query.get("sysOriginChild"));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function collectFromStorage(collected, storage) {
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferredKeys = [
|
||||
"authorization",
|
||||
"Authorization",
|
||||
"token",
|
||||
"accessToken",
|
||||
"headerInfo",
|
||||
"headersFromApp",
|
||||
"appHeaders",
|
||||
"persist:root",
|
||||
"user",
|
||||
"userInfo",
|
||||
];
|
||||
|
||||
preferredKeys.forEach(function (key) {
|
||||
try {
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index < storage.length; index += 1) {
|
||||
try {
|
||||
const key = storage.key(index);
|
||||
collectCandidate(collected, storage.getItem(key));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectCandidate(collected, value) {
|
||||
const normalized = normalizeHeaderCandidate(value, 0);
|
||||
if (normalized && Object.keys(normalized).length > 0) {
|
||||
collected.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHeaderCandidate(value, depth) {
|
||||
if (depth > 4 || value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed[0] === "{" || trimmed[0] === "[") {
|
||||
try {
|
||||
return normalizeHeaderCandidate(JSON.parse(trimmed), depth + 1);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (/bearer\\s+/i.test(trimmed)) {
|
||||
return {
|
||||
authorization: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce(function (result, entry) {
|
||||
const normalized = normalizeHeaderCandidate(entry, depth + 1);
|
||||
return normalized ? Object.assign(result, normalized) : result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
const aliasMap = {
|
||||
authorization: ["authorization", "Authorization", "token", "accessToken"],
|
||||
reqLang: ["reqLang", "Req-Lang", "lang", "language"],
|
||||
appVersion: ["appVersion", "App-Version"],
|
||||
buildVersion: ["buildVersion", "Build-Version"],
|
||||
appChannel: ["appChannel", "App-Channel"],
|
||||
reqImei: ["reqImei", "Req-Imei"],
|
||||
sysOrigin: ["sysOrigin", "Sys-Origin"],
|
||||
sysOriginChild: ["sysOriginChild", "Sys-Origin-Child"],
|
||||
};
|
||||
|
||||
Object.keys(aliasMap).forEach(function (targetKey) {
|
||||
aliasMap[targetKey].forEach(function (sourceKey) {
|
||||
mapHeaderValue(normalized, targetKey, value[sourceKey]);
|
||||
});
|
||||
});
|
||||
|
||||
["headerInfo", "headers", "auth", "data", "state"].forEach(function (nestedKey) {
|
||||
const nestedValue = normalizeHeaderCandidate(value[nestedKey], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
if (
|
||||
key === "headerInfo" ||
|
||||
key === "headers" ||
|
||||
key === "auth" ||
|
||||
key === "data" ||
|
||||
key === "state"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value[key] === "object") {
|
||||
const nestedValue = normalizeHeaderCandidate(value[key], depth + 1);
|
||||
if (nestedValue) {
|
||||
Object.assign(normalized, nestedValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mapHeaderValue(target, key, value) {
|
||||
if (value == null || value === "" || target[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
target[key] = String(value);
|
||||
}
|
||||
})();
|
||||