存量前端:生图分组/灯箱/重生控件/导入文案/驳回横幅+SSE+总核销表

- ImageStrategyGroup/ImageLightbox/RegenControls/ImportTextModal/
  RejectReasonBanner 新建组件
- tasks images/text/new + history + 配置/布局/SSE hooks 存量改动
- 总核销表进度更新

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-18 11:17:53 +08:00
parent d85dcd401b
commit 25f86b2f4a
20 changed files with 741 additions and 60 deletions

View File

@@ -10,6 +10,7 @@ import { FlywheelBanner } from '@/components/FlywheelBanner';
import { ConfigPreviewPanel } from '@/components/tasks/ConfigPreviewPanel';
import { RecentTasksBar } from '@/components/tasks/RecentTasksBar';
import { NewTaskForm } from '@/components/tasks/NewTaskForm';
import { ImportTextModal } from '@/components/tasks/ImportTextModal';
import { api } from '@/lib/api';
import { Product, CreateTaskRequest } from '@/types/index';
import { PaginatedResponse } from '@/types/index';
@@ -32,6 +33,7 @@ export default function NewTaskPage() {
});
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [importOpen, setImportOpen] = useState(false);
useEffect(() => { loadProducts(); }, []);
@@ -45,17 +47,22 @@ export default function NewTaskPage() {
}
}
async function handleSubmit(e: React.FormEvent, track: 'ai' | 'import') {
e.preventDefault();
if (!selectedProduct) { setError('请选择产品'); return; }
if (!form.theme.trim()) { setError('请填写今天主题'); return; }
function validate(): boolean {
if (!selectedProduct) { setError('请选择产品'); return false; }
if (!form.theme.trim()) { setError('请填写今天主题'); return false; }
// 禁降级:产品入镜但该产品没传参考图 → 拦在前端,引导去上传
if (form.need_product_image && !selectedProduct.image_path) {
setError('该产品还没上传参考图,无法生成产品入镜内容。请先到「配置-产品库」上传产品图,或关闭下方「产品入镜」开关。');
return;
return false;
}
setSubmitting(true);
setError('');
return true;
}
async function handleSubmit(e: React.FormEvent, track: 'ai' | 'import') {
e.preventDefault();
if (!validate() || !selectedProduct) return;
setSubmitting(true);
try {
const req: CreateTaskRequest = { product_id: selectedProduct.id, ...form, track };
const data = await api.post<{ id: number }>('/api/v1/tasks', req);
@@ -68,6 +75,43 @@ export default function NewTaskPage() {
}
}
// 轨B先校验产品/主题,通过后开弹窗让用户贴文案
function openImport(e: React.FormEvent) {
e.preventDefault();
if (validate()) setImportOpen(true);
}
// 弹窗确认:建 import 任务 → 逐条导入候选池 → 跳 text 页
async function handleImportConfirm(contents: string[]) {
if (!selectedProduct) return;
setSubmitting(true);
try {
const req: CreateTaskRequest = { product_id: selectedProduct.id, ...form, track: 'import' };
const data = await api.post<{ id: number }>('/api/v1/tasks', req);
// 任务已建。逐条导入;某条失败也不丢任务——跳进已建任务的 text 页,
// 半成品文案在那里可见可续,避免任务 ID 丢失变成找不回的孤儿任务。
let imported = 0;
try {
for (const content of contents) {
await api.post(`/api/v1/tasks/${data.id}/import-text`, { content });
imported += 1;
}
} catch (impErr) {
const ae = impErr as ApiError;
setError(`已导入 ${imported}/${contents.length} 条后中断(${ae?.message || '导入失败'}),已为你保留任务 #${data.id},可在文案页补齐。`);
}
setImportOpen(false);
router.push(`/tasks/${data.id}/text`);
} catch (err) {
// 建任务本身失败(还没有任务 ID
const apiErr = err as ApiError;
setError(apiErr?.message || getErrorAction(apiErr?.code).message);
setImportOpen(false);
} finally {
setSubmitting(false);
}
}
return (
<div className="flex flex-col h-full">
<ProgressBar currentStep={1} />
@@ -86,7 +130,7 @@ export default function NewTaskPage() {
submitting={submitting}
error={error}
onSubmitAi={(e) => handleSubmit(e, 'ai')}
onSubmitImport={(e) => handleSubmit(e, 'import')}
onSubmitImport={openImport}
/>
<div className="mt-8"><RecentTasksBar /></div>
</div>
@@ -94,6 +138,13 @@ export default function NewTaskPage() {
{/* 右侧配置预览面板 */}
{selectedProduct && <ConfigPreviewPanel product={selectedProduct} />}
</div>
<ImportTextModal
open={importOpen}
submitting={submitting}
onClose={() => setImportOpen(false)}
onConfirm={handleImportConfirm}
/>
</div>
);
}