Просмотр исходного кода

feat: migrate domestic product voc detail

gangvy 1 месяц назад
Родитель
Сommit
1b3224dcd8

+ 6 - 5
README.md

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

+ 3 - 2
TASKS.md

@@ -14,16 +14,17 @@
 - [x] 接通商品搜索、商品选择、VOC 页面商品上下文和真实零评论空状态
 - [x] 迁入经营概览、商品经营详情、品类分析和竞品关系页面
 - [x] 迁入 VOC 趋势总览、类目 VOC 和风险预警页面
+- [x] 迁入商品 VOC 详情,复用原详情布局、样式和共享组件,并接通六个内容标签
 - [x] 复用原 `PageHeader`、`SummaryMetricCard`、`ContentCard`、`DataTableShell`、`ProductIdentity`、`BoardTabs`、`NeuButton`、`LoadingSpinner` 和 `EmptyState`
 - [x] 将左侧导航整理为“经营分析”“市场与竞品”“VOC 全局监测”“VOC 深度洞察”和“数据接入状态”五组结构
 - [x] 将原 `FilterBar` 解耦为可关闭旧数据选项自动加载的外部受控模式
 - [x] 将 VOC 总览、类目 VOC、风险预警和数据状态页的卡片、页签、按钮统一回原共享组件
 - [x] 为数据状态、VOC 总览和类目 VOC 补齐共享加载态与错误态
-- [x] 接通商品列表到经营详情、经营详情到 VOC 的真实商品上下文
+- [x] 接通商品列表和经营详情到商品 VOC 详情的真实商品上下文
 - [x] 增加数据接入状态页
 - [x] 将 Parse 和电商中转默认地址改为同源路径
 - [x] 移除旧跨境线上域名和旧 Parse 二次登录逻辑
-- [x] 完成 13 个启用入口的桌面与移动端响应式验收
+- [x] 完成 14 个启用入口的桌面与移动端响应式验收
 - [x] 修复类目 VOC 首次加载的 Angular 变更检测错误
 - [x] 完成构建、单测和数据完整性校验
 

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

@@ -57,6 +57,11 @@ export const routes: Routes = [
         title: '国内商品 VOC',
         loadComponent: () => import('../modules/voc-insight/voc-insight.component').then((m) => m.VocInsightComponent),
       },
+      {
+        path: 'detail',
+        title: '商品 VOC 详情',
+        loadComponent: () => import('../modules/voc-insight/voc-insight-detail/domestic-voc-detail.component').then((m) => m.DomesticVocDetailComponent),
+      },
       {
         path: 'voice',
         title: '用户声音分类',

+ 3 - 1
src/modules/domestic-voc/product-operating-detail.component.ts

@@ -135,7 +135,9 @@ export class DomesticProductOperatingDetailComponent implements OnInit, OnDestro
   }
 
   goVoc(): void {
-    this.router.navigate(['/voc-insight/voice']);
+    this.router.navigate(['/voc-insight/detail'], {
+      queryParams: { productId: this.product?.productId || null },
+    });
   }
 
   goCompetitors(): void {

+ 273 - 0
src/modules/voc-insight/voc-insight-detail/domestic-voc-detail.component.html

@@ -0,0 +1,273 @@
+<div class="detail-container">
+  @if (isLoading) {
+    <div class="detail-state">
+      <app-loading-spinner
+        text="正在加载商品 VOC 详情..."
+        subtext="读取国内商品经营与评论数据"
+        size="lg"
+        variant="card"
+        tone="blue">
+      </app-loading-spinner>
+    </div>
+  }
+
+  @if (error && !isLoading) {
+    <div class="detail-state">
+      <app-error-state
+        title="商品详情暂不可用"
+        [description]="error"
+        [showBack]="true"
+        (retry)="loadProduct()"
+        (back)="backToList()">
+      </app-error-state>
+    </div>
+  }
+
+  @if (product && dataset && !isLoading) {
+    <app-page-header
+      title="商品 VOC 详情"
+      eyebrow="Product VOC Detail"
+      description="在原商品详情框架内统一查看经营指标、评论证据、改进建议与趋势。"
+      icon="🔬"
+      [badges]="[{ label: dataset.platform, tone: 'blue' }, { label: reviews.length ? '评论已接入' : '评论待接入', tone: reviews.length ? 'green' : 'amber' }]"
+      [meta]="[{ label: '商品ID', value: product.productId }, { label: '型号', value: product.model || '-' }]"
+      [lastUpdated]="date(dataset.generatedAt)"
+      [sampleSize]="reviews.length"
+      [confidence]="reviews.length ? 'medium' : 'low'">
+      <app-neu-button pageHeaderActions size="sm" (btnClick)="backToList()">← 返回商品列表</app-neu-button>
+      <app-neu-button pageHeaderActions size="sm" color="primary" (btnClick)="goBusinessDetail()">经营详情</app-neu-button>
+    </app-page-header>
+
+    <div class="own-product-detail-layout">
+      <aside class="own-product-rail">
+        <div class="rail-image">
+          <img src="assets/images/placeholder.svg" [alt]="product.title" />
+        </div>
+
+        <h2 class="rail-title">{{ product.title }}</h2>
+        <div class="rail-sub">商品ID:{{ product.productId }}</div>
+        <div class="rail-brand">{{ product.brand }}</div>
+
+        <div class="rail-badges">
+          <span class="rail-badge status active">自营商品</span>
+          <span class="rail-badge platform">{{ dataset.platform }}</span>
+          @if (product.category3) {
+            <span class="rail-badge category">{{ product.category3 }}</span>
+          }
+        </div>
+
+        <div class="rail-meta-list">
+          <div class="rail-meta-row"><span>型号</span><strong>{{ product.model || '-' }}</strong></div>
+          <div class="rail-meta-row"><span>品牌</span><strong>{{ product.brand }}</strong></div>
+          <div class="rail-meta-row"><span>数据周期</span><strong>{{ date(dataset.source.dateRange.start) }} 至 {{ date(dataset.source.dateRange.end) }}</strong></div>
+          <div class="rail-meta-row"><span>评论样本</span><strong>{{ number(reviews.length) }} 条</strong></div>
+          <div class="rail-meta-row"><span>竞品关系</span><strong>{{ number(product.relationCount) }} 条</strong></div>
+        </div>
+
+        <div class="rail-actions">
+          <app-neu-button size="sm" color="primary" (btnClick)="setActiveTab('reviews')">查看评论</app-neu-button>
+          <app-neu-button size="sm" (btnClick)="goCompetitors()">竞品关系</app-neu-button>
+        </div>
+      </aside>
+
+      <section class="own-product-main">
+        <div class="own-overview-card">
+          <div class="overview-copy">
+            <span class="overview-eyebrow">Domestic Product Overview</span>
+            <app-product-identity
+              [title]="product.title"
+              [asin]="product.productId"
+              [productSku]="product.model"
+              [subtitle]="product.category3 || product.category2 || product.category1"
+              skuLabel="型号"
+              identifierLabel="商品ID"
+              size="md"
+              [showImage]="false">
+            </app-product-identity>
+            <div class="overview-meta">
+              <span>{{ product.brand }}</span>
+              <span>{{ dataset.platform }}</span>
+              <span>{{ product.category2 || product.category1 }}</span>
+              <span>{{ product.relationCount }} 条竞品关系</span>
+            </div>
+          </div>
+          <div class="overview-price-block">
+            <span>平均成交单价</span>
+            <strong>{{ currency(product.summary.averageUnitPrice) }}</strong>
+            <small class="price-note">当前数据周期</small>
+          </div>
+          <div class="detail-tabs detail-tabs-shared">
+            <app-board-tabs
+              [tabs]="tabs"
+              [activeKey]="activeTab"
+              density="compact"
+              (tabChange)="setActiveTab($event)">
+            </app-board-tabs>
+          </div>
+        </div>
+
+        <div class="detail-tab-panels">
+          <section class="detail-tab-panel" [hidden]="activeTab !== 'overview'">
+            <div class="own-metrics-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]="number(product.summary.transactionCustomers) + ' 位成交客户'"></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.clickThroughRate)" icon="↗" tone="blue" [description]="number(product.summary.clicks) + ' 次点击'"></app-summary-metric-card>
+              <app-summary-metric-card label="退款金额" [value]="currency(product.summary.refundAmount)" icon="↩" [tone]="product.summary.refundToGmvRate >= 0.1 ? 'red' : 'amber'" [description]="percent(product.summary.refundToGmvRate) + ' 成交金额占比'"></app-summary-metric-card>
+              <app-summary-metric-card label="评论样本" [value]="number(reviews.length)" icon="▤" [tone]="reviews.length ? 'green' : 'default'" [description]="reviews.length ? '已接入真实评论' : '当前源文件未包含评论'"></app-summary-metric-card>
+            </div>
+
+            <div class="own-info-grid">
+              <app-content-card title="商品归属" subtitle="国内商品标准身份" tone="blue" density="compact">
+                <div class="info-list">
+                  <div class="info-row"><span>案例</span><strong>{{ dataset.caseName }}</strong></div>
+                  <div class="info-row"><span>平台</span><strong>{{ dataset.platform }}</strong></div>
+                  <div class="info-row"><span>商品ID</span><strong class="mono">{{ product.productId }}</strong></div>
+                  <div class="info-row"><span>型号</span><strong>{{ product.model || '-' }}</strong></div>
+                </div>
+              </app-content-card>
+
+              <app-content-card title="经营概况" subtitle="当前数据周期汇总" tone="green" density="compact">
+                <div class="info-list">
+                  <div class="info-row"><span>曝光</span><strong>{{ number(product.summary.impressions) }}</strong></div>
+                  <div class="info-row"><span>访客</span><strong>{{ number(product.summary.visitors) }}</strong></div>
+                  <div class="info-row"><span>加购</span><strong>{{ number(product.summary.cartUnits) }}</strong></div>
+                  <div class="info-row"><span>订单</span><strong>{{ number(product.summary.transactionOrders) }}</strong></div>
+                </div>
+              </app-content-card>
+
+              <app-content-card title="VOC 就绪度" subtitle="只展示已接通的真实能力" [tone]="reviews.length ? 'green' : 'amber'" density="compact">
+                <div class="info-list">
+                  <div class="info-row"><span>评论数据</span><strong>{{ reviews.length ? '已接入' : '待接入' }}</strong></div>
+                  <div class="info-row"><span>主题分析</span><strong>{{ reviews.length ? '可计算' : '等待评论' }}</strong></div>
+                  <div class="info-row"><span>评论明细</span><strong>{{ number(reviews.length) }} 条</strong></div>
+                  <div class="info-row"><span>经营趋势</span><strong>已接通</strong></div>
+                </div>
+              </app-content-card>
+            </div>
+          </section>
+
+          <section class="detail-tab-panel" [hidden]="activeTab !== 'voc'">
+            <div class="own-metrics-grid">
+              <app-summary-metric-card label="评论总数" [value]="number(reviews.length)" icon="▤" tone="blue" [sampleSize]="reviews.length"></app-summary-metric-card>
+              <app-summary-metric-card label="平均评分" [value]="reviews.length ? decimal(reviewSummary.average) : '-'" unit="/ 5" icon="★" tone="amber" [sampleSize]="reviews.length"></app-summary-metric-card>
+              <app-summary-metric-card label="正向评论" [value]="reviews.length ? percent(reviewSummary.positive) : '-'" icon="↑" tone="green" [sampleSize]="reviews.length"></app-summary-metric-card>
+            </div>
+
+            @if (reviews.length) {
+              <app-content-card title="评论情绪分布" subtitle="根据真实评分区间计算" tone="purple">
+                <div class="rating-distribution compact-distribution">
+                  <div class="rating-row"><span class="stars">正向(4-5 星)</span><span class="rating-bar"><span class="bar-fill positive" [style.width.%]="reviewSummary.positive * 100"></span></span><span class="rating-percent">{{ percent(reviewSummary.positive) }}</span></div>
+                  <div class="rating-row"><span class="stars">中性(3 星)</span><span class="rating-bar"><span class="bar-fill neutral" [style.width.%]="reviewSummary.neutral * 100"></span></span><span class="rating-percent">{{ percent(reviewSummary.neutral) }}</span></div>
+                  <div class="rating-row"><span class="stars">负向(1-2 星)</span><span class="rating-bar"><span class="bar-fill negative" [style.width.%]="reviewSummary.negative * 100"></span></span><span class="rating-percent">{{ percent(reviewSummary.negative) }}</span></div>
+                </div>
+              </app-content-card>
+            } @else {
+              <app-alert-card
+                title="VOC 分析框架已就绪,评论证据尚未接入"
+                description="当前德玛仕数据文件包含商品经营和竞品关系,不包含评论明细,因此不生成情绪、主题、痛点或满意度结论。"
+                primaryMetric="0 条评论"
+                icon="!"
+                tone="amber">
+              </app-alert-card>
+              <app-content-card title="评论 VOC" subtitle="接入评论后在此复用原系统的评分、主题和痛点分析布局" variant="dashed" tone="amber">
+                <app-empty-state
+                  variant="low-sample"
+                  title="暂无可分析的评论样本"
+                  description="页面结构和数据适配入口已经保留,后续数据源只需写入统一评论模型即可展示。">
+                </app-empty-state>
+              </app-content-card>
+            }
+          </section>
+
+          <section class="detail-tab-panel" [hidden]="activeTab !== 'reviews'">
+            <app-content-card
+              title="商品评论"
+              [subtitle]="reviews.length ? '共 ' + reviews.length + ' 条真实评论' : '当前数据源尚未提供评论明细'"
+              tone="blue">
+              @if (reviews.length) {
+                <div class="review-list">
+                  @for (review of reviews; track review.reviewId) {
+                    <app-review-card [review]="review"></app-review-card>
+                  }
+                </div>
+              } @else {
+                <app-empty-state
+                  variant="plain"
+                  icon="▤"
+                  title="暂无评论明细"
+                  description="经营数据和商品关系已可查看,评论采集能力将在数据源阶段接入。">
+                </app-empty-state>
+              }
+            </app-content-card>
+          </section>
+
+          <section class="detail-tab-panel" [hidden]="activeTab !== 'recommendations'">
+            <app-alert-card
+              title="当前建议仅基于经营指标"
+              description="评论样本为 0,以下内容不代表用户声音结论;接入评论后再补充痛点证据和产品迭代建议。"
+              icon="i"
+              tone="blue">
+            </app-alert-card>
+            <div class="recommendation-stack">
+              @for (item of recommendations; track item.title) {
+                <app-alert-card
+                  [title]="item.title"
+                  [description]="item.description"
+                  [primaryMetric]="item.metric"
+                  [icon]="item.icon"
+                  [tone]="item.tone">
+                </app-alert-card>
+              }
+            </div>
+          </section>
+
+          <section class="detail-tab-panel" [hidden]="activeTab !== 'trends'">
+            <app-content-card title="商品经营趋势" subtitle="每日成交金额与成交件数,来源于当前国内数据集" tone="blue">
+              @if (activeTab === 'trends') {
+                @if (product.trend.length) {
+                  <div echarts class="domestic-trend-chart" [options]="trendOptions"></div>
+                } @else {
+                  <app-empty-state title="暂无趋势数据" description="当前商品没有可展示的日度经营指标。"></app-empty-state>
+                }
+              }
+            </app-content-card>
+          </section>
+
+          <section class="detail-tab-panel" [hidden]="activeTab !== 'details'">
+            <div class="own-info-grid detail-info-grid">
+              <app-content-card title="商品档案" subtitle="业务标准字段" tone="blue">
+                <div class="info-list">
+                  <div class="info-row"><span>商品ID</span><strong class="mono">{{ product.productId }}</strong></div>
+                  <div class="info-row"><span>商品键</span><strong class="mono">{{ product.productKey }}</strong></div>
+                  <div class="info-row"><span>品牌</span><strong>{{ product.brand }}</strong></div>
+                  <div class="info-row"><span>型号</span><strong>{{ product.model || '-' }}</strong></div>
+                  <div class="info-row"><span>商品角色</span><strong>{{ product.role === 'own' ? '自营商品' : '竞品' }}</strong></div>
+                </div>
+              </app-content-card>
+
+              <app-content-card title="类目归属" subtitle="三级类目结构" tone="purple">
+                <div class="info-list">
+                  <div class="info-row"><span>一级类目</span><strong>{{ product.category1 || '-' }}</strong></div>
+                  <div class="info-row"><span>二级类目</span><strong>{{ product.category2 || '-' }}</strong></div>
+                  <div class="info-row"><span>三级类目</span><strong>{{ product.category3 || '-' }}</strong></div>
+                  <div class="info-row"><span>竞品关系</span><strong>{{ product.relationCount }} 条</strong></div>
+                </div>
+              </app-content-card>
+
+              <app-content-card title="数据来源" subtitle="可追溯的数据口径" tone="green">
+                <div class="info-list">
+                  <div class="info-row"><span>案例</span><strong>{{ dataset.caseName }}</strong></div>
+                  <div class="info-row"><span>平台</span><strong>{{ dataset.platform }}</strong></div>
+                  <div class="info-row"><span>源文件</span><strong>{{ dataset.source.sourceFile }}</strong></div>
+                  <div class="info-row"><span>数据周期</span><strong>{{ date(dataset.source.dateRange.start) }} 至 {{ date(dataset.source.dateRange.end) }}</strong></div>
+                  <div class="info-row"><span>生成时间</span><strong>{{ date(dataset.generatedAt) }}</strong></div>
+                </div>
+              </app-content-card>
+            </div>
+          </section>
+        </div>
+      </section>
+    </div>
+  }
+</div>

+ 112 - 0
src/modules/voc-insight/voc-insight-detail/domestic-voc-detail.component.scss

@@ -0,0 +1,112 @@
+:host {
+  display: block;
+  min-width: 0;
+}
+
+.detail-state {
+  min-height: 520px;
+  display: grid;
+  place-items: center;
+}
+
+.rail-badge.platform {
+  background: #dbeafe;
+  color: #2563eb;
+}
+
+.rail-badge.category {
+  max-width: 100%;
+  background: #f3e8ff;
+  color: #7e22ce;
+  line-height: 1.3;
+}
+
+.rail-actions app-neu-button {
+  display: block;
+  min-width: 0;
+}
+
+.overview-copy app-product-identity {
+  display: block;
+  margin: 6px 0 10px;
+}
+
+.overview-price-block .price-note {
+  text-decoration: none;
+}
+
+.detail-tabs-shared {
+  display: block;
+  width: 100%;
+  padding: 0;
+  border: 0;
+  border-radius: 0;
+  background: transparent;
+  overflow: visible;
+}
+
+.detail-tabs-shared app-board-tabs {
+  display: block;
+  max-width: 100%;
+}
+
+.own-metrics-grid app-summary-metric-card,
+.own-info-grid app-content-card {
+  display: block;
+  min-width: 0;
+}
+
+.compact-distribution {
+  max-width: 760px;
+}
+
+.compact-distribution .bar-fill.positive {
+  background: #059669;
+}
+
+.compact-distribution .bar-fill.neutral {
+  background: #d97706;
+}
+
+.compact-distribution .bar-fill.negative {
+  background: #dc2626;
+}
+
+.review-list,
+.recommendation-stack {
+  display: grid;
+  gap: 12px;
+}
+
+.review-list {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.domestic-trend-chart {
+  width: 100%;
+  height: 360px;
+}
+
+.detail-info-grid {
+  align-items: stretch;
+}
+
+@media (max-width: 960px) {
+  .review-list {
+    grid-template-columns: 1fr;
+  }
+}
+
+@media (max-width: 768px) {
+  .detail-state {
+    min-height: 420px;
+  }
+
+  .domestic-trend-chart {
+    height: 300px;
+  }
+
+  .detail-tabs-shared {
+    overflow-x: auto;
+  }
+}

+ 327 - 0
src/modules/voc-insight/voc-insight-detail/domestic-voc-detail.component.ts

@@ -0,0 +1,327 @@
+import { CommonModule } from '@angular/common';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import type { EChartsOption } from 'echarts';
+import { NgxEchartsModule } from 'ngx-echarts';
+import { Subscription, combineLatest } from 'rxjs';
+import {
+  DomesticDataset,
+  DomesticProduct,
+  DomesticReview,
+} from '../../../app/core/models/domestic.models';
+import { DomesticDatasetService } from '../../../app/core/services/domestic-dataset.service';
+import { AlertCardComponent } from '../../shared/components/alert-card/alert-card.component';
+import { BoardTabItem, BoardTabsComponent } from '../../shared/components/board-tabs/board-tabs.component';
+import { ContentCardComponent } from '../../shared/components/content-card/content-card.component';
+import { EmptyStateComponent } from '../../shared/components/empty-state/empty-state.component';
+import { ErrorStateComponent } from '../../shared/components/error-state/error-state.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 { ReviewCardComponent } from '../../shared/components/review-card/review-card.component';
+import { SummaryMetricCardComponent } from '../../shared/components/summary-metric-card/summary-metric-card.component';
+import { VocInsightStateService } from '../../shared/services/voc-insight-state.service';
+
+type DetailTab = 'overview' | 'voc' | 'reviews' | 'recommendations' | 'trends' | 'details';
+
+interface OperatingRecommendation {
+  title: string;
+  description: string;
+  metric: string;
+  icon: string;
+  tone: 'blue' | 'green' | 'amber' | 'red' | 'neutral';
+}
+
+@Component({
+  selector: 'app-domestic-voc-detail',
+  standalone: true,
+  imports: [
+    CommonModule,
+    NgxEchartsModule,
+    AlertCardComponent,
+    BoardTabsComponent,
+    ContentCardComponent,
+    EmptyStateComponent,
+    ErrorStateComponent,
+    LoadingSpinnerComponent,
+    NeuButtonComponent,
+    PageHeaderComponent,
+    ProductIdentityComponent,
+    ReviewCardComponent,
+    SummaryMetricCardComponent,
+  ],
+  templateUrl: './domestic-voc-detail.component.html',
+  styleUrls: [
+    './voc-insight-detail.component.scss',
+    './domestic-voc-detail.component.scss',
+  ],
+})
+export class DomesticVocDetailComponent implements OnInit, OnDestroy {
+  private loadSubscription?: Subscription;
+
+  isLoading = true;
+  error = '';
+  dataset: DomesticDataset | null = null;
+  product: DomesticProduct | null = null;
+  reviews: DomesticReview[] = [];
+  activeTab: DetailTab = 'overview';
+  trendOptions: EChartsOption = {};
+
+  tabs: BoardTabItem[] = this.buildTabs(0);
+
+  constructor(
+    private readonly route: ActivatedRoute,
+    private readonly router: Router,
+    private readonly datasetService: DomesticDatasetService,
+    private readonly stateService: VocInsightStateService,
+  ) {}
+
+  ngOnInit(): void {
+    this.loadProduct();
+  }
+
+  ngOnDestroy(): void {
+    this.loadSubscription?.unsubscribe();
+  }
+
+  loadProduct(): void {
+    this.loadSubscription?.unsubscribe();
+    this.isLoading = true;
+    this.error = '';
+
+    this.loadSubscription = combineLatest([
+      this.route.queryParamMap,
+      this.datasetService.dataset$,
+    ]).subscribe({
+      next: ([queryParams, dataset]) => {
+        const requestedId = queryParams.get('productId') || queryParams.get('asin') || '';
+        const contextId = this.stateService.currentAsin;
+        const fallback = [...dataset.products]
+          .filter((item) => item.role === 'own')
+          .sort((a, b) => b.summary.gmv - a.summary.gmv)[0] || dataset.products[0];
+        const productId = requestedId || contextId || fallback?.productId || '';
+        const product = dataset.products.find((item) => item.productId === productId);
+
+        if (!product && requestedId) {
+          this.dataset = dataset;
+          this.product = null;
+          this.reviews = [];
+          this.error = `未找到商品 ${requestedId},请返回商品列表重新选择。`;
+          this.isLoading = false;
+          return;
+        }
+
+        this.dataset = dataset;
+        this.product = product || fallback || null;
+        this.reviews = this.product
+          ? (dataset.reviews ?? []).filter((review) => review.productId === this.product?.productId)
+          : [];
+        this.tabs = this.buildTabs(this.reviews.length);
+        this.activeTab = this.toTab(queryParams.get('tab'));
+
+        if (this.product) {
+          this.syncProductContext(this.product);
+          this.trendOptions = this.buildTrendOptions(this.product);
+        } else {
+          this.error = '当前数据集中没有可查看的商品。';
+        }
+        this.isLoading = false;
+      },
+      error: () => {
+        this.product = null;
+        this.reviews = [];
+        this.error = '商品详情数据加载失败,请检查本地数据文件后重试。';
+        this.isLoading = false;
+      },
+    });
+  }
+
+  setActiveTab(key: string): void {
+    this.activeTab = this.toTab(key);
+  }
+
+  backToList(): void {
+    this.router.navigate(['/voc-insight/products']);
+  }
+
+  goBusinessDetail(): void {
+    this.router.navigate(['/domestic/product'], {
+      queryParams: { productId: this.product?.productId || null },
+    });
+  }
+
+  goCompetitors(): void {
+    this.router.navigate(['/domestic/competitors'], {
+      queryParams: { productId: this.product?.productId || null },
+    });
+  }
+
+  get reviewSummary(): { average: number; positive: number; neutral: number; negative: number } {
+    if (!this.reviews.length) {
+      return { average: 0, positive: 0, neutral: 0, negative: 0 };
+    }
+    const total = this.reviews.length;
+    const average = this.reviews.reduce((sum, review) => sum + Number(review.rating || 0), 0) / total;
+    return {
+      average,
+      positive: this.reviews.filter((review) => review.rating >= 4).length / total,
+      neutral: this.reviews.filter((review) => review.rating === 3).length / total,
+      negative: this.reviews.filter((review) => review.rating <= 2).length / total,
+    };
+  }
+
+  get recommendations(): OperatingRecommendation[] {
+    if (!this.product) return [];
+    const summary = this.product.summary;
+    const items: OperatingRecommendation[] = [];
+
+    if (summary.refundToGmvRate >= 0.1) {
+      items.push({
+        title: '优先核查退款原因',
+        description: '退款金额占比达到两位数。评论明细尚未接入,先从订单售后、型号说明和履约记录中定位原因。',
+        metric: this.percent(summary.refundToGmvRate),
+        icon: '↩',
+        tone: 'red',
+      });
+    } else {
+      items.push({
+        title: '保持退款指标监控',
+        description: '当前退款金额占比未触发高风险阈值,建议随数据周期持续复核。',
+        metric: this.percent(summary.refundToGmvRate),
+        icon: '✓',
+        tone: 'green',
+      });
+    }
+
+    items.push({
+      title: '复核流量到成交的转化路径',
+      description: '结合访客、加购和成交订单继续排查详情页信息完整度与商品卖点表达。',
+      metric: this.percent(summary.conversionRate),
+      icon: '◎',
+      tone: summary.conversionRate < 0.06 ? 'amber' : 'blue',
+    });
+
+    items.push({
+      title: this.product.relationCount ? '纳入竞品定期对比' : '补充竞品映射',
+      description: this.product.relationCount
+        ? '当前商品已有竞品关系,可按同类商品持续对比成交、价格和后续评论主题。'
+        : '当前商品没有竞品关系,建议先补充同类竞品后再开展横向 VOC 分析。',
+      metric: `${this.product.relationCount} 条关系`,
+      icon: '↔',
+      tone: this.product.relationCount ? 'blue' : 'amber',
+    });
+
+    return items;
+  }
+
+  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 new Intl.NumberFormat('zh-CN', {
+      style: 'percent',
+      minimumFractionDigits: 1,
+      maximumFractionDigits: 1,
+    }).format(value || 0);
+  }
+
+  decimal(value: number): string {
+    return Number(value || 0).toFixed(1);
+  }
+
+  date(value?: string): string {
+    if (!value) return '-';
+    const parsed = new Date(value);
+    return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString('zh-CN');
+  }
+
+  private syncProductContext(product: DomesticProduct): void {
+    if (this.stateService.currentAsin === product.productId) return;
+    this.stateService.setProduct({
+      asin: product.productId,
+      title: product.title,
+      photo: 'assets/images/placeholder.svg',
+      reviewCount: this.reviews.length,
+      rating: this.reviewSummary.average,
+      productSku: product.model,
+      sellerSku: product.productId,
+      storeName: this.dataset?.caseName || product.brand,
+      itemStatus: 1,
+    });
+  }
+
+  private buildTabs(reviewCount: number): BoardTabItem[] {
+    return [
+      { key: 'overview', label: '概览', icon: '▦' },
+      { key: 'voc', label: 'VOC', icon: '◉', badges: [{ label: reviewCount, tone: reviewCount ? 'green' : 'default' }] },
+      { key: 'reviews', label: '评论', icon: '▤', badges: [{ label: reviewCount, tone: reviewCount ? 'blue' : 'default' }] },
+      { key: 'recommendations', label: '建议', icon: '✓' },
+      { key: 'trends', label: '趋势', icon: '↗' },
+      { key: 'details', label: '详情', icon: '≡' },
+    ];
+  }
+
+  private toTab(value: string | null): DetailTab {
+    const allowed: DetailTab[] = ['overview', 'voc', 'reviews', 'recommendations', 'trends', 'details'];
+    return allowed.includes(value as DetailTab) ? value as DetailTab : 'overview';
+  }
+
+  private buildTrendOptions(product: DomesticProduct): EChartsOption {
+    return {
+      animationDuration: 450,
+      color: ['#2563eb', '#059669'],
+      tooltip: { trigger: 'axis' },
+      legend: { data: ['成交金额', '成交件数'], top: 0, right: 0 },
+      grid: { left: 16, right: 18, top: 48, bottom: 12, containLabel: true },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: product.trend.map((item) => item.date.slice(5)),
+        axisLine: { lineStyle: { color: '#cbd5e1' } },
+        axisLabel: { color: '#64748b' },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '金额(元)',
+          axisLabel: { color: '#64748b' },
+          splitLine: { lineStyle: { color: '#eef2f7' } },
+        },
+        {
+          type: 'value',
+          name: '件数',
+          axisLabel: { color: '#64748b' },
+          splitLine: { show: false },
+        },
+      ],
+      series: [
+        {
+          name: '成交金额',
+          type: 'line',
+          smooth: true,
+          symbolSize: 7,
+          data: product.trend.map((item) => item.gmv),
+          areaStyle: { color: 'rgba(37, 99, 235, 0.08)' },
+        },
+        {
+          name: '成交件数',
+          type: 'line',
+          smooth: true,
+          yAxisIndex: 1,
+          symbolSize: 7,
+          data: product.trend.map((item) => item.soldUnits),
+        },
+      ],
+    };
+  }
+}

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

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