hyapp-admin-platform/src/features/resources/pages/ResourceGrantListPage.jsx
2026-06-24 19:49:05 +08:00

411 lines
15 KiB
JavaScript

import Add from "@mui/icons-material/Add";
import UndoOutlined from "@mui/icons-material/UndoOutlined";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { AdminFormDialog, AdminFormFieldGrid, AdminFormSection } from "@/shared/ui/AdminFormDialog.jsx";
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import {
AdminActionIconButton,
AdminListBody,
AdminListPage,
AdminListToolbar,
AdminRowActions,
} from "@/shared/ui/AdminListLayout.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { formatMillis } from "@/shared/utils/time.js";
import { GiftResourceSelectField } from "@/features/resources/components/GiftResourceSelectDrawer.jsx";
import {
resourceGrantStatusFilters,
resourceGrantStatusLabels,
resourceGrantSubjectLabels,
} from "@/features/resources/constants.js";
import { useResourceGrantListPage } from "@/features/resources/hooks/useResourcePages.js";
import styles from "@/features/resources/resources.module.css";
const grantColumns = [
{
key: "grant",
label: "赠送记录",
width: "minmax(240px, 1.2fr)",
render: (grant) => (
<div className={styles.stack}>
<span className={styles.name}>{grant.grantId || "-"}</span>
<span className={styles.meta}>{grant.commandId || "-"}</span>
</div>
),
},
{
key: "target",
label: "用户信息",
width: "minmax(240px, 1.1fr)",
render: (grant) => <GrantTargetUser grant={grant} />,
},
{
key: "operator",
label: "操作人",
width: "minmax(180px, 1fr)",
render: (grant) => <GrantOperator grant={grant} />,
},
{
key: "subject",
label: "对象",
width: "minmax(160px, 0.8fr)",
render: (grant) => (
<div className={styles.stack}>
<span>{resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "-"}</span>
<span className={styles.meta}>{grant.grantSubjectId || "-"}</span>
</div>
),
},
{
key: "items",
label: "发放内容",
width: "minmax(180px, 1fr)",
render: (grant) => <GrantItems grant={grant} />,
},
{
key: "status",
label: "状态",
width: "minmax(100px, 0.55fr)",
render: (grant) => (
<span className={`status-badge status-badge--${grantStatusTone(grant.status)}`}>
<span className="status-point" />
{resourceGrantStatusLabels[grant.status] || grant.status || "-"}
</span>
),
},
{
key: "reason",
label: "原因",
width: "minmax(160px, 1fr)",
render: (grant) => grant.reason || "-",
},
{
key: "time",
label: "创建时间",
width: "minmax(170px, 0.9fr)",
render: (grant) => formatMillis(grant.createdAtMs),
},
];
export function ResourceGrantListPage() {
const page = useResourceGrantListPage();
const items = page.data.items || [];
const total = page.data.total || 0;
const columns = [
...grantColumns.map((column) => {
if (column.key === "target") {
return {
...column,
filter: createTextColumnFilter({
placeholder: "搜索用户 ID",
value: page.query,
onChange: page.changeQuery,
}),
};
}
if (column.key === "status") {
return {
...column,
filter: createOptionsColumnFilter({
options: resourceGrantStatusFilters,
placeholder: "搜索状态",
value: page.status,
onChange: page.changeStatus,
}),
};
}
return column;
}),
{
key: "actions",
label: "操作",
width: "minmax(88px, 0.45fr)",
render: (grant) => <ResourceGrantActions grant={grant} page={page} />,
},
];
return (
<AdminListPage>
<AdminListToolbar
actions={
page.abilities.canCreateGrant ? (
<AdminActionIconButton label="资源赠送" primary onClick={page.openCreateGrant}>
<Add fontSize="small" />
</AdminActionIconButton>
) : null
}
/>
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<AdminListBody>
<DataTable
columns={columns}
items={items}
minWidth="1460px"
pagination={
total > 0
? {
page: page.page,
pageSize: page.data.pageSize || 50,
total,
onPageChange: page.setPage,
}
: undefined
}
rowKey={(grant) => grant.grantId}
/>
</AdminListBody>
</DataState>
<ResourceGrantDialog
disabled={!page.abilities.canCreateGrant}
form={page.form}
giftOptions={page.giftOptions}
giftTypeOptions={page.giftTypeOptions}
groupOptions={page.groupOptions}
loading={page.loadingAction === "grant-create"}
open={page.activeAction === "create"}
optionsLoading={page.optionsLoading}
resourceOptions={page.resourceOptions}
setForm={page.setForm}
onClose={page.closeAction}
onSubmit={page.submitGrant}
/>
</AdminListPage>
);
}
function ResourceGrantActions({ grant, page }) {
const canRevoke =
page.abilities.canRevokeGrant && grant?.status === "succeeded" && grant?.grantSubjectType === "resource_group";
if (!canRevoke) {
return <span className={styles.meta}>-</span>;
}
return (
<AdminRowActions>
<AdminActionIconButton
aria-label="撤销"
disabled={page.loadingAction === `grant-revoke-${grant.grantId}`}
label="撤销"
sx={dangerActionSx}
tooltip="撤销资源组赠送"
onClick={() => page.revokeGrant(grant)}
>
<UndoOutlined fontSize="small" />
</AdminActionIconButton>
</AdminRowActions>
);
}
function ResourceGrantDialog({
disabled,
form,
giftOptions,
giftTypeOptions,
groupOptions,
loading,
onClose,
onSubmit,
open,
optionsLoading,
resourceOptions,
setForm,
}) {
const submitDisabled = disabled || loading || optionsLoading;
return (
<AdminFormDialog
loading={loading}
open={open}
submitDisabled={submitDisabled}
title="资源赠送"
onClose={onClose}
onSubmit={onSubmit}
>
<AdminFormSection title="赠送对象">
<TextField
disabled={disabled}
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
label="用户 ID / 短号"
required
value={form.targetUserId}
onChange={(event) => setForm({ ...form, targetUserId: event.target.value })}
/>
</AdminFormSection>
<AdminFormSection title="赠送内容">
<AdminFormFieldGrid>
<TextField
disabled={disabled}
label="对象类型"
required
select
value={form.subjectType}
onChange={(event) =>
setForm({
...form,
groupId: "",
resourceId: "",
resourceIds: [],
subjectType: event.target.value,
})
}
>
<MenuItem value="resource">资源</MenuItem>
<MenuItem value="resource_group">资源组</MenuItem>
</TextField>
{form.subjectType === "resource" ? (
<>
<GiftResourceSelectField
drawerTitle="选择资源"
disabled={disabled || optionsLoading}
giftTypes={giftTypeOptions}
gifts={giftOptions}
label="资源"
loading={optionsLoading}
required
resources={resourceOptions}
selectedResourceIds={form.resourceIds || []}
onChange={(resourceIds) =>
setForm({ ...form, resourceId: resourceIds[0] || "", resourceIds })
}
/>
<TextField
disabled={disabled}
label="数量"
required
type="number"
value={form.quantity}
onChange={(event) => setForm({ ...form, quantity: event.target.value })}
/>
<TextField
disabled={disabled}
label="有效天数"
type="number"
value={form.durationDays}
onChange={(event) => setForm({ ...form, durationDays: event.target.value })}
/>
</>
) : (
<TextField
disabled={disabled || optionsLoading}
label="资源组"
required
select
value={form.groupId}
onChange={(event) => setForm({ ...form, groupId: event.target.value })}
>
{groupOptions.map((group) => (
<MenuItem key={group.groupId} value={String(group.groupId)}>
{group.name || group.groupCode || group.groupId}
</MenuItem>
))}
</TextField>
)}
</AdminFormFieldGrid>
</AdminFormSection>
<AdminFormSection title="备注">
<TextField
disabled={disabled}
label="原因"
multiline
minRows={2}
required
value={form.reason}
onChange={(event) => setForm({ ...form, reason: event.target.value })}
/>
</AdminFormSection>
</AdminFormDialog>
);
}
function grantStatusTone(status) {
if (status === "revoked") {
return "stopped";
}
if (status === "failed") {
return "danger";
}
if (status === "pending") {
return "warning";
}
return status || "stopped";
}
const dangerActionSx = {
borderColor: "var(--danger-border)",
color: "var(--danger)",
"&:hover": {
borderColor: "var(--danger-border-strong)",
backgroundColor: "var(--danger-surface)",
color: "var(--danger)",
},
};
function GrantItems({ grant }) {
const items = grant.items || [];
if (!items.length) {
return "-";
}
return (
<div className={styles.stack}>
{items.slice(0, 2).map((item) => (
<span key={item.grantItemId || `${grant.grantId}-${item.resourceId}`}>
{item.resourceId || "-"} / {item.quantity || 0}
{item.durationMs ? ` / ${formatDuration(item.durationMs)}` : ""}
</span>
))}
{items.length > 2 ? <span className={styles.meta}>+{items.length - 2}</span> : null}
</div>
);
}
function GrantTargetUser({ grant }) {
const user = grant.targetUser || grant.target_user || grant.user || {};
const userId = user.userId || user.user_id || grant.targetUserId;
return (
<AdminUserIdentity
openInAppUserDetail
rows={[user.displayUserId || user.display_user_id, userId]}
user={{ ...user, userId }}
/>
);
}
function GrantOperator({ grant }) {
const operator = grant.operator || {};
const source = operator.source || grant.grantSource || "";
const operatorUserId = operator.userId || grant.operatorUserId;
if (source === "manager_center") {
return (
<AdminUserIdentity
openInAppUserDetail
name={operator.username || operator.name}
user={{ ...operator, userId: operatorUserId }}
/>
);
}
if (source === "admin") {
const name = operator.name || operator.username || (operatorUserId ? `Admin ${operatorUserId}` : "-");
return (
<div className={styles.stack}>
<span className={styles.name}>{name}</span>
<span className={styles.meta}>{operator.username || operatorUserId || "-"}</span>
</div>
);
}
if (!operatorUserId) {
return "-";
}
return (
<div className={styles.stack}>
<span className={styles.name}>{operatorUserId}</span>
<span className={styles.meta}>{source || "-"}</span>
</div>
);
}
function formatDuration(durationMs) {
const days = Number(durationMs || 0) / (24 * 60 * 60 * 1000);
return Number.isInteger(days) ? `${days}` : `${days.toFixed(1)}`;
}