125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
import { normalizeRateDisplay } from "../../shared/rate-normalizer";
|
|
import { escapeCsvCell } from "../../shared/csv";
|
|
import type { MarketRecord } from "./types";
|
|
|
|
export type CsvColumn = {
|
|
header: string;
|
|
readValue: (record: MarketRecord) => string;
|
|
};
|
|
|
|
const FALLBACK_BASE_COLUMNS: CsvColumn[] = [
|
|
{
|
|
header: "达人ID",
|
|
readValue: (record: MarketRecord) => record.authorId
|
|
},
|
|
{
|
|
header: "达人名称",
|
|
readValue: (record: MarketRecord) => record.authorName
|
|
},
|
|
{
|
|
header: "地区",
|
|
readValue: (record: MarketRecord) => record.location ?? ""
|
|
},
|
|
{
|
|
header: "21-60s报价",
|
|
readValue: (record: MarketRecord) => record.price21To60s ?? ""
|
|
}
|
|
];
|
|
|
|
const RATE_COLUMNS: CsvColumn[] = [
|
|
{
|
|
header: "商单视频看后搜率",
|
|
readValue: (record: MarketRecord) =>
|
|
record.rates?.singleVideoAfterSearchRate
|
|
? normalizeRateDisplay(record.rates.singleVideoAfterSearchRate)
|
|
: ""
|
|
},
|
|
{
|
|
header: "个人视频看后搜率",
|
|
readValue: (record: MarketRecord) =>
|
|
record.rates?.personalVideoAfterSearchRate
|
|
? normalizeRateDisplay(record.rates.personalVideoAfterSearchRate)
|
|
: ""
|
|
}
|
|
];
|
|
|
|
const BACKEND_METRIC_COLUMNS: CsvColumn[] = [
|
|
{
|
|
header: "秒思api-看后搜率",
|
|
readValue: (record: MarketRecord) =>
|
|
record.backendMetrics?.afterViewSearchRate ?? ""
|
|
},
|
|
{
|
|
header: "秒思api-看后搜数",
|
|
readValue: (record: MarketRecord) =>
|
|
record.backendMetrics?.afterViewSearchCount ?? ""
|
|
},
|
|
{
|
|
header: "秒思api-新增A3数",
|
|
readValue: (record: MarketRecord) =>
|
|
record.backendMetrics?.a3IncreaseCount ?? ""
|
|
},
|
|
{
|
|
header: "秒思api-新增A3率",
|
|
readValue: (record: MarketRecord) =>
|
|
record.backendMetrics?.newA3Rate ?? ""
|
|
},
|
|
{
|
|
header: "秒思api-CPA3",
|
|
readValue: (record: MarketRecord) => record.backendMetrics?.cpa3 ?? ""
|
|
},
|
|
{
|
|
header: "秒思api-cp_search",
|
|
readValue: (record: MarketRecord) => record.backendMetrics?.cpSearch ?? ""
|
|
}
|
|
];
|
|
|
|
export function listRateCsvHeaders(): string[] {
|
|
return RATE_COLUMNS.map((column) => column.header);
|
|
}
|
|
|
|
export function listBackendMetricCsvHeaders(): string[] {
|
|
return BACKEND_METRIC_COLUMNS.map((column) => column.header);
|
|
}
|
|
|
|
export function buildMarketCsv(records: MarketRecord[]): string {
|
|
const csvColumns = buildMarketCsvColumns(records);
|
|
const headerLine = csvColumns.map((column) => column.header).join(",");
|
|
const rowLines = records.map((record) =>
|
|
csvColumns.map((column) => escapeCsvCell(column.readValue(record))).join(",")
|
|
);
|
|
|
|
return [headerLine, ...rowLines].join("\n");
|
|
}
|
|
|
|
export function buildMarketCsvColumns(records: MarketRecord[]): CsvColumn[] {
|
|
const baseColumns = buildBaseColumns(records);
|
|
return [...baseColumns, ...RATE_COLUMNS, ...BACKEND_METRIC_COLUMNS];
|
|
}
|
|
|
|
export function buildBaseColumns(records: MarketRecord[]): CsvColumn[] {
|
|
const orderedHeaders: string[] = [];
|
|
const seenHeaders = new Set<string>();
|
|
const excludedHeaders = new Set(["代表视频"]);
|
|
|
|
records.forEach((record) => {
|
|
Object.keys(record.exportFields ?? {}).forEach((header) => {
|
|
if (seenHeaders.has(header) || excludedHeaders.has(header)) {
|
|
return;
|
|
}
|
|
|
|
seenHeaders.add(header);
|
|
orderedHeaders.push(header);
|
|
});
|
|
});
|
|
|
|
if (orderedHeaders.length === 0) {
|
|
return FALLBACK_BASE_COLUMNS;
|
|
}
|
|
|
|
return orderedHeaders.map((header) => ({
|
|
header,
|
|
readValue: (record: MarketRecord) => record.exportFields?.[header] ?? ""
|
|
}));
|
|
}
|