1
This commit is contained in:
Vendored
+132
-132
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-Dhv0zQu5.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BLkGzXo7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -618,28 +618,26 @@ export async function getOpenTypeAll(): Promise<{ data: OpenTypeItem[] }> {
|
||||
// ── Generation Records (Admin) ─────────────────────────────
|
||||
|
||||
export async function getAdminGenerationRecords(params?: {
|
||||
userId?: string; status?: string; page?: number; pageSize?: number;
|
||||
userId?: string;
|
||||
status?: string;
|
||||
engineId?: string;
|
||||
includeMediaReferences?: boolean;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<{ total: number; items: any[] }> {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.userId) q.set('user_id', params.userId);
|
||||
if (params?.status) q.set('status', params.status);
|
||||
if (params?.engineId) q.set('engine_id', params.engineId);
|
||||
if (params?.includeMediaReferences !== undefined) {
|
||||
q.set('include_media_references', String(params.includeMediaReferences));
|
||||
}
|
||||
if (params?.page) q.set('page', String(params.page));
|
||||
if (params?.pageSize) q.set('page_size', String(params.pageSize));
|
||||
const qs = q.toString();
|
||||
return api.get(`/admin/generation-records${qs ? `?${qs}` : ''}`);
|
||||
}
|
||||
|
||||
export async function adminUpdateGenerationStatus(
|
||||
recordId: string, status: string, videoUrl?: string
|
||||
): Promise<void> {
|
||||
await api.put(`/admin/generation-records/${recordId}/status`, { status, video_url: videoUrl });
|
||||
}
|
||||
|
||||
export async function adminGenerateVideo(
|
||||
recordId: string, aspectRatio: string, resolution: string, image_size: string
|
||||
): Promise<void> {
|
||||
await api.post(`/admin/generation-records/${recordId}/generate`, { aspect_ratio: aspectRatio, resolution, image_size });
|
||||
}
|
||||
|
||||
// ── Generation AI Engines (Admin) ─────────────────────────────
|
||||
|
||||
@@ -685,8 +683,13 @@ export async function getAdminHotOpeningTasks(params?: AdminHotOpeningTaskQueryP
|
||||
return api.get<HotOpeningTaskListOut>(`/hot-opening-replications/tasks${qs ? `?${qs}` : ''}`);
|
||||
}
|
||||
|
||||
export async function getAdminHotOpeningTaskDetail(projectId: string): Promise<ReplicationProjectDetailOut> {
|
||||
return api.get<ReplicationProjectDetailOut>(`/hot-opening-replications/tasks/${projectId}`);
|
||||
export async function getAdminHotOpeningTaskDetail(
|
||||
projectId: string,
|
||||
flowVersion: 'v1' | 'v2',
|
||||
): Promise<ReplicationProjectDetailOut> {
|
||||
return flowVersion === 'v2'
|
||||
? api.get<ReplicationProjectDetailOut>(`/v2/hot-opening-replications/tasks/${projectId}`)
|
||||
: api.get<ReplicationProjectDetailOut>(`/hot-opening-replications/tasks/${projectId}`);
|
||||
}
|
||||
|
||||
export async function getAdminShotTaskSets(params?: AdminShotTaskSetQueryParams): Promise<ShotTaskSetListOut> {
|
||||
@@ -725,8 +728,13 @@ export async function getAdminShotSegmentDetail(segmentId: string): Promise<Shot
|
||||
return api.get<ShotSegmentDetailOut>(`/shot-replications/segments/${segmentId}`);
|
||||
}
|
||||
|
||||
export async function getAdminShotProjectDetail(projectId: string): Promise<ReplicationProjectDetailOut> {
|
||||
return api.get<ReplicationProjectDetailOut>(`/shot-replications/projects/${projectId}`);
|
||||
export async function getAdminShotProjectDetail(
|
||||
projectId: string,
|
||||
flowVersion: 'v1' | 'v2',
|
||||
): Promise<ReplicationProjectDetailOut> {
|
||||
return flowVersion === 'v2'
|
||||
? api.get<ReplicationProjectDetailOut>(`/v2/shot-replications/projects/${projectId}`)
|
||||
: api.get<ReplicationProjectDetailOut>(`/shot-replications/projects/${projectId}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,10 +22,9 @@ import {
|
||||
CloseCircleOutlined,
|
||||
SearchOutlined,
|
||||
VideoCameraOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
FileImageOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getAdminGenerationRecords, adminUpdateGenerationStatus, adminGenerateVideo } from '../api';
|
||||
import { getAdminGenerationRecords, getVideoEngines, getImageEngines } from '../api';
|
||||
import type { AdminGenerationRecord, GenerationAIMediaReference } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
|
||||
@@ -194,13 +193,14 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
const [pageSize] = useState(20);
|
||||
const [filterStatus, setFilterStatus] = useState<string>('');
|
||||
const [filterUserId, setFilterUserId] = useState<string>('');
|
||||
const [filterEngineId, setFilterEngineId] = useState<string>('');
|
||||
const [filterIncludeMedia, setFilterIncludeMedia] = useState<'' | 'true' | 'false'>('');
|
||||
const [engineOptions, setEngineOptions] = useState<Array<{ value: string; label: string }>>([]);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [preview, setPreview] = useState<AdminGenerationRecord | null>(null);
|
||||
const [resourceState, setResourceState] = useState<PreviewResourceState>(EMPTY_RESOURCE_STATE);
|
||||
const [videoPlaying, setVideoPlaying] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [updating, setUpdating] = useState<string | null>(null);
|
||||
const [genModal, setGenModal] = useState<{ record: AdminGenerationRecord; ratio: string; resolution: string; image_size: string } | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -208,6 +208,8 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
const res = await getAdminGenerationRecords({
|
||||
userId: filterUserId.trim() || undefined,
|
||||
status: filterStatus || undefined,
|
||||
engineId: filterEngineId || undefined,
|
||||
includeMediaReferences: filterIncludeMedia === '' ? undefined : filterIncludeMedia === 'true',
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
@@ -240,6 +242,10 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
imageTokensUsed: item.imageTokensUsed || 0,
|
||||
imageProportion: item.imageProportion,
|
||||
imagePx: item.imagePx,
|
||||
engineId: item.engineId,
|
||||
engineName: item.engineName,
|
||||
engineSnapshot: item.engineSnapshot,
|
||||
includeMediaReferences: item.includeMediaReferences,
|
||||
})));
|
||||
setTotal(res.total || 0);
|
||||
} catch {
|
||||
@@ -247,12 +253,32 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filterStatus, filterUserId, page, pageSize]);
|
||||
}, [filterStatus, filterUserId, filterEngineId, filterIncludeMedia, page, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load, reloadKey]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getImageEngines({ includeDeleted: true }),
|
||||
getVideoEngines({ includeDeleted: true }),
|
||||
])
|
||||
.then(([imageEngines, videoEngines]) => {
|
||||
const items = [...(imageEngines || []), ...(videoEngines || [])];
|
||||
const seen = new Set<string>();
|
||||
setEngineOptions(items.reduce<Array<{ value: string; label: string }>>((acc, item: any) => {
|
||||
const id = String(item?.id || '');
|
||||
if (!id || seen.has(id)) return acc;
|
||||
seen.add(id);
|
||||
const deletedSuffix = item?.deletedAt ? '(已删除)' : '';
|
||||
acc.push({ value: id, label: item?.name ? `${item.name}${deletedSuffix} (${id})` : `${id}${deletedSuffix}` });
|
||||
return acc;
|
||||
}, []));
|
||||
})
|
||||
.catch(() => setEngineOptions([]));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!preview) {
|
||||
setResourceState(EMPTY_RESOURCE_STATE);
|
||||
@@ -349,33 +375,7 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleStatusUpdate = async (recordId: string, newStatus: string, videoUrl?: string) => {
|
||||
setUpdating(recordId);
|
||||
try {
|
||||
await adminUpdateGenerationStatus(recordId, newStatus, videoUrl);
|
||||
message.success('状态已更新');
|
||||
load();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '更新失败');
|
||||
} finally {
|
||||
setUpdating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!genModal) return;
|
||||
setUpdating(genModal.record.id);
|
||||
try {
|
||||
await adminGenerateVideo(genModal.record.id, genModal.ratio, genModal.resolution, genModal.image_size);
|
||||
message.success(`已提交${genModal.record.genType === 'video' ? '视频' : '图片'}生成`);
|
||||
setGenModal(null);
|
||||
load();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '生成失败');
|
||||
} finally {
|
||||
setUpdating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
@@ -416,21 +416,27 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
title: '参数', key: 'params', width: 160,
|
||||
render: (_: any, r: AdminGenerationRecord) => (
|
||||
r.genType === 'video' ? (
|
||||
r.duration || r.aspectRatio || r.resolution ? (
|
||||
<Space size={4} wrap>
|
||||
{r.duration ? <Tag>{r.duration}s</Tag> : null}
|
||||
{r.aspectRatio ? <Tag>{r.aspectRatio}</Tag> : null}
|
||||
{r.resolution ? <Tag>{r.resolution}</Tag> : null}
|
||||
</Space>
|
||||
) : <Tag color="default">待配置</Tag>
|
||||
<Space size={4} wrap>
|
||||
{r.duration ? <Tag>{r.duration}s</Tag> : null}
|
||||
{r.aspectRatio ? <Tag>{r.aspectRatio}</Tag> : null}
|
||||
{r.resolution ? <Tag>{r.resolution}</Tag> : null}
|
||||
{!r.duration && !r.aspectRatio && !r.resolution ? <Tag color="default">待配置</Tag> : null}
|
||||
{r.engineName || r.engineId ? <Tag color="purple">{r.engineName || truncateId(r.engineId || '')}</Tag> : null}
|
||||
<Tag color={r.includeMediaReferences ? 'green' : 'default'}>
|
||||
{r.includeMediaReferences ? '携带附件' : '不携带附件'}{r.references?.length ? `(${r.references.length})` : ''}
|
||||
</Tag>
|
||||
</Space>
|
||||
) : (
|
||||
r.imageSize || r.imageProportion || r.imagePx ? (
|
||||
<Space size={4} wrap>
|
||||
{r.imageSize ? <Tag>{r.imageSize}</Tag> : null}
|
||||
{r.imageProportion ? <Tag>{r.imageProportion}</Tag> : null}
|
||||
{r.imagePx ? <Tag>{r.imagePx}</Tag> : null}
|
||||
</Space>
|
||||
) : <Tag color="default">待配置</Tag>
|
||||
<Space size={4} wrap>
|
||||
{r.imageSize ? <Tag>{r.imageSize}</Tag> : null}
|
||||
{r.imageProportion ? <Tag>{r.imageProportion}</Tag> : null}
|
||||
{r.imagePx ? <Tag>{r.imagePx}</Tag> : null}
|
||||
{!r.imageSize && !r.imageProportion && !r.imagePx ? <Tag color="default">待配置</Tag> : null}
|
||||
{r.engineName || r.engineId ? <Tag color="purple">{r.engineName || truncateId(r.engineId || '')}</Tag> : null}
|
||||
<Tag color={r.includeMediaReferences ? 'green' : 'default'}>
|
||||
{r.includeMediaReferences ? '携带附件' : '不携带附件'}{r.references?.length ? `(${r.references.length})` : ''}
|
||||
</Tag>
|
||||
</Space>
|
||||
)
|
||||
),
|
||||
},
|
||||
@@ -467,72 +473,14 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 150, fixed: 'right' as const,
|
||||
title: '操作', key: 'action', width: 90, fixed: 'right' as const,
|
||||
render: (_: any, r: AdminGenerationRecord) => (
|
||||
<Space size={4} wrap>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => handleOpenPreview(r)}>
|
||||
详情
|
||||
</Button>
|
||||
{r.status === 'generating' ? (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
loading={updating === r.id}
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: '确认操作',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: '确定将此记录标记为失败?',
|
||||
onOk: () => handleStatusUpdate(r.id, 'failed'),
|
||||
});
|
||||
}}
|
||||
>
|
||||
标记失败
|
||||
</Button>
|
||||
) : null}
|
||||
{r.status === 'failed' ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
danger
|
||||
loading={updating === r.id}
|
||||
onClick={() => setGenModal({ record: r, ratio: r.aspectRatio || '16:9', resolution: r.resolution || '720p', image_size: r.imageSize || '2K' })}
|
||||
>
|
||||
重试生成
|
||||
</Button>
|
||||
) : null}
|
||||
{r.status === 'prompt_optimized' ? (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={updating === r.id}
|
||||
onClick={() => setGenModal({ record: r, ratio: r.aspectRatio || '16:9', resolution: r.resolution || '720p', image_size: r.imageSize || '2K' })}
|
||||
style={{ background: '#6366f1', border: 'none' }}
|
||||
>
|
||||
生成{r.genType === 'video' ? '视频' : '图片'}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
loading={updating === r.id}
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: '确认操作',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: '确定将此记录标记为失败?',
|
||||
onOk: () => handleStatusUpdate(r.id, 'failed'),
|
||||
});
|
||||
}}
|
||||
>
|
||||
标记失败
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => handleOpenPreview(r)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
], [handleOpenPreview, updating]);
|
||||
], [handleOpenPreview]);
|
||||
|
||||
const previewTypeConfig = preview ? (GEN_TYPE_MAP[preview.genType || ''] || { text: preview.genType || '-', color: 'default', icon: null }) : null;
|
||||
const previewStatusConfig = preview ? (STATUS_MAP[preview.status] || { color: 'default', text: preview.status || '-', icon: null }) : null;
|
||||
@@ -894,6 +842,27 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
{ value: 'failed', label: '失败' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
placeholder="引擎筛选"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 220 }}
|
||||
value={filterEngineId || undefined}
|
||||
onChange={(v) => { setFilterEngineId(v || ''); setPage(1); }}
|
||||
options={engineOptions}
|
||||
/>
|
||||
<Select
|
||||
placeholder="附件状态"
|
||||
allowClear
|
||||
style={{ width: 130 }}
|
||||
value={filterIncludeMedia || undefined}
|
||||
onChange={(v) => { setFilterIncludeMedia((v || '') as '' | 'true' | 'false'); setPage(1); }}
|
||||
options={[
|
||||
{ value: 'true', label: '携带附件' },
|
||||
{ value: 'false', label: '不携带附件' },
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
placeholder="用户ID搜索"
|
||||
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||
@@ -1062,73 +1031,6 @@ const AdminGenerationRecords: React.FC = () => {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Generate modal */}
|
||||
<Modal
|
||||
title={(
|
||||
<Space>
|
||||
{genModal && (genModal.record.genType === 'video' ? <PlayCircleOutlined /> : <FileImageOutlined />)}
|
||||
{genModal && (genModal.record.genType === 'video' ? '生成视频' : '生成图片')}
|
||||
</Space>
|
||||
)}
|
||||
open={!!genModal}
|
||||
onCancel={() => setGenModal(null)}
|
||||
onOk={handleGenerate}
|
||||
okText="提交生成"
|
||||
cancelText="取消"
|
||||
confirmLoading={genModal ? updating === genModal.record.id : false}
|
||||
width={420}
|
||||
>
|
||||
{genModal ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 16 }}>
|
||||
{genModal.record.genType === 'video' ? (
|
||||
<>
|
||||
<div style={{ padding: 12, borderRadius: 10, background: '#f8f9fc' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block' }}>时长</Typography.Text>
|
||||
<Typography.Text strong>{genModal.record.duration || 5}s</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>画面比例</Typography.Text>
|
||||
<Select
|
||||
value={genModal.ratio}
|
||||
onChange={(v) => setGenModal((prev) => (prev ? { ...prev, ratio: v } : null))}
|
||||
style={{ width: '100%' }}
|
||||
options={['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'].map((r) => ({ value: r, label: r }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>分辨率</Typography.Text>
|
||||
<Select
|
||||
value={genModal.resolution}
|
||||
onChange={(v) => setGenModal((prev) => (prev ? { ...prev, resolution: v } : null))}
|
||||
style={{ width: '100%' }}
|
||||
options={['480p', '720p', '1080p'].map((r) => ({ value: r, label: r }))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ padding: 12, borderRadius: 10, background: '#f8f9fc' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block' }}>尺寸</Typography.Text>
|
||||
<Typography.Text strong>{genModal.record.imagePx || '-'}</Typography.Text>
|
||||
</div>
|
||||
<div style={{ padding: 12, borderRadius: 10, background: '#f8f9fc' }}>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', display: 'block' }}>比例</Typography.Text>
|
||||
<Typography.Text strong>{genModal.record.imageProportion || '-'}</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>分辨率</Typography.Text>
|
||||
<Select
|
||||
value={genModal.image_size}
|
||||
onChange={(v) => setGenModal((prev) => (prev ? { ...prev, image_size: v } : null))}
|
||||
style={{ width: '100%' }}
|
||||
options={['2K', '4K'].map((r) => ({ value: r, label: r }))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -207,6 +207,7 @@ const AdminHotOpeningReplications: React.FC = () => {
|
||||
),
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 130, render: (v: string) => <StatusTag status={v} /> },
|
||||
{ title: '流程版本', dataIndex: 'flowVersion', width: 100, render: (v: string) => <Tag color={v === 'v2' ? 'blue' : 'default'}>{String(v || 'v1').toUpperCase()}</Tag> },
|
||||
{ title: '当前步骤', dataIndex: 'currentStepCode', width: 140, render: (v: string) => STEP_MAP[v] || v || '-' },
|
||||
{ title: '图片结果', dataIndex: 'finalImageUrl', width: 90, render: (v: string) => v ? <Tag color="success">有</Tag> : <Tag>无</Tag> },
|
||||
{ title: '视频结果', dataIndex: 'finalVideoUrl', width: 90, render: (v: string) => v ? <Tag color="success">有</Tag> : <Tag>无</Tag> },
|
||||
@@ -218,7 +219,7 @@ const AdminHotOpeningReplications: React.FC = () => {
|
||||
fixed: 'right',
|
||||
width: 110,
|
||||
render: (_, record) => (
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => navigate(`/hot-opening-replications/${record.id}`)}>详情</Button>
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => navigate(`/hot-opening-replications/${record.id}?flow_version=${record.flowVersion === 'v2' ? 'v2' : 'v1'}`)}>详情</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
PlayCircleOutlined,
|
||||
VideoCameraOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { getAdminHotOpeningTaskDetail, getAdminShotProjectDetail } from '../api';
|
||||
import type { ReplicationProjectDetailOut, ReplicationStepOut } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
@@ -36,7 +36,7 @@ interface AdminReplicationProjectDetailProps {
|
||||
moduleType?: ReplicationModuleType;
|
||||
}
|
||||
|
||||
const STEP_ORDER = [
|
||||
const V1_STEP_ORDER = [
|
||||
'material_input',
|
||||
'image_prompt_optimize',
|
||||
'image_generate',
|
||||
@@ -44,6 +44,12 @@ const STEP_ORDER = [
|
||||
'video_generate',
|
||||
];
|
||||
|
||||
const V2_STEP_ORDER = [
|
||||
'material_input',
|
||||
'video_prompt_optimize',
|
||||
'video_generate',
|
||||
];
|
||||
|
||||
const STEP_DESCRIPTIONS: Record<string, string> = {
|
||||
material_input: '参考素材、项目名称和核心内容点',
|
||||
image_prompt_optimize: '图片 AI 提词优化结果',
|
||||
@@ -104,6 +110,8 @@ const renderPromptText = (value?: string | null, empty = '暂无提词') => {
|
||||
const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps> = ({ moduleType = 'shot_replicate' }) => {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
||||
const [detail, setDetail] = useState<ReplicationProjectDetailOut | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -112,15 +120,15 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = moduleType === 'hot_opening_replicate'
|
||||
? await getAdminHotOpeningTaskDetail(projectId)
|
||||
: await getAdminShotProjectDetail(projectId);
|
||||
? await getAdminHotOpeningTaskDetail(projectId, flowVersion)
|
||||
: await getAdminShotProjectDetail(projectId, flowVersion);
|
||||
setDetail(res);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载复刻项目详情失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [moduleType, projectId]);
|
||||
}, [flowVersion, moduleType, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -134,8 +142,10 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
|
||||
const moduleValue = detail?.module || moduleType;
|
||||
const moduleName = getModuleLabel(moduleValue);
|
||||
const isV2 = detail?.flowVersion === 'v2';
|
||||
const effectiveStepOrder = isV2 ? V2_STEP_ORDER : V1_STEP_ORDER;
|
||||
|
||||
const stepItems = useMemo(() => STEP_ORDER.map(code => {
|
||||
const stepItems = useMemo(() => effectiveStepOrder.map(code => {
|
||||
const step = stepsByCode[code];
|
||||
let status: 'wait' | 'process' | 'finish' | 'error' = 'wait';
|
||||
if (step?.status === 'completed') status = 'finish';
|
||||
@@ -146,7 +156,7 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
description: step ? <StatusTag status={step.status} /> : '未创建',
|
||||
status,
|
||||
};
|
||||
}), [stepsByCode]);
|
||||
}), [effectiveStepOrder, stepsByCode]);
|
||||
|
||||
const defaultActiveKeys = useMemo(() => buildDefaultActiveKeys(detail, stepsByCode), [detail, stepsByCode]);
|
||||
|
||||
@@ -242,7 +252,7 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
},
|
||||
{
|
||||
key: 'video_prompt_optimize',
|
||||
label: <StepHeader index={4} stepCode="video_prompt_optimize" step={videoPromptStep} current={detail.currentStepCode === 'video_prompt_optimize'} />,
|
||||
label: <StepHeader index={isV2 ? 2 : 4} stepCode="video_prompt_optimize" step={videoPromptStep} current={detail.currentStepCode === 'video_prompt_optimize'} />,
|
||||
children: (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Descriptions column={3} bordered size="small">
|
||||
@@ -265,7 +275,7 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
},
|
||||
{
|
||||
key: 'video_generate',
|
||||
label: <StepHeader index={5} stepCode="video_generate" step={videoGenerateStep} current={detail.currentStepCode === 'video_generate'} />,
|
||||
label: <StepHeader index={isV2 ? 3 : 5} stepCode="video_generate" step={videoGenerateStep} current={detail.currentStepCode === 'video_generate'} />,
|
||||
children: (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Descriptions column={3} bordered size="small">
|
||||
@@ -300,6 +310,9 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
),
|
||||
},
|
||||
];
|
||||
const visibleCollapseItems = isV2
|
||||
? collapseItems.filter(item => !['image_prompt_optimize', 'image_generate'].includes(String(item.key)))
|
||||
: collapseItems;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
@@ -322,6 +335,8 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
<Descriptions.Item label="用户名">{detail.userName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="标题">{detail.title || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="当前步骤"><Tooltip title={detail.currentStepCode || ''}>{getStepCodeLabel(detail.currentStepCode)}</Tooltip></Descriptions.Item>
|
||||
<Descriptions.Item label="流程版本"><Tag color={isV2 ? 'blue' : 'default'}>{String(detail.flowVersion || 'v1').toUpperCase()}</Tag></Descriptions.Item>
|
||||
<Descriptions.Item label="步骤数量">{detail.stepCount || effectiveStepOrder.length}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态"><StatusTag status={detail.status} /></Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{safeDate(detail.createdAt)}</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">{safeDate(detail.updatedAt)}</Descriptions.Item>
|
||||
@@ -336,9 +351,11 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
|
||||
<Card title="最终结果预览">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 16 }}>
|
||||
<Card size="small" title="最终图片">
|
||||
<MediaPreview type="image" url={detail.finalImageUrl || detail.imageGeneration?.resultImageUrl} height={220} emptyDescription="暂无最终图片" />
|
||||
</Card>
|
||||
{!isV2 ? (
|
||||
<Card size="small" title="最终图片">
|
||||
<MediaPreview type="image" url={detail.finalImageUrl || detail.imageGeneration?.resultImageUrl} height={220} emptyDescription="暂无最终图片" />
|
||||
</Card>
|
||||
) : null}
|
||||
<Card size="small" title="最终视频封面">
|
||||
<MediaPreview type="image" url={detail.finalVideoCoverUrl || detail.videoGeneration?.resultVideoCoverUrl} height={220} emptyDescription="暂无最终视频封面" />
|
||||
</Card>
|
||||
@@ -348,7 +365,7 @@ const AdminReplicationProjectDetail: React.FC<AdminReplicationProjectDetailProps
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Collapse defaultActiveKey={defaultActiveKeys} items={collapseItems} />
|
||||
<Collapse defaultActiveKey={defaultActiveKeys} items={visibleCollapseItems} />
|
||||
|
||||
<Collapse
|
||||
items={[
|
||||
|
||||
@@ -284,9 +284,14 @@ const AdminShotTaskSetDetail: React.FC = () => {
|
||||
width: 220,
|
||||
render: (_, record) => record.moduleProjectId ? (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Button type="link" style={{ padding: 0 }} onClick={() => navigate(`/shot-replications/projects/${record.moduleProjectId}`)}>{shortId(record.moduleProjectId)}</Button>
|
||||
<Button type="link" style={{ padding: 0 }} onClick={() => navigate(`/shot-replications/projects/${record.moduleProjectId}?flow_version=${record.moduleProjectFlowVersion === 'v2' ? 'v2' : 'v1'}`)}>{shortId(record.moduleProjectId)}</Button>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{record.moduleProjectTitle || getStepCodeLabel(record.moduleProjectCurrentStepCode)}</Typography.Text>
|
||||
<StatusTag status={record.moduleProjectStatus} />
|
||||
<Space size={4}>
|
||||
<StatusTag status={record.moduleProjectStatus} />
|
||||
<Tag color={record.moduleProjectFlowVersion === 'v2' ? 'blue' : 'default'}>
|
||||
{String(record.moduleProjectFlowVersion || 'v1').toUpperCase()}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Space>
|
||||
) : <Tag>未创建</Tag>,
|
||||
},
|
||||
@@ -313,7 +318,8 @@ const AdminShotTaskSetDetail: React.FC = () => {
|
||||
<Descriptions.Item label="切割状态"><StatusTag status={segmentDetail.splitStatus} /></Descriptions.Item>
|
||||
<Descriptions.Item label="分析状态"><StatusTag status={segmentDetail.analysisStatus} /></Descriptions.Item>
|
||||
<Descriptions.Item label="复刻状态"><StatusTag status={segmentDetail.replicateStatus} /></Descriptions.Item>
|
||||
<Descriptions.Item label="关联项目">{segmentDetail.moduleProjectId ? <Button type="link" onClick={() => navigate(`/shot-replications/projects/${segmentDetail.moduleProjectId}`)}>{segmentDetail.moduleProjectId}</Button> : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联项目">{segmentDetail.moduleProjectId ? <Button type="link" onClick={() => navigate(`/shot-replications/projects/${segmentDetail.moduleProjectId}?flow_version=${segmentDetail.moduleProjectFlowVersion === 'v2' ? 'v2' : 'v1'}`)}>{segmentDetail.moduleProjectId}</Button> : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="项目流程版本">{segmentDetail.moduleProjectId ? <Tag color={segmentDetail.moduleProjectFlowVersion === 'v2' ? 'blue' : 'default'}>{String(segmentDetail.moduleProjectFlowVersion || 'v1').toUpperCase()}</Tag> : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="片段内容" span={2}>{segmentDetail.segmentContent || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="片段分类">{segmentDetail.segmentCategory || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="片段受众">{segmentDetail.segmentAudience || '-'}</Descriptions.Item>
|
||||
|
||||
@@ -394,6 +394,10 @@ export interface AdminGenerationRecord {
|
||||
imageTokensUsed?: number;
|
||||
imageProportion?: string;
|
||||
imagePx?: string;
|
||||
engineId?: string;
|
||||
engineName?: string;
|
||||
engineSnapshot?: Record<string, unknown> | null;
|
||||
includeMediaReferences?: boolean;
|
||||
}
|
||||
|
||||
export type GenerationAITaskStatus = 'pending' | 'generating' | 'completed' | 'failed' | string;
|
||||
@@ -572,6 +576,9 @@ export interface ReplicationProjectDetailOut {
|
||||
title?: string | null;
|
||||
status: ModuleReplicationStatus;
|
||||
currentStepCode?: string | null;
|
||||
flowVersion?: 'v1' | 'v2' | string | null;
|
||||
stepCount?: number;
|
||||
stepIoSchemaVersion?: string | null;
|
||||
finalImageUrl?: string | null;
|
||||
finalVideoUrl?: string | null;
|
||||
finalVideoCoverUrl?: string | null;
|
||||
@@ -594,6 +601,8 @@ export interface HotOpeningTaskListItemOut {
|
||||
title?: string | null;
|
||||
status: ModuleReplicationStatus;
|
||||
currentStepCode?: string | null;
|
||||
flowVersion?: 'v1' | 'v2' | string | null;
|
||||
stepCount?: number;
|
||||
sourceProjectName?: string | null;
|
||||
targetProjectName?: string | null;
|
||||
coreContentPoint?: string | null;
|
||||
@@ -707,6 +716,7 @@ export interface ShotSegmentOut {
|
||||
moduleProjectTitle?: string | null;
|
||||
moduleProjectStatus?: string | null;
|
||||
moduleProjectCurrentStepCode?: string | null;
|
||||
moduleProjectFlowVersion?: 'v1' | 'v2' | string | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user