448 lines
18 KiB
JavaScript
448 lines
18 KiB
JavaScript
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||
import ImageOutlined from "@mui/icons-material/ImageOutlined";
|
||
import MenuItem from "@mui/material/MenuItem";
|
||
import TextField from "@mui/material/TextField";
|
||
import { useMemo } from "react";
|
||
import { useSplashScreenConfigPage } from "@/features/app-config/hooks/useSplashScreenConfigPage.js";
|
||
import styles from "@/features/app-config/app-config.module.css";
|
||
import {
|
||
AdminFormDialog,
|
||
AdminFormFieldGrid,
|
||
AdminFormSection,
|
||
AdminFormSwitchField,
|
||
} from "@/shared/ui/AdminFormDialog.jsx";
|
||
import {
|
||
AdminActionIconButton,
|
||
AdminListBody,
|
||
AdminListPage,
|
||
AdminListToolbar,
|
||
AdminRowActions,
|
||
} from "@/shared/ui/AdminListLayout.jsx";
|
||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||
import { formatMillis } from "@/shared/utils/time.js";
|
||
|
||
const statusOptions = [
|
||
["", "全部状态"],
|
||
["active", "启用"],
|
||
["disabled", "关闭"],
|
||
["expired", "过期"],
|
||
];
|
||
|
||
const platformOptions = [
|
||
["", "全部平台"],
|
||
["android", "安卓"],
|
||
["ios", "iOS"],
|
||
];
|
||
|
||
export function SplashScreenConfigPage() {
|
||
const page = useSplashScreenConfigPage();
|
||
const items = page.data.items || [];
|
||
const columns = useMemo(() => splashScreenColumns(page), [page]);
|
||
|
||
return (
|
||
<AdminListPage>
|
||
<AdminListToolbar
|
||
actions={
|
||
page.abilities.canUpdate ? (
|
||
<AdminActionIconButton label="新增开屏" primary onClick={page.openCreate}>
|
||
<AddOutlined fontSize="small" />
|
||
</AdminActionIconButton>
|
||
) : null
|
||
}
|
||
/>
|
||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||
<AdminListBody>
|
||
<DataTable
|
||
columns={columns}
|
||
items={items}
|
||
minWidth="1320px"
|
||
pagination={{
|
||
itemCount: items.length,
|
||
page: 1,
|
||
pageSize: items.length || 1,
|
||
total: page.data.total ?? items.length,
|
||
}}
|
||
rowKey={(item) => item.id}
|
||
/>
|
||
</AdminListBody>
|
||
</DataState>
|
||
|
||
<AdminFormDialog
|
||
loading={page.loadingAction === "create" || page.loadingAction === "edit"}
|
||
open={Boolean(page.activeAction)}
|
||
size="large"
|
||
submitDisabled={
|
||
!page.abilities.canUpdate || page.loadingAction === "create" || page.loadingAction === "edit"
|
||
}
|
||
title={page.editingItem ? "编辑开屏配置" : "新增开屏配置"}
|
||
onClose={page.closeDialog}
|
||
onSubmit={page.submitSplashScreen}
|
||
>
|
||
<div className={styles.splashFormLayout}>
|
||
<div className={styles.splashAssetPane}>
|
||
<AdminFormSection title="素材">
|
||
<UploadField
|
||
className={styles.splashUploadField}
|
||
disabled={!page.abilities.canUpdate || !page.abilities.canUpload}
|
||
label="封面图"
|
||
previewClassName={styles.splashUploadPreview}
|
||
value={page.form.coverUrl}
|
||
onChange={(coverUrl) => page.setForm({ coverUrl })}
|
||
/>
|
||
</AdminFormSection>
|
||
</div>
|
||
<div className={styles.splashFieldPane}>
|
||
<AdminFormSection title="跳转信息">
|
||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||
<TextField
|
||
disabled={!page.abilities.canUpdate}
|
||
label="类型"
|
||
required
|
||
select
|
||
value={page.form.splashType}
|
||
onChange={(event) => page.setForm({ splashType: event.target.value })}
|
||
>
|
||
<MenuItem value="h5">H5</MenuItem>
|
||
<MenuItem value="app">APP</MenuItem>
|
||
</TextField>
|
||
<TextField
|
||
disabled={!page.abilities.canUpdate}
|
||
label="平台"
|
||
required
|
||
select
|
||
value={page.form.platform}
|
||
onChange={(event) => page.setForm({ platform: event.target.value })}
|
||
>
|
||
<MenuItem value="android">安卓</MenuItem>
|
||
<MenuItem value="ios">iOS</MenuItem>
|
||
</TextField>
|
||
<TextField
|
||
className={styles.formWideField}
|
||
disabled={!page.abilities.canUpdate}
|
||
label={page.form.splashType === "h5" ? "参数(H5链接)" : "参数"}
|
||
required={page.form.splashType === "h5"}
|
||
value={page.form.param}
|
||
onChange={(event) => page.setForm({ param: event.target.value })}
|
||
/>
|
||
</AdminFormFieldGrid>
|
||
</AdminFormSection>
|
||
<AdminFormSection title="投放设置">
|
||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||
<RegionSelect
|
||
disabled={!page.abilities.canUpdate}
|
||
emptyLabel="全部区域"
|
||
loading={page.loadingRegions}
|
||
options={page.regionOptions}
|
||
value={page.form.regionId}
|
||
onChange={(regionId) => page.setForm({ regionId })}
|
||
/>
|
||
<CountrySelect
|
||
disabled={!page.abilities.canUpdate || page.loadingCountries}
|
||
emptyLabel="全部国家"
|
||
label="国家"
|
||
options={page.countryOptions}
|
||
value={page.form.countryCode}
|
||
onChange={(countryCode) => page.setForm({ countryCode })}
|
||
/>
|
||
<TextField
|
||
disabled={!page.abilities.canUpdate}
|
||
label="排序"
|
||
slotProps={{ htmlInput: { step: 1 } }}
|
||
type="number"
|
||
value={page.form.sortOrder}
|
||
onChange={(event) => page.setForm({ sortOrder: event.target.value })}
|
||
/>
|
||
<TextField
|
||
disabled={!page.abilities.canUpdate}
|
||
label="展示时长(ms)"
|
||
slotProps={{ htmlInput: { min: 0, step: 100 } }}
|
||
type="number"
|
||
value={page.form.displayDurationMs}
|
||
onChange={(event) => page.setForm({ displayDurationMs: event.target.value })}
|
||
/>
|
||
<AdminFormSwitchField
|
||
checked={page.form.status === "active"}
|
||
checkedLabel="启用"
|
||
className={styles.splashStatusField}
|
||
disabled={!page.abilities.canUpdate}
|
||
label="开屏状态"
|
||
switchProps={{ inputProps: { "aria-label": "开屏启用状态" } }}
|
||
uncheckedLabel={page.form.status === "expired" ? "过期" : "关闭"}
|
||
onChange={(checked) => page.setForm({ status: checked ? "active" : "disabled" })}
|
||
/>
|
||
<TimeRangeFilter
|
||
className={styles.formWideField}
|
||
disabled={!page.abilities.canUpdate}
|
||
label="投放时间区间"
|
||
value={{ endMs: page.form.endsAtMs, startMs: page.form.startsAtMs }}
|
||
onChange={(range) =>
|
||
page.setForm({ endsAtMs: range.endMs, startsAtMs: range.startMs })
|
||
}
|
||
/>
|
||
</AdminFormFieldGrid>
|
||
</AdminFormSection>
|
||
<AdminFormSection title="描述">
|
||
<TextField
|
||
disabled={!page.abilities.canUpdate}
|
||
label="描述"
|
||
multiline
|
||
minRows={2}
|
||
value={page.form.description}
|
||
onChange={(event) => page.setForm({ description: event.target.value })}
|
||
/>
|
||
</AdminFormSection>
|
||
</div>
|
||
</div>
|
||
</AdminFormDialog>
|
||
</AdminListPage>
|
||
);
|
||
}
|
||
|
||
function splashScreenColumns(page) {
|
||
const columns = [
|
||
{
|
||
key: "cover",
|
||
label: "封面图",
|
||
render: (item) => <SplashCover src={item.coverUrl} />,
|
||
width: "minmax(96px, 0.45fr)",
|
||
},
|
||
{
|
||
key: "target",
|
||
label: "类型 / 参数",
|
||
render: (item) => <Stack primary={typeLabel(item.splashType)} secondary={item.param || "-"} />,
|
||
filter: createTextColumnFilter({
|
||
placeholder: "搜索参数、国家、描述",
|
||
value: page.keyword,
|
||
onChange: page.setKeyword,
|
||
}),
|
||
width: "minmax(260px, 1.3fr)",
|
||
},
|
||
{
|
||
key: "platform",
|
||
label: "平台",
|
||
render: (item) => platformLabel(item.platform),
|
||
filter: createOptionsColumnFilter({
|
||
options: platformOptions,
|
||
placeholder: "搜索平台",
|
||
value: page.platform,
|
||
onChange: page.setPlatform,
|
||
}),
|
||
width: "minmax(110px, 0.6fr)",
|
||
},
|
||
{
|
||
key: "status",
|
||
label: "状态",
|
||
render: (item) => <StatusBadge status={item.status} />,
|
||
filter: createOptionsColumnFilter({
|
||
options: statusOptions,
|
||
placeholder: "搜索状态",
|
||
value: page.status,
|
||
onChange: page.setStatus,
|
||
}),
|
||
width: "minmax(100px, 0.55fr)",
|
||
},
|
||
{
|
||
key: "region",
|
||
label: "区域",
|
||
render: (item) => regionLabel(page.regionOptions, item.regionId),
|
||
filter: createRegionColumnFilter({
|
||
loading: page.loadingRegions,
|
||
options: page.regionOptions,
|
||
value: page.regionId,
|
||
onChange: page.setRegionId,
|
||
}),
|
||
width: "minmax(150px, 0.8fr)",
|
||
},
|
||
{
|
||
key: "country",
|
||
label: "国家",
|
||
render: (item) => countryLabel(page.countryOptions, item.countryCode),
|
||
filter: createOptionsColumnFilter({
|
||
emptyLabel: "全部国家",
|
||
loading: page.loadingCountries,
|
||
options: page.countryOptions,
|
||
placeholder: "搜索国家",
|
||
value: page.countryCode,
|
||
onChange: page.setCountryCode,
|
||
}),
|
||
width: "minmax(150px, 0.8fr)",
|
||
},
|
||
{
|
||
key: "sortOrder",
|
||
label: "排序",
|
||
width: "minmax(80px, 0.4fr)",
|
||
},
|
||
{
|
||
key: "displayDurationMs",
|
||
label: "展示时长",
|
||
render: (item) => durationLabel(item.displayDurationMs),
|
||
width: "minmax(110px, 0.55fr)",
|
||
},
|
||
{
|
||
key: "deliveryTime",
|
||
label: "投放时间",
|
||
render: (item) => deliveryTimeLabel(item.startsAtMs, item.endsAtMs),
|
||
width: "minmax(220px, 1fr)",
|
||
},
|
||
{
|
||
key: "description",
|
||
label: "描述",
|
||
render: (item) => item.description || "-",
|
||
width: "minmax(180px, 0.9fr)",
|
||
},
|
||
{
|
||
key: "updatedAtMs",
|
||
label: "更新时间",
|
||
render: (item) => formatMillis(item.updatedAtMs),
|
||
width: "minmax(160px, 0.8fr)",
|
||
},
|
||
];
|
||
|
||
if (!page.abilities.canUpdate) {
|
||
return columns;
|
||
}
|
||
|
||
return [
|
||
...columns,
|
||
{
|
||
key: "actions",
|
||
label: "操作",
|
||
render: (item) => <SplashActions item={item} page={page} />,
|
||
width: "minmax(96px, 0.45fr)",
|
||
},
|
||
];
|
||
}
|
||
|
||
function SplashCover({ src }) {
|
||
if (!src) {
|
||
return (
|
||
<span className={styles.splashCoverEmpty}>
|
||
<ImageOutlined fontSize="small" />
|
||
</span>
|
||
);
|
||
}
|
||
return <img alt="" className={styles.splashCover} src={src} />;
|
||
}
|
||
|
||
function SplashActions({ item, page }) {
|
||
const deleting = page.loadingAction === `delete:${item.id}`;
|
||
return (
|
||
<AdminRowActions>
|
||
<AdminActionIconButton
|
||
disabled={Boolean(page.loadingAction)}
|
||
label="编辑"
|
||
onClick={() => page.openEdit(item)}
|
||
>
|
||
<EditOutlined fontSize="small" />
|
||
</AdminActionIconButton>
|
||
<AdminActionIconButton
|
||
disabled={deleting || Boolean(page.loadingAction)}
|
||
label="删除"
|
||
onClick={() => page.removeSplashScreen(item)}
|
||
>
|
||
<DeleteOutlineOutlined fontSize="small" />
|
||
</AdminActionIconButton>
|
||
</AdminRowActions>
|
||
);
|
||
}
|
||
|
||
function CountrySelect({ className, disabled, emptyLabel, label = "国家", onChange, options, value }) {
|
||
return (
|
||
<TextField
|
||
className={className}
|
||
disabled={disabled}
|
||
label={label}
|
||
select
|
||
size="small"
|
||
value={value || ""}
|
||
onChange={(event) => onChange(event.target.value)}
|
||
>
|
||
<MenuItem value="">{emptyLabel}</MenuItem>
|
||
{options.map((option) => (
|
||
<MenuItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</MenuItem>
|
||
))}
|
||
</TextField>
|
||
);
|
||
}
|
||
|
||
function Stack({ primary, secondary }) {
|
||
return (
|
||
<div className="cell-stack">
|
||
<span>{primary || "-"}</span>
|
||
<span className="muted">{secondary || "-"}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function StatusBadge({ status }) {
|
||
const tone = status === "active" ? "succeeded" : status === "expired" ? "warning" : "stopped";
|
||
return (
|
||
<span className={`status-badge status-badge--${tone}`}>
|
||
<span className="status-point" />
|
||
{statusLabel(status)}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function typeLabel(value) {
|
||
return value === "app" ? "APP" : "H5";
|
||
}
|
||
|
||
function platformLabel(value) {
|
||
if (value === "android") {
|
||
return "安卓";
|
||
}
|
||
if (value === "ios") {
|
||
return "iOS";
|
||
}
|
||
return value || "-";
|
||
}
|
||
|
||
function deliveryTimeLabel(startsAtMs, endsAtMs) {
|
||
const start = startsAtMs ? formatMillis(startsAtMs) : "不限";
|
||
const end = endsAtMs ? formatMillis(endsAtMs) : "不限";
|
||
return `${start} - ${end}`;
|
||
}
|
||
|
||
function statusLabel(status) {
|
||
if (status === "active") {
|
||
return "启用";
|
||
}
|
||
if (status === "expired") {
|
||
return "过期";
|
||
}
|
||
return "关闭";
|
||
}
|
||
|
||
function durationLabel(value) {
|
||
const duration = Number(value || 3000);
|
||
if (!Number.isFinite(duration) || duration < 0) {
|
||
return "3000 ms";
|
||
}
|
||
return `${duration} ms`;
|
||
}
|
||
|
||
function regionLabel(options, value) {
|
||
if (!value) {
|
||
return "全部区域";
|
||
}
|
||
return options.find((option) => option.value === String(value))?.label || `区域 ${value}`;
|
||
}
|
||
|
||
function countryLabel(options, value) {
|
||
if (!value) {
|
||
return "全部国家";
|
||
}
|
||
return options.find((option) => option.value === value)?.label || value;
|
||
}
|