391 lines
14 KiB
JavaScript
391 lines
14 KiB
JavaScript
import AddOutlined from "@mui/icons-material/AddOutlined";
|
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
import TextField from "@mui/material/TextField";
|
|
import { useMemo } from "react";
|
|
import { useRechargeProductsPage } from "@/features/payment/hooks/useRechargeProductsPage.js";
|
|
import { Button } from "@/shared/ui/Button.jsx";
|
|
import {
|
|
AdminFormAmountField,
|
|
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 { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx";
|
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
|
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
|
import { formatMillis } from "@/shared/utils/time.js";
|
|
|
|
const statusOptions = [
|
|
["", "全部状态"],
|
|
["active", "上架"],
|
|
["disabled", "下架"],
|
|
];
|
|
|
|
const platformOptions = [
|
|
["", "全部平台"],
|
|
["web", "Web"],
|
|
["android", "Android"],
|
|
["ios", "iOS"],
|
|
];
|
|
|
|
const audienceOptions = [
|
|
["", "全部用户类型"],
|
|
["normal", "普通用户"],
|
|
["coin_seller", "币商"],
|
|
];
|
|
|
|
export function RechargeProductConfigPage() {
|
|
const page = useRechargeProductsPage();
|
|
const items = page.data?.items || [];
|
|
const regionLabels = useMemo(() => regionLabelMap(page.regionOptions), [page.regionOptions]);
|
|
const regionValues = useMemo(() => page.regionOptions.map((option) => option.value), [page.regionOptions]);
|
|
const columns = useMemo(() => productColumns(page, regionLabels), [page, regionLabels]);
|
|
const saving = page.loadingAction === "create" || page.loadingAction === "edit";
|
|
const canSubmit = page.editingItem ? page.abilities.canUpdateProduct : page.abilities.canCreateProduct;
|
|
|
|
return (
|
|
<AdminListPage>
|
|
<AdminListToolbar
|
|
actions={
|
|
page.abilities.canCreateProduct ? (
|
|
<Button
|
|
startIcon={<AddOutlined fontSize="small" />}
|
|
variant="primary"
|
|
onClick={page.openCreate}
|
|
>
|
|
新增内购
|
|
</Button>
|
|
) : null
|
|
}
|
|
/>
|
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
|
<AdminListBody>
|
|
<DataTable
|
|
columns={columns}
|
|
items={items}
|
|
minWidth="1180px"
|
|
pagination={{
|
|
page: page.page,
|
|
pageSize: page.pageSize,
|
|
total: page.data?.total || 0,
|
|
onPageChange: page.setPage,
|
|
}}
|
|
rowKey={(item) => item.productId}
|
|
/>
|
|
</AdminListBody>
|
|
</DataState>
|
|
|
|
<AdminFormDialog
|
|
loading={saving}
|
|
open={Boolean(page.activeAction)}
|
|
size="compact"
|
|
submitDisabled={!canSubmit || saving}
|
|
title={page.editingItem ? "编辑内购配置" : "新增内购配置"}
|
|
onClose={page.closeDialog}
|
|
onSubmit={page.submitProduct}
|
|
>
|
|
<AdminFormSection title="商品信息">
|
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
|
<TextField
|
|
disabled={!canSubmit}
|
|
label="产品名称"
|
|
required
|
|
value={page.form.productName}
|
|
onChange={(event) => page.setForm({ productName: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={!canSubmit}
|
|
label="平台"
|
|
required
|
|
select
|
|
value={page.form.platform}
|
|
onChange={(event) => page.setForm({ platform: event.target.value })}
|
|
>
|
|
<MenuItem value="web">Web</MenuItem>
|
|
<MenuItem value="android">Android</MenuItem>
|
|
<MenuItem value="ios">iOS</MenuItem>
|
|
</TextField>
|
|
<TextField
|
|
disabled={!canSubmit}
|
|
label="用户类型"
|
|
required
|
|
select
|
|
value={page.form.audienceType}
|
|
onChange={(event) => page.setForm({ audienceType: event.target.value })}
|
|
>
|
|
<MenuItem value="normal">普通用户</MenuItem>
|
|
<MenuItem value="coin_seller">币商</MenuItem>
|
|
</TextField>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
<AdminFormSection title="价格配置">
|
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
|
<AdminFormAmountField
|
|
disabled={!canSubmit}
|
|
inputProps={{ inputMode: "decimal" }}
|
|
label="金额USDT"
|
|
required
|
|
unit="USDT"
|
|
value={page.form.amountUsdt}
|
|
onChange={(event) => page.setForm({ amountUsdt: event.target.value })}
|
|
/>
|
|
<TextField
|
|
disabled={!canSubmit}
|
|
inputProps={{ min: 1, step: 1 }}
|
|
label="获得金币数"
|
|
required
|
|
type="number"
|
|
value={page.form.coinAmount}
|
|
onChange={(event) => page.setForm({ coinAmount: event.target.value })}
|
|
/>
|
|
</AdminFormFieldGrid>
|
|
</AdminFormSection>
|
|
<AdminFormSection title="投放设置">
|
|
<MultiValueAutocomplete
|
|
disabled={!canSubmit}
|
|
label="支持区域"
|
|
labels={regionLabels}
|
|
loading={page.loadingRegions}
|
|
options={regionValues}
|
|
required
|
|
value={page.form.regionIds}
|
|
onChange={(regionIds) => page.setForm({ regionIds })}
|
|
/>
|
|
<AdminFormSwitchField
|
|
checked={Boolean(page.form.enabled)}
|
|
checkedLabel="上架"
|
|
disabled={!canSubmit}
|
|
label="是否上架"
|
|
uncheckedLabel="下架"
|
|
onChange={(checked) => page.setForm({ enabled: checked })}
|
|
/>
|
|
</AdminFormSection>
|
|
<AdminFormSection title="描述">
|
|
<TextField
|
|
disabled={!canSubmit}
|
|
label="描述"
|
|
multiline
|
|
minRows={2}
|
|
value={page.form.description}
|
|
onChange={(event) => page.setForm({ description: event.target.value })}
|
|
/>
|
|
</AdminFormSection>
|
|
</AdminFormDialog>
|
|
</AdminListPage>
|
|
);
|
|
}
|
|
|
|
function productColumns(page, regionLabels) {
|
|
const columns = [
|
|
{
|
|
key: "product",
|
|
label: "产品",
|
|
render: (item) => <Stack primary={item.productName} secondary={item.productCode || "-"} />,
|
|
filter: createTextColumnFilter({
|
|
placeholder: "搜索产品、描述",
|
|
value: page.keyword,
|
|
onChange: page.setKeyword,
|
|
}),
|
|
width: "minmax(240px, 1.2fr)",
|
|
},
|
|
{
|
|
key: "amount",
|
|
label: "金额 / 金币",
|
|
render: (item) => (
|
|
<Stack
|
|
primary={`${item.amountUsdt || formatMicroAmount(item.amountUsdtMicro)} USDT`}
|
|
secondary={`${formatNumber(item.coinAmount)} 金币`}
|
|
/>
|
|
),
|
|
width: "minmax(160px, 0.75fr)",
|
|
},
|
|
{
|
|
key: "platform",
|
|
label: "平台",
|
|
render: (item) => platformLabel(item.platform),
|
|
filter: createOptionsColumnFilter({
|
|
options: platformOptions,
|
|
placeholder: "搜索平台",
|
|
value: page.platform,
|
|
onChange: page.setPlatform,
|
|
}),
|
|
width: "minmax(120px, 0.55fr)",
|
|
},
|
|
{
|
|
key: "audienceType",
|
|
label: "用户类型",
|
|
render: (item) => audienceLabel(item.audienceType),
|
|
filter: createOptionsColumnFilter({
|
|
options: audienceOptions,
|
|
placeholder: "搜索用户类型",
|
|
value: page.audienceType,
|
|
onChange: page.setAudienceType,
|
|
}),
|
|
width: "minmax(130px, 0.6fr)",
|
|
},
|
|
{
|
|
key: "regionIds",
|
|
label: "支持区域",
|
|
render: (item) => regionListLabel(regionLabels, item.regionIds),
|
|
filter: createRegionColumnFilter({
|
|
loading: page.loadingRegions,
|
|
options: page.regionOptions,
|
|
value: page.regionId,
|
|
onChange: page.setRegionId,
|
|
}),
|
|
width: "minmax(220px, 1fr)",
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "状态",
|
|
render: (item) => <ProductStatusSwitch item={item} page={page} />,
|
|
filter: createOptionsColumnFilter({
|
|
options: statusOptions,
|
|
placeholder: "搜索状态",
|
|
value: page.status,
|
|
onChange: page.setStatus,
|
|
}),
|
|
width: "minmax(110px, 0.5fr)",
|
|
},
|
|
{
|
|
key: "description",
|
|
label: "描述",
|
|
render: (item) => item.description || "-",
|
|
width: "minmax(180px, 0.9fr)",
|
|
},
|
|
{
|
|
key: "updatedAtMs",
|
|
label: "更新时间",
|
|
render: (item) => formatMillis(item.updatedAtMs),
|
|
width: "minmax(160px, 0.75fr)",
|
|
},
|
|
];
|
|
|
|
if (!page.abilities.canUpdateProduct && !page.abilities.canDeleteProduct) {
|
|
return columns;
|
|
}
|
|
|
|
return [
|
|
...columns,
|
|
{
|
|
key: "actions",
|
|
label: "操作",
|
|
render: (item) => <ProductActions item={item} page={page} />,
|
|
width: "minmax(96px, 0.45fr)",
|
|
},
|
|
];
|
|
}
|
|
|
|
function ProductStatusSwitch({ item, page }) {
|
|
const checked = Boolean(item.enabled);
|
|
const disabled = !page.abilities.canUpdateProduct || page.loadingAction === `status:${item.productId}`;
|
|
return (
|
|
<AdminSwitch
|
|
checked={checked}
|
|
checkedLabel="上架"
|
|
disabled={disabled}
|
|
inputProps={{ "aria-label": checked ? "下架内购" : "上架内购" }}
|
|
uncheckedLabel="下架"
|
|
onChange={(event) => page.toggleProductEnabled(item, event.target.checked)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function ProductActions({ item, page }) {
|
|
const deleting = page.loadingAction === `delete:${item.productId}`;
|
|
return (
|
|
<AdminRowActions>
|
|
{page.abilities.canUpdateProduct ? (
|
|
<AdminActionIconButton
|
|
disabled={Boolean(page.loadingAction)}
|
|
label="编辑"
|
|
onClick={() => page.openEdit(item)}
|
|
>
|
|
<EditOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
) : null}
|
|
{page.abilities.canDeleteProduct ? (
|
|
<AdminActionIconButton
|
|
disabled={deleting || Boolean(page.loadingAction)}
|
|
label="删除"
|
|
onClick={() => page.removeProduct(item)}
|
|
>
|
|
<DeleteOutlineOutlined fontSize="small" />
|
|
</AdminActionIconButton>
|
|
) : null}
|
|
</AdminRowActions>
|
|
);
|
|
}
|
|
|
|
function Stack({ primary, secondary }) {
|
|
return (
|
|
<div className="cell-stack">
|
|
<span>{primary || "-"}</span>
|
|
<span className="muted">{secondary || "-"}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function platformLabel(value) {
|
|
if (value === "web") {
|
|
return "Web";
|
|
}
|
|
if (value === "android") {
|
|
return "Android";
|
|
}
|
|
if (value === "ios") {
|
|
return "iOS";
|
|
}
|
|
return value || "-";
|
|
}
|
|
|
|
function audienceLabel(value) {
|
|
if (value === "coin_seller") {
|
|
return "币商";
|
|
}
|
|
if (value === "normal") {
|
|
return "普通用户";
|
|
}
|
|
return value || "普通用户";
|
|
}
|
|
|
|
function regionLabelMap(options) {
|
|
return options.reduce((labels, option) => {
|
|
labels[String(option.value)] = option.name || option.label;
|
|
return labels;
|
|
}, {});
|
|
}
|
|
|
|
function regionListLabel(labels, regionIds = []) {
|
|
if (!regionIds.length) {
|
|
return "-";
|
|
}
|
|
return regionIds.map((regionId) => labels[String(regionId)] || `区域 ${regionId}`).join("、");
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
const numberValue = Number(value || 0);
|
|
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : "-";
|
|
}
|
|
|
|
function formatMicroAmount(value) {
|
|
const numberValue = Number(value || 0);
|
|
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
|
return "0";
|
|
}
|
|
return String(numberValue / 1_000_000).replace(/\.?0+$/, "");
|
|
}
|