import { useMemo, useState, type FormEvent } from 'react'; import { FileText, Image, Pencil, Plus, RotateCcw, Trash2, Video } from 'lucide-react'; import type { PricingRule, PricingRuleInput, PricingRuleSet, PricingRuleSetUpsertRequest } from '@easyai-ai-gateway/contracts'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, FormDialog, Input, Label, Select } from '../../components/ui'; import type { LoadState } from '../../types'; import { unitLabel } from './PricingEditorControls'; import { createDefaultPricingRules, KeyValueEditor, PricingRuleVisualEditor } from './PricingRuleVisualEditor'; type PricingForm = { ruleSetKey: string; name: string; description: string; category: string; currency: string; status: string; metadata: Record; rules: PricingRuleInput[]; }; const categories = ['default', 'custom', 'provider', 'model']; const statuses = ['active', 'deprecated', 'hidden']; const currencies = ['resource', 'credit', 'cny', 'usd']; export function PricingRulesPanel(props: { message: string; pricingRuleSets: PricingRuleSet[]; state: LoadState; onDeletePricingRuleSet: (ruleSetId: string) => Promise; onSavePricingRuleSet: (input: PricingRuleSetUpsertRequest, ruleSetId?: string) => Promise; }) { const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(''); const [form, setForm] = useState(() => createEmptyForm()); const [localError, setLocalError] = useState(''); const [query, setQuery] = useState(''); const [pendingDeleteRuleSet, setPendingDeleteRuleSet] = useState(null); const filtered = useMemo(() => { const keyword = query.trim().toLowerCase(); if (!keyword) return props.pricingRuleSets; return props.pricingRuleSets.filter((item) => `${item.ruleSetKey} ${item.name} ${item.description ?? ''}`.toLowerCase().includes(keyword)); }, [props.pricingRuleSets, query]); async function submit(event: FormEvent) { event.preventDefault(); setLocalError(''); try { await props.onSavePricingRuleSet(formToPayload(form), editingId || undefined); closeDialog(); } catch (err) { setLocalError(err instanceof Error ? err.message : '定价规则保存失败'); } } function openCreateDialog() { setEditingId(''); setForm(createEmptyForm()); setLocalError(''); setDialogOpen(true); } function editRuleSet(ruleSet: PricingRuleSet) { setEditingId(ruleSet.id); setForm(ruleSetToForm(ruleSet)); setLocalError(''); setDialogOpen(true); } function closeDialog() { setEditingId(''); setForm(createEmptyForm()); setLocalError(''); setDialogOpen(false); } function updateCurrency(currency: string) { setForm({ ...form, currency, rules: ensureRules(form.rules, currency) }); } async function deleteRuleSet(ruleSet: PricingRuleSet) { if (isDefaultRuleSet(ruleSet)) { setLocalError('默认计价规则不能删除。'); return; } try { await props.onDeletePricingRuleSet(ruleSet.id); setPendingDeleteRuleSet(null); if (editingId === ruleSet.id) closeDialog(); } catch (err) { setLocalError(err instanceof Error ? err.message : '定价规则删除失败'); } } return (
定价规则 {props.pricingRuleSets.length}

规则集可包含文本、图像、视频等多个计价条目,平台或平台模型绑定规则集后再通过折扣率整体调价。

setQuery(event.target.value)} placeholder="搜索规则名称 / key" />
{(props.message || localError) &&

{localError || props.message}

}
{filtered.map((ruleSet) => (
{ruleSet.name} {ruleSet.ruleSetKey}
{ruleSet.status}

{ruleSet.description || '未填写说明'}

{ruleSet.category} {ruleSet.currency} {pricingRuleSummaries(ruleSet).length} 条计价规则
{pricingRuleSummaries(ruleSet).map((item) => ( {pricingRuleSummaryIcon(item.kind)}{item.label} {item.value} ))}
))} {!filtered.length && ( 暂无定价规则 )}
)} open={dialogOpen} title={editingId ? '编辑定价规则' : '新增定价规则'} onClose={closeDialog} onSubmit={submit} > setForm({ ...form, rules })} /> setForm({ ...form, metadata })} /> setPendingDeleteRuleSet(null)} onConfirm={() => pendingDeleteRuleSet ? deleteRuleSet(pendingDeleteRuleSet) : undefined} />
); } function createEmptyForm(currency = 'resource'): PricingForm { return { ruleSetKey: '', name: '', description: '', category: 'custom', currency, status: 'active', metadata: {}, rules: createDefaultPricingRules(currency), }; } function ruleSetToForm(ruleSet: PricingRuleSet): PricingForm { return { ruleSetKey: ruleSet.ruleSetKey, name: ruleSet.name, description: ruleSet.description ?? '', category: ruleSet.category, currency: ruleSet.currency, status: ruleSet.status, metadata: ruleSet.metadata ?? {}, rules: (ruleSet.rules ?? []).map(ruleToInput), }; } function ruleToInput(rule: PricingRule): PricingRuleInput { return { ruleKey: rule.ruleKey, displayName: rule.displayName, resourceType: rule.resourceType, unit: rule.unit, basePrice: rule.basePrice, currency: rule.currency, baseWeight: rule.baseWeight ?? {}, dynamicWeight: rule.dynamicWeight ?? {}, calculatorType: rule.calculatorType, dimensionSchema: rule.dimensionSchema ?? {}, formulaConfig: rule.formulaConfig ?? {}, priority: rule.priority, status: rule.status, metadata: rule.metadata ?? {}, }; } function formToPayload(form: PricingForm): PricingRuleSetUpsertRequest { const rules = ensureRules(form.rules, form.currency).map((rule, index) => ({ ...rule, displayName: rule.displayName?.trim() || `${rule.resourceType} 计价`, priority: rule.priority ?? (index + 1) * 10, ruleKey: rule.ruleKey?.trim() || `${rule.resourceType}_${index + 1}`, })); return { ruleSetKey: form.ruleSetKey.trim(), name: form.name.trim(), description: form.description.trim(), category: form.category, currency: form.currency, status: form.status, metadata: form.metadata, rules, }; } type PricingRuleSummary = { kind: 'text' | 'image' | 'video' | 'custom'; label: string; value: string; }; function pricingRuleSummaries(ruleSet: PricingRuleSet): PricingRuleSummary[] { const rules = ruleSet.rules ?? []; const textTotal = rules.find((rule) => rule.resourceType === 'text_total' || rule.ruleKey === 'text'); const textInput = rules.find((rule) => rule.resourceType === 'text_input'); const textCachedInput = rules.find( (rule) => rule.resourceType === 'text_cached_input', ); const textOutput = rules.find((rule) => rule.resourceType === 'text_output'); const image = rules.find((rule) => rule.resourceType === 'image'); const video = rules.find((rule) => rule.resourceType === 'video'); const items: PricingRuleSummary[] = []; if (textTotal || textInput || textCachedInput || textOutput) { const inputPrice = Number(textTotal?.formulaConfig?.inputTokenPrice ?? textInput?.basePrice ?? textTotal?.basePrice ?? 0); const cachedInputPrice = Number( textTotal?.formulaConfig?.cachedInputTokenPrice ?? textTotal?.formulaConfig?.inputCacheHitTokenPrice ?? textCachedInput?.basePrice ?? inputPrice, ); const outputPrice = Number(textTotal?.formulaConfig?.outputTokenPrice ?? textOutput?.basePrice ?? 0); items.push({ kind: 'text', label: '文本', value: `输入 ${formatPrice(inputPrice)} / 缓存输入 ${formatPrice(cachedInputPrice)} / 输出 ${formatPrice(outputPrice)} / 1K Token`, }); } if (image) items.push({ kind: 'image', label: '图像', value: `${formatPrice(image.basePrice)} / ${unitLabel(image.unit)}` }); if (video) items.push({ kind: 'video', label: '视频', value: `${formatPrice(video.basePrice)} / ${unitLabel(video.unit)}` }); if (items.length) return items; return rules.slice(0, 3).map((rule) => ({ kind: 'custom', label: rule.displayName || rule.resourceType, value: `${formatPrice(rule.basePrice)} / ${unitLabel(rule.unit)}`, })); } function pricingRuleSummaryIcon(kind: PricingRuleSummary['kind']) { if (kind === 'image') return ; if (kind === 'video') return