修复菜单消失
This commit is contained in:
@@ -38,4 +38,8 @@ public class CollectDataSubmitResultRequest {
|
|||||||
@JsonAlias({"rows", "data", "items"})
|
@JsonAlias({"rows", "data", "items"})
|
||||||
@Schema(description = "本分片回传数据行")
|
@Schema(description = "本分片回传数据行")
|
||||||
private List<CollectDataSubmitRowDto> items = new ArrayList<>();
|
private List<CollectDataSubmitRowDto> items = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonAlias({"summary", "summary_rows", "汇总"})
|
||||||
|
@Schema(description = "Python 端按关键词聚合的配送方式 / 页数统计;通常仅在 done=true 时携带,空数组等同于不携带,Java 回退到自聚合 rawItems。")
|
||||||
|
private List<CollectDataSummaryRowDto> summaries = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.nanri.aiimage.modules.collectdata.model.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@Schema(description = "采集数据 Python 关键词级聚合统计行,通常仅在 done=true 时携带")
|
||||||
|
public class CollectDataSummaryRowDto {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关键词原文。
|
||||||
|
*/
|
||||||
|
@JsonAlias({"关键词", "key_word", "key word"})
|
||||||
|
@Schema(description = "关键词原文", example = "phone case")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 该关键词命中 FBA 配送方式的行数。
|
||||||
|
*/
|
||||||
|
@Schema(description = "FBA 行数", example = "12")
|
||||||
|
private Integer fba;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 该关键词命中 FBM 配送方式的行数。
|
||||||
|
*/
|
||||||
|
@Schema(description = "FBM 行数", example = "8")
|
||||||
|
private Integer fbm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 该关键词命中 AMZ 配送方式的行数。
|
||||||
|
*/
|
||||||
|
@Schema(description = "AMZ 行数", example = "5")
|
||||||
|
private Integer amz;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 该关键词「无配送方式」(未识别)行数。
|
||||||
|
*/
|
||||||
|
@JsonProperty("none_count")
|
||||||
|
@JsonAlias({"none", "unknown", "无配送方式", "noneCount"})
|
||||||
|
@Schema(description = "无配送方式(未识别 FBA/FBM/AMZ)行数", example = "3")
|
||||||
|
private Integer noneCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 该关键词所有命中行的最大页码,由 Python 抓取端汇总。
|
||||||
|
*/
|
||||||
|
@JsonProperty("total_page")
|
||||||
|
@JsonAlias({"page", "totalPage", "页数", "max_page", "maxPage"})
|
||||||
|
@Schema(description = "该关键词总页数(Python 端取所有命中行的最大页码)", example = "10")
|
||||||
|
private Integer totalPage;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.nanri.aiimage.modules.collectdata.service;
|
package com.nanri.aiimage.modules.collectdata.service;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSummaryRowDto;
|
||||||
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo;
|
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
@@ -30,14 +31,18 @@ public class CollectDataExcelAssemblyService {
|
|||||||
*
|
*
|
||||||
* @param outputXlsx 目标文件
|
* @param outputXlsx 目标文件
|
||||||
* @param items 经过 ASIN 去重 / 无效品牌过滤 / 品牌检测后保留下来的明细行,写入「采集数据结果」sheet
|
* @param items 经过 ASIN 去重 / 无效品牌过滤 / 品牌检测后保留下来的明细行,写入「采集数据结果」sheet
|
||||||
* @param rawItems Python 回传的全量原始行(不经后端任何过滤),写入「结果文件」sheet 的关键词聚合统计
|
* @param summaries Python 在 done=true 时携带的关键词级聚合统计;非空时优先使用,直接落「结果文件」sheet
|
||||||
|
* @param rawItems Python 回传的全量原始行(不经后端任何过滤);仅在 summaries 为空时作为 fallback 自聚合
|
||||||
*/
|
*/
|
||||||
public void writeWorkbook(File outputXlsx, List<CollectDataResultRowVo> items, List<CollectDataResultRowVo> rawItems) {
|
public void writeWorkbook(File outputXlsx,
|
||||||
|
List<CollectDataResultRowVo> items,
|
||||||
|
List<CollectDataSummaryRowDto> summaries,
|
||||||
|
List<CollectDataResultRowVo> rawItems) {
|
||||||
SXSSFWorkbook workbook = new SXSSFWorkbook(200);
|
SXSSFWorkbook workbook = new SXSSFWorkbook(200);
|
||||||
workbook.setCompressTempFiles(true);
|
workbook.setCompressTempFiles(true);
|
||||||
try (FileOutputStream outputStream = new FileOutputStream(outputXlsx)) {
|
try (FileOutputStream outputStream = new FileOutputStream(outputXlsx)) {
|
||||||
writeDetailSheet(workbook, items);
|
writeDetailSheet(workbook, items);
|
||||||
writeSummarySheet(workbook, rawItems);
|
writeSummarySheet(workbook, summaries, rawItems);
|
||||||
workbook.write(outputStream);
|
workbook.write(outputStream);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.warn("[collect-data] write workbook failed: {}", ex.getMessage());
|
log.warn("[collect-data] write workbook failed: {}", ex.getMessage());
|
||||||
@@ -76,11 +81,14 @@ public class CollectDataExcelAssemblyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「结果文件」sheet 基于 Python 回传的全量原始数据按关键词聚合:
|
* 「结果文件」sheet 的关键词级聚合:
|
||||||
* - FBA / FBM / AMZ / 无配送方式 列为各分类的命中行数;
|
* - 优先使用 Python 在 done=true 时携带的 summaries,避免 Java 端二次聚合带来的口径漂移;
|
||||||
* - 页数列取该关键词所有原始行中页码的最大值,反映 Python 实际抓取到的总页数。
|
* - 当 summaries 为空(Python 端尚未接入或解析失败)时,回退到基于 rawItems 自聚合,
|
||||||
|
* 保证 Excel 不会因迁移过渡期而出现空表。等 Python 端全部接入并稳定后,可移除 fallback 分支。
|
||||||
*/
|
*/
|
||||||
private void writeSummarySheet(SXSSFWorkbook workbook, List<CollectDataResultRowVo> rawItems) {
|
private void writeSummarySheet(SXSSFWorkbook workbook,
|
||||||
|
List<CollectDataSummaryRowDto> summaries,
|
||||||
|
List<CollectDataResultRowVo> rawItems) {
|
||||||
Sheet sheet = workbook.createSheet(SHEET_SUMMARY_NAME);
|
Sheet sheet = workbook.createSheet(SHEET_SUMMARY_NAME);
|
||||||
Row headerRow = sheet.createRow(0);
|
Row headerRow = sheet.createRow(0);
|
||||||
for (int i = 0; i < SHEET_SUMMARY_HEADER.length; i++) {
|
for (int i = 0; i < SHEET_SUMMARY_HEADER.length; i++) {
|
||||||
@@ -88,6 +96,30 @@ public class CollectDataExcelAssemblyService {
|
|||||||
sheet.setColumnWidth(i, (i == 0 ? 28 : 12) * 256);
|
sheet.setColumnWidth(i, (i == 0 ? 28 : 12) * 256);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (summaries != null && !summaries.isEmpty()) {
|
||||||
|
// 优先分支:Python 携带的关键词级聚合直接落表,null 字段统一兜底为 0;页数 ≤0 写空字符串。
|
||||||
|
int rowIndex = 1;
|
||||||
|
for (CollectDataSummaryRowDto item : summaries) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Row row = sheet.createRow(rowIndex++);
|
||||||
|
row.createCell(0).setCellValue(safe(item.getKeyword()));
|
||||||
|
row.createCell(1).setCellValue(intOrZero(item.getFba()));
|
||||||
|
row.createCell(2).setCellValue(intOrZero(item.getFbm()));
|
||||||
|
row.createCell(3).setCellValue(intOrZero(item.getAmz()));
|
||||||
|
row.createCell(4).setCellValue(intOrZero(item.getNoneCount()));
|
||||||
|
Integer totalPage = item.getTotalPage();
|
||||||
|
if (totalPage != null && totalPage > 0) {
|
||||||
|
row.createCell(5).setCellValue(totalPage);
|
||||||
|
} else {
|
||||||
|
row.createCell(5).setCellValue("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 分支:基于 rawItems 自聚合(Python 端未接入时使用)。
|
||||||
Map<String, KeywordSummary> grouped = new LinkedHashMap<>();
|
Map<String, KeywordSummary> grouped = new LinkedHashMap<>();
|
||||||
if (rawItems != null) {
|
if (rawItems != null) {
|
||||||
for (CollectDataResultRowVo item : rawItems) {
|
for (CollectDataResultRowVo item : rawItems) {
|
||||||
@@ -131,6 +163,10 @@ public class CollectDataExcelAssemblyService {
|
|||||||
return value == null ? "" : value;
|
return value == null ? "" : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int intOrZero(Integer value) {
|
||||||
|
return value == null ? 0 : value;
|
||||||
|
}
|
||||||
|
|
||||||
private static class KeywordSummary {
|
private static class KeywordSummary {
|
||||||
int fba;
|
int fba;
|
||||||
int fbm;
|
int fbm;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataParseRequest;
|
|||||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSourceFileDto;
|
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSourceFileDto;
|
||||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitResultRequest;
|
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitRowDto;
|
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitRowDto;
|
||||||
|
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSummaryRowDto;
|
||||||
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataCountryPrefEntity;
|
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataCountryPrefEntity;
|
||||||
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataItemEntity;
|
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataItemEntity;
|
||||||
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataCountryPreferenceVo;
|
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataCountryPreferenceVo;
|
||||||
@@ -454,6 +455,11 @@ public class CollectDataService {
|
|||||||
persistScope(taskId, scopeKey, scopeHash, chunkTotal, request);
|
persistScope(taskId, scopeKey, scopeHash, chunkTotal, request);
|
||||||
|
|
||||||
stats.finalRowCount = countFinalRows(taskId);
|
stats.finalRowCount = countFinalRows(taskId);
|
||||||
|
// Python 在 done=true 那次回传携带关键词级聚合统计;非空时按"最后一次为准"覆盖。
|
||||||
|
List<CollectDataSummaryRowDto> incomingSummaries = request.getSummaries();
|
||||||
|
if (incomingSummaries != null && !incomingSummaries.isEmpty()) {
|
||||||
|
stats.summaries = new ArrayList<>(incomingSummaries);
|
||||||
|
}
|
||||||
persistStats(task, stats);
|
persistStats(task, stats);
|
||||||
|
|
||||||
if (request.getError() != null && !request.getError().isBlank()) {
|
if (request.getError() != null && !request.getError().isBlank()) {
|
||||||
@@ -812,6 +818,8 @@ public class CollectDataService {
|
|||||||
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
||||||
throw new BusinessException("结果记录不存在");
|
throw new BusinessException("结果记录不存在");
|
||||||
}
|
}
|
||||||
|
// 先加载 stats,使 Python 携带的 summaries 可优先用于「结果文件」sheet;rawRows 仅作为 fallback。
|
||||||
|
CollectDataStats stats = loadStats(task);
|
||||||
List<CollectDataResultRowVo> rows = loadFinalRows(task.getId());
|
List<CollectDataResultRowVo> rows = loadFinalRows(task.getId());
|
||||||
// Sheet「结果文件」按需求基于 Python 回传的全量数据聚合,不经后端 ASIN/品牌过滤丢弃,
|
// Sheet「结果文件」按需求基于 Python 回传的全量数据聚合,不经后端 ASIN/品牌过滤丢弃,
|
||||||
// 因此从 biz_task_chunk 反序列化全部原始行。
|
// 因此从 biz_task_chunk 反序列化全部原始行。
|
||||||
@@ -820,7 +828,7 @@ public class CollectDataService {
|
|||||||
String filename = buildResultFilename(task, result);
|
String filename = buildResultFilename(task, result);
|
||||||
File xlsx = FileUtil.file(workRoot, filename);
|
File xlsx = FileUtil.file(workRoot, filename);
|
||||||
try {
|
try {
|
||||||
excelAssemblyService.writeWorkbook(xlsx, rows, rawRows);
|
excelAssemblyService.writeWorkbook(xlsx, rows, stats.summaries, rawRows);
|
||||||
String objectKey = ossStorageService.uploadResultFile(xlsx, MODULE_TYPE);
|
String objectKey = ossStorageService.uploadResultFile(xlsx, MODULE_TYPE);
|
||||||
result.setResultFilename(filename);
|
result.setResultFilename(filename);
|
||||||
result.setResultFileUrl(objectKey);
|
result.setResultFileUrl(objectKey);
|
||||||
@@ -831,7 +839,7 @@ public class CollectDataService {
|
|||||||
result.setErrorMessage(null);
|
result.setErrorMessage(null);
|
||||||
fileResultMapper.updateById(result);
|
fileResultMapper.updateById(result);
|
||||||
|
|
||||||
CollectDataStats stats = loadStats(task);
|
// 复用同一份 stats 更新 finalRowCount 后再持久化,避免重复 loadStats 丢失 summaries。
|
||||||
stats.finalRowCount = rows.size();
|
stats.finalRowCount = rows.size();
|
||||||
persistStats(task, stats);
|
persistStats(task, stats);
|
||||||
task.setStatus(STATUS_SUCCESS);
|
task.setStatus(STATUS_SUCCESS);
|
||||||
@@ -998,6 +1006,16 @@ public class CollectDataService {
|
|||||||
stats.brandRejectedCount = root.path("brandRejectedCount").asInt(0);
|
stats.brandRejectedCount = root.path("brandRejectedCount").asInt(0);
|
||||||
stats.brandQueryFailedCount = root.path("brandQueryFailedCount").asInt(0);
|
stats.brandQueryFailedCount = root.path("brandQueryFailedCount").asInt(0);
|
||||||
stats.finalRowCount = root.path("finalRowCount").asInt(0);
|
stats.finalRowCount = root.path("finalRowCount").asInt(0);
|
||||||
|
// 反序列化 Python 携带的关键词级聚合;解析失败保持空 list,仅 warn 不抛,避免影响其他统计
|
||||||
|
JsonNode summariesNode = root.get("summaries");
|
||||||
|
if (summariesNode != null && summariesNode.isArray()) {
|
||||||
|
try {
|
||||||
|
stats.summaries = objectMapper.convertValue(summariesNode,
|
||||||
|
new TypeReference<List<CollectDataSummaryRowDto>>() {});
|
||||||
|
} catch (Exception convertEx) {
|
||||||
|
log.warn("[collect-data] parse summaries failed taskId={} err={}", task.getId(), convertEx.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.warn("[collect-data] parse result stats failed taskId={} err={}", task.getId(), ex.getMessage());
|
log.warn("[collect-data] parse result stats failed taskId={} err={}", task.getId(), ex.getMessage());
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1035,8 @@ public class CollectDataService {
|
|||||||
payload.put("brandRejectedCount", stats.brandRejectedCount);
|
payload.put("brandRejectedCount", stats.brandRejectedCount);
|
||||||
payload.put("brandQueryFailedCount", stats.brandQueryFailedCount);
|
payload.put("brandQueryFailedCount", stats.brandQueryFailedCount);
|
||||||
payload.put("finalRowCount", stats.finalRowCount);
|
payload.put("finalRowCount", stats.finalRowCount);
|
||||||
|
// 持久化 Python 携带的关键词级聚合;后续 processResultFileJob 阶段读取用于落 Excel
|
||||||
|
payload.put("summaries", stats.summaries == null ? List.of() : stats.summaries);
|
||||||
task.setResultJson(objectMapper.writeValueAsString(payload));
|
task.setResultJson(objectMapper.writeValueAsString(payload));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("保存采集统计失败");
|
throw new BusinessException("保存采集统计失败");
|
||||||
@@ -1129,6 +1149,8 @@ public class CollectDataService {
|
|||||||
private int brandRejectedCount;
|
private int brandRejectedCount;
|
||||||
private int brandQueryFailedCount;
|
private int brandQueryFailedCount;
|
||||||
private int finalRowCount;
|
private int finalRowCount;
|
||||||
|
// Python 在 done=true 时携带的关键词级聚合统计;空表示由 Java 端用 rawItems 自聚合兜底
|
||||||
|
private List<CollectDataSummaryRowDto> summaries = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ type NavItem = {
|
|||||||
key: string
|
key: string
|
||||||
label: string
|
label: string
|
||||||
href?: string
|
href?: string
|
||||||
|
columnKey?: string
|
||||||
|
aliases?: ReadonlyArray<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavGroup = {
|
type NavGroup = {
|
||||||
@@ -79,17 +81,21 @@ type NavGroup = {
|
|||||||
items: ReadonlyArray<NavItem>
|
items: ReadonlyArray<NavItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
active: ActiveNavKey
|
active: ActiveNavKey
|
||||||
showNav?: boolean
|
showNav?: boolean
|
||||||
showHomeLink?: boolean
|
showHomeLink?: boolean
|
||||||
showActions?: boolean
|
showActions?: boolean
|
||||||
}>()
|
}>(), {
|
||||||
|
showNav: true,
|
||||||
|
showHomeLink: true,
|
||||||
|
showActions: true,
|
||||||
|
})
|
||||||
|
|
||||||
const active = props.active
|
const active = props.active
|
||||||
const showNav = props.showNav ?? true
|
const showNav = props.showNav
|
||||||
const showHomeLink = props.showHomeLink ?? true
|
const showHomeLink = props.showHomeLink
|
||||||
const showActions = props.showActions ?? true
|
const showActions = props.showActions
|
||||||
const allowedColumnKeys = ref<string[] | null>(null)
|
const allowedColumnKeys = ref<string[] | null>(null)
|
||||||
|
|
||||||
const navGroups: ReadonlyArray<NavGroup> = [
|
const navGroups: ReadonlyArray<NavGroup> = [
|
||||||
@@ -115,7 +121,7 @@ const navGroups: ReadonlyArray<NavGroup> = [
|
|||||||
{ key: 'delete-brand', label: '删除ASIN', href: '/new_web_source/delete-brand.html' },
|
{ key: 'delete-brand', label: '删除ASIN', href: '/new_web_source/delete-brand.html' },
|
||||||
{ key: 'product-risk', label: '商品风险解决', href: '/new_web_source/product-risk.html' },
|
{ key: 'product-risk', label: '商品风险解决', href: '/new_web_source/product-risk.html' },
|
||||||
{ key: 'shop-match', label: '定时匹配', href: '/new_web_source/shop-match.html' },
|
{ key: 'shop-match', label: '定时匹配', href: '/new_web_source/shop-match.html' },
|
||||||
{ key: 'pricing', label: '跟价', href: '/new_web_source/price-track.html' },
|
{ key: 'pricing', label: '跟价', href: '/new_web_source/price-track.html', aliases: ['price-track'] },
|
||||||
{ key: 'patrol-delete', label: '巡店删除', href: '/new_web_source/patrol-delete.html' },
|
{ key: 'patrol-delete', label: '巡店删除', href: '/new_web_source/patrol-delete.html' },
|
||||||
{ key: 'query-asin', label: '查询ASIN', href: '/new_web_source/query-asin.html' },
|
{ key: 'query-asin', label: '查询ASIN', href: '/new_web_source/query-asin.html' },
|
||||||
{ key: 'withdraw', label: '取款' },
|
{ key: 'withdraw', label: '取款' },
|
||||||
@@ -132,12 +138,29 @@ const navGroups: ReadonlyArray<NavGroup> = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function getItemPermissionKeys(item: NavItem) {
|
||||||
|
return [item.key, item.columnKey, ...(item.aliases || [])]
|
||||||
|
.map((key) => String(key || '').trim().toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
const visibleNavGroups = computed(() => {
|
const visibleNavGroups = computed(() => {
|
||||||
if (allowedColumnKeys.value === null) {
|
if (allowedColumnKeys.value === null) {
|
||||||
return navGroups
|
return navGroups
|
||||||
}
|
}
|
||||||
const allowedSet = new Set(allowedColumnKeys.value)
|
const allowedSet = new Set(allowedColumnKeys.value)
|
||||||
return navGroups.filter((group) => allowedSet.has(group.columnKey))
|
|
||||||
|
const groups = navGroups.flatMap((group) => {
|
||||||
|
if (allowedSet.has(group.columnKey)) {
|
||||||
|
return [group]
|
||||||
|
}
|
||||||
|
const items = group.items.filter((item) =>
|
||||||
|
getItemPermissionKeys(item).some((key) => allowedSet.has(key)),
|
||||||
|
)
|
||||||
|
return items.length ? [{ ...group, items }] : []
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups.length ? groups : navGroups
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ export interface PermissionMenuItem {
|
|||||||
id: number | string
|
id: number | string
|
||||||
name?: string
|
name?: string
|
||||||
column_key?: string
|
column_key?: string
|
||||||
|
columnKey?: string
|
||||||
route_path?: string
|
route_path?: string
|
||||||
|
routePath?: string
|
||||||
menu_type?: string
|
menu_type?: string
|
||||||
sort_order?: number
|
sort_order?: number
|
||||||
created_at?: string
|
created_at?: string
|
||||||
@@ -12,8 +14,10 @@ export interface PermissionMenuItem {
|
|||||||
|
|
||||||
interface PermissionMenuResponse {
|
interface PermissionMenuResponse {
|
||||||
success: boolean
|
success: boolean
|
||||||
|
data?: PermissionMenuItem[]
|
||||||
items?: PermissionMenuItem[]
|
items?: PermissionMenuItem[]
|
||||||
error?: string
|
error?: string
|
||||||
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentUserId() {
|
function getCurrentUserId() {
|
||||||
@@ -29,6 +33,23 @@ function getAppPermissionCacheKey(uid: number) {
|
|||||||
return `app_column_permissions:${String(uid)}`
|
return `app_column_permissions:${String(uid)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAuthToken() {
|
||||||
|
return typeof window === 'undefined' ? '' : window.localStorage.getItem('aiimage_auth_token') || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeColumnKeys(items: PermissionMenuItem[] | undefined) {
|
||||||
|
const keys = new Set<string>()
|
||||||
|
for (const item of items || []) {
|
||||||
|
for (const value of [item.column_key, item.columnKey, item.route_path, item.routePath]) {
|
||||||
|
const key = String(value || '').trim().toLowerCase()
|
||||||
|
if (key) {
|
||||||
|
keys.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(keys)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCurrentUserAppColumnKeys() {
|
export async function getCurrentUserAppColumnKeys() {
|
||||||
const uid = getCurrentUserId()
|
const uid = getCurrentUserId()
|
||||||
const cacheKey = getAppPermissionCacheKey(uid)
|
const cacheKey = getAppPermissionCacheKey(uid)
|
||||||
@@ -36,28 +57,35 @@ export async function getCurrentUserAppColumnKeys() {
|
|||||||
try {
|
try {
|
||||||
const cachedItems = JSON.parse(window.localStorage.getItem(cacheKey) || 'null') as PermissionMenuItem[] | null
|
const cachedItems = JSON.parse(window.localStorage.getItem(cacheKey) || 'null') as PermissionMenuItem[] | null
|
||||||
if (Array.isArray(cachedItems)) {
|
if (Array.isArray(cachedItems)) {
|
||||||
return cachedItems
|
const cachedKeys = normalizeColumnKeys(cachedItems)
|
||||||
.map((item) => String(item.column_key || '').trim().toLowerCase())
|
if (cachedKeys.length) {
|
||||||
.filter(Boolean)
|
return cachedKeys
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {}
|
||||||
|
const token = getAuthToken()
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
const res = await requestGetJson<PermissionMenuResponse>(
|
const res = await requestGetJson<PermissionMenuResponse>(
|
||||||
`/api/admin/user/${encodeURIComponent(String(uid))}/column-permissions`,
|
`/newApi/api/admin/permission-users/${encodeURIComponent(String(uid))}/column-permissions`,
|
||||||
{
|
{
|
||||||
params: { menu_type: 'app' },
|
params: { menuType: 'app' },
|
||||||
|
headers,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
throw new Error(res.error || '获取菜单权限失败')
|
throw new Error(res.error || res.message || '获取菜单权限失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const items = res.data || res.items || []
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(cacheKey, JSON.stringify(res.items || []))
|
window.localStorage.setItem(cacheKey, JSON.stringify(items))
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
|
|
||||||
return (res.items || [])
|
return normalizeColumnKeys(items)
|
||||||
.map((item) => String(item.column_key || '').trim().toLowerCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user