充值弹窗
This commit is contained in:
parent
f5d06e1e52
commit
97915b5980
@ -7,7 +7,6 @@ import Tabs from "@mui/material/Tabs";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useRechargeProductsPage } from "@/features/payment/hooks/useRechargeProductsPage.js";
|
import { useRechargeProductsPage } from "@/features/payment/hooks/useRechargeProductsPage.js";
|
||||||
import { ThirdPartyPaymentContent } from "@/features/payment/pages/ThirdPartyPaymentPage.jsx";
|
|
||||||
import styles from "@/features/payment/payment.module.css";
|
import styles from "@/features/payment/payment.module.css";
|
||||||
import { Button } from "@/shared/ui/Button.jsx";
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import {
|
import {
|
||||||
@ -51,10 +50,25 @@ const audienceOptions = [
|
|||||||
["coin_seller", "币商"],
|
["coin_seller", "币商"],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const productTabConfigs = {
|
||||||
|
google: {
|
||||||
|
createLabel: "新增 Google 商品",
|
||||||
|
dialogEditTitle: "编辑 Google 商品",
|
||||||
|
dialogCreateTitle: "新增 Google 商品",
|
||||||
|
platform: "android",
|
||||||
|
},
|
||||||
|
third_party: {
|
||||||
|
createLabel: "新增三方商品",
|
||||||
|
dialogEditTitle: "编辑三方商品",
|
||||||
|
dialogCreateTitle: "新增三方商品",
|
||||||
|
platform: "web",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function RechargeProductConfigPage() {
|
export function RechargeProductConfigPage() {
|
||||||
const page = useRechargeProductsPage();
|
const page = useRechargeProductsPage();
|
||||||
const [activeProductTab, setActiveProductTab] = useState("google");
|
const [activeProductTab, setActiveProductTab] = useState("google");
|
||||||
const googleProductPlatform = "android";
|
const activeProductConfig = productTabConfigs[activeProductTab] || productTabConfigs.google;
|
||||||
const { platform, setPlatform } = page;
|
const { platform, setPlatform } = page;
|
||||||
const items = page.data?.items || [];
|
const items = page.data?.items || [];
|
||||||
const regionLabels = useMemo(() => regionLabelMap(page.regionOptions), [page.regionOptions]);
|
const regionLabels = useMemo(() => regionLabelMap(page.regionOptions), [page.regionOptions]);
|
||||||
@ -64,14 +78,15 @@ export function RechargeProductConfigPage() {
|
|||||||
const canSubmit = page.editingItem ? page.abilities.canUpdateProduct : page.abilities.canCreateProduct;
|
const canSubmit = page.editingItem ? page.abilities.canUpdateProduct : page.abilities.canCreateProduct;
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
page.openCreate();
|
page.openCreate();
|
||||||
page.setForm({ platform: googleProductPlatform });
|
page.setForm({ platform: activeProductConfig.platform });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeProductTab === "google" && platform !== googleProductPlatform) {
|
// 商品接口已经支持按 platform 区分 Google / 三方档位;tab 只负责把查询和新建表单固定到对应平台。
|
||||||
setPlatform(googleProductPlatform);
|
if (platform !== activeProductConfig.platform) {
|
||||||
|
setPlatform(activeProductConfig.platform);
|
||||||
}
|
}
|
||||||
}, [activeProductTab, googleProductPlatform, platform, setPlatform]);
|
}, [activeProductConfig.platform, platform, setPlatform]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
@ -81,140 +96,127 @@ export function RechargeProductConfigPage() {
|
|||||||
<Tab label="三方" value="third_party" />
|
<Tab label="三方" value="third_party" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
{activeProductTab === "google" ? (
|
<AdminListToolbar
|
||||||
<>
|
actions={
|
||||||
<AdminListToolbar
|
page.abilities.canCreateProduct ? (
|
||||||
actions={
|
<Button startIcon={<AddOutlined fontSize="small" />} variant="primary" onClick={openCreate}>
|
||||||
page.abilities.canCreateProduct ? (
|
{activeProductConfig.createLabel}
|
||||||
<Button
|
</Button>
|
||||||
startIcon={<AddOutlined fontSize="small" />}
|
) : null
|
||||||
variant="primary"
|
}
|
||||||
onClick={openCreate}
|
/>
|
||||||
>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
新增 Google 商品
|
<AdminListBody>
|
||||||
</Button>
|
<DataTable
|
||||||
) : null
|
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}
|
||||||
/>
|
/>
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
</AdminListBody>
|
||||||
<AdminListBody>
|
</DataState>
|
||||||
<DataTable
|
<AdminFormDialog
|
||||||
columns={columns}
|
loading={saving}
|
||||||
items={items}
|
open={Boolean(page.activeAction)}
|
||||||
minWidth="1180px"
|
size="compact"
|
||||||
pagination={{
|
submitDisabled={!canSubmit || saving}
|
||||||
page: page.page,
|
title={page.editingItem ? activeProductConfig.dialogEditTitle : activeProductConfig.dialogCreateTitle}
|
||||||
pageSize: page.pageSize,
|
onClose={page.closeDialog}
|
||||||
total: page.data?.total || 0,
|
onSubmit={page.submitProduct}
|
||||||
onPageChange: page.setPage,
|
>
|
||||||
}}
|
<AdminFormSection title="商品信息">
|
||||||
rowKey={(item) => item.productId}
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||||
/>
|
|
||||||
</AdminListBody>
|
|
||||||
</DataState>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ThirdPartyPaymentContent embedded />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeProductTab === "google" ? (
|
|
||||||
<AdminFormDialog
|
|
||||||
loading={saving}
|
|
||||||
open={Boolean(page.activeAction)}
|
|
||||||
size="compact"
|
|
||||||
submitDisabled={!canSubmit || saving}
|
|
||||||
title={page.editingItem ? "编辑 Google 商品" : "新增 Google 商品"}
|
|
||||||
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
|
|
||||||
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
|
<TextField
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
label="描述"
|
label="产品名称"
|
||||||
multiline
|
required
|
||||||
minRows={2}
|
value={page.form.productName}
|
||||||
value={page.form.description}
|
onChange={(event) => page.setForm({ productName: event.target.value })}
|
||||||
onChange={(event) => page.setForm({ description: event.target.value })}
|
|
||||||
/>
|
/>
|
||||||
</AdminFormSection>
|
<TextField
|
||||||
</AdminFormDialog>
|
disabled
|
||||||
) : null}
|
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>
|
</AdminListPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/features/payment/pages/RechargeProductConfigPage.test.jsx
Normal file
111
src/features/payment/pages/RechargeProductConfigPage.test.jsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
|
import { RechargeProductConfigPage } from "@/features/payment/pages/RechargeProductConfigPage.jsx";
|
||||||
|
import { useRechargeProductsPage } from "@/features/payment/hooks/useRechargeProductsPage.js";
|
||||||
|
|
||||||
|
vi.mock("@/features/payment/hooks/useRechargeProductsPage.js", () => ({
|
||||||
|
useRechargeProductsPage: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recharge product page defaults to google product list", () => {
|
||||||
|
vi.mocked(useRechargeProductsPage).mockReturnValue(pageFixture());
|
||||||
|
|
||||||
|
render(<RechargeProductConfigPage />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("tab", { name: "Google", selected: true })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "新增 Google 商品" })).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText("coins_480000").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.queryByText("支付汇率全球同步")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("third-party tab reuses product dialog and locks create platform to web", () => {
|
||||||
|
const page = pageFixture();
|
||||||
|
vi.mocked(useRechargeProductsPage).mockReturnValue(page);
|
||||||
|
|
||||||
|
render(<RechargeProductConfigPage />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "三方" }));
|
||||||
|
|
||||||
|
expect(screen.getByRole("tab", { name: "三方", selected: true })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "新增三方商品" })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("支付汇率全球同步")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "新增三方商品" }));
|
||||||
|
|
||||||
|
expect(page.openCreate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(page.setForm).toHaveBeenCalledWith({ platform: "web" });
|
||||||
|
});
|
||||||
|
|
||||||
|
function pageFixture() {
|
||||||
|
return {
|
||||||
|
abilities: {
|
||||||
|
canCreateProduct: true,
|
||||||
|
canDeleteProduct: true,
|
||||||
|
canUpdateProduct: true,
|
||||||
|
},
|
||||||
|
activeAction: "",
|
||||||
|
audienceType: "",
|
||||||
|
closeDialog: vi.fn(),
|
||||||
|
data: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
amountUsdt: "5.99",
|
||||||
|
audienceType: "normal",
|
||||||
|
coinAmount: 480000,
|
||||||
|
description: "480,000 coins",
|
||||||
|
enabled: true,
|
||||||
|
platform: "android",
|
||||||
|
productCode: "coins_480000",
|
||||||
|
productId: 1,
|
||||||
|
productName: "coins_480000",
|
||||||
|
regionIds: [1, 2],
|
||||||
|
updatedAtMs: 1760000000000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
editingItem: null,
|
||||||
|
error: null,
|
||||||
|
form: {
|
||||||
|
amountUsdt: "",
|
||||||
|
audienceType: "normal",
|
||||||
|
coinAmount: "",
|
||||||
|
description: "",
|
||||||
|
enabled: true,
|
||||||
|
platform: "android",
|
||||||
|
productName: "",
|
||||||
|
regionIds: [],
|
||||||
|
},
|
||||||
|
keyword: "",
|
||||||
|
loading: false,
|
||||||
|
loadingAction: "",
|
||||||
|
loadingRegions: false,
|
||||||
|
openCreate: vi.fn(),
|
||||||
|
openEdit: vi.fn(),
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
platform: "android",
|
||||||
|
regionId: "",
|
||||||
|
regionOptions: [
|
||||||
|
{ label: "中东 · ME · 1", name: "中东", value: "1" },
|
||||||
|
{ label: "欧洲美区 · EU · 2", name: "欧洲美区", value: "2" },
|
||||||
|
],
|
||||||
|
reload: vi.fn(),
|
||||||
|
removeProduct: vi.fn(),
|
||||||
|
resetFilters: vi.fn(),
|
||||||
|
setAudienceType: vi.fn(),
|
||||||
|
setForm: vi.fn(),
|
||||||
|
setKeyword: vi.fn(),
|
||||||
|
setPage: vi.fn(),
|
||||||
|
setPlatform: vi.fn(),
|
||||||
|
setRegionId: vi.fn(),
|
||||||
|
setStatus: vi.fn(),
|
||||||
|
status: "",
|
||||||
|
submitProduct: vi.fn(),
|
||||||
|
toggleProductEnabled: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user