import Add from "@mui/icons-material/Add"; import CalendarMonthOutlined from "@mui/icons-material/CalendarMonthOutlined"; import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined"; import ChevronLeftOutlined from "@mui/icons-material/ChevronLeftOutlined"; import ChevronRightOutlined from "@mui/icons-material/ChevronRightOutlined"; import EditOutlined from "@mui/icons-material/EditOutlined"; import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; import IconButton from "@mui/material/IconButton"; import InputAdornment from "@mui/material/InputAdornment"; import MenuItem from "@mui/material/MenuItem"; import Popover from "@mui/material/Popover"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; import TextField from "@mui/material/TextField"; import { useMemo, useState } from "react"; import { AdminFormDialog, AdminFormFieldGrid, AdminFormSection, AdminFormSwitchField, } from "@/shared/ui/AdminFormDialog.jsx"; import { Button } from "@/shared/ui/Button.jsx"; import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { InlineEditableCell } from "@/shared/ui/InlineEditableCell.jsx"; import { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx"; import { AdminActionIconButton, AdminListBody, AdminListPage, AdminRowActions, AdminListToolbar, } from "@/shared/ui/AdminListLayout.jsx"; import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx"; import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js"; import { formatMillis } from "@/shared/utils/time.js"; import { defaultGiftTypeOptions, resourceStatusFilters } from "@/features/resources/constants.js"; import { GiftTypeConfigDialog } from "@/features/resources/components/GiftTypeConfigDialog.jsx"; import { ResourceSelectField } from "@/features/resources/components/ResourceSelectDrawer.jsx"; import { useGiftListPage } from "@/features/resources/hooks/useResourcePages.js"; import styles from "@/features/resources/resources.module.css"; const effectOptions = [ ["animation", "动画"], ["music", "音乐"], ["global_broadcast", "全局广播"], ]; const effectLabels = Object.fromEntries(effectOptions); const weekLabels = ["一", "二", "三", "四", "五", "六", "日"]; const baseColumns = (giftTypeOptions) => [ { key: "id", label: "ID", width: "minmax(88px, 0.4fr)", render: (gift) => gift.giftId || "-", }, { key: "gift", label: "礼物", width: "minmax(260px, 1.4fr)", render: (gift) => , }, { key: "type", label: "类型", width: "minmax(130px, 0.6fr)", render: (gift) => giftTypeLabel(gift.giftTypeCode, giftTypeOptions), }, { key: "price", label: "价格", width: "minmax(130px, 0.6fr)", render: (gift) => giftPriceLabel(gift), }, { key: "regions", label: "区域", width: "minmax(160px, 0.8fr)", }, { key: "sortOrder", label: "排序", width: "minmax(96px, 0.45fr)", }, { key: "effects", label: "有效期 / 特效", width: "minmax(210px, 1.1fr)", render: (gift) => (
{giftEffectDetails(gift).map((item, index) => ( 0 ? styles.meta : undefined} key={item}> {item} ))}
), }, { key: "status", label: "状态", width: "minmax(92px, 0.55fr)", }, { key: "time", label: "更新时间", width: "minmax(170px, 0.9fr)", render: (gift) => formatMillis(gift.updatedAtMs || gift.createdAtMs), }, { key: "actions", label: "操作", width: "minmax(76px, 0.4fr)", }, ]; export function GiftListPage() { const page = useGiftListPage(); const items = page.data.items || []; const total = page.data.total || 0; const isEditing = page.activeAction === "edit"; const giftRegionOptions = [{ label: "全局 · 0", value: "0" }, ...page.regionOptions]; const regionLabelById = buildRegionLabelMap(giftRegionOptions); const giftTypeOptions = page.giftTypeOptions?.length ? page.giftTypeOptions : defaultGiftTypeOptions; const tableColumns = baseColumns(giftTypeOptions).map((column) => { if (column.key === "regions") { return { ...column, filter: createRegionColumnFilter({ loading: page.loadingRegions, options: giftRegionOptions, value: page.regionId, onChange: page.changeRegionId, }), render: (gift) => , }; } if (column.key === "status") { return { ...column, filter: createOptionsColumnFilter({ options: resourceStatusFilters, placeholder: "搜索状态", value: page.status, onChange: page.changeStatus, }), render: (gift) => , }; } if (column.key === "type") { return { ...column, filter: createOptionsColumnFilter({ emptyLabel: "全部类型", loading: page.giftTypesLoading, options: giftTypeOptions.map((item) => ({ label: item.displayName || item.tabName || item.tabKey, value: item.tabKey, })), placeholder: "搜索类型", value: page.giftTypeCode, onChange: page.changeGiftTypeCode, }), }; } if (column.key === "sortOrder") { return { ...column, render: (gift) => , }; } if (column.key === "gift") { return { ...column, filter: createTextColumnFilter({ placeholder: "搜索礼物名称、礼物 ID", value: page.query, onChange: page.changeQuery, }), }; } if (column.key === "actions") { return { ...column, render: (gift) => , }; } return column; }); return ( {page.abilities.canUpdateGift ? ( ) : null} {page.abilities.canCreateGift ? ( ) : null} } /> 0 ? { page: page.page, pageSize: page.data.pageSize || 50, total, onPageChange: page.setPage, } : undefined } rowKey={(gift) => gift.giftId} /> ); } function GiftRowActions({ gift, page }) { return ( page.openEditGift(gift)} > ); } function GiftStatusSwitch({ gift, page }) { const checked = gift.status === "active"; return ( page.toggleGift(gift, event.target.checked)} /> ); } function GiftSortOrderCell({ gift, page }) { const originalValue = String(gift.sortOrder ?? 0); const saving = Boolean(page.giftSortSavingIds?.[gift.giftId]); return ( String(value || "0").trim() || "0"} saving={saving} type="number" value={originalValue} width="76px" onSave={(nextValue) => page.saveGiftSortOrder(gift, nextValue)} /> ); } function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open, page, regionOptions }) { const submitDisabled = disabled || loading || page.resourceOptionsLoading || page.loadingRegions || page.giftTypesLoading; const showGiftIdentityFields = mode === "edit" || Boolean(form.resourceId); return ( page.selectGiftResource(resourceId)} /> {showGiftIdentityFields ? ( <> page.setForm({ ...form, name: event.target.value })} /> page.setForm({ ...form, giftId: event.target.value })} /> ) : null} page.setForm({ ...form, giftTypeCode: event.target.value })} > {page.giftTypeOptions.map((item) => ( {item.displayName || item.tabKey} ))} page.setForm({ ...form, sortOrder: event.target.value })} /> page.setForm({ ...form, enabled: checked })} /> page.setForm({ ...form, regionIds })} /> page.setForm({ ...form, effectiveFrom })} /> page.setForm({ ...form, effectiveTo })} /> page.setForm({ ...form, effectTypes })} /> ); } function DateTimeField({ disabled, label, onChange, value }) { const parsedValue = parseLocalDateTime(value); const [anchorEl, setAnchorEl] = useState(null); const [draftDate, setDraftDate] = useState(parsedValue || new Date()); const [viewDate, setViewDate] = useState(parsedValue || new Date()); const days = useMemo(() => calendarDays(viewDate), [viewDate]); const open = Boolean(anchorEl); const openPicker = (event) => { if (disabled) { return; } const baseDate = parseLocalDateTime(value) || new Date(); setDraftDate(baseDate); setViewDate(new Date(baseDate.getFullYear(), baseDate.getMonth(), 1)); setAnchorEl(event.currentTarget); }; const closePicker = () => setAnchorEl(null); const changeMonth = (offset) => { setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1)); }; const changeDay = (day) => { setDraftDate( (current) => new Date(day.getFullYear(), day.getMonth(), day.getDate(), current.getHours(), current.getMinutes()), ); setViewDate(new Date(day.getFullYear(), day.getMonth(), 1)); }; const changeTime = (unit, nextValue) => { setDraftDate((current) => { const nextDate = new Date(current); if (unit === "hour") { nextDate.setHours(Number(nextValue)); } else { nextDate.setMinutes(Number(nextValue)); } return nextDate; }); }; return ( <> ), }} label={label} placeholder="长期有效" value={formatDateTimeInputLabel(value)} onClick={openPicker} />
changeMonth(-1)}> {monthTitle(viewDate)} changeMonth(1)}>
{weekLabels.map((weekday) => ( {weekday} ))} {days.map((day) => { const isOutside = day.getMonth() !== viewDate.getMonth(); const isSelected = sameDay(day, draftDate); return ( ); })}
changeTime("hour", event.target.value)} > {Array.from({ length: 24 }, (_, hour) => ( {twoDigits(hour)} ))} changeTime("minute", event.target.value)} > {Array.from({ length: 60 }, (_, minute) => ( {twoDigits(minute)} ))}
); } function GiftRegionSelect({ disabled, labels, onChange, options, value }) { const selected = value || []; const mergedOptions = [...new Set([...options.map((option) => option.value), ...selected])].sort( (left, right) => Number(left) - Number(right), ); return ( ); } function GiftEffectSelect({ disabled, onChange, value }) { const selected = value || []; return ( optionValue)} value={selected} onChange={onChange} /> ); } function RegionTags({ regionIds = [], regionLabelById }) { const items = [...new Set(regionIds)].sort((left, right) => Number(left) - Number(right)); if (!items.length) { return "-"; } return (
{items.map((regionId) => ( {regionLabelById[String(regionId)] || regionId} ))}
); } function GiftIdentity({ gift }) { const preview = imageURL(gift.resource?.previewUrl || gift.resource?.assetUrl); const resourceCode = String(gift.resource?.resourceCode || "").trim(); const title = `${gift.name || "-"}${resourceCode ? `(${resourceCode})` : ""}`; return (
{preview ? : }
{title}
); } 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"); } function giftTypeLabel(value, options = defaultGiftTypeOptions) { return options.find((option) => option.tabKey === value)?.displayName || value || "普通礼物"; } function giftPriceLabel(gift) { return `金币 · ${formatNumber(gift.coinPrice)}`; } function effectTypeLabel(value) { return effectOptions.find(([optionValue]) => optionValue === value)?.[1] || value; } function effectTypesLabel(values) { const list = values || []; return list.length ? list.map(effectTypeLabel).join("、") : "无特效"; } function giftEffectDetails(gift) { const details = []; const effective = effectiveRangeLabel(gift); if (effective) { details.push(effective); } const effects = effectTypesLabel(gift.effectTypes); if (effects !== "无特效") { details.push(effects); } return details.length ? details : ["-"]; } function parseLocalDateTime(value) { const normalizedValue = String(value || "").trim(); if (!normalizedValue) { return null; } const [datePart, timePart = "00:00"] = normalizedValue.split("T"); const [year, month, day] = datePart.split("-").map(Number); const [hour = 0, minute = 0] = timePart.split(":").map(Number); if (![year, month, day, hour, minute].every(Number.isFinite)) { return null; } const date = new Date(year, month - 1, day, hour, minute); return Number.isNaN(date.getTime()) ? null : date; } function toDatetimeLocalValue(date) { return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}T${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`; } function formatDateTimeInputLabel(value) { const date = parseLocalDateTime(value); if (!date) { return ""; } return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())} ${twoDigits(date.getHours())}:${twoDigits(date.getMinutes())}`; } function calendarDays(viewDate) { const firstDay = new Date(viewDate.getFullYear(), viewDate.getMonth(), 1); const offset = (firstDay.getDay() + 6) % 7; const startDate = new Date(firstDay); startDate.setDate(firstDay.getDate() - offset); return Array.from({ length: 42 }, (_, index) => { const day = new Date(startDate); day.setDate(startDate.getDate() + index); return day; }); } function sameDay(left, right) { return ( left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate() ); } function monthTitle(date) { return `${date.getFullYear()}年 ${date.getMonth() + 1}月`; } function twoDigits(value) { return String(value).padStart(2, "0"); } function effectiveRangeLabel(gift) { if (!gift.effectiveFromMs && !gift.effectiveToMs) { return ""; } return `${shortDateTime(gift.effectiveFromMs) || "不限"} - ${shortDateTime(gift.effectiveToMs) || "不限"}`; } function shortDateTime(value) { if (!value) { return ""; } return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }).format(new Date(Number(value))); } function buildRegionLabelMap(options = []) { return options.reduce((acc, option) => { acc[option.value] = shortRegionLabel(option.label); return acc; }, {}); } function shortRegionLabel(label) { return String(label || "").split(" · ")[0] || String(label || ""); }