2026-05-09 13:36:49 +08:00

328 lines
12 KiB
JavaScript

import Add from "@mui/icons-material/Add";
import EditOutlined from "@mui/icons-material/EditOutlined";
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
import FormControlLabel from "@mui/material/FormControlLabel";
import MenuItem from "@mui/material/MenuItem";
import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField";
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { UploadField } from "@/shared/ui/UploadField.jsx";
import {
AdminActionIconButton,
AdminFilterResetButton,
AdminListBody,
AdminListPage,
AdminRowActions,
AdminListToolbar,
} from "@/shared/ui/AdminListLayout.jsx";
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
import { formatMillis } from "@/shared/utils/time.js";
import {
grantStrategyLabels,
resourceStatusFilters,
resourceTypeFilters,
resourceTypeLabels,
} from "@/features/resources/constants.js";
import { useResourceListPage } from "@/features/resources/hooks/useResourcePages.js";
import styles from "@/features/resources/resources.module.css";
const resourceTypeOptions = resourceTypeFilters.filter(([value]) => value);
const baseColumns = [
{
key: "resource",
label: "资源",
width: "minmax(260px, 1.5fr)",
render: (resource) => <ResourceIdentity resource={resource} />,
},
{
key: "type",
label: "类型",
width: "minmax(120px, 0.7fr)",
render: (resource) => resourceTypeLabels[resource.resourceType] || resource.resourceType || "-",
},
{
key: "grant",
label: "发放规则",
width: "minmax(170px, 1fr)",
render: (resource) => <GrantInfo resource={resource} />,
},
{
key: "scopes",
label: "使用范围",
width: "minmax(180px, 1fr)",
render: (resource) => <TagList values={resource.usageScopes} />,
},
{
key: "status",
label: "状态",
width: "minmax(92px, 0.55fr)",
},
{
key: "time",
label: "更新时间",
width: "minmax(170px, 0.9fr)",
render: (resource) => formatMillis(resource.updatedAtMs || resource.createdAtMs),
},
{
key: "actions",
label: "操作",
width: "minmax(76px, 0.4fr)",
},
];
export function ResourceListPage() {
const page = useResourceListPage();
const items = page.data.items || [];
const total = page.data.total || 0;
const createDisabled = !page.abilities.canCreate || !page.abilities.canUpload;
const columns = baseColumns.map((column) =>
column.key === "resource"
? {
...column,
filter: createTextColumnFilter({
placeholder: "搜索资源名称、资源编码",
value: page.query,
onChange: page.changeQuery,
}),
}
: column.key === "type"
? {
...column,
filter: createOptionsColumnFilter({
options: resourceTypeFilters,
placeholder: "搜索类型",
value: page.resourceType,
onChange: page.changeResourceType,
}),
}
: column.key === "status"
? {
...column,
filter: createOptionsColumnFilter({
options: resourceStatusFilters,
placeholder: "搜索状态",
value: page.status,
onChange: page.changeStatus,
}),
render: (resource) => <ResourceStatusSwitch page={page} resource={resource} />,
}
: column.key === "actions"
? {
...column,
render: (resource) => <ResourceRowActions page={page} resource={resource} />,
}
: column,
);
return (
<AdminListPage>
<AdminListToolbar
filters={
<AdminFilterResetButton
disabled={!page.query && !page.status && !page.resourceType}
onClick={page.resetFilters}
/>
}
actions={
page.abilities.canCreate ? (
<AdminActionIconButton label="添加资源" primary onClick={page.openCreateResource}>
<Add fontSize="small" />
</AdminActionIconButton>
) : null
}
/>
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<AdminListBody>
<DataTable
columns={columns}
items={items}
minWidth="1120px"
rowKey={(resource) => resource.resourceId}
/>
{total > 0 ? (
<PaginationBar
page={page.page}
pageSize={page.data.pageSize || 10}
total={total}
onPageChange={page.setPage}
/>
) : null}
</AdminListBody>
</DataState>
<ResourceFormDialog
disabled={page.activeAction === "edit" ? !page.abilities.canUpdate : createDisabled}
form={page.form}
loading={
page.activeAction === "edit"
? page.loadingAction === "resource-edit"
: page.loadingAction === "resource-create"
}
mode={page.activeAction === "edit" ? "edit" : "create"}
open={page.activeAction === "create" || page.activeAction === "edit"}
setForm={page.setForm}
uploadDisabled={!page.abilities.canUpload}
onClose={page.closeAction}
onSubmit={page.submitResource}
/>
</AdminListPage>
);
}
function ResourceRowActions({ page, resource }) {
return (
<AdminRowActions>
<AdminActionIconButton
disabled={!page.abilities.canUpdate}
label="编辑资源"
onClick={() => page.openEditResource(resource)}
>
<EditOutlined fontSize="small" />
</AdminActionIconButton>
</AdminRowActions>
);
}
function ResourceFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open, setForm, uploadDisabled }) {
const submitDisabled = disabled || loading;
return (
<AdminFormDialog
loading={loading}
open={open}
submitDisabled={submitDisabled}
title={mode === "edit" ? "编辑资源" : "添加资源"}
onClose={onClose}
onSubmit={onSubmit}
>
<TextField
label="资源名称"
required
value={form.name}
onChange={(event) => setForm({ ...form, name: event.target.value })}
/>
<TextField
label="资源编码"
required
value={form.resourceCode}
onChange={(event) => setForm({ ...form, resourceCode: event.target.value })}
/>
<TextField
label="资源类型"
required
select
value={form.resourceType}
onChange={(event) => setForm({ ...form, resourceType: event.target.value, walletAssetAmount: "" })}
>
{resourceTypeOptions.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
{form.resourceType === "coin" ? (
<TextField
label="金币数量"
required
type="number"
value={form.walletAssetAmount}
onChange={(event) => setForm({ ...form, walletAssetAmount: event.target.value })}
/>
) : null}
<UploadField
disabled={uploadDisabled}
label="资源封面"
value={form.previewUrl}
onChange={(previewUrl) => setForm({ ...form, previewUrl })}
/>
<UploadField
disabled={uploadDisabled}
label="资源素材"
value={form.assetUrl}
onChange={(assetUrl) => setForm({ ...form, assetUrl })}
/>
<FormControlLabel
control={
<Switch
checked={form.enabled}
onChange={(event) => setForm({ ...form, enabled: event.target.checked })}
/>
}
label={form.enabled ? "启用" : "禁用"}
/>
</AdminFormDialog>
);
}
function ResourceIdentity({ resource }) {
const preview = imageURL(resource.previewUrl || resource.assetUrl);
return (
<div className={styles.identity}>
<span className={styles.thumb}>
{preview ? <img src={preview} alt="" /> : <Inventory2Outlined fontSize="small" />}
</span>
<div className={styles.identityText}>
<span className={styles.name}>{resource.name || "-"}</span>
<span className={styles.meta}>{resource.resourceCode || resource.resourceId}</span>
</div>
</div>
);
}
function GrantInfo({ resource }) {
const strategy = grantStrategyLabels[resource.grantStrategy] || resource.grantStrategy || "-";
if (resource.walletAssetType || resource.walletAssetAmount) {
return (
<div className={styles.stack}>
<span>{strategy}</span>
<span className={styles.meta}>
{resource.walletAssetType || "-"} {formatNumber(resource.walletAssetAmount)}
</span>
</div>
);
}
return strategy;
}
function ResourceStatusSwitch({ page, resource }) {
const checked = resource.status === "active";
return (
<Switch
checked={checked}
disabled={!page.abilities.canUpdate || page.loadingAction === `resource-status-${resource.resourceId}`}
inputProps={{ "aria-label": checked ? "禁用资源" : "启用资源" }}
onChange={(event) => page.toggleResource(resource, event.target.checked)}
/>
);
}
function TagList({ values = [] }) {
const items = values.filter(Boolean);
if (!items.length) {
return "-";
}
return (
<div className="admin-tag-list">
{items.map((value) => (
<span className="admin-tag" key={value}>
{value}
</span>
))}
</div>
);
}
function imageURL(value) {
const url = String(value || "").trim();
if (!url) {
return "";
}
return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : "";
}
function formatNumber(value) {
return Number(value || 0).toLocaleString("zh-CN");
}