公会相关
This commit is contained in:
parent
ca194e46a3
commit
0f92efea87
@ -957,6 +957,48 @@
|
||||
"x-permissions": ["coin-seller:stock-credit"]
|
||||
}
|
||||
},
|
||||
"/admin/coin-seller-salary-rates/{region_id}": {
|
||||
"get": {
|
||||
"operationId": "getCoinSellerSalaryRates",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "region_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "coin-seller:exchange-rate",
|
||||
"x-permissions": ["coin-seller:exchange-rate"]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "replaceCoinSellerSalaryRates",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "region_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "coin-seller:exchange-rate",
|
||||
"x-permissions": ["coin-seller:exchange-rate"]
|
||||
}
|
||||
},
|
||||
"/admin/countries": {
|
||||
"get": {
|
||||
"operationId": "listCountries",
|
||||
|
||||
@ -36,6 +36,7 @@ export const PERMISSIONS = {
|
||||
coinSellerCreate: "coin-seller:create",
|
||||
coinSellerUpdate: "coin-seller:update",
|
||||
coinSellerStockCredit: "coin-seller:stock-credit",
|
||||
coinSellerExchangeRate: "coin-seller:exchange-rate",
|
||||
coinLedgerView: "coin-ledger:view",
|
||||
coinAdjustmentView: "coin-adjustment:view",
|
||||
coinAdjustmentCreate: "coin-adjustment:create",
|
||||
|
||||
@ -35,10 +35,17 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
flex: 0 0 auto;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.selectedMeta {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--admin-font-size);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
@ -28,6 +28,8 @@ export function useAppUsersPage() {
|
||||
const [countryForm, setCountryForm] = useState(emptyCountryForm);
|
||||
const [passwordForm, setPasswordForm] = useState(emptyPasswordForm);
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const [selectedUserIds, setSelectedUserIds] = useState([]);
|
||||
const [patchedUsers, setPatchedUsers] = useState({});
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword: query,
|
||||
@ -36,7 +38,7 @@ export function useAppUsersPage() {
|
||||
[query, status],
|
||||
);
|
||||
const {
|
||||
data = emptyData,
|
||||
data: queryData = emptyData,
|
||||
error,
|
||||
loading,
|
||||
reload,
|
||||
@ -48,6 +50,18 @@ export function useAppUsersPage() {
|
||||
pageSize,
|
||||
queryKey: ["app-users", filters, page],
|
||||
});
|
||||
const data = useMemo(() => {
|
||||
const items = queryData.items || [];
|
||||
return {
|
||||
...queryData,
|
||||
items: items.map((user) => (patchedUsers[user.userId] ? { ...user, ...patchedUsers[user.userId] } : user)),
|
||||
};
|
||||
}, [patchedUsers, queryData]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedUserIds([]);
|
||||
setPatchedUsers({});
|
||||
}, [page, query, status]);
|
||||
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
@ -132,23 +146,58 @@ export function useAppUsersPage() {
|
||||
|
||||
const toggleBan = async (user) => {
|
||||
const banned = isBanned(user);
|
||||
await runAction(`status-${user.userId}`, banned ? "用户已解封" : "用户已封禁", async () => {
|
||||
const result = banned ? await unbanAppUser(user.userId) : await banAppUser(user.userId);
|
||||
patchUsers([mergeUserStatus(user, result, banned ? "active" : "banned")]);
|
||||
setSelectedUserIds((current) => current.filter((userId) => userId !== user.userId));
|
||||
});
|
||||
};
|
||||
|
||||
const batchBan = async () => {
|
||||
const users = (data.items || []).filter((user) => selectedUserIds.includes(user.userId) && !isBanned(user));
|
||||
if (!users.length) {
|
||||
showToast("请先选择未封禁用户", "warning");
|
||||
return;
|
||||
}
|
||||
const ok = await confirm({
|
||||
confirmText: banned ? "解封" : "封禁",
|
||||
message: `${user.username || user.displayUserId || user.userId} 的状态会立即变更。`,
|
||||
title: banned ? "解封用户" : "封禁用户",
|
||||
tone: banned ? "primary" : "danger",
|
||||
confirmText: "封禁",
|
||||
message: `将封禁 ${users.length} 个用户。`,
|
||||
title: "批量封禁用户",
|
||||
tone: "danger",
|
||||
});
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
await runAction(`status-${user.userId}`, banned ? "用户已解封" : "用户已封禁", async () => {
|
||||
if (banned) {
|
||||
await unbanAppUser(user.userId);
|
||||
} else {
|
||||
await banAppUser(user.userId);
|
||||
|
||||
setLoadingAction("batch-ban");
|
||||
try {
|
||||
const results = await Promise.allSettled(users.map((user) => banAppUser(user.userId)));
|
||||
const updatedUsers = results
|
||||
.map((result, index) =>
|
||||
result.status === "fulfilled" ? mergeUserStatus(users[index], result.value, "banned") : null,
|
||||
)
|
||||
.filter(Boolean);
|
||||
const failedCount = results.length - updatedUsers.length;
|
||||
|
||||
if (updatedUsers.length) {
|
||||
patchUsers(updatedUsers);
|
||||
setSelectedUserIds((current) =>
|
||||
current.filter((userId) => !updatedUsers.some((updatedUser) => updatedUser.userId === userId)),
|
||||
);
|
||||
}
|
||||
await reload();
|
||||
});
|
||||
|
||||
if (failedCount > 0) {
|
||||
showToast(
|
||||
updatedUsers.length ? `已封禁 ${updatedUsers.length} 个,${failedCount} 个失败` : "批量封禁失败",
|
||||
updatedUsers.length ? "warning" : "error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(`已封禁 ${updatedUsers.length} 个用户`, "success");
|
||||
} finally {
|
||||
setLoadingAction("");
|
||||
}
|
||||
};
|
||||
|
||||
const runAction = async (action, successMessage, submitter) => {
|
||||
@ -163,10 +212,24 @@ export function useAppUsersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const patchUsers = (users) => {
|
||||
setPatchedUsers((current) => {
|
||||
const next = { ...current };
|
||||
users.forEach((user) => {
|
||||
if (!user?.userId) {
|
||||
return;
|
||||
}
|
||||
next[user.userId] = { ...(next[user.userId] || {}), ...user };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
abilities,
|
||||
activeAction,
|
||||
activeUser,
|
||||
batchBan,
|
||||
changeQuery,
|
||||
changeStatus,
|
||||
closeAction,
|
||||
@ -186,10 +249,12 @@ export function useAppUsersPage() {
|
||||
query,
|
||||
reload,
|
||||
resetFilters,
|
||||
selectedUserIds,
|
||||
setCountryForm,
|
||||
setEditForm,
|
||||
setPage,
|
||||
setPasswordForm,
|
||||
setSelectedUserIds,
|
||||
status,
|
||||
submitCountry,
|
||||
submitEdit,
|
||||
@ -201,3 +266,11 @@ export function useAppUsersPage() {
|
||||
function isBanned(user) {
|
||||
return user.status === "banned" || user.status === "disabled";
|
||||
}
|
||||
|
||||
function mergeUserStatus(currentUser, result, fallbackStatus) {
|
||||
return {
|
||||
...currentUser,
|
||||
...(result || {}),
|
||||
status: result?.status || fallbackStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,10 +4,12 @@ import LockOpenOutlined from "@mui/icons-material/LockOpenOutlined";
|
||||
import PasswordOutlined from "@mui/icons-material/PasswordOutlined";
|
||||
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { AdminFormDialog, AdminFormSection } from "@/shared/ui/AdminFormDialog.jsx";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||
@ -49,7 +51,51 @@ export function AppUserListPage() {
|
||||
const page = useAppUsersPage();
|
||||
const items = page.data.items || [];
|
||||
const total = page.data.total || 0;
|
||||
const selectableItems = items.filter((user) => !isBannedUser(user));
|
||||
const allChecked =
|
||||
selectableItems.length > 0 && selectableItems.every((user) => page.selectedUserIds.includes(user.userId));
|
||||
const selectedOnPageCount = selectableItems.filter((user) => page.selectedUserIds.includes(user.userId)).length;
|
||||
const someChecked = selectedOnPageCount > 0 && !allChecked;
|
||||
const toggleAll = (checked) => {
|
||||
if (!checked) {
|
||||
page.setSelectedUserIds((current) =>
|
||||
current.filter((userId) => !selectableItems.some((user) => user.userId === userId)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
page.setSelectedUserIds((current) => Array.from(new Set([...current, ...selectableItems.map((user) => user.userId)])));
|
||||
};
|
||||
const tableColumns = [
|
||||
{
|
||||
key: "select",
|
||||
header: (
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
disabled={!page.abilities.canStatus || selectableItems.length === 0}
|
||||
indeterminate={someChecked}
|
||||
size="small"
|
||||
onChange={(event) => toggleAll(event.target.checked)}
|
||||
/>
|
||||
),
|
||||
width: "52px",
|
||||
render: (user) => {
|
||||
const banned = isBannedUser(user);
|
||||
return (
|
||||
<Checkbox
|
||||
checked={page.selectedUserIds.includes(user.userId)}
|
||||
disabled={!page.abilities.canStatus || banned || page.loadingAction === "batch-ban"}
|
||||
size="small"
|
||||
onChange={(event) => {
|
||||
page.setSelectedUserIds((current) =>
|
||||
event.target.checked
|
||||
? Array.from(new Set([...current, user.userId]))
|
||||
: current.filter((userId) => userId !== user.userId),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
...columns[0],
|
||||
filter: createTextColumnFilter({
|
||||
@ -90,6 +136,19 @@ export function AppUserListPage() {
|
||||
<div className={styles.contentPanel}>
|
||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||
<div className={styles.listBlock}>
|
||||
{page.abilities.canStatus ? (
|
||||
<div className={styles.toolbar}>
|
||||
<span className={styles.selectedMeta}>已选 {page.selectedUserIds.length} 个</span>
|
||||
<Button
|
||||
disabled={!page.selectedUserIds.length || page.loadingAction === "batch-ban"}
|
||||
startIcon={<BlockOutlined fontSize="small" />}
|
||||
variant="danger"
|
||||
onClick={page.batchBan}
|
||||
>
|
||||
批量封禁
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<DataTable
|
||||
columns={tableColumns}
|
||||
items={items}
|
||||
@ -310,3 +369,7 @@ function formatNumber(value) {
|
||||
function formatCountry(user) {
|
||||
return user.countryDisplayName || user.countryName || user.country || "-";
|
||||
}
|
||||
|
||||
function isBannedUser(user) {
|
||||
return user.status === "banned" || user.status === "disabled";
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import Add from "@mui/icons-material/Add";
|
||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
@ -119,19 +120,18 @@ function PolicyBaseFields({ disabled, page }) {
|
||||
required
|
||||
className={styles.fieldCompact}
|
||||
disabled={disabled}
|
||||
label="金币转钻石比例"
|
||||
helperText="0 表示不折算"
|
||||
label="剩余钻石兑美元"
|
||||
size="small"
|
||||
value={form.giftCoinToDiamondRatio}
|
||||
onChange={(event) => setForm({ giftCoinToDiamondRatio: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
className={styles.fieldCompact}
|
||||
disabled={disabled}
|
||||
label="剩余钻石转美元比例"
|
||||
size="small"
|
||||
value={form.residualDiamondToUsdRate}
|
||||
onChange={(event) => setForm({ residualDiamondToUsdRate: event.target.value })}
|
||||
slotProps={{
|
||||
htmlInput: { min: 0, step: 1 },
|
||||
input: {
|
||||
endAdornment: <InputAdornment position="end">钻石=1美元</InputAdornment>,
|
||||
},
|
||||
}}
|
||||
type="number"
|
||||
value={form.residualDiamondsPerUsd}
|
||||
onChange={(event) => setForm({ residualDiamondsPerUsd: event.target.value })}
|
||||
/>
|
||||
<TimeRangeFilter
|
||||
className={styles.timeRangeField}
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { vi, test, expect } from "vitest";
|
||||
import { HostAgencyPolicyDrawer } from "@/features/host-agency-policy/components/HostAgencyPolicyDrawer.jsx";
|
||||
|
||||
test("host agency policy drawer shows residual diamonds per USD and hides gift ratio input", () => {
|
||||
render(<HostAgencyPolicyDrawer page={drawerPage()} />);
|
||||
|
||||
expect(screen.queryByLabelText("金币转钻石比例")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("剩余钻石兑美元")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
||||
expect(screen.getByText("钻石=1美元")).toBeInTheDocument();
|
||||
expect(screen.getByText("0 表示不折算")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
function drawerPage() {
|
||||
return {
|
||||
abilities: {
|
||||
canCreate: true,
|
||||
canUpdate: true,
|
||||
},
|
||||
addLevel: vi.fn(),
|
||||
closeDrawer: vi.fn(),
|
||||
drawerOpen: true,
|
||||
editingPolicy: null,
|
||||
form: {
|
||||
effectiveFrom: "",
|
||||
effectiveTo: "",
|
||||
enabled: false,
|
||||
levels: [
|
||||
{
|
||||
agencySalaryUsd: "0.5",
|
||||
hostCoinReward: "90000",
|
||||
hostSalaryUsd: "1.5",
|
||||
level: "1",
|
||||
requiredDiamonds: "350000",
|
||||
sortOrder: "1",
|
||||
status: "active",
|
||||
},
|
||||
],
|
||||
name: "Lalu Host & Agency Salary Policy",
|
||||
regionId: "1001",
|
||||
residualDiamondsPerUsd: "100",
|
||||
settlementMode: "daily",
|
||||
settlementTriggerMode: "automatic",
|
||||
},
|
||||
loadingAction: "",
|
||||
loadingRegions: false,
|
||||
regionOptions: [{ label: "默认区域", value: "1001" }],
|
||||
removeLevel: vi.fn(),
|
||||
setForm: vi.fn(),
|
||||
submitPolicy: vi.fn((event) => event.preventDefault()),
|
||||
updateLevel: vi.fn(),
|
||||
};
|
||||
}
|
||||
@ -12,6 +12,10 @@ import {
|
||||
updateHostAgencyPolicy,
|
||||
} from "@/features/host-agency-policy/api";
|
||||
import { useHostAgencyPolicyAbilities } from "@/features/host-agency-policy/permissions.js";
|
||||
import {
|
||||
residualDiamondsPerUsdToRate,
|
||||
residualRateToDiamondsPerUsd,
|
||||
} from "@/features/host-agency-policy/residualRate.js";
|
||||
import { hostAgencyPolicyFormSchema } from "@/features/host-agency-policy/schema";
|
||||
|
||||
const pageSize = 50;
|
||||
@ -51,12 +55,11 @@ const emptyForm = (policy = null) => ({
|
||||
effectiveTo: normalizeTimeMs(policy?.effectiveToMs),
|
||||
// 新建默认停用,运营保存草稿后再手动启用,避免半配置状态影响结算匹配。
|
||||
enabled: policy ? policy.status === "active" : false,
|
||||
giftCoinToDiamondRatio: policy?.giftCoinToDiamondRatio || "1",
|
||||
// 编辑时保留后端等级;新建时一次铺满 24 个默认等级,减少运营逐级录入成本。
|
||||
levels: policy?.levels?.length ? policy.levels.map(levelToForm) : defaultLevels.map(defaultLevelToForm),
|
||||
name: policy?.name || "Lalu Host & Agency Salary Policy",
|
||||
regionId: policy?.regionId ? String(policy.regionId) : "",
|
||||
residualDiamondToUsdRate: policy?.residualDiamondToUsdRate || "0",
|
||||
residualDiamondsPerUsd: residualRateToDiamondsPerUsd(policy?.residualDiamondToUsdRate || "0"),
|
||||
settlementMode: policy?.settlementMode || "daily",
|
||||
settlementTriggerMode: policy?.settlementTriggerMode || "automatic",
|
||||
});
|
||||
@ -321,7 +324,8 @@ function buildPayload(form) {
|
||||
return {
|
||||
effective_from_ms: timeValueToMs(form.effectiveFrom),
|
||||
effective_to_ms: timeValueToMs(form.effectiveTo),
|
||||
gift_coin_to_diamond_ratio: String(form.giftCoinToDiamondRatio || "1").trim(),
|
||||
// 主播周期钻石入账比例由“礼物钻石”配置页控制;工资政策不再提供第二套可编辑比例。
|
||||
gift_coin_to_diamond_ratio: "1",
|
||||
levels: form.levels
|
||||
.map((level) => ({
|
||||
// 保存等级时统一转 number,避免字符串数字进入后端后造成弱类型歧义。
|
||||
@ -337,7 +341,7 @@ function buildPayload(form) {
|
||||
.sort((left, right) => left.level - right.level),
|
||||
name: String(form.name || "").trim(),
|
||||
region_id: Number(form.regionId || 0),
|
||||
residual_diamond_to_usd_rate: String(form.residualDiamondToUsdRate || "0").trim(),
|
||||
residual_diamond_to_usd_rate: residualDiamondsPerUsdToRate(form.residualDiamondsPerUsd),
|
||||
settlement_mode: form.settlementMode || "daily",
|
||||
// 触发方式会被发布到 wallet 运行表;manual 表示后续只能从工资结算页人工触发。
|
||||
settlement_trigger_mode: form.settlementTriggerMode || "automatic",
|
||||
|
||||
@ -21,6 +21,7 @@ import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { HostAgencyPolicyDrawer } from "@/features/host-agency-policy/components/HostAgencyPolicyDrawer.jsx";
|
||||
import { useHostAgencyPolicyPage } from "@/features/host-agency-policy/hooks/useHostAgencyPolicyPage.js";
|
||||
import { TeamSalaryPolicyPanel } from "@/features/host-agency-policy/pages/TeamSalaryPolicyPanel.jsx";
|
||||
import { residualRateLabel } from "@/features/host-agency-policy/residualRate.js";
|
||||
import styles from "@/features/host-agency-policy/host-agency-policy.module.css";
|
||||
|
||||
const statusOptions = [
|
||||
@ -238,8 +239,8 @@ function SettlementSummary({ item }) {
|
||||
<div className={styles.stack}>
|
||||
<span>{settlementLabel(item.settlementMode)}</span>
|
||||
<span className={styles.meta}>{settlementTriggerLabel(item.settlementTriggerMode)}</span>
|
||||
<span className={styles.meta}>金币转钻石 {item.giftCoinToDiamondRatio || "1"}</span>
|
||||
<span className={styles.meta}>剩余钻石转美元 {item.residualDiamondToUsdRate || "0"}</span>
|
||||
<span className={styles.meta}>入账钻石按礼物钻石配置</span>
|
||||
<span className={styles.meta}>{residualRateLabel(item.residualDiamondToUsdRate)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
37
src/features/host-agency-policy/residualRate.js
Normal file
37
src/features/host-agency-policy/residualRate.js
Normal file
@ -0,0 +1,37 @@
|
||||
const maxRateScale = 12;
|
||||
|
||||
export function residualDiamondsPerUsdToRate(value) {
|
||||
const diamonds = Number(String(value ?? "").trim());
|
||||
if (!Number.isFinite(diamonds) || diamonds <= 0) {
|
||||
return "0";
|
||||
}
|
||||
return trimDecimal((1 / diamonds).toFixed(maxRateScale));
|
||||
}
|
||||
|
||||
export function residualRateToDiamondsPerUsd(rate) {
|
||||
const value = Number(String(rate ?? "").trim());
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return "0";
|
||||
}
|
||||
const diamonds = 1 / value;
|
||||
const rounded = Math.round(diamonds);
|
||||
// 后端 rate 只保留 12 位小数,1/3 这类配置回显时会有极小误差;贴近整数时按整数展示。
|
||||
if (Math.abs(diamonds - rounded) < 0.000001) {
|
||||
return String(rounded);
|
||||
}
|
||||
return trimDecimal(diamonds.toFixed(6));
|
||||
}
|
||||
|
||||
export function residualRateLabel(rate) {
|
||||
const diamonds = residualRateToDiamondsPerUsd(rate);
|
||||
return diamonds === "0" ? "剩余钻石不折美元" : `剩余钻石 ${diamonds}钻石=1美元`;
|
||||
}
|
||||
|
||||
function trimDecimal(value) {
|
||||
const text = String(value || "0");
|
||||
if (!text.includes(".")) {
|
||||
return text;
|
||||
}
|
||||
const trimmed = text.replace(/0+$/, "").replace(/\.$/, "");
|
||||
return trimmed || "0";
|
||||
}
|
||||
20
src/features/host-agency-policy/residualRate.test.js
Normal file
20
src/features/host-agency-policy/residualRate.test.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
residualDiamondsPerUsdToRate,
|
||||
residualRateLabel,
|
||||
residualRateToDiamondsPerUsd,
|
||||
} from "@/features/host-agency-policy/residualRate.js";
|
||||
|
||||
describe("host agency residual diamond rate", () => {
|
||||
test("submits diamonds per USD as backend USD-per-diamond rate", () => {
|
||||
expect(residualDiamondsPerUsdToRate("100")).toBe("0.01");
|
||||
expect(residualDiamondsPerUsdToRate("1000000")).toBe("0.000001");
|
||||
expect(residualDiamondsPerUsdToRate("0")).toBe("0");
|
||||
});
|
||||
|
||||
test("displays backend rate as diamonds per USD", () => {
|
||||
expect(residualRateToDiamondsPerUsd("0.01")).toBe("100");
|
||||
expect(residualRateLabel("0.01")).toBe("剩余钻石 100钻石=1美元");
|
||||
expect(residualRateLabel("0")).toBe("剩余钻石不折美元");
|
||||
});
|
||||
});
|
||||
@ -21,11 +21,10 @@ export const hostAgencyPolicyFormSchema = z
|
||||
effectiveFrom: z.union([z.string(), z.number()]).optional(),
|
||||
effectiveTo: z.union([z.string(), z.number()]).optional(),
|
||||
enabled: z.boolean(),
|
||||
giftCoinToDiamondRatio: z.union([z.string(), z.number()]),
|
||||
levels: z.array(levelSchema).min(1, "请至少配置一个等级"),
|
||||
name: z.string().trim().min(1, "请输入政策名称").max(120, "政策名称不能超过 120 个字符"),
|
||||
regionId: z.union([z.string(), z.number()]),
|
||||
residualDiamondToUsdRate: z.union([z.string(), z.number()]),
|
||||
residualDiamondsPerUsd: z.union([z.string(), z.number()]),
|
||||
settlementMode: z.enum(settlementModes),
|
||||
settlementTriggerMode: z.enum(settlementTriggerModes),
|
||||
})
|
||||
@ -35,14 +34,11 @@ export const hostAgencyPolicyFormSchema = z
|
||||
if (!Number.isInteger(regionId) || regionId <= 0) {
|
||||
context.addIssue({ code: "custom", message: "请选择适用区域", path: ["regionId"] });
|
||||
}
|
||||
if (!isFixedDecimal(value.giftCoinToDiamondRatio, 6, false)) {
|
||||
context.addIssue({ code: "custom", message: "金币转钻石比例不正确", path: ["giftCoinToDiamondRatio"] });
|
||||
}
|
||||
if (!isFixedDecimal(value.residualDiamondToUsdRate, 12, true)) {
|
||||
if (!isNonNegativeInteger(value.residualDiamondsPerUsd)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "剩余钻石转美元比例不正确",
|
||||
path: ["residualDiamondToUsdRate"],
|
||||
message: "剩余钻石兑美元配置不正确",
|
||||
path: ["residualDiamondsPerUsd"],
|
||||
});
|
||||
}
|
||||
const effectiveFromMs = timeValueToMs(value.effectiveFrom);
|
||||
@ -190,6 +186,11 @@ function isFixedDecimal(value: unknown, scale: number, allowZero: boolean) {
|
||||
return decimalToScaled(value, scale, allowZero) >= 0;
|
||||
}
|
||||
|
||||
function isNonNegativeInteger(value: unknown) {
|
||||
const raw = String(value ?? "").trim();
|
||||
return /^\d+$/.test(raw);
|
||||
}
|
||||
|
||||
function decimalToScaled(value: unknown, scale: number, allowZero = true) {
|
||||
// 小数校验转换成整数刻度比较,避免 JS 浮点数直接参与金额大小判断。
|
||||
const raw = String(value ?? "").trim();
|
||||
|
||||
@ -9,6 +9,8 @@ import type {
|
||||
BDProfileDto,
|
||||
BDStatusPayload,
|
||||
CoinSellerDto,
|
||||
CoinSellerSalaryRatesDto,
|
||||
CoinSellerSalaryRatesPayload,
|
||||
CoinSellerStockCreditDto,
|
||||
CoinSellerStockCreditPayload,
|
||||
CoinSellerStatusPayload,
|
||||
@ -250,6 +252,30 @@ export function creditCoinSellerStock(
|
||||
);
|
||||
}
|
||||
|
||||
export function getCoinSellerSalaryRates(regionId: EntityId): Promise<CoinSellerSalaryRatesDto> {
|
||||
const endpoint = API_ENDPOINTS.getCoinSellerSalaryRates;
|
||||
return apiRequest<CoinSellerSalaryRatesDto>(
|
||||
apiEndpointPath(API_OPERATIONS.getCoinSellerSalaryRates, { region_id: regionId }),
|
||||
{
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function replaceCoinSellerSalaryRates(
|
||||
regionId: EntityId,
|
||||
payload: CoinSellerSalaryRatesPayload,
|
||||
): Promise<CoinSellerSalaryRatesDto> {
|
||||
const endpoint = API_ENDPOINTS.replaceCoinSellerSalaryRates;
|
||||
return apiRequest<CoinSellerSalaryRatesDto, CoinSellerSalaryRatesPayload>(
|
||||
apiEndpointPath(API_OPERATIONS.replaceCoinSellerSalaryRates, { region_id: regionId }),
|
||||
{
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function createAgency(payload: CreateAgencyPayload): Promise<CreateAgencyResultDto> {
|
||||
const endpoint = API_ENDPOINTS.createAgency;
|
||||
return apiRequest<CreateAgencyResultDto, CreateAgencyPayload>(apiEndpointPath(API_OPERATIONS.createAgency), {
|
||||
|
||||
@ -3,7 +3,14 @@ import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { createCoinSeller, creditCoinSellerStock, listCoinSellers, setCoinSellerStatus } from "@/features/host-org/api";
|
||||
import {
|
||||
createCoinSeller,
|
||||
creditCoinSellerStock,
|
||||
getCoinSellerSalaryRates,
|
||||
listCoinSellers,
|
||||
replaceCoinSellerSalaryRates,
|
||||
setCoinSellerStatus,
|
||||
} from "@/features/host-org/api";
|
||||
import { useCoinSellerAbilities } from "@/features/host-org/permissions.js";
|
||||
import {
|
||||
coinSellerStatusSchema,
|
||||
@ -28,6 +35,13 @@ const emptyStockForm = () => ({
|
||||
rechargeAmount: "",
|
||||
type: "usdt_purchase",
|
||||
});
|
||||
const emptyRateTier = (index = 0) => ({
|
||||
coinPerUsd: "",
|
||||
enabled: true,
|
||||
maxUsd: "",
|
||||
minUsd: "",
|
||||
sortOrder: (index + 1) * 10,
|
||||
});
|
||||
|
||||
export function useHostCoinSellersPage() {
|
||||
const abilities = useCoinSellerAbilities();
|
||||
@ -41,6 +55,9 @@ export function useHostCoinSellersPage() {
|
||||
const [createForm, setCreateForm] = useState(emptyCreateForm);
|
||||
const [editForm, setEditForm] = useState(emptyEditForm);
|
||||
const [stockForm, setStockForm] = useState(emptyStockForm);
|
||||
const [rateRegionId, setRateRegionId] = useState("");
|
||||
const [rateTiers, setRateTiers] = useState([]);
|
||||
const [loadingRates, setLoadingRates] = useState(false);
|
||||
const [selectedSeller, setSelectedSeller] = useState(null);
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const filters = useMemo(
|
||||
@ -117,11 +134,69 @@ export function useHostCoinSellersPage() {
|
||||
setActiveAction("stock");
|
||||
};
|
||||
|
||||
const openRateSettings = async () => {
|
||||
if (!abilities.canExchangeRate) {
|
||||
return;
|
||||
}
|
||||
const nextRegionId = rateRegionId || regionId || regionOptions[0]?.value || "";
|
||||
setRateRegionId(nextRegionId);
|
||||
setActiveAction("rates");
|
||||
if (nextRegionId) {
|
||||
await loadRateTiers(nextRegionId);
|
||||
} else {
|
||||
setRateTiers([]);
|
||||
}
|
||||
};
|
||||
|
||||
const closeAction = () => {
|
||||
setActiveAction("");
|
||||
setSelectedSeller(null);
|
||||
};
|
||||
|
||||
const changeRateRegionId = async (value) => {
|
||||
setRateRegionId(value);
|
||||
if (value) {
|
||||
await loadRateTiers(value);
|
||||
} else {
|
||||
setRateTiers([]);
|
||||
}
|
||||
};
|
||||
|
||||
const loadRateTiers = async (targetRegionId = rateRegionId) => {
|
||||
if (!targetRegionId) {
|
||||
return;
|
||||
}
|
||||
setLoadingRates(true);
|
||||
try {
|
||||
const data = await getCoinSellerSalaryRates(targetRegionId);
|
||||
const next = (data?.tiers || []).map((tier, index) => ({
|
||||
coinPerUsd: String(tier.coinPerUsd || ""),
|
||||
enabled: tier.status ? tier.status === "active" : tier.enabled !== false,
|
||||
maxUsd: tier.maxUsd || centsToUSD(tier.maxUsdMinor),
|
||||
minUsd: tier.minUsd || centsToUSD(tier.minUsdMinor),
|
||||
sortOrder: tier.sortOrder || (index + 1) * 10,
|
||||
}));
|
||||
setRateTiers(next.length ? next : [emptyRateTier(0)]);
|
||||
} catch (err) {
|
||||
setRateTiers([emptyRateTier(0)]);
|
||||
showToast(err.message || "加载兑换比例失败", "error");
|
||||
} finally {
|
||||
setLoadingRates(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addRateTier = () => {
|
||||
setRateTiers((items) => [...items, emptyRateTier(items.length)]);
|
||||
};
|
||||
|
||||
const removeRateTier = (index) => {
|
||||
setRateTiers((items) => items.filter((_, itemIndex) => itemIndex !== index));
|
||||
};
|
||||
|
||||
const updateRateTier = (index, patch) => {
|
||||
setRateTiers((items) => items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)));
|
||||
};
|
||||
|
||||
const submitCreateSeller = async (event) => {
|
||||
event.preventDefault();
|
||||
await runAction("coin-seller-create", "币商已添加", async () => {
|
||||
@ -163,6 +238,29 @@ export function useHostCoinSellersPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const submitRateSettings = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!rateRegionId) {
|
||||
showToast("请选择区域", "error");
|
||||
return;
|
||||
}
|
||||
await runAction("coin-seller-rates", "兑换比例已保存", async () => {
|
||||
const payload = {
|
||||
tiers: rateTiers
|
||||
.filter((tier) => tier.minUsd || tier.maxUsd || tier.coinPerUsd)
|
||||
.map((tier, index) => ({
|
||||
coinPerUsd: Number(tier.coinPerUsd),
|
||||
enabled: tier.enabled !== false,
|
||||
maxUsd: String(tier.maxUsd || "").trim(),
|
||||
minUsd: String(tier.minUsd || "").trim(),
|
||||
sortOrder: Number(tier.sortOrder || (index + 1) * 10),
|
||||
})),
|
||||
};
|
||||
await replaceCoinSellerSalaryRates(rateRegionId, payload);
|
||||
await loadRateTiers(rateRegionId);
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSeller = async (seller, nextEnabled = seller.status !== "active") => {
|
||||
if (!abilities.canUpdate || !seller?.userId) {
|
||||
return;
|
||||
@ -206,7 +304,9 @@ export function useHostCoinSellersPage() {
|
||||
error,
|
||||
loading,
|
||||
loadingAction,
|
||||
loadingRates,
|
||||
loadingRegions,
|
||||
openRateSettings,
|
||||
openCreateSeller,
|
||||
openEditSeller,
|
||||
openStockCredit,
|
||||
@ -214,7 +314,12 @@ export function useHostCoinSellersPage() {
|
||||
query,
|
||||
regionId,
|
||||
regionOptions,
|
||||
rateRegionId,
|
||||
rateTiers,
|
||||
reload,
|
||||
addRateTier,
|
||||
changeRateRegionId,
|
||||
removeRateTier,
|
||||
resetFilters,
|
||||
selectedSeller,
|
||||
setCreateForm,
|
||||
@ -223,13 +328,23 @@ export function useHostCoinSellersPage() {
|
||||
setStockForm,
|
||||
status,
|
||||
stockForm,
|
||||
submitRateSettings,
|
||||
submitCreateSeller,
|
||||
submitEditSeller,
|
||||
submitStockCredit,
|
||||
toggleSeller,
|
||||
updateRateTier,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCommandId(prefix) {
|
||||
return `${prefix}-${Date.now()}`;
|
||||
}
|
||||
|
||||
function centsToUSD(value) {
|
||||
const amount = Number(value || 0);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return "";
|
||||
}
|
||||
return `${Math.trunc(amount / 100)}.${String(Math.abs(amount % 100)).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
@ -100,6 +100,43 @@
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.rateDrawerForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.rateTierHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
font-weight: 760;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.rateTierList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.rateTierRow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) auto auto;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.rateTierSwitch {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flagCell {
|
||||
font-size: var(--admin-font-size);
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import Add from "@mui/icons-material/Add";
|
||||
import DeleteOutline from "@mui/icons-material/DeleteOutline";
|
||||
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
||||
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
import TextField from "@mui/material/TextField";
|
||||
@ -12,6 +15,7 @@ import {
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||||
import { coinSellerStatusFilters } from "@/features/host-org/constants.js";
|
||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
@ -121,9 +125,14 @@ export function HostCoinSellersPage() {
|
||||
value: page.regionId,
|
||||
onChange: page.changeRegionId,
|
||||
});
|
||||
const toolbarActions = page.abilities.canCreate
|
||||
? [{ icon: <Add fontSize="small" />, label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }]
|
||||
: [];
|
||||
const toolbarActions = [
|
||||
page.abilities.canExchangeRate
|
||||
? { icon: <SettingsOutlined fontSize="small" />, label: "设置兑换比例", onClick: page.openRateSettings }
|
||||
: null,
|
||||
page.abilities.canCreate
|
||||
? { icon: <Add fontSize="small" />, label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<section className={styles.root}>
|
||||
@ -274,6 +283,91 @@ export function HostCoinSellersPage() {
|
||||
/>
|
||||
</AdminFormSection>
|
||||
</HostOrgActionModal>
|
||||
|
||||
<SideDrawer
|
||||
actions={
|
||||
<>
|
||||
<Button onClick={page.closeAction}>取消</Button>
|
||||
<Button
|
||||
disabled={!page.abilities.canExchangeRate || page.loadingAction === "coin-seller-rates"}
|
||||
variant="contained"
|
||||
onClick={page.submitRateSettings}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
open={page.activeAction === "rates"}
|
||||
title="币商工资兑换比例"
|
||||
width="wide"
|
||||
onClose={page.closeAction}
|
||||
>
|
||||
<form className={styles.rateDrawerForm} onSubmit={page.submitRateSettings}>
|
||||
<TextField
|
||||
disabled={page.loadingRegions || page.loadingRates}
|
||||
label="区域"
|
||||
required
|
||||
select
|
||||
value={page.rateRegionId}
|
||||
onChange={(event) => page.changeRateRegionId(event.target.value)}
|
||||
>
|
||||
{page.regionOptions.map((region) => (
|
||||
<MenuItem key={region.value} value={region.value}>
|
||||
{region.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<div className={styles.rateTierHeader}>
|
||||
<span>区间配置</span>
|
||||
<Button size="small" variant="outlined" onClick={page.addRateTier}>
|
||||
添加区间
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.rateTierList}>
|
||||
{page.rateTiers.map((tier, index) => (
|
||||
<div className={styles.rateTierRow} key={`${index}-${tier.sortOrder}`}>
|
||||
<TextField
|
||||
label="min USD"
|
||||
required
|
||||
value={tier.minUsd}
|
||||
onChange={(event) => page.updateRateTier(index, { minUsd: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="max USD"
|
||||
required
|
||||
value={tier.maxUsd}
|
||||
onChange={(event) => page.updateRateTier(index, { maxUsd: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
|
||||
label="coin per USD"
|
||||
required
|
||||
value={tier.coinPerUsd}
|
||||
onChange={(event) =>
|
||||
page.updateRateTier(index, { coinPerUsd: event.target.value })
|
||||
}
|
||||
/>
|
||||
<div className={styles.rateTierSwitch}>
|
||||
<AdminSwitch
|
||||
checked={tier.enabled !== false}
|
||||
checkedLabel="启用"
|
||||
label="状态"
|
||||
uncheckedLabel="停用"
|
||||
onChange={(event) => page.updateRateTier(index, { enabled: event.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
<AdminActionIconButton
|
||||
disabled={page.rateTiers.length <= 1}
|
||||
label="删除区间"
|
||||
onClick={() => page.removeRateTier(index)}
|
||||
>
|
||||
<DeleteOutline fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
</SideDrawer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -56,6 +56,7 @@ export function useCoinSellerAbilities() {
|
||||
|
||||
return {
|
||||
canCreate: can(PERMISSIONS.coinSellerCreate),
|
||||
canExchangeRate: can(PERMISSIONS.coinSellerExchangeRate),
|
||||
canStockCredit: can(PERMISSIONS.coinSellerStockCredit),
|
||||
canUpdate: can(PERMISSIONS.coinSellerUpdate),
|
||||
canView: can(PERMISSIONS.coinSellerView),
|
||||
|
||||
@ -85,6 +85,7 @@ export const API_OPERATIONS = {
|
||||
exportLoginLogs: "exportLoginLogs",
|
||||
exportOperationLogs: "exportOperationLogs",
|
||||
exportUsers: "exportUsers",
|
||||
getCoinSellerSalaryRates: "getCoinSellerSalaryRates",
|
||||
getCountry: "getCountry",
|
||||
getFirstRechargeRewardConfig: "getFirstRechargeRewardConfig",
|
||||
getLuckyGiftConfig: "getLuckyGiftConfig",
|
||||
@ -159,6 +160,7 @@ export const API_OPERATIONS = {
|
||||
navigationMenus: "navigationMenus",
|
||||
publishHostAgencyPolicy: "publishHostAgencyPolicy",
|
||||
refresh: "refresh",
|
||||
replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates",
|
||||
replaceRegionBlocks: "replaceRegionBlocks",
|
||||
replaceRegionCountries: "replaceRegionCountries",
|
||||
replaceRoleDataScopes: "replaceRoleDataScopes",
|
||||
@ -721,6 +723,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "user:export",
|
||||
permissions: ["user:export"]
|
||||
},
|
||||
getCoinSellerSalaryRates: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.getCoinSellerSalaryRates,
|
||||
path: "/v1/admin/coin-seller-salary-rates/{region_id}",
|
||||
permission: "coin-seller:exchange-rate",
|
||||
permissions: ["coin-seller:exchange-rate"]
|
||||
},
|
||||
getCountry: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.getCountry,
|
||||
@ -1227,6 +1236,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
operationId: API_OPERATIONS.refresh,
|
||||
path: "/v1/auth/refresh"
|
||||
},
|
||||
replaceCoinSellerSalaryRates: {
|
||||
method: "PUT",
|
||||
operationId: API_OPERATIONS.replaceCoinSellerSalaryRates,
|
||||
path: "/v1/admin/coin-seller-salary-rates/{region_id}",
|
||||
permission: "coin-seller:exchange-rate",
|
||||
permissions: ["coin-seller:exchange-rate"]
|
||||
},
|
||||
replaceRegionBlocks: {
|
||||
method: "PUT",
|
||||
operationId: API_OPERATIONS.replaceRegionBlocks,
|
||||
|
||||
44
src/shared/api/generated/schema.d.ts
vendored
44
src/shared/api/generated/schema.d.ts
vendored
@ -660,6 +660,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/coin-seller-salary-rates/{region_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["getCoinSellerSalaryRates"];
|
||||
put: operations["replaceCoinSellerSalaryRates"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/countries": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -3512,6 +3528,34 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
getCoinSellerSalaryRates: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
region_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
replaceCoinSellerSalaryRates: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
region_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listCountries: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@ -457,6 +457,39 @@ export interface CoinSellerStockCreditDto {
|
||||
transactionId: string;
|
||||
}
|
||||
|
||||
export interface CoinSellerSalaryRateTierDto {
|
||||
coinPerUsd: number;
|
||||
enabled?: boolean;
|
||||
maxUsd?: string;
|
||||
maxUsdMinor: number;
|
||||
minUsd?: string;
|
||||
minUsdMinor: number;
|
||||
regionId?: EntityId;
|
||||
sortOrder?: number;
|
||||
status?: string;
|
||||
updatedAtMs?: number;
|
||||
}
|
||||
|
||||
export interface CoinSellerSalaryRatesDto {
|
||||
regionId: EntityId;
|
||||
tiers: CoinSellerSalaryRateTierDto[];
|
||||
}
|
||||
|
||||
export interface CoinSellerSalaryRateTierPayload {
|
||||
coinPerUsd: number;
|
||||
enabled?: boolean;
|
||||
maxUsd?: string;
|
||||
maxUsdMinor?: number;
|
||||
minUsd?: string;
|
||||
minUsdMinor?: number;
|
||||
sortOrder?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface CoinSellerSalaryRatesPayload {
|
||||
tiers: CoinSellerSalaryRateTierPayload[];
|
||||
}
|
||||
|
||||
export interface AppUserDto {
|
||||
avatar?: string;
|
||||
coin?: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user