feat: honor all-range export and batch scope
This commit is contained in:
+23
-24
@@ -9,7 +9,7 @@
|
|||||||
项目输入包括:
|
项目输入包括:
|
||||||
|
|
||||||
- 巨量星图达人市场页面中的达人列表、筛选条件和分页结果;
|
- 巨量星图达人市场页面中的达人列表、筛选条件和分页结果;
|
||||||
- 使用者在页面上勾选的达人;
|
- 使用者在页面上勾选的达人(可选);
|
||||||
- 使用者粘贴的达人星图 ID;
|
- 使用者粘贴的达人星图 ID;
|
||||||
- 使用者填写的批次名称;
|
- 使用者填写的批次名称;
|
||||||
- 使用者选择的导出字段和传播指标筛选规则;
|
- 使用者选择的导出字段和传播指标筛选规则;
|
||||||
@@ -105,12 +105,12 @@
|
|||||||
### 2.5 导出选中达人数据
|
### 2.5 导出选中达人数据
|
||||||
|
|
||||||
- 触发者:使用者点击 `导出选中达人数据`。
|
- 触发者:使用者点击 `导出选中达人数据`。
|
||||||
- 输入:当前勾选达人、当前导出范围、字段选择配置、传播指标筛选规则。
|
- 输入:可选的当前勾选达人、当前导出范围、字段选择配置、传播指标筛选规则。
|
||||||
- 处理:必须先勾选达人;插件收集导出范围内的达人,只保留该范围内已勾选的达人,然后按字段选择配置补充内容数据、效果预估、画像、看后搜率和秒思指标。未勾选的数据类别不会调用对应补充接口;从未保存过字段配置时保持默认全量导出。
|
- 处理:选择 `全部` 且没有勾选达人时,插件从第 1 页收集当前筛选结果的全部分页并全部导出;只要存在勾选达人,则勾选优先,只保留导出范围内已勾选的达人。其他范围仍需要勾选达人。随后按字段选择配置补充内容数据、效果预估、画像、看后搜率和秒思指标。未勾选的数据类别不会调用对应补充接口;从未保存过字段配置时保持默认全量导出。
|
||||||
- 输出:CSV 文件下载到浏览器默认下载目录。
|
- 输出:CSV 文件下载到浏览器默认下载目录。
|
||||||
- 下一步:使用者检查 CSV 内容。
|
- 下一步:使用者检查 CSV 内容。
|
||||||
- 人工操作:需要使用者勾选达人并点击按钮。
|
- 人工操作:选择 `全部` 时可直接点击按钮;选择其他范围时需要先勾选达人。
|
||||||
- 条件分支:如果当前导出范围内没有选中的达人,则不下载 CSV 并提示;单项补充失败不阻止下载,CSV 的 `导出状态` 和 `失败原因` 会标明部分成功及失败来源。
|
- 条件分支:存在勾选但全部范围内没有勾选达人时,不下载 CSV 并提示;单项补充失败不阻止下载,CSV 的 `导出状态` 和 `失败原因` 会标明部分成功及失败来源。
|
||||||
|
|
||||||
### 2.6 按星图 ID 导出
|
### 2.6 按星图 ID 导出
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
|
|
||||||
- 触发者:使用者点击 `提交批次`。
|
- 触发者:使用者点击 `提交批次`。
|
||||||
- 输入:当前范围或已勾选达人、批次名称、登录用户信息。
|
- 输入:当前范围或已勾选达人、批次名称、登录用户信息。
|
||||||
- 处理:插件先要求输入批次名称,再收集达人数据,应用传播指标规则和选中规则,检查登录状态,组装批次 payload,提交到后端。
|
- 处理:插件先要求输入批次名称,再收集达人数据并应用传播指标规则。选择 `全部` 且没有勾选达人时,提交当前筛选结果的全部分页;只要存在勾选达人,则勾选优先,不会回退为全部结果。最后检查登录状态,组装批次 payload,提交到后端。
|
||||||
- 输出:后端生成批次;页面显示 `批次提交成功` 或失败原因。
|
- 输出:后端生成批次;页面显示 `批次提交成功` 或失败原因。
|
||||||
- 下一步:在后端系统中继续处理批次。
|
- 下一步:在后端系统中继续处理批次。
|
||||||
- 人工操作:需要使用者输入批次名称。
|
- 人工操作:需要使用者输入批次名称。
|
||||||
@@ -211,11 +211,11 @@
|
|||||||
### 3.6 导出选中达人数据
|
### 3.6 导出选中达人数据
|
||||||
|
|
||||||
- 步骤目的:把使用者选定的达人数据导出为 CSV。
|
- 步骤目的:把使用者选定的达人数据导出为 CSV。
|
||||||
- 输入内容:已勾选达人、当前导出范围、字段选择配置、传播指标筛选规则。
|
- 输入内容:可选的已勾选达人、当前导出范围、字段选择配置、传播指标筛选规则。
|
||||||
- 处理规则:
|
- 处理规则:
|
||||||
- 必须先勾选达人;
|
- 选择 `全部` 且没有勾选达人时,从第 1 页收集并导出当前筛选结果的全部分页;
|
||||||
- 先按导出范围收集达人;
|
- 存在勾选达人时,勾选优先,严格保留导出范围内已勾选的达人;
|
||||||
- 再严格保留当前导出范围内已勾选的达人;
|
- 选择当前页、前 5 页、前 10 页或自定义页数且没有勾选时,提示并停止;
|
||||||
- 只补充已选字段对应的画像、效果预估、传播指标、看后搜率和秒思指标,未选数据类别不调用对应接口;
|
- 只补充已选字段对应的画像、效果预估、传播指标、看后搜率和秒思指标,未选数据类别不调用对应接口;
|
||||||
- 基础字段、导出状态和失败原因等固定保留;
|
- 基础字段、导出状态和失败原因等固定保留;
|
||||||
- 画像请求全部失败时仍下载 CSV,已成功的效果预估等字段正常写入;
|
- 画像请求全部失败时仍下载 CSV,已成功的效果预估等字段正常写入;
|
||||||
@@ -223,8 +223,8 @@
|
|||||||
- 输出结果:CSV 文件。
|
- 输出结果:CSV 文件。
|
||||||
- 外部依赖:星图画像接口、商业能力接口、传播指标接口、公司后端指标接口、浏览器下载能力。
|
- 外部依赖:星图画像接口、商业能力接口、传播指标接口、公司后端指标接口、浏览器下载能力。
|
||||||
- 失败后如何处理:
|
- 失败后如何处理:
|
||||||
- 没有勾选达人:提示并停止;
|
- 非 `全部` 范围没有勾选达人:提示并停止;
|
||||||
- 当前范围内无选中达人:提示并停止;
|
- 存在勾选但全部范围内无选中达人:提示并停止;
|
||||||
- 单个达人部分失败:记录为部分成功或失败;
|
- 单个达人部分失败:记录为部分成功或失败;
|
||||||
- 画像失败:对应画像单元格留空,失败来源写入 `失败原因`,其他成功数据仍保留并下载。
|
- 画像失败:对应画像单元格留空,失败来源写入 `失败原因`,其他成功数据仍保留并下载。
|
||||||
- 是否影响后续步骤:不影响外部系统写入;只影响本次下载结果。
|
- 是否影响后续步骤:不影响外部系统写入;只影响本次下载结果。
|
||||||
@@ -277,9 +277,9 @@
|
|||||||
- 点击后先输入批次名称;
|
- 点击后先输入批次名称;
|
||||||
- 取消输入则停止;
|
- 取消输入则停止;
|
||||||
- 批次名为空则提示并停止;
|
- 批次名为空则提示并停止;
|
||||||
- 如果存在已勾选达人,则优先提交当前范围内已勾选达人;
|
- 选择 `全部` 且没有勾选达人时,从第 1 页提交当前筛选结果的全部分页;
|
||||||
- 如果没有勾选达人,则提交当前范围内所有达人;
|
- 选择 `全部` 且存在已勾选达人时,严格提交全部范围内已勾选达人;
|
||||||
- 如果已勾选达人不在当前范围内,则回退为提交当前范围内所有达人;
|
- 选择 `全部` 时,如果已勾选达人不在结果内,则提示并停止,不会回退为提交全部达人;
|
||||||
- 前端不生成批次 ID,批次 ID 由后端生成;
|
- 前端不生成批次 ID,批次 ID 由后端生成;
|
||||||
- payload 包含登录用户 ID、创建人名称、资源地址、批次名称、创建时间和达人列表;
|
- payload 包含登录用户 ID、创建人名称、资源地址、批次名称、创建时间和达人列表;
|
||||||
- 达人列表包含达人 ID、达人名称,存在核心用户 ID 时额外带上。
|
- 达人列表包含达人 ID、达人名称,存在核心用户 ID 时额外带上。
|
||||||
@@ -356,10 +356,9 @@
|
|||||||
### 5.3 过滤规则
|
### 5.3 过滤规则
|
||||||
|
|
||||||
- 星图页面中缺少达人 ID 或达人名称的行会跳过;
|
- 星图页面中缺少达人 ID 或达人名称的行会跳过;
|
||||||
- 导出选中达人数据必须有已勾选达人;
|
- 导出选中达人数据在选择 `全部` 且没有勾选时,保留当前筛选结果的全部分页;其他范围必须有已勾选达人;
|
||||||
- 画像导出只保留当前导出范围内的已勾选达人;
|
- 选择 `全部` 且存在勾选达人时,画像导出和提交批次都只保留全部范围内已勾选达人;
|
||||||
- 提交批次如果存在已选达人,会优先保留当前范围内的已选达人;
|
- 选择 `全部` 时,勾选达人不在当前筛选结果内则不回退为全部达人;
|
||||||
- 如果当前范围内没有任何已选达人,提交批次会回退为当前范围全部达人;
|
|
||||||
- 传播指标规则启用后,不满足全部已选规则的达人会被过滤;
|
- 传播指标规则启用后,不满足全部已选规则的达人会被过滤;
|
||||||
- 按 ID 导出时,非 16 到 20 位纯数字 token 会过滤。
|
- 按 ID 导出时,非 16 到 20 位纯数字 token 会过滤。
|
||||||
|
|
||||||
@@ -485,7 +484,7 @@
|
|||||||
|
|
||||||
1. 在星图市场中完成筛选;
|
1. 在星图市场中完成筛选;
|
||||||
2. 等待页面列表加载完成;
|
2. 等待页面列表加载完成;
|
||||||
3. 勾选需要导出的达人;
|
3. 选择 `全部` 时可不勾选;选择其他范围时勾选需要导出的达人;
|
||||||
4. 可选:点击 `选择字段` 调整 CSV 字段;
|
4. 可选:点击 `选择字段` 调整 CSV 字段;
|
||||||
5. 可选:选择完播率、互动率筛选指标,并分别填写阈值和视频口径;
|
5. 可选:选择完播率、互动率筛选指标,并分别填写阈值和视频口径;
|
||||||
6. 点击 `导出选中达人数据`;
|
6. 点击 `导出选中达人数据`;
|
||||||
@@ -589,7 +588,7 @@
|
|||||||
| 批次提交后端地址 | 提交达人批次的目标服务 | 当前工作区为 `http://localhost:8083` | 其他后端地址未确认 | 影响批次提交去向 | 需要重新构建并重新加载插件 | 提交到错误环境、重复或丢失业务数据 |
|
| 批次提交后端地址 | 提交达人批次的目标服务 | 当前工作区为 `http://localhost:8083` | 其他后端地址未确认 | 影响批次提交去向 | 需要重新构建并重新加载插件 | 提交到错误环境、重复或丢失业务数据 |
|
||||||
| 后端指标服务地址 | 查询秒思 api 指标 | `https://talent-search.intelligrow.cn` | 未确认 | 影响页面增强列和 CSV 秒思字段 | 需要重新构建并重新加载插件 | 指标为空、权限错误或消耗错误环境额度 |
|
| 后端指标服务地址 | 查询秒思 api 指标 | `https://talent-search.intelligrow.cn` | 未确认 | 影响页面增强列和 CSV 秒思字段 | 需要重新构建并重新加载插件 | 指标为空、权限错误或消耗错误环境额度 |
|
||||||
| COS 更新清单 URL | 插件弹窗检查新版本 | `https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/latest.json` | 其他 HTTPS URL | 影响更新提示和安装包下载 | 需要重新构建并重新加载插件 | 用户无法更新或下载错误包 |
|
| COS 更新清单 URL | 插件弹窗检查新版本 | `https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/latest.json` | 其他 HTTPS URL | 影响更新提示和安装包下载 | 需要重新构建并重新加载插件 | 用户无法更新或下载错误包 |
|
||||||
| 导出范围 | 决定收集哪些页面达人 | 当前工具栏默认隐藏,默认值为前 5 页;当前用户主入口通常要求勾选达人 | 当前页、前 5 页、前 10 页、全部、自定义 | 影响导出或提交的达人集合 | 不需要重启 | 范围过大增加接口调用量 |
|
| 导出范围 | 决定收集和最终处理哪些页面达人 | 当前工具栏默认值为前 5 页;选择 `全部` 且未勾选时处理当前筛选结果全量,存在勾选时勾选优先 | 当前页、前 5 页、前 10 页、全部、自定义 | 影响导出或提交的达人集合 | 不需要重启 | 范围过大增加接口调用量 |
|
||||||
| 传播指标规则 | 导出或提交前二次过滤达人 | 默认不选择任何指标 | 完播率、互动率;每个已选指标填写非负阈值并选择独立视频口径 | 影响最终保留达人集合和星图接口请求口径 | 不需要重启 | 指标、口径或阈值选错会过滤掉目标达人 |
|
| 传播指标规则 | 导出或提交前二次过滤达人 | 默认不选择任何指标 | 完播率、互动率;每个已选指标填写非负阈值并选择独立视频口径 | 影响最终保留达人集合和星图接口请求口径 | 不需要重启 | 指标、口径或阈值选错会过滤掉目标达人 |
|
||||||
| 字段选择 | 控制 CSV 可选字段 | 默认全选 | 可选字段集合 | 影响 CSV 列 | 不需要重启,会本地保存 | 漏导业务字段 |
|
| 字段选择 | 控制 CSV 可选字段 | 默认全选 | 可选字段集合 | 影响 CSV 列 | 不需要重启,会本地保存 | 漏导业务字段 |
|
||||||
|
|
||||||
@@ -630,8 +629,8 @@
|
|||||||
### 9.5 失败表现
|
### 9.5 失败表现
|
||||||
|
|
||||||
- 未登录:星图页面显示登录提示,不出现业务工具栏;
|
- 未登录:星图页面显示登录提示,不出现业务工具栏;
|
||||||
- 没勾选就导出选中达人数据:提示请先勾选;
|
- 非 `全部` 范围没勾选就导出选中达人数据:提示请先勾选;
|
||||||
- 当前范围无选中达人:提示当前导出范围内没有选中的达人;
|
- 选择 `全部` 且勾选达人不在当前筛选结果内:提示全部范围内没有选中的达人;
|
||||||
- 全部已选补充数据都失败:CSV 仍会下载,该行标记为部分成功或失败并列明失败原因;
|
- 全部已选补充数据都失败:CSV 仍会下载,该行标记为部分成功或失败并列明失败原因;
|
||||||
- 按 ID 没有有效 ID:提示请输入有效的达人星图 ID;
|
- 按 ID 没有有效 ID:提示请输入有效的达人星图 ID;
|
||||||
- 批次提交失败:状态区显示接口错误或通用失败提示;
|
- 批次提交失败:状态区显示接口错误或通用失败提示;
|
||||||
@@ -699,7 +698,7 @@
|
|||||||
- Drone 发布是否为唯一正式发布方式:未确认。
|
- Drone 发布是否为唯一正式发布方式:未确认。
|
||||||
- 插件是否有统一日志、错误上报或审计记录:未确认。
|
- 插件是否有统一日志、错误上报或审计记录:未确认。
|
||||||
- 页面增强指标是否有跨页面或跨浏览器持久缓存:未确认;当前仅确认有页面会话内记录。
|
- 页面增强指标是否有跨页面或跨浏览器持久缓存:未确认;当前仅确认有页面会话内记录。
|
||||||
- 导出全部页面时最多导出多少页:后台静默导出当前最多尝试 200 页;真实星图侧上限未确认。
|
- 导出全部页面时最多导出多少页:后台静默导出从第 1 页开始,当前最多尝试 200 页;真实星图侧上限未确认。
|
||||||
- 传播指标请求是否应该限制并发:当前筛选已按同一达人相同口径去重,但真实业务级并发上限仍未确认。
|
- 传播指标请求是否应该限制并发:当前筛选已按同一达人相同口径去重,但真实业务级并发上限仍未确认。
|
||||||
- 后续业务系统如何消费批次:未确认。
|
- 后续业务系统如何消费批次:未确认。
|
||||||
|
|
||||||
|
|||||||
+56
-20
@@ -339,16 +339,15 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
const toolbarHandlers = {
|
const toolbarHandlers = {
|
||||||
onExportAudienceProfile: async () => {
|
onExportAudienceProfile: async () => {
|
||||||
syncSelectionStateFromDom();
|
syncSelectionStateFromDom();
|
||||||
if (selectedAuthorIds.size === 0) {
|
|
||||||
setToolbarExportStatus(toolbar, "请先勾选需要导出数据的达人");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportTarget = readToolbarExportTarget(toolbar);
|
const exportTarget = readToolbarExportTarget(toolbar);
|
||||||
if (!exportTarget.target) {
|
if (!exportTarget.target) {
|
||||||
setToolbarExportStatus(toolbar, exportTarget.error ?? "导出配置无效");
|
setToolbarExportStatus(toolbar, exportTarget.error ?? "导出配置无效");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedAuthorIds.size === 0 && exportTarget.target.mode !== "all") {
|
||||||
|
setToolbarExportStatus(toolbar, "请先勾选需要导出数据的达人");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setToolbarBusyState(toolbar, true);
|
setToolbarBusyState(toolbar, true);
|
||||||
try {
|
try {
|
||||||
@@ -356,16 +355,27 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
const exportSelection = resolveAudienceProfileExportSelection(
|
const exportSelection = resolveAudienceProfileExportSelection(
|
||||||
selectedHeaders
|
selectedHeaders
|
||||||
);
|
);
|
||||||
const selectedRecords = filterRecordsBySelectionStrict(
|
const resolvedRecords = await exportRecords(
|
||||||
await exportRecords(exportTarget.target, "画像导出中", {
|
exportTarget.target,
|
||||||
|
"画像导出中",
|
||||||
|
{
|
||||||
includeBackendMetrics: exportSelection.includeBackendMetrics,
|
includeBackendMetrics: exportSelection.includeBackendMetrics,
|
||||||
includeRates: exportSelection.includeRates,
|
includeRates: exportSelection.includeRates,
|
||||||
showDetailedProgress: false,
|
showDetailedProgress: false,
|
||||||
spreadInfoConfigs: exportSelection.spreadInfoConfigs
|
spreadInfoConfigs: exportSelection.spreadInfoConfigs
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
const selectedRecords = filterRecordsForAudienceProfileExport(
|
||||||
|
resolvedRecords,
|
||||||
|
exportTarget.target
|
||||||
);
|
);
|
||||||
if (selectedRecords.length === 0) {
|
if (selectedRecords.length === 0) {
|
||||||
setToolbarExportStatus(toolbar, "当前导出范围内没有选中的达人");
|
setToolbarExportStatus(
|
||||||
|
toolbar,
|
||||||
|
exportTarget.target.mode === "all" && selectedAuthorIds.size > 0
|
||||||
|
? "全部范围内没有选中的达人"
|
||||||
|
: "当前导出范围内没有选中的达人"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,18 +520,29 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
setToolbarBusyState(toolbar, true);
|
setToolbarBusyState(toolbar, true);
|
||||||
try {
|
try {
|
||||||
const hasSelectedAuthors = selectedAuthorIds.size > 0;
|
const hasSelectedAuthors = selectedAuthorIds.size > 0;
|
||||||
const records = filterRecordsBySelection(
|
const resolvedRecords = await applySpreadThresholdFilter(
|
||||||
await applySpreadThresholdFilter(
|
await exportRecords(
|
||||||
await exportRecords(
|
exportTarget.target,
|
||||||
exportTarget.target,
|
hasSelectedAuthors ? "提交已选达人中" : "提交中",
|
||||||
hasSelectedAuthors ? "提交已选达人中" : "提交中",
|
{
|
||||||
{
|
showDetailedProgress: !hasSelectedAuthors
|
||||||
showDetailedProgress: !hasSelectedAuthors
|
}
|
||||||
}
|
),
|
||||||
),
|
spreadFilter.filter
|
||||||
spreadFilter.filter
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
const records =
|
||||||
|
exportTarget.target.mode === "all"
|
||||||
|
? filterRecordsForAllRange(resolvedRecords)
|
||||||
|
: filterRecordsBySelection(resolvedRecords);
|
||||||
|
if (records.length === 0) {
|
||||||
|
setToolbarExportStatus(
|
||||||
|
toolbar,
|
||||||
|
exportTarget.target.mode === "all" && hasSelectedAuthors
|
||||||
|
? "全部范围内没有选中的达人"
|
||||||
|
: "当前范围内没有可提交的达人"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const authState = await getAuthState();
|
const authState = await getAuthState();
|
||||||
if (!authState.isAuthenticated) {
|
if (!authState.isAuthenticated) {
|
||||||
throw new Error("请先登录插件");
|
throw new Error("请先登录插件");
|
||||||
@@ -1168,6 +1189,21 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
return records.filter((record) => selectedAuthorIds.has(record.authorId));
|
return records.filter((record) => selectedAuthorIds.has(record.authorId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterRecordsForAllRange(records: MarketRecord[]): MarketRecord[] {
|
||||||
|
return selectedAuthorIds.size === 0
|
||||||
|
? records
|
||||||
|
: filterRecordsBySelectionStrict(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRecordsForAudienceProfileExport(
|
||||||
|
records: MarketRecord[],
|
||||||
|
target: MarketExportTarget
|
||||||
|
): MarketRecord[] {
|
||||||
|
return target.mode === "all"
|
||||||
|
? filterRecordsForAllRange(records)
|
||||||
|
: filterRecordsBySelectionStrict(records);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAudienceProfileSet(
|
async function loadAudienceProfileSet(
|
||||||
record: MarketRecord,
|
record: MarketRecord,
|
||||||
exportSelection: AudienceProfileExportSelection
|
exportSelection: AudienceProfileExportSelection
|
||||||
|
|||||||
@@ -51,10 +51,11 @@ export function createSilentExportController(
|
|||||||
|
|
||||||
const mergedRecords = new Map<string, MarketRecord>();
|
const mergedRecords = new Map<string, MarketRecord>();
|
||||||
const maxPageCount = target.mode === "count" ? target.pageCount : 200;
|
const maxPageCount = target.mode === "count" ? target.pageCount : 200;
|
||||||
|
const initialPage = target.mode === "all" ? 1 : baseRequest.initialPage;
|
||||||
let totalPagesHint: number | undefined;
|
let totalPagesHint: number | undefined;
|
||||||
|
|
||||||
for (let offset = 0; offset < maxPageCount; offset += 1) {
|
for (let offset = 0; offset < maxPageCount; offset += 1) {
|
||||||
const pageNumber = baseRequest.initialPage + offset;
|
const pageNumber = initialPage + offset;
|
||||||
options.onProgress?.({
|
options.onProgress?.({
|
||||||
currentPage: offset + 1,
|
currentPage: offset + 1,
|
||||||
totalPages: target.mode === "count" ? target.pageCount : totalPagesHint
|
totalPages: target.mode === "count" ? target.pageCount : totalPagesHint
|
||||||
|
|||||||
@@ -1791,7 +1791,7 @@ describe("market-content-entry", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("audience profile export requires selected creators", async () => {
|
test("audience profile export requires selected creators outside of the all range", async () => {
|
||||||
document.body.innerHTML = buildRealMarketFixture([
|
document.body.innerHTML = buildRealMarketFixture([
|
||||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||||
]);
|
]);
|
||||||
@@ -1825,6 +1825,109 @@ describe("market-content-entry", () => {
|
|||||||
).toContain("请先勾选需要导出数据的达人");
|
).toContain("请先勾选需要导出数据的达人");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("audience profile export includes all filtered creators when all is selected without checks", async () => {
|
||||||
|
const records = [
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
];
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(records);
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({ page: 1 }),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & { fetch?: typeof fetch }
|
||||||
|
).fetch = vi.fn(async () => ({
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(records),
|
||||||
|
totalPages: 1
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
}));
|
||||||
|
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
buildAudienceProfileCsv,
|
||||||
|
document,
|
||||||
|
loadAudienceProfile: async () => ({ status: "success" as const }),
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
onCsvReady: vi.fn(),
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
setSelectValue('[data-plugin-export-range="select"]', "all");
|
||||||
|
dispatchChange('[data-plugin-export-range="select"]');
|
||||||
|
click('[data-plugin-export-audience-profile="button"]');
|
||||||
|
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildAudienceProfileCsv.mock.calls[0]?.[0].map(({ record }) => record.authorId)
|
||||||
|
).toEqual(["111", "222"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("audience profile export gives checked creators priority in the all range", async () => {
|
||||||
|
const records = [
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
];
|
||||||
|
document.body.innerHTML = buildRealMarketFixture(records);
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({ page: 1 }),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & { fetch?: typeof fetch }
|
||||||
|
).fetch = vi.fn(async () => ({
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows(records),
|
||||||
|
totalPages: 1
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
}));
|
||||||
|
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
buildAudienceProfileCsv,
|
||||||
|
document,
|
||||||
|
loadAudienceProfile: async () => ({ status: "success" as const }),
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
onCsvReady: vi.fn(),
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
clickSelectionCheckboxForAuthor("222");
|
||||||
|
setSelectValue('[data-plugin-export-range="select"]', "all");
|
||||||
|
dispatchChange('[data-plugin-export-range="select"]');
|
||||||
|
click('[data-plugin-export-audience-profile="button"]');
|
||||||
|
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildAudienceProfileCsv.mock.calls[0]?.[0].map(({ record }) => record.authorId)
|
||||||
|
).toEqual(["222"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("audience profile export loads profiles only for selected creators", async () => {
|
test("audience profile export loads profiles only for selected creators", async () => {
|
||||||
document.body.innerHTML = buildRealMarketFixture([
|
document.body.innerHTML = buildRealMarketFixture([
|
||||||
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
|
||||||
@@ -2967,8 +3070,62 @@ describe("market-content-entry", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("all-range batch submit does not fall back when checked creators are outside the results", async () => {
|
||||||
|
document.body.innerHTML = buildRealMarketFixture([
|
||||||
|
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
|
||||||
|
]);
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({ page: 1 }),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & { fetch?: typeof fetch }
|
||||||
|
).fetch = vi.fn(async () => ({
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
marketList: buildMarketListResponseRows([
|
||||||
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
|
]),
|
||||||
|
totalPages: 1
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
}));
|
||||||
|
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||||
|
|
||||||
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
|
const controller = trackController(createMarketController({
|
||||||
|
document,
|
||||||
|
loadAuthorMetrics: async () => ({
|
||||||
|
success: false,
|
||||||
|
reason: "request-failed"
|
||||||
|
}),
|
||||||
|
promptBatchName: vi.fn(() => "自动选择批次"),
|
||||||
|
submitBatch,
|
||||||
|
window
|
||||||
|
}));
|
||||||
|
|
||||||
|
await controller.ready;
|
||||||
|
clickSelectionCheckboxForAuthor("111");
|
||||||
|
setSelectValue('[data-plugin-export-range="select"]', "all");
|
||||||
|
dispatchChange('[data-plugin-export-range="select"]');
|
||||||
|
removeDefaultSpreadMetricFilter();
|
||||||
|
click('[data-plugin-batch-submit="button"]');
|
||||||
|
|
||||||
|
await waitForCondition(() =>
|
||||||
|
document.querySelector('[data-plugin-export-status="text"]')?.textContent ===
|
||||||
|
"全部范围内没有选中的达人"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(submitBatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"default paged batch submit keeps detailed progress when no creators are selected",
|
"all-range batch submit keeps detailed progress when no creators are selected",
|
||||||
async () => {
|
async () => {
|
||||||
const pages = [
|
const pages = [
|
||||||
[
|
[
|
||||||
@@ -3042,6 +3199,9 @@ describe("market-content-entry", () => {
|
|||||||
|
|
||||||
await controller.ready;
|
await controller.ready;
|
||||||
|
|
||||||
|
setSelectValue('[data-plugin-export-range="select"]', "all");
|
||||||
|
dispatchChange('[data-plugin-export-range="select"]');
|
||||||
|
|
||||||
removeDefaultSpreadMetricFilter();
|
removeDefaultSpreadMetricFilter();
|
||||||
click('[data-plugin-batch-submit="button"]');
|
click('[data-plugin-batch-submit="button"]');
|
||||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||||
|
|||||||
@@ -155,6 +155,56 @@ describe("silent-export-controller", () => {
|
|||||||
expect(records?.map((record) => record.authorId)).toEqual(["2", "3"]);
|
expect(records?.map((record) => record.authorId)).toEqual(["2", "3"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("starts all-result exports from page 1 even after browsing to a later page", async () => {
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-sces-market-request-snapshot",
|
||||||
|
JSON.stringify({
|
||||||
|
body: JSON.stringify({
|
||||||
|
page_param: {
|
||||||
|
page: 2
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://xingtu.cn/api/mock-market-search"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestedPages: number[] = [];
|
||||||
|
const controller = createSilentExportController({
|
||||||
|
document,
|
||||||
|
fetchImpl: async (_url, init) => {
|
||||||
|
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
||||||
|
page_param?: { page?: number };
|
||||||
|
};
|
||||||
|
const pageNo = body.page_param?.page ?? 0;
|
||||||
|
requestedPages.push(pageNo);
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: async () => ({
|
||||||
|
authors: [
|
||||||
|
{
|
||||||
|
attribute_datas: {
|
||||||
|
nickname: `达人${pageNo}`
|
||||||
|
},
|
||||||
|
star_id: String(pageNo)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
page: pageNo,
|
||||||
|
totalPages: 3
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const records = await controller.exportRecords({ mode: "all" });
|
||||||
|
|
||||||
|
expect(requestedPages).toEqual([1, 2, 3]);
|
||||||
|
expect(records?.map((record) => record.authorId)).toEqual(["1", "2", "3"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps attribute_datas.id as the spread author id while preserving star_id as row id", async () => {
|
test("keeps attribute_datas.id as the spread author id while preserving star_id as row id", async () => {
|
||||||
document.documentElement.setAttribute(
|
document.documentElement.setAttribute(
|
||||||
"data-sces-market-request-snapshot",
|
"data-sces-market-request-snapshot",
|
||||||
|
|||||||
Reference in New Issue
Block a user