软件前端更新
This commit is contained in:
5
app/.env
5
app/.env
@@ -13,8 +13,7 @@ client_name=ShuFuAI
|
||||
|
||||
|
||||
# java_api_base=http://47.111.163.154:18080
|
||||
# java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://8.136.19.173:18080
|
||||
java_api_base=http://121.196.149.225:18080
|
||||
java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://121.196.149.225:18080
|
||||
|
||||
|
||||
|
||||
@@ -156,9 +156,6 @@ public class SimilarAsinCozeClient {
|
||||
private InspectAttempt inspectOnce(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||
String raw = runWorkflowAsyncAndWait(rows, prompt, apiKey);
|
||||
List<CozeResult> results = parseResults(raw);
|
||||
if (rows.size() > 1 && !results.isEmpty() && results.stream().noneMatch(this::hasIdentity)) {
|
||||
throw new PartialCozeResultException(0, rows.size(), results.size());
|
||||
}
|
||||
List<SimilarAsinResultRowDto> merged = mergeRows(rows, results);
|
||||
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
|
||||
}
|
||||
@@ -259,18 +256,14 @@ public class SimilarAsinCozeClient {
|
||||
}
|
||||
|
||||
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||
List<String> groupKeys = rows.stream().map(row -> nonBlank(row.getGroupKey(), rowKey(row.getId(), row.getAsin(), row.getCountry()))).toList();
|
||||
List<String> rowTokens = rows.stream().map(row -> nonBlank(row.getRowToken(), "")).toList();
|
||||
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
||||
List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList();
|
||||
List<String> countries = rows.stream().map(row -> nonBlank(row.getCountry(), "")).toList();
|
||||
List<String> skus = rows.stream().map(row -> nonBlank(row.getSku(), "")).toList();
|
||||
List<String> titles = rows.stream().map(row -> nonBlank(row.getTitle(), row.getAsin())).toList();
|
||||
List<String> urls = rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList();
|
||||
List<List<String>> urlLists = rows.stream().map(SimilarAsinResultRowDto::getUrls).toList();
|
||||
|
||||
Map<String, Object> parameters = new LinkedHashMap<>();
|
||||
parameters.put("items", buildItemObjects(groupKeys, rowTokens, rowIds, asins, countries, skus, titles, urls, urlLists));
|
||||
parameters.put("items", buildItemObjects(asins, skus, titles, urls, urlLists));
|
||||
parameters.put("prompt", prompt == null ? "" : prompt);
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
parameters.put("api_key", apiKey.trim());
|
||||
@@ -308,11 +301,7 @@ public class SimilarAsinCozeClient {
|
||||
return normalized.substring(0, 6) + "***" + normalized.substring(normalized.length() - 4);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildItemObjects(List<String> groupKeys,
|
||||
List<String> rowTokens,
|
||||
List<String> rowIds,
|
||||
List<String> asins,
|
||||
List<String> countries,
|
||||
private List<Map<String, Object>> buildItemObjects(List<String> asins,
|
||||
List<String> skus,
|
||||
List<String> titles,
|
||||
List<String> urls,
|
||||
@@ -320,11 +309,7 @@ public class SimilarAsinCozeClient {
|
||||
List<Map<String, Object>> items = new ArrayList<>(asins.size());
|
||||
for (int i = 0; i < asins.size(); i++) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("group_key", groupKeys.get(i));
|
||||
item.put("row_token", rowTokens.get(i));
|
||||
item.put("row_id", rowIds.get(i));
|
||||
item.put("asin", asins.get(i));
|
||||
item.put("country", countries.get(i));
|
||||
item.put("sku", skus.get(i));
|
||||
item.put("title", titles.get(i));
|
||||
item.put("url", urls.get(i));
|
||||
|
||||
@@ -37,7 +37,10 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/similar-asin")
|
||||
@Tag(name = "相似ASIN检测", description = "相似ASIN检测任务接口。前端上传 Excel 后由 Java 解析并创建任务;Python 回传商品数据;Java 负责攒批调用 Coze、补齐子行、生成最终 xlsx 并上传 OSS。")
|
||||
@Tag(
|
||||
name = "相似 ASIN 检测",
|
||||
description = "前端上传 Excel 后由 Java 解析并创建任务;Python 按分组抓取商品数据并回传;Java 再按批调用 Coze 并生成最终 xlsx。"
|
||||
)
|
||||
public class SimilarAsinController {
|
||||
|
||||
private final SimilarAsinTaskService service;
|
||||
@@ -69,13 +72,19 @@ public class SimilarAsinController {
|
||||
}
|
||||
|
||||
@PostMapping("/parse")
|
||||
@Operation(summary = "解析 Excel 并创建任务", description = "解析上传后的 Excel 文件,提取 id、ASIN、国家、URL、标题等字段。返回给前端的数据只包含整数 id 和 n_1 行;n_2、n_3 等子行会保存在 OSS 解析载荷中,用于最终结果补齐。创建后的任务状态为 PENDING,不会自动推送 Python。")
|
||||
@Operation(
|
||||
summary = "解析 Excel 并创建任务",
|
||||
description = "解析上传后的 Excel 文件,提取 id、ASIN、国家、URL、标题、SKU 等字段。返回给前端的数据同时包含全量有效行 items,以及按主数据块分组后的 groups,便于 Python 按组抓取后再回传。创建后的任务状态为 PENDING,不会自动推送 Python。"
|
||||
)
|
||||
public ApiResponse<SimilarAsinParseVo> parse(@Valid @RequestBody SimilarAsinParseRequest request) {
|
||||
return ApiResponse.success(service.parseAndCreateTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}/parsed-payload")
|
||||
@Operation(summary = "获取货源查询完整解析载荷", description = "给 Python 队列消费端使用。parse 接口只返回轻量预览,大文件完整行数据通过该接口按 taskId 拉取。")
|
||||
@Operation(
|
||||
summary = "获取货源查询完整解析载荷",
|
||||
description = "给 Python 队列消费端使用。返回全量有效行 items/allItems 以及分组后的 groups,便于 Python 按组抓取后回传。"
|
||||
)
|
||||
public ApiResponse<SimilarAsinParsedPayloadDto> parsedPayload(
|
||||
@Parameter(description = "货源查询任务 ID", required = true, example = "7004")
|
||||
@PathVariable Long taskId,
|
||||
@@ -85,17 +94,17 @@ public class SimilarAsinController {
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
@Operation(summary = "查询相似ASIN检测总览", description = "查询当前用户的运行中、成功、失败和已结束任务数量。页面进入时请求一次即可,不需要持续轮询。")
|
||||
@Operation(summary = "查询相似 ASIN 检测总览", description = "查询当前用户的运行中、成功、失败和已结束任务数量。")
|
||||
public ApiResponse<SimilarAsinDashboardVo> dashboard(
|
||||
@Parameter(description = "当前用户 ID,用于隔离不同用户的任务和历史记录。", required = true, example = "1")
|
||||
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(service.dashboard(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/history")
|
||||
@Operation(summary = "查询相似ASIN检测历史", description = "查询当前用户最近的相似ASIN检测历史记录,包含源文件名、任务状态、行数、错误信息和最终 xlsx 下载地址。")
|
||||
@Operation(summary = "查询相似 ASIN 检测历史", description = "查询当前用户最近的相似 ASIN 检测历史记录。")
|
||||
public ApiResponse<SimilarAsinHistoryVo> history(
|
||||
@Parameter(description = "当前用户 ID,用于查询该用户自己的历史记录。", required = true, example = "1")
|
||||
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||
@RequestParam("user_id") Long userId,
|
||||
@Parameter(description = "history limit, default 50, max 100", example = "50")
|
||||
@RequestParam(value = "limit", required = false, defaultValue = "50") Integer limit) {
|
||||
@@ -103,26 +112,29 @@ public class SimilarAsinController {
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/progress/batch")
|
||||
@Operation(summary = "批量查询任务进度", description = "前端只对活跃任务调用该接口,建议 6 秒一次。接口只返回轻量任务状态,不返回明细结果。")
|
||||
@Operation(summary = "批量查询任务进度", description = "前端仅对活跃任务轮询该接口,返回轻量任务状态。")
|
||||
public ApiResponse<SimilarAsinTaskBatchVo> progress(@Valid @RequestBody SimilarAsinTaskBatchRequest request) {
|
||||
return ApiResponse.success(service.progressBatch(request.getTaskIds()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/activate")
|
||||
@Operation(summary = "激活任务", description = "前端手动推送 Python 队列成功后调用,将任务从 PENDING 改为 RUNNING,并记录后端内部活跃时间。后续活跃时间由 Python 回传结果接口自动刷新,不需要单独心跳接口。")
|
||||
@Operation(summary = "激活任务", description = "前端手动推送 Python 队列成功后调用,将任务从 PENDING 改为 RUNNING。")
|
||||
public ApiResponse<Void> activate(
|
||||
@Parameter(description = "相似ASIN检测任务 ID,即解析接口返回的 taskId。", required = true, example = "3938")
|
||||
@Parameter(description = "相似 ASIN 检测任务 ID", required = true, example = "3938")
|
||||
@PathVariable Long taskId,
|
||||
@Parameter(description = "当前用户 ID,必须与创建任务的用户一致。", required = true, example = "1")
|
||||
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
service.activateTask(taskId, userId);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/result")
|
||||
@Operation(summary = "提交 Python 回传结果", description = "Python 回传商品数据接口。请通过 groups[].items[] 回传分组结果;Java 先原样保存回传数据,再内部攒批调用 Coze。done=true 表示 Python 已完成全部回传,Java 会强制处理剩余未满批的数据并生成最终 xlsx。")
|
||||
@Operation(
|
||||
summary = "提交 Python 回传结果",
|
||||
description = "Python 请通过 groups[].items[] 回传分组抓取结果;Java 先原样保存回传数据,再内部攒批调用 Coze。done=true 表示 Python 已完成全部回传,Java 会强制处理剩余未满批的数据并生成最终 xlsx。"
|
||||
)
|
||||
public ApiResponse<Void> result(
|
||||
@Parameter(description = "相似ASIN检测任务 ID。任务必须处于 RUNNING 状态。", required = true, example = "3938")
|
||||
@Parameter(description = "相似 ASIN 检测任务 ID,任务必须处于 RUNNING 状态", required = true, example = "3938")
|
||||
@PathVariable Long taskId,
|
||||
@Valid @RequestBody SimilarAsinSubmitResultRequest request,
|
||||
jakarta.servlet.http.HttpServletResponse response) {
|
||||
@@ -133,29 +145,29 @@ public class SimilarAsinController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/tasks/{taskId}")
|
||||
@Operation(summary = "删除任务", description = "删除当前用户的一条相似ASIN检测任务,同时清理任务结果、scope 状态和分片记录。")
|
||||
@Operation(summary = "删除任务", description = "删除当前用户的一条相似 ASIN 检测任务,同时清理任务结果、scope 状态和分片记录。")
|
||||
public ApiResponse<Void> deleteTask(
|
||||
@Parameter(description = "相似ASIN检测任务 ID。", required = true, example = "3938")
|
||||
@Parameter(description = "相似 ASIN 检测任务 ID", required = true, example = "3938")
|
||||
@PathVariable Long taskId,
|
||||
@Parameter(description = "当前用户 ID,必须与创建任务的用户一致。", required = true, example = "1")
|
||||
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
service.deleteTask(taskId, userId);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@DeleteMapping("/history/{resultId}")
|
||||
@Operation(summary = "删除历史记录", description = "删除当前用户的一条相似ASIN检测历史记录。只删除 biz_file_result 记录,不主动删除任务主记录。")
|
||||
@Operation(summary = "删除历史记录", description = "删除当前用户的一条相似 ASIN 检测历史记录。")
|
||||
public ApiResponse<Void> deleteHistory(
|
||||
@Parameter(description = "历史结果记录 ID,即 history 接口返回的 resultId。", required = true, example = "1001")
|
||||
@Parameter(description = "历史结果记录 ID", required = true, example = "1001")
|
||||
@PathVariable Long resultId,
|
||||
@Parameter(description = "当前用户 ID。", required = true, example = "1")
|
||||
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
service.deleteHistory(resultId, userId);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/results/{resultId}/download")
|
||||
@Operation(summary = "下载相似ASIN检测结果文件")
|
||||
@Operation(summary = "下载相似 ASIN 检测结果文件")
|
||||
public void downloadResult(
|
||||
@Parameter(description = "结果记录 ID", required = true, example = "1001")
|
||||
@PathVariable Long resultId,
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "相似ASIN检测解析载荷")
|
||||
@Schema(description = "相似 ASIN 解析载荷")
|
||||
public class SimilarAsinParsedPayloadDto {
|
||||
@Schema(description = "AI 提示词")
|
||||
private String aiPrompt;
|
||||
@@ -23,12 +23,12 @@ public class SimilarAsinParsedPayloadDto {
|
||||
@Schema(description = "Excel 原始表头列表")
|
||||
private List<String> headers = new ArrayList<>();
|
||||
|
||||
@Schema(description = "兼容旧链路的平铺有效行,现为全部有效行")
|
||||
@Schema(description = "平铺的全量有效行")
|
||||
private List<SimilarAsinParsedRowVo> items = new ArrayList<>();
|
||||
|
||||
@Schema(description = "按相邻主 ID 块分组后的完整数据")
|
||||
@Schema(description = "按主数据块分组后的完整数据,每个 group.items 都是需要 Python 抓取的全量子行")
|
||||
private List<SimilarAsinParsedGroupVo> groups = new ArrayList<>();
|
||||
|
||||
@Schema(description = "完整有效行")
|
||||
@Schema(description = "全量有效行,用于兼容旧链路与结果文件组装")
|
||||
private List<SimilarAsinParsedRowVo> allItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -2564,7 +2564,6 @@ public class SimilarAsinTaskService {
|
||||
vo.setValues(readRowValues(row, headers, formatter));
|
||||
allRows.add(vo);
|
||||
}
|
||||
hydratePromptFields(allRows);
|
||||
if (allRows.isEmpty()) {
|
||||
throw new BusinessException("no valid similar ASIN rows");
|
||||
}
|
||||
@@ -2577,29 +2576,6 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void hydratePromptFields(List<SimilarAsinParsedRowVo> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, List<SimilarAsinParsedRowVo>> rowsByBaseId = groupRowsByBaseId(rows);
|
||||
for (List<SimilarAsinParsedRowVo> siblings : rowsByBaseId.values()) {
|
||||
String title = "";
|
||||
String url = "";
|
||||
for (SimilarAsinParsedRowVo sibling : siblings) {
|
||||
title = firstNonBlank(title, sibling.getTitle());
|
||||
url = firstNonBlank(url, sibling.getUrl());
|
||||
}
|
||||
for (SimilarAsinParsedRowVo sibling : siblings) {
|
||||
if (normalize(sibling.getTitle()).isBlank()) {
|
||||
sibling.setTitle(title);
|
||||
}
|
||||
if (normalize(sibling.getUrl()).isBlank()) {
|
||||
sibling.setUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasPromptFields(SimilarAsinParsedRowVo row) {
|
||||
return row != null && (!normalize(row.getTitle()).isBlank() || !normalize(row.getUrl()).isBlank());
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ bucket_path = "nanri-image/"
|
||||
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
||||
|
||||
import os
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://47.111.163.154:18080').rstrip('/')
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://121.196.149.225:18080').rstrip('/')
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://121.196.149.225:18080').rstrip('/')
|
||||
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
||||
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||
|
||||
@@ -192,6 +192,7 @@ const dashboard = ref<AppearancePatentDashboardVo>({
|
||||
failedTaskCount: 0,
|
||||
})
|
||||
const historyItems = ref<AppearancePatentHistoryItem[]>([])
|
||||
const liveProgressItems = ref<Record<number, AppearancePatentHistoryItem>>({})
|
||||
|
||||
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
||||
const pendingParseItem = computed<AppearancePatentHistoryItem | null>(() => {
|
||||
@@ -209,10 +210,21 @@ const pendingParseItem = computed<AppearancePatentHistoryItem | null>(() => {
|
||||
})
|
||||
const currentItems = computed(() => {
|
||||
const activeIds = new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value])
|
||||
const activeItems = historyItems.value.filter((item) => {
|
||||
const activeItems = historyItems.value
|
||||
.filter((item) => {
|
||||
const status = normalizeTaskStatus(item)
|
||||
return item.taskId != null && (activeIds.has(item.taskId) || status === 'RUNNING' || status === 'PENDING')
|
||||
})
|
||||
.map((item) => mergeCurrentTaskItem(item))
|
||||
for (const [taskIdText, liveItem] of Object.entries(liveProgressItems.value)) {
|
||||
const taskId = Number(taskIdText)
|
||||
if (!Number.isFinite(taskId) || taskId <= 0) continue
|
||||
if (!activeIds.has(taskId) && normalizeTaskStatus(liveItem) !== 'RUNNING' && normalizeTaskStatus(liveItem) !== 'PENDING') {
|
||||
continue
|
||||
}
|
||||
if (activeItems.some((item) => item.taskId === taskId)) continue
|
||||
activeItems.push(mergeCurrentTaskItem(liveItem))
|
||||
}
|
||||
if (!pendingParseItem.value) return activeItems
|
||||
const exists = activeItems.some((item) => item.taskId === pendingParseItem.value?.taskId)
|
||||
return exists ? activeItems : [pendingParseItem.value, ...activeItems]
|
||||
@@ -224,6 +236,20 @@ const historyOnlyItems = computed(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
function mergeCurrentTaskItem(item: AppearancePatentHistoryItem) {
|
||||
if (item.taskId == null) return item
|
||||
const liveItem = liveProgressItems.value[item.taskId]
|
||||
if (!liveItem) return item
|
||||
return {
|
||||
...item,
|
||||
...liveItem,
|
||||
sourceFilename: liveItem.sourceFilename || item.sourceFilename,
|
||||
resultFilename: liveItem.resultFilename || item.resultFilename,
|
||||
rowCount: liveItem.rowCount ?? item.rowCount,
|
||||
createdAt: liveItem.createdAt || item.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
function effectiveAiPrompt() {
|
||||
return aiPrompt.value.trim() || defaultAiPrompt
|
||||
}
|
||||
@@ -460,6 +486,11 @@ function addPollingTask(taskId: number) {
|
||||
|
||||
function removePollingTask(taskId: number) {
|
||||
pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId)
|
||||
if (!pendingFileTaskIds.value.includes(taskId)) {
|
||||
const next = { ...liveProgressItems.value }
|
||||
delete next[taskId]
|
||||
liveProgressItems.value = next
|
||||
}
|
||||
savePollingIds()
|
||||
}
|
||||
|
||||
@@ -473,6 +504,11 @@ function addPendingFileTask(taskId: number) {
|
||||
|
||||
function removePendingFileTask(taskId: number) {
|
||||
pendingFileTaskIds.value = pendingFileTaskIds.value.filter((id) => id !== taskId)
|
||||
if (!pollingTaskIds.value.includes(taskId)) {
|
||||
const next = { ...liveProgressItems.value }
|
||||
delete next[taskId]
|
||||
liveProgressItems.value = next
|
||||
}
|
||||
savePollingIds()
|
||||
}
|
||||
|
||||
@@ -525,7 +561,13 @@ async function refreshTaskProgress() {
|
||||
const task = detail.task
|
||||
if (!task?.id) continue
|
||||
const item = detail.items?.[0]
|
||||
if (item) mergeHistoryItem(item)
|
||||
if (item) {
|
||||
liveProgressItems.value = {
|
||||
...liveProgressItems.value,
|
||||
[task.id]: item,
|
||||
}
|
||||
mergeHistoryItem(item)
|
||||
}
|
||||
if (task.status === 'SUCCESS' || task.status === 'FAILED') {
|
||||
removePollingTask(task.id)
|
||||
shouldRefreshDashboard = true
|
||||
@@ -627,7 +669,7 @@ async function loadHistory(options: { force?: boolean } = {}) {
|
||||
if (historyInFlight) return historyInFlight
|
||||
historyInFlight = getAppearancePatentHistory()
|
||||
.then((res) => {
|
||||
historyItems.value = res.items || []
|
||||
historyItems.value = mergeHistoryItemsPreservingLiveProgress(historyItems.value, res.items || [])
|
||||
lastHistoryLoadedAt = Date.now()
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -636,6 +678,66 @@ async function loadHistory(options: { force?: boolean } = {}) {
|
||||
return historyInFlight
|
||||
}
|
||||
|
||||
function mergeHistoryItemsPreservingLiveProgress(
|
||||
previousItems: AppearancePatentHistoryItem[],
|
||||
incomingItems: AppearancePatentHistoryItem[],
|
||||
) {
|
||||
const previousByTaskId = new Map<number, AppearancePatentHistoryItem>()
|
||||
const previousByResultId = new Map<number, AppearancePatentHistoryItem>()
|
||||
for (const item of previousItems || []) {
|
||||
if (item.taskId != null) previousByTaskId.set(item.taskId, item)
|
||||
if (item.resultId != null) previousByResultId.set(item.resultId, item)
|
||||
}
|
||||
return (incomingItems || []).map((incoming) => {
|
||||
const previous = incoming.resultId != null
|
||||
? previousByResultId.get(incoming.resultId)
|
||||
: incoming.taskId != null
|
||||
? previousByTaskId.get(incoming.taskId)
|
||||
: undefined
|
||||
return mergeHistoryItemPreservingLiveProgress(previous, incoming)
|
||||
})
|
||||
}
|
||||
|
||||
function mergeHistoryItemPreservingLiveProgress(
|
||||
previous: AppearancePatentHistoryItem | undefined,
|
||||
incoming: AppearancePatentHistoryItem,
|
||||
) {
|
||||
if (!previous) return incoming
|
||||
const shouldPreserveLiveProgress =
|
||||
(normalizeTaskStatus(previous) === 'RUNNING' || isResultPreparing(previous) || showFileProgress(previous))
|
||||
&& !showFileProgress(incoming)
|
||||
&& !canDownload(incoming)
|
||||
|
||||
if (!shouldPreserveLiveProgress) {
|
||||
return incoming
|
||||
}
|
||||
|
||||
return {
|
||||
...incoming,
|
||||
fileJobId: incoming.fileJobId ?? previous.fileJobId,
|
||||
fileStatus: incoming.fileStatus || previous.fileStatus,
|
||||
fileError: incoming.fileError || previous.fileError,
|
||||
fileReady: incoming.fileReady || previous.fileReady,
|
||||
fileProgressPercent: hasMeaningfulProgressValue(incoming.fileProgressPercent)
|
||||
? incoming.fileProgressPercent
|
||||
: previous.fileProgressPercent,
|
||||
fileProgressCurrent: hasMeaningfulProgressValue(incoming.fileProgressCurrent)
|
||||
? incoming.fileProgressCurrent
|
||||
: previous.fileProgressCurrent,
|
||||
fileProgressTotal: hasMeaningfulProgressValue(incoming.fileProgressTotal)
|
||||
? incoming.fileProgressTotal
|
||||
: previous.fileProgressTotal,
|
||||
fileProgressMessage: (incoming.fileProgressMessage || '').trim()
|
||||
? incoming.fileProgressMessage
|
||||
: previous.fileProgressMessage,
|
||||
}
|
||||
}
|
||||
|
||||
function hasMeaningfulProgressValue(value: unknown) {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) && num > 0
|
||||
}
|
||||
|
||||
function statusText(item: AppearancePatentHistoryItem) {
|
||||
const status = normalizeTaskStatus(item)
|
||||
if (status === 'RUNNING') return '执行中'
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||
</button>
|
||||
<button type="button" class="btn-run btn-queue" :disabled="pushing || !parsedRows.length" @click="pushToPythonQueue">
|
||||
<button type="button" class="btn-run btn-queue" :disabled="pushing || !parseResult?.taskId" @click="pushToPythonQueue">
|
||||
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -107,23 +107,6 @@
|
||||
</div>
|
||||
|
||||
<div class="subsection-title">匹配任务</div>
|
||||
<div class="result-list-wrap preview-wrap">
|
||||
<div class="result-list-header">
|
||||
<span>解析结果</span>
|
||||
<span v-if="parsedRows.length" class="muted">共 {{ parsedRows.length }} 条</span>
|
||||
</div>
|
||||
<div v-if="!parsedRows.length" class="empty-tasks">暂无解析结果</div>
|
||||
<el-table v-else :data="previewRows" height="120" class="result-table">
|
||||
<el-table-column prop="displayId" label="ID" width="90" />
|
||||
<el-table-column prop="sku" label="SKU" width="130" />
|
||||
<el-table-column prop="asin" label="ASIN" width="130" />
|
||||
<el-table-column prop="country" label="国家" width="100" />
|
||||
<el-table-column prop="price" label="价格" width="90" />
|
||||
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="url" label="URL" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="result-list-wrap">
|
||||
<div class="result-list-header">
|
||||
<span>当前任务</span>
|
||||
@@ -240,10 +223,8 @@ import {
|
||||
parseSimilarAsin,
|
||||
type ProductCategoryItemVo,
|
||||
type SimilarAsinFilterConditionVo,
|
||||
type SimilarAsinParsedGroup,
|
||||
type SimilarAsinDashboardVo,
|
||||
type SimilarAsinHistoryItem,
|
||||
type SimilarAsinParsedRow,
|
||||
type SimilarAsinParseVo,
|
||||
type UploadedFileRef,
|
||||
type UploadFileVo,
|
||||
@@ -258,7 +239,6 @@ import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
const selectedFileNames = ref<string[]>([])
|
||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||
const parseResult = ref<SimilarAsinParseVo | null>(null)
|
||||
const parsedGroups = ref<SimilarAsinParsedGroup[]>([])
|
||||
type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
||||
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||
const filterConditionInput = ref('')
|
||||
@@ -292,10 +272,30 @@ const dashboard = ref<SimilarAsinDashboardVo>({
|
||||
})
|
||||
const historyItems = ref<SimilarAsinHistoryItem[]>([])
|
||||
|
||||
const parsedRows = computed<SimilarAsinParsedRow[]>(() => parseResult.value?.items || [])
|
||||
const previewRows = computed(() => parsedRows.value)
|
||||
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
||||
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
||||
const pendingParseItem = computed<SimilarAsinHistoryItem | null>(() => {
|
||||
const result = parseResult.value
|
||||
if (!result?.taskId) return null
|
||||
const exists = historyItems.value.some((item) => item.taskId === result.taskId)
|
||||
if (exists) return null
|
||||
return {
|
||||
taskId: result.taskId,
|
||||
sourceFilename: result.sourceFilename,
|
||||
rowCount: result.acceptedRows,
|
||||
taskStatus: 'PENDING',
|
||||
success: false,
|
||||
}
|
||||
})
|
||||
const currentItems = computed(() => {
|
||||
const activeIds = new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value])
|
||||
const activeItems = historyItems.value.filter((item) => {
|
||||
const status = normalizeTaskStatus(item)
|
||||
return item.taskId != null && (activeIds.has(item.taskId) || status === 'RUNNING' || status === 'PENDING')
|
||||
})
|
||||
if (!pendingParseItem.value) return activeItems
|
||||
const exists = activeItems.some((item) => item.taskId === pendingParseItem.value?.taskId)
|
||||
return exists ? activeItems : [pendingParseItem.value, ...activeItems]
|
||||
})
|
||||
const historyOnlyItems = computed(() =>
|
||||
historyItems.value.filter((i) => {
|
||||
const status = normalizeTaskStatus(i)
|
||||
@@ -470,7 +470,6 @@ async function selectFiles() {
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||
parseResult.value = null
|
||||
parsedGroups.value = []
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
}
|
||||
@@ -493,7 +492,6 @@ async function selectFolder() {
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
parseResult.value = null
|
||||
parsedGroups.value = []
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
||||
@@ -572,7 +570,6 @@ async function parseFiles() {
|
||||
const res = await parseSimilarAsin(files, filterCondition, effectiveCozeApiKey())
|
||||
parseResult.value = res
|
||||
queuedTaskSummary.value = null
|
||||
parsedGroups.value = []
|
||||
queuePayloadText.value = ''
|
||||
ElMessage.success(`解析完成,共 ${res.acceptedRows} 条`)
|
||||
} catch (e) {
|
||||
@@ -633,7 +630,6 @@ async function pushToPythonQueue() {
|
||||
|
||||
function clearParsedTask() {
|
||||
parseResult.value = null
|
||||
parsedGroups.value = []
|
||||
queuePayloadText.value = ''
|
||||
}
|
||||
|
||||
@@ -985,7 +981,6 @@ onUnmounted(() => {
|
||||
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 8px; background: #1e1e1e; min-height: 180px; margin: 0 0 16px; }
|
||||
.result-list-header { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #2a2a2a; color: #ddd; font-size: 14px; }
|
||||
.empty-tasks { color: #666; font-size: 13px; padding: 18px; text-align: center; }
|
||||
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2a2a2a; --el-table-text-color: #ccc; --el-table-border-color: #333; }
|
||||
.task-list { list-style: none; margin: 0; padding: 12px; }
|
||||
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 8px; background: #222; }
|
||||
.left { flex: 1; min-width: 0; }
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>外观专利检测</title>
|
||||
<script type="module" crossorigin src="/assets/appearance-patent.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DfxdJ4w6.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/appearance-patent-D7zGgGOj.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/appearance-patent--ibZ-SKG.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
1
new_web_source/assets/appearance-patent--ibZ-SKG.css
Normal file
1
new_web_source/assets/appearance-patent--ibZ-SKG.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
new_web_source/assets/brand-DfxdJ4w6.js
Normal file
1
new_web_source/assets/brand-DfxdJ4w6.js
Normal file
@@ -0,0 +1 @@
|
||||
import{be as r}from"./pywebview-ZMN052YE.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
new_web_source/assets/el-checkbox-CzNtyWdF.css
Normal file
1
new_web_source/assets/el-checkbox-CzNtyWdF.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/el-input-DDf1iPG4.css
Normal file
1
new_web_source/assets/el-input-DDf1iPG4.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
55
new_web_source/assets/pywebview-ZMN052YE.js
Normal file
55
new_web_source/assets/pywebview-ZMN052YE.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
new_web_source/assets/shop-match-BPVfWxWj.css
Normal file
1
new_web_source/assets/shop-match-BPVfWxWj.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
new_web_source/assets/similar-asin-CSjyVCTW.css
Normal file
1
new_web_source/assets/similar-asin-CSjyVCTW.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
new_web_source/assets/zh-cn-BXYgxART.js
Normal file
1
new_web_source/assets/zh-cn-BXYgxART.js
Normal file
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>格式转换 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DfxdJ4w6.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
||||
</head>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据去重 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DfxdJ4w6.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
||||
</head>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>删除品牌 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DfxdJ4w6.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BJod9K65.css">
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>巡店删除 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/patrol-delete.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-DCaAv7Wt.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BXYgxART.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/patrol-delete-CBBdonF4.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-DDf1iPG4.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-checkbox-CzNtyWdF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>跟价 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/price-track.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DfxdJ4w6.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/price-track-Hozgt_em.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-DDf1iPG4.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-checkbox-CzNtyWdF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>商品风险解决 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/product-risk.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/product-risk-DV5AzjVc.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-DDf1iPG4.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-checkbox-CzNtyWdF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>巡店删除 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/query-asin.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-DCaAv7Wt.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BXYgxART.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/query-asin-B4RsOiza.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-DDf1iPG4.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-checkbox-CzNtyWdF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>匹配店铺 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/shop-match.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-DCaAv7Wt.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BXYgxART.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/shop-match-B2HWAgBY.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/shop-match-BPVfWxWj.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-input-DDf1iPG4.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-checkbox-CzNtyWdF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>货源查询</title>
|
||||
<script type="module" crossorigin src="/assets/similar-asin.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-DNfEzGOK.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-D4E3c93-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DfxdJ4w6.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/similar-asin-0mttxPNV.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-CZbviO2X.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/similar-asin-CSjyVCTW.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-checkbox-CzNtyWdF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据拆分 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-ZMN052YE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DfxdJ4w6.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user