- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段); 修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品 - dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开; settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch - 新增用户管理(users API/admin service/改密页) + alembic 022/023/024 - 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example - A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178 lines
7.0 KiB
TypeScript
178 lines
7.0 KiB
TypeScript
'use client';
|
||
/**
|
||
* 屏2 开新任务(熟手 /tasks/new)
|
||
* 使用 NewTaskForm + ConfigPreviewPanel + RecentTasksBar + FlywheelBanner
|
||
*/
|
||
import { useEffect, useState } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { ProgressBar } from '@/components/layout/ProgressBar';
|
||
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, Benchmark } from '@/types/index';
|
||
import { PaginatedResponse } from '@/types/index';
|
||
import { usePreferenceStore } from '@/stores/preferenceStore';
|
||
import { getErrorAction } from '@/types/errors';
|
||
import { ApiError } from '@/types/index';
|
||
|
||
export default function NewTaskPage() {
|
||
const router = useRouter();
|
||
const { context, fetchContextByProduct } = usePreferenceStore();
|
||
const [products, setProducts] = useState<Product[]>([]);
|
||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||
const [benchmarks, setBenchmarks] = useState<Benchmark[]>([]);
|
||
const [form, setForm] = useState<Omit<CreateTaskRequest, 'product_id'>>({
|
||
benchmark_ids: [],
|
||
theme: '',
|
||
text_count: 8,
|
||
image_count: 6,
|
||
track: 'ai',
|
||
need_product_image: true,
|
||
});
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [error, setError] = useState('');
|
||
const [importOpen, setImportOpen] = useState(false);
|
||
const [editingProduct, setEditingProduct] = useState(false); // 提升:供右侧「编辑配置」(运营/组长)触发就地编辑
|
||
|
||
useEffect(() => { loadProducts(); }, []);
|
||
|
||
// 选品变化:拉该产品标杆笔记;切产品时清空已选标杆(避免跨产品串用)
|
||
useEffect(() => {
|
||
if (!selectedProduct) { setBenchmarks([]); return; }
|
||
setForm((f) => ({ ...f, benchmark_ids: [] }));
|
||
api.get<{ items: Benchmark[] }>(`/api/v1/products/${selectedProduct.id}/benchmarks`)
|
||
.then((d) => setBenchmarks(d.items || []))
|
||
.catch(() => setBenchmarks([]));
|
||
}, [selectedProduct]);
|
||
|
||
// 选品变化:拉该产品飞轮偏好上下文,驱动顶部 FlywheelBanner
|
||
useEffect(() => {
|
||
if (!selectedProduct) return;
|
||
fetchContextByProduct(selectedProduct.id);
|
||
}, [selectedProduct?.id]);
|
||
|
||
async function loadProducts() {
|
||
try {
|
||
const data = await api.get<PaginatedResponse<Product>>('/api/v1/products');
|
||
setProducts(data.items);
|
||
if (data.items.length > 0) setSelectedProduct(data.items[0]);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
// #1 内联建品:把新产品并入列表顶部并选中(QuickProductCreate 已含上传产品图)
|
||
function handleProductCreated(p: Product) {
|
||
setProducts((list) => [p, ...list.filter((x) => x.id !== p.id)]);
|
||
setSelectedProduct(p);
|
||
}
|
||
|
||
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 false;
|
||
}
|
||
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);
|
||
router.push(`/tasks/${data.id}/text`);
|
||
} catch (err) {
|
||
const apiErr = err as ApiError;
|
||
setError(apiErr?.message || getErrorAction(apiErr?.code).message);
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
}
|
||
|
||
// 轨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} />
|
||
{context && <FlywheelBanner context={context} />}
|
||
|
||
<div className="flex flex-1 overflow-hidden">
|
||
{/* 主表单区 */}
|
||
<div className="flex-1 p-6 overflow-auto">
|
||
<h2 className="text-xl font-bold text-text-primary mb-6">开新任务</h2>
|
||
<NewTaskForm
|
||
products={products}
|
||
selectedProduct={selectedProduct}
|
||
onSelectProduct={setSelectedProduct}
|
||
onProductCreated={handleProductCreated}
|
||
editingProduct={editingProduct}
|
||
setEditingProduct={setEditingProduct}
|
||
benchmarks={benchmarks}
|
||
form={form}
|
||
onFormChange={(patch) => setForm((f) => ({ ...f, ...patch }))}
|
||
submitting={submitting}
|
||
error={error}
|
||
onSubmitAi={(e) => handleSubmit(e, 'ai')}
|
||
onSubmitImport={openImport}
|
||
/>
|
||
<div className="mt-8"><RecentTasksBar /></div>
|
||
</div>
|
||
|
||
{/* 右侧配置预览面板 */}
|
||
{selectedProduct && <ConfigPreviewPanel product={selectedProduct} onEditInline={() => setEditingProduct(true)} />}
|
||
</div>
|
||
|
||
<ImportTextModal
|
||
open={importOpen}
|
||
submitting={submitting}
|
||
onClose={() => setImportOpen(false)}
|
||
onConfirm={handleImportConfirm}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|