384 lines
34 KiB
TypeScript
384 lines
34 KiB
TypeScript
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;
|