video-shuoshan/web/src/components/RecordDetailModal.tsx
seaislee1209 b50ad147cd
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m0s
feat: v0.15.0 Seedance 2.0 Fast 模型上线 + 四档计费
- Fast 模型:取消隐藏 Toolbar 选项,用户可选 AirDrama / AirDrama Fast
- 四档计费:按模型+有无视频参考选单价(2.0: 46/28, Fast: 37/22 元/百万tokens)
- QuotaConfig 新增 base_token_price_fast / base_token_price_fast_video 字段
- 系统设置页 4 个价格输入框(Seedance 2.0 + Fast 各两个)
- 前端预估动态选价:根据当前选的模型和有无视频参考实时计算
- 推理接入点:Fast EP ep-m-20260329211530-68999
- 消费记录表格+CSV+详情弹窗加"模型"列
- 轮询间隔改为全程固定 5 秒

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:33:02 +08:00

148 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { AdminRecord } from '../types';
import { ReferenceList } from './ReferenceList';
const STATUS_MAP: Record<string, { label: string; color: string; bg: string }> = {
completed: { label: '已完成', color: '#00b894', bg: 'rgba(0,184,148,0.15)' },
failed: { label: '失败', color: '#e74c3c', bg: 'rgba(231,76,60,0.15)' },
processing: { label: '生成中', color: '#00b8e6', bg: 'rgba(0,184,230,0.15)' },
queued: { label: '排队中', color: '#00b8e6', bg: 'rgba(0,184,230,0.15)' },
};
const MODE_MAP: Record<string, string> = { universal: '全能参考', keyframe: '首尾帧' };
interface Props {
record: AdminRecord;
onClose: () => void;
showTeam?: boolean;
showCost?: boolean;
}
export function RecordDetailModal({ record: r, onClose, showTeam, showCost }: Props) {
const st = STATUS_MAP[r.status] || STATUS_MAP.processing;
const elapsed = (() => {
if (!r.completed_at) return '-';
const ms = new Date(r.completed_at).getTime() - new Date(r.created_at).getTime();
if (ms < 0) return '-';
const sec = Math.round(ms / 1000);
if (sec < 60) return `${sec}`;
const min = Math.floor(sec / 60);
const s = sec % 60;
return `${min}${s > 0 ? s + '秒' : ''}`;
})();
const refs = r.reference_urls || [];
return (
<>
<div style={overlay} onClick={onClose}>
<div style={modal} onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div style={header}>
<span style={{ fontSize: 16, fontWeight: 600, color: '#e2e2ea' }}></span>
<button style={closeBtn} onClick={onClose}></button>
</div>
<div style={body}>
{/* Status */}
<div style={{ marginBottom: 16 }}>
<span style={{ ...statusBadge, color: st.color, background: st.bg }}>{st.label}</span>
</div>
{/* Error */}
{r.status === 'failed' && r.error_message && (
<div style={errorBox}>
<div style={{ fontWeight: 500, marginBottom: 4 }}></div>
<div>{r.error_message}</div>
{r.raw_error && r.raw_error !== r.error_message && (
<div style={{ marginTop: 8, fontSize: 11, color: '#888', fontFamily: 'monospace', wordBreak: 'break-all' }}>
{r.raw_error}
</div>
)}
</div>
)}
{/* Info Grid */}
<div style={sectionTitle}></div>
<div style={infoGrid}>
{r.ark_task_id && <InfoItem label="任务ID" value={r.ark_task_id} />}
{r.username && <InfoItem label="用户" value={r.username} />}
{showTeam && r.team_name && <InfoItem label="团队" value={r.team_name} />}
<InfoItem label="提交时间" value={new Date(r.created_at).toLocaleString('zh-CN')} />
<InfoItem label="耗时" value={elapsed} />
<InfoItem label="模型" value={r.model === 'seedance_2.0_fast' ? 'AirDrama Fast' : 'AirDrama'} />
<InfoItem label="模式" value={MODE_MAP[r.mode] || r.mode} />
<InfoItem label="比例" value={r.aspect_ratio || '-'} />
<InfoItem label="时长" value={r.duration != null ? `${r.duration}` : '-'} />
<InfoItem label="Tokens" value={(r.tokens_consumed || 0).toLocaleString()} />
{showCost && <InfoItem label="费用" value={`¥${(r.cost_amount || 0).toFixed(2)}`} />}
{r.seed != null && r.seed !== -1 && <InfoItem label="种子值" value={String(r.seed)} />}
</div>
{/* Prompt */}
<div style={sectionTitle}></div>
<div style={promptBox}>{r.prompt || '(无提示词)'}</div>
{/* References */}
{refs.length > 0 && (
<>
<div style={sectionTitle}>{refs.length}</div>
<ReferenceList references={refs} />
</>
)}
</div>
</div>
</div>
</>
);
}
function InfoItem({ label, value }: { label: string; value: string }) {
return (
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 11, color: '#888', marginBottom: 2 }}>{label}</div>
<div style={{ fontSize: 13, color: '#e2e2ea', wordBreak: 'break-all' }}>{value}</div>
</div>
);
}
// Styles
const overlay: React.CSSProperties = {
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex',
alignItems: 'center', justifyContent: 'center', zIndex: 10000,
};
const modal: React.CSSProperties = {
background: '#111118', border: '1px solid #2a2a38', borderRadius: 12,
width: 560, maxHeight: '80vh', display: 'flex', flexDirection: 'column',
};
const header: React.CSSProperties = {
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '16px 20px', borderBottom: '1px solid #2a2a38',
};
const closeBtn: React.CSSProperties = {
background: 'none', border: 'none', color: '#888', fontSize: 16, cursor: 'pointer',
padding: '4px 8px', borderRadius: 4,
};
const body: React.CSSProperties = {
padding: 20, overflowY: 'auto', flex: 1,
};
const statusBadge: React.CSSProperties = {
padding: '4px 12px', borderRadius: 6, fontSize: 13, fontWeight: 500,
};
const errorBox: React.CSSProperties = {
background: 'rgba(231,76,60,0.08)', border: '1px solid rgba(231,76,60,0.2)',
borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 13, color: '#e74c3c',
};
const sectionTitle: React.CSSProperties = {
fontSize: 12, color: '#888', fontWeight: 500, marginBottom: 8, marginTop: 16,
textTransform: 'uppercase', letterSpacing: 1,
};
const infoGrid: React.CSSProperties = {
display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px 16px',
};
const promptBox: React.CSSProperties = {
background: '#0a0a0f', borderRadius: 8, padding: 12, fontSize: 13,
color: '#ccc', lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-all',
maxHeight: 150, overflowY: 'auto',
};