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) => (
))}
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) => (
))}
changeTime("minute", event.target.value)}
>
{Array.from({ length: 60 }, (_, 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 || "");
}