"use client";
import { startTransition, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import {
downloadCompetitorAnalysisWorkbook,
downloadCompetitorWorkbook,
downloadKeywordAnalysisWorkbook,
downloadKeywordInsightGroupsWorkbook,
downloadKeywordInsightSuggestionsWorkbook,
downloadMarkdownFile,
downloadSalesWorkbook,
} from "@/lib/product-planning/export";
import {
PRODUCT_PLANNING_ADVANCED_DISABLED,
PRODUCT_PLANNING_ADVANCED_DISABLED_MESSAGE,
} from "@/lib/product-planning/advanced-disabled";
import {
parseKeywordInputFile,
parseManualSegmentationFile,
parseQaImportFile,
parseReviewImportFile,
} from "@/lib/product-planning/file-parsers";
import { analyzeKeywordInputs } from "@/lib/product-planning/keyword-segmentation";
import type {
KeywordAnalysisRow,
ProductPlanningAnalyzeResponse,
ProductPlanningCompetitorAnalysisRecord,
ProductPlanningKeywordInsightGroup,
ProductPlanningKeywordInsightResponse,
ProductPlanningKeywordInsightSuggestion,
ProductPlanningMarketplaceItemRef,
ProductPlanningMarketplaceItemsResponse,
ProductPlanningPlatform,
ProductPlanningPropertyRow,
ProductPlanningRowsResponse,
ProductPlanningTaobaoItem,
ProductPlanningTemplate,
ProductPlanningTemplateListResponse,
ProductPlanningTemplateMutationResponse,
TaobaoQaRow,
TaobaoReviewRow,
} from "@/lib/product-planning/types";
import {
extractTaobaoItemId,
parseMarketplaceItemReference,
uniqueBy,
} from "@/lib/product-planning/utils";
type FetchProgressState = {
running: boolean;
total: number;
completed: number;
failed: number;
currentLabel: string;
};
type MarketplaceItemRef = ProductPlanningMarketplaceItemRef;
type CompetitorImageAnalyzeParams = {
item: ProductPlanningTaobaoItem;
imageUrl: string;
imageLabel: string;
imageKey: string;
};
type ProductPlanningSessionState = {
version: 1;
mainTab: string;
manualSegmentationMode: boolean;
categoryWords: string;
keywordRows: KeywordAnalysisRow[];
productInput: string;
minPage: string;
maxPage: string;
reviewRows: TaobaoReviewRow[];
qaRows: TaobaoQaRow[];
resultTab: string;
reviewMarkdown: string | null;
qaMarkdown: string | null;
advancedTab: string;
competitorInput: string;
competitorItems: ProductPlanningTaobaoItem[];
competitorAnalysisRecords: Record<
string,
ProductPlanningCompetitorAnalysisRecord
>;
activeCompetitorAnalysisKey: string | null;
insightKeyword: string;
insightSuggestions: ProductPlanningKeywordInsightSuggestion[];
insightGroups: ProductPlanningKeywordInsightGroup[];
salesInput: string;
salesItems: ProductPlanningTaobaoItem[];
templateSearch: string;
savedSnapshots: ProductPlanningSavedSnapshot[];
activeSnapshotId: string | null;
snapshotSearch: string;
snapshotKindFilter: SnapshotKindFilter;
};
type ProductPlanningSavedSnapshot = {
id: string;
name: string;
kind: "review" | "qa" | "competitor";
title: string;
markdown: string;
createdAt: string;
};
type SnapshotKindFilter = "all" | ProductPlanningSavedSnapshot["kind"];
const PRODUCT_PLANNING_SESSION_KEY = "product-planning-session-v1";
function parseCategoryWords(value: string) {
return value
.split(/[\n,,;;\s]+/)
.map((item) => item.trim())
.filter(Boolean);
}
function ErrorText({ message }: { message: string | null }) {
if (!message) return null;
return
{message}
;
}
function MarkdownPanel({
title,
markdown,
actions,
}: {
title: string;
markdown: string | null;
actions?: import("react").ReactNode;
}) {
if (!markdown) return null;
return (
{title}
{actions ? (
{actions}
) : null}
{markdown}
);
}
function KeywordResultsTable({ rows }: { rows: KeywordAnalysisRow[] }) {
if (rows.length === 0) return null;
return (
| 词语 |
拆分片段 |
标签分类 |
出现次数 |
搜索人气 |
支付转化率 |
{rows.map((row) => (
| {row.keyword} |
{row.segments.join(" / ")}
|
{row.category} |
{row.occurrenceCount} |
{typeof row.searchPopularity === "number"
? row.searchPopularity
: "-"}
|
{typeof row.conversionRate === "number"
? `${(row.conversionRate * 100).toFixed(2)}%`
: "-"}
|
))}
);
}
function ReviewTable({ rows }: { rows: TaobaoReviewRow[] }) {
if (rows.length === 0)
return (
还没有评价数据。
);
return (
| 商品ID |
评论时间 |
评价内容 |
追评 |
SKU |
买家ID |
{rows.map((row, index) => (
| {row.itemId || "-"} |
{row.date || "-"} |
{row.content || "-"}
|
{row.appendComment || "-"}
|
{row.sku || "-"} |
{row.userId || "-"} |
))}
);
}
function QaTable({ rows }: { rows: TaobaoQaRow[] }) {
if (rows.length === 0)
return (
还没有问大家数据。
);
return (
| 商品ID |
问题 |
问题补充 |
回答 |
回答用户 |
回答时间 |
官方 |
{rows.map((row, index) => (
| {row.itemId || "-"} |
{row.question || "-"}
|
{row.questionDetail || "-"}
|
{row.answer || "-"}
|
{row.answerUser || "-"} |
{row.answerDate || "-"} |
{row.isOfficial ? "是" : "否"} |
))}
);
}
function formatInteger(value: number | null | undefined) {
if (typeof value !== "number" || Number.isNaN(value)) return "-";
return value.toLocaleString("zh-CN");
}
function formatPrice(value: number | null | undefined) {
if (typeof value !== "number" || Number.isNaN(value)) return "-";
return `¥${value.toFixed(2)}`;
}
function formatPlatform(platform: ProductPlanningPlatform, isTmall = false) {
if (platform === "jd") return "京东";
return isTmall ? "天猫" : "淘宝";
}
function formatDataSource(value: ProductPlanningTaobaoItem["dataSource"]) {
if (value === "detail-plus") return "详情增强";
if (value === "legacy") return "旧接口兜底";
if (value === "jd") return "京东接口";
return "未标记";
}
function formatPropertyPreview(
properties: ProductPlanningPropertyRow[],
limit = 8
) {
if (properties.length === 0) return "暂无商品参数";
return properties
.slice(0, limit)
.map((entry) => `${entry.name}:${entry.value}`)
.join(" | ");
}
function createSnapshotId() {
if (
typeof crypto !== "undefined" &&
typeof crypto.randomUUID === "function"
) {
return crypto.randomUUID();
}
return `snapshot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function formatDateTime(value: string) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "-";
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function formatFileSize(value: number) {
if (!Number.isFinite(value) || value <= 0) return "-";
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
}
function downloadBlobFile(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
}
function CompetitorTable({
rows,
analyzingItemKey,
analyzingSkuKey,
onAnalyzeImage,
onAnalyzeSku,
onViewAnalysis,
analysisRecords,
activeAnalysisKey,
}: {
rows: ProductPlanningTaobaoItem[];
analyzingItemKey: string | null;
analyzingSkuKey: string | null;
onAnalyzeImage: (params: CompetitorImageAnalyzeParams) => void;
onAnalyzeSku: (item: ProductPlanningTaobaoItem) => void;
onViewAnalysis: (key: string) => void;
analysisRecords: Record;
activeAnalysisKey: string | null;
}) {
if (rows.length === 0) {
return (
还没有竞品数据。
);
}
return (
| 数据来源 |
平台 |
商品ID |
主图 |
标题 |
店铺 |
当前价格 |
划线价格 |
总销量 |
近期销量 |
类目ID |
材质 |
标准品类 |
市场分类 |
SKU概览 |
图数 |
参数预览 |
操作 |
{rows.map((row) => {
const mainImageKey = `${row.platform}:${row.itemId}:main`;
const skuKey = `${row.platform}:${row.itemId}:sku`;
return (
|
{formatDataSource(row.dataSource)}
|
{formatPlatform(row.platform, row.isTmall)}
|
{row.itemId} |
{row.mainImage ? (
) : (
"-"
)}
|
{row.title}
{row.detailUrl ? (
查看商品链接
) : null}
|
{row.shopName || "-"} |
{formatPrice(row.price)} |
{formatPrice(row.originalPrice)} |
{formatInteger(row.totalSales)} |
{formatInteger(row.recentSales)} |
{row.categoryId || "-"} |
{row.material || "-"}
|
{row.standardProductType || "-"} |
{row.marketCategoryName || "-"} |
{row.skus.length > 0 ? (
{row.skus.slice(0, 4).map((sku) => (
{sku.name}:{formatPrice(sku.price)}
))}
) : (
"-"
)}
|
主图 {row.itemImages.length} / 详情 {row.detailImages.length}
|
{formatPropertyPreview(row.properties, 10)}
|
|
);
})}
);
}
function SalesTable({ rows }: { rows: ProductPlanningTaobaoItem[] }) {
if (rows.length === 0) {
return (
还没有销量数据。
);
}
return (
| 平台 |
商品ID |
标题 |
近期销量 |
当前价格 |
店铺 |
发货地 |
材质 |
{rows.map((row) => (
|
{formatPlatform(row.platform, row.isTmall)}
|
{row.itemId} |
{row.title}
{row.detailUrl ? (
查看商品链接
) : null}
|
{formatInteger(row.recentSales)}
|
{formatPrice(row.price)} |
{row.shopName || "-"} |
{row.location || "-"} |
{row.material || "-"} |
))}
);
}
function KeywordInsightSuggestionTable({
rows,
}: {
rows: ProductPlanningKeywordInsightSuggestion[];
}) {
if (rows.length === 0) {
return (
还没有搜索建议数据。
);
}
return (
| 排名 |
关键词 |
权重 |
{rows.map((row) => (
| {row.rank} |
{row.keyword} |
{row.weight.toLocaleString("zh-CN")}
|
))}
);
}
function KeywordInsightGroupTable({
rows,
}: {
rows: ProductPlanningKeywordInsightGroup[];
}) {
if (rows.length === 0) {
return (
还没有标签分组数据。
);
}
return (
| 组序号 |
分组类型 |
标签 |
{rows.map((row) => (
| {row.index} |
{row.type} |
{row.tags.map((tag) => (
{tag.title}
))}
|
))}
);
}
export function ProductPlanningPage({
canManageTemplates = false,
}: {
canManageTemplates?: boolean;
}) {
const sessionHydratedRef = useRef(false);
const [mainTab, setMainTab] = useState("keywords");
const [manualSegmentationMode, setManualSegmentationMode] = useState(false);
const [categoryWords, setCategoryWords] = useState("");
const [keywordRows, setKeywordRows] = useState([]);
const [keywordError, setKeywordError] = useState(null);
const [keywordAnalyzing, setKeywordAnalyzing] = useState(false);
const keywordFileRef = useRef(null);
const manualFileRef = useRef(null);
const [templates, setTemplates] = useState([]);
const [templateError, setTemplateError] = useState(null);
const [templatesLoading, setTemplatesLoading] = useState(false);
const [templateSearch, setTemplateSearch] = useState("");
const [templateUsingId, setTemplateUsingId] = useState(null);
const [templateDownloadingId, setTemplateDownloadingId] = useState<
string | null
>(null);
const [templateUploading, setTemplateUploading] = useState(false);
const [templateDeletingId, setTemplateDeletingId] = useState(
null
);
const [templateCategoryName, setTemplateCategoryName] = useState("");
const [templateDescription, setTemplateDescription] = useState("");
const templateUploadRef = useRef(null);
const [productInput, setProductInput] = useState("");
const [minPage, setMinPage] = useState("1");
const [maxPage, setMaxPage] = useState("3");
const [reviewRows, setReviewRows] = useState([]);
const [qaRows, setQaRows] = useState([]);
const [reviewError, setReviewError] = useState(null);
const [qaError, setQaError] = useState(null);
const [resultTab, setResultTab] = useState("qa");
const [reviewMarkdown, setReviewMarkdown] = useState(null);
const [qaMarkdown, setQaMarkdown] = useState(null);
const [reviewAnalyzing, setReviewAnalyzing] = useState(false);
const [qaAnalyzing, setQaAnalyzing] = useState(false);
const [fetchProgress, setFetchProgress] = useState({
running: false,
total: 0,
completed: 0,
failed: 0,
currentLabel: "",
});
const cancelFetchRef = useRef(false);
const reviewImportRef = useRef(null);
const qaImportRef = useRef(null);
const [advancedTab, setAdvancedTab] = useState("competitor");
const [competitorInput, setCompetitorInput] = useState("");
const [competitorItems, setCompetitorItems] = useState<
ProductPlanningTaobaoItem[]
>([]);
const [competitorError, setCompetitorError] = useState(null);
const [competitorLoading, setCompetitorLoading] = useState(false);
const [competitorImageError, setCompetitorImageError] = useState<
string | null
>(null);
const [competitorImageLoadingId, setCompetitorImageLoadingId] = useState<
string | null
>(null);
const [competitorSkuError, setCompetitorSkuError] = useState(
null
);
const [competitorSkuLoadingKey, setCompetitorSkuLoadingKey] = useState<
string | null
>(null);
const [competitorAnalysisRecords, setCompetitorAnalysisRecords] = useState<
Record
>({});
const [activeCompetitorAnalysisKey, setActiveCompetitorAnalysisKey] =
useState(null);
const [insightKeyword, setInsightKeyword] = useState("");
const [insightSuggestions, setInsightSuggestions] = useState<
ProductPlanningKeywordInsightSuggestion[]
>([]);
const [insightGroups, setInsightGroups] = useState<
ProductPlanningKeywordInsightGroup[]
>([]);
const [insightError, setInsightError] = useState(null);
const [insightLoading, setInsightLoading] = useState(false);
const [salesInput, setSalesInput] = useState("");
const [salesItems, setSalesItems] = useState([]);
const [salesError, setSalesError] = useState(null);
const [salesLoading, setSalesLoading] = useState(false);
const [savedSnapshots, setSavedSnapshots] = useState<
ProductPlanningSavedSnapshot[]
>([]);
const [activeSnapshotId, setActiveSnapshotId] = useState(null);
const [snapshotSearch, setSnapshotSearch] = useState("");
const [snapshotKindFilter, setSnapshotKindFilter] =
useState("all");
async function handleKeywordAnalyze() {
const keywordFile = keywordFileRef.current?.files?.[0];
const manualFile = manualFileRef.current?.files?.[0];
setKeywordError(null);
setKeywordAnalyzing(true);
try {
if (manualSegmentationMode) {
if (!manualFile) {
throw new Error("请先上传手动分词文件");
}
const rows = await parseManualSegmentationFile(manualFile);
startTransition(() => setKeywordRows(rows));
toast.success(`已导入 ${rows.length} 条手动分词结果`);
return;
}
if (!keywordFile) {
throw new Error("请先上传关键词文件");
}
const inputs = await parseKeywordInputFile(keywordFile);
const rows = analyzeKeywordInputs(inputs, {
categoryWords: parseCategoryWords(categoryWords),
});
startTransition(() => setKeywordRows(rows));
toast.success(`已完成 ${inputs.length} 条关键词分析`);
} catch (error) {
const message = error instanceof Error ? error.message : "关键词分析失败";
setKeywordError(message);
toast.error(message);
} finally {
setKeywordAnalyzing(false);
}
}
async function handleKeywordExport() {
if (keywordRows.length === 0) {
toast.error("当前没有可导出的关键词结果");
return;
}
try {
await downloadKeywordAnalysisWorkbook(keywordRows);
toast.success("关键词分析结果已导出");
} catch (error) {
console.error("[ProductPlanning] export failed:", error);
toast.error("导出关键词分析结果失败");
}
}
async function loadTemplates(showToast = false) {
setTemplatesLoading(true);
try {
const response = await fetch("/api/product-planning/templates", {
method: "GET",
cache: "no-store",
});
const payload =
(await response.json()) as ProductPlanningTemplateListResponse;
if (!response.ok || !payload.success) {
throw new Error(payload.error || "模板库加载失败");
}
setTemplateError(null);
setTemplates(payload.templates);
if (showToast) {
toast.success("模板库已刷新");
}
} catch (error) {
const message = error instanceof Error ? error.message : "模板库加载失败";
setTemplateError(message);
if (showToast) {
toast.error(message);
}
} finally {
setTemplatesLoading(false);
}
}
useEffect(() => {
void loadTemplates();
}, []);
useEffect(() => {
if (PRODUCT_PLANNING_ADVANCED_DISABLED && mainTab === "advanced") {
setMainTab("keywords");
toast.info(PRODUCT_PLANNING_ADVANCED_DISABLED_MESSAGE);
}
}, [mainTab]);
useEffect(() => {
try {
const raw = sessionStorage.getItem(PRODUCT_PLANNING_SESSION_KEY);
if (!raw) {
sessionHydratedRef.current = true;
return;
}
const parsed = JSON.parse(
raw
) as Partial | null;
if (!parsed || parsed.version !== 1) {
sessionHydratedRef.current = true;
return;
}
if (typeof parsed.mainTab === "string") {
setMainTab(
PRODUCT_PLANNING_ADVANCED_DISABLED && parsed.mainTab === "advanced"
? "keywords"
: parsed.mainTab
);
}
if (typeof parsed.manualSegmentationMode === "boolean") {
setManualSegmentationMode(parsed.manualSegmentationMode);
}
if (typeof parsed.categoryWords === "string")
setCategoryWords(parsed.categoryWords);
if (Array.isArray(parsed.keywordRows))
setKeywordRows(parsed.keywordRows as KeywordAnalysisRow[]);
if (typeof parsed.productInput === "string")
setProductInput(parsed.productInput);
if (typeof parsed.minPage === "string") setMinPage(parsed.minPage);
if (typeof parsed.maxPage === "string") setMaxPage(parsed.maxPage);
if (Array.isArray(parsed.reviewRows))
setReviewRows(parsed.reviewRows as TaobaoReviewRow[]);
if (Array.isArray(parsed.qaRows))
setQaRows(parsed.qaRows as TaobaoQaRow[]);
if (typeof parsed.resultTab === "string") setResultTab(parsed.resultTab);
if (
typeof parsed.reviewMarkdown === "string" ||
parsed.reviewMarkdown === null
) {
setReviewMarkdown(parsed.reviewMarkdown ?? null);
}
if (typeof parsed.qaMarkdown === "string" || parsed.qaMarkdown === null) {
setQaMarkdown(parsed.qaMarkdown ?? null);
}
if (typeof parsed.advancedTab === "string")
setAdvancedTab(parsed.advancedTab);
if (typeof parsed.competitorInput === "string")
setCompetitorInput(parsed.competitorInput);
if (Array.isArray(parsed.competitorItems)) {
setCompetitorItems(
parsed.competitorItems as ProductPlanningTaobaoItem[]
);
}
if (
parsed.competitorAnalysisRecords &&
typeof parsed.competitorAnalysisRecords === "object" &&
!Array.isArray(parsed.competitorAnalysisRecords)
) {
setCompetitorAnalysisRecords(
parsed.competitorAnalysisRecords as Record<
string,
ProductPlanningCompetitorAnalysisRecord
>
);
}
if (
typeof parsed.activeCompetitorAnalysisKey === "string" ||
parsed.activeCompetitorAnalysisKey === null
) {
setActiveCompetitorAnalysisKey(
parsed.activeCompetitorAnalysisKey ?? null
);
}
if (typeof parsed.insightKeyword === "string")
setInsightKeyword(parsed.insightKeyword);
if (Array.isArray(parsed.insightSuggestions)) {
setInsightSuggestions(
parsed.insightSuggestions as ProductPlanningKeywordInsightSuggestion[]
);
}
if (Array.isArray(parsed.insightGroups)) {
setInsightGroups(
parsed.insightGroups as ProductPlanningKeywordInsightGroup[]
);
}
if (typeof parsed.salesInput === "string")
setSalesInput(parsed.salesInput);
if (Array.isArray(parsed.salesItems)) {
setSalesItems(parsed.salesItems as ProductPlanningTaobaoItem[]);
}
if (typeof parsed.templateSearch === "string")
setTemplateSearch(parsed.templateSearch);
if (Array.isArray(parsed.savedSnapshots)) {
setSavedSnapshots(
parsed.savedSnapshots as ProductPlanningSavedSnapshot[]
);
}
if (
typeof parsed.activeSnapshotId === "string" ||
parsed.activeSnapshotId === null
) {
setActiveSnapshotId(parsed.activeSnapshotId ?? null);
}
if (typeof parsed.snapshotSearch === "string")
setSnapshotSearch(parsed.snapshotSearch);
if (
parsed.snapshotKindFilter === "all" ||
parsed.snapshotKindFilter === "review" ||
parsed.snapshotKindFilter === "qa" ||
parsed.snapshotKindFilter === "competitor"
) {
setSnapshotKindFilter(parsed.snapshotKindFilter);
}
} catch (error) {
console.warn("[ProductPlanning] restore session failed:", error);
} finally {
sessionHydratedRef.current = true;
}
}, []);
async function fetchTemplateBlob(template: ProductPlanningTemplate) {
const response = await fetch(
`/api/product-planning/templates/${encodeURIComponent(template.id)}`,
{
method: "GET",
}
);
if (!response.ok) {
const payload = (await response
.json()
.catch(() => null)) as ProductPlanningTemplateMutationResponse | null;
throw new Error(payload?.error || "模板下载失败");
}
return await response.blob();
}
async function handleTemplateUse(template: ProductPlanningTemplate) {
setTemplateUsingId(template.id);
setKeywordError(null);
try {
const blob = await fetchTemplateBlob(template);
const file = new File([blob], template.fileName, {
type: template.contentType || blob.type || "application/octet-stream",
});
const inputs = await parseKeywordInputFile(file);
const nextCategoryWords = template.categoryName;
const rows = analyzeKeywordInputs(inputs, {
categoryWords: parseCategoryWords(nextCategoryWords),
});
startTransition(() => {
setManualSegmentationMode(false);
setCategoryWords(nextCategoryWords);
setKeywordRows(rows);
});
toast.success(`已加载模板“${template.categoryName}”并完成拆词`);
} catch (error) {
const message = error instanceof Error ? error.message : "模板加载失败";
setKeywordError(message);
toast.error(message);
} finally {
setTemplateUsingId(null);
}
}
async function handleTemplateDownload(template: ProductPlanningTemplate) {
setTemplateDownloadingId(template.id);
try {
const blob = await fetchTemplateBlob(template);
downloadBlobFile(blob, template.fileName);
toast.success(`模板“${template.categoryName}”已开始下载`);
} catch (error) {
const message = error instanceof Error ? error.message : "模板下载失败";
setTemplateError(message);
toast.error(message);
} finally {
setTemplateDownloadingId(null);
}
}
async function handleTemplateUpload() {
const file = templateUploadRef.current?.files?.[0];
if (!file) {
toast.error("请先选择要上传的模板文件");
return;
}
const categoryName = templateCategoryName.trim();
if (!categoryName) {
toast.error("请先填写模板名称");
return;
}
setTemplateUploading(true);
try {
const formData = new FormData();
formData.set("categoryName", categoryName);
formData.set("description", templateDescription.trim());
formData.set("file", file);
const response = await fetch("/api/product-planning/templates", {
method: "POST",
body: formData,
});
const payload =
(await response.json()) as ProductPlanningTemplateMutationResponse;
if (!response.ok || !payload.success || !payload.template) {
throw new Error(payload.error || "模板上传失败");
}
setTemplateCategoryName("");
setTemplateDescription("");
if (templateUploadRef.current) {
templateUploadRef.current.value = "";
}
setTemplateError(null);
await loadTemplates();
toast.success(`模板“${payload.template.categoryName}”上传成功`);
} catch (error) {
const message = error instanceof Error ? error.message : "模板上传失败";
setTemplateError(message);
toast.error(message);
} finally {
setTemplateUploading(false);
}
}
async function handleTemplateDelete(template: ProductPlanningTemplate) {
if (!window.confirm(`确认删除模板“${template.categoryName}”?`)) {
return;
}
setTemplateDeletingId(template.id);
try {
const response = await fetch(
`/api/product-planning/templates/${encodeURIComponent(template.id)}`,
{
method: "DELETE",
}
);
const payload =
(await response.json()) as ProductPlanningTemplateMutationResponse;
if (!response.ok || !payload.success) {
throw new Error(payload.error || "模板删除失败");
}
if (templateUsingId === template.id) {
setTemplateUsingId(null);
}
setTemplateError(null);
await loadTemplates();
toast.success(`模板“${template.categoryName}”已删除`);
} catch (error) {
const message = error instanceof Error ? error.message : "模板删除失败";
setTemplateError(message);
toast.error(message);
} finally {
setTemplateDeletingId(null);
}
}
function cancelBatchFetch() {
cancelFetchRef.current = true;
setFetchProgress((current) => ({
...current,
running: false,
currentLabel: "已取消抓取",
}));
}
async function fetchRowsForItem(itemId: string, page: number) {
const [reviewResponse, qaResponse] = await Promise.all([
fetch(
`/api/product-planning/taobao/reviews?id=${encodeURIComponent(itemId)}&page=${page}`,
{
method: "GET",
}
),
fetch("/api/product-planning/taobao/qa", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ itemId, page }),
}),
]);
const reviewPayload =
(await reviewResponse.json()) as ProductPlanningRowsResponse;
const qaPayload =
(await qaResponse.json()) as ProductPlanningRowsResponse;
return {
reviewOk: reviewResponse.ok && reviewPayload.success,
qaOk: qaResponse.ok && qaPayload.success,
reviewPayload,
qaPayload,
};
}
async function handleBatchFetch() {
const productIds = productInput
.split(/\r?\n/)
.map((line) => extractTaobaoItemId(line))
.filter((item): item is string => Boolean(item));
if (productIds.length === 0) {
const message = "请输入淘宝链接或商品 ID";
setReviewError(message);
setQaError(message);
toast.error(message);
return;
}
if (productIds.length > 20) {
const message = "最多支持 20 个商品同时抓取";
setReviewError(message);
setQaError(message);
toast.error(message);
return;
}
const min = Math.max(Number.parseInt(minPage, 10) || 1, 1);
const max = Math.min(Number.parseInt(maxPage, 10) || 1, 20);
if (min > max) {
const message = "最小页数不能大于最大页数";
setReviewError(message);
setQaError(message);
toast.error(message);
return;
}
cancelFetchRef.current = false;
setReviewError(null);
setQaError(null);
setReviewMarkdown(null);
setQaMarkdown(null);
const totalSteps = productIds.length * (max - min + 1);
const nextReviewRows: TaobaoReviewRow[] = [];
const nextQaRows: TaobaoQaRow[] = [];
let failures = 0;
let completed = 0;
setFetchProgress({
running: true,
total: totalSteps,
completed: 0,
failed: 0,
currentLabel: "开始抓取数据...",
});
try {
for (const itemId of productIds) {
for (let page = min; page <= max; page += 1) {
if (cancelFetchRef.current) {
toast("已取消批量抓取");
break;
}
setFetchProgress((current) => ({
...current,
currentLabel: `正在抓取商品 ${itemId} 第 ${page} 页`,
}));
try {
const result = await fetchRowsForItem(itemId, page);
if (result.reviewOk) {
nextReviewRows.push(...result.reviewPayload.rows);
} else {
failures += 1;
setReviewError(result.reviewPayload.error || "评价接口返回失败");
}
if (result.qaOk) {
nextQaRows.push(...result.qaPayload.rows);
} else {
failures += 1;
setQaError(result.qaPayload.error || "问大家接口返回失败");
}
} catch (error) {
failures += 2;
console.error("[ProductPlanning] batch fetch failed:", error);
setReviewError("部分评价数据抓取失败");
setQaError("部分问大家数据抓取失败");
} finally {
completed += 1;
setFetchProgress((current) => ({
...current,
completed,
failed: failures,
}));
}
}
if (cancelFetchRef.current) break;
}
if (!cancelFetchRef.current) {
startTransition(() => {
setReviewRows(
uniqueBy(nextReviewRows, (row) =>
[
row.itemId,
row.date,
row.content,
row.appendComment || "",
row.userId || "",
].join("::")
)
);
setQaRows(
uniqueBy(nextQaRows, (row) =>
[
row.itemId,
row.question,
row.questionDetail || "",
row.answer,
row.answerUser || "",
row.answerDate || "",
row.isOfficial ? "1" : "0",
].join("::")
)
);
});
toast.success("批量抓取完成");
}
} finally {
setFetchProgress((current) => ({
...current,
running: false,
currentLabel: cancelFetchRef.current ? "已取消抓取" : "抓取完成",
}));
}
}
async function handleReviewImport(file: File) {
try {
const rows = await parseReviewImportFile(file);
setReviewError(null);
setReviewMarkdown(null);
startTransition(() => setReviewRows(rows));
setResultTab("review");
toast.success(`已导入 ${rows.length} 条评价数据`);
} catch (error) {
const message = error instanceof Error ? error.message : "导入评价表失败";
setReviewError(message);
toast.error(message);
}
}
async function handleQaImport(file: File) {
try {
const rows = await parseQaImportFile(file);
setQaError(null);
setQaMarkdown(null);
startTransition(() => setQaRows(rows));
setResultTab("qa");
toast.success(`已导入 ${rows.length} 条问大家数据`);
} catch (error) {
const message =
error instanceof Error ? error.message : "导入问大家表失败";
setQaError(message);
toast.error(message);
}
}
async function handleAnalyze(kind: "review" | "qa") {
const rows = kind === "review" ? reviewRows : qaRows;
if (rows.length === 0) {
toast.error(
kind === "review"
? "当前没有可分析的评价数据"
: "当前没有可分析的问大家数据"
);
return;
}
if (kind === "review") {
setReviewAnalyzing(true);
setReviewMarkdown(null);
} else {
setQaAnalyzing(true);
setQaMarkdown(null);
}
try {
const response = await fetch("/api/product-planning/analyze", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ kind, rows }),
});
const payload = (await response.json()) as ProductPlanningAnalyzeResponse;
if (!response.ok || !payload.success || !payload.markdown) {
throw new Error(payload.error || "分析失败");
}
if (kind === "review") {
setReviewMarkdown(payload.markdown);
} else {
setQaMarkdown(payload.markdown);
}
toast.success(kind === "review" ? "评价分析完成" : "问大家分析完成");
} catch (error) {
const message = error instanceof Error ? error.message : "AI 分析失败";
if (kind === "review") {
setReviewError(message);
} else {
setQaError(message);
}
toast.error(message);
} finally {
if (kind === "review") {
setReviewAnalyzing(false);
} else {
setQaAnalyzing(false);
}
}
}
async function fetchMarketplaceItemsBatch(
references: MarketplaceItemRef[],
options: { includeMonthSale?: boolean } = {}
) {
const response = await fetch("/api/product-planning/marketplace/items", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
items: references,
includeMonthSale: options.includeMonthSale !== false,
}),
});
const payload =
(await response.json()) as ProductPlanningMarketplaceItemsResponse;
if (!response.ok || !payload.items) {
throw new Error(payload.error || "批量获取商品详情失败");
}
const rows = payload.items
.filter((result) => result.success && result.item)
.map((result) => result.item as ProductPlanningTaobaoItem);
const errors = payload.items
.filter((result) => !result.success)
.map((result) => `${result.platform}:${result.itemId} ${result.error || "查询失败"}`);
if (rows.length === 0) {
throw new Error(errors[0] || payload.error || "批量查询未获取到可用商品");
}
return { rows, errors };
}
function parseMarketplaceItemLines(value: string) {
return value
.split(/\r?\n/)
.map((line) => parseMarketplaceItemReference(line))
.filter((item): item is MarketplaceItemRef => Boolean(item));
}
async function handleCompetitorFetch() {
const itemRefs = parseMarketplaceItemLines(competitorInput);
if (itemRefs.length === 0) {
const message =
"请输入至少一个淘宝/京东链接,或使用 jd:商品ID 的形式输入京东商品";
setCompetitorError(message);
toast.error(message);
return;
}
setCompetitorLoading(true);
setCompetitorError(null);
setCompetitorImageError(null);
setCompetitorSkuError(null);
setCompetitorAnalysisRecords({});
setActiveCompetitorAnalysisKey(null);
try {
const references = uniqueBy(
itemRefs,
(item) => `${item.platform}:${item.itemId}`
);
const { rows, errors } = await fetchMarketplaceItemsBatch(references, {
includeMonthSale: true,
});
startTransition(() => setCompetitorItems(rows));
if (errors.length > 0) {
const message = `成功 ${rows.length} 个,失败 ${errors.length} 个:${errors.slice(0, 3).join(";")}`;
setCompetitorError(message);
toast.warning(message);
} else {
toast.success(`已获取 ${rows.length} 个竞品商品数据`);
}
} catch (error) {
const message =
error instanceof Error ? error.message : "获取竞品数据失败";
setCompetitorError(message);
toast.error(message);
} finally {
setCompetitorLoading(false);
}
}
async function handleCompetitorExport() {
if (competitorItems.length === 0) {
toast.error("当前没有可导出的竞品数据");
return;
}
try {
await downloadCompetitorWorkbook(competitorItems);
toast.success("竞品分析结果已导出");
} catch (error) {
console.error("[ProductPlanning] competitor export failed:", error);
toast.error("导出竞品分析结果失败");
}
}
async function handleCompetitorImageAnalyze(
params: CompetitorImageAnalyzeParams
) {
const { item, imageUrl, imageLabel, imageKey } = params;
if (!imageUrl) {
toast.error("当前图片不存在,无法分析");
return;
}
setCompetitorImageLoadingId(imageKey);
setCompetitorImageError(null);
try {
const response = await fetch("/api/product-planning/analyze-image", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
itemId: item.itemId,
imageUrl,
title: item.title,
}),
});
const payload = (await response.json()) as ProductPlanningAnalyzeResponse;
if (!response.ok || !payload.success || !payload.markdown) {
throw new Error(payload.error || "竞品主图分析失败");
}
const markdown = payload.markdown;
const title = `${item.title || item.itemId} - ${imageLabel}`;
setCompetitorAnalysisRecords((current) => ({
...current,
[imageKey]: {
title,
markdown,
kind: "image",
updatedAt: new Date().toISOString(),
},
}));
setActiveCompetitorAnalysisKey(imageKey);
toast.success(`${imageLabel}分析完成`);
} catch (error) {
const message =
error instanceof Error ? error.message : "竞品图片分析失败";
setCompetitorImageError(message);
toast.error(message);
} finally {
setCompetitorImageLoadingId(null);
}
}
async function handleCompetitorSkuAnalyze(item: ProductPlanningTaobaoItem) {
if (item.skus.length === 0) {
toast.error("当前商品没有可分析的 SKU 数据");
return;
}
const skuKey = `${item.platform}:${item.itemId}:sku`;
setCompetitorSkuLoadingKey(skuKey);
setCompetitorSkuError(null);
try {
const response = await fetch("/api/product-planning/analyze-sku", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
itemId: item.itemId,
title: item.title,
platform: item.platform,
skus: item.skus,
}),
});
const payload = (await response.json()) as ProductPlanningAnalyzeResponse;
if (!response.ok || !payload.success || !payload.markdown) {
throw new Error(payload.error || "SKU 卖点分析失败");
}
const markdown = payload.markdown;
setCompetitorAnalysisRecords((current) => ({
...current,
[skuKey]: {
title: `${item.title || item.itemId} - SKU 卖点分析`,
markdown,
kind: "sku",
updatedAt: new Date().toISOString(),
},
}));
setActiveCompetitorAnalysisKey(skuKey);
toast.success("SKU 卖点分析完成");
} catch (error) {
const message =
error instanceof Error ? error.message : "SKU 卖点分析失败";
setCompetitorSkuError(message);
toast.error(message);
} finally {
setCompetitorSkuLoadingKey(null);
}
}
async function handleKeywordInsightSearch() {
const keyword = insightKeyword.trim();
if (!keyword) {
const message = "请输入要分析的关键词";
setInsightError(message);
toast.error(message);
return;
}
setInsightLoading(true);
setInsightError(null);
try {
const response = await fetch(
`/api/product-planning/keyword-insight?q=${encodeURIComponent(keyword)}`
);
const payload =
(await response.json()) as ProductPlanningKeywordInsightResponse;
if (!response.ok || !payload.success) {
throw new Error(payload.error || "关键词洞察分析失败");
}
setInsightSuggestions(payload.suggestions);
setInsightGroups(payload.groups);
toast.success(`已获取 ${payload.suggestions.length} 条搜索建议`);
} catch (error) {
const message =
error instanceof Error ? error.message : "关键词洞察分析失败";
setInsightError(message);
toast.error(message);
} finally {
setInsightLoading(false);
}
}
async function handleInsightSuggestionsExport() {
if (insightSuggestions.length === 0) {
toast.error("当前没有可导出的搜索建议");
return;
}
try {
await downloadKeywordInsightSuggestionsWorkbook(insightSuggestions);
toast.success("搜索建议已导出");
} catch (error) {
console.error(
"[ProductPlanning] insight suggestions export failed:",
error
);
toast.error("导出搜索建议失败");
}
}
async function handleInsightGroupsExport() {
if (insightGroups.length === 0) {
toast.error("当前没有可导出的标签分组");
return;
}
try {
await downloadKeywordInsightGroupsWorkbook(insightGroups);
toast.success("标签分组已导出");
} catch (error) {
console.error("[ProductPlanning] insight groups export failed:", error);
toast.error("导出标签分组失败");
}
}
async function handleSalesFetch() {
const itemRefs = parseMarketplaceItemLines(salesInput);
if (itemRefs.length === 0) {
const message =
"请输入至少一个淘宝/京东链接,或使用 jd:商品ID 的形式输入京东商品";
setSalesError(message);
toast.error(message);
return;
}
setSalesLoading(true);
setSalesError(null);
try {
const references = uniqueBy(
itemRefs,
(item) => `${item.platform}:${item.itemId}`
);
const { rows, errors } = await fetchMarketplaceItemsBatch(references, {
includeMonthSale: true,
});
startTransition(() => setSalesItems(rows));
if (errors.length > 0) {
const message = `成功 ${rows.length} 个,失败 ${errors.length} 个:${errors.slice(0, 3).join(";")}`;
setSalesError(message);
toast.warning(message);
} else {
toast.success(`已获取 ${rows.length} 个商品的销量数据`);
}
} catch (error) {
const message =
error instanceof Error ? error.message : "获取销量数据失败";
setSalesError(message);
toast.error(message);
} finally {
setSalesLoading(false);
}
}
async function handleSalesExport() {
if (salesItems.length === 0) {
toast.error("当前没有可导出的销量数据");
return;
}
try {
await downloadSalesWorkbook(salesItems);
toast.success("销量结果已导出");
} catch (error) {
console.error("[ProductPlanning] sales export failed:", error);
toast.error("导出销量结果失败");
}
}
function handleMarkdownExport(
markdown: string | null,
fileName: string,
errorMessage: string
) {
if (!markdown) {
toast.error(errorMessage);
return;
}
try {
downloadMarkdownFile(markdown, fileName);
toast.success("分析结果已导出");
} catch (error) {
console.error("[ProductPlanning] markdown export failed:", error);
toast.error("导出分析结果失败");
}
}
async function handleCompetitorAnalysisWorkbookExport() {
if (Object.keys(competitorAnalysisRecords).length === 0) {
toast.error("当前没有可导出的竞品分析结果");
return;
}
try {
await downloadCompetitorAnalysisWorkbook(competitorAnalysisRecords);
toast.success("竞品分析结果已导出");
} catch (error) {
console.error(
"[ProductPlanning] competitor analysis export failed:",
error
);
toast.error("导出竞品分析结果失败");
}
}
function handleViewCompetitorAnalysis(key: string) {
if (!competitorAnalysisRecords[key]) {
toast.error("该分析结果还没有生成");
return;
}
setActiveCompetitorAnalysisKey(key);
}
function handleSaveSnapshot(params: {
kind: ProductPlanningSavedSnapshot["kind"];
defaultName: string;
title: string;
markdown: string | null;
}) {
if (!params.markdown) {
toast.error("当前没有可保存的分析结果");
return;
}
const inputName = window
.prompt("请输入快照名称", params.defaultName)
?.trim();
if (!inputName) {
return;
}
const snapshot: ProductPlanningSavedSnapshot = {
id: createSnapshotId(),
name: inputName,
kind: params.kind,
title: params.title,
markdown: params.markdown,
createdAt: new Date().toISOString(),
};
setSavedSnapshots((current) => [snapshot, ...current].slice(0, 30));
setActiveSnapshotId(snapshot.id);
toast.success("分析快照已保存");
}
function handleDeleteSnapshot(snapshotId: string) {
setSavedSnapshots((current) =>
current.filter((snapshot) => snapshot.id !== snapshotId)
);
setActiveSnapshotId((current) => (current === snapshotId ? null : current));
toast.success("快照已删除");
}
function handleRenameSnapshot(snapshotId: string) {
const target = savedSnapshots.find(
(snapshot) => snapshot.id === snapshotId
);
if (!target) {
toast.error("未找到该快照");
return;
}
const nextName = window.prompt("请输入新的快照名称", target.name)?.trim();
if (!nextName || nextName === target.name) {
return;
}
setSavedSnapshots((current) =>
current.map((snapshot) =>
snapshot.id === snapshotId
? {
...snapshot,
name: nextName,
}
: snapshot
)
);
toast.success("快照名称已更新");
}
function handleViewSnapshot(snapshotId: string) {
if (!savedSnapshots.some((snapshot) => snapshot.id === snapshotId)) {
toast.error("未找到该快照");
return;
}
setActiveSnapshotId(snapshotId);
}
function handleClearSession() {
if (!window.confirm("确认清空当前商品策划会话内容并刷新页面?")) {
return;
}
sessionStorage.removeItem(PRODUCT_PLANNING_SESSION_KEY);
window.location.reload();
}
const progressValue =
fetchProgress.total > 0
? Math.round((fetchProgress.completed / fetchProgress.total) * 100)
: 0;
const normalizedTemplateSearch = templateSearch.trim().toLowerCase();
const filteredTemplates = templates.filter((template) => {
if (!normalizedTemplateSearch) return true;
return [
template.categoryName,
template.description || "",
template.fileName,
].some((value) => value.toLowerCase().includes(normalizedTemplateSearch));
});
const activeCompetitorAnalysis = activeCompetitorAnalysisKey
? competitorAnalysisRecords[activeCompetitorAnalysisKey] || null
: null;
const activeSnapshot = activeSnapshotId
? savedSnapshots.find((snapshot) => snapshot.id === activeSnapshotId) ||
null
: null;
const normalizedSnapshotSearch = snapshotSearch.trim().toLowerCase();
const filteredSnapshots = savedSnapshots.filter((snapshot) => {
if (snapshotKindFilter !== "all" && snapshot.kind !== snapshotKindFilter) {
return false;
}
if (!normalizedSnapshotSearch) return true;
return [snapshot.name, snapshot.title, snapshot.markdown].some((value) =>
value.toLowerCase().includes(normalizedSnapshotSearch)
);
});
useEffect(() => {
if (!sessionHydratedRef.current) return;
const payload: ProductPlanningSessionState = {
version: 1,
mainTab,
manualSegmentationMode,
categoryWords,
keywordRows,
productInput,
minPage,
maxPage,
reviewRows,
qaRows,
resultTab,
reviewMarkdown,
qaMarkdown,
advancedTab,
competitorInput,
competitorItems,
competitorAnalysisRecords,
activeCompetitorAnalysisKey,
insightKeyword,
insightSuggestions,
insightGroups,
salesInput,
salesItems,
templateSearch,
savedSnapshots,
activeSnapshotId,
snapshotSearch,
snapshotKindFilter,
};
try {
sessionStorage.setItem(
PRODUCT_PLANNING_SESSION_KEY,
JSON.stringify(payload)
);
} catch (error) {
console.warn("[ProductPlanning] persist session failed:", error);
}
}, [
activeCompetitorAnalysisKey,
advancedTab,
categoryWords,
competitorAnalysisRecords,
competitorInput,
competitorItems,
insightGroups,
insightKeyword,
insightSuggestions,
keywordRows,
mainTab,
manualSegmentationMode,
maxPage,
minPage,
productInput,
qaMarkdown,
qaRows,
resultTab,
reviewMarkdown,
reviewRows,
savedSnapshots,
salesInput,
salesItems,
snapshotKindFilter,
snapshotSearch,
templateSearch,
activeSnapshotId,
]);
return (
商品策划工具
在当前项目内完成关键词拆词、评价/问大家抓取、本地表格导入和 AI
分析总结。
分析快照
将当前分析结果保存为会话内命名快照,便于回看、导出和对比。
已保存 {savedSnapshots.length} 条
setSnapshotSearch(event.target.value)}
placeholder="搜索快照名称、标题或内容"
value={snapshotSearch}
/>
{(
[
["all", "全部"],
["review", "评价"],
["qa", "问大家"],
["competitor", "竞品"],
] as Array<[SnapshotKindFilter, string]>
).map(([value, label]) => (
))}
{savedSnapshots.length === 0 ? (
还没有保存任何分析快照。你可以在评价、问大家或竞品分析结果卡片里点击“保存快照”。
) : filteredSnapshots.length === 0 ? (
没有匹配当前筛选条件的快照。
) : (
{filteredSnapshots.map((snapshot) => (
{snapshot.name}
{snapshot.kind === "review"
? "评价分析"
: snapshot.kind === "qa"
? "问大家分析"
: "竞品分析"}
{formatDateTime(snapshot.createdAt)}
{snapshot.title}
))}
)}
{
if (PRODUCT_PLANNING_ADVANCED_DISABLED && value === "advanced") {
toast.info(PRODUCT_PLANNING_ADVANCED_DISABLED_MESSAGE);
setMainTab("keywords");
return;
}
setMainTab(value);
}}
value={mainTab}
>
关键词拆词
评价 / 问大家分析
{PRODUCT_PLANNING_ADVANCED_DISABLED ? null : (
拓展分析
)}
关键词拆词分析
支持 txt、xlsx、csv 关键词文件,也支持直接导入手动分词结果。
手动分词模式
开启后会跳过自动拆词,直接使用你上传表格中的“词语 /
标签分类”等结果。
{keywordRows.length > 0 ? (
已生成 {keywordRows.length} 条结果
) : null}
分析结果
按出现次数聚合展示拆词结果。
{keywordRows.length === 0 ? (
还没有拆词结果。上传文件并点击“开始拆词”后会在这里展示。
) : (
)}
云端模板库
共享常用品类关键词源数据,支持一键加载、下载,管理员可维护模板内容。
共 {templates.length} 个模板
setTemplateSearch(event.target.value)}
placeholder="按模板名称、描述或文件名筛选"
value={templateSearch}
/>
{canManageTemplates ? (
) : null}
{filteredTemplates.length === 0 ? (
{templates.length === 0
? "当前还没有可用模板。"
: "没有匹配当前搜索条件的模板。"}
) : (
{filteredTemplates.map((template) => (
{template.categoryName}
更新于 {formatDateTime(template.updatedAt)}
{formatFileSize(template.fileSize)}
{template.description || "未填写模板说明"}
文件:{template.fileName}
{canManageTemplates ? (
) : null}
))}
)}
{/* Left Column */}
{/* 抓取淘宝数据 */}
抓取商品数据
输入淘宝链接或商品 ID,自动抓取评价与问大家。
{fetchProgress.running ? (
) : null}
{fetchProgress.total > 0 ? (
{fetchProgress.currentLabel || "准备中..."}
{fetchProgress.completed}/{fetchProgress.total}
{fetchProgress.failed > 0 ? (
失败:{fetchProgress.failed}
) : null}
) : null}
{/* 本地导入 */}
本地导入数据
导入本地表格,直接进行 AI 分析。
{
const file = event.target.files?.[0];
if (file) {
void handleReviewImport(file);
}
event.target.value = "";
}}
ref={reviewImportRef}
type="file"
/>
{
const file = event.target.files?.[0];
if (file) {
void handleQaImport(file);
}
event.target.value = "";
}}
ref={qaImportRef}
type="file"
/>
{/* Right Column */}
问大家数据
{qaRows.length > 0 && (
{qaRows.length}
)}
评价数据
{reviewRows.length > 0 && (
{reviewRows.length}
)}
{resultTab === "qa" ? (
) : (
)}
{resultTab === "qa" ? (
{qaMarkdown && (
>
}
markdown={qaMarkdown}
title="✨ 问大家分析结论"
/>
)}
) : (
{reviewMarkdown && (
>
}
markdown={reviewMarkdown}
title="✨ 评价分析结论"
/>
)}
)}
竞品分析
关键词洞察
ID 销量查询
多平台竞品分析
支持淘宝链接、京东链接,以及 `jd:商品ID`
形式的京东输入,系统会聚合商品详情、SKU 和销量做横向对比。
{competitorItems.length > 0 ? (
已获取 {competitorItems.length} 个竞品
) : null}
竞品结果
统一展示价格、销量、材质和 SKU
概览,也支持直接分析竞品主图。
void handleCompetitorImageAnalyze(params)
}
onAnalyzeSku={(item) =>
void handleCompetitorSkuAnalyze(item)
}
onViewAnalysis={handleViewCompetitorAnalysis}
rows={competitorItems}
/>
>
}
markdown={activeCompetitorAnalysis?.markdown || null}
title={
activeCompetitorAnalysis
? `${activeCompetitorAnalysis.kind === "sku" ? "SKU" : "竞品图片"}分析:${activeCompetitorAnalysis.title}`
: "竞品分析结果"
}
/>
关键词洞察
调用淘宝搜索建议接口,输出推荐词和智能标签分组。
搜索建议
按接口返回的权重排序展示。
标签分组
保留热门标签与普通标签的分组信息。
ID 销量查询
支持淘宝链接、京东链接,以及 `jd:商品ID`
形式的京东输入,系统会聚合销量、价格、店铺和材质信息。
{salesItems.length > 0 ? (
已查询 {salesItems.length} 个商品
) : null}
销量结果
展示近期销量、价格和材质摘要。
);
}