feat: add csv exporter
This commit is contained in:
parent
26bdff8daa
commit
c846ab9f4a
49
src/content/market/csv-exporter.ts
Normal file
49
src/content/market/csv-exporter.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { normalizeRateDisplay } from "../../shared/rate-normalizer";
|
||||||
|
import { escapeCsvCell } from "../../shared/csv";
|
||||||
|
import type { MarketRecord } from "./types";
|
||||||
|
|
||||||
|
const CSV_COLUMNS = [
|
||||||
|
{
|
||||||
|
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 ?? ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "单视频看后搜率",
|
||||||
|
readValue: (record: MarketRecord) =>
|
||||||
|
record.rates?.singleVideoAfterSearchRate
|
||||||
|
? normalizeRateDisplay(record.rates.singleVideoAfterSearchRate)
|
||||||
|
: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "个人视频看后搜率",
|
||||||
|
readValue: (record: MarketRecord) =>
|
||||||
|
record.rates?.personalVideoAfterSearchRate
|
||||||
|
? normalizeRateDisplay(record.rates.personalVideoAfterSearchRate)
|
||||||
|
: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "插件数据状态",
|
||||||
|
readValue: (record: MarketRecord) => record.status
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function buildMarketCsv(records: MarketRecord[]): string {
|
||||||
|
const headerLine = CSV_COLUMNS.map((column) => column.header).join(",");
|
||||||
|
const rowLines = records.map((record) =>
|
||||||
|
CSV_COLUMNS.map((column) => escapeCsvCell(column.readValue(record))).join(",")
|
||||||
|
);
|
||||||
|
|
||||||
|
return [headerLine, ...rowLines].join("\n");
|
||||||
|
}
|
||||||
7
src/shared/csv.ts
Normal file
7
src/shared/csv.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function escapeCsvCell(value: string): string {
|
||||||
|
if (/[",\n]/.test(value)) {
|
||||||
|
return `"${value.replace(/"/g, "\"\"")}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
74
tests/csv-exporter.test.ts
Normal file
74
tests/csv-exporter.test.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { buildMarketCsv } from "../src/content/market/csv-exporter";
|
||||||
|
import type { MarketRecord } from "../src/content/market/types";
|
||||||
|
|
||||||
|
describe("csv-exporter", () => {
|
||||||
|
test("uses the expected header order", () => {
|
||||||
|
const csv = buildMarketCsv([]);
|
||||||
|
const [headerLine] = csv.split("\n");
|
||||||
|
|
||||||
|
expect(headerLine).toBe(
|
||||||
|
[
|
||||||
|
"达人ID",
|
||||||
|
"达人名称",
|
||||||
|
"地区",
|
||||||
|
"21-60s报价",
|
||||||
|
"单视频看后搜率",
|
||||||
|
"个人视频看后搜率",
|
||||||
|
"插件数据状态"
|
||||||
|
].join(",")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("escapes commas and quotes", () => {
|
||||||
|
const csv = buildMarketCsv([
|
||||||
|
{
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice, \"A\"",
|
||||||
|
location: "Hangzhou",
|
||||||
|
price21To60s: "450000",
|
||||||
|
status: "success",
|
||||||
|
rates: {
|
||||||
|
singleVideoAfterSearchRate: "0.5%-1%",
|
||||||
|
personalVideoAfterSearchRate: "1% - 3%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [, rowLine] = csv.split("\n");
|
||||||
|
expect(rowLine).toContain("\"Alice, \"\"A\"\"\"");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emits empty rate fields plus failed status for failed rows", () => {
|
||||||
|
const csv = buildMarketCsv([
|
||||||
|
{
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice",
|
||||||
|
status: "failed",
|
||||||
|
failureReason: "request-failed"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [, rowLine] = csv.split("\n");
|
||||||
|
expect(rowLine).toBe("123,Alice,,,,,failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses normalized display values in export rows", () => {
|
||||||
|
const csv = buildMarketCsv([
|
||||||
|
{
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice",
|
||||||
|
status: "success",
|
||||||
|
rates: {
|
||||||
|
singleVideoAfterSearchRate: "0.5%-1%",
|
||||||
|
personalVideoAfterSearchRate: "0.02 - 0.1%"
|
||||||
|
}
|
||||||
|
} satisfies MarketRecord
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [, rowLine] = csv.split("\n");
|
||||||
|
expect(rowLine).toContain("0.5% - 1%");
|
||||||
|
expect(rowLine).toContain("0.02% - 0.1%");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user