Przeglądaj źródła

feat: complete domestic VOC framework migration

gangvy 1 miesiąc temu
rodzic
commit
b4a1996f5f

+ 9 - 4
README.md

@@ -2,7 +2,7 @@
 
 基于现有 `msq-voc-web` 前端框架打包出的国内电商 VOC 通用模板,首个案例为德玛仕京东商品。项目保留原系统的 Angular App Shell、顶部搜索、侧边导航、共享组件、VOC 页面结构和 ECharts 能力,只替换最小闭环所需的数据入口与国内商品语义。
 
-> 当前状态(2026-07-22):真实框架迁入和本地德玛仕数据闭环已完成;独立数据库、JustOneAPI 评论采集和独立环境部署尚未完成
+> 当前状态(2026-07-23):原项目 App Shell、共享组件体系、经营分析、市场与竞品、VOC 全局监测和 VOC 深度洞察框架已完成迁入;已启用页面均使用国内商品语义和统一状态组件
 
 ## 迁入基线
 
@@ -18,6 +18,9 @@
 - `/domestic/product`:单商品经营详情、流量转化、退款表现和商品档案
 - `/domestic/categories`:二级/三级类目经营对比
 - `/domestic/competitors`:竞品关系、映射覆盖率、搜索和本品跳转
+- `/domestic/voc-overview`:VOC 核心指标、情绪健康度、问题与热词总览
+- `/domestic/category-voc`:类目 VOC 四象限、下钻维度与诊断摘要
+- `/domestic/risk-alerts`:商品风险、场景化投诉与异常激增预警框架
 - `/voc-insight/products`:德玛仕商品列表、搜索、排序和商品选择
 - `/voc-insight/voice`:用户声音分类;无评论时展示真实空状态
 - `/voc-insight/pain-point`:痛点深度洞察
@@ -37,7 +40,8 @@
 4. 按商品 ID、型号或名称搜索并选择商品,先进入经营详情,再进入 VOC 洞察。
 5. 在原 VOC 页面中保持商品上下文。
 6. 对当前真实的 0 条评论展示空状态,不伪造情绪、痛点或分析结果。
-7. 查看导入统计和数据质量问题。
+7. 查看 VOC 趋势总览、类目 VOC 和风险预警的完整页面结构与真实空状态。
+8. 查看导入统计和数据质量问题。
 
 德玛仕数据摘要:
 
@@ -119,8 +123,9 @@ npm run import:demashi -- "E:\xwechat_files\wxid_ay08t6ugo2h922_df4f\msg\file\20
 - `npm run build`:通过
 - `npm test`:5 项 Jasmine 测试通过
 - `npm run verify:data`:通过
-- 桌面端:经营概览、商品详情、品类切换、竞品搜索、商品选择、VOC 路由和空状态通过
-- 移动端:分组导航抽屉、导航后自动收起、经营概览、品类页和竞品页通过
+- 桌面端:13 个启用入口的页面身份、主内容、导航、空状态和交互通过
+- 移动端:13 个启用入口无页面级横向溢出,导航抽屉展开/收起、标题和控件布局通过
+- 浏览器控制台:启用入口无 Angular 运行时错误、警告或失败请求
 - 运行时请求:当前激活闭环只请求同源德玛仕 JSON,没有访问旧 Parse 或旧跨境域名
 - `npm audit --omit=dev`:17 项原框架依赖公告(1 critical、11 high、5 moderate),正式公网部署前必须升级处理
 

+ 7 - 3
TASKS.md

@@ -1,6 +1,6 @@
 # Saas VOC 任务清单
 
-更新时间:2026-07-22
+更新时间:2026-07-23
 
 ## 已完成
 
@@ -13,12 +13,16 @@
 - [x] 接入 2,817 个真实商品、9,717 条日指标和 40 条竞品关系
 - [x] 接通商品搜索、商品选择、VOC 页面商品上下文和真实零评论空状态
 - [x] 迁入经营概览、商品经营详情、品类分析和竞品关系页面
-- [x] 将左侧导航整理为“经营分析”“VOC 深度洞察”和“数据接入状态”三层结构
+- [x] 迁入 VOC 趋势总览、类目 VOC 和风险预警页面
+- [x] 复用原 `PageHeader`、`SummaryMetricCard`、`ContentCard`、`DataTableShell`、`ProductIdentity`、`BoardTabs`、`NeuButton`、`LoadingSpinner` 和 `EmptyState`
+- [x] 将左侧导航整理为“经营分析”“市场与竞品”“VOC 全局监测”“VOC 深度洞察”和“数据接入状态”五组结构
+- [x] 将原 `FilterBar` 解耦为可关闭旧数据选项自动加载的外部受控模式
 - [x] 接通商品列表到经营详情、经营详情到 VOC 的真实商品上下文
 - [x] 增加数据接入状态页
 - [x] 将 Parse 和电商中转默认地址改为同源路径
 - [x] 移除旧跨境线上域名和旧 Parse 二次登录逻辑
-- [x] 完成桌面与移动端响应式验收
+- [x] 完成 13 个启用入口的桌面与移动端响应式验收
+- [x] 修复类目 VOC 首次加载的 Angular 变更检测错误
 - [x] 完成构建、单测和数据完整性校验
 
 ## 周六前 P0

+ 15 - 0
src/app/app.routes.ts

@@ -30,6 +30,21 @@ export const routes: Routes = [
         title: '竞品关系',
         loadComponent: () => import('../modules/domestic-voc/competitor-relations.component').then((m) => m.DomesticCompetitorRelationsComponent),
       },
+      {
+        path: 'voc-overview',
+        title: 'VOC 趋势总览',
+        loadComponent: () => import('../modules/monitoring/pages/voc-overview/voc-overview.component').then((m) => m.VocOverviewComponent),
+      },
+      {
+        path: 'category-voc',
+        title: '类目 VOC',
+        loadComponent: () => import('../modules/monitoring/pages/category-voc/category-voc.component').then((m) => m.CategoryVocComponent),
+      },
+      {
+        path: 'risk-alerts',
+        title: '风险预警',
+        loadComponent: () => import('../modules/monitoring/pages/risk-alert/risk-alert.component').then((m) => m.RiskAlertComponent),
+      },
     ],
   },
   {

+ 172 - 0
src/app/core/services/domestic-monitoring-view.service.ts

@@ -0,0 +1,172 @@
+import { Injectable, inject } from '@angular/core';
+import { Observable, map, of } from 'rxjs';
+import {
+  CategoryVocDensityItem,
+  CoreIssueSummary,
+  DesignerRiskRankingItem,
+  EmotionHealthScore,
+  KeywordCloud,
+  RiskAlertItem,
+  ScenarioComplaintCluster,
+  SentimentSpikeEvent,
+} from '../../../modules/shared/interfaces/monitoring-interfaces';
+import { FilterOptions } from '../../../modules/shared/interfaces/voc-data.interface';
+import { DomesticProduct } from '../models/domestic.models';
+import { DomesticDatasetService } from './domestic-dataset.service';
+
+interface DomesticVocMetrics {
+  vocCount: number;
+  vocTrend: number;
+  warningCount: number;
+  todayCount: number;
+  yesterdayCount: number;
+  recentPositiveRate: number;
+  productRatingFallback: boolean;
+  productRatingCount: number;
+  productCount: number;
+  avgProductRating: number;
+  periodReviewCount: number;
+  scopedReviewCount: number;
+  listingReviewVolume: number;
+}
+
+@Injectable({ providedIn: 'root' })
+export class DomesticMonitoringViewService {
+  private readonly dataset = inject(DomesticDatasetService);
+
+  getVocMetrics(_filters?: FilterOptions): Observable<DomesticVocMetrics> {
+    return this.dataset.dataset$.pipe(map((dataset) => ({
+      vocCount: dataset.summary.reviewCount,
+      vocTrend: 0,
+      warningCount: 0,
+      todayCount: 0,
+      yesterdayCount: 0,
+      recentPositiveRate: 0,
+      productRatingFallback: false,
+      productRatingCount: 0,
+      productCount: dataset.summary.metricProducts,
+      avgProductRating: 0,
+      periodReviewCount: dataset.summary.reviewCount,
+      scopedReviewCount: dataset.summary.reviewCount,
+      listingReviewVolume: 0,
+    })));
+  }
+
+  getEmotionHealthScore(_filters?: FilterOptions): Observable<EmotionHealthScore> {
+    return of({
+      score: 0,
+      trend: 'stable',
+      positiveRate: 0,
+      negativeRate: 0,
+      neutralRate: 0,
+      totalReviews: 0,
+    });
+  }
+
+  getCoreIssueSummary(_filters?: FilterOptions): Observable<CoreIssueSummary[]> {
+    return of([]);
+  }
+
+  getKeywordCloud(_filters?: FilterOptions): Observable<KeywordCloud[]> {
+    return of([]);
+  }
+
+  getDesignerRiskRanking(_filters?: FilterOptions): Observable<DesignerRiskRankingItem[]> {
+    return of([]);
+  }
+
+  getRiskAlertItems(_filters?: FilterOptions): Observable<RiskAlertItem[]> {
+    return of([]);
+  }
+
+  getScenarioComplaints(_filters?: FilterOptions): Observable<ScenarioComplaintCluster[]> {
+    return of([]);
+  }
+
+  getSentimentSpikeEvents(_filters?: FilterOptions): Observable<SentimentSpikeEvent[]> {
+    return of([]);
+  }
+
+  getCategoryProductRows(
+    _source: 'own' | 'competitor' = 'own',
+    _filters?: FilterOptions,
+  ): Observable<Map<string, any[]>> {
+    return this.dataset.dataset$.pipe(map((dataset) => {
+      const groups = new Map<string, any[]>();
+      dataset.products.forEach((product) => {
+        const category = product.category2 || product.category1 || '未分类';
+        const rows = groups.get(category) ?? [];
+        rows.push({
+          asin: product.productId,
+          productId: product.productId,
+          title: product.title,
+          category,
+          subCategory: product.category3,
+          brand: product.category3 || product.brand,
+          sales: product.summary.soldUnits,
+          price: product.summary.averageUnitPrice,
+          designerName: '',
+        });
+        groups.set(category, rows);
+      });
+      return groups;
+    }));
+  }
+
+  getCategoryVocDensity(_filters?: FilterOptions): Observable<CategoryVocDensityItem[]> {
+    return this.dataset.dataset$.pipe(map((dataset) => {
+      const groups = new Map<string, DomesticProduct[]>();
+      dataset.products.forEach((product) => {
+        const category = product.category2 || product.category1 || '未分类';
+        groups.set(category, [...(groups.get(category) ?? []), product]);
+      });
+
+      const totals = Array.from(groups.values()).map((products) =>
+        products.reduce((sum, product) => sum + product.summary.soldUnits, 0),
+      );
+      const orderedTotals = [...totals].sort((a, b) => a - b);
+      const median = orderedTotals[Math.floor(orderedTotals.length / 2)] ?? 0;
+
+      return Array.from(groups.entries())
+        .map(([category, products]): CategoryVocDensityItem => {
+          const totalSales = products.reduce((sum, product) => sum + product.summary.soldUnits, 0);
+          const totalGmv = products.reduce((sum, product) => sum + product.summary.gmv, 0);
+          const subCategories = new Map<string, DomesticProduct[]>();
+          products.forEach((product) => {
+            const name = product.category3 || '未分类';
+            subCategories.set(name, [...(subCategories.get(name) ?? []), product]);
+          });
+          const topBrands = Array.from(subCategories.entries())
+            .map(([brand, rows]) => ({
+              brand,
+              count: rows.length,
+              avgSales: rows.length
+                ? rows.reduce((sum, product) => sum + product.summary.soldUnits, 0) / rows.length
+                : 0,
+            }))
+            .sort((a, b) => b.avgSales - a.avgSales)
+            .slice(0, 12);
+
+          return {
+            category,
+            categoryZh: category,
+            parentCategory: products[0]?.category1 || '国内电商',
+            parentCategoryZh: products[0]?.category1 || '国内电商',
+            productCount: products.length,
+            reviewCount: 0,
+            density: 0,
+            positiveRate: 0,
+            negativeRate: 0,
+            avgRating: 0,
+            emotionScore: 0,
+            avgSales: products.length ? totalSales / products.length : 0,
+            totalSales,
+            avgPrice: totalSales ? totalGmv / totalSales : 0,
+            quadrant: totalSales >= median ? 'star' : 'opportunity',
+            topBrands,
+          };
+        })
+        .sort((a, b) => b.totalSales - a.totalSales || a.category.localeCompare(b.category, 'zh-CN'));
+    }));
+  }
+}

+ 51 - 36
src/modules/domestic-voc/category-analysis.component.ts

@@ -4,56 +4,64 @@ 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 { BoardTabItem, BoardTabsComponent } from '../shared/components/board-tabs/board-tabs.component';
 import { ContentCardComponent } from '../shared/components/content-card/content-card.component';
 import { DataTableShellComponent } from '../shared/components/data-table-shell/data-table-shell.component';
+import { LoadingSpinnerComponent } from '../shared/components/loading-spinner/loading-spinner.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],
+  imports: [CommonModule, NgxEchartsModule, BoardTabsComponent, PageHeaderComponent, SummaryMetricCardComponent, ContentCardComponent, DataTableShellComponent, LoadingSpinnerComponent],
   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>
+      @if (loading) {
+        <app-loading-spinner variant="card" tone="blue" text="正在加载品类分析"></app-loading-spinner>
+      } @else {
+        <app-board-tabs
+          [tabs]="dimensionTabs"
+          [activeKey]="dimension"
+          density="compact"
+          (activeKeyChange)="setDimension($event)">
+        </app-board-tabs>
 
-      <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>
+        <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-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>
+        <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'],
@@ -65,7 +73,12 @@ export class DomesticCategoryAnalysisComponent implements OnInit, OnDestroy {
   private category2Rows: DomesticCategorySummary[] = [];
   private category3Rows: DomesticCategorySummary[] = [];
 
+  readonly dimensionTabs: BoardTabItem[] = [
+    { key: 'category2', label: '二级类目', iconKey: 'category' },
+    { key: 'category3', label: '三级类目', iconKey: 'trend' },
+  ];
   dimension: 'category2' | 'category3' = 'category2';
+  loading = true;
   rows: DomesticCategorySummary[] = [];
   totalProducts = 0;
   totalGmv = 0;
@@ -81,6 +94,7 @@ export class DomesticCategoryAnalysisComponent implements OnInit, OnDestroy {
         this.category3Rows = analysis.category3;
         this.allRelations = analysis.relationCount;
         this.refreshRows();
+        this.loading = false;
       });
   }
 
@@ -89,7 +103,8 @@ export class DomesticCategoryAnalysisComponent implements OnInit, OnDestroy {
     this.destroy$.complete();
   }
 
-  setDimension(dimension: 'category2' | 'category3'): void {
+  setDimension(dimension: string): void {
+    if (dimension !== 'category2' && dimension !== 'category3') return;
     if (this.dimension === dimension) return;
     this.dimension = dimension;
     this.refreshRows();

+ 17 - 8
src/modules/domestic-voc/competitor-relations.component.ts

@@ -7,15 +7,17 @@ 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 { LoadingSpinnerComponent } from '../shared/components/loading-spinner/loading-spinner.component';
 import { MsqSearchBoxComponent } from '../shared/components/msq-search-box/msq-search-box.component';
 import { PageHeaderComponent } from '../shared/components/page-header/page-header.component';
+import { ProductIdentityComponent } from '../shared/components/product-identity/product-identity.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],
+  imports: [CommonModule, FormsModule, PageHeaderComponent, SummaryMetricCardComponent, DataTableShellComponent, LoadingSpinnerComponent, MsqSearchBoxComponent, ProductIdentityComponent],
   template: `
     <section class="domestic-page">
       <app-page-header title="竞品关系" eyebrow="Competitor Mapping" description="管理德玛仕商品与国内平台竞品的映射,并明确区分已配置关系和待采集指标。" icon="⇄"></app-page-header>
@@ -28,7 +30,7 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
           <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="没有匹配的竞品关系">
+        <app-data-table-shell title="竞品映射明细" subtitle="当前展示已配置的本品与竞品映射关系" [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>
@@ -38,12 +40,17 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
               @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>
+                    <app-product-identity
+                      [showImage]="false"
+                      [title]="row.ownTitle"
+                      [productSku]="row.ownModel"
+                      [asin]="row.ownProductId"
+                      [subtitle]="row.ownProductAvailable ? '' : '经营数据缺失'"
+                      [clickable]="row.ownProductAvailable"
+                      [muted]="!row.ownProductAvailable"
+                      size="xs"
+                      (identityClick)="openOwnProduct(row)">
+                    </app-product-identity>
                   </td>
                   <td>{{ row.category }}</td>
                   <td>{{ row.competitorBrand }}</td>
@@ -54,6 +61,8 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
             </tbody>
           </table>
         </app-data-table-shell>
+      } @else {
+        <app-loading-spinner variant="card" tone="blue" text="正在加载竞品关系"></app-loading-spinner>
       }
     </section>
   `,

+ 2 - 2
src/modules/domestic-voc/data-status.component.ts

@@ -25,7 +25,7 @@ import { SummaryMetricCardComponent } from '../shared/components/summary-metric-
           <app-summary-metric-card label="日指标记录" [value]="data.summary.metricRows" icon="▤" tone="blue"></app-summary-metric-card>
           <app-summary-metric-card label="经营商品" [value]="data.summary.metricProducts" icon="□" tone="green"></app-summary-metric-card>
           <app-summary-metric-card label="竞品关系" [value]="data.summary.relations" icon="⇄" tone="amber"></app-summary-metric-card>
-          <app-summary-metric-card label="评论样本" [value]="data.summary.reviewCount" icon="◇" tone="red" description="等待 JustOneAPI"></app-summary-metric-card>
+          <app-summary-metric-card label="评论样本" [value]="data.summary.reviewCount" icon="◇" tone="red" description="当前暂无样本"></app-summary-metric-card>
         </div>
 
         <div class="status-grid">
@@ -42,7 +42,7 @@ import { SummaryMetricCardComponent } from '../shared/components/summary-metric-
               <li><span class="done">已完成</span> 原 VOC 前端框架迁入</li>
               <li><span class="done">已完成</span> 商品 ID 兼容层与本地案例数据</li>
               <li><span class="waiting">待处理</span> 独立 PostgreSQL / Parse 应用</li>
-              <li><span class="waiting">待处理</span> Fmode / JustOneAPI 评论采集</li>
+              <li><span class="waiting">待处理</span> 评论采集服务</li>
             </ol>
           </app-content-card>
         </div>

+ 30 - 111
src/modules/domestic-voc/domestic-page.shared.scss

@@ -1,102 +1,65 @@
+@use '../shared/styles/layout' as layout;
+@use '../shared/styles/table' as table;
+@use '../shared/styles/chart' as chart;
+@use '../shared/styles/tokens' as tokens;
+
 :host {
   display: block;
   min-width: 0;
 }
 
 .domestic-page {
-  display: flex;
+  @include layout.page-shell(20px);
   min-height: 100%;
-  flex-direction: column;
-  gap: 20px;
-  padding: 24px 24px 84px;
-  background: #f7f8fc;
+  padding-bottom: 84px;
 }
 
 .metric-grid {
-  display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 16px;
+  @include layout.dashboard-grid(4, 200px, 16px);
 }
 
 .analysis-grid {
-  display: grid;
-  grid-template-columns: minmax(0, 1.6fr) minmax(300px, 0.8fr);
-  gap: 16px;
+  @include layout.two-column-layout((1.6, 0.8), 16px, 1120px);
   align-items: stretch;
 }
 
 .detail-grid {
-  display: grid;
-  grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.7fr);
-  gap: 16px;
+  @include layout.two-column-layout((1.5, 0.7), 16px, 1120px);
 }
 
 .chart {
-  width: 100%;
-  height: 310px;
+  @include chart.chart-canvas(310px);
+  flex: none;
 }
 
 .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%;
+  @include table.base-table;
   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;
-}
+  th,
+  td {
+    height: 48px;
+    padding: 0 14px;
+  }
 
-.analytics-table th {
-  color: #64748b;
-  background: #f8fafc;
-  font-size: 12px;
-  font-weight: 700;
-  white-space: nowrap;
-}
+  th {
+    color: tokens.$color-text-muted;
+    font-weight: 700;
+  }
 
-.analytics-table td {
-  color: #334155;
-  font-size: 13px;
-}
+  td {
+    color: tokens.$color-text-secondary;
+  }
 
-.analytics-table td.numeric,
-.analytics-table th.numeric {
-  text-align: right;
-  font-variant-numeric: tabular-nums;
+  td.numeric,
+  th.numeric {
+    @include table.mono-num;
+    text-align: right;
+  }
 }
 
 .row-link {
@@ -247,34 +210,6 @@
   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%);
 }
@@ -297,28 +232,12 @@
   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;

+ 18 - 3
src/modules/domestic-voc/operating-overview.component.ts

@@ -8,14 +8,17 @@ 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 { LoadingSpinnerComponent } from '../shared/components/loading-spinner/loading-spinner.component';
+import { NeuButtonComponent } from '../shared/components/neu-button/neu-button.component';
 import { PageHeaderComponent } from '../shared/components/page-header/page-header.component';
+import { ProductIdentityComponent } from '../shared/components/product-identity/product-identity.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],
+  imports: [CommonModule, NgxEchartsModule, PageHeaderComponent, SummaryMetricCardComponent, ContentCardComponent, DataTableShellComponent, LoadingSpinnerComponent, NeuButtonComponent, ProductIdentityComponent],
   changeDetection: ChangeDetectionStrategy.OnPush,
   template: `
     <section class="domestic-page">
@@ -29,7 +32,7 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
             { 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-neu-button pageHeaderActions size="sm" color="primary" (btnClick)="goProducts()">商品列表</app-neu-button>
         </app-page-header>
 
         <div class="metric-grid">
@@ -62,7 +65,17 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
             <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>
+                    <app-product-identity
+                      [showImage]="false"
+                      [title]="product.title"
+                      [productSku]="product.model"
+                      [asin]="product.productId"
+                      [clickable]="true"
+                      size="xs"
+                      (identityClick)="openProduct(product)">
+                    </app-product-identity>
+                  </td>
                   <td>{{ product.category3 || product.category2 }}</td>
                   <td class="numeric">{{ currency(product.summary.gmv) }}</td>
                   <td class="numeric">{{ number(product.summary.soldUnits) }}</td>
@@ -73,6 +86,8 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
             </tbody>
           </table>
         </app-data-table-shell>
+      } @else {
+        <app-loading-spinner variant="card" tone="blue" text="正在加载经营概览"></app-loading-spinner>
       }
     </section>
   `,

+ 8 - 4
src/modules/domestic-voc/product-operating-detail.component.ts

@@ -7,6 +7,8 @@ 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 { LoadingSpinnerComponent } from '../shared/components/loading-spinner/loading-spinner.component';
+import { NeuButtonComponent } from '../shared/components/neu-button/neu-button.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';
@@ -15,7 +17,7 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
 @Component({
   selector: 'app-domestic-product-operating-detail',
   standalone: true,
-  imports: [CommonModule, NgxEchartsModule, PageHeaderComponent, SummaryMetricCardComponent, ContentCardComponent, VocProductPickerComponent],
+  imports: [CommonModule, NgxEchartsModule, PageHeaderComponent, SummaryMetricCardComponent, ContentCardComponent, LoadingSpinnerComponent, NeuButtonComponent, VocProductPickerComponent],
   template: `
     <section class="domestic-page">
       <app-page-header
@@ -23,8 +25,8 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
         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-neu-button pageHeaderActions size="sm" (btnClick)="goCompetitors()">竞品关系</app-neu-button>
+        <app-neu-button pageHeaderActions size="sm" color="primary" (btnClick)="goVoc()">查看 VOC</app-neu-button>
       </app-page-header>
 
       <div class="product-picker-wrap"><app-voc-product-picker></app-voc-product-picker></div>
@@ -50,7 +52,7 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
               <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>
+              <div><dt>评论样本</dt><dd>0 条 · 暂无评论样本</dd></div>
             </dl>
           </app-content-card>
         </div>
@@ -75,6 +77,8 @@ import { VocInsightStateService } from '../shared/services/voc-insight-state.ser
             </dl>
           </app-content-card>
         </div>
+      } @else {
+        <app-loading-spinner variant="card" tone="blue" text="正在加载商品经营详情"></app-loading-spinner>
       }
     </section>
   `,

+ 9 - 11
src/modules/monitoring/pages/category-voc/category-voc.component.ts

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { Subject, Subscription } from 'rxjs';
 import { takeUntil } from 'rxjs/operators';
-import { MonitoringDataService } from '../../../shared/services/monitoring-data.service';
+import { DomesticMonitoringViewService } from '../../../../app/core/services/domestic-monitoring-view.service';
 import { CategoryVocDensityItem } from '../../../shared/interfaces/monitoring-interfaces';
 import { FilterBarComponent } from '../../../shared/components/filter-bar/filter-bar.component';
 import { PageHeaderComponent } from '../../../shared/components/page-header/page-header.component';
@@ -40,7 +40,7 @@ interface TableGroup {
   template: `
     <div class="category-voc-container">
       <app-page-header
-        title="品类分布"
+        title="类目 VOC"
         eyebrow="Monitoring"
         description="各品类 VOC 表现对比 · 销量 vs 差评率四象限 · 识别问题/机会品类"
         icon="📦">
@@ -55,18 +55,16 @@ interface TableGroup {
         <app-summary-metric-card label="品类总数" [value]="categories.length" tone="blue" icon="📦"></app-summary-metric-card>
         <app-summary-metric-card label="产品总数" [value]="totalProducts | number" tone="blue" icon="📊"></app-summary-metric-card>
         <app-summary-metric-card label="评论总数" [value]="totalReviews | number" tone="default" icon="📝"></app-summary-metric-card>
-        <app-summary-metric-card label="平均情绪得分" [value]="avgEmotionScore" [tone]="avgEmotionScore >= 75 ? 'green' : avgEmotionScore >= 50 ? 'amber' : 'red'" icon="😀"></app-summary-metric-card>
-        <app-summary-metric-card label="平均密度(条/产品)" [value]="avgDensity" tone="default" icon="📈"></app-summary-metric-card>
+        <app-summary-metric-card label="平均情绪得分" [value]="totalReviews ? avgEmotionScore : '-'" [tone]="totalReviews ? (avgEmotionScore >= 75 ? 'green' : avgEmotionScore >= 50 ? 'amber' : 'red') : 'default'" icon="😀"></app-summary-metric-card>
+        <app-summary-metric-card label="平均密度(条/产品)" [value]="totalReviews ? avgDensity : '-'" tone="default" icon="📈"></app-summary-metric-card>
       </div>
 
       <app-filter-bar
         [filters]="currentFilters"
-        [enableDesigner]="true"
-        [designerOptions]="designerFilterOptions"
-        [enableOperator]="true"
-        [enableDeveloper]="true"
-        [enableListingStatus]="true"
-        [enableFulfillmentType]="true"
+        [autoLoadOptions]="false"
+        [showSite]="false"
+        [showStore]="false"
+        [showActions]="false"
         [showDatePicker]="false"
         (filtersChange)="onFilterChange($event)">
       </app-filter-bar>
@@ -531,7 +529,7 @@ export class CategoryVocComponent implements OnInit, OnDestroy {
   ];
 
   constructor(
-    private monitoringService: MonitoringDataService,
+    private monitoringService: DomesticMonitoringViewService,
     private ngZone: NgZone,
     private cdr: ChangeDetectorRef
   ) {}

+ 18 - 19
src/modules/monitoring/pages/risk-alert/risk-alert.component.ts

@@ -1,10 +1,10 @@
 import { Component, OnInit, OnDestroy } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
-import { Router } from '@angular/router';
+import { Router, RouterModule } from '@angular/router';
 import { Subject, forkJoin } from 'rxjs';
 import { takeUntil } from 'rxjs/operators';
-import { MonitoringDataService } from '../../../shared/services/monitoring-data.service';
+import { DomesticMonitoringViewService } from '../../../../app/core/services/domestic-monitoring-view.service';
 import { RiskAlertItem, ScenarioComplaintCluster, SentimentSpikeEvent } from '../../../shared/interfaces/monitoring-interfaces';
 import { MsqSearchBoxComponent } from '../../../shared/components/msq-search-box/msq-search-box.component';
 import { FilterBarComponent } from '../../../shared/components/filter-bar/filter-bar.component';
@@ -19,7 +19,7 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
 @Component({
   selector: 'app-risk-alert',
   standalone: true,
-  imports: [CommonModule, FormsModule, MsqSearchBoxComponent, FilterBarComponent, PageHeaderComponent, BoardTabsComponent, EmptyStateComponent, LoadingSpinnerComponent, ProductIdentityComponent],
+  imports: [CommonModule, FormsModule, RouterModule, MsqSearchBoxComponent, FilterBarComponent, PageHeaderComponent, BoardTabsComponent, EmptyStateComponent, LoadingSpinnerComponent, ProductIdentityComponent],
   template: `
     <div class="risk-alert-container">
       <app-page-header
@@ -28,7 +28,7 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
         description="在系统性差评爆发前实现自动拦截与止损"
         icon="🛡️">
         <div pageHeaderActions class="header-right">
-          <msq-search-box placeholder="搜索 ASIN..." [(ngModel)]="searchQuery"></msq-search-box>
+          <msq-search-box placeholder="搜索商品ID或名称" [(ngModel)]="searchQuery"></msq-search-box>
           <button class="refresh-btn" (click)="refreshData()" [disabled]="loading">
             <span [class.spinning]="loading">🔄</span> 刷新
           </button>
@@ -41,11 +41,10 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
 
       <app-filter-bar
         [filters]="currentFilters"
-        [enableDesigner]="true"
-        [enableOperator]="true"
-        [enableDeveloper]="true"
-        [enableListingStatus]="true"
-        [enableFulfillmentType]="true"
+        [autoLoadOptions]="false"
+        [showSite]="false"
+        [showStore]="false"
+        [showActions]="false"
         [showDatePicker]="false"
         (filtersChange)="onFilterChange($event)">
       </app-filter-bar>
@@ -111,7 +110,7 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
                     [clickable]="true"
                     [externalHint]="true"
                     size="md"
-                    (identityClick)="openAmazonPage(item.asin)">
+                    (identityClick)="openProductDetail(item.asin)">
                   </app-product-identity>
                   <span class="rp-severity-badge" [class]="item.severity">
                     {{ item.severity === 'critical' ? '🔴 严重' : item.severity === 'warning' ? '🟡 警告' : '🟢 正常' }}
@@ -137,7 +136,7 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
                       差评率
                       <b [style.color]="item.negativeRate > 30 ? '#dc2626' : item.negativeRate > 15 ? '#d97706' : '#16a34a'">{{ item.negativeRate }}%</b>
                       @if (item.negativeRateBasis === 'rating_estimate') {
-                        <span class="rp-estimate-tag" title="基于 Amazon 评分占比估算,非可分析评论样本">估算</span>
+                        <span class="rp-estimate-tag" title="基于平台评分占比估算,非可分析评论样本">估算</span>
                       }
                       <span class="neg-bar-mini">
                         <span class="neg-bar-fill" [style.width.%]="Math.min(item.negativeRate, 100)"
@@ -149,7 +148,7 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
                   @if (item.negativeRateBasis === 'reviews') {
                     <span>可分析评论 <b>{{ item.analyzableReviews | number }}</b></span>
                   } @else {
-                    <span title="Amazon 评分计数,非可分析评论样本">Amazon评分 <b>{{ item.ratingsCount | number }}</b></span>
+                    <span title="平台评分计数,非可分析评论样本">平台评分 <b>{{ item.ratingsCount | number }}</b></span>
                   }
                 </div>
                 @if (item.alerts.length > 0) {
@@ -169,7 +168,7 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
                     <div class="rp-detail-actions">
                       <button class="action-btn primary" (click)="navigateToVocAnalysis(item.asin)">📊 VOC 深度分析</button>
                       <button class="action-btn" (click)="navigateToEmotionAnalysis(item.asin)">😊 情感分析</button>
-                      <button class="action-btn" (click)="openAmazonPage(item.asin)">🛒 Amazon 商品页</button>
+                      <button class="action-btn" (click)="openProductDetail(item.asin)">商品经营详情</button>
                     </div>
                     <div class="rp-detail-info">
                       <span>预警数: <b>{{ item.alerts.length }}</b></span>
@@ -244,7 +243,7 @@ type RiskAlertTabKey = 'products' | 'scenarios' | 'spikes';
                 <div class="spike-asins">
                   <span class="spike-asins-label">涉及商品:</span>
                   @for (asin of spike.affectedAsins.slice(0, expandedSpike === spike.date ? 20 : 3); track asin) {
-                    <a class="spike-asin-tag" [href]="'https://www.amazon.com/dp/' + asin" target="_blank" (click)="$event.stopPropagation()">{{ asin }}</a>
+                    <a class="spike-asin-tag" [routerLink]="['/domestic/product']" [queryParams]="{ productId: asin }" (click)="$event.stopPropagation()">{{ asin }}</a>
                   }
                   @if (spike.affectedAsins.length > 3 && expandedSpike !== spike.date) {
                     <span class="spike-more">+{{ spike.affectedAsins.length - 3 }} 更多</span>
@@ -430,7 +429,7 @@ export class RiskAlertComponent implements OnInit, OnDestroy {
   normalCount = 0;
 
   constructor(
-    private monitoringService: MonitoringDataService,
+    private monitoringService: DomesticMonitoringViewService,
     private router: Router
   ) {}
 
@@ -514,15 +513,15 @@ export class RiskAlertComponent implements OnInit, OnDestroy {
   }
 
   navigateToVocAnalysis(asin: string): void {
-    this.router.navigate(['/voc-insight/scenario-dashboard'], { queryParams: { asin } });
+    this.router.navigate(['/voc-insight/voice'], { queryParams: { productId: asin } });
   }
 
   navigateToEmotionAnalysis(asin: string): void {
-    this.router.navigate(['/monitoring/voc-scene'], { queryParams: { asin } });
+    this.router.navigate(['/voc-insight/voice'], { queryParams: { productId: asin } });
   }
 
-  openAmazonPage(asin: string): void {
-    window.open(`https://www.amazon.com/dp/${asin}`, '_blank');
+  openProductDetail(productId: string): void {
+    this.router.navigate(['/domestic/product'], { queryParams: { productId } });
   }
 
   get sortedRiskItems(): RiskAlertItem[] {

+ 17 - 16
src/modules/monitoring/pages/voc-overview/voc-overview.component.ts

@@ -4,7 +4,7 @@ import { RouterModule } from '@angular/router';
 import { Subject, forkJoin } from 'rxjs';
 import { takeUntil, catchError } from 'rxjs/operators';
 import { of } from 'rxjs';
-import { MonitoringDataService } from '../../../shared/services/monitoring-data.service';
+import { DomesticMonitoringViewService } from '../../../../app/core/services/domestic-monitoring-view.service';
 import { EmotionHealthScore, CoreIssueSummary, KeywordCloud, DesignerRiskRankingItem } from '../../../shared/interfaces/monitoring-interfaces';
 import { FilterBarComponent } from '../../../shared/components/filter-bar/filter-bar.component';
 import { PageHeaderComponent } from '../../../shared/components/page-header/page-header.component';
@@ -40,11 +40,10 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
 
       <app-filter-bar
         [filters]="currentFilters"
-        [enableDesigner]="true"
-        [enableOperator]="true"
-        [enableDeveloper]="true"
-        [enableListingStatus]="true"
-        [enableFulfillmentType]="true"
+        [autoLoadOptions]="false"
+        [showSite]="false"
+        [showStore]="false"
+        [showActions]="false"
         [showDatePicker]="false"
         (filtersChange)="onFilterChange($event)">
       </app-filter-bar>
@@ -72,8 +71,8 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
                   @if (item.topAsins.length) {
                     <div class="designer-risk-asins">
                       @for (asin of item.topAsins; track asin.asin) {
-                        <a [href]="'https://www.amazon.com/dp/' + asin.asin" target="_blank" class="risk-asin-tag">
-                          {{ asin.asin }} · {{ asin.negativeRate }}%
+                        <a [routerLink]="['/domestic/product']" [queryParams]="{ productId: asin.asin }" class="risk-asin-tag">
+                          商品ID {{ asin.asin }} · {{ asin.negativeRate }}%
                         </a>
                       }
                     </div>
@@ -96,7 +95,7 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
         <!-- 左:健康度仪表盘 -->
         <div class="panel health-gauge-panel">
           <h2 class="panel-title">情绪健康度仪表盘</h2>
-          @if (healthScore) {
+          @if (healthScore && healthScore.totalReviews > 0) {
             <div class="gauge-wrapper">
               <div class="gauge-ring">
                 <svg viewBox="0 0 120 120" class="gauge-svg">
@@ -137,6 +136,8 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
                 <span class="legend-item bad">&lt;50 预警</span>
               </div>
             </div>
+          } @else {
+            <app-empty-state title="暂无情绪样本" description="页面结构已就绪,当前保持真实空状态。" icon="◌" size="sm"></app-empty-state>
           }
         </div>
 
@@ -215,7 +216,7 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
 
       <!-- 快速导航 -->
       <div class="nav-cards">
-        <a class="nav-card" routerLink="/monitoring/risk-alert">
+        <a class="nav-card" routerLink="/domestic/risk-alerts">
           <span class="nav-card-icon">🛡️</span>
           <span class="nav-card-title">风险预警看板</span>
           <span class="nav-card-desc">异常波动 · 场景化投诉 · 舆情信号</span>
@@ -223,15 +224,15 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
             <span class="nav-card-badge critical">{{ warningCount }} 项预警</span>
           }
         </a>
-        <a class="nav-card" routerLink="/monitoring/category-voc">
+        <a class="nav-card" routerLink="/domestic/category-voc">
           <span class="nav-card-icon">📦</span>
           <span class="nav-card-title">品类分布</span>
           <span class="nav-card-desc">品类VOC密度 · 类目情绪排行</span>
         </a>
-        <a class="nav-card" routerLink="/monitoring/micro-trend">
-          <span class="nav-card-icon">🔥</span>
-          <span class="nav-card-title">微趋势 Top</span>
-          <span class="nav-card-desc">飙升卖点词 · 早期痛点预警</span>
+        <a class="nav-card" routerLink="/voc-insight/voice">
+          <span class="nav-card-icon"></span>
+          <span class="nav-card-title">用户声音分类</span>
+          <span class="nav-card-desc">情绪分类 · 评论样本 · 用户表达</span>
         </a>
       </div>
     </div>
@@ -351,7 +352,7 @@ export class VocOverviewComponent implements OnInit, OnDestroy {
   topNegativeKeywords: KeywordCloud[] = [];
   designerRiskRanking: DesignerRiskRankingItem[] = [];
 
-  constructor(private monitoringService: MonitoringDataService) {}
+  constructor(private monitoringService: DomesticMonitoringViewService) {}
 
   ngOnInit(): void {
     this.loadData();

+ 1 - 1
src/modules/shared/components/empty-state/empty-state.component.ts

@@ -160,7 +160,7 @@ export class EmptyStateComponent {
     'plain': { icon: '📭', title: '暂无数据', description: '' },
     'card': { icon: '📭', title: '暂无数据', description: '' },
     'table': { icon: '📋', title: '暂无数据', description: '' },
-    'product-required': { icon: '🧭', title: '请先选择产品', description: '选择具体产品后查看该 ASIN 的分析结果。' },
+    'product-required': { icon: '🧭', title: '请先选择商品', description: '选择具体商品后查看对应的分析结果。' },
     'low-sample': { icon: '⚠️', title: '样本量不足', description: '当前筛选条件下样本量较小,分析结果可能不具统计意义。' }
   };
 

+ 13 - 5
src/modules/shared/components/filter-bar/filter-bar.component.ts

@@ -149,6 +149,7 @@ export class FilterBarComponent implements OnInit, OnChanges {
   @Input() showActions = true;
   @Input() collapsible = false;
   @Input() collapsed = false;
+  @Input() autoLoadOptions = true;
   @Input() summaryLabel = '筛选条件';
   @Input() set designerOptions(options: Array<MsqDropdownOption | string> | null | undefined) {
     if (options === undefined || options === null) {
@@ -310,12 +311,19 @@ export class FilterBarComponent implements OnInit, OnChanges {
 
   ngOnInit() {
     this.initializeFilters();
-    this.loadSiteOptions();
-    this.loadShops();
-    this.loadDimensionOptions();
-    if (this.enableCategory) {
-      this.loadCategoryTree();
+    if (this.autoLoadOptions) {
+      this.loadSiteOptions();
+      this.loadShops();
+      this.loadDimensionOptions();
+      if (this.enableCategory) {
+        this.loadCategoryTree();
+      }
+      return;
     }
+    this.shopsLoaded = true;
+    // Parent pages update loading and result state from this output. Defer the
+    // first emission so those updates do not run during the child's init check.
+    queueMicrotask(() => this.emitFilters());
   }
 
   ngOnChanges(changes: SimpleChanges) {

+ 20 - 4
src/modules/shared/components/header/header.component.html

@@ -3,7 +3,13 @@
     <div class="brand-container">
       <h1 class="brand-title">国内电商 VOC</h1>
     </div>
-    <button class="collapse-toggle" (click)="toggleCollapse()">
+    <button
+      class="collapse-toggle"
+      type="button"
+      [title]="isCollapsed ? '展开导航' : '收起导航'"
+      [attr.aria-label]="isCollapsed ? '展开导航' : '收起导航'"
+      [attr.aria-expanded]="!isCollapsed"
+      (click)="toggleCollapse()">
       <span class="toggle-icon" [innerHTML]="icon(isCollapsed ? 'panelLeftOpen' : 'panelLeftClose')"></span>
     </button>
   </div>
@@ -23,7 +29,7 @@
       (keydown)="onSearchKeydown($event)"
       (keydown.escape)="clearSearch()"
     />
-    <button class="search-clear" *ngIf="searchKeyword" (mousedown)="clearSearch(); $event.preventDefault()">×</button>
+    <button class="search-clear" type="button" title="清空搜索" aria-label="清空搜索" *ngIf="searchKeyword" (mousedown)="clearSearch(); $event.preventDefault()">×</button>
     <span class="search-loading" *ngIf="searchLoading" [innerHTML]="icon('loader')"></span>
 
     <!-- 搜索历史下拉 -->
@@ -82,7 +88,17 @@
   </div>
 
   <div class="header-right">
-    <div class="user-profile" (click)="toggleUserMenu()" [class.active]="showUserMenu">
+    <div
+      class="user-profile"
+      role="button"
+      tabindex="0"
+      title="账户菜单"
+      aria-label="账户菜单"
+      [attr.aria-expanded]="showUserMenu"
+      (click)="toggleUserMenu()"
+      (keydown.enter)="toggleUserMenu()"
+      (keydown.space)="$event.preventDefault(); toggleUserMenu()"
+      [class.active]="showUserMenu">
       <div class="avatar-circle">
         <span class="icon header-svg-icon" [innerHTML]="icon('gear')"></span>
       </div>
@@ -103,7 +119,7 @@
   <div class="modal">
     <div class="modal-header">
       <h3>修改密码</h3>
-      <button class="close-btn" (click)="closePasswordModal()">×</button>
+      <button class="close-btn" type="button" title="关闭" aria-label="关闭" (click)="closePasswordModal()">×</button>
     </div>
     <div class="modal-body">
       <div class="form-group">

+ 21 - 0
src/modules/shared/components/navigation/navigation.component.ts

@@ -82,10 +82,31 @@ export class NavigationComponent {
         { 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/market',
+      displayName: '市场与竞品',
+      icon: 'market',
+      requiresAuth: false,
+      expanded: true,
+      children: [
         { path: '/domestic/categories', displayName: '品类分析', icon: 'category', requiresAuth: false },
+        { path: '/domestic/category-voc', displayName: '类目 VOC', icon: 'category', requiresAuth: false },
         { path: '/domestic/competitors', displayName: '竞品关系', icon: 'competitor', requiresAuth: false },
       ],
     },
+    {
+      path: '/domestic/monitoring',
+      displayName: 'VOC 全局监测',
+      icon: 'monitoring',
+      requiresAuth: false,
+      expanded: true,
+      children: [
+        { path: '/domestic/voc-overview', displayName: 'VOC 趋势总览', icon: 'trend', requiresAuth: false },
+        { path: '/domestic/risk-alerts', displayName: '风险预警', icon: 'alert', requiresAuth: false },
+      ],
+    },
     {
       path: '/voc-insight',
       displayName: 'VOC 深度洞察',