Your Name 987ab3f6a0
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: support batch competitor planning lookup
2026-04-30 17:35:25 +08:00

298 lines
11 KiB
TypeScript

import type {
KeywordAnalysisRow,
ProductPlanningCompetitorAnalysisRecord,
ProductPlanningKeywordInsightGroup,
ProductPlanningKeywordInsightSuggestion,
ProductPlanningTaobaoItem,
} from "./types";
function formatPlatform(value: ProductPlanningTaobaoItem["platform"], isTmall: boolean) {
if (value === "jd") return "京东";
return isTmall ? "天猫" : "淘宝";
}
function formatDataSource(value: ProductPlanningTaobaoItem["dataSource"]) {
if (value === "detail-plus") return "详情增强";
if (value === "legacy") return "旧接口兜底";
if (value === "jd") return "京东接口";
return "未标记";
}
function formatPropertySummary(row: ProductPlanningTaobaoItem, limit = 10) {
return row.properties
.slice(0, limit)
.map((entry) => `${entry.name}:${entry.value}`)
.join(" | ");
}
function formatConversionRate(value?: number) {
if (typeof value !== "number") return "";
return `${(value * 100).toFixed(2)}%`;
}
function triggerBlobDownload(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);
}
export async function downloadKeywordAnalysisWorkbook(
rows: KeywordAnalysisRow[],
baseFileName = "商品策划工具_关键词分析"
) {
const { Workbook } = await import("exceljs");
const workbook = new Workbook();
const sheet = workbook.addWorksheet("关键词拆词结果");
sheet.columns = [
{ header: "词语", key: "keyword", width: 24 },
{ header: "拆分片段", key: "segments", width: 28 },
{ header: "标签分类", key: "category", width: 16 },
{ header: "出现次数", key: "occurrenceCount", width: 12 },
{ header: "搜索人气", key: "searchPopularity", width: 14 },
{ header: "支付转化率", key: "conversionRate", width: 14 },
];
sheet.getRow(1).font = { bold: true };
sheet.views = [{ state: "frozen", ySplit: 1 }];
for (const row of rows) {
sheet.addRow({
keyword: row.keyword,
segments: row.segments.join(" / "),
category: row.category,
occurrenceCount: row.occurrenceCount,
searchPopularity: row.searchPopularity ?? "",
conversionRate: formatConversionRate(row.conversionRate),
});
}
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
triggerBlobDownload(blob, `${baseFileName}_${new Date().toISOString().slice(0, 10)}.xlsx`);
}
async function downloadWorkbook(params: {
worksheetName: string;
columns: Array<{ header: string; key: string; width: number }>;
rows: Array<Record<string, unknown>>;
fileName: string;
}) {
const { Workbook } = await import("exceljs");
const workbook = new Workbook();
const sheet = workbook.addWorksheet(params.worksheetName);
sheet.columns = params.columns;
sheet.getRow(1).font = { bold: true };
sheet.views = [{ state: "frozen", ySplit: 1 }];
for (const row of params.rows) {
sheet.addRow(row);
}
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
triggerBlobDownload(blob, `${params.fileName}_${new Date().toISOString().slice(0, 10)}.xlsx`);
}
export async function downloadCompetitorWorkbook(
rows: ProductPlanningTaobaoItem[],
baseFileName = "商品策划工具_竞品分析"
) {
await downloadWorkbook({
worksheetName: "竞品分析",
fileName: baseFileName,
columns: [
{ header: "数据来源", key: "dataSource", width: 14 },
{ header: "平台", key: "platform", width: 12 },
{ header: "商品ID", key: "itemId", width: 18 },
{ header: "请求ID", key: "requestedItemId", width: 18 },
{ header: "商品标题", key: "title", width: 40 },
{ header: "品牌", key: "brand", width: 18 },
{ header: "店铺", key: "shopName", width: 26 },
{ header: "店铺ID", key: "shopId", width: 18 },
{ header: "发货地", key: "location", width: 18 },
{ header: "类目ID", key: "categoryId", width: 18 },
{ header: "当前价格", key: "price", width: 14 },
{ header: "划线价格", key: "originalPrice", width: 14 },
{ header: "总销量", key: "totalSales", width: 14 },
{ header: "近期销量", key: "recentSales", width: 14 },
{ header: "材质", key: "material", width: 24 },
{ header: "详细材质", key: "detailedMaterial", width: 36 },
{ header: "标准品类", key: "standardProductType", width: 22 },
{ header: "市场分类", key: "marketCategoryName", width: 22 },
{ header: "价格带", key: "priceBand", width: 16 },
{ header: "SKU数量", key: "skuCount", width: 12 },
{ header: "主图数", key: "itemImageCount", width: 12 },
{ header: "详情图数", key: "detailImageCount", width: 12 },
{ header: "主图视频", key: "videoUrl", width: 36 },
{ header: "企划详情ID", key: "detailStorageRecordId", width: 14 },
{ header: "产品记录ID", key: "productRecordId", width: 14 },
{ header: "参数预览", key: "propertySummary", width: 80 },
{ header: "商品链接", key: "detailUrl", width: 48 },
{ header: "来源链接", key: "sourceUrl", width: 48 },
],
rows: rows.map((row) => ({
dataSource: formatDataSource(row.dataSource),
platform: formatPlatform(row.platform, row.isTmall),
itemId: row.itemId,
requestedItemId: row.requestedItemId || row.itemId,
title: row.title,
brand: row.brand || "",
shopName: row.shopName || "",
shopId: row.shopId || "",
location: row.location || "",
categoryId: row.categoryId || "",
price: row.price ?? "",
originalPrice: row.originalPrice ?? "",
totalSales: row.totalSales ?? "",
recentSales: row.recentSales ?? "",
material: row.material || "",
detailedMaterial: row.detailedMaterial || "",
standardProductType: row.standardProductType || "",
marketCategoryName: row.marketCategoryName || "",
priceBand: row.priceBand || "",
skuCount: row.skuCount ?? row.skus.length,
itemImageCount: row.itemImages.length,
detailImageCount: row.detailImages.length,
videoUrl: row.videoUrl || "",
detailStorageRecordId: row.detailStorageRecordId || "",
productRecordId: row.productRecordId || "",
propertySummary: formatPropertySummary(row),
detailUrl: row.detailUrl || "",
sourceUrl: row.sourceUrl || "",
})),
});
}
export async function downloadSalesWorkbook(
rows: ProductPlanningTaobaoItem[],
baseFileName = "商品策划工具_ID销量查询"
) {
await downloadWorkbook({
worksheetName: "ID销量查询",
fileName: baseFileName,
columns: [
{ header: "平台", key: "platform", width: 12 },
{ header: "商品ID", key: "itemId", width: 18 },
{ header: "商品标题", key: "title", width: 40 },
{ header: "近期销量", key: "recentSales", width: 14 },
{ header: "当前价格", key: "price", width: 14 },
{ header: "划线价格", key: "originalPrice", width: 14 },
{ header: "品牌", key: "brand", width: 18 },
{ header: "店铺", key: "shopName", width: 26 },
{ header: "发货地", key: "location", width: 18 },
{ header: "材质", key: "material", width: 22 },
{ header: "详情图数", key: "detailImageCount", width: 12 },
{ header: "参数预览", key: "propertySummary", width: 80 },
{ header: "商品链接", key: "detailUrl", width: 48 },
],
rows: rows.map((row) => ({
platform: formatPlatform(row.platform, row.isTmall),
itemId: row.itemId,
title: row.title,
recentSales: row.recentSales ?? "",
price: row.price ?? "",
originalPrice: row.originalPrice ?? "",
brand: row.brand || "",
shopName: row.shopName || "",
location: row.location || "",
material: row.material || "",
detailImageCount: row.detailImages.length,
propertySummary: formatPropertySummary(row),
detailUrl: row.detailUrl || "",
})),
});
}
export async function downloadKeywordInsightSuggestionsWorkbook(
rows: ProductPlanningKeywordInsightSuggestion[],
baseFileName = "商品策划工具_关键词洞察_搜索建议"
) {
await downloadWorkbook({
worksheetName: "搜索建议",
fileName: baseFileName,
columns: [
{ header: "排名", key: "rank", width: 10 },
{ header: "关键词", key: "keyword", width: 28 },
{ header: "权重", key: "weight", width: 16 },
],
rows: rows.map((row) => ({
rank: row.rank,
keyword: row.keyword,
weight: row.weight,
})),
});
}
export async function downloadKeywordInsightGroupsWorkbook(
rows: ProductPlanningKeywordInsightGroup[],
baseFileName = "商品策划工具_关键词洞察_标签分组"
) {
await downloadWorkbook({
worksheetName: "标签分组",
fileName: baseFileName,
columns: [
{ header: "组序号", key: "index", width: 10 },
{ header: "分组类型", key: "type", width: 20 },
{ header: "标签名称", key: "title", width: 28 },
{ header: "标签热度", key: "tagType", width: 12 },
],
rows: rows.flatMap((group) =>
group.tags.map((tag) => ({
index: group.index,
type: group.type,
title: tag.title,
tagType: tag.type === "hot" ? "热门" : "普通",
}))
),
});
}
export function downloadMarkdownFile(
markdown: string,
baseFileName = "商品策划工具_分析结果"
) {
const blob = new Blob([markdown], {
type: "text/markdown;charset=utf-8",
});
triggerBlobDownload(blob, `${baseFileName}_${new Date().toISOString().slice(0, 10)}.md`);
}
export async function downloadCompetitorAnalysisWorkbook(
records: Record<string, ProductPlanningCompetitorAnalysisRecord>,
baseFileName = "商品策划工具_竞品分析结果"
) {
const rows = Object.entries(records)
.map(([key, value]) => ({
key,
kind: value.kind === "sku" ? "SKU卖点分析" : "图片分析",
title: value.title,
updatedAt: value.updatedAt,
markdown: value.markdown,
}))
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
await downloadWorkbook({
worksheetName: "AI分析结果",
fileName: baseFileName,
columns: [
{ header: "结果键", key: "key", width: 26 },
{ header: "分析类型", key: "kind", width: 16 },
{ header: "标题", key: "title", width: 42 },
{ header: "更新时间", key: "updatedAt", width: 24 },
{ header: "Markdown结果", key: "markdown", width: 120 },
],
rows,
});
}