147 lines
8.7 KiB
TypeScript
147 lines
8.7 KiB
TypeScript
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;
|