// ==UserScript== // @name 小红书蒲公英达人信息导出 // @namespace https://pgy.xiaohongshu.com/ // @version 0.1.1 // @description 输入达人主页链接或达人 ID,勾选字段后导出 Excel // @match https://pgy.xiaohongshu.com/* // @grant GM_xmlhttpRequest // @connect api.internal.intelligrow.cn // @connect xhslink.com // @require https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js // ==/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) { function gmFetch(url, options) { return new Promise((resolve, reject) => { const headers = options && options.headers ? options.headers : {}; GM_xmlhttpRequest({ method: (options && options.method) || "GET", url, headers, onload(res) { resolve({ ok: res.status >= 200 && res.status < 300, status: res.status, json: () => Promise.resolve(JSON.parse(res.responseText)), }); }, onerror(err) { reject(new Error("GM_xmlhttpRequest 网络错误: " + (err.statusText || url))); }, }); }); } const API_BASE = "https://pgy.xiaohongshu.com/api/solar/cooperator/user/blogger/"; const PROXY_API_BASE = "https://api.internal.intelligrow.cn"; const SUPPLEMENTAL_ENDPOINTS = [ { namespace: "fansProfile", buildUrl: (userId) => `https://pgy.xiaohongshu.com/api/solar/kol/data/${encodeURIComponent( userId, )}/fans_profile`, }, { namespace: "dataSummary", buildUrl: (userId) => `${PROXY_API_BASE}/v1/pugongying/data_summary?userId=${encodeURIComponent(userId)}`, extraHeaders: () => ({ "X-Cookie": root.document.cookie, "Authorization": "Bearer PsjpalaBZF2EVIyU5M7V9KHzUstOIN82LyMn9nqLekExyxIBnjjURlMKMDBSZwrG", }), }, { namespace: "fansSummary", buildUrl: (userId) => `${PROXY_API_BASE}/v1/pugongying/fans_summary?userId=${encodeURIComponent(userId)}`, extraHeaders: () => ({ "X-Cookie": root.document.cookie, "Authorization": "Bearer PsjpalaBZF2EVIyU5M7V9KHzUstOIN82LyMn9nqLekExyxIBnjjURlMKMDBSZwrG", }), }, ]; const NAMESPACE_LABEL_MAP = { fansProfile: "粉丝画像", dataSummary: "数据概览", fansSummary: "粉丝概览", }; const FIELD_LABEL_MAP = { id: "ID", dataSummary: "数据概览", "dataSummary.mCpuvNum": "外溢进店中位数", "dataSummary.estimatePictureCpm": "预估CPM(图文)", "dataSummary.estimateVideoCpm": "预估CPM(视频)", "dataSummary.picReadCost": "预估阅读单价(图文)", "dataSummary.videoReadCost": "预估阅读单价(视频)", "dataSummary.fans30GrowthRate": "粉丝量变化幅度", fansSummary: "粉丝概览", "fansSummary.activeFansRate": "活跃粉丝占比(%)", "fansSummary.readFansRate": "阅读粉丝占比(%)", "fansSummary.engageFansRate": "互动粉丝占比(%)", fansProfile: "粉丝画像", "fansProfile.ages.<18": "18岁以下粉丝占比", "fansProfile.ages.18-24": "18-24岁粉丝占比", "fansProfile.ages.25-34": "25-34岁粉丝占比", "fansProfile.ages.35-44": "35-44岁粉丝占比", "fansProfile.ages.>44": "44岁以上粉丝占比", "fansProfile.gender.male": "粉丝男性占比", "fansProfile.gender.female": "粉丝女性占比", userId: "达人ID", name: "达人昵称", redId: "小红书号", location: "地区", travelAreaList: "可接受的出行地", personalTags: "人设标签", fansCount: "粉丝数", likeCollectCountInfo: "获赞与收藏", businessNoteCount: "商业笔记数", 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 FIELD_GROUPS = [ { label: "达人信息", fields: SELECTABLE_FIELD_PATHS.filter( (p) => !p.startsWith("fansProfile.") && !p.startsWith("dataSummary.") && !p.startsWith("fansSummary."), ), }, { label: "数据概览", fields: SELECTABLE_FIELD_PATHS.filter((p) => p.startsWith("dataSummary.")), }, { label: "粉丝画像", fields: SELECTABLE_FIELD_PATHS.filter( (p) => p.startsWith("fansProfile.") || p.startsWith("fansSummary."), ), }, ]; 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)) { if ( value.length && value.every( (item) => isPlainObject(item) && "group" in item && "percent" in item, ) ) { for (const item of value) { baseTarget[`${nextPath}.${item.group}`] = item.percent; } } else { baseTarget[nextPath] = summarizeArray(value); } continue; } if (isPlainObject(value)) { flattenRecord(value, nextPath, baseTarget); continue; } baseTarget[nextPath] = normalizeScalar(value); } return baseTarget; } function resolveShortUrl(url) { return new Promise((resolve) => { if (typeof GM_xmlhttpRequest !== "function") { resolve(url); return; } GM_xmlhttpRequest({ method: "HEAD", url, onload(res) { resolve(res.finalUrl || url); }, onerror() { resolve(url); }, }); }); } const SHORT_LINK_HOSTS = ["xhslink.com"]; function extractIdFromUrl(parsedUrl) { 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 ""; } async 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 directId = extractIdFromUrl(parsedUrl); if (directId) { return directId; } if (SHORT_LINK_HOSTS.some((h) => parsedUrl.hostname.endsWith(h))) { const realUrl = await resolveShortUrl(raw); try { return extractIdFromUrl(new URL(realUrl)); } catch (error) { return ""; } } return ""; } async function parseCreatorInputs(rawInput) { const values = normalizeScalar(rawInput) .split(/[\n,,\s]+/) .map((item) => item.trim()) .filter(Boolean); const ids = []; const seen = new Set(); const resolved = await Promise.all(values.map((v) => extractBloggerId(v))); for (const id of resolved) { 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 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 extra = typeof config.extraHeaders === "function" ? config.extraHeaders() : {}; const hasExtra = Object.keys(extra).length > 0; const fetcher = hasExtra && typeof GM_xmlhttpRequest === "function" ? gmFetch : fetchImpl; const response = await fetcher(config.buildUrl(userId), { method: "GET", credentials: "include", headers: { accept: "application/json, text/plain, */*", ...extra, }, }); 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; } async function mapWithConcurrency(items, limit, mapper, onDone) { const list = Array.isArray(items) ? items : []; if (!list.length) { return []; } const size = Math.max(1, Number(limit) || 1); const workerCount = Math.min(size, list.length); const results = new Array(list.length); let nextIndex = 0; let doneCount = 0; const worker = async () => { while (true) { const index = nextIndex; nextIndex += 1; if (index >= list.length) { return; } results[index] = await mapper(list[index], index); doneCount += 1; if (typeof onDone === "function") { onDone(doneCount, list.length); } } }; const workers = Array.from({ length: workerCount }, () => worker()); await Promise.all(workers); return results; } 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); const concurrency = Math.max(1, Number(settings.concurrency) || 4); let cachedRecords = []; let cachedFields = []; return { async preview(rawInput, onProgress) { const ids = await parseCreatorInputs(rawInput); if (!ids.length) { throw new Error("请输入至少一个有效的达人主页链接或达人 ID。"); } const report = (current, total) => { if (typeof onProgress === "function") { onProgress(current, total); } }; report(0, ids.length); const records = await mapWithConcurrency( ids, concurrency, async (id) => { const raw = await fetchMergedBloggerRecord(id, fetchImpl); return { id, raw, flattened: flattenRecord(raw), }; }, (done, total) => report(done, total), ); 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 headers = fields.map((field) => getFieldLabel(field)); if (!root.XLSX) { throw new Error("未加载 SheetJS,无法导出 xlsx。"); } const aoa = [headers.slice()]; for (const record of cachedRecords) { aoa.push(fields.map((field) => record.flattened[field] || "")); } const ws = root.XLSX.utils.aoa_to_sheet(aoa); const wb = root.XLSX.utils.book_new(); root.XLSX.utils.book_append_sheet(wb, ws, "达人数据"); const content = root.XLSX.write(wb, { bookType: "xlsx", type: "array" }); return { filename: `xhs-bloggers-${formatTimestamp(now())}.xlsx`, columns: fields, headers, 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 report = (percentage, message) => { if (typeof onProgress !== "function") { return; } onProgress(Math.max(0, Math.min(100, percentage)), message || ""); }; report(0, "正在生成 Excel(.xlsx)..."); const aoa = [headers.slice()]; const yieldEvery = 50; for (let index = 0; index < total; index += 1) { const record = cachedRecords[index]; aoa.push(fields.map((field) => record.flattened[field] || "")); 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)); } } report(100, "正在打包 xlsx..."); const XLSX = await ensureXlsx(); const ws = XLSX.utils.aoa_to_sheet(aoa); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "达人数据"); const content = XLSX.write(wb, { bookType: "xlsx", type: "array" }); return { filename: `xhs-bloggers-${formatTimestamp(now())}.xlsx`, 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; } } const XLSX_CDN_URLS = [ "https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js", "https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js", "https://cdn.bootcdn.net/ajax/libs/xlsx/0.18.5/xlsx.full.min.js", ]; const loadedScripts = new Map(); function loadScript(url) { if (loadedScripts.has(url)) { return loadedScripts.get(url); } const promise = new Promise((resolve, reject) => { const script = root.document.createElement("script"); script.src = url; script.async = true; script.onload = () => resolve(); script.onerror = () => reject(new Error(`加载脚本失败:${url}`)); root.document.head.appendChild(script); }); loadedScripts.set(url, promise); return promise; } async function ensureXlsx() { if (root.XLSX && root.XLSX.utils && typeof root.XLSX.write === "function") { return root.XLSX; } for (const url of XLSX_CDN_URLS) { try { await loadScript(url); if (root.XLSX && root.XLSX.utils && typeof root.XLSX.write === "function") { return root.XLSX; } } catch (error) { // try next url } } throw new Error("加载 SheetJS 失败,可能被网络或页面 CSP 限制。"); } function downloadFile(filename, content) { const blob = new Blob([content], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); 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-group-header { font-size: 14px; font-weight: 800; color: #7a6152; padding: 8px 4px 4px; border-bottom: 1px solid rgba(141, 88, 51, 0.15); } .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 = `
输入达人主页链接或达人 ID,选择需要的 Excel 表头后直接导出。每行一个达人链接或达人 ID。