All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m0s
- 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>
148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
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',
|
||
};
|