feat: update export automation scripts
This commit is contained in:
parent
6dcdbc1983
commit
07ff72eafa
@ -1,12 +1,14 @@
|
||||
// ==UserScript==
|
||||
// @name 小红书蒲公英达人信息导出
|
||||
// @namespace https://pgy.xiaohongshu.com/
|
||||
// @version 0.1.1
|
||||
// @description 输入达人主页链接或达人 ID,勾选字段后导出 Excel
|
||||
// @version 0.1.2
|
||||
// @author wangxuesheng
|
||||
// @description 输入达人主页链接或达人 ID,勾选字段后导出 xlsx 或飞书电子表格
|
||||
// @match https://pgy.xiaohongshu.com/*
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @connect api.internal.intelligrow.cn
|
||||
// @connect xhslink.com
|
||||
// @connect open.feishu.cn
|
||||
// @require https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js
|
||||
// ==/UserScript==
|
||||
|
||||
@ -19,15 +21,27 @@
|
||||
function gmFetch(url, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = options && options.headers ? options.headers : {};
|
||||
GM_xmlhttpRequest({
|
||||
const request =
|
||||
typeof GM_xmlhttpRequest === "function"
|
||||
? GM_xmlhttpRequest
|
||||
: root.GM && typeof root.GM.xmlHttpRequest === "function"
|
||||
? root.GM.xmlHttpRequest.bind(root.GM)
|
||||
: null;
|
||||
if (!request) {
|
||||
reject(new Error("当前脚本管理器不支持 GM_xmlhttpRequest,无法跨域请求。"));
|
||||
return;
|
||||
}
|
||||
request({
|
||||
method: (options && options.method) || "GET",
|
||||
url,
|
||||
headers,
|
||||
data: options && options.body,
|
||||
onload(res) {
|
||||
resolve({
|
||||
ok: res.status >= 200 && res.status < 300,
|
||||
status: res.status,
|
||||
json: () => Promise.resolve(JSON.parse(res.responseText)),
|
||||
json: () =>
|
||||
Promise.resolve(res.responseText ? JSON.parse(res.responseText) : {}),
|
||||
});
|
||||
},
|
||||
onerror(err) {
|
||||
@ -40,6 +54,9 @@
|
||||
const API_BASE =
|
||||
"https://pgy.xiaohongshu.com/api/solar/cooperator/user/blogger/";
|
||||
const PROXY_API_BASE = "https://api.internal.intelligrow.cn";
|
||||
const FEISHU_OPEN_API_BASE = "https://open.feishu.cn/open-apis";
|
||||
const FEISHU_APP_ID_STORAGE_KEY = "xhs-pgy-export:feishu-app-id";
|
||||
const FEISHU_APP_SECRET_STORAGE_KEY = "xhs-pgy-export:feishu-app-secret";
|
||||
const SUPPLEMENTAL_ENDPOINTS = [
|
||||
{
|
||||
namespace: "fansProfile",
|
||||
@ -135,6 +152,13 @@
|
||||
const STORAGE_INPUT_KEY = "xhs-pgy-export:last-input";
|
||||
const SCRIPT_FLAG = "__xhsPgyExportMounted__";
|
||||
|
||||
function hasGmRequest() {
|
||||
return (
|
||||
typeof GM_xmlhttpRequest === "function" ||
|
||||
Boolean(root.GM && typeof root.GM.xmlHttpRequest === "function")
|
||||
);
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === "[object Object]";
|
||||
}
|
||||
@ -410,6 +434,51 @@
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeCellValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function buildFeishuSheetValues(records, selectedFields) {
|
||||
const fields = Array.isArray(selectedFields) ? selectedFields : [];
|
||||
const values = [fields.map((field) => getFieldLabel(field))];
|
||||
const list = Array.isArray(records) ? records : [];
|
||||
for (const record of list) {
|
||||
const flattened = record && record.flattened ? record.flattened : {};
|
||||
values.push(fields.map((field) => normalizeCellValue(flattened[field])));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function columnIndexToName(index) {
|
||||
let value = Math.max(1, Number(index) || 1);
|
||||
let name = "";
|
||||
while (value > 0) {
|
||||
const remainder = (value - 1) % 26;
|
||||
name = String.fromCharCode(65 + remainder) + name;
|
||||
value = Math.floor((value - 1) / 26);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function buildFeishuRange(sheetId, rowCount, columnCount) {
|
||||
const safeSheetId = normalizeScalar(sheetId) || "0";
|
||||
const safeRowCount = Math.max(1, Number(rowCount) || 1);
|
||||
const safeColumnCount = Math.max(1, Number(columnCount) || 1);
|
||||
return `${safeSheetId}!A1:${columnIndexToName(safeColumnCount)}${safeRowCount}`;
|
||||
}
|
||||
|
||||
function formatTimestamp(date) {
|
||||
const safeDate = date instanceof Date ? date : new Date();
|
||||
const parts = [
|
||||
@ -467,7 +536,7 @@
|
||||
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 fetcher = hasExtra && hasGmRequest() ? gmFetch : fetchImpl;
|
||||
const response = await fetcher(config.buildUrl(userId), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
@ -515,6 +584,207 @@
|
||||
return mergedPayload;
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response, actionName) {
|
||||
if (!response || !response.ok) {
|
||||
const status = response ? response.status : "unknown";
|
||||
throw new Error(`${actionName}失败,状态码:${status}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
if (Number(json && json.code) !== 0) {
|
||||
throw new Error(`${actionName}失败:${(json && (json.msg || json.message)) || "未知错误"}`);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
async function feishuApiRequest(path, options) {
|
||||
const settings = options || {};
|
||||
const fetchImpl =
|
||||
settings.fetchImpl ||
|
||||
(hasGmRequest() ? gmFetch : null) ||
|
||||
(typeof root.fetch === "function" ? root.fetch.bind(root) : null);
|
||||
if (typeof fetchImpl !== "function") {
|
||||
throw new Error("当前环境不支持 fetch,无法请求飞书接口。");
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
...(settings.headers || {}),
|
||||
};
|
||||
const response = await fetchImpl(`${FEISHU_OPEN_API_BASE}${path}`, {
|
||||
method: settings.method || "GET",
|
||||
headers,
|
||||
body: settings.body === undefined ? undefined : JSON.stringify(settings.body),
|
||||
});
|
||||
return parseJsonResponse(response, settings.actionName || "请求飞书接口");
|
||||
}
|
||||
|
||||
async function getFeishuTenantAccessToken(options) {
|
||||
const settings = options || {};
|
||||
const appId = settings.appId;
|
||||
const appSecret = settings.appSecret;
|
||||
if (!appId || !appSecret) {
|
||||
throw new Error("缺少飞书应用 app_id 或 app_secret。");
|
||||
}
|
||||
|
||||
const json = await feishuApiRequest("/auth/v3/tenant_access_token/internal", {
|
||||
method: "POST",
|
||||
body: {
|
||||
app_id: appId,
|
||||
app_secret: appSecret,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "获取飞书应用访问凭证",
|
||||
});
|
||||
|
||||
if (!json.tenant_access_token) {
|
||||
throw new Error("获取飞书应用访问凭证失败:响应中缺少 tenant_access_token。");
|
||||
}
|
||||
return json.tenant_access_token;
|
||||
}
|
||||
|
||||
async function createFeishuSpreadsheet(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
if (!token) {
|
||||
throw new Error("缺少飞书 tenant_access_token,无法创建电子表格。");
|
||||
}
|
||||
|
||||
const json = await feishuApiRequest("/sheets/v3/spreadsheets", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: {
|
||||
title: settings.title || `蒲公英达人导出-${formatTimestamp(new Date())}`,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "创建飞书电子表格",
|
||||
});
|
||||
|
||||
const spreadsheet = json?.data?.spreadsheet || json?.data || {};
|
||||
const spreadsheetToken =
|
||||
spreadsheet.spreadsheet_token || spreadsheet.token || json?.data?.spreadsheet_token;
|
||||
if (!spreadsheetToken) {
|
||||
throw new Error("创建飞书电子表格失败:响应中缺少 spreadsheet_token。");
|
||||
}
|
||||
|
||||
return {
|
||||
spreadsheetToken,
|
||||
url: spreadsheet.url || json?.data?.url || "",
|
||||
};
|
||||
}
|
||||
|
||||
async function getFeishuFirstSheetId(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
const spreadsheetToken = settings.spreadsheetToken;
|
||||
if (!token || !spreadsheetToken) {
|
||||
throw new Error("缺少飞书表格访问参数,无法获取工作表信息。");
|
||||
}
|
||||
|
||||
const json = await feishuApiRequest(
|
||||
`/sheets/v2/spreadsheets/${encodeURIComponent(spreadsheetToken)}/metainfo`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "获取飞书工作表信息",
|
||||
},
|
||||
);
|
||||
|
||||
const sheets = json?.data?.sheets || [];
|
||||
const firstSheet = sheets[0] || {};
|
||||
const sheetId = firstSheet.sheetId || firstSheet.sheet_id || firstSheet.id;
|
||||
if (!sheetId) {
|
||||
throw new Error("获取飞书工作表信息失败:响应中缺少 sheetId。");
|
||||
}
|
||||
return sheetId;
|
||||
}
|
||||
|
||||
async function writeFeishuSheetValues(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
const spreadsheetToken = settings.spreadsheetToken;
|
||||
const sheetId = settings.sheetId;
|
||||
const values = Array.isArray(settings.values) ? settings.values : [];
|
||||
if (!token || !spreadsheetToken || !sheetId) {
|
||||
throw new Error("缺少飞书表格写入参数。");
|
||||
}
|
||||
if (!values.length) {
|
||||
throw new Error("没有可写入飞书电子表格的数据。");
|
||||
}
|
||||
|
||||
const range = buildFeishuRange(sheetId, values.length, values[0]?.length || 1);
|
||||
await feishuApiRequest(
|
||||
`/sheets/v2/spreadsheets/${encodeURIComponent(spreadsheetToken)}/values_batch_update`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: {
|
||||
valueRanges: [
|
||||
{
|
||||
range,
|
||||
values,
|
||||
},
|
||||
],
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "写入飞书电子表格",
|
||||
},
|
||||
);
|
||||
|
||||
return { range };
|
||||
}
|
||||
|
||||
async function exportRecordsToFeishuSpreadsheet(options) {
|
||||
const settings = options || {};
|
||||
const records = Array.isArray(settings.records) ? settings.records : [];
|
||||
const fields = Array.isArray(settings.fields) ? settings.fields : [];
|
||||
if (!records.length) {
|
||||
throw new Error("没有可导出的达人数据,请先读取数据。");
|
||||
}
|
||||
if (!fields.length) {
|
||||
throw new Error("请至少勾选一个导出字段。");
|
||||
}
|
||||
|
||||
const fetchImpl = settings.fetchImpl;
|
||||
const tenantAccessToken = await getFeishuTenantAccessToken({
|
||||
appId: settings.appId,
|
||||
appSecret: settings.appSecret,
|
||||
fetchImpl,
|
||||
});
|
||||
const spreadsheet = await createFeishuSpreadsheet({
|
||||
tenantAccessToken,
|
||||
title: settings.title,
|
||||
fetchImpl,
|
||||
});
|
||||
const sheetId = await getFeishuFirstSheetId({
|
||||
tenantAccessToken,
|
||||
spreadsheetToken: spreadsheet.spreadsheetToken,
|
||||
fetchImpl,
|
||||
});
|
||||
const values = buildFeishuSheetValues(records, fields);
|
||||
const writeResult = await writeFeishuSheetValues({
|
||||
tenantAccessToken,
|
||||
spreadsheetToken: spreadsheet.spreadsheetToken,
|
||||
sheetId,
|
||||
values,
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
return {
|
||||
...spreadsheet,
|
||||
sheetId,
|
||||
rowCount: records.length,
|
||||
range: writeResult.range,
|
||||
};
|
||||
}
|
||||
|
||||
async function mapWithConcurrency(items, limit, mapper, onDone) {
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
if (!list.length) {
|
||||
@ -678,6 +948,37 @@
|
||||
};
|
||||
},
|
||||
|
||||
async exportFeishuSpreadsheet(selectedFields, onProgress) {
|
||||
if (!cachedRecords.length) {
|
||||
throw new Error("请先读取字段并确认达人数据。");
|
||||
}
|
||||
|
||||
const fields =
|
||||
Array.isArray(selectedFields) && selectedFields.length
|
||||
? selectedFields
|
||||
: cachedFields.map((field) => field.path);
|
||||
|
||||
const report = (percentage, message) => {
|
||||
if (typeof onProgress !== "function") {
|
||||
return;
|
||||
}
|
||||
onProgress(Math.max(0, Math.min(100, percentage)), message || "");
|
||||
};
|
||||
|
||||
report(0, "正在获取飞书应用访问凭证...");
|
||||
const credentials = resolveFeishuCredentials(settings);
|
||||
const result = await exportRecordsToFeishuSpreadsheet({
|
||||
appId: credentials.appId,
|
||||
appSecret: credentials.appSecret,
|
||||
title: `蒲公英达人导出-${formatTimestamp(now())}`,
|
||||
records: cachedRecords,
|
||||
fields,
|
||||
fetchImpl: hasGmRequest() ? undefined : fetchImpl,
|
||||
});
|
||||
report(100, "已写入飞书电子表格");
|
||||
return result;
|
||||
},
|
||||
|
||||
getState() {
|
||||
return {
|
||||
records: cachedRecords.slice(),
|
||||
@ -716,6 +1017,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFeishuCredentials(settings) {
|
||||
const options = settings || {};
|
||||
return {
|
||||
appId: options.feishuAppId || loadLocal(FEISHU_APP_ID_STORAGE_KEY, ""),
|
||||
appSecret: options.feishuAppSecret || loadLocal(FEISHU_APP_SECRET_STORAGE_KEY, ""),
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
@ -897,6 +1206,13 @@
|
||||
box-shadow: 0 16px 34px rgba(187, 61, 14, 0.28);
|
||||
}
|
||||
|
||||
.xhs-export-fab.local {
|
||||
right: 144px;
|
||||
color: #5e412f;
|
||||
background: rgba(110, 67, 41, 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.xhs-export-fab:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
@ -1110,7 +1426,17 @@
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: #7c5b48;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.xhs-export-modal-link {
|
||||
color: #c8581c;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.xhs-export-modal-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.xhs-export-modal-actions {
|
||||
@ -1263,7 +1589,8 @@
|
||||
</div>
|
||||
<div class="xhs-export-field-select"></div>
|
||||
</div>
|
||||
<button class="xhs-export-fab" data-action="export">导出表格</button>
|
||||
<button class="xhs-export-fab local" data-action="export-local">下载 xlsx</button>
|
||||
<button class="xhs-export-fab" data-action="export-feishu">导出飞书</button>
|
||||
`;
|
||||
|
||||
const modalBackdrop = doc.createElement("div");
|
||||
@ -1279,7 +1606,7 @@
|
||||
<path d="M62 105 L92 132 L146 78" fill="none" stroke="#9adf86" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="xhs-export-modal-title">下载已完成</h3>
|
||||
<h3 class="xhs-export-modal-title">导出已完成</h3>
|
||||
<p class="xhs-export-modal-subtitle"></p>
|
||||
<div class="xhs-export-modal-actions">
|
||||
<button class="xhs-export-modal-btn" type="button">知道了</button>
|
||||
@ -1295,7 +1622,8 @@
|
||||
toggle,
|
||||
panel,
|
||||
input: panel.querySelector(".xhs-export-input"),
|
||||
exportButton: panel.querySelector('[data-action="export"]'),
|
||||
localExportButton: panel.querySelector('[data-action="export-local"]'),
|
||||
feishuExportButton: panel.querySelector('[data-action="export-feishu"]'),
|
||||
status: panel.querySelector(".xhs-export-status"),
|
||||
progress: panel.querySelector(".xhs-export-progress"),
|
||||
progressText: panel.querySelector(".xhs-export-progress-text"),
|
||||
@ -1474,24 +1802,67 @@
|
||||
refs.modalBackdrop.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
function openModal(refs, subtitle, autoCloseMs) {
|
||||
function openModal(refs, subtitle) {
|
||||
if (!refs || !refs.modalBackdrop) {
|
||||
return;
|
||||
}
|
||||
if (typeof subtitle === "string" && refs.modalSubtitle) {
|
||||
refs.modalSubtitle.textContent = subtitle;
|
||||
if (refs.modalSubtitle) {
|
||||
refs.modalSubtitle.replaceChildren();
|
||||
if (subtitle && typeof subtitle === "object" && typeof subtitle.nodeType === "number") {
|
||||
refs.modalSubtitle.appendChild(subtitle);
|
||||
} else if (typeof subtitle === "string") {
|
||||
refs.modalSubtitle.textContent = subtitle;
|
||||
}
|
||||
}
|
||||
refs.modalBackdrop.classList.add("is-open");
|
||||
refs.modalBackdrop.setAttribute("aria-hidden", "false");
|
||||
|
||||
if (refs.modalTimer) {
|
||||
clearTimeout(refs.modalTimer);
|
||||
refs.modalTimer = null;
|
||||
}
|
||||
const delay =
|
||||
typeof autoCloseMs === "number" && Number.isFinite(autoCloseMs) && autoCloseMs > 0
|
||||
? autoCloseMs
|
||||
: 2500;
|
||||
refs.modalTimer = setTimeout(() => closeModal(refs), delay);
|
||||
}
|
||||
|
||||
function buildFeishuSuccessModalContent(result) {
|
||||
const fragment = root.document.createDocumentFragment();
|
||||
const rowCount = result && result.rowCount;
|
||||
fragment.appendChild(root.document.createTextNode(`已导出 ${rowCount} 条数据:`));
|
||||
|
||||
const url = result && result.url;
|
||||
if (url) {
|
||||
const link = root.document.createElement("a");
|
||||
link.className = "xhs-export-modal-link";
|
||||
link.href = url;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
link.textContent = "飞书表格链接";
|
||||
fragment.appendChild(link);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
fragment.appendChild(
|
||||
root.document.createTextNode(`表格 token:${(result && result.spreadsheetToken) || ""}`),
|
||||
);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function setExportButtonsDisabled(refs, disabled) {
|
||||
if (refs.localExportButton) {
|
||||
refs.localExportButton.disabled = Boolean(disabled);
|
||||
}
|
||||
if (refs.feishuExportButton) {
|
||||
refs.feishuExportButton.disabled = Boolean(disabled);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareExportData(controller, refs, checkedFields, progressEnd) {
|
||||
const rawInput = refs.input.value;
|
||||
saveLocal(STORAGE_INPUT_KEY, rawInput);
|
||||
await controller.preview(rawInput, (current, total) => {
|
||||
const pct = total ? Math.floor((current / total) * progressEnd) : 0;
|
||||
setProgress(refs, pct, `正在读取达人数据 ${current}/${total || 0}`, false);
|
||||
});
|
||||
return checkedFields;
|
||||
}
|
||||
|
||||
function bindUi(controller, refs) {
|
||||
@ -1508,39 +1879,24 @@
|
||||
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 () => {
|
||||
refs.localExportButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const checkedFields = getCheckedFields(refs.fields);
|
||||
if (!checkedFields.length) {
|
||||
throw new Error("请至少勾选一个导出字段。");
|
||||
}
|
||||
|
||||
refs.exportButton.disabled = true;
|
||||
setExportButtonsDisabled(refs, 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);
|
||||
});
|
||||
await prepareExportData(controller, refs, checkedFields, 45);
|
||||
setStatus(refs.status, "正在生成导出文件...", false);
|
||||
const result = await controller.exportSheetAsync(
|
||||
checkedFields,
|
||||
@ -1554,7 +1910,7 @@
|
||||
);
|
||||
downloadFile(result.filename, result.content);
|
||||
setProgress(refs, 100, "已触发下载", false);
|
||||
openModal(refs, `文件:${result.filename}`, 2500);
|
||||
openModal(refs, `文件:${result.filename}`);
|
||||
setStatus(
|
||||
refs.status,
|
||||
`已导出 ${result.rowCount ?? (result.rows ? result.rows.length : 0)} 条达人数据,文件名:${result.filename}`,
|
||||
@ -1564,7 +1920,48 @@
|
||||
setProgress(refs, 100, "导出失败", true);
|
||||
setStatus(refs.status, error.message || "导出失败。", true);
|
||||
} finally {
|
||||
refs.exportButton.disabled = false;
|
||||
setExportButtonsDisabled(refs, false);
|
||||
}
|
||||
});
|
||||
|
||||
refs.feishuExportButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const checkedFields = getCheckedFields(refs.fields);
|
||||
if (!checkedFields.length) {
|
||||
throw new Error("请至少勾选一个导出字段。");
|
||||
}
|
||||
|
||||
setExportButtonsDisabled(refs, true);
|
||||
hideProgress(refs);
|
||||
setProgress(refs, 0, "准备导出到飞书...", false);
|
||||
setStatus(refs.status, "正在读取达人数据,请稍候...", false);
|
||||
|
||||
await prepareExportData(controller, refs, checkedFields, 40);
|
||||
setStatus(refs.status, "正在创建飞书电子表格并写入数据...", false);
|
||||
const result = await controller.exportFeishuSpreadsheet(
|
||||
checkedFields,
|
||||
(percentage, message) =>
|
||||
setProgress(
|
||||
refs,
|
||||
40 + Math.floor((percentage * 60) / 100),
|
||||
message || "正在写入飞书电子表格...",
|
||||
false,
|
||||
),
|
||||
);
|
||||
setProgress(refs, 100, "已写入飞书电子表格", false);
|
||||
openModal(refs, buildFeishuSuccessModalContent(result));
|
||||
setStatus(
|
||||
refs.status,
|
||||
result.url
|
||||
? `已导出 ${result.rowCount} 条达人数据到飞书:${result.url}`
|
||||
: `已导出 ${result.rowCount} 条达人数据到飞书,表格 token:${result.spreadsheetToken}`,
|
||||
false,
|
||||
);
|
||||
} catch (error) {
|
||||
setProgress(refs, 100, "导出飞书失败", true);
|
||||
setStatus(refs.status, error.message || "导出飞书失败。", true);
|
||||
} finally {
|
||||
setExportButtonsDisabled(refs, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1599,13 +1996,19 @@
|
||||
return {
|
||||
API_BASE,
|
||||
buildExportRows,
|
||||
buildFeishuRange,
|
||||
buildFeishuSheetValues,
|
||||
buildFieldOptions,
|
||||
buildSpreadsheetXml,
|
||||
createExportController,
|
||||
createFeishuSpreadsheet,
|
||||
extractBloggerId,
|
||||
exportRecordsToFeishuSpreadsheet,
|
||||
fetchMergedBloggerRecord,
|
||||
flattenRecord,
|
||||
getFieldLabel,
|
||||
getFeishuFirstSheetId,
|
||||
getFeishuTenantAccessToken,
|
||||
parseCreatorInputs,
|
||||
writeFeishuSheetValues,
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
// The userscript exports some helpers in a shorthand object; in Node that means any
|
||||
// undeclared identifiers referenced there must exist on `global`.
|
||||
global.buildSpreadsheetXml = () => "";
|
||||
global.document = { cookie: "x=y" };
|
||||
global.GM_xmlhttpRequest = undefined;
|
||||
|
||||
const api = require("./xhs-pgy-export.user.js");
|
||||
|
||||
@ -38,3 +36,268 @@ test("data_summary request includes business=1", async () => {
|
||||
assert.equal(parsed.searchParams.get("business"), "1");
|
||||
assert.equal(parsed.searchParams.get("userId"), "u-123");
|
||||
});
|
||||
|
||||
test("buildFeishuSheetValues uses labels as header row", () => {
|
||||
const records = [
|
||||
{
|
||||
flattened: {
|
||||
userId: "u-1",
|
||||
name: "达人 A",
|
||||
fansCount: 123,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const values = api.buildFeishuSheetValues(records, ["userId", "name", "fansCount"]);
|
||||
|
||||
assert.deepEqual(values, [
|
||||
["达人ID", "达人昵称", "粉丝数"],
|
||||
["u-1", "达人 A", 123],
|
||||
]);
|
||||
});
|
||||
|
||||
test("buildFeishuRange expands columns past Z", () => {
|
||||
assert.equal(api.buildFeishuRange("abc123", 1, 1), "abc123!A1:A1");
|
||||
assert.equal(api.buildFeishuRange("abc123", 2, 28), "abc123!A1:AB2");
|
||||
});
|
||||
|
||||
test("exportRecordsToFeishuSpreadsheet creates spreadsheet then writes values", async () => {
|
||||
const calls = [];
|
||||
|
||||
async function fetchImpl(url, options) {
|
||||
const body = options && options.body ? JSON.parse(options.body) : null;
|
||||
calls.push({
|
||||
url: String(url),
|
||||
method: options && options.method,
|
||||
headers: options && options.headers,
|
||||
body,
|
||||
});
|
||||
|
||||
if (String(url).includes("/auth/v3/tenant_access_token/internal")) {
|
||||
return okJson({ code: 0, tenant_access_token: "tenant-token" });
|
||||
}
|
||||
if (String(url).includes("/sheets/v3/spreadsheets")) {
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
spreadsheet: {
|
||||
spreadsheet_token: "spreadsheet-token",
|
||||
url: "https://feishu.example/spreadsheet-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/metainfo")) {
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
sheets: [
|
||||
{
|
||||
sheetId: "sheet-a",
|
||||
title: "Sheet1",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/values_batch_update")) {
|
||||
return okJson({ code: 0, data: {} });
|
||||
}
|
||||
|
||||
throw new Error(`unexpected url: ${url}`);
|
||||
}
|
||||
|
||||
const result = await api.exportRecordsToFeishuSpreadsheet({
|
||||
appId: "cli_xxx",
|
||||
appSecret: "secret",
|
||||
title: "测试导出",
|
||||
records: [
|
||||
{
|
||||
flattened: {
|
||||
userId: "u-1",
|
||||
name: "达人 A",
|
||||
},
|
||||
},
|
||||
],
|
||||
fields: ["userId", "name"],
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
assert.equal(result.spreadsheetToken, "spreadsheet-token");
|
||||
assert.equal(result.url, "https://feishu.example/spreadsheet-token");
|
||||
assert.equal(result.sheetId, "sheet-a");
|
||||
assert.equal(calls.length, 4);
|
||||
assert.deepEqual(calls[0].body, {
|
||||
app_id: "cli_xxx",
|
||||
app_secret: "secret",
|
||||
});
|
||||
assert.equal(calls[1].body.title, "测试导出");
|
||||
assert.equal(calls[2].method, "GET");
|
||||
assert.equal(calls[3].headers.Authorization, "Bearer tenant-token");
|
||||
assert.deepEqual(calls[3].body, {
|
||||
valueRanges: [
|
||||
{
|
||||
range: "sheet-a!A1:B2",
|
||||
values: [
|
||||
["达人ID", "达人昵称"],
|
||||
["u-1", "达人 A"],
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("controller reads Feishu credentials from localStorage instead of bundled secrets", async () => {
|
||||
const previousLocalStorage = global.localStorage;
|
||||
const previousGm = global.GM_xmlhttpRequest;
|
||||
const sentAuthBodies = [];
|
||||
|
||||
global.GM_xmlhttpRequest = undefined;
|
||||
global.localStorage = {
|
||||
getItem(key) {
|
||||
if (key === "xhs-pgy-export:feishu-app-id") {
|
||||
return JSON.stringify("cli_from_storage");
|
||||
}
|
||||
if (key === "xhs-pgy-export:feishu-app-secret") {
|
||||
return JSON.stringify("secret_from_storage");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
setItem() {},
|
||||
};
|
||||
|
||||
async function fetchImpl(url, options) {
|
||||
const body = options && options.body ? JSON.parse(options.body) : null;
|
||||
if (String(url).startsWith(api.API_BASE)) {
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
id: "60379f3c000000000101e53f",
|
||||
userId: "60379f3c000000000101e53f",
|
||||
name: "达人 A",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/auth/v3/tenant_access_token/internal")) {
|
||||
sentAuthBodies.push(body);
|
||||
return okJson({ code: 0, tenant_access_token: "tenant-token" });
|
||||
}
|
||||
if (String(url).includes("/sheets/v3/spreadsheets")) {
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
spreadsheet: {
|
||||
spreadsheet_token: "spreadsheet-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/metainfo")) {
|
||||
return okJson({ code: 0, data: { sheets: [{ sheetId: "sheet-a" }] } });
|
||||
}
|
||||
if (String(url).includes("/values_batch_update")) {
|
||||
return okJson({ code: 0, data: {} });
|
||||
}
|
||||
throw new Error(`unexpected url: ${url}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = api.createExportController({ fetchImpl });
|
||||
await controller.preview("60379f3c000000000101e53f");
|
||||
await controller.exportFeishuSpreadsheet(["userId", "name"]);
|
||||
|
||||
assert.deepEqual(sentAuthBodies, [
|
||||
{
|
||||
app_id: "cli_from_storage",
|
||||
app_secret: "secret_from_storage",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
global.localStorage = previousLocalStorage;
|
||||
global.GM_xmlhttpRequest = previousGm;
|
||||
}
|
||||
});
|
||||
|
||||
test("controller uses GM_xmlhttpRequest for Feishu even when page fetch is available", async () => {
|
||||
const gmUrls = [];
|
||||
const pageFetchUrls = [];
|
||||
const previousGm = global.GM_xmlhttpRequest;
|
||||
const previousLocalStorage = global.localStorage;
|
||||
|
||||
global.localStorage = {
|
||||
getItem(key) {
|
||||
if (key === "xhs-pgy-export:feishu-app-id") {
|
||||
return JSON.stringify("cli_from_storage");
|
||||
}
|
||||
if (key === "xhs-pgy-export:feishu-app-secret") {
|
||||
return JSON.stringify("secret_from_storage");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
setItem() {},
|
||||
};
|
||||
|
||||
global.GM_xmlhttpRequest = (options) => {
|
||||
gmUrls.push(options.url);
|
||||
let payload;
|
||||
if (String(options.url).includes("/auth/v3/tenant_access_token/internal")) {
|
||||
payload = { code: 0, tenant_access_token: "tenant-token" };
|
||||
} else if (String(options.url).includes("/sheets/v3/spreadsheets")) {
|
||||
payload = {
|
||||
code: 0,
|
||||
data: {
|
||||
spreadsheet: {
|
||||
spreadsheet_token: "spreadsheet-token",
|
||||
url: "https://feishu.example/spreadsheet-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (String(options.url).includes("/metainfo")) {
|
||||
payload = {
|
||||
code: 0,
|
||||
data: { sheets: [{ sheetId: "sheet-a" }] },
|
||||
};
|
||||
} else if (String(options.url).includes("/values_batch_update")) {
|
||||
payload = { code: 0, data: {} };
|
||||
} else {
|
||||
throw new Error(`unexpected GM url: ${options.url}`);
|
||||
}
|
||||
|
||||
options.onload({
|
||||
status: 200,
|
||||
responseText: JSON.stringify(payload),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
async function pageFetch(url) {
|
||||
pageFetchUrls.push(String(url));
|
||||
if (String(url).startsWith(api.API_BASE)) {
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
id: "60379f3c000000000101e53f",
|
||||
userId: "60379f3c000000000101e53f",
|
||||
name: "达人 A",
|
||||
},
|
||||
});
|
||||
}
|
||||
return okJson({ code: 0, data: {} });
|
||||
}
|
||||
|
||||
const controller = api.createExportController({ fetchImpl: pageFetch });
|
||||
await controller.preview("60379f3c000000000101e53f");
|
||||
const result = await controller.exportFeishuSpreadsheet(["userId", "name"]);
|
||||
|
||||
assert.equal(result.spreadsheetToken, "spreadsheet-token");
|
||||
assert.equal(gmUrls.filter((url) => String(url).includes("open.feishu.cn")).length, 4);
|
||||
assert.equal(
|
||||
pageFetchUrls.some((url) => url.includes("open.feishu.cn")),
|
||||
false,
|
||||
"Feishu requests should not use page fetch because browsers block cross-origin OpenAPI calls",
|
||||
);
|
||||
} finally {
|
||||
global.GM_xmlhttpRequest = previousGm;
|
||||
global.localStorage = previousLocalStorage;
|
||||
}
|
||||
});
|
||||
|
||||
@ -44,14 +44,24 @@ function extractReportId(reportInfo) {
|
||||
);
|
||||
}
|
||||
|
||||
function extractReportInfo(input) {
|
||||
if (!input.payload) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return ensureObject(input.payload, 'payload');
|
||||
}
|
||||
|
||||
function validateAndNormalizeReportInput(input) {
|
||||
const reportInfo = ensureObject(input, 'body');
|
||||
const reportId = extractReportId(reportInfo);
|
||||
const body = ensureObject(input, 'body');
|
||||
const reportId = extractReportId(body);
|
||||
|
||||
if (!reportId) {
|
||||
throw new ValidationError('report_id is required in response info');
|
||||
}
|
||||
|
||||
const reportInfo = extractReportInfo(body);
|
||||
|
||||
return {
|
||||
reportId,
|
||||
reportInfo,
|
||||
@ -68,6 +78,7 @@ function toDatabaseRecord(normalizedReport) {
|
||||
module.exports = {
|
||||
ValidationError,
|
||||
extractReportId,
|
||||
extractReportInfo,
|
||||
validateAndNormalizeReportInput,
|
||||
toDatabaseRecord,
|
||||
};
|
||||
|
||||
@ -18,6 +18,31 @@ function createResponseInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
function createPersistRequest() {
|
||||
return {
|
||||
reportId: 'report-001',
|
||||
sourceType: 'MANUAL_CAPTURE',
|
||||
sourceReportId: null,
|
||||
aadvid: '1648829117232140',
|
||||
payload: {
|
||||
name: '测试报告',
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
categories: [],
|
||||
analysisDims: ['MARKETOVERVIEW'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('validateAndNormalizeReportInput accepts persist request with payload', () => {
|
||||
const input = createPersistRequest();
|
||||
|
||||
const result = validateAndNormalizeReportInput(input);
|
||||
|
||||
assert.equal(result.reportId, 'report-001');
|
||||
assert.deepEqual(result.reportInfo, input.payload);
|
||||
});
|
||||
|
||||
test('validateAndNormalizeReportInput accepts response info with data.reportId', () => {
|
||||
const input = createResponseInfo();
|
||||
|
||||
@ -74,3 +99,20 @@ test('toDatabaseRecord maps response info into segmented_market_reports columns'
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('toDatabaseRecord maps persist request payload into segmented_market_reports report_info', () => {
|
||||
const normalized = validateAndNormalizeReportInput(createPersistRequest());
|
||||
|
||||
const record = toDatabaseRecord(normalized);
|
||||
|
||||
assert.deepEqual(record, {
|
||||
report_id: 'report-001',
|
||||
report_info: {
|
||||
name: '测试报告',
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
categories: [],
|
||||
analysisDims: ['MARKETOVERVIEW'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,6 +14,22 @@ function createResponseInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
function createPersistRequest() {
|
||||
return {
|
||||
reportId: 'report-001',
|
||||
sourceType: 'MANUAL_CAPTURE',
|
||||
sourceReportId: null,
|
||||
aadvid: '1648829117232140',
|
||||
payload: {
|
||||
name: '测试报告',
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
categories: [],
|
||||
analysisDims: ['MARKETOVERVIEW'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withServer(repository, callback) {
|
||||
const app = createApp({
|
||||
repository,
|
||||
@ -131,3 +147,43 @@ test('POST /api/reports persists raw response info and returns created response'
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('POST /api/reports persists persist request payload as report_info', async () => {
|
||||
let savedRecord = null;
|
||||
|
||||
await withServer(
|
||||
{
|
||||
async save(record) {
|
||||
savedRecord = record;
|
||||
return {
|
||||
reportId: record.report_id,
|
||||
created: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const persistRequest = createPersistRequest();
|
||||
const response = await fetch(`${baseUrl}/api/reports`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(persistRequest),
|
||||
});
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
assert.deepEqual(body, {
|
||||
success: true,
|
||||
data: {
|
||||
reportId: 'report-001',
|
||||
created: true,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(savedRecord, {
|
||||
report_id: 'report-001',
|
||||
report_info: persistRequest.payload,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -199,19 +199,53 @@ test("buildAutoCopyPayload deep clones payload, shifts top-level dates, and appe
|
||||
assert.notEqual(copied.nested, original.nested);
|
||||
});
|
||||
|
||||
test("buildPersistRequest keeps the response info unchanged for backend parsing", () => {
|
||||
const responseInfo = {
|
||||
code: 0,
|
||||
message: "success",
|
||||
data: {
|
||||
reportId: "report-001",
|
||||
status: "SUCCESS",
|
||||
},
|
||||
test("buildPersistRequest sends create payload and metadata to the backend", () => {
|
||||
const payload = {
|
||||
name: "测试报告",
|
||||
startTime: "2025-03-01",
|
||||
endTime: "2026-02-28",
|
||||
categories: [],
|
||||
analysisDims: ["MARKETOVERVIEW"],
|
||||
};
|
||||
|
||||
const request = api.buildPersistRequest(responseInfo);
|
||||
const request = api.buildPersistRequest({
|
||||
payload,
|
||||
aadvid: "1648829117232140",
|
||||
reportId: "report-001",
|
||||
sourceType: "MANUAL_CAPTURE",
|
||||
sourceReportId: null,
|
||||
});
|
||||
|
||||
assert.deepEqual(request, responseInfo);
|
||||
assert.notEqual(request, responseInfo);
|
||||
assert.notEqual(request.data, responseInfo.data);
|
||||
assert.deepEqual(request, {
|
||||
reportId: "report-001",
|
||||
sourceType: "MANUAL_CAPTURE",
|
||||
sourceReportId: null,
|
||||
aadvid: "1648829117232140",
|
||||
payload: {
|
||||
name: "测试报告",
|
||||
startTime: "2025-03-01",
|
||||
endTime: "2026-02-28",
|
||||
categories: [],
|
||||
analysisDims: ["MARKETOVERVIEW"],
|
||||
},
|
||||
});
|
||||
assert.notEqual(request.payload, payload);
|
||||
});
|
||||
|
||||
test("buildPersistRequest records auto-copy source report id", () => {
|
||||
const request = api.buildPersistRequest({
|
||||
payload: {
|
||||
name: "同比报告",
|
||||
startTime: "2024-03-01",
|
||||
endTime: "2025-02-28",
|
||||
},
|
||||
aadvid: "1648829117232140",
|
||||
reportId: "report-002",
|
||||
sourceType: "AUTO_COPY",
|
||||
sourceReportId: "report-001",
|
||||
});
|
||||
|
||||
assert.equal(request.reportId, "report-002");
|
||||
assert.equal(request.sourceType, "AUTO_COPY");
|
||||
assert.equal(request.sourceReportId, "report-001");
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// @namespace https://yuntu.oceanengine.com/
|
||||
// @version 0.1.0
|
||||
// @description 记录最近一次成功创建的云图报告,并一键复制同比报告
|
||||
// @author wangxi
|
||||
// @author wangxuesheng
|
||||
// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketList*
|
||||
// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketcreation*
|
||||
// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketDetail/*
|
||||
@ -162,8 +162,20 @@
|
||||
return cloned;
|
||||
}
|
||||
|
||||
function buildPersistRequest(responseJson) {
|
||||
return deepCloneJson(responseJson);
|
||||
function buildPersistRequest({
|
||||
payload,
|
||||
aadvid,
|
||||
reportId,
|
||||
sourceType,
|
||||
sourceReportId,
|
||||
}) {
|
||||
return {
|
||||
reportId: String(reportId),
|
||||
sourceType,
|
||||
sourceReportId: sourceReportId ? String(sourceReportId) : null,
|
||||
aadvid: String(aadvid),
|
||||
payload: deepCloneJson(payload),
|
||||
};
|
||||
}
|
||||
|
||||
function parseJson(text) {
|
||||
@ -320,7 +332,7 @@
|
||||
let lastError = null;
|
||||
|
||||
for (const base of bases) {
|
||||
try {
|
||||
try {
|
||||
if (typeof GM_xmlhttpRequest !== "function") {
|
||||
const response = await fetch(`${base}/api/reports`, {
|
||||
method: "POST",
|
||||
@ -424,6 +436,7 @@
|
||||
try {
|
||||
const nextPayload = buildAutoCopyPayload(payload);
|
||||
const requestUrl = getCreateRequestUrl(meta);
|
||||
const sourceReportId = meta.reportId || null;
|
||||
const { responseJson } = await createReportThroughPage(requestUrl, nextPayload);
|
||||
const reportId = extractReportId(responseJson);
|
||||
|
||||
@ -440,7 +453,15 @@
|
||||
};
|
||||
|
||||
writeStoredData(nextPayload, nextMeta);
|
||||
await persistToLocalServer(buildPersistRequest(responseJson));
|
||||
await persistToLocalServer(
|
||||
buildPersistRequest({
|
||||
payload: nextPayload,
|
||||
aadvid: nextMeta.aadvid,
|
||||
reportId,
|
||||
sourceType: "AUTO_COPY",
|
||||
sourceReportId,
|
||||
}),
|
||||
);
|
||||
|
||||
showToast(`同比报告创建成功:${reportId}`, "success");
|
||||
} catch (error) {
|
||||
@ -486,7 +507,15 @@
|
||||
writeStoredData(payload, meta);
|
||||
updateButtonState();
|
||||
|
||||
await persistToLocalServer(buildPersistRequest(responseJson));
|
||||
await persistToLocalServer(
|
||||
buildPersistRequest({
|
||||
payload,
|
||||
aadvid,
|
||||
reportId,
|
||||
sourceType: "MANUAL_CAPTURE",
|
||||
sourceReportId: null,
|
||||
}),
|
||||
);
|
||||
|
||||
showToast(`已记录报告配置:${reportId}`, "success");
|
||||
} catch (error) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user