399 lines
15 KiB
JavaScript
399 lines
15 KiB
JavaScript
import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined";
|
||
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
||
import SearchOutlined from "@mui/icons-material/SearchOutlined";
|
||
import Checkbox from "@mui/material/Checkbox";
|
||
import InputAdornment from "@mui/material/InputAdornment";
|
||
import Tab from "@mui/material/Tab";
|
||
import Tabs from "@mui/material/Tabs";
|
||
import TextField from "@mui/material/TextField";
|
||
import { useMemo, useState } from "react";
|
||
import {
|
||
resourcePriceTypeLabels,
|
||
resourceTypeFilters,
|
||
resourceTypeLabels,
|
||
} from "@/features/resources/constants.js";
|
||
import { Button } from "@/shared/ui/Button.jsx";
|
||
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||
import styles from "@/features/resources/components/GiftResourceSelectDrawer.module.css";
|
||
|
||
const drawerZIndex = 1600;
|
||
|
||
export function GiftResourceSelectField({
|
||
disabled = false,
|
||
drawerTitle = "选择资源",
|
||
excludedResourceIds = [],
|
||
giftTypes = [],
|
||
gifts = [],
|
||
label = "资源",
|
||
loading = false,
|
||
onChange = () => {},
|
||
placeholder = "请选择资源",
|
||
required = true,
|
||
resources = [],
|
||
selectedResourceIds = [],
|
||
}) {
|
||
const [open, setOpen] = useState(false);
|
||
const [query, setQuery] = useState("");
|
||
const [activeType, setActiveType] = useState("");
|
||
const selectedIds = useMemo(() => selectedResourceIds.map((id) => String(id)), [selectedResourceIds]);
|
||
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
||
const excludedIds = useMemo(() => excludedResourceIds.map((id) => String(id)), [excludedResourceIds]);
|
||
const excludedIdSet = useMemo(() => new Set(excludedIds), [excludedIds]);
|
||
const giftsByResourceId = useMemo(() => {
|
||
const items = new Map();
|
||
gifts.forEach((gift) => {
|
||
if (gift?.resourceId) {
|
||
items.set(String(gift.resourceId), gift);
|
||
}
|
||
});
|
||
return items;
|
||
}, [gifts]);
|
||
const resourceItems = useMemo(
|
||
() => buildResourceItems(resources, giftsByResourceId),
|
||
[giftsByResourceId, resources],
|
||
);
|
||
const visibleResourceItems = useMemo(
|
||
() =>
|
||
resourceItems.filter((item) => {
|
||
const resourceId = String(item.resource.resourceId);
|
||
return !excludedIdSet.has(resourceId) || selectedIdSet.has(resourceId);
|
||
}),
|
||
[excludedIdSet, resourceItems, selectedIdSet],
|
||
);
|
||
const visibleResources = useMemo(() => visibleResourceItems.map((item) => item.resource), [visibleResourceItems]);
|
||
const itemCounts = useMemo(() => countResourceItems(visibleResourceItems), [visibleResourceItems]);
|
||
const categoryOptions = useMemo(
|
||
() => buildCategoryOptions(giftTypes, visibleResources, visibleResourceItems),
|
||
[giftTypes, visibleResourceItems, visibleResources],
|
||
);
|
||
const activeCategory = categoryOptions.some((item) => item.value === activeType)
|
||
? activeType
|
||
: categoryOptions.find((item) => itemCounts.get(item.value))?.value || categoryOptions[0]?.value || "";
|
||
const resourcesById = useMemo(() => {
|
||
const items = new Map();
|
||
resourceItems.forEach((item) => {
|
||
items.set(String(item.resource.resourceId), item);
|
||
});
|
||
return items;
|
||
}, [resourceItems]);
|
||
const displayValue = useMemo(() => {
|
||
if (!selectedIds.length) {
|
||
return "";
|
||
}
|
||
const labels = selectedIds.map((resourceId) => resourceName(resourcesById.get(resourceId), resourceId));
|
||
if (labels.length <= 3) {
|
||
return labels.join(",");
|
||
}
|
||
return `${labels.slice(0, 3).join(",")} 等 ${labels.length} 个资源`;
|
||
}, [resourcesById, selectedIds]);
|
||
const filteredResources = useMemo(() => {
|
||
const keyword = query.trim().toLowerCase();
|
||
return visibleResourceItems.filter((item) => {
|
||
if (activeCategory && item.categoryValue !== activeCategory) {
|
||
return false;
|
||
}
|
||
if (!keyword) {
|
||
return true;
|
||
}
|
||
const resource = item.resource || {};
|
||
const gift = item.gift || {};
|
||
const haystack = [
|
||
resource.name,
|
||
resource.resourceCode,
|
||
resource.resourceId,
|
||
resource.resourceType,
|
||
gift.name,
|
||
gift.giftId,
|
||
gift.giftTypeCode,
|
||
]
|
||
.filter(Boolean)
|
||
.join(" ")
|
||
.toLowerCase();
|
||
return haystack.includes(keyword);
|
||
});
|
||
}, [activeCategory, query, visibleResourceItems]);
|
||
|
||
const openDrawer = () => {
|
||
if (!disabled && !loading) {
|
||
setOpen(true);
|
||
}
|
||
};
|
||
|
||
const toggleResource = (item) => {
|
||
if (!item?.resource?.resourceId || disabled) {
|
||
return;
|
||
}
|
||
const resourceId = String(item.resource.resourceId);
|
||
if (selectedIdSet.has(resourceId)) {
|
||
onChange(selectedIds.filter((selectedId) => selectedId !== resourceId));
|
||
return;
|
||
}
|
||
onChange([...selectedIds, resourceId]);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<TextField
|
||
fullWidth
|
||
className={styles.field}
|
||
disabled={disabled || loading}
|
||
label={label}
|
||
placeholder={loading ? "资源加载中" : placeholder}
|
||
required={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
|
||
actions={
|
||
<Button type="button" variant="primary" onClick={() => setOpen(false)}>
|
||
完成
|
||
</Button>
|
||
}
|
||
className={styles.drawer}
|
||
contentClassName={styles.drawerBody}
|
||
drawerProps={{ sx: { zIndex: drawerZIndex } }}
|
||
open={open}
|
||
title={drawerTitle}
|
||
width="wide"
|
||
onClose={() => setOpen(false)}
|
||
>
|
||
<div className={styles.filters}>
|
||
<TextField
|
||
className={styles.search}
|
||
placeholder="搜索资源名称、ID"
|
||
slotProps={{
|
||
input: {
|
||
startAdornment: (
|
||
<InputAdornment position="start">
|
||
<SearchOutlined fontSize="small" />
|
||
</InputAdornment>
|
||
),
|
||
},
|
||
}}
|
||
value={query}
|
||
onChange={(event) => setQuery(event.target.value)}
|
||
/>
|
||
{categoryOptions.length ? (
|
||
<Tabs
|
||
aria-label="资源分类"
|
||
className={styles.tabs}
|
||
value={activeCategory}
|
||
variant="scrollable"
|
||
onChange={(_, value) => setActiveType(value)}
|
||
>
|
||
{categoryOptions.map((item) => (
|
||
<Tab key={item.value} label={item.label} value={item.value} />
|
||
))}
|
||
</Tabs>
|
||
) : null}
|
||
</div>
|
||
<div className={styles.grid}>
|
||
{filteredResources.map((item) => {
|
||
const resourceId = String(item.resource.resourceId);
|
||
const selected = selectedIdSet.has(resourceId);
|
||
const name = resourceName(item);
|
||
return (
|
||
<button
|
||
aria-pressed={selected}
|
||
className={[styles.card, selected ? styles.cardSelected : ""]
|
||
.filter(Boolean)
|
||
.join(" ")}
|
||
key={resourceId}
|
||
type="button"
|
||
onClick={() => toggleResource(item)}
|
||
>
|
||
<Checkbox
|
||
checked={selected}
|
||
className={styles.checkbox}
|
||
inputProps={{
|
||
"aria-label": `${selected ? "取消选择" : "选择"} ${name}`,
|
||
readOnly: true,
|
||
}}
|
||
tabIndex={-1}
|
||
/>
|
||
<ResourceThumb item={item} />
|
||
<span className={styles.name}>{name}</span>
|
||
<span className={styles.meta}>
|
||
{item.resource.resourceCode || `资源 ${item.resource.resourceId || "-"}`}
|
||
</span>
|
||
<span className={styles.tags}>
|
||
<span className={styles.tag}>{categoryLabel(item, categoryOptions)}</span>
|
||
<span className={styles.tag}>{resourcePriceLabel(item)}</span>
|
||
</span>
|
||
</button>
|
||
);
|
||
})}
|
||
{!filteredResources.length ? <div className={styles.empty}>当前无可选资源</div> : null}
|
||
</div>
|
||
</SideDrawer>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function buildResourceItems(resources, giftsByResourceId) {
|
||
const seen = new Set();
|
||
return resources
|
||
.filter((resource) => resource?.resourceId)
|
||
.filter((resource) => {
|
||
const resourceId = String(resource.resourceId);
|
||
if (seen.has(resourceId)) {
|
||
return false;
|
||
}
|
||
seen.add(resourceId);
|
||
return true;
|
||
})
|
||
.map((resource) => {
|
||
const gift = giftsByResourceId.get(String(resource.resourceId));
|
||
const category = categoryForResource(resource, gift);
|
||
return {
|
||
categoryLabel: category.label,
|
||
categoryValue: category.value,
|
||
gift,
|
||
resource,
|
||
};
|
||
});
|
||
}
|
||
|
||
function buildCategoryOptions(giftTypes, resources, resourceItems) {
|
||
const options = [];
|
||
const resourceTypes = new Set(resources.map((resource) => resource?.resourceType).filter(Boolean));
|
||
resourceTypeFilters.forEach(([type, label], index) => {
|
||
if (!type) {
|
||
return;
|
||
}
|
||
if (type === "gift") {
|
||
options.push(...buildGiftCategoryOptions(giftTypes, resourceItems, index));
|
||
if (resourceItems.some((item) => item.categoryValue === "resource:gift")) {
|
||
options.push({ label: resourceTypeLabels.gift || "礼物", order: index * 100, value: "resource:gift" });
|
||
}
|
||
return;
|
||
}
|
||
options.push({ label, order: index * 100, value: `resource:${type}` });
|
||
resourceTypes.delete(type);
|
||
});
|
||
resourceTypes.forEach((type) => {
|
||
options.push({
|
||
label: resourceTypeLabels[type] || type,
|
||
order: options.length * 100,
|
||
value: `resource:${type}`,
|
||
});
|
||
});
|
||
return dedupeCategories(options);
|
||
}
|
||
|
||
function buildGiftCategoryOptions(giftTypes, resourceItems, resourceTypeOrder) {
|
||
const options = [];
|
||
const knownGiftTypes = new Set();
|
||
giftTypes
|
||
.filter((item) => item?.status !== "disabled")
|
||
.forEach((item) => {
|
||
const value = String(item.tabKey || item.typeCode || "").trim();
|
||
if (!value) {
|
||
return;
|
||
}
|
||
knownGiftTypes.add(value);
|
||
options.push({
|
||
label: item.displayName || item.tabName || value,
|
||
order: resourceTypeOrder * 100 + Number(item.sortOrder || 0),
|
||
value: `gift:${value}`,
|
||
});
|
||
});
|
||
resourceItems.forEach((item) => {
|
||
const value = String(item.gift?.giftTypeCode || "").trim();
|
||
if (value && !knownGiftTypes.has(value)) {
|
||
knownGiftTypes.add(value);
|
||
options.push({
|
||
label: value,
|
||
order: resourceTypeOrder * 100 + 1000 + options.length,
|
||
value: `gift:${value}`,
|
||
});
|
||
}
|
||
});
|
||
return options;
|
||
}
|
||
|
||
function dedupeCategories(options) {
|
||
const byValue = new Map();
|
||
options.forEach((item) => {
|
||
if (!byValue.has(item.value)) {
|
||
byValue.set(item.value, item);
|
||
}
|
||
});
|
||
return Array.from(byValue.values()).sort((left, right) => left.order - right.order);
|
||
}
|
||
|
||
function categoryForResource(resource, gift) {
|
||
if (resource.resourceType === "gift" && gift?.giftTypeCode) {
|
||
return { value: `gift:${gift.giftTypeCode}` };
|
||
}
|
||
const type = resource.resourceType || "unknown";
|
||
return {
|
||
label: resourceTypeLabels[type] || type,
|
||
value: `resource:${type}`,
|
||
};
|
||
}
|
||
|
||
function categoryLabel(item, categoryOptions) {
|
||
return categoryOptions.find((option) => option.value === item.categoryValue)?.label || item.categoryLabel || "资源";
|
||
}
|
||
|
||
function countResourceItems(resourceItems) {
|
||
const counts = new Map();
|
||
resourceItems.forEach((item) => {
|
||
counts.set(item.categoryValue, (counts.get(item.categoryValue) || 0) + 1);
|
||
});
|
||
return counts;
|
||
}
|
||
|
||
function ResourceThumb({ item }) {
|
||
const imageUrl = imageURL(item.resource.previewUrl || item.resource.assetUrl || item.resource.animationUrl);
|
||
return (
|
||
<span className={styles.thumb}>
|
||
{imageUrl ? (
|
||
<img alt="" src={imageUrl} />
|
||
) : item.resource.resourceType === "gift" ? (
|
||
<CardGiftcardOutlined fontSize="small" />
|
||
) : (
|
||
<Inventory2Outlined fontSize="small" />
|
||
)}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function resourceName(item, fallbackResourceId = "") {
|
||
const resource = item?.resource || {};
|
||
const gift = item?.gift || {};
|
||
return gift.name || resource.name || resource.resourceCode || `资源 #${fallbackResourceId || resource.resourceId || "-"}`;
|
||
}
|
||
|
||
function resourcePriceLabel(item = {}) {
|
||
const resource = item.resource || {};
|
||
const gift = item.gift || {};
|
||
if (resource.resourceType === "gift" && (gift.chargeAssetType === "COIN" || Number(gift.coinPrice || 0) > 0)) {
|
||
return `${formatNumber(gift.coinPrice)}金币`;
|
||
}
|
||
if (resource.priceType === "coin") {
|
||
return `${formatNumber(resource.coinPrice)}金币`;
|
||
}
|
||
if (resource.priceType === "free") {
|
||
return resourcePriceTypeLabels.free;
|
||
}
|
||
return "未配置价格";
|
||
}
|
||
|
||
function imageURL(value) {
|
||
const url = String(value || "").trim();
|
||
return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : "";
|
||
}
|
||
|
||
function formatNumber(value) {
|
||
return Number(value || 0).toLocaleString("zh-CN");
|
||
}
|