This commit is contained in:
zhx 2026-06-11 18:17:16 +08:00
parent 53b74f9dc2
commit 7a6f0db859
10 changed files with 2256 additions and 2025 deletions

View File

@ -270,6 +270,18 @@
"x-permissions": ["first-recharge-reward:view"]
}
},
"/admin/activity/invite-reward/claims": {
"get": {
"operationId": "listInviteActivityRewardClaims",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"x-permission": "invite-activity-reward:view",
"x-permissions": ["invite-activity-reward:view"]
}
},
"/admin/activity/cumulative-recharge-reward/grants": {
"get": {
"operationId": "listCumulativeRechargeRewardGrants",
@ -488,6 +500,28 @@
"x-permissions": ["cumulative-recharge-reward:update"]
}
},
"/admin/activity/invite-reward/config": {
"get": {
"operationId": "getInviteActivityRewardConfig",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"x-permission": "invite-activity-reward:view",
"x-permissions": ["invite-activity-reward:view"]
},
"put": {
"operationId": "updateInviteActivityRewardConfig",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"x-permission": "invite-activity-reward:update",
"x-permissions": ["invite-activity-reward:update"]
}
},
"/admin/activity/room-turnover-reward/config": {
"get": {
"operationId": "getRoomTurnoverRewardConfig",

View File

@ -19,6 +19,8 @@ export interface InviteActivityRewardTierDto {
export interface InviteActivityRewardConfigDto {
appCode?: string;
enabled: boolean;
perInviteInviterRewardCoinAmount: number;
perInviteInviteeRewardCoinAmount: number;
tiers: InviteActivityRewardTierDto[];
updatedByAdminId?: number;
createdAtMs?: number;
@ -27,6 +29,8 @@ export interface InviteActivityRewardConfigDto {
export interface InviteActivityRewardConfigPayload {
enabled: boolean;
per_invite_inviter_reward_coin_amount: number;
per_invite_invitee_reward_coin_amount: number;
tiers: Array<{
tier_id?: number;
reward_type: string;
@ -95,9 +99,7 @@ export function updateInviteActivityRewardConfig(
).then(normalizeConfig);
}
export function listInviteActivityRewardClaims(
query: PageQuery = {},
): Promise<ApiPage<InviteActivityRewardClaimDto>> {
export function listInviteActivityRewardClaims(query: PageQuery = {}): Promise<ApiPage<InviteActivityRewardClaimDto>> {
const endpoint = API_ENDPOINTS.listInviteActivityRewardClaims;
return apiRequest<ApiPage<RawClaim>>(apiEndpointPath(API_OPERATIONS.listInviteActivityRewardClaims), {
method: endpoint.method,
@ -113,6 +115,12 @@ function normalizeConfig(item: RawConfig): InviteActivityRewardConfigDto {
return {
appCode: stringValue(item.appCode ?? item.app_code),
enabled: Boolean(item.enabled),
perInviteInviterRewardCoinAmount: numberValue(
item.perInviteInviterRewardCoinAmount ?? item.per_invite_inviter_reward_coin_amount,
),
perInviteInviteeRewardCoinAmount: numberValue(
item.perInviteInviteeRewardCoinAmount ?? item.per_invite_invitee_reward_coin_amount,
),
tiers: rawTiers.map(normalizeTier),
updatedByAdminId: numberValue(item.updatedByAdminId ?? item.updated_by_admin_id),
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),

View File

@ -75,6 +75,39 @@ export function InviteActivityRewardConfigDrawer({
onChange={(event) => setForm((current) => ({ ...current, enabled: event.target.checked }))}
/>
</section>
<section className="form-drawer__section">
<div className="form-drawer__section-title">每邀请一人奖励</div>
<div className={styles.inlineFields}>
<TextField
disabled={disabled}
inputProps={{ min: 0, step: 1 }}
label="邀请人获得金币"
placeholder="0"
type="number"
value={form.perInviteInviterRewardCoinAmount || ""}
onChange={(event) =>
setForm((current) => ({
...current,
perInviteInviterRewardCoinAmount: event.target.value,
}))
}
/>
<TextField
disabled={disabled}
inputProps={{ min: 0, step: 1 }}
label="被邀请人获得金币"
placeholder="0"
type="number"
value={form.perInviteInviteeRewardCoinAmount || ""}
onChange={(event) =>
setForm((current) => ({
...current,
perInviteInviteeRewardCoinAmount: event.target.value,
}))
}
/>
</div>
</section>
<section className="form-drawer__section">
<div className={styles.drawerSectionTitle}>
<span>奖励档位</span>
@ -152,7 +185,9 @@ export function InviteActivityRewardConfigDrawer({
placeholder="1000"
type="number"
value={tier.rewardCoinAmount}
onChange={(event) => updateTier(index, { rewardCoinAmount: event.target.value })}
onChange={(event) =>
updateTier(index, { rewardCoinAmount: event.target.value })
}
/>
),
},
@ -167,7 +202,9 @@ export function InviteActivityRewardConfigDrawer({
label={`档位 ${index + 1} 状态`}
uncheckedLabel="停用"
onChange={(event) =>
updateTier(index, { status: event.target.checked ? "active" : "inactive" })
updateTier(index, {
status: event.target.checked ? "active" : "inactive",
})
}
/>
),

View File

@ -22,10 +22,18 @@ export function InviteActivityRewardConfigSummary({ canUpdate, config, configLoa
{config?.enabled ? "启用" : "停用"}
</span>
</SummaryItem>
<SummaryItem label="累计充值档位">{thresholdSummary(rechargeTiers, "thresholdCoinAmount", "金币")}</SummaryItem>
<SummaryItem label="累计充值档位">
{thresholdSummary(rechargeTiers, "thresholdCoinAmount", "金币")}
</SummaryItem>
<SummaryItem label="有效人数档位">
{thresholdSummary(validInviteTiers, "thresholdValidInviteCount", "人")}
</SummaryItem>
<SummaryItem label="邀请人即时奖励">
{coinSummary(config?.perInviteInviterRewardCoinAmount)}
</SummaryItem>
<SummaryItem label="被邀请人即时奖励">
{coinSummary(config?.perInviteInviteeRewardCoinAmount)}
</SummaryItem>
<SummaryItem label="奖励">{thresholdSummary(activeTiers, "rewardCoinAmount", "金币")}</SummaryItem>
</div>
<div className={styles.summaryActions}>
@ -69,6 +77,11 @@ function thresholdSummary(tiers, field, unit) {
return min === max ? `${formatNumber(min)} ${unit}` : `${formatNumber(min)} - ${formatNumber(max)} ${unit}`;
}
function coinSummary(value) {
const amount = Number(value || 0);
return amount > 0 ? `${formatNumber(amount)} 金币` : "-";
}
function formatNumber(value) {
return new Intl.NumberFormat("zh-CN").format(Number(value || 0));
}

View File

@ -10,7 +10,12 @@ import { useToast } from "@/shared/ui/ToastProvider.jsx";
const pageSize = 50;
const emptyClaims = { items: [], page: 1, pageSize, total: 0 };
const emptyForm = { enabled: true, tiers: [] };
const emptyForm = {
enabled: true,
perInviteInviterRewardCoinAmount: "",
perInviteInviteeRewardCoinAmount: "",
tiers: [],
};
export function useInviteActivityRewardPage() {
const abilities = useInviteActivityRewardAbilities();
@ -139,6 +144,12 @@ export function useInviteActivityRewardPage() {
function formFromConfig(config) {
return {
enabled: Boolean(config.enabled),
perInviteInviterRewardCoinAmount: config.perInviteInviterRewardCoinAmount
? String(config.perInviteInviterRewardCoinAmount)
: "",
perInviteInviteeRewardCoinAmount: config.perInviteInviteeRewardCoinAmount
? String(config.perInviteInviteeRewardCoinAmount)
: "",
tiers: (config.tiers || []).map((tier) => ({
tierId: tier.tierId || 0,
rewardType: tier.rewardType || "recharge",
@ -154,10 +165,16 @@ function formFromConfig(config) {
}
function payloadFromForm(form) {
const perInviteInviterRewardCoinAmount = Number(form.perInviteInviterRewardCoinAmount || 0);
const perInviteInviteeRewardCoinAmount = Number(form.perInviteInviteeRewardCoinAmount || 0);
if (perInviteInviterRewardCoinAmount < 0 || perInviteInviteeRewardCoinAmount < 0) {
throw new Error("每邀请一人奖励金币不能小于 0");
}
const tiers = (form.tiers || []).map((tier, index) => {
const rewardType = tier.rewardType === "valid_invite" ? "valid_invite" : "recharge";
const thresholdCoinAmount = rewardType === "recharge" ? Number(tier.thresholdCoinAmount || 0) : 0;
const thresholdValidInviteCount = rewardType === "valid_invite" ? Number(tier.thresholdValidInviteCount || 0) : 0;
const thresholdValidInviteCount =
rewardType === "valid_invite" ? Number(tier.thresholdValidInviteCount || 0) : 0;
const rewardCoinAmount = Number(tier.rewardCoinAmount || 0);
if (rewardType === "recharge" && thresholdCoinAmount <= 0) {
throw new Error("累计充值金币门槛必须大于 0");
@ -186,10 +203,20 @@ function payloadFromForm(form) {
sort_order: index,
};
});
if (form.enabled && tiers.filter((tier) => tier.status === "active").length === 0) {
throw new Error("开启后至少需要一个启用档位");
if (
form.enabled &&
tiers.filter((tier) => tier.status === "active").length === 0 &&
perInviteInviterRewardCoinAmount <= 0 &&
perInviteInviteeRewardCoinAmount <= 0
) {
throw new Error("开启后至少需要一个启用档位或每邀请一人奖励");
}
return { enabled: Boolean(form.enabled), tiers };
return {
enabled: Boolean(form.enabled),
per_invite_inviter_reward_coin_amount: perInviteInviterRewardCoinAmount,
per_invite_invitee_reward_coin_amount: perInviteInviteeRewardCoinAmount,
tiers,
};
}
function formatNumber(value) {

View File

@ -92,6 +92,12 @@
font-weight: 700;
}
.inlineFields {
display: grid;
grid-template-columns: repeat(2, minmax(180px, 1fr));
gap: var(--space-3);
}
@media (max-width: 900px) {
.summaryPanel {
align-items: flex-start;
@ -105,3 +111,9 @@
gap: var(--space-3);
}
}
@media (max-width: 640px) {
.inlineFields {
grid-template-columns: 1fr;
}
}

View File

@ -18,6 +18,8 @@ const claimStatusOptions = [
const rewardTypeOptions = [
["recharge", "累计充值"],
["valid_invite", "有效人数"],
["invite_inviter", "邀请人即时"],
["invite_invitee", "被邀请人即时"],
];
const columnsBase = [
@ -202,10 +204,16 @@ function thresholdLabel(claim) {
if (claim.rewardType === "valid_invite") {
return `${formatNumber(claim.thresholdValidInviteCount)}`;
}
if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") {
return claim.tierId ? `关联用户 ${claim.tierId}` : "-";
}
return `${formatNumber(claim.thresholdCoinAmount)} 金币`;
}
function reachedLabel(claim) {
if (claim.rewardType === "invite_inviter" || claim.rewardType === "invite_invitee") {
return "1 人";
}
const unit = claim.rewardType === "valid_invite" ? "人" : "金币";
return `${formatNumber(claim.reachedValue)} ${unit}`;
}

View File

@ -111,8 +111,8 @@ export function RoomRocketConfigEditor({
<TextField
required
disabled={disabled}
inputProps={{ min: 0, step: 1000 }}
label="发射倒计时 ms"
slotProps={{ htmlInput: { min: 0, step: 1000 } }}
type="number"
value={form.launchDelayMs}
onChange={(event) => updateRoot(setForm, "launchDelayMs", event.target.value)}
@ -127,8 +127,8 @@ export function RoomRocketConfigEditor({
<TextField
required
disabled={disabled}
inputProps={{ min: 0, step: 1000 }}
label="广播延迟 ms"
slotProps={{ htmlInput: { min: 0, step: 1000 } }}
type="number"
value={form.broadcastDelayMs}
onChange={(event) => updateRoot(setForm, "broadcastDelayMs", event.target.value)}
@ -175,8 +175,8 @@ export function RoomRocketConfigEditor({
<TextField
required
disabled={disabled}
inputProps={{ min: 1, step: 1 }}
label="燃料阈值"
slotProps={{ htmlInput: { min: 1, step: 1 } }}
type="number"
value={level.fuelThreshold}
onChange={(event) =>
@ -326,8 +326,8 @@ function RewardTable({ disabled, levelIndex, poolKey, resourceGroups, rewards, s
<TextField
required
disabled={disabled}
inputProps={{ min: 1, step: 1 }}
label="权重"
slotProps={{ htmlInput: { min: 1, step: 1 } }}
type="number"
value={reward.weight}
onChange={(event) =>
@ -338,8 +338,8 @@ function RewardTable({ disabled, levelIndex, poolKey, resourceGroups, rewards, s
<TextField
required
disabled={disabled}
inputProps={{ min: 1, step: 1 }}
label="数量"
slotProps={{ htmlInput: { min: 1, step: 1 } }}
type="number"
value={reward.quantity}
onChange={(event) =>
@ -428,8 +428,8 @@ function FuelRuleEditor({ disabled, form, setForm }) {
/>
<TextField
disabled={disabled}
inputProps={{ min: 0, step: 1 }}
label="倍率 ppm"
slotProps={{ htmlInput: { min: 0, step: 1 } }}
type="number"
value={rule.multiplierPpm}
onChange={(event) =>
@ -438,8 +438,8 @@ function FuelRuleEditor({ disabled, form, setForm }) {
/>
<TextField
disabled={disabled}
inputProps={{ min: 0, step: 1 }}
label="固定燃料"
slotProps={{ htmlInput: { min: 0, step: 1 } }}
type="number"
value={rule.fixedFuel}
onChange={(event) => updateFuelRule(setForm, index, { fixedFuel: event.target.value })}

File diff suppressed because it is too large Load Diff

View File

@ -244,6 +244,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/activity/invite-reward/claims": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listInviteActivityRewardClaims"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/activity/cumulative-recharge-reward/grants": {
parameters: {
query?: never;
@ -420,6 +436,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/activity/invite-reward/config": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getInviteActivityRewardConfig"];
put: operations["updateInviteActivityRewardConfig"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/activity/room-turnover-reward/config": {
parameters: {
query?: never;
@ -3554,6 +3586,18 @@ export interface operations {
200: components["responses"]["EmptyResponse"];
};
};
listInviteActivityRewardClaims: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
listCumulativeRechargeRewardGrants: {
parameters: {
query?: never;
@ -3741,6 +3785,30 @@ export interface operations {
200: components["responses"]["EmptyResponse"];
};
};
getInviteActivityRewardConfig: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
updateInviteActivityRewardConfig: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
getRoomTurnoverRewardConfig: {
parameters: {
query?: never;