328 lines
12 KiB
JavaScript
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");
|
|
}
|