758 lines
29 KiB
JavaScript
758 lines
29 KiB
JavaScript
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 { 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 { useGiftListPage } from "@/features/resources/hooks/useResourcePages.js";
|
|
import styles from "@/features/resources/resources.module.css";
|
|
|
|
const chargeAssetOptions = [
|
|
["COIN", "金币"],
|
|
["DIAMOND", "钻石"],
|
|
];
|
|
const effectOptions = [
|
|
["animation", "动画"],
|
|
["music", "音乐"],
|
|
["global_broadcast", "全局广播"],
|
|
];
|
|
const effectLabels = Object.fromEntries(effectOptions);
|
|
const weekLabels = ["一", "二", "三", "四", "五", "六", "日"];
|
|
|
|
const baseColumns = (giftTypeOptions) => [
|
|
{
|
|
key: "gift",
|
|
label: "礼物",
|
|
width: "minmax(260px, 1.4fr)",
|
|
render: (gift) => <GiftIdentity gift={gift} />,
|
|
},
|
|
{
|
|
key: "resource",
|
|
label: "资源",
|
|
width: "minmax(220px, 1.1fr)",
|
|
render: (gift) => <ResourceRef gift={gift} />,
|
|
},
|
|
{
|
|
key: "price",
|
|
label: "类型 / 价格",
|
|
width: "minmax(150px, 0.75fr)",
|
|
render: (gift) => (
|
|
<div className={styles.stack}>
|
|
<span>{giftTypeLabel(gift.giftTypeCode, giftTypeOptions)}</span>
|
|
<span className={styles.meta}>
|
|
{chargeAssetLabel(gift.chargeAssetType)} · {formatNumber(gift.coinPrice)}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "income",
|
|
label: "积分 / 热度",
|
|
width: "minmax(140px, 0.8fr)",
|
|
render: (gift) => (
|
|
<div className={styles.stack}>
|
|
<span>{formatNumber(gift.giftPointAmount)}</span>
|
|
<span className={styles.meta}>{formatNumber(gift.heatValue)}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "regions",
|
|
label: "区域",
|
|
width: "minmax(160px, 0.8fr)",
|
|
},
|
|
{
|
|
key: "effects",
|
|
label: "有效期 / 特效",
|
|
width: "minmax(210px, 1.1fr)",
|
|
render: (gift) => (
|
|
<div className={styles.stack}>
|
|
<span>{effectiveRangeLabel(gift)}</span>
|
|
<span className={styles.meta}>{effectTypesLabel(gift.effectTypes)}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
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) => <RegionTags regionIds={gift.regionIds} regionLabelById={regionLabelById} />,
|
|
};
|
|
}
|
|
if (column.key === "status") {
|
|
return {
|
|
...column,
|
|
filter: createOptionsColumnFilter({
|
|
options: resourceStatusFilters,
|
|
placeholder: "搜索状态",
|
|
value: page.status,
|
|
onChange: page.changeStatus,
|
|
}),
|
|
render: (gift) => <GiftStatusSwitch gift={gift} page={page} />,
|
|
};
|
|
}
|
|
if (column.key === "gift") {
|
|
return {
|
|
...column,
|
|
filter: createTextColumnFilter({
|
|
placeholder: "搜索礼物名称、礼物 ID",
|
|
value: page.query,
|
|
onChange: page.changeQuery,
|
|
}),
|
|
};
|
|
}
|
|
if (column.key === "actions") {
|
|
return {
|
|
...column,
|
|
render: (gift) => <GiftRowActions gift={gift} page={page} />,
|
|
};
|
|
}
|
|
return column;
|
|
});
|
|
|
|
return (
|
|
<AdminListPage>
|
|
<AdminListToolbar
|
|
actions={
|
|
<>
|
|
{page.abilities.canUpdateGift ? (
|
|
<AdminActionIconButton label="礼物类型" onClick={page.openGiftTypeDialog}>
|
|
<SettingsOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
) : null}
|
|
{page.abilities.canCreateGift ? (
|
|
<AdminActionIconButton label="添加礼物" primary onClick={page.openCreateGift}>
|
|
<Add fontSize="small" />
|
|
</AdminActionIconButton>
|
|
) : null}
|
|
</>
|
|
}
|
|
/>
|
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
|
<AdminListBody>
|
|
<DataTable
|
|
columns={tableColumns}
|
|
items={items}
|
|
minWidth="1480px"
|
|
pagination={
|
|
total > 0
|
|
? {
|
|
page: page.page,
|
|
pageSize: page.data.pageSize || 10,
|
|
total,
|
|
onPageChange: page.setPage,
|
|
}
|
|
: undefined
|
|
}
|
|
rowKey={(gift) => gift.giftId}
|
|
/>
|
|
</AdminListBody>
|
|
</DataState>
|
|
<GiftFormDialog
|
|
disabled={isEditing ? !page.abilities.canUpdateGift : !page.abilities.canCreateGift}
|
|
form={page.form}
|
|
loading={isEditing ? page.loadingAction === "gift-edit" : page.loadingAction === "gift-create"}
|
|
mode={isEditing ? "edit" : "create"}
|
|
open={page.activeAction === "create" || page.activeAction === "edit"}
|
|
page={page}
|
|
regionOptions={giftRegionOptions}
|
|
onClose={page.closeAction}
|
|
onSubmit={page.submitGift}
|
|
/>
|
|
<GiftTypeConfigDialog
|
|
disabled={!page.abilities.canUpdateGift}
|
|
form={page.giftTypeForm}
|
|
loading={page.loadingAction === "gift-types"}
|
|
open={page.giftTypeDialogOpen}
|
|
optionsLoading={page.giftTypesLoading}
|
|
onChangeItem={page.updateGiftTypeFormItem}
|
|
onClose={page.closeGiftTypeDialog}
|
|
onSubmit={page.submitGiftTypes}
|
|
/>
|
|
</AdminListPage>
|
|
);
|
|
}
|
|
|
|
function GiftRowActions({ gift, page }) {
|
|
return (
|
|
<AdminRowActions>
|
|
<AdminActionIconButton
|
|
disabled={!page.abilities.canUpdateGift}
|
|
label="编辑礼物"
|
|
onClick={() => page.openEditGift(gift)}
|
|
>
|
|
<EditOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
</AdminRowActions>
|
|
);
|
|
}
|
|
|
|
function GiftStatusSwitch({ gift, page }) {
|
|
const checked = gift.status === "active";
|
|
return (
|
|
<AdminSwitch
|
|
checked={checked}
|
|
disabled={!page.abilities.canStatusGift || page.loadingAction === `gift-status-${gift.giftId}`}
|
|
inputProps={{ "aria-label": checked ? "禁用礼物" : "启用礼物" }}
|
|
onChange={(event) => page.toggleGift(gift, event.target.checked)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<AdminFormDialog
|
|
loading={loading}
|
|
open={open}
|
|
size="large"
|
|
submitDisabled={submitDisabled}
|
|
title={mode === "edit" ? "编辑礼物" : "添加礼物"}
|
|
onClose={onClose}
|
|
onSubmit={onSubmit}
|
|
>
|
|
<AdminFormSection title="资源信息">
|
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
|
<TextField
|
|
disabled={disabled || page.resourceOptionsLoading}
|
|
label="礼物资源"
|
|
required
|
|
select
|
|
value={form.resourceId}
|
|
onChange={(event) => page.selectGiftResource(event.target.value)}
|
|
>
|
|
{page.resourceOptions.map((resource) => (
|
|
<MenuItem key={resource.resourceId} value={String(resource.resourceId)}>
|
|
{resource.name || resource.resourceCode || resource.resourceId}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
{showGiftIdentityFields ? (
|
|
<>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="礼物名称"
|
|
required
|
|
value={form.name}
|
|
onChange={(event) => page.setForm({ ...form, name: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled || mode === "edit"}
|
|
label="礼物 ID"
|
|
required
|
|
value={form.giftId}
|
|
onChange={(event) => page.setForm({ ...form, giftId: event.target.value })}
|
|
/>
|
|
</>
|
|
) : null}
|
|
<TextField
|
|
disabled={disabled || page.giftTypesLoading}
|
|
label="礼物类型"
|
|
required
|
|
select
|
|
value={form.giftTypeCode}
|
|
onChange={(event) => page.setForm({ ...form, giftTypeCode: event.target.value })}
|
|
>
|
|
{page.giftTypeOptions.map((item) => (
|
|
<MenuItem key={item.tabKey} value={item.tabKey}>
|
|
{item.displayName || item.tabKey}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
<AdminFormSection title="价格与排序">
|
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
|
<TextField
|
|
disabled={disabled}
|
|
label="收费类型"
|
|
required
|
|
select
|
|
value={form.chargeAssetType}
|
|
onChange={(event) => page.setForm({ ...form, chargeAssetType: event.target.value })}
|
|
>
|
|
{chargeAssetOptions.map(([value, label]) => (
|
|
<MenuItem key={value} value={value}>
|
|
{label}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="价格"
|
|
required
|
|
type="number"
|
|
value={form.coinPrice}
|
|
onChange={(event) => page.changeGiftPrice(event.target.value)}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="积分"
|
|
type="number"
|
|
value={form.giftPointAmount}
|
|
onChange={(event) => page.setForm({ ...form, giftPointAmount: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="热度"
|
|
type="number"
|
|
value={form.heatValue}
|
|
onChange={(event) => page.setForm({ ...form, heatValue: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={disabled}
|
|
label="排序"
|
|
type="number"
|
|
value={form.sortOrder}
|
|
onChange={(event) => page.setForm({ ...form, sortOrder: event.target.value })}
|
|
/>
|
|
<AdminFormSwitchField
|
|
checked={form.enabled}
|
|
checkedLabel="启用"
|
|
className={styles.giftSwitch}
|
|
disabled={disabled}
|
|
label="礼物状态"
|
|
uncheckedLabel="禁用"
|
|
onChange={(checked) => page.setForm({ ...form, enabled: checked })}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
<AdminFormSection title="投放与时间">
|
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
|
<GiftRegionSelect
|
|
disabled={disabled || page.loadingRegions}
|
|
labels={buildRegionLabelMap(regionOptions)}
|
|
options={regionOptions}
|
|
value={form.regionIds}
|
|
onChange={(regionIds) => page.setForm({ ...form, regionIds })}
|
|
/>
|
|
<DateTimeField
|
|
disabled={disabled}
|
|
label="有效开始时间"
|
|
value={form.effectiveFrom}
|
|
onChange={(effectiveFrom) => page.setForm({ ...form, effectiveFrom })}
|
|
/>
|
|
<DateTimeField
|
|
disabled={disabled}
|
|
label="有效结束时间"
|
|
value={form.effectiveTo}
|
|
onChange={(effectiveTo) => page.setForm({ ...form, effectiveTo })}
|
|
/>
|
|
<GiftEffectSelect
|
|
disabled={disabled}
|
|
value={form.effectTypes}
|
|
onChange={(effectTypes) => page.setForm({ ...form, effectTypes })}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
</AdminFormDialog>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<>
|
|
<TextField
|
|
className={styles.dateTimeField}
|
|
disabled={disabled}
|
|
inputProps={{ readOnly: true }}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<CalendarMonthOutlined fontSize="small" />
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
label={label}
|
|
placeholder="长期有效"
|
|
value={formatDateTimeInputLabel(value)}
|
|
onClick={openPicker}
|
|
/>
|
|
<Popover
|
|
anchorEl={anchorEl}
|
|
anchorOrigin={{ horizontal: "left", vertical: "bottom" }}
|
|
open={open}
|
|
transformOrigin={{ horizontal: "left", vertical: "top" }}
|
|
onClose={closePicker}
|
|
>
|
|
<div className={styles.datePopover}>
|
|
<div className={styles.calendarHeader}>
|
|
<IconButton aria-label="上个月" size="small" onClick={() => changeMonth(-1)}>
|
|
<ChevronLeftOutlined fontSize="small" />
|
|
</IconButton>
|
|
<span className={styles.calendarTitle}>{monthTitle(viewDate)}</span>
|
|
<IconButton aria-label="下个月" size="small" onClick={() => changeMonth(1)}>
|
|
<ChevronRightOutlined fontSize="small" />
|
|
</IconButton>
|
|
</div>
|
|
<div className={styles.calendarGrid}>
|
|
{weekLabels.map((weekday) => (
|
|
<span className={styles.weekday} key={weekday}>
|
|
{weekday}
|
|
</span>
|
|
))}
|
|
{days.map((day) => {
|
|
const isOutside = day.getMonth() !== viewDate.getMonth();
|
|
const isSelected = sameDay(day, draftDate);
|
|
return (
|
|
<button
|
|
className={[
|
|
styles.dayButton,
|
|
isOutside ? styles.dayOutside : "",
|
|
isSelected ? styles.daySelected : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
key={day.toISOString()}
|
|
type="button"
|
|
onClick={() => changeDay(day)}
|
|
>
|
|
{day.getDate()}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className={styles.timeGrid}>
|
|
<TextField
|
|
label="时"
|
|
select
|
|
size="small"
|
|
value={String(draftDate.getHours())}
|
|
onChange={(event) => changeTime("hour", event.target.value)}
|
|
>
|
|
{Array.from({ length: 24 }, (_, hour) => (
|
|
<MenuItem key={hour} value={String(hour)}>
|
|
{twoDigits(hour)}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<TextField
|
|
label="分"
|
|
select
|
|
size="small"
|
|
value={String(draftDate.getMinutes())}
|
|
onChange={(event) => changeTime("minute", event.target.value)}
|
|
>
|
|
{Array.from({ length: 60 }, (_, minute) => (
|
|
<MenuItem key={minute} value={String(minute)}>
|
|
{twoDigits(minute)}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
</div>
|
|
<div className={styles.popoverActions}>
|
|
<Button
|
|
onClick={() => {
|
|
onChange("");
|
|
closePicker();
|
|
}}
|
|
>
|
|
清空
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
onChange(toDatetimeLocalValue(draftDate));
|
|
closePicker();
|
|
}}
|
|
>
|
|
确定
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Popover>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<MultiValueAutocomplete
|
|
disabled={disabled}
|
|
label="区域"
|
|
labels={labels}
|
|
options={mergedOptions}
|
|
required
|
|
value={selected}
|
|
onChange={onChange}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function GiftEffectSelect({ disabled, onChange, value }) {
|
|
const selected = value || [];
|
|
return (
|
|
<MultiValueAutocomplete
|
|
disabled={disabled}
|
|
label="特效"
|
|
labels={effectLabels}
|
|
options={effectOptions.map(([optionValue]) => 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 (
|
|
<div className="admin-tag-list">
|
|
{items.map((regionId) => (
|
|
<span className="admin-tag" key={regionId}>
|
|
{regionLabelById[String(regionId)] || regionId}
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GiftIdentity({ gift }) {
|
|
const preview = imageURL(gift.resource?.previewUrl || gift.resource?.assetUrl);
|
|
return (
|
|
<div className={styles.identity}>
|
|
<span className={styles.thumb}>
|
|
{preview ? <img src={preview} alt="" /> : <CardGiftcardOutlined fontSize="small" />}
|
|
</span>
|
|
<div className={styles.identityText}>
|
|
<span className={styles.name}>{gift.name || "-"}</span>
|
|
<span className={styles.meta}>{gift.giftId}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ResourceRef({ gift }) {
|
|
const resource = gift.resource || {};
|
|
return (
|
|
<div className={styles.stack}>
|
|
<span>{resource.name || "-"}</span>
|
|
<span className={styles.meta}>{resource.resourceCode || gift.resourceId || "-"}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 chargeAssetLabel(value) {
|
|
return chargeAssetOptions.find(([optionValue]) => optionValue === value)?.[1] || value || "金币";
|
|
}
|
|
|
|
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 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 || "");
|
|
}
|