Files
video-gen/video-gen-admin/src/pages/AdminCreditRecords.tsx
T
2026-07-10 17:09:14 +08:00

384 lines
34 KiB
TypeScript
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 React, { useEffect, useMemo, useState } from 'react';
import {
Button, Card, DatePicker, Descriptions, Drawer, Input, message, Select, Space, Table, Tag, Typography,
} from 'antd';
import {
ArrowDownOutlined, ArrowUpOutlined, DownloadOutlined, EyeOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { exportStyledExcel, type StyledExcelColumn } from '../utils/excelExport';
import { getCreditRecords, getTeamOptions } from '../api';
import type { AdminCreditRecord, AdminCreditRecordQueryParams, AdminCreditRecordSummary, AdminTeamOption } from '../types';
import { formatDate } from '../utils/formatDate';
const TEAM_UNASSIGNED_VALUE = '__none__';
const DEFAULT_SUMMARY: AdminCreditRecordSummary = {
totalRecharge: 0, totalConsume: 0, totalRefund: 0, transactionCount: 0,
generationCount: 0, generationAttemptCount: 0, imageGenerationCount: 0, videoGenerationCount: 0,
imageConsume: 0, videoConsume: 0, textConsume: 0, analysisConsume: 0,
totalTokens: 0, inputTokens: 0, outputTokens: 0,
attachmentImageCount: 0, attachmentVideoCount: 0, attachmentAudioCount: 0, attachmentTotalCount: 0,
generatedImageCount: 0, generatedVideoCount: 0, generatedTotalCount: 0,
providerCostCalculatedTotal: '0.00000000', providerCostEstimatedTotal: '0.00000000',
providerCostCombinedTotal: '0.00000000', providerCostTotal: '0.00000000',
providerCostPendingCount: 0, providerCostEstimatedCount: 0, providerCostAbnormalCount: 0,
};
const RECORD_TYPE_MAP: Record<string, { text: string; color: string; icon: React.ReactNode }> = {
recharge: { text: '充值', color: 'green', icon: <ArrowUpOutlined /> },
consume: { text: '消费', color: 'red', icon: <ArrowDownOutlined /> },
refund: { text: '回退', color: 'blue', icon: <RollbackOutlined /> },
team_internal: { text: '团队内部', color: 'cyan', icon: <WalletOutlined /> },
};
const userScopeOptions = [
{ value: '', label: '全部用户' }, { value: 'admin', label: '后台用户' },
{ value: 'frontend_internal', label: '前台内部用户' }, { value: 'frontend_external', label: '前台外部用户' },
];
const recordTypeOptions = [
{ value: '', label: '全部流水' }, { value: 'recharge', label: '充值' }, { value: 'consume', label: '消费' },
{ value: 'refund', label: '回退' }, { value: 'team_internal', label: '团队内部' },
];
const creditSubjectOptions = [
{ value: '', label: '全部积分类型' }, { value: 'media', label: '图片/视频生成积分' },
{ value: 'text', label: '提词优化积分' }, { value: 'module', label: '模块功能积分' },
{ value: 'analysis', label: '分析积分' }, { value: 'split', label: '切片积分' },
{ value: 'admin_adjust', label: '管理员调整' }, { value: 'team_internal', label: '团队内部转移' },
{ value: 'recharge', label: '充值积分' }, { value: 'unknown', label: '历史未知' },
];
const mediaTypeOptions = [{ value: '', label: '全部媒体' }, { value: 'image', label: '图片' }, { value: 'video', label: '视频' }];
const chargeKindOptions = [
{ value: '', label: '全部扣费子类' }, { value: 'media', label: '媒体生成' }, { value: 'text_prompt', label: '提词优化' },
{ value: 'file_parse', label: '文件解析' }, { value: 'vision_input', label: '图片理解' },
{ value: 'module_create', label: '创建模块项目' }, { value: 'video_analysis', label: '视频分析' },
{ value: 'video_split', label: '视频切片' }, { value: 'admin_adjust', label: '管理员调整' },
{ value: 'team_internal', label: '团队内部转移' },
];
const sourceModuleOptions = [
{ value: '', label: '全部模块' }, { value: 'ai_creation', label: 'AI创作' }, { value: 'generation_record', label: '项目记录' },
{ value: 'hot_opening_replicate', label: '爆款开头复刻' }, { value: 'shot_replicate', label: '拆镜复刻' },
{ value: 'admin', label: '后台管理' }, { value: 'payment', label: '支付充值' }, { value: 'team', label: '团队管理' },
];
const sourceStepOptions = [
{ value: '', label: '全部步骤' }, { value: 'image_prompt_optimize', label: '图片提词优化' },
{ value: 'image_generate', label: '图片生成' }, { value: 'video_prompt_optimize', label: '视频提词优化' },
{ value: 'video_generate', label: '视频生成' }, { value: 'video_analysis', label: '视频分析' },
];
const billingSceneOptions = [
{ value: '', label: '全部计费场景' },
{ value: 'ai_creation_image_generate', label: 'AI创作图片生成' },
{ value: 'ai_creation_video_generate', label: 'AI创作视频生成' },
{ value: 'generation_record_text_prompt_optimize', label: '项目记录提词优化' },
{ value: 'generation_record_image_generate', label: '项目记录图片生成' },
{ value: 'generation_record_video_generate', label: '项目记录视频生成' },
{ value: 'generation_record_file_parse', label: '项目记录文件解析' },
{ value: 'generation_record_vision_input', label: '项目记录图片理解' },
{ value: 'hot_opening_project_create', label: '爆款开头复刻创建项目' },
{ value: 'hot_opening_image_prompt_optimize', label: '爆款开头复刻图片提词优化' },
{ value: 'hot_opening_image_generate', label: '爆款开头复刻图片生成' },
{ value: 'hot_opening_video_prompt_optimize', label: '爆款开头复刻视频提词优化' },
{ value: 'hot_opening_video_generate', label: '爆款开头复刻视频生成' },
{ value: 'shot_video_analysis', label: '拆镜视频分析' },
{ value: 'shot_original_video_analysis', label: '拆镜复刻原视频分析' },
{ value: 'shot_segment_video_analysis', label: '拆镜复刻片段视频分析' },
{ value: 'shot_video_split', label: '拆镜切片' },
{ value: 'shot_segment_replicate_create', label: '从切片创建复刻项目' },
{ value: 'shot_image_prompt_optimize', label: '拆镜复刻图片提词优化' },
{ value: 'shot_image_generate', label: '拆镜复刻图片生成' },
{ value: 'shot_video_prompt_optimize', label: '拆镜复刻视频提词优化' },
{ value: 'shot_video_generate', label: '拆镜复刻视频生成' },
{ value: 'recharge', label: '充值' },
{ value: 'admin_adjust', label: '管理员调整' },
{ value: 'refund', label: '回退' },
{ value: 'team_internal_transfer', label: '团队内部转账' },
{ value: 'unknown', label: '历史未知' },
];
const costStatusOptions = [
{ value: '', label: '全部成本状态' }, { value: 'pending', label: '待回填' }, { value: 'calculated', label: '已核算' },
{ value: 'estimated', label: '估算' }, { value: 'unmatched_rule', label: '未匹配价格' },
{ value: 'usage_missing', label: '用量缺失' }, { value: 'historical_price_unavailable', label: '历史价格缺失' },
{ value: 'historical_engine_unavailable', label: '历史引擎缺失' },
{ value: 'provider_result_uncertain', label: '供应商结果不确定' },
{ value: 'not_incurred', label: '供应商费用未发生' },
{ value: 'error', label: '核算异常' }, { value: 'not_applicable', label: '不涉及成本' },
];
function n(value: number | string | undefined | null, digits = 0): string {
const parsed = Number(value || 0);
return parsed.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits });
}
function engineTypeLabel(type?: string): string {
if (type === 'model') return '提词/分析模型';
if (type === 'image') return '图片引擎';
if (type === 'video') return '视频引擎';
return '执行配置';
}
function buildScope(scope: string): Pick<AdminCreditRecordQueryParams, 'userType' | 'frontendUserKind'> {
if (scope === 'admin') return { userType: 'admin' };
if (scope === 'frontend_internal') return { userType: 'frontend', frontendUserKind: 'internal' };
if (scope === 'frontend_external') return { userType: 'frontend', frontendUserKind: 'external' };
return {};
}
function costStatusColor(status?: string): string {
if (status === 'calculated') return 'green';
if (status === 'estimated') return 'orange';
if (status === 'pending') return 'blue';
if (status === 'not_applicable') return 'default';
return 'red';
}
const JsonBlock: React.FC<{ value?: Record<string, any> | null }> = ({ value }) => (
<pre style={{ background: '#f7f8fa', borderRadius: 8, padding: 12, overflow: 'auto', whiteSpace: 'pre-wrap' }}>
{value ? JSON.stringify(value, null, 2) : '-'}
</pre>
);
const AdminCreditRecords: React.FC = () => {
const [records, setRecords] = useState<AdminCreditRecord[]>([]);
const [summary, setSummary] = useState<AdminCreditRecordSummary>(DEFAULT_SUMMARY);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [exporting, setExporting] = useState(false);
const [exportProgress, setExportProgress] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [detail, setDetail] = useState<AdminCreditRecord | null>(null);
const [userScope, setUserScope] = useState('');
const [teamFilter, setTeamFilter] = useState('');
const [teamOptions, setTeamOptions] = useState<AdminTeamOption[]>([]);
const [recordType, setRecordType] = useState('');
const [creditSubject, setCreditSubject] = useState('');
const [mediaType, setMediaType] = useState('');
const [chargeKind, setChargeKind] = useState('');
const [sourceModule, setSourceModule] = useState('');
const [sourceStepCode, setSourceStepCode] = useState('');
const [billingScene, setBillingScene] = useState('');
const [userNameFilter, setUserNameFilter] = useState('');
const [engineProvider, setEngineProvider] = useState('');
const [engineModelName, setEngineModelName] = useState('');
const [pricingVersionCode, setPricingVersionCode] = useState('');
const [providerCostStatus, setProviderCostStatus] = useState('');
const [hasAttachment, setHasAttachment] = useState('');
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
const query = useMemo<AdminCreditRecordQueryParams>(() => ({
page, pageSize, userName: userNameFilter || undefined, teamId: teamFilter || undefined,
recordType: recordType || undefined, creditSubject: creditSubject || undefined, mediaType: mediaType || undefined,
chargeKind: chargeKind || undefined, sourceModule: sourceModule || undefined, sourceStepCode: sourceStepCode || undefined,
billingScene: billingScene || undefined,
engineProvider: engineProvider || undefined, engineModelName: engineModelName || undefined,
pricingVersionCode: pricingVersionCode || undefined, providerCostStatus: providerCostStatus || undefined,
hasAttachment: hasAttachment === '' ? undefined : hasAttachment === 'true',
startDate: dateRange[0]?.format('YYYY-MM-DD'), endDate: dateRange[1]?.format('YYYY-MM-DD'),
...buildScope(userScope),
}), [page, pageSize, userNameFilter, teamFilter, recordType, creditSubject, mediaType, chargeKind, sourceModule, sourceStepCode, billingScene, engineProvider, engineModelName, pricingVersionCode, providerCostStatus, hasAttachment, dateRange, userScope]);
const load = async () => {
setLoading(true);
try {
const res = await getCreditRecords(query);
setRecords(res.items || []);
setTotal(res.total || 0);
setSummary({ ...DEFAULT_SUMMARY, ...(res.summary || {}) });
} catch (e: any) {
message.error(e?.message || '加载积分记录失败');
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, [query]);
useEffect(() => { getTeamOptions(true).then(setTeamOptions).catch(() => {}); }, []);
const handleReset = () => {
setUserScope(''); setTeamFilter(''); setRecordType(''); setCreditSubject(''); setMediaType(''); setChargeKind('');
setSourceModule(''); setSourceStepCode(''); setBillingScene(''); setUserNameFilter(''); setEngineProvider(''); setEngineModelName('');
setPricingVersionCode(''); setProviderCostStatus(''); setHasAttachment(''); setDateRange([null, null]); setPage(1);
};
const exportExcel = async () => {
setExporting(true);
setExportProgress('准备导出...');
try {
const exportPageSize = 500;
const baseQuery = { ...query, page: 1, pageSize: exportPageSize };
const first = await getCreditRecords(baseQuery);
const all: AdminCreditRecord[] = [...(first.items || [])];
const exportSummary = { ...DEFAULT_SUMMARY, ...(first.summary || {}) };
const totalRows = first.total || 0;
const totalPages = Math.max(1, Math.ceil(totalRows / exportPageSize));
for (let p = 2; p <= totalPages; p += 1) {
const res = await getCreditRecords({ ...baseQuery, page: p });
all.push(...(res.items || []));
setExportProgress(`正在获取 ${Math.min(all.length, totalRows)} / ${totalRows}`);
}
const columns: StyledExcelColumn<AdminCreditRecord>[] = [
{ title: '时间', maxWidth: 22, render: r => formatDate(r.createdAt || '') },
{ title: '用户', maxWidth: 20, render: r => r.username || '-' }, { title: '手机号', maxWidth: 18, render: r => r.phone || '-' },
{ title: '用户类型', maxWidth: 16, render: r => r.userTypeLabel || '-' },
{ title: '前台归类', maxWidth: 18, render: r => r.frontendUserKindLabel || '-' },
{ title: '归属团队', maxWidth: 20, render: r => r.teamNameSnapshot || '未分配团队' },
{ title: '流水类型', maxWidth: 14, render: r => r.recordTypeLabel || r.type }, { title: '积分类型', maxWidth: 20, render: r => r.creditSubjectLabel || '-' },
{ title: '扣费子类', maxWidth: 20, render: r => r.chargeKindLabel || '-' }, { title: '模块', maxWidth: 20, render: r => r.sourceModuleLabel || '-' },
{ title: '模块步骤', maxWidth: 22, render: r => r.sourceStepCodeLabel || '-' }, { title: '计费场景', maxWidth: 32, render: r => r.billingSceneLabel || '-' },
{ title: '媒体类型', maxWidth: 12, align: 'center', render: r => r.mediaTypeLabel || '-' },
{ title: '变动积分', numFmt: '#,##0.00', align: 'right', render: r => r.amount }, { title: '变动后余额', numFmt: '#,##0.00', align: 'right', render: r => r.balanceAfter },
{ title: '输入Token', numFmt: '#,##0', align: 'right', render: r => r.inputTokens || 0 }, { title: '输出Token', numFmt: '#,##0', align: 'right', render: r => r.outputTokens || 0 },
{ title: '实际Token', numFmt: '#,##0', align: 'right', render: r => r.totalTokens || 0 },
{ title: '图片附件数', numFmt: '#,##0', render: r => r.attachmentImageCount || 0 }, { title: '视频附件数', numFmt: '#,##0', render: r => r.attachmentVideoCount || 0 },
{ title: '音频附件数', numFmt: '#,##0', render: r => r.attachmentAudioCount || 0 }, { title: '附件总数', numFmt: '#,##0', render: r => r.attachmentTotalCount || 0 },
{ title: '输入视频总时长(秒)', numFmt: '#,##0.000000', render: r => Number(r.attachmentVideoDurationSeconds || 0) },
{ title: '输入音频总时长(秒)', numFmt: '#,##0.000000', render: r => Number(r.attachmentAudioDurationSeconds || 0) },
{ title: '请求生成数', numFmt: '#,##0', render: r => r.requestedOutputCount || 0 }, { title: '实际生成图片数', numFmt: '#,##0', render: r => r.generatedImageCount || 0 },
{ title: '实际生成视频数', numFmt: '#,##0', render: r => r.generatedVideoCount || 0 }, { title: '实际生成总数', numFmt: '#,##0', render: r => r.generatedTotalCount || 0 },
{ title: '供应商', maxWidth: 18, render: r => r.engineProvider || '-' }, { title: '模型', maxWidth: 30, render: r => r.engineModelName || '-' },
{ title: '计价模式', maxWidth: 24, render: r => r.pricingBillingModeLabel || '-' }, { title: '计算器版本', maxWidth: 28, render: r => r.pricingCalculatorVersion || '-' },
{ title: '计价版本', maxWidth: 22, render: r => r.pricingVersionCode || '-' }, { title: '用量来源', maxWidth: 20, render: r => r.pricingUsageSource || '-' },
{ title: '计价时间', maxWidth: 22, render: r => formatDate(r.pricingReferenceAt || '') },
{ title: '供应商成本', numFmt: '#,##0.00000000', align: 'right', render: r => Number(r.providerCostAmount || 0) },
{ title: '成本币种', render: r => r.providerCostCurrency || 'CNY' }, { title: '成本状态', maxWidth: 18, render: r => r.providerCostStatusLabel || '-' },
{ title: '是否估算', render: r => r.providerCostIsEstimated ? '是' : '否' },
{ title: '最终核算时间', maxWidth: 22, render: r => formatDate(r.providerCostFinalizedAt || '') },
{ title: '主供应商用量', render: r => r.providerUsagePrimary ? '是' : '否' },
{ title: '执行类型', maxWidth: 18, render: r => engineTypeLabel(r.engineType) },
{ title: '执行配置', maxWidth: 28, render: r => r.engineName || '-' },
{ title: '关联状态', maxWidth: 14, align: 'center', render: r => r.ownerDeleted ? '关联已删除' : '正常' },
{ title: '说明', maxWidth: 42, render: r => r.description || '' }, { title: '业务归属类型', maxWidth: 22, render: r => r.ownerType || '' },
{ title: '业务归属ID', maxWidth: 30, render: r => r.ownerId || '' }, { title: 'BizKey', maxWidth: 36, render: r => r.bizKey || '' },
];
exportStyledExcel({
filename: `积分流水_${dayjs().format('YYYYMMDD_HHmmss')}.xlsx`, sheetName: '积分流水', title: '积分流水与供应商成本核查',
metadataRows: [['筛选时间', `${dateRange[0]?.format('YYYY-MM-DD') || '不限'}${dateRange[1]?.format('YYYY-MM-DD') || '不限'}`], ['导出时间', dayjs().format('YYYY-MM-DD HH:mm:ss')], ['导出条数', totalRows]],
summaryRows: [
['总充值', exportSummary.totalRecharge], ['总消费', exportSummary.totalConsume], ['总回退', exportSummary.totalRefund], ['交易笔数', exportSummary.transactionCount],
['生成条数', exportSummary.generationCount], ['生成尝试次数', exportSummary.generationAttemptCount],
['图片生成条数', exportSummary.imageGenerationCount], ['视频生成条数', exportSummary.videoGenerationCount],
['图片消费积分', exportSummary.imageConsume], ['视频消费积分', exportSummary.videoConsume],
['提词消费积分', exportSummary.textConsume], ['视频分析积分', exportSummary.analysisConsume],
['总 Token', exportSummary.totalTokens], ['输入 Token', exportSummary.inputTokens], ['输出 Token', exportSummary.outputTokens],
['输入图片附件数', exportSummary.attachmentImageCount], ['输入视频附件数', exportSummary.attachmentVideoCount], ['输入音频附件数', exportSummary.attachmentAudioCount],
['实际生成图片数', exportSummary.generatedImageCount], ['实际生成视频数', exportSummary.generatedVideoCount],
['已核算供应商成本', Number(exportSummary.providerCostCalculatedTotal || 0)],
['估算供应商成本', Number(exportSummary.providerCostEstimatedTotal || 0)],
['成本参考合计', Number(exportSummary.providerCostCombinedTotal || 0)],
['待核算流水数', exportSummary.providerCostPendingCount],
['估算成本流水数', exportSummary.providerCostEstimatedCount], ['异常成本流水数', exportSummary.providerCostAbnormalCount],
], columns, rows: all,
});
message.success('Excel 已导出');
} catch (e: any) {
message.error(e?.message || '导出失败');
} finally { setExporting(false); setExportProgress(''); }
};
const columns = [
{ title: '用户', dataIndex: 'username', width: 130, fixed: 'left' as const, render: (v: string, r: AdminCreditRecord) => <div><Typography.Text strong>{v || '-'}</Typography.Text><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.phone || '-'}</div></div> },
{ title: '用户类型', dataIndex: 'userTypeLabel', width: 120, render: (_: string, r: AdminCreditRecord) => <div><Tag color={r.userType === 'admin' ? 'orange' : 'blue'}>{r.userTypeLabel || '-'}</Tag><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.frontendUserKindLabel || '-'}</div></div> },
{ title: '归属团队', dataIndex: 'teamNameSnapshot', width: 130, render: (v: string) => v ? <Tag color="blue">{v}</Tag> : <Typography.Text type="secondary">未分配</Typography.Text> },
{ title: '流水类型', dataIndex: 'recordType', width: 105, render: (v: string, r: AdminCreditRecord) => { const cfg = RECORD_TYPE_MAP[v] || { text: r.recordTypeLabel || v || '-', color: 'default', icon: null }; return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>; } },
{ title: '积分类型', dataIndex: 'creditSubjectLabel', width: 150, render: (v: string, r: AdminCreditRecord) => <div><Tag>{v || '-'}</Tag><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.chargeKindLabel || '-'}</div></div> },
{ title: '模块', dataIndex: 'sourceModuleLabel', width: 135, render: (v: string) => v || '-' },
{ title: '步骤/场景', key: 'scene', width: 230, render: (_: any, r: AdminCreditRecord) => <div><div>{r.billingSceneLabel || '-'}</div><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.sourceStepCodeLabel || '-'}</div></div> },
{ title: '媒体', dataIndex: 'mediaTypeLabel', width: 85, render: (v: string) => v ? <Tag color="purple">{v}</Tag> : '-' },
{ title: '变动积分', dataIndex: 'amount', width: 120, sorter: (a: AdminCreditRecord, b: AdminCreditRecord) => a.amount - b.amount, render: (v: number) => <Typography.Text strong style={{ color: v > 0 ? '#10b981' : '#ef4444' }}>{v > 0 ? '+' : ''}{n(v, 2)}</Typography.Text> },
{ title: '余额', dataIndex: 'balanceAfter', width: 110, render: (v: number) => n(v, 2) },
{ title: 'Token', key: 'tokens', width: 140, render: (_: any, r: AdminCreditRecord) => <div><b>{n(r.totalTokens)}</b><div style={{ fontSize: 12, color: '#94a3b8' }}> {n(r.inputTokens)} / {n(r.outputTokens)}</div></div> },
{ title: '执行配置', key: 'engine', width: 240, render: (_: any, r: AdminCreditRecord) => <div><Tag color={r.engineType === 'model' ? 'geekblue' : r.engineType === 'image' ? 'purple' : r.engineType === 'video' ? 'cyan' : 'default'}>{engineTypeLabel(r.engineType)}</Tag><div>{r.engineName || '-'}</div><div style={{ fontSize: 12, color: '#94a3b8' }}>{[r.engineProvider, r.engineModelName].filter(Boolean).join(' / ') || '-'}</div></div> },
{ title: '关联状态', dataIndex: 'ownerDeleted', width: 105, render: (v: boolean) => <Tag color={v ? 'red' : 'green'}>{v ? '已删除' : '正常'}</Tag> },
{ title: '说明', dataIndex: 'description', width: 240, ellipsis: true },
{ title: '时间', dataIndex: 'createdAt', width: 165, render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text> },
{ title: '附件', key: 'attachments', width: 155, render: (_: any, r: AdminCreditRecord) => <div> {n(r.attachmentImageCount)} / {n(r.attachmentVideoCount)} / {n(r.attachmentAudioCount)}<div style={{ fontSize: 12, color: '#94a3b8' }}>合计 {n(r.attachmentTotalCount)}</div></div> },
{ title: '生成产出', key: 'outputs', width: 150, render: (_: any, r: AdminCreditRecord) => <div> {n(r.generatedImageCount)} / {n(r.generatedVideoCount)}<div style={{ fontSize: 12, color: '#94a3b8' }}>请求 {n(r.requestedOutputCount)} / 实际 {n(r.generatedTotalCount)}</div></div> },
{ title: '供应商实价', key: 'providerCost', width: 175, render: (_: any, r: AdminCreditRecord) => <div><Typography.Text strong>{r.providerCostCurrency || 'CNY'} {n(r.providerCostAmount, 8)}</Typography.Text><div><Tag color={costStatusColor(r.providerCostStatus)}>{r.providerCostStatusLabel || '-'}</Tag>{r.providerCostIsEstimated && <Tag color="orange">估算</Tag>}</div></div> },
{ title: '计价版本', key: 'pricing', width: 220, render: (_: any, r: AdminCreditRecord) => <div><div>{r.pricingVersionCode || '未锁价'}</div><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.pricingBillingModeLabel || '-'} / {r.pricingCalculatorVersion || '-'}</div></div> },
{ title: '操作', key: 'action', width: 90, fixed: 'right' as const, render: (_: any, r: AdminCreditRecord) => <Button size="small" icon={<EyeOutlined />} onClick={() => setDetail(r)}>核查</Button> },
];
return <div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
<Card bordered={false}><Space><ArrowUpOutlined style={{ color: '#10b981', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总充值</div><div style={{ fontSize: 22, fontWeight: 800, color: '#10b981' }}>+{n(summary.totalRecharge, 2)}</div></div></Space></Card>
<Card bordered={false}><Space><ArrowDownOutlined style={{ color: '#ef4444', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总消费</div><div style={{ fontSize: 22, fontWeight: 800, color: '#ef4444' }}>-{n(summary.totalConsume, 2)}</div></div></Space></Card>
<Card bordered={false}><Space><RollbackOutlined style={{ color: '#3b82f6', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总回退</div><div style={{ fontSize: 22, fontWeight: 800, color: '#3b82f6' }}>+{n(summary.totalRefund, 2)}</div></div></Space></Card>
<Card bordered={false}><Space><WalletOutlined style={{ color: '#6366f1', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>交易 / 生成</div><div style={{ fontSize: 22, fontWeight: 800 }}>{n(summary.transactionCount)} / {n(summary.generationCount)}</div></div></Space></Card>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
<Card size="small">图片生成:{n(summary.imageGenerationCount)} / {n(summary.imageConsume, 2)} 积分</Card>
<Card size="small">视频生成:{n(summary.videoGenerationCount)} / {n(summary.videoConsume, 2)} 积分</Card>
<Card size="small">提词消费:{n(summary.textConsume, 2)} 积分</Card>
<Card size="small">视频分析:{n(summary.analysisConsume, 2)} 积分</Card>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
<Card size="small">输入附件(图//音):{n(summary.attachmentImageCount)} / {n(summary.attachmentVideoCount)} / {n(summary.attachmentAudioCount)}</Card>
<Card size="small">实际产出(图/视):{n(summary.generatedImageCount)} / {n(summary.generatedVideoCount)}</Card>
<Card size="small">已核算成本:¥{n(summary.providerCostCalculatedTotal, 8)}</Card>
<Card size="small">估算成本:{n(summary.providerCostEstimatedCount)} / ¥{n(summary.providerCostEstimatedTotal, 8)}</Card>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
<Card size="small"> Token{n(summary.totalTokens)}</Card>
<Card size="small">待核算:{n(summary.providerCostPendingCount)} </Card>
<Card size="small">异常成本:{n(summary.providerCostAbnormalCount)} </Card>
<Card size="small">成本参考合计:¥{n(summary.providerCostCombinedTotal, 8)}</Card>
</div>
<Card bordered={false} style={{ borderRadius: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
<Space wrap>
<Select value={userScope} onChange={v => { setPage(1); setUserScope(v); }} style={{ width: 150 }} options={userScopeOptions} />
<Select value={teamFilter} onChange={v => { setPage(1); setTeamFilter(v); }} style={{ width: 170 }} options={[{ value: '', label: '全部团队' }, { value: TEAM_UNASSIGNED_VALUE, label: '未分配团队' }, ...teamOptions.map(t => ({ value: t.id, label: t.status === 'disabled' ? `${t.name}(禁用)` : t.name }))]} />
<Select value={recordType} onChange={v => { setPage(1); setRecordType(v); }} style={{ width: 130 }} options={recordTypeOptions} />
<Select value={creditSubject} onChange={v => { setPage(1); setCreditSubject(v); }} style={{ width: 180 }} options={creditSubjectOptions} />
<Select value={mediaType} onChange={v => { setPage(1); setMediaType(v); }} style={{ width: 110 }} options={mediaTypeOptions} />
<Select value={chargeKind} onChange={v => { setPage(1); setChargeKind(v); }} style={{ width: 150 }} options={chargeKindOptions} />
<Select value={sourceModule} onChange={v => { setPage(1); setSourceModule(v); }} style={{ width: 160 }} options={sourceModuleOptions} />
<Select value={sourceStepCode} onChange={v => { setPage(1); setSourceStepCode(v); }} style={{ width: 160 }} options={sourceStepOptions} />
<Select value={billingScene} onChange={v => { setPage(1); setBillingScene(v); }} style={{ width: 220 }} options={billingSceneOptions} />
<Select value={providerCostStatus} onChange={v => { setPage(1); setProviderCostStatus(v); }} style={{ width: 160 }} options={costStatusOptions} />
<Select value={hasAttachment} onChange={v => { setPage(1); setHasAttachment(v); }} style={{ width: 130 }} options={[{ value: '', label: '全部附件' }, { value: 'true', label: '有附件' }, { value: 'false', label: '无附件' }]} />
<Input placeholder="用户名/手机号/邮箱" value={userNameFilter} onChange={e => { setPage(1); setUserNameFilter(e.target.value); }} style={{ width: 190 }} allowClear />
<Input placeholder="供应商" value={engineProvider} onChange={e => { setPage(1); setEngineProvider(e.target.value); }} style={{ width: 130 }} allowClear />
<Input placeholder="模型名称" value={engineModelName} onChange={e => { setPage(1); setEngineModelName(e.target.value); }} style={{ width: 230 }} allowClear />
<Input placeholder="计价版本" value={pricingVersionCode} onChange={e => { setPage(1); setPricingVersionCode(e.target.value); }} style={{ width: 180 }} allowClear />
<DatePicker.RangePicker value={dateRange} onChange={dates => { setPage(1); setDateRange(dates ? [dates[0], dates[1]] : [null, null]); }} />
</Space>
<Space><Button onClick={handleReset}>重置</Button><Button icon={<ReloadOutlined />} onClick={load}>刷新</Button><Button type="primary" icon={<DownloadOutlined />} loading={exporting} onClick={exportExcel}>下载 Excel</Button></Space>
</div>
{exportProgress && <div style={{ marginBottom: 12, color: '#6366f1' }}>{exportProgress}</div>}
<Table columns={columns} dataSource={records} rowKey="id" loading={loading} scroll={{ x: 3650 }} pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); }, showTotal: t => `共 ${t} 条` }} />
</Card>
<Drawer open={!!detail} width={860} title="积分流水财务核查" onClose={() => setDetail(null)}>
{detail && <>
<Descriptions bordered size="small" column={2}>
<Descriptions.Item label="流水ID" span={2}>{detail.id}</Descriptions.Item>
<Descriptions.Item label="用户">{detail.username || '-'} / {detail.phone || '-'}</Descriptions.Item>
<Descriptions.Item label="发生时间">{formatDate(detail.createdAt || '')}</Descriptions.Item>
<Descriptions.Item label="积分变动">{n(detail.amount, 2)}</Descriptions.Item>
<Descriptions.Item label="余额">{n(detail.balanceAfter, 2)}</Descriptions.Item>
<Descriptions.Item label="模型" span={2}>{detail.engineProvider || '-'} / {detail.engineModelName || detail.engineName || '-'}</Descriptions.Item>
<Descriptions.Item label="计价版本">{detail.pricingVersionCode || '-'}</Descriptions.Item>
<Descriptions.Item label="计价模式">{detail.pricingBillingModeLabel || '-'}</Descriptions.Item>
<Descriptions.Item label="计算器版本">{detail.pricingCalculatorVersion || '-'}</Descriptions.Item>
<Descriptions.Item label="用量来源">{detail.pricingUsageSource || '-'}</Descriptions.Item>
<Descriptions.Item label="计价参考时间">{formatDate(detail.pricingReferenceAt || '')}</Descriptions.Item>
<Descriptions.Item label="价格生效区间">{formatDate(detail.pricingEffectiveFrom || '')} {detail.pricingEffectiveTo ? formatDate(detail.pricingEffectiveTo) : '长期'}</Descriptions.Item>
<Descriptions.Item label="供应商成本"><b>{detail.providerCostCurrency || 'CNY'} {n(detail.providerCostAmount, 8)}</b></Descriptions.Item>
<Descriptions.Item label="核算状态"><Tag color={costStatusColor(detail.providerCostStatus)}>{detail.providerCostStatusLabel || '-'}</Tag></Descriptions.Item>
<Descriptions.Item label="最终核算时间">{formatDate(detail.providerCostFinalizedAt || '')}</Descriptions.Item>
<Descriptions.Item label="主供应商用量">{detail.providerUsagePrimary ? '是' : '否'}</Descriptions.Item>
<Descriptions.Item label="附件统计"> {detail.attachmentImageCount} / {detail.attachmentVideoCount} / {detail.attachmentAudioCount}</Descriptions.Item>
<Descriptions.Item label="产出统计">请求 {detail.requestedOutputCount} / {detail.generatedImageCount} / {detail.generatedVideoCount}</Descriptions.Item>
<Descriptions.Item label="Token"> {detail.inputTokens} / {detail.outputTokens} / {detail.totalTokens}</Descriptions.Item>
<Descriptions.Item label="业务归属">{detail.ownerType || '-'} / {detail.ownerId || '-'}</Descriptions.Item>
</Descriptions>
<Typography.Title level={5}>计价快照</Typography.Title><JsonBlock value={detail.pricingSnapshotJson} />
<Typography.Title level={5}>标准化用量</Typography.Title><JsonBlock value={detail.usageSnapshotJson} />
<Typography.Title level={5}>附件快照</Typography.Title><JsonBlock value={detail.attachmentSnapshotJson} />
<Typography.Title level={5}>生成快照</Typography.Title><JsonBlock value={detail.generationSnapshotJson} />
</>}
</Drawer>
</div>;
};
export default AdminCreditRecords;