hyapp-admin-platform/src/features/app-config/pages/SplashScreenConfigPage.jsx
2026-06-11 01:01:53 +08:00

448 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}