hyapp-admin-platform/src/features/achievements/pages/AchievementConfigPage.jsx
2026-07-02 19:23:47 +08:00

342 lines
13 KiB
JavaScript

import Add from "@mui/icons-material/Add";
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import EditOutlined from "@mui/icons-material/EditOutlined";
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { useState } from "react";
import { AdminFormDialog, AdminFormFieldGrid, AdminFormSection } from "@/shared/ui/AdminFormDialog.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import {
AdminActionIconButton,
AdminFilterResetButton,
AdminFilterSelect,
AdminListBody,
AdminListPage,
AdminListToolbar,
AdminRowActions,
} from "@/shared/ui/AdminListLayout.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
import {
achievementTypeLabels,
achievementTypeOptions,
collectionTypeFilterOptions,
} from "@/features/achievements/constants.js";
import { useAchievementsPage } from "@/features/achievements/hooks/useAchievementsPage.js";
import styles from "@/features/achievements/achievements.module.css";
const baseColumns = [
{
key: "badge",
label: "徽章",
width: "96px",
},
{
key: "title",
label: "成就名称",
width: "minmax(180px, 1fr)",
render: (achievement) => <span className={styles.name}>{achievement.title || achievement.achievementId}</span>,
},
{
key: "achievementType",
label: "成就类型",
width: "minmax(140px, 0.55fr)",
render: (achievement) => (
<span className={styles.typeBadge}>{achievementTypeLabel(achievement.achievementType)}</span>
),
},
{
key: "sortOrder",
label: "排序",
width: "96px",
render: (achievement) => <span className={styles.meta}>{achievement.sortOrder || 0}</span>,
},
{
key: "description",
label: "成就描述",
width: "minmax(260px, 1.4fr)",
render: (achievement) => achievement.description || <span className={styles.emptyValue}>未填写</span>,
},
{
key: "actions",
label: "操作",
width: "112px",
},
];
export function AchievementConfigPage() {
const page = useAchievementsPage();
const items = page.data.items || [];
const total = page.data.total || 0;
const isEditing = page.activeAction === "edit";
const columns = baseColumns.map((column) => {
if (column.key === "badge") {
return {
...column,
render: (achievement) => (
<BadgeCover id={achievement.primaryBadgeResourceId} resources={page.badgeResources} />
),
};
}
if (column.key === "actions") {
return {
...column,
render: (achievement) => <AchievementRowActions achievement={achievement} page={page} />,
};
}
return column;
});
return (
<AdminListPage>
<AdminListToolbar
filters={
<>
<AdminFilterSelect
label="成就类型"
options={collectionTypeFilterOptions}
value={page.collectionType}
onChange={page.changeCollectionType}
/>
<AdminFilterResetButton disabled={!page.collectionType} onClick={page.resetFilters} />
</>
}
actions={
page.abilities.canCreate ? (
<AdminActionIconButton label="添加成就" primary onClick={page.openCreate}>
<Add fontSize="small" />
</AdminActionIconButton>
) : null
}
/>
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<AdminListBody>
<DataTable
columns={columns}
items={items}
minWidth="900px"
pagination={
total > 0
? {
page: page.page,
pageSize: page.data.pageSize || 50,
total,
onPageChange: page.setPage,
}
: undefined
}
rowKey={(achievement) => achievement.achievementId}
/>
</AdminListBody>
</DataState>
<AchievementFormDialog
disabled={isEditing ? !page.abilities.canUpdate : !page.abilities.canCreate}
form={page.form}
loading={isEditing ? page.loadingAction === "edit" : page.loadingAction === "create"}
mode={isEditing ? "edit" : "create"}
open={page.activeAction === "create" || page.activeAction === "edit"}
page={page}
onClose={page.closeAction}
onSubmit={page.submitAchievement}
/>
</AdminListPage>
);
}
function AchievementRowActions({ achievement, page }) {
return (
<AdminRowActions>
<AdminActionIconButton
disabled={!page.abilities.canUpdate}
label="编辑成就"
onClick={() => page.openEdit(achievement)}
>
<EditOutlined fontSize="small" />
</AdminActionIconButton>
<AdminActionIconButton
disabled={!page.abilities.canUpdate || page.loadingAction === `delete-${achievement.achievementId}`}
label="删除成就"
onClick={() => page.deleteAchievement(achievement)}
>
<DeleteOutlineOutlined fontSize="small" />
</AdminActionIconButton>
</AdminRowActions>
);
}
function AchievementFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open, page }) {
return (
<AdminFormDialog
loading={loading}
open={open}
size="wide"
submitDisabled={disabled || loading}
submitLabel={mode === "edit" ? "保存成就" : "创建成就"}
title={mode === "edit" ? "编辑成就" : "创建成就"}
onClose={onClose}
onSubmit={onSubmit}
>
<AdminFormSection title="基础信息">
<AdminFormFieldGrid>
<BadgeResourceSelectField
disabled={disabled}
label="选择徽章资源"
resources={page.badgeResources}
value={form.primaryBadgeResourceId}
onChange={(value) => page.setForm((current) => ({ ...current, primaryBadgeResourceId: value }))}
/>
<TextField
disabled={disabled}
label="成就类型"
required
select
value={form.achievementType}
onChange={changeField(page.setForm, "achievementType")}
>
{achievementTypeOptions.map(([value, label]) => (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
))}
</TextField>
<TextField
disabled={disabled}
label="成就名称"
required
value={form.title}
onChange={changeField(page.setForm, "title")}
/>
<TextField
disabled={disabled}
label="排序"
type="number"
value={form.sortOrder}
onChange={changeField(page.setForm, "sortOrder")}
/>
<TextField
disabled={disabled}
label="描述"
value={form.description}
onChange={changeField(page.setForm, "description")}
/>
</AdminFormFieldGrid>
</AdminFormSection>
</AdminFormDialog>
);
}
function BadgeCover({ id, resources }) {
const resource = findResource(resources, id);
const imageUrl = resourceImageUrl(resource);
return (
<span className={styles.badgeCover}>
{imageUrl ? <img alt="" src={imageUrl} /> : <Inventory2Outlined fontSize="small" />}
</span>
);
}
function BadgeResourceSelectField({ disabled, label, onChange, resources, value }) {
const [open, setOpen] = useState(false);
const normalizedValue = String(value || "");
const selected = findResource(resources, normalizedValue);
const displayValue = selected ? resourceName(selected) : normalizedValue ? `资源 #${normalizedValue}` : "";
const options = withSelectedResource(resources, normalizedValue);
const openDrawer = () => {
if (!disabled) {
setOpen(true);
}
};
const selectResource = (resourceId) => {
onChange(String(resourceId || ""));
setOpen(false);
};
return (
<>
<TextField
fullWidth
className={styles.resourceField}
disabled={disabled}
label={label}
required
slotProps={{ htmlInput: { "aria-haspopup": "dialog", readOnly: true, role: "button" } }}
value={displayValue}
onClick={openDrawer}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
openDrawer();
}
}}
/>
<SideDrawer
className={styles.resourceDrawer}
contentClassName={styles.resourceDrawerBody}
drawerProps={{ sx: { zIndex: 1600 } }}
open={open}
title="选择徽章资源"
width="wide"
onClose={() => setOpen(false)}
>
<div className={styles.resourceGrid}>
{options.map((resource) => {
const resourceValue = String(resource.resourceId);
const selectedResource = resourceValue === normalizedValue;
return (
<button
className={[styles.resourceCard, selectedResource ? styles.resourceCardSelected : ""]
.filter(Boolean)
.join(" ")}
key={resource.resourceId}
type="button"
onClick={() => selectResource(resource.resourceId)}
>
<BadgeCover id={resource.resourceId} resources={options} />
<span className={styles.resourceName}>{resourceName(resource)}</span>
<span className={styles.meta}>
{resource.resourceCode || `ID ${resource.resourceId}`}
</span>
</button>
);
})}
{!options.length ? <div className={styles.emptyState}>当前无数据</div> : null}
</div>
</SideDrawer>
</>
);
}
function changeField(setter, key) {
return (event) => {
const value = event.target.value;
setter((current) => ({ ...current, [key]: value }));
};
}
function withSelectedResource(resources, value) {
const selectedId = Number(value || 0);
if (!selectedId || resources.some((resource) => Number(resource.resourceId) === selectedId)) {
return resources;
}
return [{ resourceId: selectedId, name: `资源 #${selectedId}` }, ...resources];
}
function achievementTypeLabel(type) {
return achievementTypeLabels[type] || type || "-";
}
function findResource(resources, id) {
return resources.find((item) => Number(item.resourceId) === Number(id));
}
function resourceImageUrl(resource) {
return resource?.previewUrl || resource?.assetUrl || resource?.animationUrl || "";
}
function resourceName(resource) {
return resource?.name || resource?.resourceCode || `资源 #${resource?.resourceId || "-"}`;
}