|
@@ -1,6 +1,6 @@
|
|
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { RouterModule } from '@angular/router';
|
|
|
|
|
|
|
+import { Router, RouterModule } from '@angular/router';
|
|
|
import { Subject, forkJoin } from 'rxjs';
|
|
import { Subject, forkJoin } from 'rxjs';
|
|
|
import { takeUntil, catchError } from 'rxjs/operators';
|
|
import { takeUntil, catchError } from 'rxjs/operators';
|
|
|
import { of } from 'rxjs';
|
|
import { of } from 'rxjs';
|
|
@@ -9,13 +9,18 @@ import { EmotionHealthScore, CoreIssueSummary, KeywordCloud, DesignerRiskRanking
|
|
|
import { FilterBarComponent } from '../../../shared/components/filter-bar/filter-bar.component';
|
|
import { FilterBarComponent } from '../../../shared/components/filter-bar/filter-bar.component';
|
|
|
import { PageHeaderComponent } from '../../../shared/components/page-header/page-header.component';
|
|
import { PageHeaderComponent } from '../../../shared/components/page-header/page-header.component';
|
|
|
import { SummaryMetricCardComponent } from '../../../shared/components/summary-metric-card/summary-metric-card.component';
|
|
import { SummaryMetricCardComponent } from '../../../shared/components/summary-metric-card/summary-metric-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 { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component';
|
|
|
|
|
+import { EntryCardComponent } from '../../../shared/components/entry-card/entry-card.component';
|
|
|
|
|
+import { EntryCardGridComponent } from '../../../shared/components/entry-card/entry-card-grid.component';
|
|
|
|
|
+import { ErrorStateComponent } from '../../../shared/components/error-state/error-state.component';
|
|
|
import { LoadingSpinnerComponent } from '../../../shared/components/loading-spinner/loading-spinner.component';
|
|
import { LoadingSpinnerComponent } from '../../../shared/components/loading-spinner/loading-spinner.component';
|
|
|
|
|
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-voc-overview',
|
|
selector: 'app-voc-overview',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
|
- imports: [CommonModule, RouterModule, FilterBarComponent, PageHeaderComponent, SummaryMetricCardComponent, EmptyStateComponent, LoadingSpinnerComponent],
|
|
|
|
|
|
|
+ imports: [CommonModule, RouterModule, FilterBarComponent, PageHeaderComponent, SummaryMetricCardComponent, BoardTabsComponent, ContentCardComponent, EmptyStateComponent, EntryCardComponent, EntryCardGridComponent, ErrorStateComponent, LoadingSpinnerComponent],
|
|
|
template: `
|
|
template: `
|
|
|
<div class="voc-overview-container">
|
|
<div class="voc-overview-container">
|
|
|
<app-page-header
|
|
<app-page-header
|
|
@@ -48,12 +53,15 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
(filtersChange)="onFilterChange($event)">
|
|
(filtersChange)="onFilterChange($event)">
|
|
|
</app-filter-bar>
|
|
</app-filter-bar>
|
|
|
|
|
|
|
|
|
|
+ @if (loadError) {
|
|
|
|
|
+ <app-error-state
|
|
|
|
|
+ title="VOC 总览加载失败"
|
|
|
|
|
+ description="无法读取当前筛选范围内的 VOC 汇总信息。"
|
|
|
|
|
+ (retry)="loadData()">
|
|
|
|
|
+ </app-error-state>
|
|
|
|
|
+ } @else {
|
|
|
@if (designerRiskRanking.length > 0) {
|
|
@if (designerRiskRanking.length > 0) {
|
|
|
- <div class="panel designer-risk-panel">
|
|
|
|
|
- <div class="panel-header-row">
|
|
|
|
|
- <h2 class="panel-title">设计师风险排行</h2>
|
|
|
|
|
- <span class="panel-subtitle">按严重预警、警告预警与平均差评率排序</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <app-content-card class="designer-risk-panel" title="设计师风险排行" subtitle="按严重预警、警告预警与平均差评率排序" density="compact" variant="muted">
|
|
|
<div class="designer-risk-list">
|
|
<div class="designer-risk-list">
|
|
|
@for (item of designerRiskRanking; track item.designerName; let i = $index) {
|
|
@for (item of designerRiskRanking; track item.designerName; let i = $index) {
|
|
|
<div class="designer-risk-row">
|
|
<div class="designer-risk-row">
|
|
@@ -87,14 +95,13 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </app-content-card>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
<!-- 情绪健康度仪表盘 + 核心问题集中度 -->
|
|
<!-- 情绪健康度仪表盘 + 核心问题集中度 -->
|
|
|
<div class="dashboard-row">
|
|
<div class="dashboard-row">
|
|
|
<!-- 左:健康度仪表盘 -->
|
|
<!-- 左:健康度仪表盘 -->
|
|
|
- <div class="panel health-gauge-panel">
|
|
|
|
|
- <h2 class="panel-title">情绪健康度仪表盘</h2>
|
|
|
|
|
|
|
+ <app-content-card class="health-gauge-panel" title="情绪健康度仪表盘" density="compact" variant="muted">
|
|
|
@if (healthScore && healthScore.totalReviews > 0) {
|
|
@if (healthScore && healthScore.totalReviews > 0) {
|
|
|
<div class="gauge-wrapper">
|
|
<div class="gauge-wrapper">
|
|
|
<div class="gauge-ring">
|
|
<div class="gauge-ring">
|
|
@@ -139,17 +146,17 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
} @else {
|
|
} @else {
|
|
|
<app-empty-state title="暂无情绪样本" description="页面结构已就绪,当前保持真实空状态。" icon="◌" size="sm"></app-empty-state>
|
|
<app-empty-state title="暂无情绪样本" description="页面结构已就绪,当前保持真实空状态。" icon="◌" size="sm"></app-empty-state>
|
|
|
}
|
|
}
|
|
|
- </div>
|
|
|
|
|
|
|
+ </app-content-card>
|
|
|
|
|
|
|
|
<!-- 右:核心问题集中度 -->
|
|
<!-- 右:核心问题集中度 -->
|
|
|
- <div class="panel issues-panel">
|
|
|
|
|
- <div class="panel-header-row">
|
|
|
|
|
- <h2 class="panel-title">核心问题集中度</h2>
|
|
|
|
|
- <div class="issue-filter-pills">
|
|
|
|
|
- <button class="pill" [class.active]="issueFilter === 'all'" (click)="issueFilter = 'all'">全部</button>
|
|
|
|
|
- <button class="pill" [class.active]="issueFilter === 'negative'" (click)="issueFilter = 'negative'">负面</button>
|
|
|
|
|
- <button class="pill" [class.active]="issueFilter === 'positive'" (click)="issueFilter = 'positive'">正面</button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <app-content-card class="issues-panel" title="核心问题集中度" [hasHeaderActions]="true" density="compact" variant="muted">
|
|
|
|
|
+ <div contentCardActions>
|
|
|
|
|
+ <app-board-tabs
|
|
|
|
|
+ [tabs]="issueTabs"
|
|
|
|
|
+ [activeKey]="issueFilter"
|
|
|
|
|
+ density="compact"
|
|
|
|
|
+ (activeKeyChange)="setIssueFilter($event)">
|
|
|
|
|
+ </app-board-tabs>
|
|
|
</div>
|
|
</div>
|
|
|
@if (filteredIssues.length === 0 && !loading) {
|
|
@if (filteredIssues.length === 0 && !loading) {
|
|
|
<app-empty-state title="暂无核心问题数据" icon="📭" size="sm"></app-empty-state>
|
|
<app-empty-state title="暂无核心问题数据" icon="📭" size="sm"></app-empty-state>
|
|
@@ -171,13 +178,12 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </app-content-card>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 关键词热度 TOP -->
|
|
<!-- 关键词热度 TOP -->
|
|
|
<div class="dashboard-row">
|
|
<div class="dashboard-row">
|
|
|
- <div class="panel keyword-panel">
|
|
|
|
|
- <h2 class="panel-title">正向热词 Top5</h2>
|
|
|
|
|
|
|
+ <app-content-card class="keyword-panel" title="正向热词 Top5" density="compact" variant="muted" tone="green">
|
|
|
@if (topPositiveKeywords.length === 0 && !loading) {
|
|
@if (topPositiveKeywords.length === 0 && !loading) {
|
|
|
<app-empty-state title="暂无数据" icon="📭" size="sm"></app-empty-state>
|
|
<app-empty-state title="暂无数据" icon="📭" size="sm"></app-empty-state>
|
|
|
}
|
|
}
|
|
@@ -193,9 +199,8 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- <div class="panel keyword-panel">
|
|
|
|
|
- <h2 class="panel-title">负向热词 Top5</h2>
|
|
|
|
|
|
|
+ </app-content-card>
|
|
|
|
|
+ <app-content-card class="keyword-panel" title="负向热词 Top5" density="compact" variant="muted" tone="red">
|
|
|
@if (topNegativeKeywords.length === 0 && !loading) {
|
|
@if (topNegativeKeywords.length === 0 && !loading) {
|
|
|
<app-empty-state title="暂无数据" icon="📭" size="sm"></app-empty-state>
|
|
<app-empty-state title="暂无数据" icon="📭" size="sm"></app-empty-state>
|
|
|
}
|
|
}
|
|
@@ -211,30 +216,38 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </app-content-card>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 快速导航 -->
|
|
<!-- 快速导航 -->
|
|
|
- <div class="nav-cards">
|
|
|
|
|
- <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>
|
|
|
|
|
- @if (warningCount > 0) {
|
|
|
|
|
- <span class="nav-card-badge critical">{{ warningCount }} 项预警</span>
|
|
|
|
|
- }
|
|
|
|
|
- </a>
|
|
|
|
|
- <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="/voc-insight/voice">
|
|
|
|
|
- <span class="nav-card-icon">◍</span>
|
|
|
|
|
- <span class="nav-card-title">用户声音分类</span>
|
|
|
|
|
- <span class="nav-card-desc">情绪分类 · 评论样本 · 用户表达</span>
|
|
|
|
|
- </a>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <app-entry-card-grid columns="3">
|
|
|
|
|
+ <app-entry-card
|
|
|
|
|
+ title="风险预警看板"
|
|
|
|
|
+ description="异常波动 · 场景化投诉 · 舆情信号"
|
|
|
|
|
+ icon="🛡️"
|
|
|
|
|
+ [badge]="warningCount > 0 ? warningCount + ' 项预警' : ''"
|
|
|
|
|
+ cta="查看预警"
|
|
|
|
|
+ tone="red"
|
|
|
|
|
+ (activate)="navigate('/domestic/risk-alerts')">
|
|
|
|
|
+ </app-entry-card>
|
|
|
|
|
+ <app-entry-card
|
|
|
|
|
+ title="类目 VOC"
|
|
|
|
|
+ description="品类 VOC 密度 · 类目情绪排行"
|
|
|
|
|
+ icon="📦"
|
|
|
|
|
+ cta="查看类目"
|
|
|
|
|
+ tone="blue"
|
|
|
|
|
+ (activate)="navigate('/domestic/category-voc')">
|
|
|
|
|
+ </app-entry-card>
|
|
|
|
|
+ <app-entry-card
|
|
|
|
|
+ title="用户声音分类"
|
|
|
|
|
+ description="情绪分类 · 评论样本 · 用户表达"
|
|
|
|
|
+ icon="◍"
|
|
|
|
|
+ cta="查看声音"
|
|
|
|
|
+ tone="purple"
|
|
|
|
|
+ (activate)="navigate('/voc-insight/voice')">
|
|
|
|
|
+ </app-entry-card>
|
|
|
|
|
+ </app-entry-card-grid>
|
|
|
|
|
+ }
|
|
|
</div>
|
|
</div>
|
|
|
`,
|
|
`,
|
|
|
styles: [`
|
|
styles: [`
|
|
@@ -243,11 +256,7 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
.metrics-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin-bottom: 24px; }
|
|
.metrics-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin-bottom: 24px; }
|
|
|
|
|
|
|
|
.dashboard-row { display: grid; grid-template-columns: 1fr 1.5fr; gap: 20px; margin-bottom: 24px; }
|
|
.dashboard-row { display: grid; grid-template-columns: 1fr 1.5fr; gap: 20px; margin-bottom: 24px; }
|
|
|
- .panel { background: #fff; border-radius: 12px; border: 1px solid #e5e7eb; padding: 20px 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
|
|
|
|
- .panel-title { font-size: 15px; font-weight: 700; color: #111827; margin: 0 0 16px; }
|
|
|
|
|
- .panel-subtitle { font-size: 12px; color: #6b7280; }
|
|
|
|
|
- .panel-header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
|
|
|
- .panel-header-row .panel-title { margin: 0; }
|
|
|
|
|
|
|
+ .dashboard-row app-content-card { min-width: 0; }
|
|
|
|
|
|
|
|
.designer-risk-panel { margin-bottom: 24px; }
|
|
.designer-risk-panel { margin-bottom: 24px; }
|
|
|
.designer-risk-list { display: flex; flex-direction: column; gap: 10px; }
|
|
.designer-risk-list { display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -288,11 +297,6 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
.legend-item.warn { background: #fef3c7; color: #d97706; }
|
|
.legend-item.warn { background: #fef3c7; color: #d97706; }
|
|
|
.legend-item.bad { background: #fee2e2; color: #dc2626; }
|
|
.legend-item.bad { background: #fee2e2; color: #dc2626; }
|
|
|
|
|
|
|
|
- .issue-filter-pills { display: flex; gap: 4px; }
|
|
|
|
|
- .pill { padding: 4px 12px; border: 1px solid #e5e7eb; border-radius: 16px; font-size: 11px; background: #fff; color: #6b7280; cursor: pointer; transition: all 0.15s; }
|
|
|
|
|
- .pill:hover { border-color: #6366f1; color: #6366f1; }
|
|
|
|
|
- .pill.active { background: #6366f1; color: #fff; border-color: #6366f1; }
|
|
|
|
|
-
|
|
|
|
|
.issues-list { display: flex; flex-direction: column; gap: 8px; }
|
|
.issues-list { display: flex; flex-direction: column; gap: 8px; }
|
|
|
.issue-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
|
|
.issue-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
|
|
|
.issue-row.top-issue { background: #fef2f2; margin: 0 -8px; padding: 6px 8px; border-radius: 8px; }
|
|
.issue-row.top-issue { background: #fef2f2; margin: 0 -8px; padding: 6px 8px; border-radius: 8px; }
|
|
@@ -306,7 +310,6 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
.issue-pct { font-size: 12px; color: #6b7280; font-weight: 600; min-width: 32px; text-align: right; }
|
|
.issue-pct { font-size: 12px; color: #6b7280; font-weight: 600; min-width: 32px; text-align: right; }
|
|
|
.issue-count { font-size: 11px; color: #9ca3af; min-width: 40px; }
|
|
.issue-count { font-size: 11px; color: #9ca3af; min-width: 40px; }
|
|
|
|
|
|
|
|
- .keyword-panel .panel-title { margin-bottom: 12px; }
|
|
|
|
|
.kw-list { display: flex; flex-direction: column; gap: 8px; }
|
|
.kw-list { display: flex; flex-direction: column; gap: 8px; }
|
|
|
.kw-row { display: flex; align-items: center; gap: 10px; }
|
|
.kw-row { display: flex; align-items: center; gap: 10px; }
|
|
|
.kw-rank { width: 24px; height: 24px; border-radius: 50%; background: #f3f4f6; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: #6b7280; flex-shrink: 0; }
|
|
.kw-rank { width: 24px; height: 24px; border-radius: 50%; background: #f3f4f6; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: #6b7280; flex-shrink: 0; }
|
|
@@ -321,26 +324,22 @@ import { LoadingSpinnerComponent } from '../../../shared/components/loading-spin
|
|
|
.pos-text { color: #22c55e; }
|
|
.pos-text { color: #22c55e; }
|
|
|
.neg-text { color: #ef4444; }
|
|
.neg-text { color: #ef4444; }
|
|
|
|
|
|
|
|
- .nav-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
|
|
|
|
- .nav-card { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 24px 16px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; text-decoration: none; transition: all 0.2s; cursor: pointer; text-align: center; position: relative; }
|
|
|
|
|
- .nav-card:hover { border-color: #3b82f6; box-shadow: 0 4px 12px rgba(59,130,246,0.1); transform: translateY(-2px); }
|
|
|
|
|
- .nav-card-icon { font-size: 32px; }
|
|
|
|
|
- .nav-card-title { font-size: 15px; font-weight: 700; color: #111827; }
|
|
|
|
|
- .nav-card-desc { font-size: 12px; color: #6b7280; }
|
|
|
|
|
- .nav-card-badge { position: absolute; top: 10px; right: 10px; font-size: 10px; padding: 2px 8px; border-radius: 8px; font-weight: 600; }
|
|
|
|
|
- .nav-card-badge.critical { background: #fee2e2; color: #dc2626; }
|
|
|
|
|
-
|
|
|
|
|
@media (max-width: 900px) {
|
|
@media (max-width: 900px) {
|
|
|
.metrics-row { grid-template-columns: repeat(3, 1fr); }
|
|
.metrics-row { grid-template-columns: repeat(3, 1fr); }
|
|
|
.dashboard-row { grid-template-columns: 1fr; }
|
|
.dashboard-row { grid-template-columns: 1fr; }
|
|
|
- .nav-cards { grid-template-columns: 1fr; }
|
|
|
|
|
}
|
|
}
|
|
|
`]
|
|
`]
|
|
|
})
|
|
})
|
|
|
export class VocOverviewComponent implements OnInit, OnDestroy {
|
|
export class VocOverviewComponent implements OnInit, OnDestroy {
|
|
|
private destroy$ = new Subject<void>();
|
|
private destroy$ = new Subject<void>();
|
|
|
loading = true;
|
|
loading = true;
|
|
|
|
|
+ loadError = false;
|
|
|
issueFilter: 'all' | 'negative' | 'positive' = 'all';
|
|
issueFilter: 'all' | 'negative' | 'positive' = 'all';
|
|
|
|
|
+ readonly issueTabs: BoardTabItem[] = [
|
|
|
|
|
+ { key: 'all', label: '全部' },
|
|
|
|
|
+ { key: 'negative', label: '负面' },
|
|
|
|
|
+ { key: 'positive', label: '正面' },
|
|
|
|
|
+ ];
|
|
|
currentFilters: any = { timeRange: '30days' };
|
|
currentFilters: any = { timeRange: '30days' };
|
|
|
|
|
|
|
|
vocCount = 0;
|
|
vocCount = 0;
|
|
@@ -352,7 +351,10 @@ export class VocOverviewComponent implements OnInit, OnDestroy {
|
|
|
topNegativeKeywords: KeywordCloud[] = [];
|
|
topNegativeKeywords: KeywordCloud[] = [];
|
|
|
designerRiskRanking: DesignerRiskRankingItem[] = [];
|
|
designerRiskRanking: DesignerRiskRankingItem[] = [];
|
|
|
|
|
|
|
|
- constructor(private monitoringService: DomesticMonitoringViewService) {}
|
|
|
|
|
|
|
+ constructor(
|
|
|
|
|
+ private monitoringService: DomesticMonitoringViewService,
|
|
|
|
|
+ private router: Router,
|
|
|
|
|
+ ) {}
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
|
ngOnInit(): void {
|
|
|
this.loadData();
|
|
this.loadData();
|
|
@@ -380,6 +382,7 @@ export class VocOverviewComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
loadData(): void {
|
|
loadData(): void {
|
|
|
this.loading = true;
|
|
this.loading = true;
|
|
|
|
|
+ this.loadError = false;
|
|
|
forkJoin({
|
|
forkJoin({
|
|
|
metrics: this.monitoringService.getVocMetrics(this.currentFilters),
|
|
metrics: this.monitoringService.getVocMetrics(this.currentFilters),
|
|
|
health: this.monitoringService.getEmotionHealthScore(this.currentFilters),
|
|
health: this.monitoringService.getEmotionHealthScore(this.currentFilters),
|
|
@@ -404,10 +407,23 @@ export class VocOverviewComponent implements OnInit, OnDestroy {
|
|
|
this.designerRiskRanking = designerRisks || [];
|
|
this.designerRiskRanking = designerRisks || [];
|
|
|
this.loading = false;
|
|
this.loading = false;
|
|
|
},
|
|
},
|
|
|
- error: () => { this.loading = false; }
|
|
|
|
|
|
|
+ error: () => {
|
|
|
|
|
+ this.loading = false;
|
|
|
|
|
+ this.loadError = true;
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ setIssueFilter(key: string): void {
|
|
|
|
|
+ if (key === 'all' || key === 'negative' || key === 'positive') {
|
|
|
|
|
+ this.issueFilter = key;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ navigate(route: string): void {
|
|
|
|
|
+ this.router.navigateByUrl(route);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
get filteredIssues(): CoreIssueSummary[] {
|
|
get filteredIssues(): CoreIssueSummary[] {
|
|
|
if (this.issueFilter === 'all') return this.coreIssues;
|
|
if (this.issueFilter === 'all') return this.coreIssues;
|
|
|
return this.coreIssues.filter(i => i.sentiment === this.issueFilter);
|
|
return this.coreIssues.filter(i => i.sentiment === this.issueFilter);
|