task-88: 解析结果前端只接收预览数据,避免大 payload 进入响应式对象
新增纯 TS 预览裁剪 parse-preview:把解析 Vo 裁剪为摘要字段 (taskId/行数统计/开关/prompt),items/groups 整包剔除,可按 previewRowLimit/previewGroupLimit 有界保留样本行供表格展示。输入 对象永不修改,taskId 非法 fail-fast,属性读取抛错时零部分结果。 BrandSimilarAsinTab 的 parseFiles 改为只接收裁剪后的预览(保留 200 行样本),数千行 items 不再进入 Vue 响应式 ref。8 个测试覆盖 默认、批量、幂等、空、单元素、截断溢出、非法输入与 getter 故障。
This commit is contained in:
@@ -212,6 +212,7 @@ import { getStoredApiSecret } from '@/shared/utils/api-secret-store'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import { createAsinForceThrottle } from '@/shared/asin-force-throttle'
|
||||
import { toParsePreview, type ParsePreviewOptions } from '@/shared/parse-preview'
|
||||
|
||||
const selectedFileNames = ref<string[]>([])
|
||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||
@@ -472,7 +473,9 @@ async function parseFiles() {
|
||||
relativePath: f.relativePath,
|
||||
}))
|
||||
const res = await parseSimilarAsin(files, effectiveCozeApiKey(), imgSwitch.value, categorySwitch.value)
|
||||
parseResult.value = res
|
||||
// 只保留摘要字段与有界预览行,避免数千行 items/groups 进入响应式对象
|
||||
const previewOptions: ParsePreviewOptions = { previewRowLimit: 200 }
|
||||
parseResult.value = toParsePreview(res, previewOptions) as unknown as SimilarAsinParseVo
|
||||
categorySwitch.value = Boolean(res.categorySwitch)
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 解析结果预览裁剪(Task 88)。
|
||||
*
|
||||
* 解析接口(similar-asin/parse)返回完整解析 Vo,其中 items 可能达数千行,
|
||||
* 直接放入 Vue 响应式 ref 会造成大量响应式代理与内存放大。本模块把 Vo
|
||||
* 裁剪为预览对象:只保留页面实际使用的摘要字段(taskId、行数统计、开关、
|
||||
* prompt 等),items/groups 整包剔除;可按 previewRowLimit / previewGroupLimit
|
||||
* 有界保留前 N 条样本行用于表格展示(limit 省略或为 0 时不保留)。
|
||||
*
|
||||
* 纯 TS 无副作用模块:输入对象永不修改;taskId 非法时 fail-fast 抛错;
|
||||
* 属性读取抛错(如 Proxy getter 故障)时调用失败,不产生部分结果。
|
||||
*/
|
||||
export interface ParsePreviewOptions {
|
||||
/** 保留的样本行条数(0 或不传表示不保留),不能为负数 */
|
||||
previewRowLimit?: number
|
||||
/** 保留的样本分组条数(0 或不传表示不保留),不能为负数 */
|
||||
previewGroupLimit?: number
|
||||
}
|
||||
|
||||
export function toParsePreview<T>(
|
||||
result: T,
|
||||
options: ParsePreviewOptions = {},
|
||||
) {
|
||||
if (typeof result !== 'object' || result == null) {
|
||||
throw new Error('解析结果必须是对象')
|
||||
}
|
||||
const { previewRowLimit, previewGroupLimit } = options
|
||||
if (previewRowLimit != null && !(previewRowLimit >= 0)) {
|
||||
throw new Error('previewRowLimit 不能为负数: ' + previewRowLimit)
|
||||
}
|
||||
if (previewGroupLimit != null && !(previewGroupLimit >= 0)) {
|
||||
throw new Error('previewGroupLimit 不能为负数: ' + previewGroupLimit)
|
||||
}
|
||||
const record = result as Record<string, unknown>
|
||||
const taskId = record.taskId
|
||||
if (typeof taskId !== 'number' || !Number.isFinite(taskId) || taskId <= 0) {
|
||||
throw new Error('taskId 必须是正整数: ' + taskId)
|
||||
}
|
||||
|
||||
const summary: Record<string, unknown> = {}
|
||||
for (const key of Object.keys(record)) {
|
||||
if (key === 'items' || key === 'groups') continue
|
||||
summary[key] = record[key]
|
||||
}
|
||||
|
||||
if (previewRowLimit != null && previewRowLimit > 0) {
|
||||
const rows = record.items
|
||||
if (Array.isArray(rows)) {
|
||||
summary.previewItems = rows.slice(0, previewRowLimit)
|
||||
}
|
||||
}
|
||||
if (previewGroupLimit != null && previewGroupLimit > 0) {
|
||||
const groups = record.groups
|
||||
if (Array.isArray(groups)) {
|
||||
summary.previewGroups = groups.slice(0, previewGroupLimit)
|
||||
}
|
||||
}
|
||||
return summary as Omit<T, 'items' | 'groups'> & {
|
||||
previewItems?: unknown[]
|
||||
previewGroups?: unknown[]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user