feat: add csv exporter

This commit is contained in:
admin123 2026-04-20 20:09:06 +08:00
parent 26bdff8daa
commit c846ab9f4a
3 changed files with 130 additions and 0 deletions

View 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
View File

@ -0,0 +1,7 @@
export function escapeCsvCell(value: string): string {
if (/[",\n]/.test(value)) {
return `"${value.replace(/"/g, "\"\"")}"`;
}
return value;
}

View 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%");
});
});