阿斯兰数据

This commit is contained in:
zhx 2026-07-03 15:53:40 +08:00
parent 9dd69b2374
commit 9fe67dc8d1
8 changed files with 544 additions and 85 deletions

View File

@ -2881,6 +2881,62 @@
"x-permissions": ["payment-bill:view"]
}
},
"/admin/payment/recharge-bills/export": {
"get": {
"operationId": "exportRechargeBills",
"parameters": [
{
"in": "query",
"name": "keyword",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "recharge_type",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "status",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "region_id",
"schema": {
"type": "integer"
}
},
{
"in": "query",
"name": "start_at_ms",
"schema": {
"type": "integer"
}
},
{
"in": "query",
"name": "end_at_ms",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"x-permission": "payment-bill:view",
"x-permissions": ["payment-bill:view"]
}
},
"/admin/payment/recharge-bills/google-paid/refresh": {
"post": {
"operationId": "refreshGoogleRechargePaid",

View File

@ -6,6 +6,7 @@ import {
approveFinanceApplication,
approveFinanceWithdrawalApplication,
createFinanceApplication,
exportFinanceRechargeBills,
fetchFinanceApplicationOptions,
fetchFinanceRechargeApps,
fetchFinanceRechargeSummary,
@ -29,6 +30,7 @@ import { FinanceShell } from "./components/FinanceShell.jsx";
import { FinanceWithdrawalApplicationList } from "./components/FinanceWithdrawalApplicationList.jsx";
import { DEFAULT_APPLICATION_FORM } from "./constants.js";
import { EMPTY_RECHARGE_SUMMARY, buildApplicationPayload, validateApplicationPayload } from "./format.js";
import { downloadCsv } from "@/shared/api/download";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
const emptyOptions = { apps: [], operations: [], walletIdentities: [] };
@ -47,7 +49,8 @@ export function FinanceApp() {
const [applicationsState, setApplicationsState] = useState({ data: { items: [], page: 1, pageSize, total: 0 }, error: "", loading: false });
const [withdrawalsState, setWithdrawalsState] = useState({ data: { items: [], page: 1, pageSize, total: 0 }, error: "", loading: false });
const [activeView, setActiveView] = useState("create");
const [rechargeDetailFilters, setRechargeDetailFilters] = useState({ appCode: "", keyword: "", page: 1, regionId: "", timeRange: { endMs: "", startMs: "" } });
const [rechargeDetailFilters, setRechargeDetailFilters] = useState({ appCode: "", keyword: "", page: 1, rechargeType: "", regionId: "", timeRange: { endMs: "", startMs: "" } });
const [rechargeExporting, setRechargeExporting] = useState(false);
const [myFilters, setMyFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "" });
const [filters, setFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "pending" });
const [withdrawalFilters, setWithdrawalFilters] = useState({ appCode: "", keyword: "", page: 1 });
@ -96,6 +99,7 @@ export function FinanceApp() {
keyword: rechargeDetailFilters.keyword,
page: rechargeDetailFilters.page,
page_size: pageSize,
recharge_type: rechargeDetailFilters.rechargeType,
region_id: rechargeDetailFilters.appCode ? rechargeDetailFilters.regionId : "",
start_at_ms: rechargeDetailFilters.timeRange.startMs,
stat_tz: "Asia/Shanghai",
@ -244,6 +248,27 @@ export function FinanceApp() {
[googlePaidRefreshing, loadRechargeDetails, rechargeAppCode, showToast]
);
const exportRechargeBills = useCallback(async () => {
if (!rechargeAppCode || rechargeExporting) {
return;
}
setRechargeExporting(true);
try {
const timestamp = new Date()
.toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" })
.replaceAll(/[: ]/g, "-");
await downloadCsv(
() => exportFinanceRechargeBills(rechargeAppCode, rechargeDetailQuery),
`充值明细-${rechargeAppCode}-${timestamp}.csv`
);
showToast("充值明细已导出", "success");
} catch (err) {
showToast(err.message || "导出充值明细失败", "error");
} finally {
setRechargeExporting(false);
}
}, [rechargeAppCode, rechargeDetailQuery, rechargeExporting, showToast]);
const loadApplications = useCallback(async () => {
if (!session?.canAuditApplication) {
return;
@ -424,6 +449,7 @@ export function FinanceApp() {
<FinanceRechargeDetailList
bills={rechargeDetailsState.data}
error={rechargeDetailsState.error}
exporting={rechargeExporting}
filters={rechargeDetailFilters}
googlePaidRefreshing={googlePaidRefreshing}
loading={rechargeDetailsState.loading}
@ -431,6 +457,7 @@ export function FinanceApp() {
regions={rechargeRegionsState.items}
summary={rechargeSummaryState.data}
summaryLoading={rechargeSummaryState.loading}
onExport={exportRechargeBills}
onFiltersChange={updateRechargeDetailFilters}
onRefreshGooglePaid={refreshGooglePaid}
onReload={() => {

View File

@ -137,6 +137,16 @@ export async function listFinanceRechargeRegions(appCode) {
return normalizeRechargeRegions(data);
}
export async function exportFinanceRechargeBills(appCode, query = {}) {
const endpoint = API_ENDPOINTS.exportRechargeBills;
return apiRequest(apiEndpointPath(API_OPERATIONS.exportRechargeBills), {
headers: { [appCodeHeader]: appCode },
method: endpoint.method,
query: cleanRechargeBillQuery(query),
raw: true
});
}
export async function refreshFinanceGoogleRechargePaid(appCode, transactionIds) {
const endpoint = API_ENDPOINTS.refreshGoogleRechargePaid;
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.refreshGoogleRechargePaid), {

View File

@ -1,4 +1,5 @@
import CloudSyncOutlined from "@mui/icons-material/CloudSyncOutlined";
import FileDownloadOutlined from "@mui/icons-material/FileDownloadOutlined";
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
import Button from "@mui/material/Button";
import MenuItem from "@mui/material/MenuItem";
@ -18,15 +19,32 @@ import {
formatMicroMoney,
formatTime,
formatUsdMinor,
rechargeSourceTone,
rechargeTypeLabel,
} from "../format.js";
const SOURCE_FILTERS = [
{ label: "全部来源", value: "" },
{ label: "谷歌充值", value: "google_play_recharge" },
{ label: "三方充值", value: "third_party" },
{ label: "币商充值", value: "coin_seller" },
];
const KPI_CARDS = [
{ filter: "", key: "total", label: "充值总和", tone: "total" },
{ filter: "google_play_recharge", key: "googlePlay", label: "谷歌充值", tone: "google" },
{ filter: "third_party", key: "thirdParty", label: "三方充值", tone: "thirdparty" },
{ filter: "coin_seller", key: "coinSeller", label: "币商充值", tone: "coinseller" },
];
export function FinanceRechargeDetailList({
bills,
error,
exporting,
filters,
googlePaidRefreshing,
loading,
onExport,
onFiltersChange,
onRefreshGooglePaid,
onReload,
@ -50,9 +68,16 @@ export function FinanceRechargeDetailList({
return (
<section className="finance-panel finance-list">
<RechargeSummaryCards loading={summaryLoading} showCoins={showCoinColumns} summary={summary} />
<div className="finance-toolbar finance-toolbar--recharge-details">
<RechargeKpiBand
activeFilter={filters.rechargeType || ""}
loading={summaryLoading}
showCoins={showCoinColumns}
summary={summary}
onSelect={(value) => onFiltersChange({ rechargeType: value })}
/>
<div className="finance-filterbar">
<TextField
className="finance-filterbar__field"
label="APP"
select
value={filters.appCode}
@ -66,10 +91,11 @@ export function FinanceRechargeDetailList({
))}
</TextField>
<TextField
className="finance-filterbar__field"
disabled={!filters.appCode}
helperText={filters.appCode ? "" : "先选择 APP"}
label="区域"
select
title={filters.appCode ? "" : "先选择 APP"}
value={filters.regionId}
onChange={(event) => onFiltersChange({ regionId: event.target.value })}
>
@ -80,36 +106,90 @@ export function FinanceRechargeDetailList({
</MenuItem>
))}
</TextField>
<TextField
className="finance-filterbar__field"
label="充值来源"
select
value={filters.rechargeType || ""}
onChange={(event) => onFiltersChange({ rechargeType: event.target.value })}
>
{SOURCE_FILTERS.map((source) => (
<MenuItem key={source.value} value={source.value}>
{source.label}
</MenuItem>
))}
</TextField>
<TimeRangeFilter
label="充值时间 · 中国时区"
value={filters.timeRange}
onChange={(timeRange) => onFiltersChange({ timeRange })}
/>
<TextField
className="finance-filterbar__field finance-filterbar__field--search"
label="订单搜索"
placeholder="交易号 / 三方单号 / 凭证号"
value={filters.keyword}
onChange={(event) => onFiltersChange({ keyword: event.target.value })}
/>
<label className="finance-switch-field">
<AdminSwitch
checked={showCoinColumns}
label="金币显示"
onChange={(event) => setShowCoinColumns(event.target.checked)}
/>
<span>金币显示</span>
</label>
<Button
disabled={googlePaidRefreshing || !filters.appCode || !pendingGoogleTxIds.length}
startIcon={<CloudSyncOutlined fontSize="small" />}
title={filters.appCode ? "查询本页谷歌订单的用户实付币种和金额" : "先选择 APP 后可查询"}
variant="outlined"
onClick={() => onRefreshGooglePaid(pendingGoogleTxIds)}
>
{googlePaidRefreshing ? "查询中…" : "查询谷歌实付"}
</Button>
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
刷新列表
</Button>
</div>
<div className="finance-actionbar">
<div className="finance-actionbar__presets">
{timePresets().map((preset) => (
<button
className={[
"finance-chip",
isPresetActive(filters.timeRange, preset) ? "finance-chip--active" : "",
]
.filter(Boolean)
.join(" ")}
key={preset.label}
type="button"
onClick={() => onFiltersChange({ timeRange: preset.range })}
>
{preset.label}
</button>
))}
</div>
<div className="finance-actionbar__actions">
<label className="finance-switch-field">
<AdminSwitch
checked={showCoinColumns}
label="金币显示"
onChange={(event) => setShowCoinColumns(event.target.checked)}
/>
<span>金币显示</span>
</label>
<Button
disabled={googlePaidRefreshing || !filters.appCode || !pendingGoogleTxIds.length}
size="small"
startIcon={<CloudSyncOutlined fontSize="small" />}
title={filters.appCode ? "查询本页谷歌订单的用户实付币种和金额" : "先选择 APP 后可查询"}
variant="outlined"
onClick={() => onRefreshGooglePaid(pendingGoogleTxIds)}
>
{googlePaidRefreshing ? "查询中…" : "查询谷歌实付"}
</Button>
<Button
disabled={exporting || !filters.appCode}
size="small"
startIcon={<FileDownloadOutlined fontSize="small" />}
title={
filters.appCode ? "按当前筛选导出全部明细CSV可用 Excel 打开)" : "先选择 APP 后可导出"
}
variant="contained"
onClick={onExport}
>
{exporting ? "导出中…" : "导出报表"}
</Button>
<Button
size="small"
startIcon={<RefreshOutlined fontSize="small" />}
variant="outlined"
onClick={onReload}
>
刷新
</Button>
</div>
</div>
{error ? <div className="finance-error">{error}</div> : null}
{loading && !items.length ? <div className="finance-skeleton" /> : null}
@ -128,45 +208,57 @@ export function FinanceRechargeDetailList({
>
<TableHead>
<TableRow>
<TableCell className="finance-time-cell">充值时间</TableCell>
<TableCell>APP</TableCell>
{showCoinColumns ? <TableCell align="right">充值金币</TableCell> : null}
<TableCell>充值来源</TableCell>
<TableCell>充值订单号</TableCell>
{showCoinColumns ? <TableCell align="right">充值金币</TableCell> : null}
{showCoinColumns ? <TableCell>金币换算</TableCell> : null}
<TableCell>账单金额</TableCell>
<TableCell>用户实付</TableCell>
<TableCell>三方税率扣款</TableCell>
<TableCell className="finance-time-cell">充值时间</TableCell>
<TableCell align="right">账单金额</TableCell>
<TableCell align="right">用户实付</TableCell>
<TableCell align="right">三方税率扣款</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.length ? (
items.map((item) => (
<TableRow key={item.id || item.transactionId}>
<TableRow hover key={item.id || item.transactionId}>
<TableCell className="finance-time-cell">
{formatTime(item.createdAtMs)}
</TableCell>
<TableCell>{appName(item, options.apps)}</TableCell>
{showCoinColumns ? (
<TableCell align="right">{coinText(item.coinAmount)}</TableCell>
) : null}
<TableCell>{rechargeTypeLabel(item.rechargeType)}</TableCell>
<TableCell>
<SourceBadge rechargeType={item.rechargeType} />
</TableCell>
<TableCell className="finance-order-cell">
<Stacked
primary={item.transactionId || "-"}
secondary={secondaryOrderNo(item)}
/>
</TableCell>
{showCoinColumns ? (
<TableCell align="right">
<AmountText value={item.coinAmount}>
{coinText(item.coinAmount)}
</AmountText>
</TableCell>
) : null}
{showCoinColumns ? <TableCell>{exchangeText(item)}</TableCell> : null}
<TableCell>{billAmountText(item)}</TableCell>
<TableCell>{userPaidText(item)}</TableCell>
<TableCell>{taxDeductionText(item)}</TableCell>
<TableCell className="finance-time-cell">
{formatTime(item.createdAtMs)}
<TableCell align="right">
<AmountText value={item.usdMinorAmount}>{billAmountText(item)}</AmountText>
</TableCell>
<TableCell align="right">
<AmountText value={item.userPaidAmountMicro}>
{userPaidText(item)}
</AmountText>
</TableCell>
<TableCell align="right">{taxDeductionText(item)}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell align="center" colSpan={visibleColumnCount}>
当前无数据
当前筛选条件下没有账单
</TableCell>
</TableRow>
)}
@ -174,63 +266,117 @@ export function FinanceRechargeDetailList({
</Table>
</TableContainer>
) : null}
{total > pageSize ? (
<div className="finance-pagination">
<div className="finance-pagination">
<span className="finance-pagination__meta">
{formatAmount(total)} · {page} / {Math.max(1, Math.ceil(total / pageSize))}
</span>
<div className="finance-pagination__buttons">
<Button
disabled={page <= 1 || loading}
size="small"
variant="outlined"
onClick={() => onFiltersChange({ page: page - 1 })}
>
上一页
</Button>
<span>
{page} / {total}
</span>
<Button
disabled={!hasNextPage || loading}
size="small"
variant="outlined"
onClick={() => onFiltersChange({ page: page + 1 })}
>
下一页
</Button>
</div>
) : null}
</div>
</section>
);
}
function RechargeSummaryCards({ loading, showCoins, summary }) {
function RechargeKpiBand({ activeFilter, loading, onSelect, showCoins, summary }) {
const data = summary || EMPTY_RECHARGE_SUMMARY;
const cards = [
{ bucket: data.total, key: "total", label: "充值总和" },
{ bucket: data.thirdParty, key: "thirdParty", label: "三方充值" },
{ bucket: data.googlePlay, key: "googlePlay", label: "谷歌充值" },
];
const totalUsd = Number(data.total?.usdMinorAmount) || 0;
return (
<div className="finance-summary-grid">
{cards.map(({ bucket, key, label }) => (
<div className="finance-summary-card" key={key}>
<span className="finance-summary-card__label">{label}</span>
<strong className="finance-summary-card__value">
{loading ? "…" : formatUsdMinor(bucket.usdMinorAmount, "USD")}
</strong>
<small>
{loading
? "统计中"
: summaryMetaText(bucket, showCoins)}
</small>
</div>
))}
<div className="finance-kpi-band">
{KPI_CARDS.map(({ filter, key, label, tone }) => {
const bucket = data[key] || { billCount: 0, coinAmount: 0, usdMinorAmount: 0 };
const active = activeFilter === filter;
return (
<button
className={["finance-kpi", `finance-kpi--${tone}`, active ? "finance-kpi--active" : ""]
.filter(Boolean)
.join(" ")}
key={key}
title={filter ? `点击只看${label}` : "点击查看全部来源"}
type="button"
onClick={() => onSelect(active && filter ? "" : filter)}
>
<span className="finance-kpi__label">{label}</span>
<strong className="finance-kpi__value">
{loading ? "…" : formatUsdMinor(bucket.usdMinorAmount, "USD")}
</strong>
<small className="finance-kpi__meta">
{loading ? "统计中" : kpiMetaText(bucket, key, totalUsd, showCoins)}
</small>
</button>
);
})}
</div>
);
}
function summaryMetaText(bucket, showCoins) {
const billCount = `${formatAmount(bucket.billCount)}`;
if (!showCoins) {
return billCount;
function kpiMetaText(bucket, key, totalUsd, showCoins) {
const parts = [`${formatAmount(bucket.billCount)}`];
if (key !== "total" && totalUsd > 0) {
const share = ((Number(bucket.usdMinorAmount) || 0) / totalUsd) * 100;
parts.push(`占比 ${share.toLocaleString("zh-CN", { maximumFractionDigits: 1 })}%`);
}
return `${billCount} · ${formatAmount(bucket.coinAmount)} 金币`;
if (showCoins) {
parts.push(`${formatAmount(bucket.coinAmount)} 金币`);
}
return parts.join(" · ");
}
function SourceBadge({ rechargeType }) {
return (
<span className={`finance-badge finance-badge--${rechargeSourceTone(rechargeType)}`}>
{rechargeTypeLabel(rechargeType)}
</span>
);
}
function AmountText({ children, value }) {
const negative = hasNumber(value) && Number(value) < 0;
return <span className={negative ? "finance-amount finance-amount--negative" : "finance-amount"}>{children}</span>;
}
const DAY_MS = 86_400_000;
const CHINA_OFFSET_MS = 8 * 3_600_000;
function chinaDayStart(timestamp) {
return Math.floor((timestamp + CHINA_OFFSET_MS) / DAY_MS) * DAY_MS - CHINA_OFFSET_MS;
}
function timePresets() {
const now = Date.now();
const todayStart = chinaDayStart(now);
const chinaNow = new Date(now + CHINA_OFFSET_MS);
const monthStart = Date.UTC(chinaNow.getUTCFullYear(), chinaNow.getUTCMonth(), 1) - CHINA_OFFSET_MS;
return [
{ label: "不限", range: { endMs: "", startMs: "" } },
{ label: "今天", range: { endMs: todayStart + DAY_MS, startMs: todayStart } },
{ label: "昨天", range: { endMs: todayStart, startMs: todayStart - DAY_MS } },
{ label: "近7天", range: { endMs: todayStart + DAY_MS, startMs: todayStart - 6 * DAY_MS } },
{ label: "本月", range: { endMs: todayStart + DAY_MS, startMs: monthStart } },
];
}
function isPresetActive(timeRange, preset) {
const current = timeRange || { endMs: "", startMs: "" };
return (
String(current.startMs || "") === String(preset.range.startMs || "") &&
String(current.endMs || "") === String(preset.range.endMs || "")
);
}
function Stacked({ primary, secondary }) {
@ -268,7 +414,7 @@ function billAmountText(item) {
}
const amount = hasNumber(item.billAmountMinor)
? item.billAmountMinor
: hasNumber(item.providerAmountMinor)
: hasPositiveNumber(item.providerAmountMinor)
? item.providerAmountMinor
: item.usdMinorAmount;
return formatUsdMinor(amount, item.currencyCode || "USD");
@ -278,7 +424,7 @@ function userPaidText(item) {
if (item.userPaidText) {
return item.userPaidText;
}
if (hasPositiveNumber(item.userPaidAmountMicro) && item.userPaidCurrencyCode) {
if (hasNonZeroNumber(item.userPaidAmountMicro) && item.userPaidCurrencyCode) {
return formatMicroMoney(item.userPaidAmountMicro, item.userPaidCurrencyCode);
}
if (isGoogleBill(item) && !item.paidSyncedAtMs) {
@ -342,3 +488,7 @@ function hasNumber(value) {
function hasPositiveNumber(value) {
return hasNumber(value) && Number(value) > 0;
}
function hasNonZeroNumber(value) {
return hasNumber(value) && Number(value) !== 0;
}

View File

@ -197,6 +197,7 @@ export function normalizeRechargeBillPage(data = {}) {
const EMPTY_RECHARGE_SUMMARY_BUCKET = { billCount: 0, coinAmount: 0, usdMinorAmount: 0 };
export const EMPTY_RECHARGE_SUMMARY = {
coinSeller: EMPTY_RECHARGE_SUMMARY_BUCKET,
googlePlay: EMPTY_RECHARGE_SUMMARY_BUCKET,
thirdParty: EMPTY_RECHARGE_SUMMARY_BUCKET,
total: EMPTY_RECHARGE_SUMMARY_BUCKET,
@ -212,6 +213,7 @@ function normalizeRechargeSummaryBucket(bucket = {}) {
export function normalizeRechargeSummary(data = {}) {
return {
coinSeller: normalizeRechargeSummaryBucket(data.coinSeller ?? data.coin_seller),
googlePlay: normalizeRechargeSummaryBucket(data.googlePlay ?? data.google_play),
thirdParty: normalizeRechargeSummaryBucket(data.thirdParty ?? data.third_party),
total: normalizeRechargeSummaryBucket(data.total),
@ -221,7 +223,7 @@ export function normalizeRechargeSummary(data = {}) {
export function mergeRechargeSummaries(summaries = []) {
const merged = normalizeRechargeSummary({});
for (const summary of summaries) {
for (const key of ["googlePlay", "thirdParty", "total"]) {
for (const key of ["coinSeller", "googlePlay", "thirdParty", "total"]) {
merged[key] = {
billCount: merged[key].billCount + summary[key].billCount,
coinAmount: merged[key].coinAmount + summary[key].coinAmount,
@ -257,6 +259,12 @@ export function rechargeTypeLabel(value) {
if (value === "coin_seller_transfer") {
return "币商充值";
}
if (value === "coin_seller_stock_purchase") {
return "币商进货";
}
if (value === "coin_seller_stock_deduction") {
return "币商冲回";
}
if (value === "google_play_recharge") {
return "谷歌充值";
}
@ -284,6 +292,21 @@ export function rechargeTypeLabel(value) {
return value || "-";
}
// rechargeSourceTone 决定来源徽章与 KPI 卡片的配色分组google/thirdparty/coinseller。
export function rechargeSourceTone(rechargeType) {
if (rechargeType === "google_play_recharge") {
return "google";
}
if (
rechargeType === "coin_seller_stock_purchase" ||
rechargeType === "coin_seller_stock_deduction" ||
rechargeType === "coin_seller_transfer"
) {
return "coinseller";
}
return "thirdparty";
}
export function statusLabel(value) {
return APPLICATION_STATUS.find(([status]) => status === value)?.[1] || value || "-";
}

View File

@ -177,35 +177,182 @@ a {
white-space: nowrap;
}
.finance-summary-grid {
.finance-kpi-band {
display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr));
grid-template-columns: repeat(4, minmax(180px, 1fr));
gap: var(--space-4);
}
.finance-summary-card {
.finance-kpi {
display: grid;
gap: 4px;
border: 1px solid var(--border);
border-radius: var(--radius-control);
border-radius: var(--radius-card);
border-left: 4px solid var(--border);
background: var(--bg-card-strong);
padding: var(--space-4);
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease,
transform 0.15s ease;
}
.finance-summary-card__label {
.finance-kpi:hover {
box-shadow: var(--shadow-soft);
transform: translateY(-1px);
}
.finance-kpi--total {
border-left-color: var(--text-secondary);
}
.finance-kpi--google {
border-left-color: #34a853;
}
.finance-kpi--thirdparty {
border-left-color: #4285f4;
}
.finance-kpi--coinseller {
border-left-color: #f9ab00;
}
.finance-kpi--active {
border-color: var(--primary, #4285f4);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary, #4285f4) 24%, transparent);
}
.finance-kpi__label {
color: var(--text-secondary);
font-size: 13px;
font-weight: 650;
}
.finance-summary-card__value {
font-size: 22px;
.finance-kpi__value {
font-size: 24px;
line-height: 1.2;
font-variant-numeric: tabular-nums;
}
.finance-summary-card small {
.finance-kpi__meta {
color: var(--text-tertiary);
}
.finance-filterbar {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
}
.finance-filterbar__field {
min-width: 150px;
}
.finance-filterbar__field--search {
min-width: 240px;
flex: 1;
}
.finance-actionbar {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
justify-content: space-between;
}
.finance-actionbar__presets {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.finance-actionbar__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
}
.finance-chip {
border: 1px solid var(--border);
border-radius: 999px;
background: transparent;
color: var(--text-secondary);
padding: 4px 12px;
font: inherit;
font-size: 13px;
cursor: pointer;
transition:
border-color 0.15s ease,
background 0.15s ease,
color 0.15s ease;
}
.finance-chip:hover {
border-color: var(--primary, #4285f4);
color: var(--primary, #4285f4);
}
.finance-chip--active {
border-color: var(--primary, #4285f4);
background: color-mix(in srgb, var(--primary, #4285f4) 12%, transparent);
color: var(--primary, #4285f4);
font-weight: 650;
}
.finance-badge {
display: inline-block;
border-radius: 999px;
padding: 2px 10px;
font-size: 12px;
font-weight: 650;
white-space: nowrap;
}
.finance-badge--google {
background: color-mix(in srgb, #34a853 14%, transparent);
color: #1e7e34;
}
.finance-badge--thirdparty {
background: color-mix(in srgb, #4285f4 14%, transparent);
color: #2b6cd4;
}
.finance-badge--coinseller {
background: color-mix(in srgb, #f9ab00 18%, transparent);
color: #b07503;
}
.finance-amount {
font-variant-numeric: tabular-nums;
}
.finance-amount--negative {
color: var(--danger);
}
.finance-order-cell {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
}
.finance-pagination__meta {
color: var(--text-secondary);
}
.finance-pagination__buttons {
display: flex;
gap: var(--space-2);
}
.finance-error {
border: 1px solid color-mix(in srgb, var(--danger) 24%, transparent);
border-radius: var(--radius-control);
@ -312,7 +459,7 @@ a {
.finance-pagination {
display: flex;
align-items: center;
justify-content: flex-end;
justify-content: space-between;
gap: var(--space-3);
color: var(--text-secondary);
}
@ -445,11 +592,14 @@ a {
.finance-form-grid,
.finance-evidence-grid,
.finance-toolbar,
.finance-summary-grid {
.finance-toolbar {
grid-template-columns: 1fr;
}
.finance-kpi-band {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.finance-create-dialog-paper {
width: calc(100vw - 32px);
max-width: calc(100vw - 32px) !important;

View File

@ -106,6 +106,7 @@ export const API_OPERATIONS = {
expireRoomRpsChallenge: "expireRoomRpsChallenge",
exportLoginLogs: "exportLoginLogs",
exportOperationLogs: "exportOperationLogs",
exportRechargeBills: "exportRechargeBills",
exportUsers: "exportUsers",
generateDiceRobots: "generateDiceRobots",
generatePrettyIds: "generatePrettyIds",
@ -984,6 +985,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "log:export",
permissions: ["log:export"]
},
exportRechargeBills: {
method: "GET",
operationId: API_OPERATIONS.exportRechargeBills,
path: "/v1/admin/payment/recharge-bills/export",
permission: "payment-bill:view",
permissions: ["payment-bill:view"]
},
exportUsers: {
method: "GET",
operationId: API_OPERATIONS.exportUsers,

View File

@ -1892,6 +1892,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/payment/recharge-bills/export": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["exportRechargeBills"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/payment/recharge-bills/google-paid/refresh": {
parameters: {
query?: never;
@ -6818,6 +6834,25 @@ export interface operations {
200: components["responses"]["EmptyResponse"];
};
};
exportRechargeBills: {
parameters: {
query?: {
keyword?: string;
recharge_type?: string;
status?: string;
region_id?: number;
start_at_ms?: number;
end_at_ms?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
refreshGoogleRechargePaid: {
parameters: {
query?: never;