| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- import { CommonModule } from '@angular/common';
- import { Component, OnDestroy, OnInit } from '@angular/core';
- import { FormsModule } from '@angular/forms';
- import { Router } from '@angular/router';
- import { FilterOptions } from '../../../shared/interfaces/voc-data.interface';
- import { FilterBarComponent } from '../../../shared/components/filter-bar/filter-bar.component';
- import { AiAnalysisPanelComponent } from '../../../shared/components/ai-analysis-panel/ai-analysis-panel.component';
- import { DesignerPerformanceService } from '../../../shared/services/designer-performance.service';
- import {
- DesignerIssueMatrixRow,
- DesignerPerformanceDashboardData,
- DesignerPerformanceFilters,
- DesignerPerformanceRankingItem,
- DesignerProductDrilldownItem,
- DesignerStarDistributionPoint
- } from '../../../shared/interfaces/designer-performance.interface';
- import { SummaryMetricCardComponent } from '../../../shared/components/summary-metric-card/summary-metric-card.component';
- import { LoadingSpinnerComponent } from '../../../shared/components/loading-spinner/loading-spinner.component';
- import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component';
- import { ErrorStateComponent } from '../../../shared/components/error-state/error-state.component';
- import { DataTableShellComponent } from '../../../shared/components/data-table-shell/data-table-shell.component';
- import { ContentCardComponent } from '../../../shared/components/content-card/content-card.component';
- import { BoardTabsComponent, BoardTabItem } from '../../../shared/components/board-tabs/board-tabs.component';
- import { ProductIdentityComponent } from '../../../shared/components/product-identity/product-identity.component';
- import { AlertCardComponent } from '../../../shared/components/alert-card/alert-card.component';
- import { SplitPanelComponent } from '../../../shared/components/split-panel/split-panel.component';
- import { ProductDimensionService } from '../../../shared/services/product-dimension.service';
- import { Subscription } from 'rxjs';
- type DesignerPerformanceTabKey = 'ranking' | 'trend' | 'issues' | 'ai';
- type DesignerProductSortKey = 'risk' | 'rating' | 'negative' | 'return';
- type DesignerRankingSortKey = 'health' | 'rating' | 'negative';
- @Component({
- selector: 'app-designer-performance',
- standalone: true,
- imports: [CommonModule, FormsModule, FilterBarComponent, AiAnalysisPanelComponent, SummaryMetricCardComponent, LoadingSpinnerComponent, EmptyStateComponent, ErrorStateComponent, DataTableShellComponent, ContentCardComponent, BoardTabsComponent, ProductIdentityComponent, AlertCardComponent, SplitPanelComponent],
- templateUrl: './designer-performance.component.html',
- styleUrls: ['./designer-performance.component.scss']
- })
- export class DesignerPerformanceComponent implements OnInit, OnDestroy {
- currentFilters: DesignerPerformanceFilters = { timeRange: '30days' };
- data?: DesignerPerformanceDashboardData;
- private dataRevision = 0;
- private highRiskRankingsCacheKey = '';
- private highRiskRankingsCache: DesignerPerformanceRankingItem[] = [];
- private highRiskHeadlineCacheKey = '';
- private highRiskHeadlineCache = '';
- private selectedDesignerSummaryCacheKey = '';
- private selectedDesignerSummaryCache: DesignerPerformanceRankingItem | undefined;
- private pageTabsCacheKey = '';
- private pageTabsCache: BoardTabItem[] = [];
- private designerFilterOptionsCacheKey = '';
- private designerFilterOptionsCache: string[] = [];
- private dimensionDesignerOptions: string[] = [];
- private designerOptionsSub?: Subscription;
- private lastRequestedFilterKey = '';
- private loadRequestSeq = 0;
- private initialFilterApplied = false;
- private initialLoadFallbackId: number | null = null;
- loading = false;
- error = '';
- selectedDesigner = '';
- designerFilterOptions: string[] = [];
- productSearch = '';
- productCurrentPage = 1;
- productPageSize = 10;
- productSortKey: DesignerProductSortKey = 'risk';
- productSortDir: 'asc' | 'desc' = 'desc';
- readonly productPageSizeOptions = [10, 20, 50];
- filteredProducts: DesignerProductDrilldownItem[] = [];
- visibleProducts: DesignerProductDrilldownItem[] = [];
- productTotalPages = 1;
- productDisplayPage = 1;
- productPageStart = 0;
- productPageEnd = 0;
- private readonly starDistributionCache = new WeakMap<object, DesignerProductDrilldownItem['starDistribution']>();
- private readonly starCountCache = new WeakMap<object, number>();
- lastUpdateTime = new Date();
- constructor(
- private designerPerformanceService: DesignerPerformanceService,
- private productDimensionService: ProductDimensionService,
- private router: Router
- ) {}
- ngOnInit(): void {
- this.loadDesignerOptions();
- }
- ngOnDestroy(): void {
- if (this.initialLoadFallbackId !== null) {
- window.clearTimeout(this.initialLoadFallbackId);
- this.initialLoadFallbackId = null;
- }
- this.designerOptionsSub?.unsubscribe();
- }
- onFilterChange(filters: FilterOptions): void {
- this.initialFilterApplied = true;
- if (this.initialLoadFallbackId !== null) {
- window.clearTimeout(this.initialLoadFallbackId);
- this.initialLoadFallbackId = null;
- }
- const nextFilters = this.normalizeFilters(filters);
- const nextFilterKey = this.getFilterKey(nextFilters);
- if (nextFilterKey === this.lastRequestedFilterKey && (this.loading || this.data)) return;
- this.currentFilters = nextFilters;
- this.selectedDesigner = this.currentFilters.designerName?.[0] || '';
- this.productCurrentPage = 1;
- this.loadData();
- }
- loadData(): void {
- if (!this.hasStoreScope(this.currentFilters)) {
- this.loading = false;
- this.error = '';
- this.data = undefined;
- this.refreshProductView();
- return;
- }
- this.lastRequestedFilterKey = this.getFilterKey(this.currentFilters);
- const requestSeq = ++this.loadRequestSeq;
- this.loading = true;
- this.error = '';
- this.designerPerformanceService.getDashboardData(this.currentFilters).subscribe({
- next: data => {
- if (requestSeq !== this.loadRequestSeq) return;
- this.data = data;
- this.dataRevision++;
- this.designerFilterOptions = this.buildDesignerFilterOptions();
- this.refreshProductView();
- this.lastUpdateTime = new Date();
- this.loading = false;
- },
- error: err => {
- if (requestSeq !== this.loadRequestSeq) return;
- this.error = err?.message || '设计师表现看板加载失败';
- this.loading = false;
- }
- });
- }
- get rankings(): DesignerPerformanceRankingItem[] {
- return this.data?.rankings || [];
- }
- // #23 设计师健康排行:评分/差评独立可排序
- rankingSortKey: DesignerRankingSortKey = 'health';
- rankingSortDir: 'asc' | 'desc' = 'asc';
- setRankingSort(key: DesignerRankingSortKey): void {
- if (this.rankingSortKey === key) {
- this.rankingSortDir = this.rankingSortDir === 'desc' ? 'asc' : 'desc';
- return;
- }
- this.rankingSortKey = key;
- // 默认:健康分升序(差的在前);评分升序;差评率降序
- this.rankingSortDir = key === 'negative' ? 'desc' : 'asc';
- }
- getRankingSortIndicator(key: DesignerRankingSortKey): string {
- if (this.rankingSortKey !== key) return '';
- return this.rankingSortDir === 'desc' ? '↓' : '↑';
- }
- sortedRankings(): DesignerPerformanceRankingItem[] {
- const list = [...this.rankings];
- const val = (it: DesignerPerformanceRankingItem): number =>
- this.rankingSortKey === 'rating' ? Number(it.avgRating || 0)
- : this.rankingSortKey === 'negative' ? Number(it.negativeRate || 0)
- : Number(it.healthScore || 0);
- const dir = this.rankingSortDir === 'desc' ? -1 : 1;
- return list.sort((a, b) => (val(a) - val(b)) * dir);
- }
- get matrixRows(): DesignerIssueMatrixRow[] {
- return this.data?.issueMatrix.rows || [];
- }
- // 高风险设计师摘要(预警条用)
- get highRiskRankings(): DesignerPerformanceRankingItem[] {
- const key = String(this.dataRevision);
- if (key === this.highRiskRankingsCacheKey) return this.highRiskRankingsCache;
- this.highRiskRankingsCacheKey = key;
- this.highRiskRankingsCache = this.rankings.filter(r => r.riskLevel === 'high');
- return this.highRiskRankingsCache;
- }
- get highRiskCount(): number {
- return this.highRiskRankings.length;
- }
- get highRiskHeadline(): string {
- const key = String(this.dataRevision);
- if (key === this.highRiskHeadlineCacheKey) return this.highRiskHeadlineCache;
- const head = this.highRiskRankings.slice(0, 2)
- .map(r => `${r.designerName} ${r.negativeRate}%`)
- .join('、');
- this.highRiskHeadlineCacheKey = key;
- this.highRiskHeadlineCache = head || '暂无高风险';
- return this.highRiskHeadlineCache;
- }
- private getFilteredProducts(): DesignerProductDrilldownItem[] {
- const keyword = this.productSearch.trim().toLowerCase();
- const items = (this.data?.products || []).filter(product => {
- const matchDesigner = !this.selectedDesigner || product.designerName === this.selectedDesigner;
- const matchKeyword = !keyword || [
- product.asin,
- product.title,
- product.sellerSku,
- product.productSku,
- product.operatorName,
- product.developerName,
- product.fulfillmentType
- ].some(value => String(value || '').toLowerCase().includes(keyword));
- return matchDesigner && matchKeyword;
- });
- return this.sortProducts(items);
- }
- get selectedDesignerSummary(): DesignerPerformanceRankingItem | undefined {
- const key = `${this.dataRevision}|${this.selectedDesigner}`;
- if (key === this.selectedDesignerSummaryCacheKey) return this.selectedDesignerSummaryCache;
- this.selectedDesignerSummaryCacheKey = key;
- this.selectedDesignerSummaryCache = this.rankings.find(item => item.designerName === this.selectedDesigner);
- return this.selectedDesignerSummaryCache;
- }
- get aiPrompt(): string {
- if (!this.data) return '';
- const data = this.data;
- const summary = data.summary;
- const starDesc = (distribution: DesignerStarDistributionPoint[]): string =>
- distribution?.length
- ? [...distribution].sort((a, b) => b.star - a.star).map(point => `${point.star}★${point.count}(${point.pct}%)`).join(' ')
- : '-';
- const rankingLines = this.rankings.map((item, index) =>
- `[D${index + 1}] ${item.designerName}: 健康分${item.healthScore}, 风险${item.riskLevel}, 运营${item.operatorNames.join('/') || '-'}, 产品${item.productCount}(在售${item.activeProductCount}), VOC${item.vocCount}条, 平均评分${item.avgRating}, 差评${item.negativeReviewCount}条/${item.negativeRate}%, 退货${item.returnCount}件/${item.returnPct}%, 严重风险${item.criticalRiskCount}个, 预警${item.warningRiskCount}个, 配送${item.fulfillmentTypes.join('/') || '-'}, 星级分布${starDesc(item.starDistribution)}, 主因${item.topReasons.join('/') || '暂无'}`
- ).join('\n');
- const issueMatrix = data.issueMatrix;
- const issueHeader = issueMatrix.issues?.length ? `问题维度:${issueMatrix.issues.join('、')}` : '';
- const matrixLines = issueMatrix.rows.map((row, index) => {
- const taxonomy = row.topIssueTaxonomy;
- const confidenceLabel = this.getIssueTaxonomyConfidenceLabel(taxonomy?.confidence);
- const keywords = taxonomy?.matchedKeywords?.length ? `,命中关键词${taxonomy.matchedKeywords.join('/')}` : '';
- const cells = row.cells.filter(cell => cell.count > 0)
- .map(cell => `${cell.issue}:${cell.count}(${cell.pct}%/${cell.riskLevel})`).join(',') || '暂无';
- return `[M${index + 1}] ${row.designerName}: 问题总数${row.totalIssueCount}, TOP问题${row.topIssue || '暂无'}, 责任角色${taxonomy?.ownerHint || '未识别'}, 置信度${confidenceLabel}${keywords};各问题分布:${cells}`;
- }).join('\n');
- const reasonMatrix = data.returnReasonMatrix;
- const reasonHeader = reasonMatrix?.reasons?.length ? `退货原因维度:${reasonMatrix.reasons.join('、')}` : '';
- const reasonLines = (reasonMatrix?.rows || []).map((row, index) => {
- const cells = row.cells.filter(cell => cell.count > 0)
- .map(cell => `${cell.reason}:${cell.count}(${cell.pct}%/${cell.riskLevel})`).join(',') || '暂无';
- return `[RM${index + 1}] ${row.designerName}: 退货总数${row.totalReturnCount}, TOP原因${row.topReason || '暂无'};各原因分布:${cells}`;
- }).join('\n');
- const returnRankLines = (data.returnRanking || []).map((item, index) => {
- const skus = (item.highRiskSkus || []).slice(0, 3)
- .map(sku => `${sku.asin || sku.sku}(${sku.productName || '-'},退${sku.returnCount ?? '-'}/${sku.returnRate}%,主因${sku.mainReason || '-'})`).join(';') || '暂无';
- return `[RR${index + 1}] ${item.designerName}: 运营${item.operatorNames.join('/') || '-'}, 产品${item.productCount}, SKU${item.skuCount}, 退货${item.returnCount}件/${item.returnPct}%, 配送${item.fulfillmentTypes.join('/') || '-'}, 主因${item.topReasons.join('/') || '暂无'};高风险SKU:${skus}`;
- }).join('\n');
- const productScope = this.filteredProducts;
- const scopeLabel = this.selectedDesigner ? `已下钻设计师「${this.selectedDesigner}」` : '全部设计师';
- const searchLabel = this.productSearch.trim() ? `,搜索「${this.productSearch.trim()}」` : '';
- const productLines = productScope.map((product, index) => {
- const alerts = product.alerts?.length
- ? product.alerts.map(alert => `${alert.severity}:${alert.type}-${alert.message}`).join(';')
- : '';
- return `[P${index + 1}] ${product.asin}/${product.productSku || product.sellerSku || '-'}: 标题${product.title || '-'}, 设计师${product.designerName}, 负责人${product.operatorName || '-'}, 开发${product.developerName || '-'}, 状态${product.itemStatus ?? '-'}, 配送${product.fulfillmentType || '-'}, 月销${product.monthlySales}, 评分${product.rating}(${product.ratingsCount}评), 差评${product.negativeReviewCount}条/${product.negativeRate}%, VOC${product.vocCount}条, 退货${product.returnCount}件/${product.returnRate}%, 风险${product.riskLevel}, 星级分布${starDesc(product.starDistribution)}${alerts ? `, 预警${alerts}` : ''}`;
- }).join('\n');
- const sampleWarnings: string[] = [];
- if (summary.totalVoc < 30) sampleWarnings.push('VOC样本少于30条,差评率结论需降级为趋势判断');
- if (summary.totalReturns < 10) sampleWarnings.push('退货样本少于10件,退货主因仅作线索');
- if (issueMatrix.rows.length < 3) sampleWarnings.push('问题矩阵覆盖设计师少于3人,避免过度横向排名');
- return [
- `【设计师表现总览】设计师${summary.designerCount}人,产品${summary.totalProducts}个,在售${summary.activeProducts}个,VOC${summary.totalVoc}条,退货${summary.totalReturns}件,严重风险${summary.criticalRiskCount}个,平均差评率${summary.avgNegativeRate}%`,
- `【健康排行·全部${this.rankings.length}人】\n${rankingLines || '暂无'}`,
- `【统一问题归因矩阵·全部${issueMatrix.rows.length}行】${issueHeader ? '\n' + issueHeader : ''}\n${matrixLines || '暂无'}`,
- `【退货原因矩阵·全部${reasonMatrix?.rows?.length || 0}行】${reasonHeader ? '\n' + reasonHeader : ''}\n${reasonLines || '暂无'}`,
- `【退货风险排行·全部${(data.returnRanking || []).length}人】\n${returnRankLines || '暂无'}`,
- `【下钻商品证据·${scopeLabel}${searchLabel},共${productScope.length}个】\n${productLines || '暂无'}`,
- `【证据链约束】输出结论必须引用[D]/[M]/[RM]/[RR]/[P]证据编号;优先使用统一问题归因与退货原因矩阵中的责任角色与置信度;${sampleWarnings.length ? sampleWarnings.join(';') : '当前样本量可支持方向性判断'}`
- ].join('\n') + '\n\n请基于以上完整数据输出设计师表现诊断:①高风险设计师及核心原因 ②优先处理的商品/问题 ③设计、运营、开发协同建议。控制在400字内。';
- }
- get aiSystemPrompt(): string {
- return '你是一名亚马逊跨境电商VOC与退货风险分析专家,擅长从设计师、运营负责人和商品维度定位产品问题。请用中文、Markdown格式输出,结论要可执行,并引用用户提供的证据编号。';
- }
- selectDesigner(designerName: string): void {
- this.selectedDesigner = this.selectedDesigner === designerName ? '' : designerName;
- this.productCurrentPage = 1;
- this.refreshProductView();
- }
- clearDesignerSelection(): void {
- this.selectedDesigner = '';
- this.productCurrentPage = 1;
- this.refreshProductView();
- }
- onProductSearchChange(value: string): void {
- this.productSearch = value;
- this.productCurrentPage = 1;
- this.refreshProductView();
- }
- onProductPageSizeChange(value: string): void {
- const nextSize = Number(value);
- if (!Number.isFinite(nextSize) || nextSize <= 0) return;
- this.productPageSize = nextSize;
- this.productCurrentPage = 1;
- this.refreshProductView();
- }
- onProductPageChange(page: number): void {
- const nextPage = Math.max(1, Math.min(this.productTotalPages, page));
- if (nextPage === this.productCurrentPage) return;
- this.productCurrentPage = nextPage;
- this.refreshProductView();
- }
- setProductSort(key: DesignerProductSortKey): void {
- if (this.productSortKey === key) {
- this.productSortDir = this.productSortDir === 'desc' ? 'asc' : 'desc';
- this.productCurrentPage = 1;
- this.refreshProductView();
- return;
- }
- this.productSortKey = key;
- this.productSortDir = key === 'rating' ? 'asc' : 'desc';
- this.productCurrentPage = 1;
- this.refreshProductView();
- }
- getProductSortIndicator(key: DesignerProductSortKey): string {
- if (this.productSortKey !== key) return '';
- return this.productSortDir === 'desc' ? '↓' : '↑';
- }
- getStarDistributionDesc(distribution: DesignerProductDrilldownItem['starDistribution']): DesignerProductDrilldownItem['starDistribution'] {
- if (!distribution?.length) return [];
- const cached = this.starDistributionCache.get(distribution);
- if (cached) return cached;
- const sorted = [...distribution].sort((a, b) => b.star - a.star);
- this.starDistributionCache.set(distribution, sorted);
- return sorted;
- }
- getTotalStarCount(distribution: DesignerProductDrilldownItem['starDistribution']): number {
- if (!distribution?.length) return 0;
- const cached = this.starCountCache.get(distribution);
- if (cached !== undefined) return cached;
- const total = distribution.reduce((sum, item) => sum + item.count, 0);
- this.starCountCache.set(distribution, total);
- return total;
- }
- // ─── 页内 Tab 切换 ───
- activeTab: DesignerPerformanceTabKey = 'ranking';
- get pageTabs(): BoardTabItem[] {
- const critical = this.data?.summary.criticalRiskCount || 0;
- const key = `${this.dataRevision}|${critical}`;
- if (key === this.pageTabsCacheKey) return this.pageTabsCache;
- this.pageTabsCacheKey = key;
- this.pageTabsCache = [
- {
- key: 'ranking',
- label: '风险排行',
- icon: '📉',
- badges: critical ? [{ label: critical, tone: 'red' }] : undefined
- },
- { key: 'trend', label: '趋势图', icon: '📈' },
- { key: 'issues', label: '问题分布', icon: '📊' },
- { key: 'ai', label: 'AI分析', icon: '🤖' }
- ];
- return this.pageTabsCache;
- }
- setActiveTab(key: string): void {
- this.activeTab = key as DesignerPerformanceTabKey;
- }
- openRiskAlert(): void {
- this.router.navigate(['/monitoring/risk-alert']);
- }
- openReturnAnalysis(): void {
- this.router.navigate(['/return-analysis/overview']);
- }
- getHealthBarWidth(score: number): number {
- return Math.max(4, Math.min(100, score));
- }
- getRiskLevelLabel(level: 'high' | 'medium' | 'low'): string {
- const map = { high: '高风险', medium: '中风险', low: '低风险' };
- return map[level];
- }
- getIssueTaxonomyConfidenceLabel(confidence?: 'high' | 'medium' | 'low'): string {
- const map = { high: '高置信', medium: '中置信', low: '低置信' };
- return confidence ? map[confidence] : '未识别';
- }
- getProductRiskLabel(level: 'critical' | 'warning' | 'normal'): string {
- const map = { critical: '严重', warning: '预警', normal: '正常' };
- return map[level];
- }
- trackByDesigner(_: number, item: DesignerPerformanceRankingItem | DesignerIssueMatrixRow): string {
- return item.designerName;
- }
- trackByProduct(_: number, item: DesignerProductDrilldownItem): string {
- return item.asin;
- }
- private refreshProductView(): void {
- const products = this.getFilteredProducts();
- this.filteredProducts = products;
- this.productTotalPages = Math.max(1, Math.ceil(products.length / this.productPageSize));
- this.productDisplayPage = Math.max(1, Math.min(this.productCurrentPage, this.productTotalPages));
- if (this.productCurrentPage !== this.productDisplayPage) {
- this.productCurrentPage = this.productDisplayPage;
- }
- const start = (this.productDisplayPage - 1) * this.productPageSize;
- this.visibleProducts = products.slice(start, start + this.productPageSize);
- this.productPageStart = products.length ? start + 1 : 0;
- this.productPageEnd = Math.min(start + this.productPageSize, products.length);
- }
- private sortProducts(products: DesignerProductDrilldownItem[]): DesignerProductDrilldownItem[] {
- const factor = this.productSortDir === 'desc' ? -1 : 1;
- const riskScore = { critical: 3, warning: 2, normal: 1 };
- const valueOf = (product: DesignerProductDrilldownItem): number => {
- if (this.productSortKey === 'rating') return Number(product.rating || 0);
- if (this.productSortKey === 'negative') return Number(product.negativeRate || 0);
- if (this.productSortKey === 'return') return Number(product.returnRate || 0);
- return riskScore[product.riskLevel] || 0;
- };
- return [...products].sort((a, b) => {
- const diff = valueOf(a) - valueOf(b);
- if (diff !== 0) return diff * factor;
- return b.negativeRate - a.negativeRate;
- });
- }
- private normalizeFilters(filters: FilterOptions): DesignerPerformanceFilters {
- return {
- timeRange: filters.timeRange || '30days',
- category: filters.category,
- sku: filters.sku,
- store: filters.store,
- storeNames: filters.storeNames,
- site: filters.site,
- designerName: this.cleanDesignerFilterValues(filters.designerName),
- operatorName: filters.operatorName,
- developerName: filters.developerName,
- productSku: filters.productSku,
- sellerSku: filters.sellerSku,
- itemStatus: filters.itemStatus,
- fulfillmentType: filters.fulfillmentType,
- customDateRange: filters.customDateRange
- };
- }
- private getFilterKey(filters: DesignerPerformanceFilters): string {
- const list = (values?: Array<string | number>) => (values || [])
- .map(value => String(value || '').trim())
- .filter(Boolean)
- .sort()
- .join('|');
- const range = filters.customDateRange;
- return [
- filters.timeRange || '30days',
- range?.start || '',
- range?.end || '',
- list(filters.category),
- list(filters.sku),
- list(filters.store),
- list(filters.storeNames),
- list(filters.site),
- list(filters.designerName),
- list(filters.operatorName),
- list(filters.developerName),
- list(filters.productSku),
- list(filters.sellerSku),
- list(filters.itemStatus),
- list(filters.fulfillmentType)
- ].join('::');
- }
- private hasStoreScope(filters: DesignerPerformanceFilters): boolean {
- return !!(filters.store?.length || filters.storeNames?.length);
- }
- private buildDesignerFilterOptions(): string[] {
- const key = `${this.dataRevision}|${this.dimensionDesignerOptions.join('|')}`;
- if (key === this.designerFilterOptionsCacheKey) return this.designerFilterOptionsCache;
- this.designerFilterOptionsCacheKey = key;
- const scopedDesignerOptions = [
- ...this.rankings.map(item => item.designerName),
- ...(this.data?.products || []).map(item => item.designerName),
- ...(this.data?.returnRanking || []).map(item => item.designerName)
- ].filter(name => this.isValidDesignerName(name));
- const fallbackDesignerOptions = scopedDesignerOptions.length ? [] : this.dimensionDesignerOptions;
- this.designerFilterOptionsCache = Array.from(new Set([
- ...scopedDesignerOptions,
- ...fallbackDesignerOptions,
- ...(this.currentFilters.designerName || [])
- ].filter(name => this.isValidDesignerName(name))))
- .sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
- return this.designerFilterOptionsCache;
- }
- private loadDesignerOptions(): void {
- this.designerOptionsSub?.unsubscribe();
- this.designerOptionsSub = this.productDimensionService.getDesignerOptions().subscribe(options => {
- this.dimensionDesignerOptions = options.filter(name => this.isValidDesignerName(name));
- this.designerFilterOptionsCacheKey = '';
- this.designerFilterOptions = this.buildDesignerFilterOptions();
- });
- }
- private cleanDesignerFilterValues(values?: string[]): string[] | undefined {
- if (!values?.length) return values;
- const cleaned = values.map(value => String(value || '').trim()).filter(value => this.isValidDesignerName(value));
- return cleaned.length ? cleaned : undefined;
- }
- private isValidDesignerName(value: any): boolean {
- const text = String(value || '').trim();
- if (!text) return false;
- const normalized = text.toLowerCase();
- return !['-', '—', '–', '未分配', '未分配设计师', '未归属', '未归属设计师', 'unknown', 'null', 'undefined', 'n/a', 'na'].includes(normalized);
- }
- }
|