工资历史

This commit is contained in:
zhx 2026-05-29 20:04:29 +08:00
parent c484579358
commit f265ccfa57
3 changed files with 540 additions and 7 deletions

View File

@ -4,7 +4,7 @@
<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>Withdraw/Exchange</title>
<link rel="stylesheet" href="./style.css?v=20260429-0300" />
<link rel="stylesheet" href="./style.css?v=20260529-0100" />
</head>
<body>
<div class="wallet-page" aria-label="Withdraw and exchange" data-i18n-aria="page_label">
@ -29,7 +29,14 @@
<main class="content">
<section class="balance-card">
<div class="balance-label" data-i18n="available_income">salary</div>
<div class="balance-head">
<div class="balance-label" data-i18n="available_income">salary</div>
<button class="balance-history-button" id="salaryHistoryButton" type="button" aria-label="Salary history" data-i18n-aria="salary_history">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m9 5 7 7-7 7" />
</svg>
</button>
</div>
<div class="balance-value">
<span class="balance-currency" aria-hidden="true">$</span>
<span id="balanceAmount">0.00</span>
@ -142,8 +149,22 @@
</main>
<div class="toast" hidden></div>
<div class="salary-history-modal" id="salaryHistoryModal" hidden>
<div class="salary-history-backdrop" data-close-salary-history></div>
<section class="salary-history-dialog" role="dialog" aria-modal="true" aria-labelledby="salaryHistoryTitle">
<div class="salary-history-head">
<h2 id="salaryHistoryTitle" data-i18n="salary_history">Salary history</h2>
<button class="salary-history-close" type="button" aria-label="Close" data-i18n-aria="close" data-close-salary-history>&times;</button>
</div>
<div class="salary-history-summary">
<span data-i18n="available_income">salary</span>
<strong id="salaryHistoryTotal">$0.00</strong>
</div>
<div class="salary-history-list" id="salaryHistoryList"></div>
</section>
</div>
<div class="home-indicator" aria-hidden="true"></div>
</div>
<script src="./script.js?v=20260507-1740" defer></script>
<script src="./script.js?v=20260529-0100" defer></script>
</body>
</html>

View File

@ -61,7 +61,19 @@
exchange_success: "Exchange success",
exchange_failed: "Exchange failed",
transfer_success: "Transfer success",
transfer_failed: "Transfer failed"
transfer_failed: "Transfer failed",
salary_history: "Salary history",
close: "Close",
loading: "Loading...",
no_salary_history: "No salary history",
income: "Income",
expense: "Expense",
balance: "Balance",
period: "Period",
days: "Days",
target: "Target",
work_report: "Work report",
load_salary_history_failed: "Failed to load salary history"
},
ar: {
page_label: "السحب والتبديل",
@ -113,7 +125,19 @@
exchange_success: "تم التبديل",
exchange_failed: "فشل التبديل",
transfer_success: "تم التحويل",
transfer_failed: "فشل التحويل"
transfer_failed: "فشل التحويل",
salary_history: "سجل الراتب",
close: "إغلاق",
loading: "جار التحميل...",
no_salary_history: "لا يوجد سجل راتب",
income: "دخل",
expense: "مصروف",
balance: "الرصيد",
period: "الفترة",
days: "الأيام",
target: "الهدف",
work_report: "تقرير العمل",
load_salary_history_failed: "فشل تحميل سجل الراتب"
},
tr: {
page_label: "Çekme ve değişim",
@ -165,7 +189,19 @@
exchange_success: "Exchange success",
exchange_failed: "Exchange failed",
transfer_success: "Transfer success",
transfer_failed: "Transfer failed"
transfer_failed: "Transfer failed",
salary_history: "Salary history",
close: "Close",
loading: "Loading...",
no_salary_history: "No salary history",
income: "Income",
expense: "Expense",
balance: "Balance",
period: "Period",
days: "Days",
target: "Target",
work_report: "Work report",
load_salary_history_failed: "Failed to load salary history"
},
id: {
page_label: "Tarik dan tukar",
@ -217,7 +253,19 @@
exchange_success: "Penukaran berhasil",
exchange_failed: "Penukaran gagal",
transfer_success: "Transfer berhasil",
transfer_failed: "Transfer gagal"
transfer_failed: "Transfer gagal",
salary_history: "Riwayat gaji",
close: "Tutup",
loading: "Memuat...",
no_salary_history: "Tidak ada riwayat gaji",
income: "Masuk",
expense: "Keluar",
balance: "Saldo",
period: "Periode",
days: "Hari",
target: "Target",
work_report: "Laporan kerja",
load_salary_history_failed: "Gagal memuat riwayat gaji"
}
};
@ -232,9 +280,14 @@
let selectedPaymentInfo = null;
let selectedPayee = null;
let currentProfile = null;
let currentIdentityInfo = null;
let isSubmitting = false;
let isSavingAddress = false;
let isSearchingPayee = false;
let isSalaryHistoryLoading = false;
let salaryHistoryItems = [];
let salaryHistoryTotal = 0;
let salaryHistoryLoadedType = "";
const inputs = {
withdraw: document.querySelector("#withdrawAmount"),
@ -255,6 +308,10 @@
const searchPayeeButton = document.querySelector("#searchPayeeButton");
const exchangeRateText = document.querySelector("#exchangeRateText");
const salarySelector = document.querySelector("#salarySelector");
const salaryHistoryButton = document.querySelector("#salaryHistoryButton");
const salaryHistoryModal = document.querySelector("#salaryHistoryModal");
const salaryHistoryList = document.querySelector("#salaryHistoryList");
const salaryHistoryTotalNode = document.querySelector("#salaryHistoryTotal");
function readURLParams() {
const result = new URLSearchParams(window.location.search);
@ -423,6 +480,18 @@
return Number.isFinite(number) ? number : 0;
}
function isTruthyIdentityValue(value) {
if (value === true || value === 1) return true;
const text = String(value || "").trim().toLowerCase();
return text === "true" || text === "1" || text === "yes";
}
function currentUserId() {
return currentProfile?.id ||
currentProfile?.userId ||
firstRawParam(["userId", "UserId", "uid", "UID", "id"]);
}
function activeInput() {
return inputs[currentMode] || null;
}
@ -461,6 +530,9 @@
if (!nextType || nextType === currentSalaryType) return;
currentSalaryType = nextType;
renderBalance(salaryBalanceValue(nextType));
if (salaryHistoryModal && !salaryHistoryModal.hidden && salaryHistoryLoadedType !== activeSalaryType()) {
openSalaryHistory();
}
}
function isValidTrc20Address(value) {
@ -501,6 +573,7 @@
if (exchangeRateText) exchangeRateText.textContent = formatExchangeRate();
renderSalarySelector();
renderPayee();
renderSalaryHistory();
updateSubmitState();
}
@ -593,6 +666,104 @@
updateSubmitState();
}
function setSalaryHistoryModalOpen(open) {
if (!salaryHistoryModal) return;
salaryHistoryModal.hidden = !open;
document.body.classList.toggle("salary-history-open", open);
}
function formatHistoryDate(value) {
if (!value) return "";
const text = String(value).replace("T", " ");
return text.length > 16 ? text.slice(0, 16) : text;
}
function formatBillPeriod(value) {
const text = String(value || "");
if (text.length < 6) return text;
return `${text.slice(0, 4)}.${text.slice(4, 6)}`;
}
function salaryTypeLabel(type) {
const value = normalizeSalaryType(type);
if (value === "BD_SALARY") return message("bd_salary", "BD salary");
if (value === "BD_LEADER_SALARY") return message("bd_leader_salary", "BD Leader salary");
return message("available_income", "salary");
}
function historyItemTitle(item) {
return item.title ||
item.eventDesc ||
item.eventDescribe ||
item.salaryTypeName ||
item.salaryEvent ||
item.event ||
message("work_report", "Work report");
}
function createSalaryHistoryItem(item) {
const row = document.createElement("article");
row.className = "salary-history-item";
const main = document.createElement("div");
main.className = "salary-history-item-main";
const title = document.createElement("div");
title.className = "salary-history-item-title";
title.textContent = historyItemTitle(item);
const meta = document.createElement("div");
meta.className = "salary-history-item-meta";
const metaItems = [];
if (item.date) metaItems.push(item.date);
if (item.period) metaItems.push(`${message("period", "Period")}: ${item.period}`);
if (item.target) metaItems.push(`${message("target", "Target")}: ${item.target}`);
if (item.days) metaItems.push(`${message("days", "Days")}: ${item.days}`);
if (item.balance !== undefined && item.balance !== null && item.balance !== "") {
metaItems.push(`${message("balance", "Balance")}: $${formatMoney(item.balance)}`);
}
meta.textContent = metaItems.join(" ");
main.append(title, meta);
const isOut = Number(item.type) === 1;
const amount = document.createElement("div");
amount.className = `salary-history-amount${isOut ? " is-out" : ""}`;
amount.textContent = `${isOut ? "-" : "+"}$${formatMoney(item.amount)}`;
row.append(main, amount);
if (item.status) {
const status = document.createElement("div");
status.className = "salary-history-status";
status.textContent = item.status;
row.appendChild(status);
}
return row;
}
function renderSalaryHistory() {
if (salaryHistoryTotalNode) salaryHistoryTotalNode.textContent = `$${formatMoney(salaryHistoryTotal)}`;
if (!salaryHistoryList) return;
if (isSalaryHistoryLoading) {
const loading = document.createElement("div");
loading.className = "salary-history-empty";
loading.textContent = message("loading", "Loading...");
salaryHistoryList.replaceChildren(loading);
return;
}
if (!salaryHistoryItems.length) {
const empty = document.createElement("div");
empty.className = "salary-history-empty";
empty.textContent = message("no_salary_history", "No salary history");
salaryHistoryList.replaceChildren(empty);
return;
}
salaryHistoryList.replaceChildren(...salaryHistoryItems.map(createSalaryHistoryItem));
}
function renderPayee() {
if (!payeeResult) return;
@ -674,6 +845,144 @@
}
}
async function fetchIdentityInfo() {
if (currentIdentityInfo) return currentIdentityInfo;
try {
const data = await requestJSON("/app/h5/identity");
currentIdentityInfo = responseBody(data) || {};
} catch (error) {
console.warn("Failed to load identity info:", error);
currentIdentityInfo = {};
}
return currentIdentityInfo;
}
function normalizeStatusLabel(status) {
const value = String(status || "").trim();
if (value === "SETTLED" || value === "PAY_OUT" || value === "Completed") return "Completed";
if (value === "UNPAID" || value === "In Progress") return "In progress";
if (value === "HANG_UP" || value === "Out of account" || value === "Out Of Account") return "Out of account";
if (value === "Pending") return "Pending";
return value;
}
function memberWorkSalary(target, identity) {
const settlement = target?.settlementResult || {};
const memberSalary = moneyNumber(settlement.memberSalary);
const ownSalary = moneyNumber(settlement.ownSalary);
const totalSalary = moneyNumber(settlement.totalSalary);
if (isTruthyIdentityValue(identity?.agent)) return memberSalary + ownSalary;
if (isTruthyIdentityValue(identity?.anchor)) return memberSalary;
return totalSalary || memberSalary || ownSalary;
}
async function fetchMemberWorkHistory() {
if (!currentProfile) await fetchUserProfile();
const userId = currentUserId();
if (!userId) throw new Error(message("missing_token", "Missing token"));
const [identity, data] = await Promise.all([
fetchIdentityInfo(),
requestJSON(`/team/member-work?userId=${encodeURIComponent(userId)}`)
]);
const body = responseBody(data) || {};
const targets = Array.isArray(body.targets) ? body.targets : [];
return {
total: body.historySalary ?? availableBalance,
items: targets.map((item) => {
const target = item.target || {};
return {
id: item.id,
title: item.billTitle || message("work_report", "Work report"),
amount: memberWorkSalary(target, identity),
type: 0,
status: normalizeStatusLabel(item.status),
target: target.acceptGiftValue,
days: target.effectiveDay,
period: item.billBelong ? formatBillPeriod(item.billBelong) : "",
date: item.billTitle || ""
};
})
};
}
async function fetchSalaryAccountHistory() {
if (!currentProfile) await fetchUserProfile();
const query = new URLSearchParams({
salaryType: activeSalaryType(),
current: "1",
size: "20"
});
const userId = currentUserId();
if (userId) query.set("userId", String(userId));
const data = await requestJSON(`/wallet/salary-account/running-water?${query.toString()}`);
const body = responseBody(data) || data || {};
const records = Array.isArray(body.records) ? body.records : Array.isArray(body.list) ? body.list : [];
return {
total: availableBalance,
items: records.map((item) => ({
id: item.id,
title: item.eventDesc || item.salaryTypeName || salaryTypeLabel(item.salaryType),
amount: item.amount,
balance: item.availableBalance ?? item.balance,
type: item.type,
status: item.salaryEvent || "",
period: item.billBelong ? formatBillPeriod(item.billBelong) : "",
date: formatHistoryDate(item.createdAt)
}))
};
}
async function fetchBankWaterHistory() {
const data = await requestJSON("/wallet/bank/balance/water/details");
const records = Array.isArray(responseBody(data)) ? responseBody(data) : Array.isArray(data) ? data : [];
return {
total: availableBalance,
items: records.map((item) => ({
id: item.id,
title: item.eventDescribe || item.event || message("salary_history", "Salary history"),
amount: item.amount,
balance: item.balance,
type: item.type,
status: item.event || "",
date: formatHistoryDate(item.createTime)
}))
};
}
async function fetchSalaryHistory() {
if (isSalaryAccountMode()) return fetchSalaryAccountHistory();
try {
return await fetchMemberWorkHistory();
} catch (error) {
console.warn("Failed to load member work history, falling back to bank water:", error);
return fetchBankWaterHistory();
}
}
async function openSalaryHistory() {
salaryHistoryLoadedType = activeSalaryType();
salaryHistoryTotal = availableBalance;
salaryHistoryItems = [];
isSalaryHistoryLoading = true;
renderSalaryHistory();
setSalaryHistoryModalOpen(true);
try {
const result = await fetchSalaryHistory();
salaryHistoryTotal = moneyNumber(result.total ?? availableBalance);
salaryHistoryItems = Array.isArray(result.items) ? result.items : [];
} catch (error) {
console.error("Failed to load salary history:", error);
salaryHistoryItems = [];
showToast(error.message || message("load_salary_history_failed", "Failed to load salary history"));
} finally {
isSalaryHistoryLoading = false;
renderSalaryHistory();
}
}
async function fetchPaymentInfo() {
try {
const data = await requestJSON("/wallet/bank-card/list");
@ -944,6 +1253,10 @@
searchPayeeButton?.addEventListener("click", searchPayee);
saveAddressButton?.addEventListener("click", saveAddress);
submitButton?.addEventListener("click", submitCurrentMode);
salaryHistoryButton?.addEventListener("click", openSalaryHistory);
document.querySelectorAll("[data-close-salary-history]").forEach((node) => {
node.addEventListener("click", () => setSalaryHistoryModalOpen(false));
});
syncBackFallback();
applyLanguage(currentLang);

View File

@ -197,12 +197,44 @@ select {
padding: 20px 24px 19px;
}
.balance-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.balance-label {
color: var(--muted);
font-size: 14px;
font-weight: 800;
}
.balance-history-button {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
flex: 0 0 34px;
border-radius: 50%;
background: #effafa;
color: #25282e;
}
.balance-history-button svg {
width: 22px;
height: 22px;
}
.balance-history-button path {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.6;
}
.balance-value {
display: flex;
align-items: center;
@ -623,6 +655,7 @@ select {
position: fixed;
left: 50%;
bottom: 34px;
z-index: 70;
transform: translateX(-50%);
padding: 10px 14px;
border-radius: 999px;
@ -632,6 +665,172 @@ select {
font-weight: 850;
}
.salary-history-modal {
position: fixed;
inset: 0;
z-index: 60;
display: flex;
align-items: flex-end;
justify-content: center;
}
.salary-history-backdrop {
position: absolute;
inset: 0;
background: rgba(14, 19, 24, 0.44);
}
.salary-history-dialog {
position: relative;
z-index: 1;
display: flex;
width: min(100%, 430px);
max-height: 78vh;
flex-direction: column;
overflow: hidden;
border-radius: 18px 18px 0 0;
background: #fff;
box-shadow: 0 -14px 36px rgba(26, 35, 44, 0.16);
}
.salary-history-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 18px 18px 12px;
}
.salary-history-head h2 {
min-width: 0;
margin: 0;
overflow: hidden;
color: #24282e;
font-size: 18px;
font-weight: 950;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.salary-history-close {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
flex: 0 0 34px;
border-radius: 50%;
background: #f3f5f5;
color: #676b72;
font-size: 28px;
line-height: 1;
}
.salary-history-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0 18px 12px;
padding: 14px;
border-radius: 8px;
background: #f3fbfa;
}
.salary-history-summary span {
color: var(--muted);
font-size: 13px;
font-weight: 850;
}
.salary-history-summary strong {
min-width: 0;
overflow-wrap: anywhere;
color: #24282e;
font-size: 20px;
font-weight: 950;
}
.salary-history-list {
display: grid;
gap: 10px;
overflow-y: auto;
padding: 0 18px calc(18px + env(safe-area-inset-bottom));
}
.salary-history-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px 12px;
padding: 14px;
border: 1px solid #eef1f1;
border-radius: 8px;
background: #fbfcfc;
}
.salary-history-item-main {
min-width: 0;
}
.salary-history-item-title {
overflow: hidden;
color: #25282e;
font-size: 14px;
font-weight: 900;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.salary-history-item-meta {
display: flex;
flex-wrap: wrap;
gap: 6px 10px;
margin-top: 6px;
color: var(--muted);
font-size: 12px;
font-weight: 750;
line-height: 1.25;
}
.salary-history-amount {
color: #20a569;
font-size: 15px;
font-weight: 950;
line-height: 1.25;
white-space: nowrap;
}
.salary-history-amount.is-out {
color: #e05d4f;
}
.salary-history-status {
justify-self: start;
grid-column: 1 / -1;
width: max-content;
max-width: 100%;
padding: 3px 8px;
border-radius: 999px;
background: #edf8f7;
color: #0d8f83;
font-size: 11px;
font-weight: 900;
}
.salary-history-empty {
padding: 24px 12px 30px;
color: var(--muted);
font-size: 14px;
font-weight: 850;
text-align: center;
}
.salary-history-open {
overflow: hidden;
}
.home-indicator {
position: fixed;
left: 50%;