/);
+ assert.match(exported.content, /达人昵称/);
+ assert.match(exported.content, /达人-08d5/);
+ assert.match(exported.content, /达人-3456/);
+});
+
+test("createExportController merges supplemental endpoint payloads into namespaced fields", async () => {
+ const seenUrls = [];
+ const controller = createExportController({
+ fetchImpl: async (url) => {
+ seenUrls.push(url);
+ if (url.includes("/api/solar/cooperator/user/blogger/")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ id: "61f27a60000000001000cb5f",
+ userId: "61f27a60000000001000cb5f",
+ name: "测试达人",
+ },
+ }),
+ };
+ }
+
+ if (url.includes("/api/pgy/kol/data/data_summary")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ avgRead: 1200,
+ avgInteract: 98,
+ },
+ }),
+ };
+ }
+
+ if (url.includes("/api/solar/kol/data_v3/fans_summary")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ cityDistribution: ["上海", "杭州"],
+ maleRate: 0.22,
+ },
+ }),
+ };
+ }
+
+ if (url.includes("/api/solar/kol/data/61f27a60000000001000cb5f/fans_profile")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ age18_24: 0.31,
+ age25_34: 0.44,
+ },
+ }),
+ };
+ }
+
+ throw new Error(`unexpected url: ${url}`);
+ },
+ });
+
+ const preview = await controller.preview("61f27a60000000001000cb5f");
+
+ assert.equal(seenUrls.length, 4);
+ assert.ok(
+ seenUrls.some((url) =>
+ url.includes("/api/pgy/kol/data/data_summary?userId=61f27a60000000001000cb5f&business=1"),
+ ),
+ );
+ assert.ok(
+ seenUrls.some((url) =>
+ url.includes("/api/solar/kol/data_v3/fans_summary?userId=61f27a60000000001000cb5f"),
+ ),
+ );
+ assert.ok(
+ seenUrls.some((url) =>
+ url.includes("/api/solar/kol/data/61f27a60000000001000cb5f/fans_profile"),
+ ),
+ );
+ assert.equal(preview.records[0].raw.dataSummary.avgRead, 1200);
+ assert.equal(preview.records[0].raw.fansSummary.maleRate, 0.22);
+ assert.equal(preview.records[0].raw.fansProfile.age18_24, 0.31);
+ assert.equal(preview.records[0].flattened["dataSummary.avgRead"], "1200");
+ assert.equal(preview.records[0].flattened["fansSummary.cityDistribution"], "上海 | 杭州");
+ assert.equal(preview.fields.find((field) => field.path === "dataSummary.avgRead").label, "平均阅读量");
+ assert.equal(
+ preview.fields.find((field) => field.path === "fansProfile.age18_24"),
+ undefined,
+ );
+});
+
+test("createExportController applies mapped Chinese headers for provided supplemental sample fields", async () => {
+ const controller = createExportController({
+ fetchImpl: async (url) => {
+ if (url.includes("/api/solar/cooperator/user/blogger/")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ id: "61f27a60000000001000cb5f",
+ userId: "61f27a60000000001000cb5f",
+ name: "李欢喜",
+ },
+ }),
+ };
+ }
+
+ if (url.includes("/api/pgy/kol/data/data_summary")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ fans30GrowthRate: "4.7",
+ activeDayInLast7: 7,
+ noteType: [{ contentTag: "母婴", percent: "100.0" }],
+ responseRate: "95.6",
+ },
+ }),
+ };
+ }
+
+ if (url.includes("/api/solar/kol/data_v3/fans_summary")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {},
+ }),
+ };
+ }
+
+ if (url.includes("/fans_profile")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ ages: [{ group: "25-34", percent: 0.67 }],
+ gender: { male: 0.12, female: 0.88 },
+ interests: [{ name: "母婴", percent: 0.17 }],
+ provinces: [{ name: "广东", percent: 0.14 }],
+ cities: [{ name: "广州", percent: 0.03 }],
+ devices: [{ name: "apple inc.", desc: "苹果", percent: 0.28 }],
+ dateKey: "2026-03-11",
+ },
+ }),
+ };
+ }
+
+ throw new Error(`unexpected url: ${url}`);
+ },
+ });
+
+ await controller.preview("61f27a60000000001000cb5f");
+ const exported = controller.exportSheet([
+ "name",
+ "dataSummary.fans30GrowthRate",
+ "dataSummary.activeDayInLast7",
+ "dataSummary.noteType",
+ "dataSummary.responseRate",
+ "fansProfile.ages",
+ "fansProfile.gender.male",
+ "fansProfile.gender.female",
+ "fansProfile.interests",
+ "fansProfile.provinces",
+ "fansProfile.cities",
+ "fansProfile.devices",
+ "fansProfile.dateKey",
+ ]);
+
+ assert.deepEqual(exported.headers, [
+ "达人昵称",
+ "近30天涨粉率",
+ "近7天活跃天数",
+ "笔记内容类型",
+ "响应率",
+ "粉丝年龄分布",
+ "粉丝男性占比",
+ "粉丝女性占比",
+ "粉丝兴趣分布",
+ "粉丝省份分布",
+ "粉丝城市分布",
+ "粉丝设备分布",
+ "画像日期",
+ ]);
+});
+
+test("createExportController applies mapped Chinese headers for provided fansSummary fields", async () => {
+ const controller = createExportController({
+ fetchImpl: async (url) => {
+ if (url.includes("/api/solar/cooperator/user/blogger/")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ id: "61f27a60000000001000cb5f",
+ userId: "61f27a60000000001000cb5f",
+ name: "李欢喜",
+ },
+ }),
+ };
+ }
+
+ if (url.includes("/api/pgy/kol/data/data_summary")) {
+ return { ok: true, json: async () => ({ data: {} }) };
+ }
+
+ if (url.includes("/api/solar/kol/data_v3/fans_summary")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ fansNum: 11824,
+ fansIncreaseNum: 534,
+ fansGrowthRate: "4.7",
+ fansGrowthBeyondRate: "90.3",
+ activeFansL28: 4329,
+ activeFansRate: "36.6",
+ activeFansBeyondRate: "31.4",
+ engageFansRate: "8.0",
+ engageFansL30: 946,
+ engageFansBeyondRate: "97.6",
+ readFansIn30: 1343,
+ readFansRate: "11.4",
+ readFansBeyondRate: "89.0",
+ payFansUserRate30d: "2.7",
+ payFansUserNum30d: 320,
+ },
+ }),
+ };
+ }
+
+ if (url.includes("/fans_profile")) {
+ return { ok: true, json: async () => ({ data: {} }) };
+ }
+
+ throw new Error(`unexpected url: ${url}`);
+ },
+ });
+
+ await controller.preview("61f27a60000000001000cb5f");
+ const exported = controller.exportSheet([
+ "fansSummary.fansNum",
+ "fansSummary.fansIncreaseNum",
+ "fansSummary.fansGrowthRate",
+ "fansSummary.fansGrowthBeyondRate",
+ "fansSummary.activeFansL28",
+ "fansSummary.activeFansRate",
+ "fansSummary.activeFansBeyondRate",
+ "fansSummary.engageFansRate",
+ "fansSummary.engageFansL30",
+ "fansSummary.engageFansBeyondRate",
+ "fansSummary.readFansIn30",
+ "fansSummary.readFansRate",
+ "fansSummary.readFansBeyondRate",
+ "fansSummary.payFansUserRate30d",
+ "fansSummary.payFansUserNum30d",
+ ]);
+
+ assert.deepEqual(exported.headers, [
+ "粉丝总数",
+ "涨粉数",
+ "粉丝增长率",
+ "粉丝增长超越率",
+ "近28天活跃粉丝数",
+ "活跃粉丝占比",
+ "活跃粉丝超越率",
+ "互动粉丝占比",
+ "近30天互动粉丝数",
+ "互动粉丝超越率",
+ "近30天阅读粉丝数",
+ "阅读粉丝占比",
+ "阅读粉丝超越率",
+ "近30天支付粉丝占比",
+ "近30天支付粉丝数",
+ ]);
+});
+
+test("createExportController tolerates supplemental endpoint failures", async () => {
+ const controller = createExportController({
+ fetchImpl: async (url) => {
+ if (url.includes("/api/solar/cooperator/user/blogger/")) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ id: "61f27a60000000001000cb5f",
+ userId: "61f27a60000000001000cb5f",
+ name: "测试达人",
+ },
+ }),
+ };
+ }
+
+ return {
+ ok: false,
+ status: 500,
+ json: async () => ({}),
+ };
+ },
+ });
+
+ const preview = await controller.preview("61f27a60000000001000cb5f");
+ assert.equal(preview.records[0].raw.name, "测试达人");
+ assert.equal(preview.records[0].raw.dataSummary, undefined);
+ assert.equal(preview.records[0].flattened.name, "测试达人");
+});
+
+test("createExportController rejects empty input and request failures", async () => {
+ const controller = createExportController({
+ fetchImpl: async () => ({
+ ok: false,
+ status: 403,
+ json: async () => ({}),
+ }),
+ });
+
+ await assert.rejects(
+ controller.preview(""),
+ /请输入至少一个有效的达人主页链接或达人 ID/,
+ );
+
+ await assert.rejects(
+ controller.preview("5776652682ec3912d6f508d5"),
+ /请求达人 5776652682ec3912d6f508d5 失败,状态码:403/,
+ );
+
+ assert.throws(
+ () => controller.exportSheet(["id"]),
+ /请先读取字段并确认达人数据/,
+ );
+});
diff --git a/pugongying/xhs-pgy-export.user.js b/pugongying/xhs-pgy-export.user.js
new file mode 100644
index 0000000..36cddbb
--- /dev/null
+++ b/pugongying/xhs-pgy-export.user.js
@@ -0,0 +1,1472 @@
+// ==UserScript==
+// @name 小红书蒲公英达人信息导出
+// @namespace https://pgy.xiaohongshu.com/
+// @version 0.1.0
+// @description 输入达人主页链接或达人 ID,勾选字段后导出 Excel
+// @match https://pgy.xiaohongshu.com/*
+// @grant none
+// ==/UserScript==
+
+(function bootstrap(root, factory) {
+ const api = factory(root);
+ if (typeof module === "object" && module.exports) {
+ module.exports = api;
+ }
+})(typeof globalThis !== "undefined" ? globalThis : this, function factory(root) {
+ const API_BASE =
+ "https://pgy.xiaohongshu.com/api/solar/cooperator/user/blogger/";
+ const SUPPLEMENTAL_ENDPOINTS = [
+ {
+ namespace: "dataSummary",
+ buildUrl: (userId) =>
+ `https://pgy.xiaohongshu.com/api/pgy/kol/data/data_summary?userId=${encodeURIComponent(
+ userId,
+ )}&business=1`,
+ },
+ {
+ namespace: "fansSummary",
+ buildUrl: (userId) =>
+ `https://pgy.xiaohongshu.com/api/solar/kol/data_v3/fans_summary?userId=${encodeURIComponent(
+ userId,
+ )}`,
+ },
+ {
+ namespace: "fansProfile",
+ buildUrl: (userId) =>
+ `https://pgy.xiaohongshu.com/api/solar/kol/data/${encodeURIComponent(
+ userId,
+ )}/fans_profile`,
+ },
+ ];
+ const NAMESPACE_LABEL_MAP = {
+ dataSummary: "数据概览",
+ fansSummary: "粉丝概览",
+ fansProfile: "粉丝画像",
+ };
+ const FIELD_LABEL_MAP = {
+ id: "ID",
+ "metrics.fans": "粉丝数",
+ dataSummary: "数据概览",
+ fansSummary: "粉丝概览",
+ fansProfile: "粉丝画像",
+ "dataSummary.fans30GrowthRate": "近30天粉丝量变化幅度",
+ "dataSummary.estimateVideoCpm": "预估视频CPM",
+ "dataSummary.estimatePictureCpm": "预估图文CPM",
+ "dataSummary.videoReadCost": "预估阅读单价(视频)",
+ "dataSummary.picReadCost": "预估阅读单价(图文)",
+ "dataSummary.mCpuvNum": "外溢进店中位数",
+ "fansProfile.ages": "粉丝年龄分布",
+ "fansProfile.gender.male": "粉丝男性占比",
+ "fansProfile.gender.female": "粉丝女性占比",
+ "fansSummary.activeFansRate": "活跃粉丝占比",
+ "fansSummary.engageFansRate": "互动粉丝占比",
+ "fansSummary.readFansRate": "阅读粉丝占比",
+ userId: "达人ID",
+ name: "达人昵称",
+ redId: "小红书号",
+ location: "地区",
+ travelAreaList: "可接受的出行地",
+ personalTags: "人设标签",
+ fansCount: "粉丝数",
+ likeCollectCountInfo: "获赞与收藏",
+ businessNoteCount: "商业笔记数",
+ totalNoteCount: "总笔记数",
+ picturePrice: "图文报价",
+ videoPrice: "视频报价",
+ lowerPrice: "最低报价",
+ userType: "用户类型",
+ tradeType: "合作行业",
+ clickMidNum: "阅读中位数",
+ accumCoopImpMedinNum30d: "近30天合作曝光中位数",
+ mEngagementNum: "互动中位数",
+ };
+ const SELECTABLE_FIELD_PATHS = Object.keys(FIELD_LABEL_MAP).filter(
+ (path) => !(path in NAMESPACE_LABEL_MAP),
+ );
+ const STORAGE_INPUT_KEY = "xhs-pgy-export:last-input";
+ const SCRIPT_FLAG = "__xhsPgyExportMounted__";
+
+ function isPlainObject(value) {
+ return Object.prototype.toString.call(value) === "[object Object]";
+ }
+
+ function normalizeScalar(value) {
+ if (value === null || value === undefined) {
+ return "";
+ }
+ if (typeof value === "string") {
+ return value.trim();
+ }
+ if (
+ typeof value === "number" ||
+ typeof value === "boolean" ||
+ typeof value === "bigint"
+ ) {
+ return String(value);
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return String(value);
+ }
+
+ function summarizeArray(list) {
+ if (!Array.isArray(list) || list.length === 0) {
+ return "";
+ }
+ const allScalar = list.every(
+ (item) =>
+ item === null ||
+ item === undefined ||
+ ["string", "number", "boolean", "bigint"].includes(typeof item),
+ );
+ if (allScalar) {
+ return list.map(normalizeScalar).filter(Boolean).join(" | ");
+ }
+ return list
+ .map((item) => {
+ if (isPlainObject(item) || Array.isArray(item)) {
+ try {
+ return JSON.stringify(item);
+ } catch (error) {
+ return String(item);
+ }
+ }
+ return normalizeScalar(item);
+ })
+ .filter(Boolean)
+ .join(" | ");
+ }
+
+ function flattenRecord(record, prefix, target) {
+ const baseTarget = target || {};
+ const currentPrefix = prefix || "";
+
+ if (!isPlainObject(record)) {
+ if (currentPrefix) {
+ baseTarget[currentPrefix] = normalizeScalar(record);
+ }
+ return baseTarget;
+ }
+
+ const keys = Object.keys(record);
+ if (keys.length === 0 && currentPrefix) {
+ baseTarget[currentPrefix] = "";
+ return baseTarget;
+ }
+
+ for (const key of keys) {
+ const nextPath = currentPrefix ? `${currentPrefix}.${key}` : key;
+ const value = record[key];
+
+ if (Array.isArray(value)) {
+ baseTarget[nextPath] = summarizeArray(value);
+ continue;
+ }
+
+ if (isPlainObject(value)) {
+ flattenRecord(value, nextPath, baseTarget);
+ continue;
+ }
+
+ baseTarget[nextPath] = normalizeScalar(value);
+ }
+
+ return baseTarget;
+ }
+
+ function extractBloggerId(value) {
+ const raw = normalizeScalar(value);
+ if (!raw) {
+ return "";
+ }
+
+ if (/^[0-9a-f]{24}$/i.test(raw)) {
+ return raw;
+ }
+
+ if (!/^https?:\/\//i.test(raw)) {
+ return "";
+ }
+
+ let parsedUrl;
+ try {
+ parsedUrl = new URL(raw);
+ } catch (error) {
+ return "";
+ }
+
+ const queryCandidates = ["id", "user_id", "userId", "bloggerId", "creatorId"];
+ for (const key of queryCandidates) {
+ const queryValue = parsedUrl.searchParams.get(key);
+ if (queryValue && /^[0-9a-f]{24}$/i.test(queryValue)) {
+ return queryValue;
+ }
+ }
+
+ const segments = parsedUrl.pathname
+ .split("/")
+ .map((segment) => segment.trim())
+ .filter(Boolean)
+ .reverse();
+
+ for (const segment of segments) {
+ if (/^[0-9a-f]{24}$/i.test(segment)) {
+ return segment;
+ }
+ }
+
+ return "";
+ }
+
+ function parseCreatorInputs(rawInput) {
+ const values = normalizeScalar(rawInput)
+ .split(/[\n,,\s]+/)
+ .map((item) => item.trim())
+ .filter(Boolean);
+
+ const ids = [];
+ const seen = new Set();
+
+ for (const value of values) {
+ const id = extractBloggerId(value);
+ if (!id || seen.has(id)) {
+ continue;
+ }
+ seen.add(id);
+ ids.push(id);
+ }
+
+ return ids;
+ }
+
+ function buildFieldOptions(records) {
+ const fieldMap = new Map();
+
+ for (const record of records) {
+ const flattened = record.flattened || {};
+ for (const path of Object.keys(flattened)) {
+ if (!FIELD_LABEL_MAP[path]) {
+ continue;
+ }
+ if (!fieldMap.has(path)) {
+ fieldMap.set(path, {
+ path,
+ label: getFieldLabel(path),
+ });
+ }
+ }
+ }
+
+ return Array.from(fieldMap.values()).sort((left, right) =>
+ left.path.localeCompare(right.path, "zh-CN"),
+ );
+ }
+
+ function buildSelectableFieldOptions() {
+ return SELECTABLE_FIELD_PATHS.map((path) => ({
+ path,
+ label: getFieldLabel(path),
+ }));
+ }
+
+ function getFieldLabel(path) {
+ if (FIELD_LABEL_MAP[path]) {
+ return FIELD_LABEL_MAP[path];
+ }
+
+ for (const [namespace, namespaceLabel] of Object.entries(NAMESPACE_LABEL_MAP)) {
+ if (path === namespace) {
+ return namespaceLabel;
+ }
+ if (path.startsWith(`${namespace}.`)) {
+ return `${namespaceLabel} - ${path.slice(namespace.length + 1)}`;
+ }
+ }
+
+ return path;
+ }
+
+ function pickDefaultFields(fieldOptions) {
+ return fieldOptions.slice(0, 12).map((field) => field.path);
+ }
+
+ function buildExportRows(records, selectedFields) {
+ return records.map((record) => {
+ const row = {};
+ for (const field of selectedFields) {
+ row[field] = record.flattened[field] || "";
+ }
+ return row;
+ });
+ }
+
+ function escapeCsvValue(value) {
+ const text = normalizeScalar(value);
+ if (/["\n,\r]/.test(text)) {
+ return `"${text.replace(/"/g, '""')}"`;
+ }
+ return text;
+ }
+
+ function buildCsvContent(config) {
+ const columns = Array.isArray(config.columns) ? config.columns : [];
+ const headers =
+ Array.isArray(config.headers) && config.headers.length === columns.length
+ ? config.headers
+ : columns;
+ const rows = Array.isArray(config.rows) ? config.rows : [];
+ const headerLine = headers.map(escapeCsvValue).join(",");
+ const bodyLines = rows.map((row) =>
+ columns
+ .map((column) => escapeCsvValue(row[column] === undefined ? "" : row[column]))
+ .join(","),
+ );
+
+ return `\uFEFF${[headerLine, ...bodyLines].join("\r\n")}`;
+ }
+
+ function buildSpreadsheetXml(config) {
+ const sheetName = typeof config.sheetName === "string" ? config.sheetName : "达人数据";
+ const columns = Array.isArray(config.columns) ? config.columns : [];
+ const headers =
+ Array.isArray(config.headers) && config.headers.length === columns.length
+ ? config.headers
+ : columns;
+ const rows = Array.isArray(config.rows) ? config.rows : [];
+ const headerCells = columns
+ .map(
+ (column, index) =>
+ `| ${escapeXml(headers[index] ?? column)} | `,
+ )
+ .join("");
+
+ const dataRows = rows
+ .map((row) => {
+ const cells = columns
+ .map((column) => {
+ const value = row[column] === undefined ? "" : row[column];
+ return `${escapeXml(value)} | `;
+ })
+ .join("");
+ return `${cells}
`;
+ })
+ .join("");
+
+ return `
+
+
+
+
+ ${headerCells}
+ ${dataRows}
+
+
+`;
+ }
+
+ function formatTimestamp(date) {
+ const safeDate = date instanceof Date ? date : new Date();
+ const parts = [
+ safeDate.getFullYear(),
+ String(safeDate.getMonth() + 1).padStart(2, "0"),
+ String(safeDate.getDate()).padStart(2, "0"),
+ "-",
+ String(safeDate.getHours()).padStart(2, "0"),
+ String(safeDate.getMinutes()).padStart(2, "0"),
+ String(safeDate.getSeconds()).padStart(2, "0"),
+ ];
+ return parts.join("");
+ }
+
+ function unwrapResponsePayload(json) {
+ if (isPlainObject(json?.data)) {
+ return json.data;
+ }
+ if (isPlainObject(json?.result)) {
+ return json.result;
+ }
+ if (isPlainObject(json)) {
+ return json;
+ }
+ return { value: json };
+ }
+
+ async function fetchBloggerRecord(id, fetchImpl) {
+ if (typeof fetchImpl !== "function") {
+ throw new Error("当前环境不支持 fetch,无法请求达人数据。");
+ }
+
+ const response = await fetchImpl(`${API_BASE}${encodeURIComponent(id)}`, {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ accept: "application/json, text/plain, */*",
+ },
+ });
+
+ if (!response || !response.ok) {
+ const status = response ? response.status : "unknown";
+ throw new Error(`请求达人 ${id} 失败,状态码:${status}`);
+ }
+
+ const json = await response.json();
+ const payload = unwrapResponsePayload(json);
+ if (!Object.prototype.hasOwnProperty.call(payload, "id")) {
+ payload.id = id;
+ }
+ return payload;
+ }
+
+ async function fetchSupplementalPayload(userId, fetchImpl, config) {
+ const response = await fetchImpl(config.buildUrl(userId), {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ accept: "application/json, text/plain, */*",
+ },
+ });
+
+ if (!response || !response.ok) {
+ const status = response ? response.status : "unknown";
+ throw new Error(
+ `请求补充数据 ${config.namespace} 失败,userId=${userId},状态码:${status}`,
+ );
+ }
+
+ const json = await response.json();
+ return unwrapResponsePayload(json);
+ }
+
+ async function fetchMergedBloggerRecord(id, fetchImpl) {
+ const primaryPayload = await fetchBloggerRecord(id, fetchImpl);
+ const userId = primaryPayload.userId || primaryPayload.id || id;
+
+ const settledPayloads = await Promise.allSettled(
+ SUPPLEMENTAL_ENDPOINTS.map((config) =>
+ fetchSupplementalPayload(userId, fetchImpl, config).then((payload) => ({
+ namespace: config.namespace,
+ payload,
+ })),
+ ),
+ );
+
+ const mergedPayload = {
+ ...primaryPayload,
+ };
+
+ for (const result of settledPayloads) {
+ if (result.status !== "fulfilled") {
+ continue;
+ }
+ mergedPayload[result.value.namespace] = result.value.payload;
+ }
+
+ return mergedPayload;
+ }
+
+ function createExportController(options) {
+ const settings = options || {};
+ const now = settings.now || (() => new Date());
+ const fetchImpl =
+ settings.fetchImpl || (typeof root.fetch === "function" ? root.fetch.bind(root) : null);
+ let cachedRecords = [];
+ let cachedFields = [];
+
+ return {
+ async preview(rawInput, onProgress) {
+ const ids = parseCreatorInputs(rawInput);
+ if (!ids.length) {
+ throw new Error("请输入至少一个有效的达人主页链接或达人 ID。");
+ }
+
+ const report = (current, total) => {
+ if (typeof onProgress === "function") {
+ onProgress(current, total);
+ }
+ };
+
+ const records = [];
+ report(0, ids.length);
+ for (const id of ids) {
+ const raw = await fetchMergedBloggerRecord(id, fetchImpl);
+ records.push({
+ id,
+ raw,
+ flattened: flattenRecord(raw),
+ });
+ report(records.length, ids.length);
+ }
+
+ cachedRecords = records;
+ cachedFields = buildFieldOptions(records);
+
+ return {
+ ids,
+ records,
+ fields: cachedFields,
+ selectedFields: pickDefaultFields(cachedFields),
+ };
+ },
+
+ exportSheet(selectedFields) {
+ if (!cachedRecords.length) {
+ throw new Error("请先读取字段并确认达人数据。");
+ }
+
+ const fields =
+ Array.isArray(selectedFields) && selectedFields.length
+ ? selectedFields
+ : cachedFields.map((field) => field.path);
+
+ const rows = buildExportRows(cachedRecords, fields);
+ const headers = fields.map((field) => getFieldLabel(field));
+ const content = buildSpreadsheetXml({
+ columns: fields,
+ headers,
+ rows,
+ sheetName: "达人数据",
+ });
+
+ return {
+ filename: `xhs-bloggers-${formatTimestamp(now())}.xls`,
+ columns: fields,
+ headers,
+ rows,
+ content,
+ };
+ },
+
+ async exportSheetAsync(selectedFields, onProgress) {
+ if (!cachedRecords.length) {
+ throw new Error("请先读取字段并确认达人数据。");
+ }
+
+ const fields =
+ Array.isArray(selectedFields) && selectedFields.length
+ ? selectedFields
+ : cachedFields.map((field) => field.path);
+
+ const headers = fields.map((field) => getFieldLabel(field));
+ const total = cachedRecords.length;
+ const headerCells = headers
+ .map((header) => `${escapeXml(header)} | `)
+ .join("");
+ const parts = [
+ `\n\n\n \n \n ${headerCells}
\n`,
+ ];
+
+ const report = (percentage, message) => {
+ if (typeof onProgress !== "function") {
+ return;
+ }
+ onProgress(Math.max(0, Math.min(100, percentage)), message || "");
+ };
+
+ report(0, "正在生成 Excel...");
+
+ const yieldEvery = 50;
+ for (let index = 0; index < total; index += 1) {
+ const record = cachedRecords[index];
+ const cells = fields
+ .map((field) => {
+ const value =
+ record && record.flattened && record.flattened[field] !== undefined
+ ? record.flattened[field]
+ : "";
+ return `${escapeXml(value)} | `;
+ })
+ .join("");
+ parts.push(` ${cells}
\n`);
+
+ const isLast = index === total - 1;
+ if (isLast || (index + 1) % yieldEvery === 0) {
+ const pct = total ? Math.floor(((index + 1) / total) * 100) : 100;
+ report(pct, `正在生成 ${index + 1}/${total}`);
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+ }
+
+ parts.push("
\n \n");
+ const content = parts.join("");
+
+ return {
+ filename: `xhs-bloggers-${formatTimestamp(now())}.xls`,
+ columns: fields,
+ headers,
+ content,
+ rowCount: total,
+ };
+ },
+
+ getState() {
+ return {
+ records: cachedRecords.slice(),
+ fields: cachedFields.slice(),
+ };
+ },
+ };
+ }
+
+ function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ function saveLocal(key, value) {
+ try {
+ root.localStorage.setItem(key, JSON.stringify(value));
+ } catch (error) {
+ return;
+ }
+ }
+
+ function loadLocal(key, fallbackValue) {
+ try {
+ const raw = root.localStorage.getItem(key);
+ if (!raw) {
+ return fallbackValue;
+ }
+ return JSON.parse(raw);
+ } catch (error) {
+ return fallbackValue;
+ }
+ }
+
+ function downloadFile(filename, content) {
+ const blob = new Blob([content], {
+ type: "application/vnd.ms-excel;charset=utf-8",
+ });
+ const link = root.document.createElement("a");
+ const blobUrl = root.URL.createObjectURL(blob);
+ link.href = blobUrl;
+ link.download = filename;
+ root.document.body.appendChild(link);
+ link.click();
+ link.remove();
+ root.URL.revokeObjectURL(blobUrl);
+ }
+
+ function injectStyles(doc) {
+ if (doc.getElementById("xhs-pgy-export-style")) {
+ return;
+ }
+
+ const style = doc.createElement("style");
+ style.id = "xhs-pgy-export-style";
+ style.textContent = `
+ .xhs-export-toggle {
+ position: fixed;
+ right: 24px;
+ bottom: 24px;
+ z-index: 99999;
+ border: 0;
+ border-radius: 999px;
+ padding: 12px 18px;
+ font-size: 14px;
+ font-weight: 700;
+ color: #fff8eb;
+ background: linear-gradient(135deg, #f45d01, #d72638);
+ box-shadow: 0 12px 28px rgba(187, 61, 14, 0.28);
+ cursor: pointer;
+ }
+
+ .xhs-export-panel {
+ position: fixed;
+ right: 24px;
+ bottom: 84px;
+ z-index: 99999;
+ width: min(420px, calc(100vw - 32px));
+ max-height: calc(100vh - 120px);
+ overflow: hidden;
+ display: none;
+ flex-direction: column;
+ border-radius: 20px;
+ background:
+ radial-gradient(circle at top right, rgba(255, 229, 205, 0.95), rgba(255, 245, 236, 0.98) 46%),
+ linear-gradient(160deg, rgba(255, 250, 246, 0.98), rgba(255, 238, 225, 0.98));
+ color: #31241d;
+ box-shadow: 0 24px 60px rgba(76, 34, 15, 0.22);
+ border: 1px solid rgba(190, 110, 61, 0.18);
+ font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
+ }
+
+ .xhs-export-panel.is-open {
+ display: flex;
+ }
+
+ .xhs-export-header {
+ padding: 18px 18px 10px;
+ }
+
+ .xhs-export-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 700;
+ }
+
+ .xhs-export-subtitle {
+ margin: 8px 0 0;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #7c5b48;
+ }
+
+ .xhs-export-body {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 0 18px 92px;
+ overflow: auto;
+ }
+
+ .xhs-export-input {
+ min-height: 104px;
+ resize: vertical;
+ border: 1px solid rgba(141, 88, 51, 0.2);
+ border-radius: 14px;
+ padding: 12px 14px;
+ font-size: 13px;
+ line-height: 1.6;
+ background: rgba(255, 255, 255, 0.75);
+ color: #2e211a;
+ }
+
+ .xhs-export-actions,
+ .xhs-export-mini-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .xhs-export-actions {
+ align-items: center;
+ }
+
+ .xhs-export-btn {
+ border: 0;
+ border-radius: 12px;
+ padding: 10px 14px;
+ font-size: 13px;
+ font-weight: 700;
+ cursor: pointer;
+ }
+
+ .xhs-export-btn.primary {
+ background: linear-gradient(135deg, #ef6a00, #d72638);
+ color: #fff8ef;
+ }
+
+ .xhs-export-fab {
+ position: absolute;
+ right: 18px;
+ bottom: 18px;
+ z-index: 2;
+ border: 0;
+ border-radius: 999px;
+ padding: 14px 18px;
+ font-size: 14px;
+ font-weight: 900;
+ letter-spacing: 0.3px;
+ cursor: pointer;
+ color: #fff8ef;
+ background: linear-gradient(135deg, #ef6a00, #d72638);
+ box-shadow: 0 16px 34px rgba(187, 61, 14, 0.28);
+ }
+
+ .xhs-export-fab:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+ box-shadow: none;
+ }
+
+ .xhs-export-btn.secondary {
+ background: rgba(110, 67, 41, 0.08);
+ color: #5e412f;
+ }
+
+ .xhs-export-btn:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+ }
+
+ .xhs-export-status {
+ min-height: 20px;
+ font-size: 12px;
+ color: #6b4b39;
+ }
+
+ .xhs-export-status.is-error {
+ color: #bb2528;
+ }
+
+ .xhs-export-progress {
+ display: grid;
+ gap: 6px;
+ padding: 10px 12px;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(123, 83, 52, 0.12);
+ }
+
+ .xhs-export-progress.is-hidden {
+ display: none;
+ }
+
+ .xhs-export-progress.is-error .xhs-export-progress-meta {
+ color: #bb2528;
+ }
+
+ .xhs-export-progress-meta {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 12px;
+ color: #6b4b39;
+ }
+
+ .xhs-export-progress-track {
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(110, 67, 41, 0.12);
+ overflow: hidden;
+ }
+
+ .xhs-export-progress-bar {
+ height: 100%;
+ width: 0%;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #ef6a00, #d72638);
+ transition: width 120ms linear;
+ }
+
+ .xhs-export-progress.is-error .xhs-export-progress-bar {
+ background: linear-gradient(90deg, #bb2528, #8a0f14);
+ }
+
+ .xhs-export-field-select {
+ display: grid;
+ gap: 8px;
+ }
+
+ .xhs-export-select {
+ display: grid;
+ gap: 8px;
+ }
+
+ .xhs-export-select-trigger {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ border: 1px solid rgba(141, 88, 51, 0.2);
+ border-radius: 14px;
+ padding: 10px 12px;
+ background: rgba(255, 255, 255, 0.75);
+ color: #2e211a;
+ font-size: 13px;
+ font-weight: 800;
+ cursor: pointer;
+ }
+
+ .xhs-export-select-trigger::after {
+ content: "";
+ width: 10px;
+ height: 10px;
+ border-right: 2px solid rgba(110, 67, 41, 0.5);
+ border-bottom: 2px solid rgba(110, 67, 41, 0.5);
+ transform: rotate(45deg);
+ transition: transform 120ms ease;
+ flex: 0 0 auto;
+ }
+
+ .xhs-export-select.is-open .xhs-export-select-trigger::after {
+ transform: rotate(-135deg);
+ }
+
+ .xhs-export-select-panel {
+ display: none;
+ gap: 10px;
+ padding: 10px 12px 12px;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(123, 83, 52, 0.12);
+ }
+
+ .xhs-export-select.is-open .xhs-export-select-panel {
+ display: grid;
+ }
+
+ .xhs-export-select-search {
+ border: 1px solid rgba(141, 88, 51, 0.2);
+ border-radius: 12px;
+ padding: 9px 10px;
+ font-size: 12px;
+ background: rgba(255, 255, 255, 0.85);
+ color: #2e211a;
+ }
+
+ .xhs-export-select-list {
+ display: grid;
+ gap: 8px;
+ max-height: 280px;
+ overflow: auto;
+ padding: 2px;
+ }
+
+ .xhs-export-select-item {
+ display: grid;
+ gap: 4px;
+ padding: 10px 12px;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(123, 83, 52, 0.12);
+ }
+
+ .xhs-export-select-item[hidden] {
+ display: none;
+ }
+
+ .xhs-export-select-item-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .xhs-export-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 100000;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 18px;
+ background: rgba(20, 12, 8, 0.32);
+ backdrop-filter: blur(2px);
+ }
+
+ .xhs-export-modal-backdrop.is-open {
+ display: flex;
+ }
+
+ .xhs-export-modal {
+ width: min(320px, calc(100vw - 36px));
+ border-radius: 22px;
+ padding: 22px 18px 18px;
+ background: rgba(255, 255, 255, 0.96);
+ box-shadow: 0 30px 80px rgba(40, 18, 8, 0.25);
+ border: 1px solid rgba(190, 110, 61, 0.18);
+ text-align: center;
+ color: #31241d;
+ }
+
+ .xhs-export-modal-icon {
+ width: 150px;
+ height: 150px;
+ margin: 2px auto 10px;
+ }
+
+ .xhs-export-modal-title {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 800;
+ letter-spacing: 0.3px;
+ }
+
+ .xhs-export-modal-subtitle {
+ margin: 8px 0 0;
+ font-size: 12px;
+ line-height: 1.45;
+ color: #7c5b48;
+ word-break: break-all;
+ }
+
+ .xhs-export-modal-actions {
+ display: flex;
+ justify-content: center;
+ margin-top: 14px;
+ }
+
+ .xhs-export-modal-btn {
+ border: 0;
+ border-radius: 999px;
+ padding: 10px 16px;
+ font-size: 13px;
+ font-weight: 800;
+ cursor: pointer;
+ background: rgba(110, 67, 41, 0.08);
+ color: #5e412f;
+ }
+
+ .xhs-export-fields {
+ display: grid;
+ gap: 8px;
+ max-height: 320px;
+ overflow: auto;
+ padding: 2px;
+ }
+
+ .xhs-export-field {
+ display: grid;
+ gap: 4px;
+ padding: 10px 12px;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(123, 83, 52, 0.12);
+ }
+
+ .xhs-export-field-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .xhs-export-checkbox {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ flex: 0 0 auto;
+ }
+
+ .xhs-export-checkbox-input {
+ position: absolute;
+ inset: 0;
+ margin: 0;
+ opacity: 0;
+ cursor: pointer;
+ }
+
+ .xhs-export-checkbox-box {
+ width: 18px;
+ height: 18px;
+ border-radius: 6px;
+ border: 2px solid rgba(110, 67, 41, 0.28);
+ background: rgba(255, 255, 255, 0.82);
+ display: grid;
+ place-items: center;
+ transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
+ }
+
+ .xhs-export-checkbox-box::after {
+ content: "";
+ width: 9px;
+ height: 5px;
+ border-left: 2px solid #fff;
+ border-bottom: 2px solid #fff;
+ transform: rotate(-45deg);
+ opacity: 0;
+ transition: opacity 120ms ease;
+ }
+
+ .xhs-export-checkbox-input:checked + .xhs-export-checkbox-box {
+ border-color: transparent;
+ background: linear-gradient(135deg, #8edb7d, #4bbf73);
+ transform: translateY(-0.5px);
+ }
+
+ .xhs-export-checkbox-input:checked + .xhs-export-checkbox-box::after {
+ opacity: 1;
+ }
+
+ .xhs-export-field:focus-within {
+ outline: 2px solid rgba(110, 67, 41, 0.18);
+ outline-offset: 2px;
+ }
+
+ .xhs-export-field-name {
+ font-size: 13px;
+ font-weight: 700;
+ color: #34251d;
+ word-break: break-all;
+ }
+
+ .xhs-export-field-sample {
+ font-size: 11px;
+ color: #7a6152;
+ word-break: break-all;
+ }
+
+ @media (max-width: 768px) {
+ .xhs-export-toggle {
+ right: 16px;
+ bottom: 16px;
+ }
+
+ .xhs-export-panel {
+ right: 16px;
+ bottom: 72px;
+ width: calc(100vw - 20px);
+ }
+ }
+ `;
+ doc.head.appendChild(style);
+ }
+
+ function createPanel(doc) {
+ const toggle = doc.createElement("button");
+ toggle.className = "xhs-export-toggle";
+ toggle.textContent = "达人导出";
+
+ const panel = doc.createElement("section");
+ panel.className = "xhs-export-panel";
+ panel.innerHTML = `
+
+
+
+ `;
+
+ const modalBackdrop = doc.createElement("div");
+ modalBackdrop.className = "xhs-export-modal-backdrop";
+ modalBackdrop.setAttribute("role", "dialog");
+ modalBackdrop.setAttribute("aria-modal", "true");
+ modalBackdrop.setAttribute("aria-hidden", "true");
+ modalBackdrop.innerHTML = `
+
+ `;
+
+ doc.body.appendChild(toggle);
+ doc.body.appendChild(panel);
+ doc.body.appendChild(modalBackdrop);
+
+ return {
+ toggle,
+ panel,
+ input: panel.querySelector(".xhs-export-input"),
+ exportButton: panel.querySelector('[data-action="export"]'),
+ status: panel.querySelector(".xhs-export-status"),
+ progress: panel.querySelector(".xhs-export-progress"),
+ progressText: panel.querySelector(".xhs-export-progress-text"),
+ progressPct: panel.querySelector(".xhs-export-progress-pct"),
+ progressBar: panel.querySelector(".xhs-export-progress-bar"),
+ modalBackdrop,
+ modalSubtitle: modalBackdrop.querySelector(".xhs-export-modal-subtitle"),
+ modalCloseButton: modalBackdrop.querySelector(".xhs-export-modal-btn"),
+ fields: panel.querySelector(".xhs-export-field-select"),
+ };
+ }
+
+ function updateFieldSelectSummary(container) {
+ if (!container) {
+ return;
+ }
+ const trigger = container.querySelector(".xhs-export-select-trigger");
+ if (!trigger) {
+ return;
+ }
+ const checkedCount = container.querySelectorAll('input[type="checkbox"]:checked').length;
+ const totalCount = container.querySelectorAll('input[type="checkbox"]').length;
+ trigger.textContent = `可选字段(已选 ${checkedCount}/${totalCount}个字段)`;
+ }
+
+ function renderFields(container, fieldOptions, selectedFields) {
+ const selected = new Set(selectedFields);
+ container.innerHTML = "";
+
+ if (!fieldOptions.length) {
+ container.innerHTML = ``;
+ return;
+ }
+
+ const wrapper = root.document.createElement("div");
+ wrapper.className = "xhs-export-select";
+ wrapper.innerHTML = `
+
+
+ `;
+ container.appendChild(wrapper);
+
+ const trigger = wrapper.querySelector(".xhs-export-select-trigger");
+ const panel = wrapper.querySelector(".xhs-export-select-panel");
+ const search = wrapper.querySelector(".xhs-export-select-search");
+ const list = wrapper.querySelector(".xhs-export-select-list");
+
+ for (const field of fieldOptions) {
+ const labelText = field.label || field.path;
+ const item = root.document.createElement("label");
+ item.className = "xhs-export-select-item";
+ item.dataset.path = field.path;
+ item.dataset.label = labelText;
+ item.innerHTML = `
+
+
+
+
+
+ ${escapeXml(labelText)}
+
+ 映射字段:${escapeXml(field.path)}
+ `;
+ list.appendChild(item);
+ }
+
+ const setOpenState = (open) => {
+ wrapper.classList.toggle("is-open", Boolean(open));
+ if (trigger) {
+ trigger.setAttribute("aria-expanded", open ? "true" : "false");
+ }
+ if (open && search) {
+ search.focus();
+ }
+ };
+
+ if (trigger) {
+ trigger.addEventListener("click", () => {
+ setOpenState(!wrapper.classList.contains("is-open"));
+ });
+ }
+
+ if (search) {
+ search.addEventListener("input", () => {
+ const q = String(search.value || "").trim().toLowerCase();
+ for (const item of list.querySelectorAll(".xhs-export-select-item")) {
+ const label = String(item.dataset.label || "").toLowerCase();
+ const path = String(item.dataset.path || "").toLowerCase();
+ item.hidden = q ? !(label.includes(q) || path.includes(q)) : false;
+ }
+ });
+ }
+
+ wrapper.addEventListener("change", () => updateFieldSelectSummary(container));
+ updateFieldSelectSummary(container);
+ }
+
+ function getCheckedFields(container) {
+ return Array.from(container.querySelectorAll('input[type="checkbox"]:checked'))
+ .map((checkbox) => checkbox.value)
+ .filter(Boolean);
+ }
+
+ function setStatus(node, message, isError) {
+ node.textContent = message;
+ node.classList.toggle("is-error", Boolean(isError));
+ }
+
+ function hideProgress(refs) {
+ if (!refs || !refs.progress) {
+ return;
+ }
+ refs.progress.classList.add("is-hidden");
+ refs.progress.classList.remove("is-error");
+ if (refs.progressBar) {
+ refs.progressBar.style.width = "0%";
+ }
+ if (refs.progressPct) {
+ refs.progressPct.textContent = "0%";
+ }
+ if (refs.progressText) {
+ refs.progressText.textContent = "准备就绪";
+ }
+ }
+
+ function setProgress(refs, percentage, message, isError) {
+ if (!refs || !refs.progress) {
+ return;
+ }
+ const pct = Math.max(0, Math.min(100, Number(percentage) || 0));
+ refs.progress.classList.remove("is-hidden");
+ refs.progress.classList.toggle("is-error", Boolean(isError));
+ if (refs.progressBar) {
+ refs.progressBar.style.width = `${pct}%`;
+ }
+ if (refs.progressPct) {
+ refs.progressPct.textContent = `${Math.round(pct)}%`;
+ }
+ if (refs.progressText && typeof message === "string" && message) {
+ refs.progressText.textContent = message;
+ }
+ }
+
+ function closeModal(refs) {
+ if (!refs || !refs.modalBackdrop) {
+ return;
+ }
+ if (refs.modalTimer) {
+ clearTimeout(refs.modalTimer);
+ refs.modalTimer = null;
+ }
+ refs.modalBackdrop.classList.remove("is-open");
+ refs.modalBackdrop.setAttribute("aria-hidden", "true");
+ }
+
+ function openModal(refs, subtitle, autoCloseMs) {
+ if (!refs || !refs.modalBackdrop) {
+ return;
+ }
+ if (typeof subtitle === "string" && refs.modalSubtitle) {
+ refs.modalSubtitle.textContent = subtitle;
+ }
+ refs.modalBackdrop.classList.add("is-open");
+ refs.modalBackdrop.setAttribute("aria-hidden", "false");
+
+ if (refs.modalTimer) {
+ clearTimeout(refs.modalTimer);
+ }
+ const delay =
+ typeof autoCloseMs === "number" && Number.isFinite(autoCloseMs) && autoCloseMs > 0
+ ? autoCloseMs
+ : 2500;
+ refs.modalTimer = setTimeout(() => closeModal(refs), delay);
+ }
+
+ function bindUi(controller, refs) {
+ const persistedInput = loadLocal(STORAGE_INPUT_KEY, "");
+ const staticFields = buildSelectableFieldOptions();
+ const defaultSelectedFields = SELECTABLE_FIELD_PATHS.slice();
+
+ refs.input.value = typeof persistedInput === "string" ? persistedInput : "";
+ renderFields(
+ refs.fields,
+ staticFields,
+ defaultSelectedFields.length ? defaultSelectedFields : SELECTABLE_FIELD_PATHS.slice(),
+ );
+ closeModal(refs);
+
+ refs.modalCloseButton.addEventListener("click", () => closeModal(refs));
+ refs.modalBackdrop.addEventListener("click", (event) => {
+ if (event.target === refs.modalBackdrop) {
+ closeModal(refs);
+ }
+ });
+ root.document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape") {
+ closeModal(refs);
+ }
+ });
+
+ refs.toggle.addEventListener("click", () => {
+ refs.panel.classList.toggle("is-open");
+ });
+
+ refs.exportButton.addEventListener("click", async () => {
+ try {
+ const checkedFields = getCheckedFields(refs.fields);
+ if (!checkedFields.length) {
+ throw new Error("请至少勾选一个导出字段。");
+ }
+
+ refs.exportButton.disabled = true;
+ hideProgress(refs);
+ setProgress(refs, 0, "准备导出...", false);
+ setStatus(refs.status, "正在读取达人数据,请稍候...", false);
+
+ const rawInput = refs.input.value;
+ saveLocal(STORAGE_INPUT_KEY, rawInput);
+ await controller.preview(rawInput, (current, total) => {
+ const pct = total ? Math.floor((current / total) * 45) : 0;
+ setProgress(refs, pct, `正在读取达人数据 ${current}/${total || 0}`, false);
+ });
+ setStatus(refs.status, "正在生成导出文件...", false);
+ const result = await controller.exportSheetAsync(
+ checkedFields,
+ (percentage, message) =>
+ setProgress(
+ refs,
+ 45 + Math.floor((percentage * 55) / 100),
+ message || "正在生成导出文件...",
+ false,
+ ),
+ );
+ downloadFile(result.filename, result.content);
+ setProgress(refs, 100, "已触发下载", false);
+ openModal(refs, `文件:${result.filename}`, 2500);
+ setStatus(
+ refs.status,
+ `已导出 ${result.rowCount ?? (result.rows ? result.rows.length : 0)} 条达人数据,文件名:${result.filename}`,
+ false,
+ );
+ } catch (error) {
+ setProgress(refs, 100, "导出失败", true);
+ setStatus(refs.status, error.message || "导出失败。", true);
+ } finally {
+ refs.exportButton.disabled = false;
+ }
+ });
+ }
+
+ function mountUserscript() {
+ if (!root.document || root[SCRIPT_FLAG]) {
+ return;
+ }
+
+ root[SCRIPT_FLAG] = true;
+ injectStyles(root.document);
+ const refs = createPanel(root.document);
+ const controller = createExportController();
+ bindUi(controller, refs);
+ }
+
+ if (
+ root &&
+ root.document &&
+ root.location &&
+ /pgy\.xiaohongshu\.com$/i.test(root.location.hostname)
+ ) {
+ if (root.document.readyState === "loading") {
+ root.document.addEventListener("DOMContentLoaded", mountUserscript, {
+ once: true,
+ });
+ } else {
+ mountUserscript();
+ }
+ }
+
+ return {
+ API_BASE,
+ buildExportRows,
+ buildFieldOptions,
+ buildSpreadsheetXml,
+ createExportController,
+ extractBloggerId,
+ fetchMergedBloggerRecord,
+ flattenRecord,
+ getFieldLabel,
+ parseCreatorInputs,
+ };
+});