342 lines
13 KiB
JavaScript
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 || "-"}`;
|
|
}
|