Browse Source

feat: expand domestic ecommerce analytics template

gangvy 2 tháng trước cách đây
mục cha
commit
43f84b5772

+ 12 - 7
README.md

@@ -14,6 +14,10 @@
 
 
 当前启用路由:
 当前启用路由:
 
 
+- `/domestic/overview`:国内经营概览、核心指标、经营趋势、类目贡献和成交商品
+- `/domestic/product`:单商品经营详情、流量转化、退款表现和商品档案
+- `/domestic/categories`:二级/三级类目经营对比
+- `/domestic/competitors`:竞品关系、映射覆盖率、搜索和本品跳转
 - `/voc-insight/products`:德玛仕商品列表、搜索、排序和商品选择
 - `/voc-insight/products`:德玛仕商品列表、搜索、排序和商品选择
 - `/voc-insight/voice`:用户声音分类;无评论时展示真实空状态
 - `/voc-insight/voice`:用户声音分类;无评论时展示真实空状态
 - `/voc-insight/pain-point`:痛点深度洞察
 - `/voc-insight/pain-point`:痛点深度洞察
@@ -29,10 +33,11 @@
 
 
 1. 加载德玛仕 Excel 的规范化结果。
 1. 加载德玛仕 Excel 的规范化结果。
 2. 展示 2,817 个有经营数据的商品。
 2. 展示 2,817 个有经营数据的商品。
-3. 按商品 ID、型号或名称搜索并选择商品。
-4. 在原 VOC 页面中保持商品上下文。
-5. 对当前真实的 0 条评论展示空状态,不伪造情绪、痛点或分析结果。
-6. 查看竞品关系、导入统计和数据质量问题。
+3. 查看经营概览、商品经营详情、二级/三级类目分析和竞品关系。
+4. 按商品 ID、型号或名称搜索并选择商品,先进入经营详情,再进入 VOC 洞察。
+5. 在原 VOC 页面中保持商品上下文。
+6. 对当前真实的 0 条评论展示空状态,不伪造情绪、痛点或分析结果。
+7. 查看导入统计和数据质量问题。
 
 
 德玛仕数据摘要:
 德玛仕数据摘要:
 
 
@@ -112,10 +117,10 @@ npm run import:demashi -- "E:\xwechat_files\wxid_ay08t6ugo2h922_df4f\msg\file\20
 ## 当前验证
 ## 当前验证
 
 
 - `npm run build`:通过
 - `npm run build`:通过
-- `npm test`:3 项 Jasmine 测试通过
+- `npm test`:5 项 Jasmine 测试通过
 - `npm run verify:data`:通过
 - `npm run verify:data`:通过
-- 桌面端:商品搜索、商品选择、VOC 路由和空状态通过
-- 移动端:App Shell 抽屉、VOC rail 和数据状态页通过
+- 桌面端:经营概览、品详情、品类切换、竞品搜索、商品选择、VOC 路由和空状态通过
+- 移动端:分组导航抽屉、导航后自动收起、经营概览、品类页和竞品页通过
 - 运行时请求:当前激活闭环只请求同源德玛仕 JSON,没有访问旧 Parse 或旧跨境域名
 - 运行时请求:当前激活闭环只请求同源德玛仕 JSON,没有访问旧 Parse 或旧跨境域名
 - `npm audit --omit=dev`:17 项原框架依赖公告(1 critical、11 high、5 moderate),正式公网部署前必须升级处理
 - `npm audit --omit=dev`:17 项原框架依赖公告(1 critical、11 high、5 moderate),正式公网部署前必须升级处理
 
 

+ 3 - 0
TASKS.md

@@ -12,6 +12,9 @@
 - [x] 实现德玛仕 Excel 规范化脚本和数据校验脚本
 - [x] 实现德玛仕 Excel 规范化脚本和数据校验脚本
 - [x] 接入 2,817 个真实商品、9,717 条日指标和 40 条竞品关系
 - [x] 接入 2,817 个真实商品、9,717 条日指标和 40 条竞品关系
 - [x] 接通商品搜索、商品选择、VOC 页面商品上下文和真实零评论空状态
 - [x] 接通商品搜索、商品选择、VOC 页面商品上下文和真实零评论空状态
+- [x] 迁入经营概览、商品经营详情、品类分析和竞品关系页面
+- [x] 将左侧导航整理为“经营分析”“VOC 深度洞察”和“数据接入状态”三层结构
+- [x] 接通商品列表到经营详情、经营详情到 VOC 的真实商品上下文
 - [x] 增加数据接入状态页
 - [x] 增加数据接入状态页
 - [x] 将 Parse 和电商中转默认地址改为同源路径
 - [x] 将 Parse 和电商中转默认地址改为同源路径
 - [x] 移除旧跨境线上域名和旧 Parse 二次登录逻辑
 - [x] 移除旧跨境线上域名和旧 Parse 二次登录逻辑

+ 3 - 0
src/app/app.component.ts

@@ -44,6 +44,9 @@ export class AppComponent {
     ).subscribe((event: any) => {
     ).subscribe((event: any) => {
       // Hide navigation on login page
       // Hide navigation on login page
       this.showNavigation = !event.url.includes('/login');
       this.showNavigation = !event.url.includes('/login');
+      window.setTimeout(() => {
+        document.querySelector<HTMLElement>('.main-content')?.scrollTo({ top: 0, left: 0 });
+      });
     });
     });
     this.layoutService.isCollapsed$.subscribe(c => this.sidebarCollapsed = c);
     this.layoutService.isCollapsed$.subscribe(c => this.sidebarCollapsed = c);
   }
   }

+ 28 - 2
src/app/app.routes.ts

@@ -1,11 +1,37 @@
 import { Routes } from '@angular/router';
 import { Routes } from '@angular/router';
 
 
 export const routes: Routes = [
 export const routes: Routes = [
-  { path: '', redirectTo: '/voc-insight/products', pathMatch: 'full' },
+  { path: '', redirectTo: '/domestic/overview', pathMatch: 'full' },
   {
   {
     path: 'login',
     path: 'login',
     loadComponent: () => import('../modules/login/login.component').then((m) => m.LoginComponent),
     loadComponent: () => import('../modules/login/login.component').then((m) => m.LoginComponent),
   },
   },
+  {
+    path: 'domestic',
+    children: [
+      { path: '', redirectTo: 'overview', pathMatch: 'full' },
+      {
+        path: 'overview',
+        title: '经营概览',
+        loadComponent: () => import('../modules/domestic-voc/operating-overview.component').then((m) => m.DomesticOperatingOverviewComponent),
+      },
+      {
+        path: 'product',
+        title: '商品经营详情',
+        loadComponent: () => import('../modules/domestic-voc/product-operating-detail.component').then((m) => m.DomesticProductOperatingDetailComponent),
+      },
+      {
+        path: 'categories',
+        title: '品类分析',
+        loadComponent: () => import('../modules/domestic-voc/category-analysis.component').then((m) => m.DomesticCategoryAnalysisComponent),
+      },
+      {
+        path: 'competitors',
+        title: '竞品关系',
+        loadComponent: () => import('../modules/domestic-voc/competitor-relations.component').then((m) => m.DomesticCompetitorRelationsComponent),
+      },
+    ],
+  },
   {
   {
     path: 'voc-insight',
     path: 'voc-insight',
     loadComponent: () => import('../modules/voc-insight/voc-insight-shell.component').then((m) => m.VocInsightShellComponent),
     loadComponent: () => import('../modules/voc-insight/voc-insight-shell.component').then((m) => m.VocInsightShellComponent),
@@ -43,5 +69,5 @@ export const routes: Routes = [
     title: '数据接入状态',
     title: '数据接入状态',
     loadComponent: () => import('../modules/domestic-voc/data-status.component').then((m) => m.DomesticDataStatusComponent),
     loadComponent: () => import('../modules/domestic-voc/data-status.component').then((m) => m.DomesticDataStatusComponent),
   },
   },
-  { path: '**', redirectTo: '/voc-insight/products' },
+  { path: '**', redirectTo: '/domestic/overview' },
 ];
 ];

+ 26 - 0
src/app/core/models/domestic.models.ts

@@ -20,6 +20,10 @@ export interface DomesticMetricSummary {
   refundToGmvRate: number;
   refundToGmvRate: number;
 }
 }
 
 
+export interface DomesticDailyMetric extends DomesticMetricSummary {
+  date: string;
+}
+
 export interface DomesticProduct {
 export interface DomesticProduct {
   platform: string;
   platform: string;
   productId: string;
   productId: string;
@@ -46,6 +50,24 @@ export interface DomesticReview {
   reviewDate?: string;
   reviewDate?: string;
 }
 }
 
 
+export interface DomesticProductRelation {
+  relationKey: string;
+  ownProductKey: string;
+  ownProductId: string;
+  competitorProductKey: string;
+  competitorProductId: string;
+  competitorBrand: string;
+  category: string;
+}
+
+export interface DomesticMappingGroup {
+  ownProductId: string;
+  ownProductKey: string;
+  model: string;
+  category: string;
+  competitors: DomesticProductRelation[];
+}
+
 export interface DomesticQuality {
 export interface DomesticQuality {
   orphanMappings: Array<{ ownProductId: string; model: string; category: string }>;
   orphanMappings: Array<{ ownProductId: string; model: string; category: string }>;
   mappingsWithoutCompetitor: Array<{ ownProductId: string; model: string; category: string }>;
   mappingsWithoutCompetitor: Array<{ ownProductId: string; model: string; category: string }>;
@@ -60,6 +82,7 @@ export interface DomesticDataset {
   source: {
   source: {
     sourceFile: string;
     sourceFile: string;
     sourceHash: string;
     sourceHash: string;
+    sheets?: string[];
     dateRange: { start: string; end: string };
     dateRange: { start: string; end: string };
   };
   };
   summary: {
   summary: {
@@ -72,7 +95,10 @@ export interface DomesticDataset {
     category3Count: number;
     category3Count: number;
     reviewCount: number;
     reviewCount: number;
   };
   };
+  dailyTotals: DomesticDailyMetric[];
   products: DomesticProduct[];
   products: DomesticProduct[];
+  mappingGroups: DomesticMappingGroup[];
+  relations: DomesticProductRelation[];
   reviews?: DomesticReview[];
   reviews?: DomesticReview[];
   quality: DomesticQuality;
   quality: DomesticQuality;
 }
 }

+ 76 - 0
src/app/core/services/domestic-analytics.service.spec.ts

@@ -0,0 +1,76 @@
+import { aggregateDomesticMetrics, buildDomesticCategorySummaries } from './domestic-analytics.service';
+import { DomesticMetricSummary, DomesticProduct } from '../models/domestic.models';
+
+describe('domestic analytics selectors', () => {
+  it('aggregates additive metrics and recalculates weighted rates', () => {
+    const result = aggregateDomesticMetrics([
+      metric({ gmv: 100, soldUnits: 2, visitors: 10, transactionOrders: 2, impressions: 20, clicks: 4, refundAmount: 10 }),
+      metric({ gmv: 300, soldUnits: 3, visitors: 30, transactionOrders: 3, impressions: 80, clicks: 8, refundAmount: 20 }),
+    ]);
+
+    expect(result.gmv).toBe(400);
+    expect(result.averageUnitPrice).toBe(80);
+    expect(result.conversionRate).toBeCloseTo(0.125, 6);
+    expect(result.clickThroughRate).toBeCloseTo(0.12, 6);
+    expect(result.refundToGmvRate).toBeCloseTo(0.075, 6);
+  });
+
+  it('groups products by the selected domestic category dimension', () => {
+    const products = [
+      product('A', '加热设备', 100),
+      product('B', '加热设备', 300),
+      product('C', '制冷设备', 200),
+    ];
+
+    const rows = buildDomesticCategorySummaries(products, 'category2');
+
+    expect(rows.map((row) => row.name)).toEqual(['加热设备', '制冷设备']);
+    expect(rows[0].productCount).toBe(2);
+    expect(rows[0].gmv).toBe(400);
+  });
+});
+
+function metric(values: Partial<DomesticMetricSummary> = {}): DomesticMetricSummary {
+  return {
+    gmv: 0,
+    soldUnits: 0,
+    transactionOrders: 0,
+    transactionCustomers: 0,
+    impressions: 0,
+    clicks: 0,
+    views: 0,
+    visitors: 0,
+    cartUnits: 0,
+    orderAmount: 0,
+    orderUnits: 0,
+    orderCount: 0,
+    refundAmount: 0,
+    refundUnits: 0,
+    refundOrders: 0,
+    conversionRate: 0,
+    clickThroughRate: 0,
+    averageUnitPrice: 0,
+    refundToGmvRate: 0,
+    ...values,
+  };
+}
+
+function product(id: string, category2: string, gmv: number): DomesticProduct {
+  return {
+    platform: 'jd',
+    productId: id,
+    productKey: `jd:${id}`,
+    asin: id,
+    role: 'own',
+    brand: '德玛仕',
+    title: id,
+    model: id,
+    category1: '商用电器',
+    category2,
+    category3: category2,
+    source: 'test',
+    relationCount: 0,
+    summary: metric({ gmv }),
+    trend: [],
+  };
+}

+ 190 - 0
src/app/core/services/domestic-analytics.service.ts

@@ -0,0 +1,190 @@
+import { Injectable, inject } from '@angular/core';
+import { map, shareReplay } from 'rxjs';
+import {
+  DomesticDataset,
+  DomesticMetricSummary,
+  DomesticProduct,
+  DomesticProductRelation,
+} from '../models/domestic.models';
+import { DomesticDatasetService } from './domestic-dataset.service';
+
+export interface DomesticCategorySummary {
+  name: string;
+  productCount: number;
+  gmv: number;
+  soldUnits: number;
+  transactionOrders: number;
+  visitors: number;
+  refundAmount: number;
+  relationCount: number;
+  conversionRate: number;
+  refundToGmvRate: number;
+}
+
+export interface DomesticCompetitorRelationRow extends DomesticProductRelation {
+  ownProduct?: DomesticProduct;
+  ownModel: string;
+  ownTitle: string;
+  ownProductAvailable: boolean;
+}
+
+export interface DomesticOverview {
+  dataset: DomesticDataset;
+  totals: DomesticMetricSummary;
+  gmvTrend: number;
+  topProducts: DomesticProduct[];
+  topCategories: DomesticCategorySummary[];
+}
+
+export interface DomesticCategoryAnalysis {
+  category2: DomesticCategorySummary[];
+  category3: DomesticCategorySummary[];
+  relationCount: number;
+}
+
+export function aggregateDomesticMetrics(rows: DomesticMetricSummary[]): DomesticMetricSummary {
+  const totals = rows.reduce((sum, row) => ({
+    gmv: sum.gmv + Number(row.gmv || 0),
+    soldUnits: sum.soldUnits + Number(row.soldUnits || 0),
+    transactionOrders: sum.transactionOrders + Number(row.transactionOrders || 0),
+    transactionCustomers: sum.transactionCustomers + Number(row.transactionCustomers || 0),
+    impressions: sum.impressions + Number(row.impressions || 0),
+    clicks: sum.clicks + Number(row.clicks || 0),
+    views: sum.views + Number(row.views || 0),
+    visitors: sum.visitors + Number(row.visitors || 0),
+    cartUnits: sum.cartUnits + Number(row.cartUnits || 0),
+    orderAmount: sum.orderAmount + Number(row.orderAmount || 0),
+    orderUnits: sum.orderUnits + Number(row.orderUnits || 0),
+    orderCount: sum.orderCount + Number(row.orderCount || 0),
+    refundAmount: sum.refundAmount + Number(row.refundAmount || 0),
+    refundUnits: sum.refundUnits + Number(row.refundUnits || 0),
+    refundOrders: sum.refundOrders + Number(row.refundOrders || 0),
+    conversionRate: 0,
+    clickThroughRate: 0,
+    averageUnitPrice: 0,
+    refundToGmvRate: 0,
+  }), emptyMetricSummary());
+
+  totals.conversionRate = divide(totals.transactionOrders, totals.visitors);
+  totals.clickThroughRate = divide(totals.clicks, totals.impressions);
+  totals.averageUnitPrice = divide(totals.gmv, totals.soldUnits);
+  totals.refundToGmvRate = divide(totals.refundAmount, totals.gmv);
+  return totals;
+}
+
+export function buildDomesticCategorySummaries(
+  products: DomesticProduct[],
+  dimension: 'category2' | 'category3' = 'category2',
+): DomesticCategorySummary[] {
+  const groups = new Map<string, DomesticProduct[]>();
+  products.forEach((product) => {
+    const name = product[dimension] || product.category1 || '未分类';
+    groups.set(name, [...(groups.get(name) ?? []), product]);
+  });
+
+  return Array.from(groups.entries())
+    .map(([name, group]) => {
+      const metrics = aggregateDomesticMetrics(group.map((product) => product.summary));
+      return {
+        name,
+        productCount: group.length,
+        gmv: metrics.gmv,
+        soldUnits: metrics.soldUnits,
+        transactionOrders: metrics.transactionOrders,
+        visitors: metrics.visitors,
+        refundAmount: metrics.refundAmount,
+        relationCount: group.reduce((sum, product) => sum + Number(product.relationCount || 0), 0),
+        conversionRate: metrics.conversionRate,
+        refundToGmvRate: metrics.refundToGmvRate,
+      };
+    })
+    .sort((a, b) => b.gmv - a.gmv || b.soldUnits - a.soldUnits || a.name.localeCompare(b.name, 'zh-CN'));
+}
+
+@Injectable({ providedIn: 'root' })
+export class DomesticAnalyticsService {
+  private readonly dataset = inject(DomesticDatasetService);
+
+  readonly overview$ = this.dataset.dataset$.pipe(
+    map((dataset): DomesticOverview => {
+      const first = dataset.dailyTotals[0]?.gmv || 0;
+      const last = dataset.dailyTotals.at(-1)?.gmv || 0;
+      return {
+        dataset,
+        totals: aggregateDomesticMetrics(dataset.dailyTotals),
+        gmvTrend: first ? (last - first) / first : 0,
+        topProducts: [...dataset.products]
+          .sort((a, b) => b.summary.gmv - a.summary.gmv || b.summary.soldUnits - a.summary.soldUnits)
+          .slice(0, 10),
+        topCategories: buildDomesticCategorySummaries(dataset.products, 'category2').slice(0, 8),
+      };
+    }),
+    shareReplay({ bufferSize: 1, refCount: false }),
+  );
+
+  readonly categoryAnalysis$ = this.dataset.dataset$.pipe(
+    map((dataset): DomesticCategoryAnalysis => ({
+      category2: buildDomesticCategorySummaries(dataset.products, 'category2'),
+      category3: buildDomesticCategorySummaries(dataset.products, 'category3'),
+      relationCount: dataset.relations.length,
+    })),
+    shareReplay({ bufferSize: 1, refCount: false }),
+  );
+
+  readonly category2$ = this.categoryAnalysis$.pipe(
+    map((analysis) => analysis.category2),
+    shareReplay({ bufferSize: 1, refCount: false }),
+  );
+
+  readonly category3$ = this.categoryAnalysis$.pipe(
+    map((analysis) => analysis.category3),
+    shareReplay({ bufferSize: 1, refCount: false }),
+  );
+
+  readonly competitorRelations$ = this.dataset.dataset$.pipe(
+    map((dataset): DomesticCompetitorRelationRow[] => {
+      const products = new Map(dataset.products.map((product) => [product.productId, product]));
+      const mappingGroups = new Map(dataset.mappingGroups.map((group) => [group.ownProductId, group]));
+      return dataset.relations.map((relation) => {
+        const ownProduct = products.get(relation.ownProductId);
+        const mapping = mappingGroups.get(relation.ownProductId);
+        return {
+          ...relation,
+          ownProduct,
+          ownModel: ownProduct?.model || mapping?.model || relation.ownProductId,
+          ownTitle: ownProduct?.title || mapping?.model || relation.ownProductId,
+          ownProductAvailable: !!ownProduct,
+        };
+      });
+    }),
+    shareReplay({ bufferSize: 1, refCount: false }),
+  );
+}
+
+function emptyMetricSummary(): DomesticMetricSummary {
+  return {
+    gmv: 0,
+    soldUnits: 0,
+    transactionOrders: 0,
+    transactionCustomers: 0,
+    impressions: 0,
+    clicks: 0,
+    views: 0,
+    visitors: 0,
+    cartUnits: 0,
+    orderAmount: 0,
+    orderUnits: 0,
+    orderCount: 0,
+    refundAmount: 0,
+    refundUnits: 0,
+    refundOrders: 0,
+    conversionRate: 0,
+    clickThroughRate: 0,
+    averageUnitPrice: 0,
+    refundToGmvRate: 0,
+  };
+}
+
+function divide(value: number, total: number): number {
+  return total > 0 ? value / total : 0;
+}

+ 130 - 0
src/modules/domestic-voc/category-analysis.component.ts

@@ -0,0 +1,130 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
+import { NgxEchartsModule } from 'ngx-echarts';
+import type { EChartsOption } from 'echarts';
+import { Subject, takeUntil } from 'rxjs';
+import { DomesticAnalyticsService, DomesticCategorySummary } from '../../app/core/services/domestic-analytics.service';
+import { ContentCardComponent } from '../shared/components/content-card/content-card.component';
+import { DataTableShellComponent } from '../shared/components/data-table-shell/data-table-shell.component';
+import { PageHeaderComponent } from '../shared/components/page-header/page-header.component';
+import { SummaryMetricCardComponent } from '../shared/components/summary-metric-card/summary-metric-card.component';
+
+@Component({
+  selector: 'app-domestic-category-analysis',
+  standalone: true,
+  imports: [CommonModule, NgxEchartsModule, PageHeaderComponent, SummaryMetricCardComponent, ContentCardComponent, DataTableShellComponent],
+  template: `
+    <section class="domestic-page">
+      <app-page-header title="品类分析" eyebrow="Category Performance" description="按国内类目层级比较商品规模、成交贡献、转化效率和退款表现。" icon="◇"></app-page-header>
+
+      <div class="segmented-control" role="group" aria-label="类目层级">
+        <button type="button" [class.active]="dimension === 'category2'" (click)="setDimension('category2')">二级类目</button>
+        <button type="button" [class.active]="dimension === 'category3'" (click)="setDimension('category3')">三级类目</button>
+      </div>
+
+      <div class="metric-grid">
+        <app-summary-metric-card label="类目数量" [value]="number(rows.length)" icon="◇" tone="blue" [description]="dimension === 'category2' ? '二级类目' : '三级类目'"></app-summary-metric-card>
+        <app-summary-metric-card label="覆盖商品" [value]="number(totalProducts)" icon="□" tone="green"></app-summary-metric-card>
+        <app-summary-metric-card label="成交金额" [value]="currency(totalGmv)" icon="¥" tone="purple"></app-summary-metric-card>
+        <app-summary-metric-card label="竞品关系归类" [value]="number(totalRelations) + ' / ' + number(allRelations)" icon="⇄" tone="amber" description="可归类 / 全部关系"></app-summary-metric-card>
+      </div>
+
+      <app-content-card title="类目成交对比" [subtitle]="'按' + (dimension === 'category2' ? '二级' : '三级') + '类目展示成交金额前 12 名'" tone="blue">
+        @if (chartOptions; as options) {
+          <div echarts class="chart" [options]="options"></div>
+        }
+      </app-content-card>
+
+      <app-data-table-shell title="类目经营明细" [subtitle]="dimension === 'category2' ? '二级类目汇总' : '三级类目汇总'" [count]="rows.length" variant="analytics" [scrollable]="true">
+        <table class="analytics-table">
+          <thead><tr><th>类目</th><th class="numeric">商品数</th><th class="numeric">成交金额</th><th class="numeric">成交件数</th><th class="numeric">订单数</th><th class="numeric">转化率</th><th class="numeric">退款占比</th><th class="numeric">竞品关系</th></tr></thead>
+          <tbody>
+            @for (row of rows; track row.name) {
+              <tr>
+                <td><span class="row-title">{{ row.name }}</span></td>
+                <td class="numeric">{{ number(row.productCount) }}</td>
+                <td class="numeric">{{ currency(row.gmv) }}</td>
+                <td class="numeric">{{ number(row.soldUnits) }}</td>
+                <td class="numeric">{{ number(row.transactionOrders) }}</td>
+                <td class="numeric">{{ percent(row.conversionRate) }}</td>
+                <td class="numeric">{{ percent(row.refundToGmvRate) }}</td>
+                <td class="numeric">{{ number(row.relationCount) }}</td>
+              </tr>
+            }
+          </tbody>
+        </table>
+      </app-data-table-shell>
+    </section>
+  `,
+  styleUrls: ['./domestic-page.shared.scss'],
+})
+export class DomesticCategoryAnalysisComponent implements OnInit, OnDestroy {
+  private readonly analytics = inject(DomesticAnalyticsService);
+  private readonly cdr = inject(ChangeDetectorRef);
+  private readonly destroy$ = new Subject<void>();
+  private category2Rows: DomesticCategorySummary[] = [];
+  private category3Rows: DomesticCategorySummary[] = [];
+
+  dimension: 'category2' | 'category3' = 'category2';
+  rows: DomesticCategorySummary[] = [];
+  totalProducts = 0;
+  totalGmv = 0;
+  totalRelations = 0;
+  allRelations = 0;
+  chartOptions?: EChartsOption;
+
+  ngOnInit(): void {
+    this.analytics.categoryAnalysis$
+      .pipe(takeUntil(this.destroy$))
+      .subscribe((analysis) => {
+        this.category2Rows = analysis.category2;
+        this.category3Rows = analysis.category3;
+        this.allRelations = analysis.relationCount;
+        this.refreshRows();
+      });
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  setDimension(dimension: 'category2' | 'category3'): void {
+    if (this.dimension === dimension) return;
+    this.dimension = dimension;
+    this.refreshRows();
+  }
+
+  currency(value: number): string {
+    return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', maximumFractionDigits: 0 }).format(value || 0);
+  }
+
+  number(value: number): string {
+    return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }).format(value || 0);
+  }
+
+  percent(value: number): string {
+    return `${((value || 0) * 100).toFixed(1)}%`;
+  }
+
+  private refreshRows(): void {
+    this.rows = this.dimension === 'category2' ? this.category2Rows : this.category3Rows;
+    this.totalProducts = this.rows.reduce((sum, row) => sum + row.productCount, 0);
+    this.totalGmv = this.rows.reduce((sum, row) => sum + row.gmv, 0);
+    this.totalRelations = this.rows.reduce((sum, row) => sum + row.relationCount, 0);
+    this.chartOptions = this.buildChartOptions(this.rows.slice(0, 12));
+    this.cdr.markForCheck();
+  }
+
+  private buildChartOptions(rows: DomesticCategorySummary[]): EChartsOption {
+    const ordered = [...rows].reverse();
+    return {
+      color: ['#4f6bed'],
+      tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+      grid: { left: 170, right: 42, top: 20, bottom: 32 },
+      xAxis: { type: 'value', axisLabel: { formatter: (value: number) => `${Math.round(value / 1000)}k` }, splitLine: { lineStyle: { color: '#edf0f4' } } },
+      yAxis: { type: 'category', data: ordered.map((row) => row.name), axisTick: { show: false }, axisLabel: { width: 150, overflow: 'truncate' } },
+      series: [{ type: 'bar', data: ordered.map((row) => Math.round(row.gmv)), barMaxWidth: 20, itemStyle: { borderRadius: [0, 4, 4, 0] } }],
+    };
+  }
+}

+ 129 - 0
src/modules/domestic-voc/competitor-relations.component.ts

@@ -0,0 +1,129 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { Subject, combineLatest, takeUntil } from 'rxjs';
+import { DomesticDataset } from '../../app/core/models/domestic.models';
+import { DomesticAnalyticsService, DomesticCompetitorRelationRow } from '../../app/core/services/domestic-analytics.service';
+import { DomesticDatasetService } from '../../app/core/services/domestic-dataset.service';
+import { DataTableShellComponent } from '../shared/components/data-table-shell/data-table-shell.component';
+import { MsqSearchBoxComponent } from '../shared/components/msq-search-box/msq-search-box.component';
+import { PageHeaderComponent } from '../shared/components/page-header/page-header.component';
+import { SummaryMetricCardComponent } from '../shared/components/summary-metric-card/summary-metric-card.component';
+import { VocInsightStateService } from '../shared/services/voc-insight-state.service';
+
+@Component({
+  selector: 'app-domestic-competitor-relations',
+  standalone: true,
+  imports: [CommonModule, FormsModule, PageHeaderComponent, SummaryMetricCardComponent, DataTableShellComponent, MsqSearchBoxComponent],
+  template: `
+    <section class="domestic-page">
+      <app-page-header title="竞品关系" eyebrow="Competitor Mapping" description="管理德玛仕商品与国内平台竞品的映射,并明确区分已配置关系和待采集指标。" icon="⇄"></app-page-header>
+
+      @if (dataset) {
+        <div class="metric-grid">
+          <app-summary-metric-card label="竞品关系" [value]="number(dataset.summary.relations)" icon="⇄" tone="blue"></app-summary-metric-card>
+          <app-summary-metric-card label="映射商品" [value]="number(dataset.summary.mappingRows)" icon="□" tone="green"></app-summary-metric-card>
+          <app-summary-metric-card label="唯一竞品" [value]="number(dataset.summary.uniqueCompetitorProducts)" icon="◇" tone="purple"></app-summary-metric-card>
+          <app-summary-metric-card label="映射覆盖率" [value]="percent(mappingCoverage)" icon="◎" tone="amber" description="按配置商品计算"></app-summary-metric-card>
+        </div>
+
+        <app-data-table-shell title="竞品映射明细" subtitle="竞品经营和评论指标将在 JustOneAPI 接通后补齐" [count]="filteredRows.length" variant="admin" [scrollable]="true" [hasToolbar]="true" [empty]="filteredRows.length === 0" emptyTitle="没有匹配的竞品关系">
+          <div dataTableToolbar class="table-toolbar">
+            <msq-search-box [(ngModel)]="searchQuery" placeholder="搜索商品ID、型号、竞品品牌或类目" (valueChange)="applyFilter($event)"></msq-search-box>
+          </div>
+          <table class="analytics-table">
+            <thead><tr><th>本品</th><th>类目</th><th>竞品品牌</th><th>竞品商品ID</th><th>数据状态</th></tr></thead>
+            <tbody>
+              @for (row of filteredRows; track row.relationKey) {
+                <tr>
+                  <td>
+                    @if (row.ownProductAvailable) {
+                      <button class="row-link" type="button" (click)="openOwnProduct(row)">{{ row.ownModel }}</button>
+                    } @else {
+                      <span class="row-title">{{ row.ownModel }}</span>
+                    }
+                    <span class="row-meta">商品ID {{ row.ownProductId }}{{ row.ownProductAvailable ? '' : ' · 经营数据缺失' }}</span>
+                  </td>
+                  <td>{{ row.category }}</td>
+                  <td>{{ row.competitorBrand }}</td>
+                  <td>{{ row.competitorProductId }}</td>
+                  <td><span class="status-pill">待采集商品与评论</span></td>
+                </tr>
+              }
+            </tbody>
+          </table>
+        </app-data-table-shell>
+      }
+    </section>
+  `,
+  styleUrls: ['./domestic-page.shared.scss'],
+})
+export class DomesticCompetitorRelationsComponent implements OnInit, OnDestroy {
+  private readonly analytics = inject(DomesticAnalyticsService);
+  private readonly datasetService = inject(DomesticDatasetService);
+  private readonly state = inject(VocInsightStateService);
+  private readonly router = inject(Router);
+  private readonly cdr = inject(ChangeDetectorRef);
+  private readonly destroy$ = new Subject<void>();
+  private allRows: DomesticCompetitorRelationRow[] = [];
+
+  dataset?: DomesticDataset;
+  filteredRows: DomesticCompetitorRelationRow[] = [];
+  searchQuery = '';
+  mappingCoverage = 0;
+
+  ngOnInit(): void {
+    combineLatest([this.datasetService.dataset$, this.analytics.competitorRelations$])
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(([dataset, rows]) => {
+        this.dataset = dataset;
+        this.allRows = rows;
+        const mapped = new Set(rows.map((row) => row.ownProductId)).size;
+        this.mappingCoverage = dataset.summary.mappingRows ? mapped / dataset.summary.mappingRows : 0;
+        this.applyFilter(this.searchQuery);
+      });
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  applyFilter(value: string): void {
+    this.searchQuery = value || '';
+    const query = this.searchQuery.trim().toLowerCase();
+    this.filteredRows = !query ? [...this.allRows] : this.allRows.filter((row) => [
+      row.ownProductId,
+      row.ownModel,
+      row.ownTitle,
+      row.competitorProductId,
+      row.competitorBrand,
+      row.category,
+    ].join(' ').toLowerCase().includes(query));
+    this.cdr.markForCheck();
+  }
+
+  openOwnProduct(row: DomesticCompetitorRelationRow): void {
+    const product = row.ownProduct;
+    if (!product) return;
+    this.state.setProduct({
+      asin: product.productId,
+      title: product.title,
+      photo: 'assets/images/placeholder.svg',
+      productSku: product.model,
+      sellerSku: product.productId,
+      storeName: '京东',
+      reviewCount: 0,
+    });
+    this.router.navigate(['/domestic/product'], { queryParams: { productId: product.productId } });
+  }
+
+  number(value: number): string {
+    return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }).format(value || 0);
+  }
+
+  percent(value: number): string {
+    return `${((value || 0) * 100).toFixed(1)}%`;
+  }
+}

+ 344 - 0
src/modules/domestic-voc/domestic-page.shared.scss

@@ -0,0 +1,344 @@
+:host {
+  display: block;
+  min-width: 0;
+}
+
+.domestic-page {
+  display: flex;
+  min-height: 100%;
+  flex-direction: column;
+  gap: 20px;
+  padding: 24px 24px 84px;
+  background: #f7f8fc;
+}
+
+.metric-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.analysis-grid {
+  display: grid;
+  grid-template-columns: minmax(0, 1.6fr) minmax(300px, 0.8fr);
+  gap: 16px;
+  align-items: stretch;
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.7fr);
+  gap: 16px;
+}
+
+.chart {
+  width: 100%;
+  height: 310px;
+}
+
+.chart.compact {
+  height: 280px;
+}
+
+.page-command {
+  min-height: 36px;
+  padding: 0 14px;
+  border: 1px solid #dbe3ef;
+  border-radius: 8px;
+  background: #ffffff;
+  color: #334155;
+  font-size: 13px;
+  font-weight: 700;
+  cursor: pointer;
+  transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
+}
+
+.page-command:hover {
+  border-color: #94a3b8;
+  color: #111827;
+  box-shadow: 0 5px 14px rgba(15, 23, 42, 0.07);
+}
+
+.page-command.primary {
+  border-color: #111827;
+  background: #111827;
+  color: #ffffff;
+}
+
+.analytics-table {
+  width: 100%;
+  min-width: 760px;
+  border-collapse: collapse;
+}
+
+.analytics-table th,
+.analytics-table td {
+  height: 48px;
+  padding: 0 14px;
+  border-bottom: 1px solid #edf0f4;
+  text-align: left;
+  vertical-align: middle;
+}
+
+.analytics-table th {
+  color: #64748b;
+  background: #f8fafc;
+  font-size: 12px;
+  font-weight: 700;
+  white-space: nowrap;
+}
+
+.analytics-table td {
+  color: #334155;
+  font-size: 13px;
+}
+
+.analytics-table td.numeric,
+.analytics-table th.numeric {
+  text-align: right;
+  font-variant-numeric: tabular-nums;
+}
+
+.row-link {
+  max-width: 420px;
+  padding: 0;
+  border: 0;
+  background: transparent;
+  color: #1d4ed8;
+  font: inherit;
+  font-weight: 700;
+  text-align: left;
+  cursor: pointer;
+}
+
+.row-link:hover {
+  text-decoration: underline;
+}
+
+.row-title {
+  display: block;
+  max-width: 420px;
+  overflow: hidden;
+  color: #1f2937;
+  font-weight: 700;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.row-meta {
+  display: block;
+  margin-top: 3px;
+  color: #94a3b8;
+  font-size: 11px;
+}
+
+.category-list {
+  display: grid;
+  gap: 13px;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.category-row {
+  display: grid;
+  grid-template-columns: minmax(100px, 1fr) minmax(100px, 1.4fr) auto;
+  gap: 12px;
+  align-items: center;
+}
+
+.category-name {
+  overflow: hidden;
+  color: #334155;
+  font-size: 12px;
+  font-weight: 700;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.category-track {
+  height: 8px;
+  overflow: hidden;
+  border-radius: 4px;
+  background: #eef2f7;
+}
+
+.category-fill {
+  height: 100%;
+  border-radius: inherit;
+  background: #4f6bed;
+}
+
+.category-value {
+  color: #475569;
+  font-size: 12px;
+  font-variant-numeric: tabular-nums;
+}
+
+.product-picker-wrap {
+  min-width: 0;
+}
+
+.product-picker-wrap ::ng-deep app-voc-product-picker {
+  display: block;
+}
+
+.product-picker-wrap ::ng-deep app-voc-product-picker > .vpc-bar {
+  margin-bottom: 0;
+}
+
+.info-list,
+.funnel-list {
+  display: grid;
+  gap: 0;
+  margin: 0;
+}
+
+.info-list > div,
+.funnel-row {
+  display: grid;
+  grid-template-columns: 110px minmax(0, 1fr);
+  gap: 12px;
+  align-items: center;
+  min-height: 44px;
+  border-bottom: 1px solid #edf0f4;
+}
+
+.info-list > div:last-child,
+.funnel-row:last-child {
+  border-bottom: 0;
+}
+
+.info-list dt,
+.funnel-label {
+  color: #64748b;
+  font-size: 12px;
+}
+
+.info-list dd {
+  min-width: 0;
+  margin: 0;
+  overflow: hidden;
+  color: #1f2937;
+  font-size: 13px;
+  font-weight: 700;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.funnel-value {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.funnel-bar {
+  height: 8px;
+  min-width: 4px;
+  border-radius: 4px;
+  background: #38a169;
+}
+
+.funnel-number {
+  margin-left: auto;
+  color: #334155;
+  font-size: 12px;
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+}
+
+.segmented-control {
+  display: inline-flex;
+  align-self: flex-start;
+  gap: 3px;
+  padding: 3px;
+  border: 1px solid #dbe3ef;
+  border-radius: 8px;
+  background: #eef2f7;
+}
+
+.segmented-control button {
+  min-height: 30px;
+  padding: 0 12px;
+  border: 0;
+  border-radius: 6px;
+  background: transparent;
+  color: #64748b;
+  font-size: 12px;
+  font-weight: 700;
+  cursor: pointer;
+}
+
+.segmented-control button.active {
+  background: #ffffff;
+  color: #1d4ed8;
+  box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08);
+}
+
+.table-toolbar {
+  width: min(380px, 100%);
+}
+
+.status-pill {
+  display: inline-flex;
+  align-items: center;
+  min-height: 24px;
+  padding: 0 8px;
+  border-radius: 6px;
+  background: #fef3c7;
+  color: #92400e;
+  font-size: 11px;
+  font-weight: 700;
+  white-space: nowrap;
+}
+
+.empty-note {
+  color: #94a3b8;
+  font-size: 12px;
+}
+
+@media (max-width: 1120px) {
+  .metric-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .analysis-grid,
+  .detail-grid {
+    grid-template-columns: 1fr;
+  }
+}
+
+@media (max-width: 720px) {
+  .domestic-page {
+    gap: 16px;
+    padding: 16px 14px 72px;
+  }
+
+  .metric-grid {
+    grid-template-columns: 1fr;
+    gap: 12px;
+  }
+
+  .chart,
+  .chart.compact {
+    height: 260px;
+  }
+
+  .category-row {
+    grid-template-columns: minmax(90px, 1fr) minmax(80px, 1fr);
+  }
+
+  .category-value {
+    grid-column: 1 / -1;
+    margin-top: -6px;
+    text-align: right;
+  }
+
+  .product-picker-wrap ::ng-deep .vpc-bar {
+    flex-wrap: wrap;
+  }
+
+  .product-picker-wrap ::ng-deep .vpc-current-identity {
+    flex: 1 1 100%;
+  }
+}

+ 147 - 0
src/modules/domestic-voc/operating-overview.component.ts

@@ -0,0 +1,147 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { Router } from '@angular/router';
+import { NgxEchartsModule } from 'ngx-echarts';
+import type { EChartsOption } from 'echarts';
+import { map } from 'rxjs';
+import { DomesticProduct } from '../../app/core/models/domestic.models';
+import { DomesticAnalyticsService } from '../../app/core/services/domestic-analytics.service';
+import { ContentCardComponent } from '../shared/components/content-card/content-card.component';
+import { DataTableShellComponent } from '../shared/components/data-table-shell/data-table-shell.component';
+import { PageHeaderComponent } from '../shared/components/page-header/page-header.component';
+import { SummaryMetricCardComponent } from '../shared/components/summary-metric-card/summary-metric-card.component';
+import { VocInsightStateService } from '../shared/services/voc-insight-state.service';
+
+@Component({
+  selector: 'app-domestic-operating-overview',
+  standalone: true,
+  imports: [CommonModule, NgxEchartsModule, PageHeaderComponent, SummaryMetricCardComponent, ContentCardComponent, DataTableShellComponent],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  template: `
+    <section class="domestic-page">
+      @if (vm$ | async; as vm) {
+        <app-page-header
+          title="经营概览"
+          eyebrow="Domestic Commerce"
+          description="汇总德玛仕京东经营数据,查看成交、流量、退款和核心商品表现。"
+          icon="▦"
+          [meta]="[
+            { label: '平台', value: '京东' },
+            { label: '周期', value: vm.dataset.source.dateRange.start + ' 至 ' + vm.dataset.source.dateRange.end }
+          ]">
+          <button pageHeaderActions class="page-command" type="button" (click)="goProducts()">商品列表 →</button>
+        </app-page-header>
+
+        <div class="metric-grid">
+          <app-summary-metric-card label="成交金额" [value]="currency(vm.totals.gmv)" icon="¥" tone="blue" [trendValue]="signedPercent(vm.gmvTrend)" trendLabel="首尾日变化" [trendDirection]="vm.gmvTrend > 0 ? 'up' : vm.gmvTrend < 0 ? 'down' : 'flat'"></app-summary-metric-card>
+          <app-summary-metric-card label="成交件数" [value]="number(vm.totals.soldUnits)" icon="□" tone="green" [description]="number(vm.totals.transactionOrders) + ' 笔成交订单'"></app-summary-metric-card>
+          <app-summary-metric-card label="访客转化率" [value]="percent(vm.totals.conversionRate)" icon="◎" tone="purple" [description]="number(vm.totals.visitors) + ' 位访客'"></app-summary-metric-card>
+          <app-summary-metric-card label="退款金额占比" [value]="percent(vm.totals.refundToGmvRate)" icon="↩" [tone]="vm.totals.refundToGmvRate > 0.12 ? 'red' : 'amber'" [description]="currency(vm.totals.refundAmount) + ' 退款金额'"></app-summary-metric-card>
+        </div>
+
+        <div class="analysis-grid">
+          <app-content-card title="7 日经营趋势" subtitle="成交金额与成交订单按日汇总" tone="blue">
+            <div echarts class="chart" [options]="vm.trendOptions"></div>
+          </app-content-card>
+          <app-content-card title="类目成交贡献" subtitle="按二级类目汇总成交金额" tone="green">
+            <ol class="category-list">
+              @for (row of vm.topCategories; track row.name) {
+                <li class="category-row">
+                  <span class="category-name" [title]="row.name">{{ row.name }}</span>
+                  <span class="category-track"><span class="category-fill" [style.width.%]="categoryWidth(row.gmv, vm.maxCategoryGmv)"></span></span>
+                  <span class="category-value">{{ currency(row.gmv) }}</span>
+                </li>
+              }
+            </ol>
+          </app-content-card>
+        </div>
+
+        <app-data-table-shell title="成交商品" subtitle="按周期成交金额排序,点击商品进入经营详情" [count]="vm.dataset.summary.metricProducts" variant="analytics" [scrollable]="true">
+          <table class="analytics-table">
+            <thead><tr><th>商品</th><th>类目</th><th class="numeric">成交金额</th><th class="numeric">成交件数</th><th class="numeric">转化率</th><th class="numeric">退款占比</th></tr></thead>
+            <tbody>
+              @for (product of vm.topProducts; track product.productKey) {
+                <tr>
+                  <td><button class="row-link" type="button" (click)="openProduct(product)">{{ product.title }}</button><span class="row-meta">{{ product.model }} · 商品ID {{ product.productId }}</span></td>
+                  <td>{{ product.category3 || product.category2 }}</td>
+                  <td class="numeric">{{ currency(product.summary.gmv) }}</td>
+                  <td class="numeric">{{ number(product.summary.soldUnits) }}</td>
+                  <td class="numeric">{{ percent(product.summary.conversionRate) }}</td>
+                  <td class="numeric">{{ percent(product.summary.refundToGmvRate) }}</td>
+                </tr>
+              }
+            </tbody>
+          </table>
+        </app-data-table-shell>
+      }
+    </section>
+  `,
+  styleUrls: ['./domestic-page.shared.scss'],
+})
+export class DomesticOperatingOverviewComponent {
+  private readonly analytics = inject(DomesticAnalyticsService);
+  private readonly router = inject(Router);
+  private readonly state = inject(VocInsightStateService);
+
+  readonly vm$ = this.analytics.overview$.pipe(map((overview) => ({
+    ...overview,
+    maxCategoryGmv: Math.max(...overview.topCategories.map((row) => row.gmv), 1),
+    trendOptions: this.buildTrendOptions(overview.dataset.dailyTotals),
+  })));
+
+  goProducts(): void {
+    this.router.navigate(['/voc-insight/products']);
+  }
+
+  openProduct(product: DomesticProduct): void {
+    this.state.setProduct({
+      asin: product.productId,
+      title: product.title,
+      photo: 'assets/images/placeholder.svg',
+      productSku: product.model,
+      sellerSku: product.productId,
+      storeName: '京东',
+      reviewCount: 0,
+    });
+    this.router.navigate(['/domestic/product'], { queryParams: { productId: product.productId } });
+  }
+
+  categoryWidth(value: number, max: number): number {
+    return Math.max(4, (value / max) * 100);
+  }
+
+  currency(value: number): string {
+    return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', maximumFractionDigits: 0 }).format(value || 0);
+  }
+
+  number(value: number): string {
+    return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }).format(value || 0);
+  }
+
+  percent(value: number): string {
+    return `${((value || 0) * 100).toFixed(1)}%`;
+  }
+
+  signedPercent(value: number): string {
+    const prefix = value > 0 ? '+' : '';
+    return `${prefix}${(value * 100).toFixed(1)}%`;
+  }
+
+  private buildTrendOptions(rows: Array<{ date: string; gmv: number; transactionOrders: number }>): EChartsOption {
+    return {
+      color: ['#4f6bed', '#38a169'],
+      tooltip: { trigger: 'axis' },
+      legend: { data: ['成交金额', '成交订单'], top: 0 },
+      grid: { left: 58, right: 52, top: 48, bottom: 34 },
+      xAxis: { type: 'category', data: rows.map((row) => row.date.slice(5)), axisTick: { show: false } },
+      yAxis: [
+        { type: 'value', name: '金额', axisLabel: { formatter: (value: number) => `${Math.round(value / 1000)}k` }, splitLine: { lineStyle: { color: '#edf0f4' } } },
+        { type: 'value', name: '订单', splitLine: { show: false } },
+      ],
+      series: [
+        { name: '成交金额', type: 'line', smooth: true, symbolSize: 7, data: rows.map((row) => Math.round(row.gmv)), areaStyle: { opacity: 0.08 } },
+        { name: '成交订单', type: 'bar', yAxisIndex: 1, barMaxWidth: 24, data: rows.map((row) => row.transactionOrders) },
+      ],
+    };
+  }
+}

+ 184 - 0
src/modules/domestic-voc/product-operating-detail.component.ts

@@ -0,0 +1,184 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { NgxEchartsModule } from 'ngx-echarts';
+import type { EChartsOption } from 'echarts';
+import { Subject, combineLatest, takeUntil } from 'rxjs';
+import { DomesticDataset, DomesticProduct } from '../../app/core/models/domestic.models';
+import { DomesticDatasetService } from '../../app/core/services/domestic-dataset.service';
+import { ContentCardComponent } from '../shared/components/content-card/content-card.component';
+import { PageHeaderComponent } from '../shared/components/page-header/page-header.component';
+import { SummaryMetricCardComponent } from '../shared/components/summary-metric-card/summary-metric-card.component';
+import { VocProductPickerComponent } from '../voc-insight/shared/components/voc-product-picker/voc-product-picker.component';
+import { VocInsightStateService } from '../shared/services/voc-insight-state.service';
+
+@Component({
+  selector: 'app-domestic-product-operating-detail',
+  standalone: true,
+  imports: [CommonModule, NgxEchartsModule, PageHeaderComponent, SummaryMetricCardComponent, ContentCardComponent, VocProductPickerComponent],
+  template: `
+    <section class="domestic-page">
+      <app-page-header
+        title="商品经营详情"
+        eyebrow="Product Performance"
+        description="查看单个商品在当前数据周期内的成交、流量、转化与退款表现。"
+        icon="□">
+        <button pageHeaderActions class="page-command" type="button" (click)="goCompetitors()">竞品关系</button>
+        <button pageHeaderActions class="page-command primary" type="button" (click)="goVoc()">查看 VOC →</button>
+      </app-page-header>
+
+      <div class="product-picker-wrap"><app-voc-product-picker></app-voc-product-picker></div>
+
+      @if (product) {
+        <div class="metric-grid">
+          <app-summary-metric-card label="成交金额" [value]="currency(product.summary.gmv)" icon="¥" tone="blue" [description]="number(product.summary.transactionOrders) + ' 笔成交订单'"></app-summary-metric-card>
+          <app-summary-metric-card label="成交件数" [value]="number(product.summary.soldUnits)" icon="□" tone="green" [description]="currency(product.summary.averageUnitPrice) + ' 客单价'"></app-summary-metric-card>
+          <app-summary-metric-card label="访客转化率" [value]="percent(product.summary.conversionRate)" icon="◎" tone="purple" [description]="number(product.summary.visitors) + ' 位访客'"></app-summary-metric-card>
+          <app-summary-metric-card label="退款金额占比" [value]="percent(product.summary.refundToGmvRate)" icon="↩" [tone]="product.summary.refundToGmvRate > 0.12 ? 'red' : 'amber'" [description]="currency(product.summary.refundAmount) + ' 退款金额'"></app-summary-metric-card>
+        </div>
+
+        <div class="detail-grid">
+          <app-content-card title="商品经营趋势" subtitle="每日成交金额、成交件数与订单数" tone="blue">
+            <div echarts class="chart" [options]="trendOptions"></div>
+          </app-content-card>
+          <app-content-card title="商品档案" subtitle="国内商品标准身份与竞品配置" tone="green">
+            <dl class="info-list">
+              <div><dt>商品ID</dt><dd>{{ product.productId }}</dd></div>
+              <div><dt>型号</dt><dd>{{ product.model || '-' }}</dd></div>
+              <div><dt>品牌</dt><dd>{{ product.brand }}</dd></div>
+              <div><dt>平台</dt><dd>京东</dd></div>
+              <div><dt>二级类目</dt><dd>{{ product.category2 || '-' }}</dd></div>
+              <div><dt>三级类目</dt><dd>{{ product.category3 || '-' }}</dd></div>
+              <div><dt>竞品关系</dt><dd>{{ relationCount }} 条</dd></div>
+              <div><dt>评论样本</dt><dd>0 条 · 等待 JustOneAPI</dd></div>
+            </dl>
+          </app-content-card>
+        </div>
+
+        <div class="detail-grid">
+          <app-content-card title="流量与转化路径" subtitle="展示真实经营指标,不推断缺失环节" tone="purple">
+            <div class="funnel-list">
+              @for (row of funnelRows; track row.label) {
+                <div class="funnel-row">
+                  <span class="funnel-label">{{ row.label }}</span>
+                  <span class="funnel-value"><span class="funnel-bar" [style.width.%]="row.width"></span><span class="funnel-number">{{ number(row.value) }}</span></span>
+                </div>
+              }
+            </div>
+          </app-content-card>
+          <app-content-card title="数据口径" subtitle="当前详情页的指标来源" tone="amber">
+            <dl class="info-list">
+              <div><dt>源文件</dt><dd>{{ dataset?.source?.sourceFile }}</dd></div>
+              <div><dt>开始日期</dt><dd>{{ dataset?.source?.dateRange?.start }}</dd></div>
+              <div><dt>结束日期</dt><dd>{{ dataset?.source?.dateRange?.end }}</dd></div>
+              <div><dt>数据来源</dt><dd>{{ product.source }}</dd></div>
+            </dl>
+          </app-content-card>
+        </div>
+      }
+    </section>
+  `,
+  styleUrls: ['./domestic-page.shared.scss'],
+})
+export class DomesticProductOperatingDetailComponent implements OnInit, OnDestroy {
+  private readonly datasetService = inject(DomesticDatasetService);
+  private readonly state = inject(VocInsightStateService);
+  private readonly route = inject(ActivatedRoute);
+  private readonly router = inject(Router);
+  private readonly cdr = inject(ChangeDetectorRef);
+  private readonly destroy$ = new Subject<void>();
+  private initialRouteApplied = false;
+
+  dataset?: DomesticDataset;
+  product?: DomesticProduct;
+  relationCount = 0;
+  trendOptions: EChartsOption = {};
+  funnelRows: Array<{ label: string; value: number; width: number }> = [];
+
+  ngOnInit(): void {
+    combineLatest([this.datasetService.dataset$, this.state.product$])
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(([dataset, context]) => {
+        this.dataset = dataset;
+        const requested = !this.initialRouteApplied ? this.route.snapshot.queryParamMap.get('productId') : '';
+        const fallback = [...dataset.products].sort((a, b) => b.summary.gmv - a.summary.gmv)[0];
+        const productId = requested || context?.asin || fallback?.productId;
+        const product = dataset.products.find((item) => item.productId === productId) || fallback;
+        this.initialRouteApplied = true;
+        if (!product) return;
+        if (context?.asin !== product.productId) {
+          this.state.setProduct({
+            asin: product.productId,
+            title: product.title,
+            photo: 'assets/images/placeholder.svg',
+            productSku: product.model,
+            sellerSku: product.productId,
+            storeName: '京东',
+            reviewCount: 0,
+          });
+        }
+        this.product = product;
+        this.relationCount = dataset.relations.filter((relation) => relation.ownProductId === product.productId).length;
+        this.trendOptions = this.buildTrendOptions(product);
+        this.funnelRows = this.buildFunnelRows(product);
+        this.cdr.markForCheck();
+      });
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  goVoc(): void {
+    this.router.navigate(['/voc-insight/voice']);
+  }
+
+  goCompetitors(): void {
+    this.router.navigate(['/domestic/competitors'], { queryParams: { productId: this.product?.productId || null } });
+  }
+
+  currency(value: number): string {
+    return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', maximumFractionDigits: 0 }).format(value || 0);
+  }
+
+  number(value: number): string {
+    return new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }).format(value || 0);
+  }
+
+  percent(value: number): string {
+    return `${((value || 0) * 100).toFixed(1)}%`;
+  }
+
+  private buildTrendOptions(product: DomesticProduct): EChartsOption {
+    return {
+      color: ['#4f6bed', '#38a169', '#d69e2e'],
+      tooltip: { trigger: 'axis' },
+      legend: { data: ['成交金额', '成交件数', '成交订单'], top: 0 },
+      grid: { left: 56, right: 50, top: 48, bottom: 34 },
+      xAxis: { type: 'category', data: product.trend.map((row) => row.date.slice(5)), axisTick: { show: false } },
+      yAxis: [
+        { type: 'value', name: '金额', splitLine: { lineStyle: { color: '#edf0f4' } } },
+        { type: 'value', name: '数量', splitLine: { show: false } },
+      ],
+      series: [
+        { name: '成交金额', type: 'line', smooth: true, symbolSize: 7, areaStyle: { opacity: 0.08 }, data: product.trend.map((row) => Math.round(row.gmv)) },
+        { name: '成交件数', type: 'bar', yAxisIndex: 1, barMaxWidth: 20, data: product.trend.map((row) => row.soldUnits) },
+        { name: '成交订单', type: 'line', yAxisIndex: 1, smooth: true, symbol: 'none', data: product.trend.map((row) => row.transactionOrders) },
+      ],
+    };
+  }
+
+  private buildFunnelRows(product: DomesticProduct): Array<{ label: string; value: number; width: number }> {
+    const rows = [
+      { label: '曝光人数', value: product.summary.impressions },
+      { label: '点击人数', value: product.summary.clicks },
+      { label: '访客人数', value: product.summary.visitors },
+      { label: '加购件数', value: product.summary.cartUnits },
+      { label: '成交订单', value: product.summary.transactionOrders },
+      { label: '成交件数', value: product.summary.soldUnits },
+    ];
+    const max = Math.max(...rows.map((row) => row.value), 1);
+    return rows.map((row) => ({ ...row, width: Math.max(3, (row.value / max) * 100) }));
+  }
+}

+ 4 - 0
src/modules/shared/components/navigation/navigation.component.scss

@@ -87,6 +87,10 @@ $neu-radius:   neu.$radius;
 .nav-item-wrapper {
 .nav-item-wrapper {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
+
+  &.has-children + .has-children {
+    margin-top: 7px;
+  }
 }
 }
 
 
 // ─── Nav Item(拟物凸起按钮)───
 // ─── Nav Item(拟物凸起按钮)───

+ 20 - 7
src/modules/shared/components/navigation/navigation.component.ts

@@ -23,7 +23,7 @@ interface NavItem {
       <div class="sidebar-menu">
       <div class="sidebar-menu">
         <ng-container *ngFor="let item of navItems">
         <ng-container *ngFor="let item of navItems">
           <!-- Parent Item -->
           <!-- Parent Item -->
-          <div class="nav-item-wrapper">
+          <div class="nav-item-wrapper" [class.has-children]="!!item.children">
             <a
             <a
               [routerLink]="item.children ? null : item.path"
               [routerLink]="item.children ? null : item.path"
               [class.active]="isActive(item)"
               [class.active]="isActive(item)"
@@ -72,18 +72,31 @@ export class NavigationComponent {
   isCollapsed = false;
   isCollapsed = false;
 
 
   navItems: NavItem[] = [
   navItems: NavItem[] = [
+    {
+      path: '/domestic',
+      displayName: '经营分析',
+      icon: 'dashboard',
+      requiresAuth: false,
+      expanded: true,
+      children: [
+        { path: '/domestic/overview', displayName: '经营概览', icon: 'overview', requiresAuth: false },
+        { path: '/voc-insight/products', displayName: '商品列表', icon: 'product', requiresAuth: false },
+        { path: '/domestic/product', displayName: '商品经营详情', icon: 'productDefine', requiresAuth: false },
+        { path: '/domestic/categories', displayName: '品类分析', icon: 'category', requiresAuth: false },
+        { path: '/domestic/competitors', displayName: '竞品关系', icon: 'competitor', requiresAuth: false },
+      ],
+    },
     {
     {
       path: '/voc-insight',
       path: '/voc-insight',
-      displayName: '国内商品 VOC',
+      displayName: 'VOC 深度洞察',
       icon: 'voc',
       icon: 'voc',
       requiresAuth: false,
       requiresAuth: false,
       expanded: true,
       expanded: true,
       children: [
       children: [
-        { path: '/voc-insight/products', displayName: '商品列表', icon: 'product', requiresAuth: false },
-        { path: '/voc-insight/voice', displayName: '用户声音分类', icon: 'emotion', requiresAuth: false },
-        { path: '/voc-insight/pain-point', displayName: '痛点深度洞察', icon: 'overview', requiresAuth: false },
-        { path: '/voc-insight/feature', displayName: '功能满足度', icon: 'product', requiresAuth: false },
-        { path: '/voc-insight/scenario', displayName: '使用场景', icon: 'trend', requiresAuth: false },
+        { path: '/voc-insight/voice', displayName: '用户声音分类', icon: 'voiceClassify', requiresAuth: false },
+        { path: '/voc-insight/pain-point', displayName: '痛点深度洞察', icon: 'painPoint', requiresAuth: false },
+        { path: '/voc-insight/feature', displayName: '功能满足度', icon: 'featureScore', requiresAuth: false },
+        { path: '/voc-insight/scenario', displayName: '使用场景', icon: 'scenario', requiresAuth: false },
       ],
       ],
     },
     },
     {
     {

+ 2 - 2
src/modules/voc-insight/voc-insight.component.ts

@@ -168,7 +168,7 @@ export class VocInsightComponent implements OnInit, OnDestroy {
     return pages;
     return pages;
   }
   }
 
 
-  // 选择产品 - 跳转到详情页
+  // 选择产品 - 先进入国内商品经营详情,再按需进入 VOC 分析。
   selectProduct(product: ProductInfo): void {
   selectProduct(product: ProductInfo): void {
     this.stateService.setProduct({
     this.stateService.setProduct({
       asin: product.asin,
       asin: product.asin,
@@ -180,7 +180,7 @@ export class VocInsightComponent implements OnInit, OnDestroy {
       productSku: product.productSku,
       productSku: product.productSku,
       storeName: product.store,
       storeName: product.store,
     });
     });
-    this.router.navigate(['/voc-insight/voice']);
+    this.router.navigate(['/domestic/product'], { queryParams: { productId: product.asin } });
   }
   }
 
 
   // 格式化数字
   // 格式化数字