| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- <main class="topic-page">
- <app-page-header
- title="主题探索"
- eyebrow="洞察中心"
- description="在同一工作台查看痛点、功能、场景和情绪,并进入主题独立页查看证据。"
- [crumbs]="crumbs">
- </app-page-header>
- <app-page-toolbar [fields]="toolbarFields">
- <div pageToolbarActions>
- <button class="icon-button" type="button" title="刷新主题数据" aria-label="刷新主题数据"
- [disabled]="refreshing" (click)="refresh()">
- <lucide-icon name="refresh-cw" [size]="16" [class.is-spinning]="refreshing"></lucide-icon>
- </button>
- </div>
- </app-page-toolbar>
- <section class="state-panel loading-state" *ngIf="loading">
- <span class="spinner"></span>
- <div><strong>正在构建真实主题索引</strong><p>读取评价正文、商品关系和现有 VOC 视图。</p></div>
- </section>
- <section class="state-panel error-state" *ngIf="!loading && errorMessage">
- <lucide-icon name="circle-alert" [size]="20"></lucide-icon>
- <div><strong>主题数据加载失败</strong><p>{{ errorMessage }}</p></div>
- <button class="secondary-button" type="button" (click)="refresh()">重试</button>
- </section>
- <ng-container *ngIf="!loading && !errorMessage && snapshot as data">
- <section class="availability-band"
- [class.is-warning]="data.meaningfulReviewCount === 0 || data.ownReviewCount === 0">
- <div class="availability-main">
- <lucide-icon [name]="data.meaningfulReviewCount ? 'database' : 'circle-alert'" [size]="19"></lucide-icon>
- <div>
- <strong *ngIf="!data.rawReviewCount">评价正文未接入</strong>
- <strong *ngIf="data.rawReviewCount && !data.meaningfulReviewCount">当前范围没有有效正文</strong>
- <strong *ngIf="data.meaningfulReviewCount && !data.ownReviewCount">当前主题仅有竞品侧证据</strong>
- <strong *ngIf="data.meaningfulReviewCount && data.ownReviewCount">当前主题证据可追溯</strong>
- <p *ngIf="data.meaningfulReviewCount && !data.ownReviewCount">
- 本品评价为 0,当前结论不能代表本品用户体验;所有主题均可回看竞品原文。
- </p>
- <p *ngIf="data.meaningfulReviewCount && data.ownReviewCount">
- 主题由当前数据集的有效评价正文聚合;置信等级只反映样本数量,不代表因果结论。
- </p>
- <p *ngIf="!data.meaningfulReviewCount">页面保留真实空状态,不使用演示主题补齐。</p>
- </div>
- </div>
- <div class="availability-facts">
- <span><b>{{ data.meaningfulReviewCount }}</b> 有效正文</span>
- <span><b>{{ data.ownReviewCount }}</b> 本品评价</span>
- <span><b>{{ data.competitorReviewCount }}</b> 竞品评价</span>
- <span>经营周期 <b>{{ data.periodStart }}</b> 至 {{ data.periodEnd }}</span>
- </div>
- </section>
- <section class="metric-strip" aria-label="主题数据摘要">
- <div class="metric-cell">
- <span>可见主题</span>
- <strong>{{ visibleTopics.length }}</strong>
- <small>当前筛选结果</small>
- </div>
- <div class="metric-cell">
- <span>有效评价</span>
- <strong>{{ data.meaningfulReviewCount }}</strong>
- <small>已排除无效占位文本</small>
- </div>
- <div class="metric-cell">
- <span>影响商品</span>
- <strong>{{ data.affectedProductCount }}</strong>
- <small>当前来源范围去重</small>
- </div>
- <div class="metric-cell">
- <span>场景聚类</span>
- <strong>{{ data.scenarioClusterCount }}</strong>
- <small>现有 VOC view service</small>
- </div>
- </section>
- <nav class="view-tabs" aria-label="主题视图">
- <button *ngFor="let view of views" type="button" [class.active]="activeView === view.id"
- [attr.aria-current]="activeView === view.id ? 'page' : null" (click)="setView(view.id)">
- <span>{{ view.label }}</span><b>{{ viewCount(view.id) }}</b>
- </button>
- </nav>
- <section class="explorer-toolbar">
- <div class="view-context">
- <strong>{{ currentView.label }}</strong>
- <span>{{ currentView.description }}</span>
- </div>
- <div class="filter-controls">
- <label class="search-control">
- <lucide-icon name="search" [size]="15"></lucide-icon>
- <input type="search" [(ngModel)]="query" (ngModelChange)="applyFilters()" placeholder="搜索主题或商品"
- aria-label="搜索主题或商品">
- <button *ngIf="query" type="button" aria-label="清除搜索" title="清除搜索" (click)="query=''; applyFilters()">
- <lucide-icon name="x" [size]="14"></lucide-icon>
- </button>
- </label>
- <label class="select-control">
- <span>来源</span>
- <select [(ngModel)]="sourceScope" (ngModelChange)="rebuildForSource()" aria-label="评价来源">
- <option *ngFor="let option of sourceOptions" [value]="option.value">{{ option.label }}</option>
- </select>
- </label>
- <label class="select-control">
- <span>可用性</span>
- <select [(ngModel)]="availabilityFilter" (ngModelChange)="applyFilters()" aria-label="主题可用性">
- <option *ngFor="let option of availabilityOptions" [value]="option.value">{{ option.label }}</option>
- </select>
- </label>
- <label class="select-control">
- <span>排序</span>
- <select [(ngModel)]="sortBy" (ngModelChange)="applyFilters()" aria-label="主题排序">
- <option *ngFor="let option of sortOptions" [value]="option.value">{{ option.label }}</option>
- </select>
- </label>
- <button class="filter-reset" type="button" (click)="clearFilters()" title="重置搜索、可用性和排序">
- <lucide-icon name="list-filter" [size]="15"></lucide-icon><span>重置</span>
- </button>
- </div>
- </section>
- <section class="topic-workbench" [class.has-detail]="selectedTopic">
- <div class="topic-list-panel">
- <div class="table-scroll" *ngIf="visibleTopics.length; else emptyTopics">
- <table class="topic-table">
- <thead>
- <tr>
- <th>主题</th>
- <th>证据</th>
- <th>影响商品</th>
- <th>趋势</th>
- <th>置信 / 可用性</th>
- <th><span class="visually-hidden">操作</span></th>
- </tr>
- </thead>
- <tbody>
- <tr
- *ngFor="let topic of visibleTopics; trackBy: trackTopic"
- class="is-clickable"
- [class.selected]="topic.id === selectedTopicId"
- (click)="selectTopic(topic)">
- <td class="topic-cell">
- <button type="button" class="topic-link" (click)="selectTopic(topic)">
- <span class="topic-name">{{ topic.label }}</span>
- <span class="topic-description">{{ topic.description }}</span>
- <span class="topic-views">
- <em *ngFor="let view of topic.views">{{ view === 'pain' ? '痛点' : view === 'feature' ? '功能' : view === 'scenario' ? '场景' : '情绪' }}</em>
- </span>
- </button>
- </td>
- <td>
- <div class="number-cell"><strong>{{ topic.evidenceCount }}</strong><span>{{ topic.share | percent:'1.0-1' }} 有效正文</span></div>
- </td>
- <td>
- <div class="number-cell"><strong>{{ topic.productCount }}</strong><span>{{ topProductLabel(topic) }}</span></div>
- </td>
- <td>
- <div class="trend-cell" *ngIf="topic.trend.length; else noTrend">
- <div class="micro-bars" aria-hidden="true">
- <i *ngFor="let point of topic.trend | slice:-10; trackBy: trackTrend"
- [style.height.%]="trendBarHeight(point, topic.trend)"></i>
- </div>
- <span [class]="'trend-' + topic.trendDirection">{{ trendLabel(topic) }}</span>
- </div>
- <ng-template #noTrend><span class="muted-value">日期不足</span></ng-template>
- </td>
- <td>
- <div class="status-stack">
- <span class="confidence-status" [class]="'confidence-' + topic.confidence">{{ topic.confidenceLabel }}</span>
- <span class="availability-status" [class.limited]="topic.availability === 'limited'">{{ topic.availabilityLabel }}</span>
- </div>
- </td>
- <td class="row-actions">
- <button class="evidence-button" type="button" [disabled]="!topic.evidence.length"
- title="进入反馈收件箱审阅首条证据" (click)="openEvidence(topic, $event)">
- <lucide-icon name="arrow-up-right" [size]="15"></lucide-icon><span>审阅</span>
- </button>
- <button class="row-open" type="button" title="进入完整主题详情页" aria-label="进入完整主题详情页"
- (click)="openDetailPage(topic); $event.stopPropagation()">
- <span>进入</span>
- <lucide-icon name="chevron-right" [size]="16"></lucide-icon>
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <ng-template #emptyTopics>
- <div class="empty-state">
- <span class="empty-icon"><lucide-icon name="message-square" [size]="23"></lucide-icon></span>
- <strong>{{ emptyTitle }}</strong>
- <p>{{ emptyDescription }}</p>
- <button *ngIf="query || availabilityFilter !== 'all'" class="secondary-button" type="button" (click)="clearFilters()">清除筛选</button>
- </div>
- </ng-template>
- <footer class="table-footer">
- <span>显示 {{ visibleTopics.length }} 个主题</span>
- <span>置信等级按同主题证据量分层:1 / 3 / 8 / 20 条</span>
- </footer>
- </div>
- <aside class="detail-panel" *ngIf="selectedTopic as topic; else selectionPlaceholder">
- <app-topic-detail [topic]="topic" (close)="closeDetail()"></app-topic-detail>
- </aside>
- <ng-template #selectionPlaceholder>
- <aside class="selection-placeholder">
- <span><lucide-icon name="message-square" [size]="20"></lucide-icon></span>
- <strong>选择一个主题查看详情</strong>
- <p>点击左侧任一主题后,这里会展开趋势、证据、影响商品和原始评价。</p>
- </aside>
- </ng-template>
- </section>
- <footer class="data-boundary">
- <lucide-icon name="database" [size]="15"></lucide-icon>
- <span>主题只覆盖当前已接入的 {{ data.rawReviewCount }} 条评价;场景结果必须由现有 VOC 服务返回并匹配原文后才展示。</span>
- </footer>
- </ng-container>
- </main>
|