表格工资相关

This commit is contained in:
zhx 2026-06-15 00:37:46 +08:00
parent 79da033dc9
commit aa7ce50e4f
7 changed files with 77 additions and 33 deletions

View File

@ -71,7 +71,6 @@ const historyColumns = [
export function SalaryWalletCell({ role, userId, userLabel, wallet }) { export function SalaryWalletCell({ role, userId, userLabel, wallet }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const balance = Number(wallet?.availableAmount || 0); const balance = Number(wallet?.availableAmount || 0);
const assetType = wallet?.assetType || salaryWalletAssetType(role);
return ( return (
<> <>
@ -81,7 +80,6 @@ export function SalaryWalletCell({ role, userId, userLabel, wallet }) {
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
<span className={styles.salaryWalletAmount}>{formatSalaryWalletAmount(balance)}</span> <span className={styles.salaryWalletAmount}>{formatSalaryWalletAmount(balance)}</span>
<span className={styles.salaryWalletAsset}>{assetType || "-"}</span>
</button> </button>
<SalaryWalletHistoryDrawer <SalaryWalletHistoryDrawer
open={open} open={open}
@ -163,10 +161,9 @@ export function SalaryWalletAdjustButton({ disabled = false, onAdjusted, role, u
onSubmit={submit} onSubmit={submit}
> >
<AdminFormSection title="目标身份"> <AdminFormSection title="目标身份">
<AdminFormFieldGrid columns="repeat(3, minmax(0, 1fr))"> <AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
<AdminFormReadOnlyField label="身份" value={salaryWalletRoleLabel(role)} /> <AdminFormReadOnlyField label="身份" value={salaryWalletRoleLabel(role)} />
<AdminFormReadOnlyField label="用户" value={userLabel || userId || "-"} /> <AdminFormReadOnlyField label="用户" value={userLabel || userId || "-"} />
<AdminFormReadOnlyField label="钱包" value={salaryWalletAssetType(role) || "-"} />
</AdminFormFieldGrid> </AdminFormFieldGrid>
</AdminFormSection> </AdminFormSection>
<AdminFormSection title="操作信息"> <AdminFormSection title="操作信息">
@ -260,7 +257,6 @@ function SalaryWalletHistoryDrawer({ onClose, open, role, userId, userLabel }) {
<div className={styles.salaryWalletSummary}> <div className={styles.salaryWalletSummary}>
<AccountBalanceWalletOutlined fontSize="small" /> <AccountBalanceWalletOutlined fontSize="small" />
<span>{salaryWalletRoleLabel(role)}</span> <span>{salaryWalletRoleLabel(role)}</span>
<span>{salaryWalletAssetType(role) || "-"}</span>
</div> </div>
<DataState error={error} loading={loading} onRetry={loadHistory}> <DataState error={error} loading={loading} onRetry={loadHistory}>
<DataTable <DataTable
@ -317,21 +313,6 @@ function parseSalaryUSDMinor(value) {
return amountMinor; return amountMinor;
} }
function salaryWalletAssetType(role) {
switch (role) {
case "host":
return "HOST_SALARY_USD";
case "agency":
return "AGENCY_SALARY_USD";
case "bd":
return "BD_SALARY_USD";
case "bd_leader":
return "ADMIN_SALARY_USD";
default:
return "";
}
}
function salaryWalletRoleLabel(role) { function salaryWalletRoleLabel(role) {
switch (role) { switch (role) {
case "host": case "host":

View File

@ -307,15 +307,6 @@
white-space: nowrap; white-space: nowrap;
} }
.salaryWalletAsset {
max-width: 100%;
overflow: hidden;
color: var(--text-tertiary);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.salaryWalletDrawer.salaryWalletDrawer { .salaryWalletDrawer.salaryWalletDrawer {
width: min(980px, calc(100vw - 32px)); width: min(980px, calc(100vw - 32px));
} }

View File

@ -80,6 +80,9 @@ export function DataTable({
); );
const gridTemplate = preparedColumns.map((column) => column.width).join(" "); const gridTemplate = preparedColumns.map((column) => column.width).join(" ");
const tableContentWidth = `${Math.ceil(
preparedColumns.reduce((total, column) => total + readMinWidth(column.width), 0)
)}px`;
const activeFilterColumn = preparedColumns.find((column) => column.key === filterMenu.columnKey && column.filter) || null; const activeFilterColumn = preparedColumns.find((column) => column.key === filterMenu.columnKey && column.filter) || null;
const activeFilter = activeFilterColumn?.filter || null; const activeFilter = activeFilterColumn?.filter || null;
const activeFilterIsText = activeFilter?.type === "text"; const activeFilterIsText = activeFilter?.type === "text";
@ -267,7 +270,11 @@ export function DataTable({
<div className="table-scroll" ref={scrollRef}> <div className="table-scroll" ref={scrollRef}>
<div <div
className={["admin-table", resizeSession ? "admin-table--resizing" : "", tableClassName].filter(Boolean).join(" ")} className={["admin-table", resizeSession ? "admin-table--resizing" : "", tableClassName].filter(Boolean).join(" ")}
style={{ "--admin-table-columns": gridTemplate, "--admin-table-min-width": minWidth }} style={{
"--admin-table-columns": gridTemplate,
"--admin-table-content-width": tableContentWidth,
"--admin-table-min-width": minWidth
}}
> >
<div className="admin-row admin-row--head"> <div className="admin-row admin-row--head">
{preparedColumns.map((column, index) => { {preparedColumns.map((column, index) => {

View File

@ -61,6 +61,22 @@ test("only shows the table filter reset action when filters are active", () => {
expect(screen.getByRole("button", { name: "重置筛选" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "重置筛选" })).toBeInTheDocument();
}); });
test("expands table background to the minimum width required by columns", () => {
const columns = [
{ key: "user", label: "用户", width: "minmax(220px, 1fr)" },
{ key: "createdAt", label: "创建时间", width: "minmax(180px, 1fr)" },
{ key: "actions", label: "操作" }
];
render(
<TimeZoneProvider>
<DataTable columns={columns} items={[{ createdAt: "2026-06-15", id: 1, user: "admin" }]} minWidth="300px" rowKey={(item) => item.id} />
</TimeZoneProvider>
);
expect(document.querySelector(".admin-table")?.style.getPropertyValue("--admin-table-content-width")).toBe("512px");
});
function TimezoneTableFixture() { function TimezoneTableFixture() {
const { setTimeZone } = useTimeZone(); const { setTimeZone } = useTimeZone();
const columns = [ const columns = [

View File

@ -8,7 +8,7 @@ export function ToastProvider({ children }) {
const [toast, setToast] = useState(null); const [toast, setToast] = useState(null);
const showToast = useCallback((message, severity = "info") => { const showToast = useCallback((message, severity = "info") => {
setToast({ message, severity }); setToast(normalizeToast(message, severity));
}, []); }, []);
const value = useMemo(() => ({ showToast }), [showToast]); const value = useMemo(() => ({ showToast }), [showToast]);
@ -39,3 +39,16 @@ export function useToast() {
} }
return context; return context;
} }
function normalizeToast(message, severity) {
if (message && typeof message === "object") {
return {
message: String(message.message || "操作失败"),
severity: message.severity || severity || "info"
};
}
return {
message: String(message || "操作失败"),
severity: severity || "info"
};
}

View File

@ -0,0 +1,36 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import { ToastProvider, useToast } from "./ToastProvider.jsx";
function ToastTrigger({ payload, severity }) {
const { showToast } = useToast();
return (
<button type="button" onClick={() => showToast(payload, severity)}>
show
</button>
);
}
test("renders object-style toast payloads as text", () => {
render(
<ToastProvider>
<ToastTrigger payload={{ message: "工资结算失败", severity: "error" }} />
</ToastProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "show" }));
expect(screen.getByText("工资结算失败")).toBeInTheDocument();
});
test("keeps tuple-style toast payloads working", () => {
render(
<ToastProvider>
<ToastTrigger payload="区域已更新" severity="success" />
</ToastProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "show" }));
expect(screen.getByText("区域已更新")).toBeInTheDocument();
});

View File

@ -1546,9 +1546,9 @@
--admin-table-cell-x: var(--space-3); --admin-table-cell-x: var(--space-3);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: max(100%, var(--admin-table-min-width, 900px)); width: max(100%, var(--admin-table-min-width, 900px), var(--admin-table-content-width, 0px));
min-height: 100%; min-height: 100%;
min-width: max(100%, var(--admin-table-min-width, 900px)); min-width: max(100%, var(--admin-table-min-width, 900px), var(--admin-table-content-width, 0px));
} }
.admin-row { .admin-row {