topic-explorer.component.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <main class="topic-page">
  2. <app-page-header
  3. title="主题探索"
  4. eyebrow="洞察中心"
  5. description="在同一工作台查看痛点、功能、场景和情绪,并进入主题独立页查看证据。"
  6. [crumbs]="crumbs">
  7. </app-page-header>
  8. <app-page-toolbar [fields]="toolbarFields">
  9. <div pageToolbarActions>
  10. <button class="icon-button" type="button" title="刷新主题数据" aria-label="刷新主题数据"
  11. [disabled]="refreshing" (click)="refresh()">
  12. <lucide-icon name="refresh-cw" [size]="16" [class.is-spinning]="refreshing"></lucide-icon>
  13. </button>
  14. </div>
  15. </app-page-toolbar>
  16. <section class="state-panel loading-state" *ngIf="loading">
  17. <span class="spinner"></span>
  18. <div><strong>正在构建真实主题索引</strong><p>读取评价正文、商品关系和现有 VOC 视图。</p></div>
  19. </section>
  20. <section class="state-panel error-state" *ngIf="!loading && errorMessage">
  21. <lucide-icon name="circle-alert" [size]="20"></lucide-icon>
  22. <div><strong>主题数据加载失败</strong><p>{{ errorMessage }}</p></div>
  23. <button class="secondary-button" type="button" (click)="refresh()">重试</button>
  24. </section>
  25. <ng-container *ngIf="!loading && !errorMessage && snapshot as data">
  26. <section class="availability-band"
  27. [class.is-warning]="data.meaningfulReviewCount === 0 || data.ownReviewCount === 0">
  28. <div class="availability-main">
  29. <lucide-icon [name]="data.meaningfulReviewCount ? 'database' : 'circle-alert'" [size]="19"></lucide-icon>
  30. <div>
  31. <strong *ngIf="!data.rawReviewCount">评价正文未接入</strong>
  32. <strong *ngIf="data.rawReviewCount && !data.meaningfulReviewCount">当前范围没有有效正文</strong>
  33. <strong *ngIf="data.meaningfulReviewCount && !data.ownReviewCount">当前主题仅有竞品侧证据</strong>
  34. <strong *ngIf="data.meaningfulReviewCount && data.ownReviewCount">当前主题证据可追溯</strong>
  35. <p *ngIf="data.meaningfulReviewCount && !data.ownReviewCount">
  36. 本品评价为 0,当前结论不能代表本品用户体验;所有主题均可回看竞品原文。
  37. </p>
  38. <p *ngIf="data.meaningfulReviewCount && data.ownReviewCount">
  39. 主题由当前数据集的有效评价正文聚合;置信等级只反映样本数量,不代表因果结论。
  40. </p>
  41. <p *ngIf="!data.meaningfulReviewCount">页面保留真实空状态,不使用演示主题补齐。</p>
  42. </div>
  43. </div>
  44. <div class="availability-facts">
  45. <span><b>{{ data.meaningfulReviewCount }}</b> 有效正文</span>
  46. <span><b>{{ data.ownReviewCount }}</b> 本品评价</span>
  47. <span><b>{{ data.competitorReviewCount }}</b> 竞品评价</span>
  48. <span>经营周期 <b>{{ data.periodStart }}</b> 至 {{ data.periodEnd }}</span>
  49. </div>
  50. </section>
  51. <section class="metric-strip" aria-label="主题数据摘要">
  52. <div class="metric-cell">
  53. <span>可见主题</span>
  54. <strong>{{ visibleTopics.length }}</strong>
  55. <small>当前筛选结果</small>
  56. </div>
  57. <div class="metric-cell">
  58. <span>有效评价</span>
  59. <strong>{{ data.meaningfulReviewCount }}</strong>
  60. <small>已排除无效占位文本</small>
  61. </div>
  62. <div class="metric-cell">
  63. <span>影响商品</span>
  64. <strong>{{ data.affectedProductCount }}</strong>
  65. <small>当前来源范围去重</small>
  66. </div>
  67. <div class="metric-cell">
  68. <span>场景聚类</span>
  69. <strong>{{ data.scenarioClusterCount }}</strong>
  70. <small>现有 VOC view service</small>
  71. </div>
  72. </section>
  73. <nav class="view-tabs" aria-label="主题视图">
  74. <button *ngFor="let view of views" type="button" [class.active]="activeView === view.id"
  75. [attr.aria-current]="activeView === view.id ? 'page' : null" (click)="setView(view.id)">
  76. <span>{{ view.label }}</span><b>{{ viewCount(view.id) }}</b>
  77. </button>
  78. </nav>
  79. <section class="explorer-toolbar">
  80. <div class="view-context">
  81. <strong>{{ currentView.label }}</strong>
  82. <span>{{ currentView.description }}</span>
  83. </div>
  84. <div class="filter-controls">
  85. <label class="search-control">
  86. <lucide-icon name="search" [size]="15"></lucide-icon>
  87. <input type="search" [(ngModel)]="query" (ngModelChange)="applyFilters()" placeholder="搜索主题或商品"
  88. aria-label="搜索主题或商品">
  89. <button *ngIf="query" type="button" aria-label="清除搜索" title="清除搜索" (click)="query=''; applyFilters()">
  90. <lucide-icon name="x" [size]="14"></lucide-icon>
  91. </button>
  92. </label>
  93. <label class="select-control">
  94. <span>来源</span>
  95. <select [(ngModel)]="sourceScope" (ngModelChange)="rebuildForSource()" aria-label="评价来源">
  96. <option *ngFor="let option of sourceOptions" [value]="option.value">{{ option.label }}</option>
  97. </select>
  98. </label>
  99. <label class="select-control">
  100. <span>可用性</span>
  101. <select [(ngModel)]="availabilityFilter" (ngModelChange)="applyFilters()" aria-label="主题可用性">
  102. <option *ngFor="let option of availabilityOptions" [value]="option.value">{{ option.label }}</option>
  103. </select>
  104. </label>
  105. <label class="select-control">
  106. <span>排序</span>
  107. <select [(ngModel)]="sortBy" (ngModelChange)="applyFilters()" aria-label="主题排序">
  108. <option *ngFor="let option of sortOptions" [value]="option.value">{{ option.label }}</option>
  109. </select>
  110. </label>
  111. <button class="filter-reset" type="button" (click)="clearFilters()" title="重置搜索、可用性和排序">
  112. <lucide-icon name="list-filter" [size]="15"></lucide-icon><span>重置</span>
  113. </button>
  114. </div>
  115. </section>
  116. <section class="topic-workbench" [class.has-detail]="selectedTopic">
  117. <div class="topic-list-panel">
  118. <div class="table-scroll" *ngIf="visibleTopics.length; else emptyTopics">
  119. <table class="topic-table">
  120. <thead>
  121. <tr>
  122. <th>主题</th>
  123. <th>证据</th>
  124. <th>影响商品</th>
  125. <th>趋势</th>
  126. <th>置信 / 可用性</th>
  127. <th><span class="visually-hidden">操作</span></th>
  128. </tr>
  129. </thead>
  130. <tbody>
  131. <tr
  132. *ngFor="let topic of visibleTopics; trackBy: trackTopic"
  133. class="is-clickable"
  134. [class.selected]="topic.id === selectedTopicId"
  135. (click)="selectTopic(topic)">
  136. <td class="topic-cell">
  137. <button type="button" class="topic-link" (click)="selectTopic(topic)">
  138. <span class="topic-name">{{ topic.label }}</span>
  139. <span class="topic-description">{{ topic.description }}</span>
  140. <span class="topic-views">
  141. <em *ngFor="let view of topic.views">{{ view === 'pain' ? '痛点' : view === 'feature' ? '功能' : view === 'scenario' ? '场景' : '情绪' }}</em>
  142. </span>
  143. </button>
  144. </td>
  145. <td>
  146. <div class="number-cell"><strong>{{ topic.evidenceCount }}</strong><span>{{ topic.share | percent:'1.0-1' }} 有效正文</span></div>
  147. </td>
  148. <td>
  149. <div class="number-cell"><strong>{{ topic.productCount }}</strong><span>{{ topProductLabel(topic) }}</span></div>
  150. </td>
  151. <td>
  152. <div class="trend-cell" *ngIf="topic.trend.length; else noTrend">
  153. <div class="micro-bars" aria-hidden="true">
  154. <i *ngFor="let point of topic.trend | slice:-10; trackBy: trackTrend"
  155. [style.height.%]="trendBarHeight(point, topic.trend)"></i>
  156. </div>
  157. <span [class]="'trend-' + topic.trendDirection">{{ trendLabel(topic) }}</span>
  158. </div>
  159. <ng-template #noTrend><span class="muted-value">日期不足</span></ng-template>
  160. </td>
  161. <td>
  162. <div class="status-stack">
  163. <span class="confidence-status" [class]="'confidence-' + topic.confidence">{{ topic.confidenceLabel }}</span>
  164. <span class="availability-status" [class.limited]="topic.availability === 'limited'">{{ topic.availabilityLabel }}</span>
  165. </div>
  166. </td>
  167. <td class="row-actions">
  168. <button class="evidence-button" type="button" [disabled]="!topic.evidence.length"
  169. title="进入反馈收件箱审阅首条证据" (click)="openEvidence(topic, $event)">
  170. <lucide-icon name="arrow-up-right" [size]="15"></lucide-icon><span>审阅</span>
  171. </button>
  172. <button class="row-open" type="button" title="进入完整主题详情页" aria-label="进入完整主题详情页"
  173. (click)="openDetailPage(topic); $event.stopPropagation()">
  174. <span>进入</span>
  175. <lucide-icon name="chevron-right" [size]="16"></lucide-icon>
  176. </button>
  177. </td>
  178. </tr>
  179. </tbody>
  180. </table>
  181. </div>
  182. <ng-template #emptyTopics>
  183. <div class="empty-state">
  184. <span class="empty-icon"><lucide-icon name="message-square" [size]="23"></lucide-icon></span>
  185. <strong>{{ emptyTitle }}</strong>
  186. <p>{{ emptyDescription }}</p>
  187. <button *ngIf="query || availabilityFilter !== 'all'" class="secondary-button" type="button" (click)="clearFilters()">清除筛选</button>
  188. </div>
  189. </ng-template>
  190. <footer class="table-footer">
  191. <span>显示 {{ visibleTopics.length }} 个主题</span>
  192. <span>置信等级按同主题证据量分层:1 / 3 / 8 / 20 条</span>
  193. </footer>
  194. </div>
  195. <aside class="detail-panel" *ngIf="selectedTopic as topic; else selectionPlaceholder">
  196. <app-topic-detail [topic]="topic" (close)="closeDetail()"></app-topic-detail>
  197. </aside>
  198. <ng-template #selectionPlaceholder>
  199. <aside class="selection-placeholder">
  200. <span><lucide-icon name="message-square" [size]="20"></lucide-icon></span>
  201. <strong>选择一个主题查看详情</strong>
  202. <p>点击左侧任一主题后,这里会展开趋势、证据、影响商品和原始评价。</p>
  203. </aside>
  204. </ng-template>
  205. </section>
  206. <footer class="data-boundary">
  207. <lucide-icon name="database" [size]="15"></lucide-icon>
  208. <span>主题只覆盖当前已接入的 {{ data.rawReviewCount }} 条评价;场景结果必须由现有 VOC 服务返回并匹配原文后才展示。</span>
  209. </footer>
  210. </ng-container>
  211. </main>