389 lines
15 KiB
JavaScript
389 lines
15 KiB
JavaScript
import Add from "@mui/icons-material/Add";
|
|
import CategoryOutlined from "@mui/icons-material/CategoryOutlined";
|
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
|
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
|
import TextField from "@mui/material/TextField";
|
|
import {
|
|
AdminFormDialog,
|
|
AdminFormFieldGrid,
|
|
AdminFormInlineActions,
|
|
AdminFormList,
|
|
AdminFormListRow,
|
|
AdminFormSection,
|
|
AdminFormSwitchField,
|
|
} from "@/shared/ui/AdminFormDialog.jsx";
|
|
import { Button } from "@/shared/ui/Button.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 { ResourceSelectField } from "@/features/resources/components/ResourceSelectDrawer.jsx";
|
|
import { resourceGroupAssetLabels, resourceStatusFilters, resourceTypeLabels } from "@/features/resources/constants.js";
|
|
import { useResourceGroupListPage } from "@/features/resources/hooks/useResourcePages.js";
|
|
import styles from "@/features/resources/resources.module.css";
|
|
|
|
const dayMillis = 24 * 60 * 60 * 1000;
|
|
|
|
const baseColumns = [
|
|
{
|
|
key: "group",
|
|
label: "资源组",
|
|
width: "minmax(260px, 1.5fr)",
|
|
render: (group) => <GroupIdentity group={group} />,
|
|
},
|
|
{
|
|
key: "items",
|
|
label: "组内资源",
|
|
width: "minmax(320px, 1.7fr)",
|
|
render: (group) => <GroupItems group={group} />,
|
|
},
|
|
{
|
|
key: "sort",
|
|
label: "排序",
|
|
width: "minmax(80px, 0.45fr)",
|
|
render: (group) => group.sortOrder ?? 0,
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "状态",
|
|
width: "minmax(92px, 0.55fr)",
|
|
},
|
|
{
|
|
key: "time",
|
|
label: "更新时间",
|
|
width: "minmax(170px, 0.9fr)",
|
|
render: (group) => formatMillis(group.updatedAtMs || group.createdAtMs),
|
|
},
|
|
{
|
|
key: "actions",
|
|
label: "操作",
|
|
width: "minmax(76px, 0.4fr)",
|
|
},
|
|
];
|
|
|
|
export function ResourceGroupListPage() {
|
|
const page = useResourceGroupListPage();
|
|
const items = page.data.items || [];
|
|
const total = page.data.total || 0;
|
|
const columns = baseColumns.map((column) =>
|
|
column.key === "group"
|
|
? {
|
|
...column,
|
|
filter: createTextColumnFilter({
|
|
placeholder: "搜索资源组名称、资源组编码",
|
|
value: page.query,
|
|
onChange: page.changeQuery,
|
|
}),
|
|
}
|
|
: column.key === "status"
|
|
? {
|
|
...column,
|
|
filter: createOptionsColumnFilter({
|
|
options: resourceStatusFilters,
|
|
placeholder: "搜索状态",
|
|
value: page.status,
|
|
onChange: page.changeStatus,
|
|
}),
|
|
render: (group) => <ResourceGroupStatusSwitch group={group} page={page} />,
|
|
}
|
|
: column.key === "actions"
|
|
? {
|
|
...column,
|
|
render: (group) => <ResourceGroupRowActions group={group} page={page} />,
|
|
}
|
|
: column,
|
|
);
|
|
|
|
return (
|
|
<AdminListPage>
|
|
<AdminListToolbar
|
|
actions={
|
|
page.abilities.canCreateGroup ? (
|
|
<AdminActionIconButton label="添加资源组" primary onClick={page.openCreateGroup}>
|
|
<Add fontSize="small" />
|
|
</AdminActionIconButton>
|
|
) : null
|
|
}
|
|
/>
|
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
|
<AdminListBody>
|
|
<DataTable
|
|
columns={columns}
|
|
items={items}
|
|
minWidth="1060px"
|
|
pagination={
|
|
total > 0
|
|
? {
|
|
page: page.page,
|
|
pageSize: page.data.pageSize || 50,
|
|
total,
|
|
onPageChange: page.setPage,
|
|
}
|
|
: undefined
|
|
}
|
|
rowKey={(group) => group.groupId}
|
|
/>
|
|
</AdminListBody>
|
|
</DataState>
|
|
<ResourceGroupFormDialog
|
|
disabled={
|
|
page.activeAction === "edit" ? !page.abilities.canUpdateGroup : !page.abilities.canCreateGroup
|
|
}
|
|
form={page.form}
|
|
loading={
|
|
page.activeAction === "edit"
|
|
? page.loadingAction === "group-edit"
|
|
: page.loadingAction === "group-create"
|
|
}
|
|
mode={page.activeAction === "edit" ? "edit" : "create"}
|
|
open={page.activeAction === "create" || page.activeAction === "edit"}
|
|
page={page}
|
|
onClose={page.closeAction}
|
|
onSubmit={page.submitGroup}
|
|
/>
|
|
</AdminListPage>
|
|
);
|
|
}
|
|
|
|
function ResourceGroupRowActions({ group, page }) {
|
|
return (
|
|
<AdminRowActions>
|
|
<AdminActionIconButton
|
|
disabled={!page.abilities.canUpdateGroup}
|
|
label="编辑资源组"
|
|
onClick={() => page.openEditGroup(group)}
|
|
>
|
|
<EditOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
</AdminRowActions>
|
|
);
|
|
}
|
|
|
|
function ResourceGroupStatusSwitch({ group, page }) {
|
|
const checked = group.status === "active";
|
|
return (
|
|
<AdminSwitch
|
|
checked={checked}
|
|
disabled={!page.abilities.canUpdateGroup || page.loadingAction === `group-status-${group.groupId}`}
|
|
inputProps={{ "aria-label": checked ? "禁用资源组" : "启用资源组" }}
|
|
onChange={(event) => page.toggleGroup(group, event.target.checked)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function ResourceGroupFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open, page }) {
|
|
const submitDisabled = disabled || loading || page.resourceOptionsLoading;
|
|
return (
|
|
<AdminFormDialog
|
|
loading={loading}
|
|
open={open}
|
|
size="wide"
|
|
submitDisabled={submitDisabled}
|
|
title={mode === "edit" ? "编辑资源组" : "添加资源组"}
|
|
onClose={onClose}
|
|
onSubmit={onSubmit}
|
|
>
|
|
<AdminFormSection title="基础信息">
|
|
<AdminFormFieldGrid columns="minmax(210px, 1fr) minmax(210px, 1fr) 112px">
|
|
<TextField
|
|
disabled={disabled}
|
|
label="资源组名称"
|
|
required
|
|
value={form.name}
|
|
onChange={(event) => page.setForm({ ...form, name: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="资源组编码"
|
|
required
|
|
value={form.groupCode}
|
|
onChange={(event) => page.setForm({ ...form, groupCode: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="排序"
|
|
type="number"
|
|
value={form.sortOrder}
|
|
onChange={(event) => page.setForm({ ...form, sortOrder: event.target.value })}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="说明"
|
|
multiline
|
|
minRows={2}
|
|
value={form.description}
|
|
onChange={(event) => page.setForm({ ...form, description: event.target.value })}
|
|
/>
|
|
<AdminFormSwitchField
|
|
checked={form.enabled}
|
|
checkedLabel="启用"
|
|
disabled={disabled}
|
|
label="资源组状态"
|
|
uncheckedLabel="禁用"
|
|
onChange={(checked) => page.setForm({ ...form, enabled: checked })}
|
|
/>
|
|
</AdminFormSection>
|
|
<AdminFormSection
|
|
title="组内资源"
|
|
actions={
|
|
<AdminFormInlineActions>
|
|
<Button
|
|
disabled={disabled || loading}
|
|
onClick={page.addResourceItem}
|
|
startIcon={<Inventory2Outlined fontSize="small" />}
|
|
>
|
|
资源
|
|
</Button>
|
|
<Button
|
|
disabled={disabled || loading}
|
|
onClick={() => page.addWalletAssetItem("COIN")}
|
|
startIcon={<MonetizationOnOutlined fontSize="small" />}
|
|
>
|
|
金币
|
|
</Button>
|
|
</AdminFormInlineActions>
|
|
}
|
|
>
|
|
<AdminFormList>
|
|
{form.items.map((item, index) => (
|
|
<ResourceGroupItemEditor
|
|
disabled={disabled || loading}
|
|
index={index}
|
|
item={item}
|
|
key={`${item.itemType}-${item.walletAssetType || item.resourceId || index}-${index}`}
|
|
page={page}
|
|
/>
|
|
))}
|
|
</AdminFormList>
|
|
</AdminFormSection>
|
|
</AdminFormDialog>
|
|
);
|
|
}
|
|
|
|
function ResourceGroupItemEditor({ disabled, index, item, page }) {
|
|
if (item.itemType === "wallet_asset") {
|
|
const label = resourceGroupAssetLabels[item.walletAssetType] || "钱包资产";
|
|
return (
|
|
<AdminFormListRow>
|
|
<TextField disabled label="类型" value={label} />
|
|
<TextField
|
|
disabled={disabled}
|
|
label={`${label}数量`}
|
|
required
|
|
type="number"
|
|
value={item.walletAssetAmount}
|
|
onChange={(event) => page.updateGroupItem(index, { walletAssetAmount: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="排序"
|
|
type="number"
|
|
value={item.sortOrder}
|
|
onChange={(event) => page.updateGroupItem(index, { sortOrder: event.target.value })}
|
|
/>
|
|
<AdminActionIconButton disabled={disabled} label="移除条目" onClick={() => page.removeGroupItem(index)}>
|
|
<DeleteOutlineOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
</AdminFormListRow>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AdminFormListRow>
|
|
<ResourceSelectField
|
|
disabled={disabled || page.resourceOptionsLoading}
|
|
drawerTitle="选择资源"
|
|
label="资源"
|
|
loading={page.resourceOptionsLoading}
|
|
resources={page.resourceOptions}
|
|
value={item.resourceId}
|
|
onChange={(resourceId) => page.updateGroupItem(index, { resourceId })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="有效天数"
|
|
required
|
|
type="number"
|
|
value={item.durationDays}
|
|
onChange={(event) => page.updateGroupItem(index, { durationDays: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="排序"
|
|
type="number"
|
|
value={item.sortOrder}
|
|
onChange={(event) => page.updateGroupItem(index, { sortOrder: event.target.value })}
|
|
/>
|
|
<AdminActionIconButton disabled={disabled} label="移除条目" onClick={() => page.removeGroupItem(index)}>
|
|
<DeleteOutlineOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
</AdminFormListRow>
|
|
);
|
|
}
|
|
|
|
function GroupIdentity({ group }) {
|
|
return (
|
|
<div className={styles.identity}>
|
|
<span className={styles.thumb}>
|
|
<CategoryOutlined fontSize="small" />
|
|
</span>
|
|
<div className={styles.identityText}>
|
|
<span className={styles.name}>{group.name || "-"}</span>
|
|
<span className={styles.meta}>{group.groupCode || group.groupId}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GroupItems({ group }) {
|
|
const items = group.items || [];
|
|
if (!items.length) {
|
|
return "-";
|
|
}
|
|
return (
|
|
<div className="admin-tag-list">
|
|
{items.slice(0, 4).map((item) => {
|
|
if (item.itemType === "wallet_asset") {
|
|
const label = resourceGroupAssetLabels[item.walletAssetType] || item.walletAssetType || "钱包资产";
|
|
return (
|
|
<span
|
|
className="admin-tag"
|
|
key={item.groupItemId || `${item.walletAssetType}-${item.walletAssetAmount}`}
|
|
>
|
|
{label} {formatNumber(item.walletAssetAmount)}
|
|
</span>
|
|
);
|
|
}
|
|
const resource = item.resource || {};
|
|
const type = resourceTypeLabels[resource.resourceType] || resource.resourceType || "资源";
|
|
return (
|
|
<span className="admin-tag" key={item.groupItemId || item.resourceId}>
|
|
{resource.name || item.resourceId} · {formatDurationDays(item.durationMs)}天 · {type}
|
|
</span>
|
|
);
|
|
})}
|
|
{items.length > 4 ? <span className="admin-tag">+{items.length - 4}</span> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatDurationDays(durationMs) {
|
|
const days = Number(durationMs || 0) / dayMillis;
|
|
return Number.isInteger(days) ? days : days.toFixed(1);
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
return Number(value || 0).toLocaleString("zh-CN");
|
|
}
|