536 lines
19 KiB
JavaScript
536 lines
19 KiB
JavaScript
import Add from "@mui/icons-material/Add";
|
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
|
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 {
|
|
resourceGroupAssetLabels,
|
|
resourceGrantStatusFilters,
|
|
resourceGrantStatusLabels,
|
|
resourceGrantSubjectLabels,
|
|
resourceTypeLabels,
|
|
} 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(220px, 1fr)",
|
|
render: (grant) => (
|
|
<div className={styles.stack}>
|
|
<span className={styles.name}>{grantSourceLabel(grant.grantSource)}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "target",
|
|
label: "用户信息",
|
|
width: "minmax(240px, 1.1fr)",
|
|
render: (grant) => <GrantTargetUser grant={grant} />,
|
|
},
|
|
{
|
|
key: "operator",
|
|
label: "操作人",
|
|
width: "minmax(190px, 0.9fr)",
|
|
render: (grant) => <GrantOperator grant={grant} />,
|
|
},
|
|
{
|
|
key: "subject",
|
|
label: "发放方式",
|
|
width: "minmax(170px, 0.75fr)",
|
|
render: (grant) => <GrantSubject grant={grant} />,
|
|
},
|
|
{
|
|
key: "items",
|
|
label: "发放内容",
|
|
width: "minmax(320px, 1.5fr)",
|
|
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(190px, 0.95fr)",
|
|
render: (grant) => <GrantReason grant={grant} />,
|
|
},
|
|
{
|
|
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="1560px"
|
|
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.grantMediaScroller}>
|
|
{items.map((item) => (
|
|
<GrantItemMedia item={item} key={item.grantItemId || `${grant.grantId}-${item.resourceId}`} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GrantItemMedia({ item }) {
|
|
const snapshot = parseGrantItemSnapshot(item.resourceSnapshotJson);
|
|
const walletAssetType = snapshotValue(snapshot, "walletAssetType", "wallet_asset_type", "WalletAssetType");
|
|
if (walletAssetType) {
|
|
const label = resourceGroupAssetLabels[walletAssetType] || walletAssetType;
|
|
const amount = snapshotValue(snapshot, "walletAssetAmount", "wallet_asset_amount", "WalletAssetAmount", "quantity");
|
|
return <span className={styles.grantAssetPill}>{`${label} ${formatNumber(amount || item.quantity)}`}</span>;
|
|
}
|
|
|
|
const label = grantItemLabel(item, snapshot);
|
|
const imageUrl = grantItemImageUrl(snapshot);
|
|
return (
|
|
<span className={styles.grantMediaCard} title={label}>
|
|
<span className={styles.grantMediaThumb}>
|
|
{imageUrl ? <img alt="" src={imageUrl} /> : <Inventory2Outlined fontSize="small" />}
|
|
</span>
|
|
<span className={styles.grantMediaName}>{label}</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function GrantSubject({ grant }) {
|
|
const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "发放";
|
|
const sourceLabel = grantSourceLabel(grant.grantSource);
|
|
return (
|
|
<div className={styles.stack}>
|
|
<span className={styles.name}>{sourceLabel === "-" ? subjectLabel : `${subjectLabel} · ${sourceLabel}`}</span>
|
|
<span className={styles.meta}>{subjectMeta(grant)}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GrantReason({ grant }) {
|
|
const label = reasonLabel(grant.reason);
|
|
const raw = String(grant.reason || "").trim();
|
|
return (
|
|
<div className={styles.stack}>
|
|
<span className={styles.name}>{label}</span>
|
|
{raw && raw !== label ? <span className={styles.meta}>{raw}</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 }}
|
|
/>
|
|
);
|
|
}
|
|
return "-";
|
|
}
|
|
|
|
const grantSourceLabels = {
|
|
achievement: "成就奖励",
|
|
admin: "后台发放",
|
|
agency_opening: "Agency 开通",
|
|
cp_weekly_rank: "CP 周榜奖励",
|
|
cumulative_recharge_reward: "累充奖励",
|
|
first_recharge_reward: "首充奖励",
|
|
game_robot_init: "机器人初始化",
|
|
growth_level: "成长等级奖励",
|
|
manager_center: "经理中心",
|
|
registration_reward: "注册奖励",
|
|
resource_shop: "道具商店",
|
|
room_rocket: "房间火箭",
|
|
seven_day_checkin: "七日签到",
|
|
weekly_star: "周星奖励",
|
|
wheel_reward: "转盘奖励",
|
|
};
|
|
|
|
const grantReasonLabels = {
|
|
...grantSourceLabels,
|
|
growth_level_registration_badge: "注册成长等级徽章",
|
|
"resource shop purchase": "道具商店购买",
|
|
};
|
|
|
|
function grantSourceLabel(source) {
|
|
const key = String(source || "").trim();
|
|
return grantSourceLabels[key] || key || "-";
|
|
}
|
|
|
|
function reasonLabel(reason) {
|
|
const key = String(reason || "").trim();
|
|
return grantReasonLabels[key] || key || "-";
|
|
}
|
|
|
|
function subjectMeta(grant) {
|
|
const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "对象";
|
|
return grant.grantSubjectId ? `${subjectLabel} ID ${grant.grantSubjectId}` : "-";
|
|
}
|
|
|
|
function grantItemLabel(item, snapshot = parseGrantItemSnapshot(item.resourceSnapshotJson)) {
|
|
const walletAssetType = snapshotValue(snapshot, "walletAssetType", "wallet_asset_type", "WalletAssetType");
|
|
if (walletAssetType) {
|
|
const label = resourceGroupAssetLabels[walletAssetType] || walletAssetType;
|
|
const amount = snapshotValue(snapshot, "walletAssetAmount", "wallet_asset_amount", "WalletAssetAmount", "quantity");
|
|
return `${label} ${formatNumber(amount || item.quantity)}`;
|
|
}
|
|
|
|
const resourceName = snapshotValue(snapshot, "name", "Name", "resourceName", "resource_name") || `资源 ${item.resourceId || "-"}`;
|
|
const resourceType = snapshotValue(snapshot, "resourceType", "resource_type", "ResourceType");
|
|
const typeLabel = resourceTypeLabels[resourceType] || resourceType || "资源";
|
|
const quantity = Number(item.quantity || 0);
|
|
const parts = [`${resourceName} x${formatNumber(quantity)}`, typeLabel];
|
|
if (item.durationMs) {
|
|
parts.push(formatDuration(item.durationMs));
|
|
}
|
|
return parts.join(" · ");
|
|
}
|
|
|
|
function grantItemImageUrl(snapshot) {
|
|
return imageURL(
|
|
snapshotValue(
|
|
snapshot,
|
|
"previewUrl",
|
|
"preview_url",
|
|
"PreviewURL",
|
|
"assetUrl",
|
|
"asset_url",
|
|
"AssetURL",
|
|
"animationUrl",
|
|
"animation_url",
|
|
"AnimationURL",
|
|
),
|
|
);
|
|
}
|
|
|
|
function parseGrantItemSnapshot(value) {
|
|
if (!value || typeof value !== "string") {
|
|
return {};
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function snapshotValue(snapshot, ...keys) {
|
|
for (const key of keys) {
|
|
const value = snapshot?.[key];
|
|
if (value !== undefined && value !== null && value !== "") {
|
|
return value;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
return Number(value || 0).toLocaleString("zh-CN");
|
|
}
|
|
|
|
function imageURL(value) {
|
|
const url = String(value || "").trim();
|
|
return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : "";
|
|
}
|
|
|
|
function formatDuration(durationMs) {
|
|
const days = Number(durationMs || 0) / (24 * 60 * 60 * 1000);
|
|
return Number.isInteger(days) ? `${days}天` : `${days.toFixed(1)}天`;
|
|
}
|