2026-07-10 14:31:56 +08:00

170 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect, useMemo, useState } from "react";
import { drawStatusOptions } from "@/features/lucky-gift/constants.js";
import { AdminFilterResetButton, AdminFilterSelect, AdminListToolbar, AdminSearchBox } from "@/shared/ui/AdminListLayout.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { TimeText } from "@/shared/ui/TimeText.jsx";
import { fetchLuckyGiftDraws } from "../api.js";
import { formatMultiplierFromPPM, formatNumber } from "../format.js";
import { OpsStatusBadge } from "./OpsStatusBadge.jsx";
const PAGE_SIZE = 20;
const initialFilters = { poolId: "", status: "", userId: "" };
export function DrawsView({ apps, defaultAppCode, poolIDsByApp }) {
// 抽奖流水量大且服务端分页,必须选定应用后按需拉取,不能像配置那样一次拉全。
const [appCode, setAppCode] = useState(defaultAppCode);
const [filters, setFilters] = useState(initialFilters);
const [page, setPage] = useState(1);
const [state, setState] = useState({ error: "", items: [], loading: false, total: 0 });
useEffect(() => {
// 首屏 dashboard 加载完成前 defaultAppCode 为空,就绪后自动选中第一个应用。
setAppCode((current) => current || defaultAppCode);
}, [defaultAppCode]);
useEffect(() => {
if (!appCode) {
return undefined;
}
let cancelled = false;
setState((current) => ({ ...current, error: "", loading: true }));
fetchLuckyGiftDraws({ appCode, page, pageSize: PAGE_SIZE, poolId: filters.poolId, status: filters.status, userId: filters.userId })
.then((result) => {
if (cancelled) {
return;
}
setState((current) => ({
error: "",
// 滚动加载:翻页时向已加载列表追加;筛选变化会把 page 重置为 1从头替换。
items: page > 1 ? [...current.items, ...result.items] : result.items,
loading: false,
total: result.total
}));
})
.catch((error) => {
if (cancelled) {
return;
}
setState((current) => ({ ...current, error: error.message || "获取抽奖记录失败", loading: false }));
});
return () => {
cancelled = true;
};
}, [appCode, filters, page]);
const changeAppCode = useCallback((nextAppCode) => {
setAppCode(nextAppCode);
// 奖池归属于应用,切换应用后旧奖池筛选一定失效,一并重置。
setFilters(initialFilters);
setPage(1);
}, []);
const changeFilter = useCallback((key, value) => {
setFilters((current) => ({ ...current, [key]: value }));
setPage(1);
}, []);
const resetFilters = useCallback(() => {
setFilters(initialFilters);
setPage(1);
}, []);
const appOptions = useMemo(() => apps.map((item) => [item.app_code ?? item.appCode, item.app_code ?? item.appCode]), [apps]);
const poolOptions = useMemo(() => (poolIDsByApp.get(appCode) || []).map((poolID) => [poolID, poolID]), [appCode, poolIDsByApp]);
const hasActiveFilters = Boolean(filters.poolId || filters.status || filters.userId);
return (
<div className="ops-view">
<AdminListToolbar
filters={
<>
<AdminFilterSelect label="应用" options={appOptions} value={appCode || ""} onChange={changeAppCode} />
<AdminFilterSelect
label="奖池"
options={[["", "全部奖池"], ...poolOptions]}
value={filters.poolId}
onChange={(value) => changeFilter("poolId", value)}
/>
<AdminFilterSelect
label="发放状态"
options={[["", "全部状态"], ...drawStatusOptions]}
value={filters.status}
onChange={(value) => changeFilter("status", value)}
/>
<AdminSearchBox
label="用户"
placeholder="用户 ID / 外部用户 ID"
value={filters.userId}
onChange={(value) => changeFilter("userId", value)}
/>
<AdminFilterResetButton disabled={!hasActiveFilters} onClick={resetFilters} />
</>
}
/>
{state.error ? <div className="ops-alert" role="alert">{state.error}</div> : null}
<DataTable
columns={drawColumns}
emptyLabel={state.loading ? "加载中..." : "暂无抽奖记录"}
items={state.items}
minWidth="1080px"
pagination={{ onPageChange: setPage, page, total: state.total }}
rowKey={(item) => item.draw_id ?? item.drawId}
title="抽奖记录"
total={state.total}
/>
</div>
);
}
const drawColumns = [
{
key: "created_at_ms",
label: "抽奖时间",
render: (item) => <TimeText value={item.created_at_ms ?? item.createdAtMs} />,
width: "minmax(150px, 1fr)"
},
{ key: "app_code", label: "应用", render: (item) => item.app_code ?? item.appCode, width: "minmax(80px, 0.6fr)" },
{
key: "user",
label: "用户",
// 外部接入应用只有 external_user_id内部应用只有数字 user_id两者互斥取有值的一侧。
render: (item) => item.external_user_id || item.externalUserId || item.user_id || item.userId || "-",
width: "minmax(120px, 1fr)"
},
{ key: "pool_id", label: "奖池", render: (item) => item.pool_id ?? item.poolId, width: "minmax(100px, 0.8fr)" },
{ key: "gift_id", label: "礼物", render: (item) => item.gift_id ?? item.giftId, width: "minmax(100px, 0.8fr)" },
{
key: "multiplier_ppm",
label: "命中倍率",
render: (item) => formatMultiplierFromPPM(item.multiplier_ppm ?? item.multiplierPpm),
width: "minmax(88px, 0.6fr)"
},
{ key: "effective_reward_coins", label: "中奖金币", render: (item) => formatNumber(item.effective_reward_coins ?? item.effectiveRewardCoins) },
{
key: "reward_status",
label: "发放状态",
render: (item) => renderRewardStatus(item.reward_status ?? item.rewardStatus),
width: "minmax(124px, 0.8fr)"
},
{
key: "rule_version",
label: "规则版本",
render: (item) => `v${item.rule_version ?? item.ruleVersion ?? "-"}`,
width: "minmax(80px, 0.5fr)"
}
];
function renderRewardStatus(status) {
if (status === "granted") {
return <OpsStatusBadge label="已发放" tone="succeeded" />;
}
if (status === "failed") {
return <OpsStatusBadge label="发放失败" tone="danger" />;
}
if (status === "pending") {
return <OpsStatusBadge label="发放中" tone="warning" />;
}
return <OpsStatusBadge label={status || "-"} />;
}