1
This commit is contained in:
Vendored
+110
-110
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-3wUbVp5v.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-B0tEuCkU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -14,7 +14,6 @@ import AdminModels from './pages/AdminModels';
|
||||
import AdminSettings from './pages/AdminSettings';
|
||||
import AdminNotificationManager from './pages/AdminNotificationManager';
|
||||
import AdminCreditRecords from './pages/AdminCreditRecords';
|
||||
import AdminModelPricingRules from './pages/AdminModelPricingRules';
|
||||
import AdminPaymentConfig from './pages/AdminPaymentConfig';
|
||||
import AdminPaymentStats from './pages/AdminPaymentStats';
|
||||
import AdminIndustries from './pages/AdminIndustries';
|
||||
@@ -88,7 +87,6 @@ const App = () => {
|
||||
<Route path="users" element={<AdminUsers />} />
|
||||
<Route path="teams" element={<AdminTeams />} />
|
||||
<Route path="credit-records" element={<AdminCreditRecords />} />
|
||||
<Route path="model-pricing" element={<AdminModelPricingRules />} />
|
||||
<Route path="models" element={<AdminModels />} />
|
||||
<Route path="credit-ratios" element={<AdminCreditRatios />} />
|
||||
<Route path="video-engines" element={<AdminVideoEngines />} />
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
VideoPromptSchemaConfigOut, VideoPromptSchemaConfigSavePayload,
|
||||
VideoPromptSchemaPreviewPayload, VideoPromptSchemaPreviewOut, VideoPromptSchemaExportOut,
|
||||
AdminCreditRecordListResponse, AdminCreditRecordQueryParams,
|
||||
ModelPricingRule, ModelPricingRuleListResponse, ModelPricingRulePayload, ModelPricingPreviewResponse,
|
||||
ResourceCapacityConfigOut, ResourceCapacityConfigPayload, AdminUserResourceCapacityOut,
|
||||
AdminTeam, AdminTeamListResponse, AdminTeamOption, AdminTeamPayload, AdminTeamQueryParams,
|
||||
PrivatePortraitConfig, PrivatePortraitProjectListOut, PrivatePortraitAssetListOut,
|
||||
@@ -297,54 +296,12 @@ export async function getCreditRecords(filters?: AdminCreditRecordQueryParams):
|
||||
setMaybe(params, 'source_module', filters?.sourceModule);
|
||||
setMaybe(params, 'source_step_code', filters?.sourceStepCode);
|
||||
setMaybe(params, 'billing_scene', filters?.billingScene);
|
||||
setMaybe(params, 'engine_provider', filters?.engineProvider);
|
||||
setMaybe(params, 'engine_model_name', filters?.engineModelName);
|
||||
setMaybe(params, 'pricing_version_code', filters?.pricingVersionCode);
|
||||
setMaybe(params, 'provider_cost_status', filters?.providerCostStatus);
|
||||
setMaybe(params, 'provider_cost_is_estimated', filters?.providerCostIsEstimated);
|
||||
setMaybe(params, 'has_attachment', filters?.hasAttachment);
|
||||
setMaybe(params, 'start_date', filters?.startDate);
|
||||
setMaybe(params, 'end_date', filters?.endDate);
|
||||
const q = params.toString() ? `?${params}` : '';
|
||||
return api.get(`/admin/credit-records${q}`);
|
||||
}
|
||||
|
||||
|
||||
export async function getModelPricingRules(filters?: {
|
||||
page?: number; pageSize?: number; provider?: string; modelName?: string; modelCategory?: string; publishStatus?: string;
|
||||
}): Promise<ModelPricingRuleListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
setMaybe(params, 'page', filters?.page);
|
||||
setMaybe(params, 'page_size', filters?.pageSize);
|
||||
setMaybe(params, 'provider', filters?.provider);
|
||||
setMaybe(params, 'model_name', filters?.modelName);
|
||||
setMaybe(params, 'model_category', filters?.modelCategory);
|
||||
setMaybe(params, 'publish_status', filters?.publishStatus);
|
||||
return api.get(`/admin/model-pricing/rules${params.toString() ? `?${params}` : ''}`);
|
||||
}
|
||||
|
||||
export async function createModelPricingRule(payload: ModelPricingRulePayload): Promise<ModelPricingRule> {
|
||||
return api.post('/admin/model-pricing/rules', payload);
|
||||
}
|
||||
|
||||
export async function updateModelPricingRule(id: string, payload: Partial<ModelPricingRulePayload>): Promise<ModelPricingRule> {
|
||||
return api.put(`/admin/model-pricing/rules/${id}`, payload);
|
||||
}
|
||||
|
||||
export async function publishModelPricingRule(id: string): Promise<ModelPricingRule> {
|
||||
return api.post(`/admin/model-pricing/rules/${id}/publish`);
|
||||
}
|
||||
|
||||
export async function disableModelPricingRule(id: string): Promise<ModelPricingRule> {
|
||||
return api.post(`/admin/model-pricing/rules/${id}/disable`);
|
||||
}
|
||||
|
||||
export async function previewModelPricing(payload: {
|
||||
billing_mode: string; calculator_version: string; rule_json: Record<string, any>; usage: Record<string, any>; currency?: string;
|
||||
}): Promise<ModelPricingPreviewResponse> {
|
||||
return api.post('/admin/model-pricing/preview', payload);
|
||||
}
|
||||
|
||||
export async function getIndustryConfigs(): Promise<any[]> {
|
||||
return api.get('/admin/industry-configs');
|
||||
}
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Alert, Button, DatePicker, Divider, Form, Input, InputNumber, Select, Space } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import type {
|
||||
ModelPricingBillingMode,
|
||||
ModelPricingCalculatorVersion,
|
||||
ModelPricingRule,
|
||||
ModelPricingRulePayload,
|
||||
} from '../../types';
|
||||
|
||||
interface Props {
|
||||
initial?: ModelPricingRule | null;
|
||||
loading?: boolean;
|
||||
onSubmit: (payload: ModelPricingRulePayload) => Promise<void> | void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const modeOptions = [
|
||||
{ value: 'text_token_tiered', label: '文本分档 Token 计价' },
|
||||
{ value: 'image_per_output', label: '按输出图片数量计价' },
|
||||
{ value: 'image_input_output_tiered', label: '输入图片 + 输出像素分档计价' },
|
||||
{ value: 'video_token_rate', label: '视频像素 Token 计价' },
|
||||
];
|
||||
|
||||
const calculatorByMode: Record<ModelPricingBillingMode, ModelPricingCalculatorVersion> = {
|
||||
text_token_tiered: 'text_token_tiered_v1',
|
||||
image_per_output: 'image_per_output_v1',
|
||||
image_input_output_tiered: 'image_input_output_tiered_v1',
|
||||
video_token_rate: 'video_pixel_token_v1',
|
||||
};
|
||||
|
||||
const billByOptions = [
|
||||
{ value: 'successful_output_count', label: '实际成功输出数' },
|
||||
{ value: 'requested_output_count', label: '请求输出数(估算)' },
|
||||
{ value: 'provider_billed_count', label: '供应商明确计费数' },
|
||||
];
|
||||
|
||||
function splitList(value?: string): string[] {
|
||||
return String(value || '').split(',').map(v => v.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function readAlias(source: Record<string, any>, snakeKey: string, camelKey: string): any {
|
||||
if (Object.prototype.hasOwnProperty.call(source, snakeKey)) return source[snakeKey];
|
||||
return source[camelKey];
|
||||
}
|
||||
|
||||
function removeKeys(source: Record<string, any>, keys: string[]): Record<string, any> {
|
||||
const result = { ...source };
|
||||
keys.forEach(key => delete result[key]);
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeTextTier(value: Record<string, any>): Record<string, any> {
|
||||
const result = removeKeys(value || {}, [
|
||||
'max_context_tokens', 'maxContextTokens',
|
||||
'input_rate', 'inputRate',
|
||||
'audio_input_rate', 'audioInputRate',
|
||||
'output_rate', 'outputRate',
|
||||
'cached_input_rate', 'cachedInputRate',
|
||||
'cached_audio_input_rate', 'cachedAudioInputRate',
|
||||
]);
|
||||
const maxContextTokens = readAlias(value || {}, 'max_context_tokens', 'maxContextTokens');
|
||||
return {
|
||||
...result,
|
||||
max_context_tokens: maxContextTokens === undefined ? null : maxContextTokens,
|
||||
input_rate: readAlias(value || {}, 'input_rate', 'inputRate'),
|
||||
audio_input_rate: readAlias(value || {}, 'audio_input_rate', 'audioInputRate'),
|
||||
output_rate: readAlias(value || {}, 'output_rate', 'outputRate'),
|
||||
cached_input_rate: readAlias(value || {}, 'cached_input_rate', 'cachedInputRate'),
|
||||
cached_audio_input_rate: readAlias(value || {}, 'cached_audio_input_rate', 'cachedAudioInputRate'),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOutputTier(value: Record<string, any>): Record<string, any> {
|
||||
const result = removeKeys(value || {}, ['max_pixels', 'maxPixels']);
|
||||
const maxPixels = readAlias(value || {}, 'max_pixels', 'maxPixels');
|
||||
return {
|
||||
...result,
|
||||
max_pixels: maxPixels === undefined ? null : maxPixels,
|
||||
rate: value?.rate,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeVideoRate(value: Record<string, any>): Record<string, any> {
|
||||
const result = removeKeys(value || {}, [
|
||||
'has_input_video', 'hasInputVideo',
|
||||
'generate_audio', 'generateAudio',
|
||||
'inference_modes', 'inferenceModes',
|
||||
]);
|
||||
const hasInputVideo = readAlias(value || {}, 'has_input_video', 'hasInputVideo');
|
||||
const generateAudio = readAlias(value || {}, 'generate_audio', 'generateAudio');
|
||||
const inferenceModes = readAlias(value || {}, 'inference_modes', 'inferenceModes');
|
||||
return {
|
||||
...result,
|
||||
...(hasInputVideo === undefined ? {} : { has_input_video: hasInputVideo }),
|
||||
...(generateAudio === undefined ? {} : { generate_audio: generateAudio }),
|
||||
...(inferenceModes === undefined ? {} : { inference_modes: inferenceModes }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端规则 JSON 统一使用 snake_case;同时兼容接口层已转换成 camelCase 的历史/当前数据。
|
||||
* 返回值会移除已知 camelCase 别名,避免保存时同时存在两套字段。
|
||||
*/
|
||||
function normalizeRuleJson(
|
||||
mode: ModelPricingBillingMode,
|
||||
value: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const source = value || {};
|
||||
const base = removeKeys(source, [
|
||||
'cache_storage_rate_per_million_token_hour', 'cacheStorageRatePerMillionTokenHour',
|
||||
'output_rate', 'outputRate',
|
||||
'bill_by', 'billBy',
|
||||
'free_input_images', 'freeInputImages',
|
||||
'input_image_rate', 'inputImageRate',
|
||||
'output_tiers', 'outputTiers',
|
||||
'token_formula', 'tokenFormula',
|
||||
'default_fps', 'defaultFps',
|
||||
'supported_resolutions', 'supportedResolutions',
|
||||
'dimension_map', 'dimensionMap',
|
||||
]);
|
||||
|
||||
if (mode === 'text_token_tiered') {
|
||||
return {
|
||||
...base,
|
||||
cache_storage_rate_per_million_token_hour: readAlias(
|
||||
source,
|
||||
'cache_storage_rate_per_million_token_hour',
|
||||
'cacheStorageRatePerMillionTokenHour',
|
||||
),
|
||||
tiers: (Array.isArray(source.tiers) ? source.tiers : []).map(item => normalizeTextTier(item || {})),
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'image_per_output') {
|
||||
return {
|
||||
...base,
|
||||
output_rate: readAlias(source, 'output_rate', 'outputRate'),
|
||||
bill_by: readAlias(source, 'bill_by', 'billBy'),
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'image_input_output_tiered') {
|
||||
const outputTiers = readAlias(source, 'output_tiers', 'outputTiers');
|
||||
return {
|
||||
...base,
|
||||
free_input_images: readAlias(source, 'free_input_images', 'freeInputImages'),
|
||||
input_image_rate: readAlias(source, 'input_image_rate', 'inputImageRate'),
|
||||
output_tiers: (Array.isArray(outputTiers) ? outputTiers : []).map(item => normalizeOutputTier(item || {})),
|
||||
bill_by: readAlias(source, 'bill_by', 'billBy'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
token_formula: readAlias(source, 'token_formula', 'tokenFormula'),
|
||||
default_fps: readAlias(source, 'default_fps', 'defaultFps'),
|
||||
supported_resolutions: readAlias(source, 'supported_resolutions', 'supportedResolutions'),
|
||||
dimension_map: readAlias(source, 'dimension_map', 'dimensionMap') || {},
|
||||
rates: (Array.isArray(source.rates) ? source.rates : []).map(item => normalizeVideoRate(item || {})),
|
||||
};
|
||||
}
|
||||
|
||||
function dimensionRows(rule: Record<string, any>): Array<Record<string, any>> {
|
||||
const rows: Array<Record<string, any>> = [];
|
||||
Object.entries(rule.dimension_map || {}).forEach(([resolution, ratios]) => {
|
||||
Object.entries((ratios || {}) as Record<string, any>).forEach(([aspectRatio, value]) => {
|
||||
const item = value as Record<string, any>;
|
||||
rows.push({ resolution, aspectRatio, width: item.width, height: item.height });
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function toFormPricing(mode: ModelPricingBillingMode, rule: Record<string, any>): Record<string, any> {
|
||||
const normalized = normalizeRuleJson(mode, rule || {});
|
||||
if (mode === 'text_token_tiered') {
|
||||
return {
|
||||
tiers: normalized.tiers || [],
|
||||
cacheStorageRate: normalized.cache_storage_rate_per_million_token_hour ?? 0,
|
||||
};
|
||||
}
|
||||
if (mode === 'image_per_output') {
|
||||
return {
|
||||
outputRate: normalized.output_rate ?? 0,
|
||||
billBy: normalized.bill_by || 'successful_output_count',
|
||||
};
|
||||
}
|
||||
if (mode === 'image_input_output_tiered') {
|
||||
return {
|
||||
freeInputImages: normalized.free_input_images ?? 1,
|
||||
inputImageRate: normalized.input_image_rate ?? 0,
|
||||
outputTiers: normalized.output_tiers || [],
|
||||
billBy: normalized.bill_by || 'successful_output_count',
|
||||
};
|
||||
}
|
||||
return {
|
||||
tokenFormula: normalized.token_formula
|
||||
|| '(input_video_seconds + output_video_seconds) * width * height * fps / 1024',
|
||||
supportedResolutions: (normalized.supported_resolutions || []).join(','),
|
||||
defaultFps: normalized.default_fps ?? 30,
|
||||
dimensionRows: dimensionRows(normalized),
|
||||
rates: (normalized.rates || []).map((r: any) => ({
|
||||
...r,
|
||||
resolutions: (r.resolutions || []).join(','),
|
||||
inferenceModes: (r.inference_modes || []).join(','),
|
||||
hasInputVideo: r.has_input_video === undefined ? 'any' : r.has_input_video ? 'true' : 'false',
|
||||
generateAudio: r.generate_audio === undefined ? 'any' : r.generate_audio ? 'true' : 'false',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function buildDimensionMap(rows: Array<Record<string, any>>): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
(rows || []).forEach(row => {
|
||||
const resolution = String(row.resolution || '').trim().toLowerCase();
|
||||
const aspectRatio = String(row.aspectRatio || '').trim();
|
||||
const width = Number(row.width || 0);
|
||||
const height = Number(row.height || 0);
|
||||
if (!resolution || !aspectRatio || width <= 0 || height <= 0) return;
|
||||
result[resolution] ||= {};
|
||||
result[resolution][aspectRatio] = { width, height };
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildRuleJson(
|
||||
mode: ModelPricingBillingMode,
|
||||
pricing: Record<string, any>,
|
||||
baseRule: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const base = normalizeRuleJson(mode, baseRule || {});
|
||||
if (mode === 'text_token_tiered') {
|
||||
return {
|
||||
...base,
|
||||
unit: 'CNY_per_million_tokens',
|
||||
cache_storage_rate_per_million_token_hour: String(pricing.cacheStorageRate ?? 0),
|
||||
tiers: (pricing.tiers || []).map((v: any) => ({
|
||||
max_context_tokens: readAlias(v || {}, 'max_context_tokens', 'maxContextTokens') === '' || readAlias(v || {}, 'max_context_tokens', 'maxContextTokens') === undefined
|
||||
? null
|
||||
: readAlias(v || {}, 'max_context_tokens', 'maxContextTokens'),
|
||||
input_rate: String(readAlias(v || {}, 'input_rate', 'inputRate') ?? 0),
|
||||
audio_input_rate: String(readAlias(v || {}, 'audio_input_rate', 'audioInputRate') ?? readAlias(v || {}, 'input_rate', 'inputRate') ?? 0),
|
||||
output_rate: String(readAlias(v || {}, 'output_rate', 'outputRate') ?? 0),
|
||||
cached_input_rate: String(readAlias(v || {}, 'cached_input_rate', 'cachedInputRate') ?? readAlias(v || {}, 'input_rate', 'inputRate') ?? 0),
|
||||
cached_audio_input_rate: String(
|
||||
readAlias(v || {}, 'cached_audio_input_rate', 'cachedAudioInputRate')
|
||||
?? readAlias(v || {}, 'cached_input_rate', 'cachedInputRate')
|
||||
?? readAlias(v || {}, 'audio_input_rate', 'audioInputRate')
|
||||
?? readAlias(v || {}, 'input_rate', 'inputRate')
|
||||
?? 0,
|
||||
),
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (mode === 'image_per_output') {
|
||||
return {
|
||||
...base,
|
||||
unit: 'CNY_per_image',
|
||||
output_rate: String(pricing.outputRate ?? 0),
|
||||
bill_by: pricing.billBy || 'successful_output_count',
|
||||
};
|
||||
}
|
||||
if (mode === 'image_input_output_tiered') {
|
||||
return {
|
||||
...base,
|
||||
unit: 'CNY_per_image',
|
||||
free_input_images: Number(pricing.freeInputImages || 0),
|
||||
input_image_rate: String(pricing.inputImageRate ?? 0),
|
||||
output_tiers: (pricing.outputTiers || []).map((v: any) => ({
|
||||
max_pixels: readAlias(v || {}, 'max_pixels', 'maxPixels') === undefined
|
||||
|| readAlias(v || {}, 'max_pixels', 'maxPixels') === null
|
||||
|| readAlias(v || {}, 'max_pixels', 'maxPixels') === ''
|
||||
? null
|
||||
: Number(readAlias(v || {}, 'max_pixels', 'maxPixels')),
|
||||
rate: String(v.rate ?? 0),
|
||||
})),
|
||||
bill_by: pricing.billBy || 'successful_output_count',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
unit: 'CNY_per_million_tokens',
|
||||
token_formula: '(input_video_seconds + output_video_seconds) * width * height * fps / 1024',
|
||||
default_fps: Number(pricing.defaultFps || 30),
|
||||
supported_resolutions: splitList(pricing.supportedResolutions).map(v => v.toLowerCase()),
|
||||
dimension_map: buildDimensionMap(pricing.dimensionRows || []),
|
||||
rates: (pricing.rates || []).map((v: any) => ({
|
||||
...(splitList(v.resolutions).length
|
||||
? { resolutions: splitList(v.resolutions).map(item => item.toLowerCase()) }
|
||||
: {}),
|
||||
...(v.hasInputVideo !== 'any' && v.hasInputVideo !== undefined
|
||||
? { has_input_video: v.hasInputVideo === 'true' }
|
||||
: {}),
|
||||
...(v.generateAudio !== 'any' && v.generateAudio !== undefined
|
||||
? { generate_audio: v.generateAudio === 'true' }
|
||||
: {}),
|
||||
...(splitList(v.inferenceModes).length
|
||||
? { inference_modes: splitList(v.inferenceModes).map(item => item.toLowerCase()) }
|
||||
: {}),
|
||||
rate: String(v.rate ?? 0),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const PricingRuleForm: React.FC<Props> = ({ initial, loading, onSubmit, onCancel }) => {
|
||||
const [form] = Form.useForm();
|
||||
const mode = Form.useWatch('billingMode', form) as ModelPricingBillingMode | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const billingMode = initial?.billingMode || 'video_token_rate';
|
||||
form.setFieldsValue({
|
||||
provider: initial?.provider || 'volcengine',
|
||||
modelName: initial?.modelName || '',
|
||||
modelCategory: initial?.modelCategory || 'video',
|
||||
billingMode,
|
||||
calculatorVersion: initial?.calculatorVersion || calculatorByMode[billingMode],
|
||||
versionCode: initial?.versionCode || `manual_${dayjs().format('YYYYMMDD_HHmmss')}`,
|
||||
effectiveRange: [
|
||||
dayjs(initial?.effectiveFrom || undefined),
|
||||
initial?.effectiveTo ? dayjs(initial.effectiveTo) : null,
|
||||
],
|
||||
currency: initial?.currency || 'CNY',
|
||||
ruleSchemaVersion: initial?.ruleSchemaVersion || 1,
|
||||
sourceUrl: initial?.sourceUrl || 'https://www.volcengine.com/docs/82379/1544106',
|
||||
sourceUpdatedAt: initial?.sourceUpdatedAt ? dayjs(initial.sourceUpdatedAt) : null,
|
||||
remark: initial?.remark || '',
|
||||
pricing: toFormPricing(billingMode, initial?.ruleJson || {}),
|
||||
});
|
||||
}, [initial, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mode) return;
|
||||
form.setFieldValue('calculatorVersion', calculatorByMode[mode]);
|
||||
}, [mode, form]);
|
||||
|
||||
const submit = async (values: any) => {
|
||||
const billingMode = values.billingMode as ModelPricingBillingMode;
|
||||
const [from, to] = values.effectiveRange || [];
|
||||
const preserveBase = initial?.billingMode === billingMode ? initial.ruleJson : {};
|
||||
await onSubmit({
|
||||
provider: values.provider,
|
||||
model_name: values.modelName,
|
||||
model_category: values.modelCategory,
|
||||
billing_mode: billingMode,
|
||||
calculator_version: calculatorByMode[billingMode],
|
||||
version_code: values.versionCode,
|
||||
effective_from: from.toISOString(),
|
||||
effective_to: to ? to.toISOString() : null,
|
||||
currency: values.currency || 'CNY',
|
||||
rule_schema_version: Number(values.ruleSchemaVersion || initial?.ruleSchemaVersion || 1),
|
||||
rule_json: buildRuleJson(billingMode, values.pricing || {}, preserveBase || {}),
|
||||
source_url: values.sourceUrl || null,
|
||||
source_updated_at: values.sourceUpdatedAt ? values.sourceUpdatedAt.toISOString() : null,
|
||||
remark: values.remark || null,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} layout="vertical" onFinish={submit} preserve={false}>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="种子规则只会创建草稿。请核对真实生效时间、单价和模型输出尺寸后再发布。"
|
||||
/>
|
||||
<Space size={12} align="start" style={{ display: 'flex' }}>
|
||||
<Form.Item name="provider" label="供应商" rules={[{ required: true }]} style={{ flex: 1 }}><Input /></Form.Item>
|
||||
<Form.Item name="modelName" label="完整模型名称" rules={[{ required: true }]} style={{ flex: 2 }}><Input /></Form.Item>
|
||||
</Space>
|
||||
<Space size={12} align="start" style={{ display: 'flex' }}>
|
||||
<Form.Item name="modelCategory" label="模型类型" rules={[{ required: true }]} style={{ flex: 1 }}>
|
||||
<Select options={[{ value: 'text', label: '文本' }, { value: 'image', label: '图片' }, { value: 'video', label: '视频' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="billingMode" label="计价模式" rules={[{ required: true }]} style={{ flex: 2 }}>
|
||||
<Select options={modeOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="calculatorVersion" label="计算器版本" style={{ flex: 2 }}><Input disabled /></Form.Item>
|
||||
</Space>
|
||||
<Space size={12} align="start" style={{ display: 'flex' }}>
|
||||
<Form.Item name="versionCode" label="价格版本号" rules={[{ required: true }]} style={{ flex: 1 }}><Input /></Form.Item>
|
||||
<Form.Item name="effectiveRange" label="真实生效区间 [开始, 结束)" rules={[{ required: true }]} style={{ flex: 2 }}>
|
||||
<DatePicker.RangePicker showTime allowEmpty={[false, true]} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="currency" label="币种" style={{ width: 100 }}><Input /></Form.Item>
|
||||
<Form.Item name="ruleSchemaVersion" label="结构版本" style={{ width: 100 }}><InputNumber min={1} /></Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider titlePlacement="start">价格参数</Divider>
|
||||
{mode === 'text_token_tiered' && <>
|
||||
<Form.Item name={['pricing', 'cacheStorageRate']} label="缓存存储单价(元/M Token·小时)">
|
||||
<InputNumber min={0} stringMode style={{ width: 260 }} />
|
||||
</Form.Item>
|
||||
<Form.List name={['pricing', 'tiers']}>
|
||||
{(fields, { add, remove }) => <>
|
||||
{fields.map(field => (
|
||||
<Space key={field.key} align="baseline" wrap>
|
||||
<Form.Item {...field} name={[field.name, 'max_context_tokens']} label="最大上下文 Token"><InputNumber min={1} /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'input_rate']} label="输入单价/M"><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'audio_input_rate']} label="音频输入/M"><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'cached_input_rate']} label="缓存文本/M"><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'cached_audio_input_rate']} label="缓存音频/M"><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'output_rate']} label="输出单价/M"><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Button danger onClick={() => remove(field.name)}>删除</Button>
|
||||
</Space>
|
||||
))}
|
||||
<Button onClick={() => add({})}>新增 Token 档位</Button>
|
||||
</>}
|
||||
</Form.List>
|
||||
</>}
|
||||
|
||||
{mode === 'image_per_output' && <Space align="baseline">
|
||||
<Form.Item name={['pricing', 'outputRate']} label="输出图片单价(元/张)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} stringMode style={{ width: 220 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['pricing', 'billBy']} label="计费数量来源"><Select style={{ width: 220 }} options={billByOptions} /></Form.Item>
|
||||
</Space>}
|
||||
|
||||
{mode === 'image_input_output_tiered' && <>
|
||||
<Space align="baseline">
|
||||
<Form.Item name={['pricing', 'freeInputImages']} label="免费输入图片数"><InputNumber min={0} /></Form.Item>
|
||||
<Form.Item name={['pricing', 'inputImageRate']} label="超出后输入图片单价"><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Form.Item name={['pricing', 'billBy']} label="输出计费数量来源"><Select style={{ width: 220 }} options={billByOptions} /></Form.Item>
|
||||
</Space>
|
||||
<Form.List name={['pricing', 'outputTiers']}>
|
||||
{(fields, { add, remove }) => <>
|
||||
{fields.map(field => <Space key={field.key} align="baseline">
|
||||
<Form.Item {...field} name={[field.name, 'max_pixels']} label="最大像素(末档留空)"><InputNumber min={1} /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'rate']} label="输出单价(元/张)"><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Button danger onClick={() => remove(field.name)}>删除</Button>
|
||||
</Space>)}
|
||||
<Button onClick={() => add({ max_pixels: null, rate: 0 })}>新增输出像素档位</Button>
|
||||
</>}
|
||||
</Form.List>
|
||||
</>}
|
||||
|
||||
{mode === 'video_token_rate' && <>
|
||||
<Form.Item name={['pricing', 'tokenFormula']} label="Token 公式说明(由计算器版本实现,不执行文本公式)"><Input disabled /></Form.Item>
|
||||
<Space align="baseline" wrap>
|
||||
<Form.Item name={['pricing', 'supportedResolutions']} label="支持分辨率(逗号分隔)"><Input placeholder="480p,720p" /></Form.Item>
|
||||
<Form.Item name={['pricing', 'defaultFps']} label="默认 FPS"><InputNumber min={1} max={120} /></Form.Item>
|
||||
</Space>
|
||||
<Divider titlePlacement="start" plain>分辨率 + 比例对应真实像素</Divider>
|
||||
<Form.List name={['pricing', 'dimensionRows']}>
|
||||
{(fields, { add, remove }) => <>
|
||||
{fields.map(field => <Space key={field.key} align="baseline" wrap>
|
||||
<Form.Item {...field} name={[field.name, 'resolution']} label="分辨率" rules={[{ required: true }]}><Input placeholder="720p" /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'aspectRatio']} label="比例" rules={[{ required: true }]}><Input placeholder="9:16" /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'width']} label="宽" rules={[{ required: true }]}><InputNumber min={1} /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'height']} label="高" rules={[{ required: true }]}><InputNumber min={1} /></Form.Item>
|
||||
<Button danger onClick={() => remove(field.name)}>删除</Button>
|
||||
</Space>)}
|
||||
<Button onClick={() => add({})}>新增尺寸映射</Button>
|
||||
</>}
|
||||
</Form.List>
|
||||
<Divider titlePlacement="start" plain>视频价格档位</Divider>
|
||||
<Form.List name={['pricing', 'rates']}>
|
||||
{(fields, { add, remove }) => <>
|
||||
{fields.map(field => <div key={field.key} style={{ border: '1px solid #eee', padding: 12, marginBottom: 12, borderRadius: 8 }}>
|
||||
<Space align="baseline" wrap>
|
||||
<Form.Item {...field} name={[field.name, 'resolutions']} label="分辨率"><Input placeholder="480p,720p;可空" /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'hasInputVideo']} label="含输入视频"><Select style={{ width: 120 }} options={[{ value: 'any', label: '不限' }, { value: 'true', label: '是' }, { value: 'false', label: '否' }]} /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'generateAudio']} label="生成音频"><Select style={{ width: 120 }} options={[{ value: 'any', label: '不限' }, { value: 'true', label: '是' }, { value: 'false', label: '否' }]} /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'inferenceModes']} label="推理模式"><Input placeholder="online,flex;可空" /></Form.Item>
|
||||
<Form.Item {...field} name={[field.name, 'rate']} label="单价/M Token" rules={[{ required: true }]}><InputNumber min={0} stringMode /></Form.Item>
|
||||
<Button danger onClick={() => remove(field.name)}>删除</Button>
|
||||
</Space>
|
||||
</div>)}
|
||||
<Button onClick={() => add({ hasInputVideo: 'any', generateAudio: 'any' })}>新增视频价格档位</Button>
|
||||
</>}
|
||||
</Form.List>
|
||||
</>}
|
||||
|
||||
<Divider titlePlacement="start">来源与备注</Divider>
|
||||
<Form.Item name="sourceUrl" label="官方来源 URL"><Input /></Form.Item>
|
||||
<Form.Item name="sourceUpdatedAt" label="官方文档更新时间(不是价格生效时间)"><DatePicker showTime /></Form.Item>
|
||||
<Form.Item name="remark" label="备注"><Input.TextArea rows={3} /></Form.Item>
|
||||
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>保存草稿</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingRuleForm;
|
||||
@@ -1,229 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Button, Card, Input, message, Space, Typography } from 'antd';
|
||||
import { previewModelPricing } from '../../api';
|
||||
import type {
|
||||
ModelPricingBillingMode,
|
||||
ModelPricingCalculatorVersion,
|
||||
ModelPricingPreviewResponse,
|
||||
} from '../../types';
|
||||
|
||||
interface Props {
|
||||
billingMode: ModelPricingBillingMode;
|
||||
calculatorVersion: ModelPricingCalculatorVersion;
|
||||
ruleJson: Record<string, any>;
|
||||
}
|
||||
|
||||
const examples: Record<ModelPricingBillingMode, Record<string, any>> = {
|
||||
text_token_tiered: {
|
||||
input_tokens: 1000,
|
||||
output_tokens: 300,
|
||||
cached_input_tokens: 0,
|
||||
audio_input_tokens: 0,
|
||||
usage_source: 'provider',
|
||||
},
|
||||
image_per_output: {
|
||||
requested_output_count: 1,
|
||||
successful_output_count: 1,
|
||||
provider_billed_count: 1,
|
||||
usage_source: 'provider_response',
|
||||
},
|
||||
image_input_output_tiered: {
|
||||
provider_input_image_count: 2,
|
||||
requested_output_count: 1,
|
||||
successful_output_count: 1,
|
||||
output_items: [{ width: 2048, height: 2048, pixels: 4194304 }],
|
||||
usage_source: 'provider_response',
|
||||
},
|
||||
video_token_rate: {
|
||||
total_tokens: 1000000,
|
||||
resolution: '720p',
|
||||
aspect_ratio: '9:16',
|
||||
has_input_video: false,
|
||||
generate_audio: false,
|
||||
inference_mode: 'online',
|
||||
usage_source: 'provider',
|
||||
},
|
||||
};
|
||||
|
||||
function readAlias(source: Record<string, any>, snakeKey: string, camelKey: string): any {
|
||||
if (Object.prototype.hasOwnProperty.call(source, snakeKey)) return source[snakeKey];
|
||||
return source[camelKey];
|
||||
}
|
||||
|
||||
function removeKeys(source: Record<string, any>, keys: string[]): Record<string, any> {
|
||||
const result = { ...source };
|
||||
keys.forEach(key => delete result[key]);
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeTextTier(value: Record<string, any>): Record<string, any> {
|
||||
const result = removeKeys(value || {}, [
|
||||
'max_context_tokens', 'maxContextTokens',
|
||||
'input_rate', 'inputRate',
|
||||
'audio_input_rate', 'audioInputRate',
|
||||
'output_rate', 'outputRate',
|
||||
'cached_input_rate', 'cachedInputRate',
|
||||
'cached_audio_input_rate', 'cachedAudioInputRate',
|
||||
]);
|
||||
|
||||
const maxContextTokens = readAlias(value || {}, 'max_context_tokens', 'maxContextTokens');
|
||||
return {
|
||||
...result,
|
||||
max_context_tokens: maxContextTokens === undefined ? null : maxContextTokens,
|
||||
input_rate: readAlias(value || {}, 'input_rate', 'inputRate'),
|
||||
audio_input_rate: readAlias(value || {}, 'audio_input_rate', 'audioInputRate'),
|
||||
output_rate: readAlias(value || {}, 'output_rate', 'outputRate'),
|
||||
cached_input_rate: readAlias(value || {}, 'cached_input_rate', 'cachedInputRate'),
|
||||
cached_audio_input_rate: readAlias(value || {}, 'cached_audio_input_rate', 'cachedAudioInputRate'),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOutputTier(value: Record<string, any>): Record<string, any> {
|
||||
const result = removeKeys(value || {}, ['max_pixels', 'maxPixels']);
|
||||
const maxPixels = readAlias(value || {}, 'max_pixels', 'maxPixels');
|
||||
return {
|
||||
...result,
|
||||
max_pixels: maxPixels === undefined ? null : maxPixels,
|
||||
rate: value?.rate,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeVideoRate(value: Record<string, any>): Record<string, any> {
|
||||
const result = removeKeys(value || {}, [
|
||||
'has_input_video', 'hasInputVideo',
|
||||
'generate_audio', 'generateAudio',
|
||||
'inference_modes', 'inferenceModes',
|
||||
]);
|
||||
|
||||
const hasInputVideo = readAlias(value || {}, 'has_input_video', 'hasInputVideo');
|
||||
const generateAudio = readAlias(value || {}, 'generate_audio', 'generateAudio');
|
||||
const inferenceModes = readAlias(value || {}, 'inference_modes', 'inferenceModes');
|
||||
|
||||
return {
|
||||
...result,
|
||||
...(hasInputVideo === undefined ? {} : { has_input_video: hasInputVideo }),
|
||||
...(generateAudio === undefined ? {} : { generate_audio: generateAudio }),
|
||||
...(inferenceModes === undefined ? {} : { inference_modes: inferenceModes }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 返回层可能会把嵌套 rule_json 一并转成 camelCase。
|
||||
* 后端计价器只接受 snake_case,因此试算前必须按计价模式恢复标准结构。
|
||||
*/
|
||||
function normalizeRuleJson(
|
||||
billingMode: ModelPricingBillingMode,
|
||||
value: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const source = value || {};
|
||||
const base = removeKeys(source, [
|
||||
'cache_storage_rate_per_million_token_hour', 'cacheStorageRatePerMillionTokenHour',
|
||||
'output_rate', 'outputRate',
|
||||
'bill_by', 'billBy',
|
||||
'free_input_images', 'freeInputImages',
|
||||
'input_image_rate', 'inputImageRate',
|
||||
'output_tiers', 'outputTiers',
|
||||
'token_formula', 'tokenFormula',
|
||||
'default_fps', 'defaultFps',
|
||||
'supported_resolutions', 'supportedResolutions',
|
||||
'dimension_map', 'dimensionMap',
|
||||
]);
|
||||
|
||||
if (billingMode === 'text_token_tiered') {
|
||||
const tiers = Array.isArray(source.tiers) ? source.tiers : [];
|
||||
return {
|
||||
...base,
|
||||
cache_storage_rate_per_million_token_hour: readAlias(
|
||||
source,
|
||||
'cache_storage_rate_per_million_token_hour',
|
||||
'cacheStorageRatePerMillionTokenHour',
|
||||
),
|
||||
tiers: tiers.map(item => normalizeTextTier(item || {})),
|
||||
};
|
||||
}
|
||||
|
||||
if (billingMode === 'image_per_output') {
|
||||
return {
|
||||
...base,
|
||||
output_rate: readAlias(source, 'output_rate', 'outputRate'),
|
||||
bill_by: readAlias(source, 'bill_by', 'billBy'),
|
||||
};
|
||||
}
|
||||
|
||||
if (billingMode === 'image_input_output_tiered') {
|
||||
const outputTiers = readAlias(source, 'output_tiers', 'outputTiers');
|
||||
return {
|
||||
...base,
|
||||
free_input_images: readAlias(source, 'free_input_images', 'freeInputImages'),
|
||||
input_image_rate: readAlias(source, 'input_image_rate', 'inputImageRate'),
|
||||
output_tiers: (Array.isArray(outputTiers) ? outputTiers : []).map(item => normalizeOutputTier(item || {})),
|
||||
bill_by: readAlias(source, 'bill_by', 'billBy'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
token_formula: readAlias(source, 'token_formula', 'tokenFormula'),
|
||||
default_fps: readAlias(source, 'default_fps', 'defaultFps'),
|
||||
supported_resolutions: readAlias(source, 'supported_resolutions', 'supportedResolutions'),
|
||||
dimension_map: readAlias(source, 'dimension_map', 'dimensionMap') || {},
|
||||
rates: (Array.isArray(source.rates) ? source.rates : []).map(item => normalizeVideoRate(item || {})),
|
||||
};
|
||||
}
|
||||
|
||||
const PricingRulePreview: React.FC<Props> = ({ billingMode, calculatorVersion, ruleJson }) => {
|
||||
const initial = useMemo(() => JSON.stringify(examples[billingMode], null, 2), [billingMode]);
|
||||
const [usageText, setUsageText] = useState(initial);
|
||||
const [result, setResult] = useState<ModelPricingPreviewResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setUsageText(initial);
|
||||
setResult(null);
|
||||
}, [initial, ruleJson, calculatorVersion]);
|
||||
|
||||
const run = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const usage = JSON.parse(usageText);
|
||||
setResult(await previewModelPricing({
|
||||
billing_mode: billingMode,
|
||||
calculator_version: calculatorVersion,
|
||||
rule_json: normalizeRuleJson(billingMode, ruleJson),
|
||||
usage,
|
||||
currency: 'CNY',
|
||||
}));
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '计价试算失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <Card size="small" title="规则试算" style={{ marginTop: 16 }}>
|
||||
<Typography.Text type="secondary">
|
||||
计算器:{calculatorVersion}。图片使用同步响应中的实际输出条目;视频优先使用供应商实际 Token。
|
||||
</Typography.Text>
|
||||
<Input.TextArea
|
||||
value={usageText}
|
||||
onChange={e => setUsageText(e.target.value)}
|
||||
rows={10}
|
||||
style={{ marginTop: 10, fontFamily: 'monospace' }}
|
||||
/>
|
||||
<Space style={{ marginTop: 10 }}>
|
||||
<Button type="primary" loading={loading} onClick={run}>开始试算</Button>
|
||||
</Space>
|
||||
{result && <Alert
|
||||
style={{ marginTop: 12 }}
|
||||
type={result.isEstimated ? 'warning' : 'success'}
|
||||
showIcon
|
||||
message={`${result.currency} ${result.amount}${result.isEstimated ? '(估算)' : ''}`}
|
||||
description={<>
|
||||
<div style={{ marginBottom: 8 }}>用量来源:{result.usageSource || '-'}</div>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{JSON.stringify(result.breakdown, null, 2)}</pre>
|
||||
</>}
|
||||
/>}
|
||||
</Card>;
|
||||
};
|
||||
|
||||
export default PricingRulePreview;
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Button, Card, DatePicker, Descriptions, Drawer, Input, message, Select, Space, Table, Tag, Typography,
|
||||
Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowDownOutlined, ArrowUpOutlined, DownloadOutlined, EyeOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined,
|
||||
ArrowDownOutlined, ArrowUpOutlined, DownloadOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { exportStyledExcel, type StyledExcelColumn } from '../utils/excelExport';
|
||||
import dayjs from 'dayjs';
|
||||
import { getCreditRecords, getTeamOptions } from '../api';
|
||||
import type { AdminCreditRecord, AdminCreditRecordQueryParams, AdminCreditRecordSummary, AdminTeamOption } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
@@ -14,15 +14,21 @@ 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,
|
||||
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,
|
||||
};
|
||||
|
||||
const RECORD_TYPE_MAP: Record<string, { text: string; color: string; icon: React.ReactNode }> = {
|
||||
@@ -33,38 +39,73 @@ const RECORD_TYPE_MAP: Record<string, { text: string; color: string; icon: React
|
||||
};
|
||||
|
||||
const userScopeOptions = [
|
||||
{ value: '', label: '全部用户' }, { value: 'admin', label: '后台用户' },
|
||||
{ value: 'frontend_internal', label: '前台内部用户' }, { value: 'frontend_external', label: '前台外部用户' },
|
||||
{ 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: '团队内部' },
|
||||
{ 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: '历史未知' },
|
||||
{ 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 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: '', 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: '团队管理' },
|
||||
{ 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: '团队管理' },
|
||||
{ value: 'unknown', 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: '视频分析' },
|
||||
{ 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创作图片生成' },
|
||||
@@ -94,45 +135,24 @@ const billingSceneOptions = [
|
||||
{ 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 n(value: number | undefined | null): string {
|
||||
return Number(value || 0).toLocaleString();
|
||||
}
|
||||
|
||||
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[]>([]);
|
||||
@@ -143,7 +163,6 @@ const AdminCreditRecords: React.FC = () => {
|
||||
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('');
|
||||
@@ -156,24 +175,24 @@ const AdminCreditRecords: React.FC = () => {
|
||||
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,
|
||||
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'),
|
||||
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]);
|
||||
}), [page, pageSize, userNameFilter, teamFilter, recordType, creditSubject, mediaType, chargeKind, sourceModule, sourceStepCode, billingScene, dateRange, userScope]);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
@@ -181,20 +200,33 @@ const AdminCreditRecords: React.FC = () => {
|
||||
const res = await getCreditRecords(query);
|
||||
setRecords(res.items || []);
|
||||
setTotal(res.total || 0);
|
||||
setSummary({ ...DEFAULT_SUMMARY, ...(res.summary || {}) });
|
||||
setSummary(res.summary || DEFAULT_SUMMARY);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载积分记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, [query]);
|
||||
useEffect(() => { getTeamOptions(true).then(setTeamOptions).catch(() => {}); }, []);
|
||||
|
||||
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);
|
||||
setUserScope('');
|
||||
setTeamFilter('');
|
||||
setRecordType('');
|
||||
setCreditSubject('');
|
||||
setMediaType('');
|
||||
setChargeKind('');
|
||||
setSourceModule('');
|
||||
setSourceStepCode('');
|
||||
setBillingScene('');
|
||||
setUserNameFilter('');
|
||||
setDateRange([null, null]);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const exportExcel = async () => {
|
||||
@@ -205,179 +237,168 @@ const AdminCreditRecords: React.FC = () => {
|
||||
const baseQuery = { ...query, page: 1, pageSize: exportPageSize };
|
||||
const first = await getCreditRecords(baseQuery);
|
||||
const all: AdminCreditRecord[] = [...(first.items || [])];
|
||||
const exportSummary = { ...DEFAULT_SUMMARY, ...(first.summary || {}) };
|
||||
const exportSummary = first.summary || DEFAULT_SUMMARY;
|
||||
const totalRows = first.total || 0;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / exportPageSize));
|
||||
setExportProgress(`正在获取 ${all.length} / ${totalRows}`);
|
||||
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 || '' },
|
||||
|
||||
const detailColumns: StyledExcelColumn<AdminCreditRecord>[] = [
|
||||
{ title: '时间', maxWidth: 22, render: (r) => formatDate(r.createdAt || '') },
|
||||
{ title: '用户', minWidth: 12, maxWidth: 20, render: (r) => r.username || '-' },
|
||||
{ title: '手机号', minWidth: 13, 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, align: 'center', render: (r) => r.recordTypeLabel || r.type || '-' },
|
||||
{ title: '积分类型', maxWidth: 20, render: (r) => r.creditSubjectLabel || '-' },
|
||||
{ title: '扣费子类', maxWidth: 22, 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: '变动积分', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0.00', render: (r) => r.amount },
|
||||
{ title: '变动后余额', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0.00', render: (r) => r.balanceAfter },
|
||||
{ title: '实际 Token', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0', render: (r) => r.totalTokens || 0 },
|
||||
{ title: '输入 Token', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0', render: (r) => r.inputTokens || 0 },
|
||||
{ title: '输出 Token', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0', render: (r) => r.outputTokens || 0 },
|
||||
{ title: '执行类型', maxWidth: 18, render: (r) => engineTypeLabel(r.engineType) },
|
||||
{ title: '执行配置', maxWidth: 28, render: (r) => r.engineName || '-' },
|
||||
{ title: '供应商', maxWidth: 18, render: (r) => r.engineProvider || '-' },
|
||||
{ title: '模型版本', maxWidth: 26, render: (r) => r.engineModelName || '-' },
|
||||
{ title: '关联状态', maxWidth: 14, align: 'center', render: (r) => r.ownerDeleted ? '关联已删除' : '正常' },
|
||||
{ title: '说明', minWidth: 18, maxWidth: 42, render: (r) => r.description || '' },
|
||||
{ title: '业务归属类型', maxWidth: 18, render: (r) => r.ownerType || '' },
|
||||
{ title: '业务归属ID', maxWidth: 28, 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]],
|
||||
|
||||
const filename = `积分流水_${dayjs().format('YYYYMMDD_HHmmss')}.xlsx`;
|
||||
exportStyledExcel<AdminCreditRecord>({
|
||||
filename,
|
||||
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,
|
||||
['总充值', 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],
|
||||
],
|
||||
columns: detailColumns,
|
||||
rows: all,
|
||||
});
|
||||
message.success('Excel 已导出');
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '导出失败');
|
||||
} finally { setExporting(false); setExportProgress(''); }
|
||||
} 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: 'userTypeLabel', width: 120, render: (_: string, r: AdminCreditRecord) => <Tag color={r.userType === 'admin' ? 'orange' : 'blue'}>{r.userTypeLabel || '-'}</Tag> },
|
||||
{ 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: '流水类型', dataIndex: 'recordType', width: 100, 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) => <Tag>{v || '-'}</Tag> },
|
||||
{ title: '模块', dataIndex: 'sourceModuleLabel', width: 130, render: (v: string) => v || '-' },
|
||||
{ title: '步骤/场景', key: 'scene', width: 210, render: (_: any, r: AdminCreditRecord) => <div><div>{r.billingSceneLabel || '-'}</div><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.sourceStepCodeLabel || '-'}</div></div> },
|
||||
{ title: '媒体', dataIndex: 'mediaTypeLabel', width: 80, 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)}</Typography.Text> },
|
||||
{ title: '余额', dataIndex: 'balanceAfter', width: 110, render: (v: number) => n(v) },
|
||||
{ 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: '执行配置', key: 'engine', width: 230, 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: 100, 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> },
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 160, render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text> },
|
||||
];
|
||||
|
||||
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>
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><Space><ArrowUpOutlined style={{ color: '#10b981', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总充值</div><div style={{ fontSize: 22, fontWeight: 800, color: '#10b981' }}>+{n(summary.totalRecharge)}</div></div></Space></Card>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><Space><ArrowDownOutlined style={{ color: '#ef4444', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总消费</div><div style={{ fontSize: 22, fontWeight: 800, color: '#ef4444' }}>-{n(summary.totalConsume)}</div></div></Space></Card>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><Space><RollbackOutlined style={{ color: '#3b82f6', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总回退</div><div style={{ fontSize: 22, fontWeight: 800, color: '#3b82f6' }}>+{n(summary.totalRefund)}</div></div></Space></Card>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><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>
|
||||
{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>;
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
|
||||
<Card size="small" bordered={false}>图片生成:{n(summary.imageGenerationCount)} 条 / {n(summary.imageConsume)} 积分</Card>
|
||||
<Card size="small" bordered={false}>视频生成:{n(summary.videoGenerationCount)} 条 / {n(summary.videoConsume)} 积分</Card>
|
||||
<Card size="small" bordered={false}>提词消费:{n(summary.textConsume)} 积分</Card>
|
||||
<Card size="small" bordered={false}>视频分析:{n(summary.analysisConsume)} 积分</Card>
|
||||
</div>
|
||||
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<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: 150 }} options={sourceModuleOptions} />
|
||||
<Select value={sourceStepCode} onChange={(v) => { setPage(1); setSourceStepCode(v); }} style={{ width: 150 }} options={sourceStepOptions} />
|
||||
<Select value={billingScene} onChange={(v) => { setPage(1); setBillingScene(v); }} style={{ width: 220 }} options={billingSceneOptions} />
|
||||
<Input placeholder="用户名/手机号/邮箱" value={userNameFilter} onChange={(e) => { setPage(1); setUserNameFilter(e.target.value); }} style={{ width: 180 }} allowClear />
|
||||
<DatePicker.RangePicker value={dateRange} onChange={(dates) => { setPage(1); setDateRange(dates ? [dates[0], dates[1]] : [null, null]); }} placeholder={['开始日期', '结束日期']} style={{ width: 250 }} />
|
||||
</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}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
onChange: (p, ps) => { setPage(p); setPageSize(ps); },
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条记录`,
|
||||
}}
|
||||
scroll={{ x: 2050 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCreditRecords;
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card, Drawer, Input, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
|
||||
import { CopyOutlined, EyeOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
createModelPricingRule,
|
||||
disableModelPricingRule,
|
||||
getModelPricingRules,
|
||||
publishModelPricingRule,
|
||||
updateModelPricingRule,
|
||||
} from '../api';
|
||||
import type { ModelPricingRule, ModelPricingRulePayload } from '../types';
|
||||
import PricingRuleForm from '../components/modelPricing/PricingRuleForm';
|
||||
import PricingRulePreview from '../components/modelPricing/PricingRulePreview';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
draft: { color: 'default', text: '草稿' },
|
||||
published: { color: 'green', text: '已发布' },
|
||||
disabled: { color: 'red', text: '已停用' },
|
||||
};
|
||||
|
||||
const modeMap: Record<string, string> = {
|
||||
text_token_tiered: '文本分档 Token',
|
||||
image_per_output: '按成功输出图片',
|
||||
image_input_output_tiered: '输入图 + 输出像素',
|
||||
video_token_rate: '视频 Token',
|
||||
};
|
||||
|
||||
const AdminModelPricingRules: React.FC = () => {
|
||||
const [rows, setRows] = useState<ModelPricingRule[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [modelName, setModelName] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
const [editing, setEditing] = useState<ModelPricingRule | null>(null);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<ModelPricingRule | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getModelPricingRules({
|
||||
page,
|
||||
pageSize,
|
||||
modelName: modelName || undefined,
|
||||
publishStatus: status || undefined,
|
||||
modelCategory: category || undefined,
|
||||
});
|
||||
setRows(res.items || []);
|
||||
setTotal(res.total || 0);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载模型计价规则失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, [page, pageSize, modelName, status, category]);
|
||||
|
||||
const submit = async (payload: ModelPricingRulePayload) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing?.id && editing.publishStatus === 'draft') {
|
||||
await updateModelPricingRule(editing.id, payload);
|
||||
} else {
|
||||
await createModelPricingRule(payload);
|
||||
}
|
||||
message.success('价格草稿已保存');
|
||||
setFormOpen(false);
|
||||
setEditing(null);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cloneRule = (rule: ModelPricingRule) => {
|
||||
setEditing({
|
||||
...rule,
|
||||
id: '',
|
||||
publishStatus: 'draft',
|
||||
versionCode: `${rule.versionCode}_copy_${dayjs().format('YYYYMMDDHHmm')}`,
|
||||
effectiveFrom: dayjs().add(1, 'minute').toISOString(),
|
||||
effectiveTo: null,
|
||||
referencedCount: 0,
|
||||
});
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '模型', dataIndex: 'modelName', width: 280, fixed: 'left' as const, render: (v: string, r: ModelPricingRule) => <div><Typography.Text strong>{v}</Typography.Text><div style={{ color: '#94a3b8', fontSize: 12 }}>{r.provider} / {r.modelCategory}</div></div> },
|
||||
{ title: '价格版本', dataIndex: 'versionCode', width: 180 },
|
||||
{ title: '计价模式/计算器', key: 'calculator', width: 230, render: (_: any, r: ModelPricingRule) => <div>{modeMap[r.billingMode] || r.billingMode}<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.calculatorVersion}</div></div> },
|
||||
{ title: '生效时间', key: 'effective', width: 290, render: (_: any, r: ModelPricingRule) => <div>{formatDate(r.effectiveFrom)}<div style={{ color: '#94a3b8', fontSize: 12 }}>至 {r.effectiveTo ? formatDate(r.effectiveTo) : '长期有效'}</div></div> },
|
||||
{ title: '状态', dataIndex: 'publishStatus', width: 100, render: (v: string) => <Tag color={(statusMap[v] || {}).color}>{(statusMap[v] || {}).text || v}</Tag> },
|
||||
{ title: '规则Hash/引用', key: 'hash', width: 190, render: (_: any, r: ModelPricingRule) => <div>{r.ruleContentHash ? `${r.ruleContentHash.slice(0, 12)}…` : '-'}<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.referencedCount || 0} 条引用</div></div> },
|
||||
{ title: '来源更新时间', dataIndex: 'sourceUpdatedAt', width: 170, render: (v: string) => v ? formatDate(v) : '-' },
|
||||
{ title: '操作', key: 'action', width: 310, fixed: 'right' as const, render: (_: any, r: ModelPricingRule) => <Space>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => setDetail(r)}>详情/试算</Button>
|
||||
{r.publishStatus === 'draft' && <Button size="small" onClick={() => { setEditing(r); setFormOpen(true); }}>编辑</Button>}
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => cloneRule(r)}>克隆新版本</Button>
|
||||
{r.publishStatus === 'draft' && <Popconfirm title="发布后价格正文不可修改,确认发布?" onConfirm={async () => { await publishModelPricingRule(r.id); message.success('已发布'); load(); }}><Button size="small" type="primary">发布</Button></Popconfirm>}
|
||||
{r.publishStatus === 'published' && <Popconfirm title="停用后不再匹配新消费,历史快照不受影响。确认?" onConfirm={async () => { await disableModelPricingRule(r.id); message.success('已停用'); load(); }}><Button size="small" danger>停用</Button></Popconfirm>}
|
||||
</Space> },
|
||||
];
|
||||
|
||||
return <div>
|
||||
<Card bordered={false} style={{ borderRadius: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<Space wrap>
|
||||
<Input allowClear placeholder="模型名称" value={modelName} onChange={e => { setPage(1); setModelName(e.target.value); }} style={{ width: 260 }} />
|
||||
<Select value={category} onChange={v => { setPage(1); setCategory(v); }} style={{ width: 130 }} options={[{ value: '', label: '全部类型' }, { value: 'text', label: '文本' }, { value: 'image', label: '图片' }, { value: 'video', label: '视频' }]} />
|
||||
<Select value={status} onChange={v => { setPage(1); setStatus(v); }} style={{ width: 130 }} options={[{ value: '', label: '全部状态' }, { value: 'draft', label: '草稿' }, { value: 'published', label: '已发布' }, { value: 'disabled', label: '已停用' }]} />
|
||||
</Space>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); setFormOpen(true); }}>新增价格版本</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table rowKey="id" columns={columns} dataSource={rows} loading={loading} scroll={{ x: 1500 }} pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
||||
</Card>
|
||||
|
||||
<Modal open={formOpen} title={editing?.id ? '编辑价格草稿' : editing ? '克隆价格版本' : '新增价格版本'} width={1100} footer={null} destroyOnClose onCancel={() => { setFormOpen(false); setEditing(null); }}>
|
||||
<PricingRuleForm initial={editing} loading={saving} onSubmit={submit} onCancel={() => { setFormOpen(false); setEditing(null); }} />
|
||||
</Modal>
|
||||
|
||||
<Drawer open={!!detail} width={760} title={detail ? `${detail.modelName} / ${detail.versionCode}` : '计价详情'} onClose={() => setDetail(null)}>
|
||||
{detail && <>
|
||||
<Space wrap style={{ marginBottom: 12 }}><Tag>{detail.provider}</Tag><Tag>{detail.modelCategory}</Tag><Tag color="blue">{modeMap[detail.billingMode] || detail.billingMode}</Tag><Tag color={(statusMap[detail.publishStatus] || {}).color}>{(statusMap[detail.publishStatus] || {}).text}</Tag></Space>
|
||||
<Typography.Paragraph>生效:{formatDate(detail.effectiveFrom)} ~ {detail.effectiveTo ? formatDate(detail.effectiveTo) : '长期有效'}<br />计算器:{detail.calculatorVersion}<br />规则 Hash:{detail.ruleContentHash || '-'}</Typography.Paragraph>
|
||||
<Typography.Paragraph>来源:{detail.sourceUrl || '-'}<br />官方更新时间:{detail.sourceUpdatedAt ? formatDate(detail.sourceUpdatedAt) : '-'}</Typography.Paragraph>
|
||||
<pre style={{ background: '#f7f8fa', borderRadius: 8, padding: 12, overflow: 'auto' }}>{JSON.stringify(detail.ruleJson, null, 2)}</pre>
|
||||
<PricingRulePreview billingMode={detail.billingMode} calculatorVersion={detail.calculatorVersion} ruleJson={detail.ruleJson} />
|
||||
</>}
|
||||
</Drawer>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default AdminModelPricingRules;
|
||||
@@ -796,20 +796,6 @@ export interface AdminCreditRecordSummary {
|
||||
totalTokens: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
attachmentImageCount: number;
|
||||
attachmentVideoCount: number;
|
||||
attachmentAudioCount: number;
|
||||
attachmentTotalCount: number;
|
||||
generatedImageCount: number;
|
||||
generatedVideoCount: number;
|
||||
generatedTotalCount: number;
|
||||
providerCostCalculatedTotal: string;
|
||||
providerCostEstimatedTotal: string;
|
||||
providerCostCombinedTotal: string;
|
||||
providerCostTotal: string;
|
||||
providerCostPendingCount: number;
|
||||
providerCostEstimatedCount: number;
|
||||
providerCostAbnormalCount: number;
|
||||
}
|
||||
|
||||
export interface AdminCreditRecord {
|
||||
@@ -862,38 +848,6 @@ export interface AdminCreditRecord {
|
||||
engineName?: string;
|
||||
engineProvider?: string;
|
||||
engineModelName?: string;
|
||||
pricingRuleId?: string;
|
||||
pricingVersionCode?: string;
|
||||
pricingBillingMode?: string;
|
||||
pricingBillingModeLabel?: string;
|
||||
pricingCalculatorVersion?: ModelPricingCalculatorVersion;
|
||||
pricingUsageSource?: string;
|
||||
pricingReferenceAt?: string;
|
||||
pricingEffectiveFrom?: string;
|
||||
pricingEffectiveTo?: string;
|
||||
pricingSnapshotHash?: string;
|
||||
providerCostCurrency?: string;
|
||||
providerCostAmount?: string;
|
||||
providerCostStatus?: string;
|
||||
providerCostStatusLabel?: string;
|
||||
providerCostCalculatedAt?: string;
|
||||
providerCostFinalizedAt?: string;
|
||||
providerCostIsEstimated?: boolean;
|
||||
providerUsagePrimary?: boolean;
|
||||
attachmentImageCount: number;
|
||||
attachmentVideoCount: number;
|
||||
attachmentAudioCount: number;
|
||||
attachmentTotalCount: number;
|
||||
attachmentVideoDurationSeconds?: string;
|
||||
attachmentAudioDurationSeconds?: string;
|
||||
requestedOutputCount: number;
|
||||
generatedImageCount: number;
|
||||
generatedVideoCount: number;
|
||||
generatedTotalCount: number;
|
||||
pricingSnapshotJson?: Record<string, any> | null;
|
||||
usageSnapshotJson?: Record<string, any> | null;
|
||||
attachmentSnapshotJson?: Record<string, any> | null;
|
||||
generationSnapshotJson?: Record<string, any> | null;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
@@ -919,83 +873,10 @@ export interface AdminCreditRecordQueryParams {
|
||||
sourceModule?: string;
|
||||
sourceStepCode?: string;
|
||||
billingScene?: string;
|
||||
engineProvider?: string;
|
||||
engineModelName?: string;
|
||||
pricingVersionCode?: string;
|
||||
providerCostStatus?: string;
|
||||
providerCostIsEstimated?: boolean;
|
||||
hasAttachment?: boolean;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
export type ModelPricingCategory = 'text' | 'image' | 'video';
|
||||
export type ModelPricingBillingMode =
|
||||
| 'text_token_tiered'
|
||||
| 'image_per_output'
|
||||
| 'image_input_output_tiered'
|
||||
| 'video_token_rate';
|
||||
export type ModelPricingRuleStatus = 'draft' | 'published' | 'disabled';
|
||||
export type ModelPricingCalculatorVersion =
|
||||
| 'text_token_tiered_v1'
|
||||
| 'image_per_output_v1'
|
||||
| 'image_input_output_tiered_v1'
|
||||
| 'video_pixel_token_v1';
|
||||
|
||||
export interface ModelPricingRule {
|
||||
id: string;
|
||||
provider: string;
|
||||
modelName: string;
|
||||
modelCategory: ModelPricingCategory;
|
||||
billingMode: ModelPricingBillingMode;
|
||||
calculatorVersion: ModelPricingCalculatorVersion;
|
||||
versionCode: string;
|
||||
effectiveFrom: string;
|
||||
effectiveTo?: string | null;
|
||||
publishStatus: ModelPricingRuleStatus;
|
||||
currency: string;
|
||||
ruleSchemaVersion: number;
|
||||
ruleContentHash: string;
|
||||
ruleJson: Record<string, any>;
|
||||
sourceUrl?: string | null;
|
||||
sourceUpdatedAt?: string | null;
|
||||
remark?: string | null;
|
||||
referencedCount: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ModelPricingRulePayload {
|
||||
provider: string;
|
||||
model_name: string;
|
||||
model_category: ModelPricingCategory;
|
||||
billing_mode: ModelPricingBillingMode;
|
||||
calculator_version: ModelPricingCalculatorVersion;
|
||||
version_code: string;
|
||||
effective_from: string;
|
||||
effective_to?: string | null;
|
||||
currency: string;
|
||||
rule_schema_version: number;
|
||||
rule_json: Record<string, any>;
|
||||
source_url?: string | null;
|
||||
source_updated_at?: string | null;
|
||||
remark?: string | null;
|
||||
}
|
||||
|
||||
export interface ModelPricingRuleListResponse {
|
||||
items: ModelPricingRule[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ModelPricingPreviewResponse {
|
||||
amount: string;
|
||||
currency: string;
|
||||
isEstimated: boolean;
|
||||
selectedRate?: string | null;
|
||||
usageSource: string;
|
||||
breakdown: Record<string, any>;
|
||||
}
|
||||
|
||||
// ── 首页素材行业装修 ──────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/components/preresultdisplay.tsx","./src/components/modelpricing/pricingruleform.tsx","./src/components/modelpricing/pricingrulepreview.tsx","./src/pages/adminauthoriz.tsx","./src/pages/adminconsume.tsx","./src/pages/admincontactrequests.tsx","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminhomematerials.tsx","./src/pages/adminhotopeningreplicationdetail.tsx","./src/pages/adminhotopeningreplications.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmateriallist.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodelpricingrules.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoauthlist.tsx","./src/pages/adminoauthapplist.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminpaymentstats.tsx","./src/pages/adminplatform.tsx","./src/pages/adminpretesttemplates.tsx","./src/pages/adminprivateportraitprojects.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminreplicationprojectdetail.tsx","./src/pages/adminsettings.tsx","./src/pages/adminshotreplications.tsx","./src/pages/adminshottasksetdetail.tsx","./src/pages/adminteams.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/pages/adminvideopromptschemaconfig.tsx","./src/pages/adminreplication/components/jsoncollapse.tsx","./src/pages/adminreplication/components/mediapreview.tsx","./src/pages/adminreplication/components/statustag.tsx","./src/pages/adminreplication/components/videopromptschemaviewer.tsx","./src/pages/homematerials/homematerialassettable.tsx","./src/pages/homematerials/homematerialcategorypanel.tsx","./src/pages/homematerials/homematerialuploadmodal.tsx","./src/pages/homematerials/mediareferenceseditor.tsx","./src/pages/homematerials/watermarkeditor.tsx","./src/pages/homematerials/watermarklibrarymodal.tsx","./src/pages/homematerials/watermarkpreview.tsx","./src/store/index.ts","./src/types/index.ts","./src/types/xlsx-js-style.d.ts","./src/utils/excelexport.ts","./src/utils/formatdate.ts","./src/utils/resourceurl.ts","./src/utils/videopromptschema.ts"],"version":"6.0.3"}
|
||||
{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/components/preresultdisplay.tsx","./src/pages/adminauthoriz.tsx","./src/pages/adminconsume.tsx","./src/pages/admincontactrequests.tsx","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminhomematerials.tsx","./src/pages/adminhotopeningreplicationdetail.tsx","./src/pages/adminhotopeningreplications.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmateriallist.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoauthlist.tsx","./src/pages/adminoauthapplist.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminpaymentstats.tsx","./src/pages/adminplatform.tsx","./src/pages/adminpretesttemplates.tsx","./src/pages/adminprivateportraitprojects.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminreplicationprojectdetail.tsx","./src/pages/adminsettings.tsx","./src/pages/adminshotreplications.tsx","./src/pages/adminshottasksetdetail.tsx","./src/pages/adminteams.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/pages/adminvideopromptschemaconfig.tsx","./src/pages/adminreplication/components/jsoncollapse.tsx","./src/pages/adminreplication/components/mediapreview.tsx","./src/pages/adminreplication/components/statustag.tsx","./src/pages/adminreplication/components/videopromptschemaviewer.tsx","./src/pages/homematerials/homematerialassettable.tsx","./src/pages/homematerials/homematerialcategorypanel.tsx","./src/pages/homematerials/homematerialuploadmodal.tsx","./src/pages/homematerials/mediareferenceseditor.tsx","./src/pages/homematerials/watermarkeditor.tsx","./src/pages/homematerials/watermarklibrarymodal.tsx","./src/pages/homematerials/watermarkpreview.tsx","./src/store/index.ts","./src/types/index.ts","./src/types/xlsx-js-style.d.ts","./src/utils/excelexport.ts","./src/utils/formatdate.ts","./src/utils/resourceurl.ts","./src/utils/videopromptschema.ts"],"version":"6.0.3"}
|
||||
Reference in New Issue
Block a user