支付和新app
This commit is contained in:
parent
64cfa472b8
commit
e25258f233
@ -622,6 +622,55 @@
|
|||||||
"x-permissions": ["seven-day-checkin:update"]
|
"x-permissions": ["seven-day-checkin:update"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/managers": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "listManagers",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/PageSize"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/Keyword"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/Status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/RegionId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/ManagerPageResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "bd:view",
|
||||||
|
"x-permissions": ["bd:view"]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "createManager",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateManagerPayload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/components/responses/ManagerResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "bd:create",
|
||||||
|
"x-permissions": ["bd:create"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/agencies": {
|
"/admin/agencies": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "listAgencies",
|
"operationId": "listAgencies",
|
||||||
@ -2477,6 +2526,18 @@
|
|||||||
"x-permissions": ["payment-third-party:update"]
|
"x-permissions": ["payment-third-party:update"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/payment/third-party-rates/sync": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "syncThirdPartyPaymentRates",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "payment-third-party:update",
|
||||||
|
"x-permissions": ["payment-third-party:update"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/regions": {
|
"/admin/regions": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "listRegions",
|
"operationId": "listRegions",
|
||||||
@ -4283,6 +4344,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ManagerPageResponse": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResponseManagerPage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ManagerResponse": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ApiResponseManager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"BDProfilePageResponse": {
|
"BDProfilePageResponse": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"content": {
|
"content": {
|
||||||
@ -4476,6 +4557,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ApiPageManager": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["items", "page", "pageSize", "total"],
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Manager"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"pageSize": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ApiPageBDProfile": {
|
"ApiPageBDProfile": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["items", "page", "pageSize", "total"],
|
"required": ["items", "page", "pageSize", "total"],
|
||||||
@ -4590,6 +4692,36 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ApiResponseManagerPage": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Envelope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/ApiPageManager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ApiResponseManager": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Envelope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/Manager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ApiResponseBDProfilePage": {
|
"ApiResponseBDProfilePage": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@ -4908,6 +5040,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Manager": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["userId"],
|
||||||
|
"properties": {
|
||||||
|
"avatar": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bdLeaderCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"createdAtMs": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"displayUserId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"lastInvitedAtMs": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"regionId": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"regionName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["active", "disabled"]
|
||||||
|
},
|
||||||
|
"updatedAtMs": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CreateManagerPayload": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["targetUserId"],
|
||||||
|
"properties": {
|
||||||
|
"contact": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"targetUserId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"BDProfile": {
|
"BDProfile": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["userId"],
|
"required": ["userId"],
|
||||||
|
|||||||
@ -34,7 +34,7 @@ describe("navigation menu helpers", () => {
|
|||||||
test("keeps only fallback menu items allowed by permissions", () => {
|
test("keeps only fallback menu items allowed by permissions", () => {
|
||||||
const items = filterNavigationByPermission(
|
const items = filterNavigationByPermission(
|
||||||
fallbackNavigation,
|
fallbackNavigation,
|
||||||
(code) => code === "agency:view" || code === "host:view",
|
(code) => code === "agency:view" || code === "bd:view" || code === "host:view",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(flattenCodes(items)).not.toContain("geo");
|
expect(flattenCodes(items)).not.toContain("geo");
|
||||||
@ -42,7 +42,8 @@ describe("navigation menu helpers", () => {
|
|||||||
expect(flattenCodes(items)).toContain("host-org-managers");
|
expect(flattenCodes(items)).toContain("host-org-managers");
|
||||||
expect(flattenCodes(items)).toContain("host-org-agencies");
|
expect(flattenCodes(items)).toContain("host-org-agencies");
|
||||||
expect(flattenCodes(items)).toContain("host-org-hosts");
|
expect(flattenCodes(items)).toContain("host-org-hosts");
|
||||||
expect(flattenCodes(items)).not.toContain("host-org-bds");
|
expect(flattenCodes(items)).toContain("host-org-bd-leaders");
|
||||||
|
expect(flattenCodes(items)).toContain("host-org-bds");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("places country and region pages under geo menu", () => {
|
test("places country and region pages under geo menu", () => {
|
||||||
@ -77,7 +78,10 @@ describe("navigation menu helpers", () => {
|
|||||||
label: "团队管理",
|
label: "团队管理",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const fallbackMenus = filterNavigationByPermission(fallbackNavigation, (code) => code === "agency:view");
|
const fallbackMenus = filterNavigationByPermission(
|
||||||
|
fallbackNavigation,
|
||||||
|
(code) => code === "agency:view" || code === "bd:view",
|
||||||
|
);
|
||||||
const merged = mergeNavigationItems(backendMenus, fallbackMenus);
|
const merged = mergeNavigationItems(backendMenus, fallbackMenus);
|
||||||
|
|
||||||
expect(flattenCodes(merged)).toContain("host-org-agencies");
|
expect(flattenCodes(merged)).toContain("host-org-agencies");
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
createBDLeader,
|
createBDLeader,
|
||||||
createCountry,
|
createCountry,
|
||||||
createCoinSeller,
|
createCoinSeller,
|
||||||
|
createManager,
|
||||||
createRegion,
|
createRegion,
|
||||||
creditCoinSellerStock,
|
creditCoinSellerStock,
|
||||||
debitCoinSellerStock,
|
debitCoinSellerStock,
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
listCoinSellers,
|
listCoinSellers,
|
||||||
listCountries,
|
listCountries,
|
||||||
listHosts,
|
listHosts,
|
||||||
|
listManagers,
|
||||||
listRegions,
|
listRegions,
|
||||||
replaceRegionCountries,
|
replaceRegionCountries,
|
||||||
setCoinSellerStatus,
|
setCoinSellerStatus,
|
||||||
@ -51,6 +53,14 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
sort_by: "salary_wallet",
|
sort_by: "salary_wallet",
|
||||||
sort_direction: "desc",
|
sort_direction: "desc",
|
||||||
});
|
});
|
||||||
|
await listManagers({
|
||||||
|
keyword: "manager",
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
region_id: 7,
|
||||||
|
status: "active",
|
||||||
|
});
|
||||||
|
await createManager({ contact: "+63", targetUserId: "1003" });
|
||||||
await listHosts({ agency_id: 41, page: 1, page_size: 10, sort_by: "diamond", sort_direction: "desc" });
|
await listHosts({ agency_id: 41, page: 1, page_size: 10, sort_by: "diamond", sort_direction: "desc" });
|
||||||
await listCoinSellers({
|
await listCoinSellers({
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -86,15 +96,17 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
|
|
||||||
const [bdUrl, bdInit] = vi.mocked(fetch).mock.calls[0];
|
const [bdUrl, bdInit] = vi.mocked(fetch).mock.calls[0];
|
||||||
const [agencyUrl] = vi.mocked(fetch).mock.calls[1];
|
const [agencyUrl] = vi.mocked(fetch).mock.calls[1];
|
||||||
const [hostUrl] = vi.mocked(fetch).mock.calls[2];
|
const [managerUrl, managerInit] = vi.mocked(fetch).mock.calls[2];
|
||||||
const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3];
|
const [managerCreateUrl, managerCreateInit] = vi.mocked(fetch).mock.calls[3];
|
||||||
const [bdLeaderCreateUrl, bdLeaderCreateInit] = vi.mocked(fetch).mock.calls[4];
|
const [hostUrl] = vi.mocked(fetch).mock.calls[4];
|
||||||
const [bdLeaderAliasUrl, bdLeaderAliasInit] = vi.mocked(fetch).mock.calls[5];
|
const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[5];
|
||||||
const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[6];
|
const [bdLeaderCreateUrl, bdLeaderCreateInit] = vi.mocked(fetch).mock.calls[6];
|
||||||
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[7];
|
const [bdLeaderAliasUrl, bdLeaderAliasInit] = vi.mocked(fetch).mock.calls[7];
|
||||||
const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[8];
|
const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[8];
|
||||||
const [coinSellerStockDebitUrl, coinSellerStockDebitInit] = vi.mocked(fetch).mock.calls[9];
|
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[9];
|
||||||
const [agencyDeleteUrl, agencyDeleteInit] = vi.mocked(fetch).mock.calls[10];
|
const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[10];
|
||||||
|
const [coinSellerStockDebitUrl, coinSellerStockDebitInit] = vi.mocked(fetch).mock.calls[11];
|
||||||
|
const [agencyDeleteUrl, agencyDeleteInit] = vi.mocked(fetch).mock.calls[12];
|
||||||
|
|
||||||
expect(String(bdUrl)).toContain("/api/v1/admin/bds?");
|
expect(String(bdUrl)).toContain("/api/v1/admin/bds?");
|
||||||
expect(String(bdUrl)).toContain("parent_leader_user_id=21");
|
expect(String(bdUrl)).toContain("parent_leader_user_id=21");
|
||||||
@ -107,6 +119,15 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
expect(String(agencyUrl)).toContain("parent_bd_user_id=31");
|
expect(String(agencyUrl)).toContain("parent_bd_user_id=31");
|
||||||
expect(String(agencyUrl)).toContain("sort_by=salary_wallet");
|
expect(String(agencyUrl)).toContain("sort_by=salary_wallet");
|
||||||
expect(String(agencyUrl)).toContain("sort_direction=desc");
|
expect(String(agencyUrl)).toContain("sort_direction=desc");
|
||||||
|
expect(String(managerUrl)).toContain("/api/v1/admin/managers?");
|
||||||
|
expect(String(managerUrl)).toContain("keyword=manager");
|
||||||
|
expect(String(managerUrl)).toContain("region_id=7");
|
||||||
|
expect(String(managerUrl)).toContain("status=active");
|
||||||
|
expect(String(managerUrl)).not.toContain("parent_bd_user_id");
|
||||||
|
expect(managerInit?.method).toBe("GET");
|
||||||
|
expect(String(managerCreateUrl)).toContain("/api/v1/admin/managers");
|
||||||
|
expect(managerCreateInit?.method).toBe("POST");
|
||||||
|
expect(JSON.parse(String(managerCreateInit?.body))).toEqual({ contact: "+63", targetUserId: "1003" });
|
||||||
expect(String(hostUrl)).toContain("/api/v1/admin/hosts?");
|
expect(String(hostUrl)).toContain("/api/v1/admin/hosts?");
|
||||||
expect(String(hostUrl)).toContain("agency_id=41");
|
expect(String(hostUrl)).toContain("agency_id=41");
|
||||||
expect(String(hostUrl)).toContain("sort_by=diamond");
|
expect(String(hostUrl)).toContain("sort_by=diamond");
|
||||||
|
|||||||
@ -25,9 +25,11 @@ import type {
|
|||||||
CreateBDLeaderPayload,
|
CreateBDLeaderPayload,
|
||||||
CreateBDPayload,
|
CreateBDPayload,
|
||||||
CreateCoinSellerPayload,
|
CreateCoinSellerPayload,
|
||||||
|
CreateManagerPayload,
|
||||||
EntityId,
|
EntityId,
|
||||||
HostCommandPayload,
|
HostCommandPayload,
|
||||||
HostProfileDto,
|
HostProfileDto,
|
||||||
|
ManagerDto,
|
||||||
PageQuery,
|
PageQuery,
|
||||||
RegionCountriesPayload,
|
RegionCountriesPayload,
|
||||||
RegionDto,
|
RegionDto,
|
||||||
@ -168,6 +170,22 @@ export function listAgencies(query: PageQuery = {}): Promise<ApiPage<AgencyDto>>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listManagers(query: PageQuery = {}): Promise<ApiPage<ManagerDto>> {
|
||||||
|
const endpoint = API_ENDPOINTS.listManagers;
|
||||||
|
return apiRequest<ApiPage<ManagerDto>>(apiEndpointPath(API_OPERATIONS.listManagers), {
|
||||||
|
method: endpoint.method,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createManager(payload: CreateManagerPayload): Promise<ManagerDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.createManager;
|
||||||
|
return apiRequest<ManagerDto, CreateManagerPayload>(apiEndpointPath(API_OPERATIONS.createManager), {
|
||||||
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function listHosts(query: PageQuery = {}): Promise<ApiPage<HostProfileDto>> {
|
export function listHosts(query: PageQuery = {}): Promise<ApiPage<HostProfileDto>> {
|
||||||
const endpoint = API_ENDPOINTS.listHosts;
|
const endpoint = API_ENDPOINTS.listHosts;
|
||||||
return apiRequest<ApiPage<HostProfileDto>>(apiEndpointPath(API_OPERATIONS.listHosts), {
|
return apiRequest<ApiPage<HostProfileDto>>(apiEndpointPath(API_OPERATIONS.listHosts), {
|
||||||
|
|||||||
@ -22,6 +22,12 @@ export const bdStatusFilters = [
|
|||||||
["disabled", "停用"]
|
["disabled", "停用"]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const managerStatusFilters = [
|
||||||
|
["", "全部"],
|
||||||
|
["active", "启用"],
|
||||||
|
["disabled", "停用"]
|
||||||
|
];
|
||||||
|
|
||||||
export const hostStatusFilters = [
|
export const hostStatusFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
|
|||||||
@ -1,31 +1,34 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { parseForm } from "@/shared/forms/validation";
|
||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||||
|
import { createManager, listManagers } from "@/features/host-org/api";
|
||||||
|
import { useBDAbilities } from "@/features/host-org/permissions.js";
|
||||||
|
import { createManagerSchema } from "@/features/host-org/schema";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import { closeAgency, listAgencies } from "@/features/host-org/api";
|
|
||||||
import { useAgencyAbilities } from "@/features/host-org/permissions.js";
|
|
||||||
|
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||||
|
const emptyManagerForm = () => ({ contact: "", targetUserId: "" });
|
||||||
|
|
||||||
export function useHostManagersPage() {
|
export function useHostManagersPage() {
|
||||||
const abilities = useAgencyAbilities();
|
const abilities = useBDAbilities();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||||
|
const [activeAction, setActiveAction] = useState("");
|
||||||
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
|
const [managerForm, setManagerForm] = useState(emptyManagerForm);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [status, setStatus] = useState("");
|
const [status, setStatus] = useState("");
|
||||||
const [regionId, setRegionId] = useState("");
|
const [regionId, setRegionId] = useState("");
|
||||||
const [parentBdUserId, setParentBdUserId] = useState("");
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [loadingAction, setLoadingAction] = useState("");
|
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
keyword: query,
|
keyword: query,
|
||||||
parent_bd_user_id: parentBdUserId,
|
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
status,
|
status,
|
||||||
}),
|
}),
|
||||||
[parentBdUserId, query, regionId, status],
|
[query, regionId, status],
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
data = emptyData,
|
data = emptyData,
|
||||||
@ -34,7 +37,7 @@ export function useHostManagersPage() {
|
|||||||
reload,
|
reload,
|
||||||
} = usePaginatedQuery({
|
} = usePaginatedQuery({
|
||||||
errorMessage: "加载经理列表失败",
|
errorMessage: "加载经理列表失败",
|
||||||
fetcher: listAgencies,
|
fetcher: listManagers,
|
||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@ -56,68 +59,58 @@ export function useHostManagersPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeParentBdUserId = (value) => {
|
|
||||||
setParentBdUserId(value);
|
|
||||||
setPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
setQuery("");
|
setQuery("");
|
||||||
setStatus("");
|
setStatus("");
|
||||||
setRegionId("");
|
setRegionId("");
|
||||||
setParentBdUserId("");
|
|
||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeManagerAgency = async (agency) => {
|
const submitManager = async (event) => {
|
||||||
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
event.preventDefault();
|
||||||
return;
|
setLoadingAction("manager-create");
|
||||||
}
|
|
||||||
await runAction(`manager-status-${agency.agencyId}`, "经理已停用", async () => {
|
|
||||||
await closeAgency(agency.agencyId, {
|
|
||||||
commandId: makeCommandId("manager-close"),
|
|
||||||
reason: "close manager agency from list switch",
|
|
||||||
});
|
|
||||||
await reload();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const runAction = async (action, successMessage, submitter) => {
|
|
||||||
setLoadingAction(action);
|
|
||||||
try {
|
try {
|
||||||
await submitter();
|
const payload = parseForm(createManagerSchema, managerForm);
|
||||||
showToast(successMessage, "success");
|
await createManager(payload);
|
||||||
|
setManagerForm(emptyManagerForm());
|
||||||
|
setActiveAction("");
|
||||||
|
showToast("经理已添加", "success");
|
||||||
|
await reload();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message || "操作失败", "error");
|
showToast(err.message || "添加经理失败", "error");
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingAction("");
|
setLoadingAction("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeAction = () => {
|
||||||
|
setActiveAction("");
|
||||||
|
setManagerForm(emptyManagerForm());
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
changeParentBdUserId,
|
activeAction,
|
||||||
changeQuery,
|
changeQuery,
|
||||||
changeRegionId,
|
changeRegionId,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
closeManagerAgency,
|
closeAction,
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
loading,
|
|
||||||
loadingAction,
|
loadingAction,
|
||||||
|
loading,
|
||||||
loadingRegions,
|
loadingRegions,
|
||||||
|
managerForm,
|
||||||
|
openManagerForm: () => setActiveAction("manager"),
|
||||||
page,
|
page,
|
||||||
parentBdUserId,
|
|
||||||
query,
|
query,
|
||||||
regionId,
|
regionId,
|
||||||
regionOptions,
|
regionOptions,
|
||||||
reload,
|
reload,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
|
setManagerForm,
|
||||||
setPage,
|
setPage,
|
||||||
status,
|
status,
|
||||||
|
submitManager,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCommandId(prefix) {
|
|
||||||
return `${prefix}-${Date.now()}`;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,55 +1,53 @@
|
|||||||
|
import Add from "@mui/icons-material/Add";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { agencyStatusFilters } from "@/features/host-org/constants.js";
|
import { managerStatusFilters, statusLabels } from "@/features/host-org/constants.js";
|
||||||
import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
|
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||||
|
import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
|
||||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||||
|
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||||
import { useHostManagersPage } from "@/features/host-org/hooks/useHostManagersPage.js";
|
import { useHostManagersPage } from "@/features/host-org/hooks/useHostManagersPage.js";
|
||||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const managerColumns = [
|
const managerColumns = [
|
||||||
{
|
{
|
||||||
key: "ownerUserId",
|
key: "userId",
|
||||||
label: "经理",
|
label: "经理",
|
||||||
render: (item) => <ManagerUser item={item} />,
|
render: (item) => <ManagerUser item={item} />,
|
||||||
width: "minmax(220px, 1.25fr)",
|
width: "minmax(220px, 1.25fr)",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "agency",
|
|
||||||
label: "Agency",
|
|
||||||
render: (item) => <HostOrgEntity id={item.agencyId} name={item.name} prefix="Agency " />,
|
|
||||||
width: "minmax(190px, 1fr)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "parentBdUserId",
|
|
||||||
label: "上级 BD",
|
|
||||||
render: (item) => <ParentBD item={item} />,
|
|
||||||
width: "minmax(220px, 1.2fr)",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "regionId",
|
key: "regionId",
|
||||||
label: "区域",
|
label: "区域",
|
||||||
render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions),
|
render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions),
|
||||||
width: "minmax(130px, 0.75fr)",
|
width: "minmax(130px, 0.75fr)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "contact",
|
||||||
|
label: "联系方式",
|
||||||
|
render: (item) => item.contact || "-",
|
||||||
|
width: "minmax(150px, 0.85fr)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
render: (item, _index, context) => <ManagerStatusSwitch item={item} page={context?.page} />,
|
render: (item) => <ManagerStatus item={item} />,
|
||||||
width: "minmax(90px, 0.7fr)",
|
width: "minmax(90px, 0.7fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "joinEnabled",
|
key: "bdLeaderCount",
|
||||||
label: "入会",
|
label: "BD Leader/Admin",
|
||||||
render: (item) => (
|
render: (item) => item.bdLeaderCount || 0,
|
||||||
<span className={item.joinEnabled ? styles.positive : styles.negative}>
|
width: "minmax(140px, 0.8fr)",
|
||||||
{item.joinEnabled ? "允许" : "禁止"}
|
},
|
||||||
</span>
|
{
|
||||||
),
|
key: "lastInvitedAtMs",
|
||||||
width: "minmax(80px, 0.6fr)",
|
label: "最近邀请",
|
||||||
|
render: (item) => formatMillis(item.lastInvitedAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
},
|
},
|
||||||
{ key: "maxHosts", label: "最大主播数", width: "minmax(100px, 0.7fr)" },
|
|
||||||
{
|
{
|
||||||
key: "updatedAtMs",
|
key: "updatedAtMs",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
@ -60,34 +58,30 @@ const managerColumns = [
|
|||||||
|
|
||||||
export function HostManagersPage() {
|
export function HostManagersPage() {
|
||||||
const page = useHostManagersPage();
|
const page = useHostManagersPage();
|
||||||
|
const createDisabled = !page.abilities.canCreate;
|
||||||
const items = page.data.items || [];
|
const items = page.data.items || [];
|
||||||
const total = page.data.total || 0;
|
const total = page.data.total || 0;
|
||||||
|
const toolbarActions = [
|
||||||
|
page.abilities.canCreate
|
||||||
|
? { icon: <Add fontSize="small" />, label: "添加经理", onClick: page.openManagerForm, variant: "primary" }
|
||||||
|
: null,
|
||||||
|
].filter(Boolean);
|
||||||
const columns = managerColumns.map((column) => {
|
const columns = managerColumns.map((column) => {
|
||||||
if (column.key === "ownerUserId") {
|
if (column.key === "userId") {
|
||||||
return {
|
return {
|
||||||
...column,
|
...column,
|
||||||
filter: createTextColumnFilter({
|
filter: createTextColumnFilter({
|
||||||
placeholder: "搜索经理、Agency、用户",
|
placeholder: "搜索经理",
|
||||||
value: page.query,
|
value: page.query,
|
||||||
onChange: page.changeQuery,
|
onChange: page.changeQuery,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (column.key === "parentBdUserId") {
|
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
filter: createTextColumnFilter({
|
|
||||||
placeholder: "搜索上级 BD 用户 ID",
|
|
||||||
value: page.parentBdUserId,
|
|
||||||
onChange: page.changeParentBdUserId,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (column.key === "status") {
|
if (column.key === "status") {
|
||||||
return {
|
return {
|
||||||
...column,
|
...column,
|
||||||
filter: createOptionsColumnFilter({
|
filter: createOptionsColumnFilter({
|
||||||
options: agencyStatusFilters,
|
options: managerStatusFilters,
|
||||||
placeholder: "搜索状态",
|
placeholder: "搜索状态",
|
||||||
value: page.status,
|
value: page.status,
|
||||||
onChange: page.changeStatus,
|
onChange: page.changeStatus,
|
||||||
@ -100,13 +94,14 @@ export function HostManagersPage() {
|
|||||||
return (
|
return (
|
||||||
<section className={styles.root}>
|
<section className={styles.root}>
|
||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
|
<HostOrgToolbar leadingActions={toolbarActions} />
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<HostOrgTable
|
<HostOrgTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
context={{ page, regionOptions: page.regionOptions }}
|
context={{ page, regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
minWidth="1250px"
|
minWidth="1240px"
|
||||||
pagination={
|
pagination={
|
||||||
total > 0
|
total > 0
|
||||||
? {
|
? {
|
||||||
@ -117,11 +112,35 @@ export function HostManagersPage() {
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
rowKey={(item) => `manager-${item.ownerUserId || item.agencyId}`}
|
rowKey={(item) => `manager-${item.userId}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
|
<HostOrgActionModal
|
||||||
|
disabled={createDisabled}
|
||||||
|
loading={page.loadingAction === "manager-create"}
|
||||||
|
open={page.activeAction === "manager"}
|
||||||
|
sectionTitle="经理信息"
|
||||||
|
onClose={page.closeAction}
|
||||||
|
onSubmit={page.submitManager}
|
||||||
|
title="添加经理"
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="用户短 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.managerForm.targetUserId}
|
||||||
|
onChange={(event) => page.setManagerForm({ ...page.managerForm, targetUserId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="联系方式"
|
||||||
|
value={page.managerForm.contact}
|
||||||
|
onChange={(event) => page.setManagerForm({ ...page.managerForm, contact: event.target.value })}
|
||||||
|
/>
|
||||||
|
</HostOrgActionModal>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -129,40 +148,19 @@ export function HostManagersPage() {
|
|||||||
function ManagerUser({ item }) {
|
function ManagerUser({ item }) {
|
||||||
return (
|
return (
|
||||||
<HostOrgPerson
|
<HostOrgPerson
|
||||||
avatar={item.ownerAvatar}
|
avatar={item.avatar}
|
||||||
displayUserId={item.ownerDisplayUserId}
|
displayUserId={item.displayUserId}
|
||||||
fallbackId={item.ownerUserId}
|
fallbackId={item.userId}
|
||||||
username={item.ownerUsername}
|
username={item.username}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ParentBD({ item }) {
|
function ManagerStatus({ item }) {
|
||||||
if (!item.parentBdUserId) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<HostOrgPerson
|
|
||||||
avatar={item.parentBdAvatar}
|
|
||||||
displayUserId={item.parentBdDisplayUserId}
|
|
||||||
fallbackId={item.parentBdUserId}
|
|
||||||
username={item.parentBdUsername || "BD"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ManagerStatusSwitch({ item, page }) {
|
|
||||||
const active = item.status === "active";
|
const active = item.status === "active";
|
||||||
return (
|
return (
|
||||||
<AdminSwitch
|
<span className={active ? styles.positive : styles.negative}>
|
||||||
checked={active}
|
{statusLabels[item.status] || item.status || "-"}
|
||||||
disabled={!active || !page?.abilities.canStatus || page.loadingAction === `manager-status-${item.agencyId}`}
|
</span>
|
||||||
inputProps={{ "aria-label": active ? "停用经理" : "经理已停用" }}
|
|
||||||
onChange={(event) => {
|
|
||||||
if (!event.target.checked) {
|
|
||||||
page.closeManagerAgency(item);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,12 @@ export const hostOrgRoutes = [
|
|||||||
permission: PERMISSIONS.regionView,
|
permission: PERMISSIONS.regionView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Manager列表",
|
label: "经理列表",
|
||||||
loader: () => import("./pages/HostManagersPage.jsx").then((module) => module.HostManagersPage),
|
loader: () => import("./pages/HostManagersPage.jsx").then((module) => module.HostManagersPage),
|
||||||
menuCode: MENU_CODES.hostOrgManagers,
|
menuCode: MENU_CODES.hostOrgManagers,
|
||||||
pageKey: "host-org-managers",
|
pageKey: "host-org-managers",
|
||||||
path: "/host/managers",
|
path: "/host/managers",
|
||||||
permission: PERMISSIONS.agencyView,
|
permission: PERMISSIONS.bdView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Agency 列表",
|
label: "Agency 列表",
|
||||||
|
|||||||
@ -105,6 +105,11 @@ export const createCoinSellerSchema = commandContactSchema.extend({
|
|||||||
targetUserId: userIdSchema,
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createManagerSchema = z.object({
|
||||||
|
contact: z.string().trim().max(128, "联系方式不能超过 128 个字符").optional().default(""),
|
||||||
|
targetUserId: userIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
export const coinSellerStatusSchema = commandContactSchema.extend({
|
export const coinSellerStatusSchema = commandContactSchema.extend({
|
||||||
status: z.enum(["active", "disabled"]),
|
status: z.enum(["active", "disabled"]),
|
||||||
targetUserId: entityIdSchema,
|
targetUserId: entityIdSchema,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import type {
|
|||||||
RechargeProductPayload,
|
RechargeProductPayload,
|
||||||
ThirdPartyPaymentChannelDto,
|
ThirdPartyPaymentChannelDto,
|
||||||
ThirdPartyPaymentMethodDto,
|
ThirdPartyPaymentMethodDto,
|
||||||
|
ThirdPartyPaymentRateSyncDto,
|
||||||
} from "@/shared/api/types";
|
} from "@/shared/api/types";
|
||||||
|
|
||||||
export function listRechargeBills(query: PageQuery = {}): Promise<ApiPage<RechargeBillDto>> {
|
export function listRechargeBills(query: PageQuery = {}): Promise<ApiPage<RechargeBillDto>> {
|
||||||
@ -97,3 +98,10 @@ export function updateThirdPartyPaymentRate(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function syncThirdPartyPaymentRates(): Promise<ThirdPartyPaymentRateSyncDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.syncThirdPartyPaymentRates;
|
||||||
|
return apiRequest<ThirdPartyPaymentRateSyncDto>(apiEndpointPath(API_OPERATIONS.syncThirdPartyPaymentRates), {
|
||||||
|
method: endpoint.method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
listThirdPartyPaymentChannels,
|
listThirdPartyPaymentChannels,
|
||||||
setThirdPartyPaymentMethodStatus,
|
setThirdPartyPaymentMethodStatus,
|
||||||
|
syncThirdPartyPaymentRates,
|
||||||
updateThirdPartyPaymentRate,
|
updateThirdPartyPaymentRate,
|
||||||
} from "@/features/payment/api";
|
} from "@/features/payment/api";
|
||||||
import { usePaymentAbilities } from "@/features/payment/permissions.js";
|
import { usePaymentAbilities } from "@/features/payment/permissions.js";
|
||||||
@ -106,6 +107,25 @@ export function useThirdPartyPaymentPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const syncRates = async () => {
|
||||||
|
// 汇率同步只触发服务端动作;汇率源、失败切换和写库都在 admin-server 完成,前端不接收可篡改的汇率输入。
|
||||||
|
if (!abilities.canUpdateThirdParty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoadingAction("sync-rates");
|
||||||
|
try {
|
||||||
|
const summary = await syncThirdPartyPaymentRates();
|
||||||
|
const fresh = await result.reload();
|
||||||
|
setRateDrafts(rateDraftsFromChannels(fresh?.items || []));
|
||||||
|
const suffix = summary?.sourceNames?.length ? `(${summary.sourceNames.join("、")})` : "";
|
||||||
|
showToast(`汇率已同步 ${summary?.updatedCount || 0} 个支付方式${suffix}`, "success");
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || "汇率同步失败", "error");
|
||||||
|
} finally {
|
||||||
|
setLoadingAction("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
activeChannel,
|
activeChannel,
|
||||||
@ -118,7 +138,17 @@ export function useThirdPartyPaymentPage() {
|
|||||||
reload: result.reload,
|
reload: result.reload,
|
||||||
saveRate,
|
saveRate,
|
||||||
setRateDraft,
|
setRateDraft,
|
||||||
|
syncRates,
|
||||||
toggleChannel,
|
toggleChannel,
|
||||||
toggleMethod,
|
toggleMethod,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rateDraftsFromChannels(channels) {
|
||||||
|
return channels.reduce((drafts, channel) => {
|
||||||
|
(channel.methods || []).forEach((method) => {
|
||||||
|
drafts[String(method.methodId)] = method.usdToCurrencyRate || "";
|
||||||
|
});
|
||||||
|
return drafts;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|||||||
@ -2,9 +2,13 @@ import AddOutlined from "@mui/icons-material/AddOutlined";
|
|||||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Tab from "@mui/material/Tab";
|
||||||
|
import Tabs from "@mui/material/Tabs";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useMemo } 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 { Button } from "@/shared/ui/Button.jsx";
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import {
|
import {
|
||||||
AdminFormAmountField,
|
AdminFormAmountField,
|
||||||
@ -49,146 +53,173 @@ const audienceOptions = [
|
|||||||
|
|
||||||
export function RechargeProductConfigPage() {
|
export function RechargeProductConfigPage() {
|
||||||
const page = useRechargeProductsPage();
|
const page = useRechargeProductsPage();
|
||||||
|
const [activeProductTab, setActiveProductTab] = useState("google");
|
||||||
|
const googleProductPlatform = "android";
|
||||||
|
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]);
|
||||||
const regionValues = useMemo(() => page.regionOptions.map((option) => option.value), [page.regionOptions]);
|
const regionValues = useMemo(() => page.regionOptions.map((option) => option.value), [page.regionOptions]);
|
||||||
const columns = useMemo(() => productColumns(page, regionLabels), [page, regionLabels]);
|
const columns = useMemo(() => productColumns(page, regionLabels, true), [page, regionLabels]);
|
||||||
const saving = page.loadingAction === "create" || page.loadingAction === "edit";
|
const saving = page.loadingAction === "create" || page.loadingAction === "edit";
|
||||||
const canSubmit = page.editingItem ? page.abilities.canUpdateProduct : page.abilities.canCreateProduct;
|
const canSubmit = page.editingItem ? page.abilities.canUpdateProduct : page.abilities.canCreateProduct;
|
||||||
|
const openCreate = () => {
|
||||||
|
page.openCreate();
|
||||||
|
page.setForm({ platform: googleProductPlatform });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeProductTab === "google" && platform !== googleProductPlatform) {
|
||||||
|
setPlatform(googleProductPlatform);
|
||||||
|
}
|
||||||
|
}, [activeProductTab, googleProductPlatform, platform, setPlatform]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<div className={styles.paymentTabsBar}>
|
||||||
actions={
|
<Tabs value={activeProductTab} onChange={(_, value) => setActiveProductTab(value)}>
|
||||||
page.abilities.canCreateProduct ? (
|
<Tab label="Google" value="google" />
|
||||||
<Button
|
<Tab label="三方" value="third_party" />
|
||||||
startIcon={<AddOutlined fontSize="small" />}
|
</Tabs>
|
||||||
variant="primary"
|
</div>
|
||||||
onClick={page.openCreate}
|
{activeProductTab === "google" ? (
|
||||||
>
|
<>
|
||||||
新增内购
|
<AdminListToolbar
|
||||||
</Button>
|
actions={
|
||||||
) : null
|
page.abilities.canCreateProduct ? (
|
||||||
}
|
<Button
|
||||||
/>
|
startIcon={<AddOutlined fontSize="small" />}
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
variant="primary"
|
||||||
<AdminListBody>
|
onClick={openCreate}
|
||||||
<DataTable
|
>
|
||||||
columns={columns}
|
新增 Google 商品
|
||||||
items={items}
|
</Button>
|
||||||
minWidth="1180px"
|
) : null
|
||||||
pagination={{
|
}
|
||||||
page: page.page,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
total: page.data?.total || 0,
|
|
||||||
onPageChange: page.setPage,
|
|
||||||
}}
|
|
||||||
rowKey={(item) => item.productId}
|
|
||||||
/>
|
/>
|
||||||
</AdminListBody>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
</DataState>
|
<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>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ThirdPartyPaymentContent embedded />
|
||||||
|
)}
|
||||||
|
|
||||||
<AdminFormDialog
|
{activeProductTab === "google" ? (
|
||||||
loading={saving}
|
<AdminFormDialog
|
||||||
open={Boolean(page.activeAction)}
|
loading={saving}
|
||||||
size="compact"
|
open={Boolean(page.activeAction)}
|
||||||
submitDisabled={!canSubmit || saving}
|
size="compact"
|
||||||
title={page.editingItem ? "编辑内购配置" : "新增内购配置"}
|
submitDisabled={!canSubmit || saving}
|
||||||
onClose={page.closeDialog}
|
title={page.editingItem ? "编辑 Google 商品" : "新增 Google 商品"}
|
||||||
onSubmit={page.submitProduct}
|
onClose={page.closeDialog}
|
||||||
>
|
onSubmit={page.submitProduct}
|
||||||
<AdminFormSection title="商品信息">
|
>
|
||||||
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
<AdminFormSection title="商品信息">
|
||||||
<TextField
|
<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}
|
disabled={!canSubmit}
|
||||||
label="产品名称"
|
label="支持区域"
|
||||||
|
labels={regionLabels}
|
||||||
|
loading={page.loadingRegions}
|
||||||
|
options={regionValues}
|
||||||
required
|
required
|
||||||
value={page.form.productName}
|
value={page.form.regionIds}
|
||||||
onChange={(event) => page.setForm({ productName: event.target.value })}
|
onChange={(regionIds) => page.setForm({ regionIds })}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<AdminFormSwitchField
|
||||||
|
checked={Boolean(page.form.enabled)}
|
||||||
|
checkedLabel="上架"
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
label="平台"
|
label="是否上架"
|
||||||
required
|
uncheckedLabel="下架"
|
||||||
select
|
onChange={(checked) => page.setForm({ enabled: checked })}
|
||||||
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 })}
|
|
||||||
/>
|
/>
|
||||||
|
</AdminFormSection>
|
||||||
|
<AdminFormSection title="描述">
|
||||||
<TextField
|
<TextField
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
inputProps={{ min: 1, step: 1 }}
|
label="描述"
|
||||||
label="获得金币数"
|
multiline
|
||||||
required
|
minRows={2}
|
||||||
type="number"
|
value={page.form.description}
|
||||||
value={page.form.coinAmount}
|
onChange={(event) => page.setForm({ description: event.target.value })}
|
||||||
onChange={(event) => page.setForm({ coinAmount: event.target.value })}
|
|
||||||
/>
|
/>
|
||||||
</AdminFormFieldGrid>
|
</AdminFormSection>
|
||||||
</AdminFormSection>
|
</AdminFormDialog>
|
||||||
<AdminFormSection title="投放设置">
|
) : null}
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function productColumns(page, regionLabels) {
|
function productColumns(page, regionLabels, lockedPlatform = false) {
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: "product",
|
key: "product",
|
||||||
@ -216,12 +247,14 @@ function productColumns(page, regionLabels) {
|
|||||||
key: "platform",
|
key: "platform",
|
||||||
label: "平台",
|
label: "平台",
|
||||||
render: (item) => platformLabel(item.platform),
|
render: (item) => platformLabel(item.platform),
|
||||||
filter: createOptionsColumnFilter({
|
filter: lockedPlatform
|
||||||
options: platformOptions,
|
? undefined
|
||||||
placeholder: "搜索平台",
|
: createOptionsColumnFilter({
|
||||||
value: page.platform,
|
options: platformOptions,
|
||||||
onChange: page.setPlatform,
|
placeholder: "搜索平台",
|
||||||
}),
|
value: page.platform,
|
||||||
|
onChange: page.setPlatform,
|
||||||
|
}),
|
||||||
width: "minmax(120px, 0.55fr)",
|
width: "minmax(120px, 0.55fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined";
|
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined";
|
||||||
import KeyboardArrowRightOutlined from "@mui/icons-material/KeyboardArrowRightOutlined";
|
import KeyboardArrowRightOutlined from "@mui/icons-material/KeyboardArrowRightOutlined";
|
||||||
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
||||||
|
import SyncOutlined from "@mui/icons-material/SyncOutlined";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -12,22 +13,37 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
|||||||
import styles from "@/features/payment/payment.module.css";
|
import styles from "@/features/payment/payment.module.css";
|
||||||
|
|
||||||
export function ThirdPartyPaymentPage() {
|
export function ThirdPartyPaymentPage() {
|
||||||
|
return <ThirdPartyPaymentContent showProductButton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThirdPartyPaymentContent({ embedded = false, showProductButton = false }) {
|
||||||
const page = useThirdPartyPaymentPage();
|
const page = useThirdPartyPaymentPage();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// 国家分组只影响展示密度,不改变后端返回的可开关粒度;每个 method 仍独立保存状态和汇率。
|
// 国家分组只影响展示密度,不改变后端返回的可开关粒度;每个 method 仍独立保存状态和汇率。
|
||||||
const groupedMethods = useMemo(() => groupMethods(page.activeChannel?.methods || []), [page.activeChannel]);
|
const groupedMethods = useMemo(() => groupMethods(page.activeChannel?.methods || []), [page.activeChannel]);
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<AdminListPage>
|
<>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<div className={styles.toolbarActions}>
|
||||||
startIcon={<SettingsOutlined fontSize="small" />}
|
<Button
|
||||||
variant="primary"
|
disabled={!page.abilities.canUpdateThirdParty || page.loadingAction === "sync-rates"}
|
||||||
onClick={() => navigate("/payment/recharge-products")}
|
startIcon={<SyncOutlined fontSize="small" />}
|
||||||
>
|
onClick={page.syncRates}
|
||||||
三方支付内购配置
|
>
|
||||||
</Button>
|
支付汇率全球同步
|
||||||
|
</Button>
|
||||||
|
{showProductButton ? (
|
||||||
|
<Button
|
||||||
|
startIcon={<SettingsOutlined fontSize="small" />}
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => navigate("/payment/recharge-products")}
|
||||||
|
>
|
||||||
|
支付内购商品
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
@ -82,9 +98,12 @@ export function ThirdPartyPaymentPage() {
|
|||||||
</div>
|
</div>
|
||||||
{group.methods.map((method) => (
|
{group.methods.map((method) => (
|
||||||
<div key={method.methodId} className={styles.methodRow}>
|
<div key={method.methodId} className={styles.methodRow}>
|
||||||
<span className={styles.methodName}>
|
<span className={styles.methodIdentity}>
|
||||||
<strong>{method.methodName || method.payWay || "-"}</strong>
|
<MethodLogo method={method} />
|
||||||
<small>{method.payWay || "-"}</small>
|
<span className={styles.methodName}>
|
||||||
|
<strong>{method.methodName || method.payWay || "-"}</strong>
|
||||||
|
<small>{method.payWay || "-"}</small>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span>{method.payType || "-"}</span>
|
<span>{method.payType || "-"}</span>
|
||||||
<span>{method.currencyCode || "-"}</span>
|
<span>{method.currencyCode || "-"}</span>
|
||||||
@ -117,9 +136,10 @@ export function ThirdPartyPaymentPage() {
|
|||||||
page.loadingAction === `status:${method.methodId}`
|
page.loadingAction === `status:${method.methodId}`
|
||||||
}
|
}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
"aria-label": method.status === "active"
|
"aria-label":
|
||||||
? "关闭支付方式"
|
method.status === "active"
|
||||||
: "打开支付方式",
|
? "关闭支付方式"
|
||||||
|
: "打开支付方式",
|
||||||
}}
|
}}
|
||||||
uncheckedLabel="关"
|
uncheckedLabel="关"
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@ -139,14 +159,28 @@ export function ThirdPartyPaymentPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AdminListBody>
|
</AdminListBody>
|
||||||
</DataState>
|
</DataState>
|
||||||
</AdminListPage>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (embedded) {
|
||||||
|
return <div className={styles.paymentEmbedded}>{content}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AdminListPage>{content}</AdminListPage>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPill({ active }) {
|
function StatusPill({ active }) {
|
||||||
return <span className={active ? styles.statusActive : styles.statusDisabled}>{active ? "已启用" : "已停用"}</span>;
|
return <span className={active ? styles.statusActive : styles.statusDisabled}>{active ? "已启用" : "已停用"}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MethodLogo({ method }) {
|
||||||
|
const label = String(method.methodName || method.payType || method.payWay || "Pay").slice(0, 2).toUpperCase();
|
||||||
|
if (method.logoUrl) {
|
||||||
|
return <img alt="" className={styles.methodLogo} src={method.logoUrl} />;
|
||||||
|
}
|
||||||
|
return <span className={styles.methodLogoFallback}>{label}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
function groupMethods(methods) {
|
function groupMethods(methods) {
|
||||||
// GLOBAL 是三方支付的跨国家方式,放在最前面,国家方式再按标题排序,方便运营先处理全局支付。
|
// GLOBAL 是三方支付的跨国家方式,放在最前面,国家方式再按标题排序,方便运营先处理全局支付。
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
|
|||||||
@ -55,6 +55,29 @@
|
|||||||
font-size: var(--admin-font-size);
|
font-size: var(--admin-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbarActions {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paymentTabsBar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paymentEmbedded {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.thirdPartyLayout {
|
.thirdPartyLayout {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -207,6 +230,37 @@
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.methodIdentity {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
grid-template-columns: 56px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodLogo,
|
||||||
|
.methodLogoFallback {
|
||||||
|
width: 56px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodLogo {
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodLogoFallback {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.methodName strong,
|
.methodName strong,
|
||||||
.methodName small {
|
.methodName small {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const paymentRoutes = [
|
|||||||
permission: PERMISSIONS.paymentThirdPartyView,
|
permission: PERMISSIONS.paymentThirdPartyView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "三方支付内购配置",
|
label: "支付内购商品",
|
||||||
loader: () =>
|
loader: () =>
|
||||||
import("./pages/RechargeProductConfigPage.jsx").then((module) => module.RechargeProductConfigPage),
|
import("./pages/RechargeProductConfigPage.jsx").then((module) => module.RechargeProductConfigPage),
|
||||||
menuCode: MENU_CODES.paymentRechargeProducts,
|
menuCode: MENU_CODES.paymentRechargeProducts,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
157
src/shared/api/generated/schema.d.ts
vendored
157
src/shared/api/generated/schema.d.ts
vendored
@ -532,6 +532,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/managers": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["listManagers"];
|
||||||
|
put?: never;
|
||||||
|
post: operations["createManager"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/agencies": {
|
"/admin/agencies": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -1188,6 +1204,22 @@ export interface paths {
|
|||||||
patch: operations["updateDiceConfig"];
|
patch: operations["updateDiceConfig"];
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/game/self-games/{game_id}/pool/adjust": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["adjustDicePool"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/game/robots": {
|
"/admin/game/robots": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -1716,6 +1748,22 @@ export interface paths {
|
|||||||
patch: operations["updateThirdPartyPaymentRate"];
|
patch: operations["updateThirdPartyPaymentRate"];
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/payment/third-party-rates/sync": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["syncThirdPartyPaymentRates"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/regions": {
|
"/admin/regions": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -2910,6 +2958,12 @@ export interface components {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
ApiPageManager: {
|
||||||
|
items: components["schemas"]["Manager"][];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
ApiPageBDProfile: {
|
ApiPageBDProfile: {
|
||||||
items: components["schemas"]["BDProfile"][];
|
items: components["schemas"]["BDProfile"][];
|
||||||
page: number;
|
page: number;
|
||||||
@ -2940,6 +2994,12 @@ export interface components {
|
|||||||
ApiResponseAgencyPage: components["schemas"]["Envelope"] & {
|
ApiResponseAgencyPage: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["ApiPageAgency"];
|
data?: components["schemas"]["ApiPageAgency"];
|
||||||
};
|
};
|
||||||
|
ApiResponseManagerPage: components["schemas"]["Envelope"] & {
|
||||||
|
data?: components["schemas"]["ApiPageManager"];
|
||||||
|
};
|
||||||
|
ApiResponseManager: components["schemas"]["Envelope"] & {
|
||||||
|
data?: components["schemas"]["Manager"];
|
||||||
|
};
|
||||||
ApiResponseBDProfilePage: components["schemas"]["Envelope"] & {
|
ApiResponseBDProfilePage: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["ApiPageBDProfile"];
|
data?: components["schemas"]["ApiPageBDProfile"];
|
||||||
};
|
};
|
||||||
@ -3011,6 +3071,25 @@ export interface components {
|
|||||||
status?: string;
|
status?: string;
|
||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
};
|
};
|
||||||
|
Manager: {
|
||||||
|
avatar?: string;
|
||||||
|
bdLeaderCount?: number;
|
||||||
|
createdAtMs?: number;
|
||||||
|
contact?: string;
|
||||||
|
displayUserId?: string;
|
||||||
|
lastInvitedAtMs?: number;
|
||||||
|
regionId?: number;
|
||||||
|
regionName?: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
status?: "active" | "disabled";
|
||||||
|
updatedAtMs?: number;
|
||||||
|
userId: string;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
CreateManagerPayload: {
|
||||||
|
contact?: string;
|
||||||
|
targetUserId: string;
|
||||||
|
};
|
||||||
BDProfile: {
|
BDProfile: {
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
createdByUserId?: number;
|
createdByUserId?: number;
|
||||||
@ -3088,6 +3167,24 @@ export interface components {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description OK */
|
/** @description OK */
|
||||||
|
ManagerPageResponse: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["ApiResponseManagerPage"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description OK */
|
||||||
|
ManagerResponse: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["ApiResponseManager"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description OK */
|
||||||
BDProfilePageResponse: {
|
BDProfilePageResponse: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@ -3901,6 +3998,40 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
listManagers: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
page?: components["parameters"]["Page"];
|
||||||
|
page_size?: components["parameters"]["PageSize"];
|
||||||
|
keyword?: components["parameters"]["Keyword"];
|
||||||
|
status?: components["parameters"]["Status"];
|
||||||
|
region_id?: components["parameters"]["RegionId"];
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["ManagerPageResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
createManager: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CreateManagerPayload"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
201: components["responses"]["ManagerResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
listAgencies: {
|
listAgencies: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
@ -4777,6 +4908,20 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
adjustDicePool: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
game_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
listDiceRobots: {
|
listDiceRobots: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -5350,6 +5495,18 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
syncThirdPartyPaymentRates: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
listRegions: {
|
listRegions: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@ -452,6 +452,7 @@ export interface ThirdPartyPaymentMethodDto {
|
|||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
countryName?: string;
|
countryName?: string;
|
||||||
currencyCode?: string;
|
currencyCode?: string;
|
||||||
|
logoUrl?: string;
|
||||||
methodId: number;
|
methodId: number;
|
||||||
methodName?: string;
|
methodName?: string;
|
||||||
payType?: string;
|
payType?: string;
|
||||||
@ -472,6 +473,21 @@ export interface ThirdPartyPaymentChannelDto {
|
|||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ThirdPartyPaymentRateSyncDto {
|
||||||
|
missingCurrencies?: string[];
|
||||||
|
rates?: Record<string, string>;
|
||||||
|
skippedCount?: number;
|
||||||
|
sourceNames?: string[];
|
||||||
|
sources?: Array<{
|
||||||
|
currencies?: string[];
|
||||||
|
error?: string;
|
||||||
|
name: string;
|
||||||
|
success: boolean;
|
||||||
|
url?: string;
|
||||||
|
}>;
|
||||||
|
updatedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppVersionDto {
|
export interface AppVersionDto {
|
||||||
appCode?: string;
|
appCode?: string;
|
||||||
buildNumber: number;
|
buildNumber: number;
|
||||||
@ -542,6 +558,26 @@ export interface AgencyDto {
|
|||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ManagerDto {
|
||||||
|
avatar?: string;
|
||||||
|
bdLeaderCount?: number;
|
||||||
|
contact?: string;
|
||||||
|
createdAtMs?: number;
|
||||||
|
displayUserId?: string;
|
||||||
|
lastInvitedAtMs?: number;
|
||||||
|
regionId?: number;
|
||||||
|
regionName?: string;
|
||||||
|
status?: "active" | "disabled";
|
||||||
|
updatedAtMs?: number;
|
||||||
|
userId: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateManagerPayload {
|
||||||
|
contact?: string;
|
||||||
|
targetUserId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HostProfileDto {
|
export interface HostProfileDto {
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user