交易流水对账明细 | AI引擎计价规则

This commit is contained in:
2026-07-10 16:57:23 +08:00
parent ad2e40bc67
commit 468184d352
50 changed files with 6998 additions and 506 deletions
@@ -0,0 +1,146 @@
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;