我正在研究一种解决方案,但发现其中存在性能问题。
我已经开发了多嵌套级别的报告系统。
基本上,它由表中带有展开和折叠按钮的行数组成。
每当用户单击“扩展”按钮时,我都会调用服务api,并在该特定行下附加包含接收到的数据的新动态组件。
现在我将获得数千行,并假设用户使用扩展按钮扩展多行,浏览器将挂起或崩溃。
最初,我尝试使用角度虚拟滚动插件,但是它在每个级别都添加了滚动条,请考虑用户是否已扩展到四个级别,而垂直滚动条则无法实现,
我认为从用户角度来看是不可行的我认为添加分页(例如“加载更多记录”)将是理想的解决方案,每当用户单击该按钮时,我都会进行ajax调用并将检索到的数据追加到最后一列之后。
我不知道如何在角度上工作
这是我的代码
报告模板:
<div class="report-table-container" >
<ng-container *ngFor="let rData of reportData; let i = index; last as isLast" >
<div class="row report-row" >
<div class="col-4" style="padding-left: 5px;">
<button
class="btn btn-sm"
*ngIf="checkIfHaveMoreSplits(this.splitOpt[0].id) !== 0 && rData.isCollapsed == true"
(click)="splitData(rowWiseFilterObj(rData,this.splitOpt[0].id),this.splitOpt[0].id,sFilters,splitOpt,i,rData,selectedDate)"
row="rData">+</button>
<button
class="btn btn-sm"
*ngIf="checkIfHaveMoreSplits(this.splitOpt[0].id) !== 0 && rData.isCollapsed == false"
(click)="removeDynamicComponent(rData,i)"
>-</button>
<span *ngIf="this.splitOpt[0].id !== '__time'">{{rData[this.splitOpt[0].id]}}</span>
<span *ngIf="this.splitOpt[0].id === '__time'">{{ rData[this.splitOpt[0].id] | date:'dd-MM-yyyy HH:mm:ss Z'}}</span>
</div>
<div class="col-2">{{convertToDecimals(rData.impressions,2)}}</div>
<div class="col-2">{{convertToDecimals(rData.conversions,2)}}</div>
<div class="col-1">{{convertToDecimals(rData.bids,2)}}</div>
<div class="col-1" >{{convertToDecimals(rData.wins,2)}}</div>
<div class="col-1">{{convertToDecimals(rData.cost,2)}}</div>
<div class="col-1">{{convertToDecimals(rData.rev_payout,2)}}</div>
</div>
<div *ngIf="isLast">{{altrows("#ffffff","#f5f5f5")}}</div>
<ng-template #dynamic ></ng-template>
</ng-container>
</div>
报告组件
export class ReportsComponent implements OnInit, AfterViewInit {
filterSelection: any = false;
dimentionSelection: any = false;
splitSelection: any = false;
dValueSelection: any = false;
filterDimentions: any = [];
selectedDimentions: any = [];
filterDimentionsValues: any = [];
currentSelectedDimension: any = [];
appLoading: any = false;
query: any = '';
q: any = '';
sFilters: any = [];
splitOpt: any = [];
posX: any = 100;
posY: any = 100;
reportData: any = [];
service: any;
reportLoading: boolean = false;
currentGraphSelection: string = "impressions";
selectedDate: any = {
startDate: moment(),
endDate: moment()
};
showRangeLabelOnInput: boolean = true;
alwaysShowCalendars: boolean = true;
keepCalendarOpeningWithRange: boolean = true;
ranges: any = {
'Today': [moment(), moment()],
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
}
invalidDates: moment.Moment[] = [];
isInvalidDate = (m: moment.Moment) => {
return this.invalidDates.some(d => d.isSame(m, 'day'))
}
// chart
Highcharts = Highcharts; // required
chartConstructor = 'chart'; // optional string, defaults to 'chart'
chartOptions = {
chart: {
type: "spline"
},
title: {
text: "Impressions"
},
plotOptions: {
area: {
fillColor: "rgba(92, 205, 222,0.2)",
lineColor: "#5ccdde",
},
series: {
marker: {
fillColor: '#FFFFFF',
lineWidth: 1,
lineColor: null // inherit from series
},
fillOpacity: 0.5
}
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: { // don't display the dummy year
month: '%e. %b',
year: '%b'
},
title: {
text: 'Time'
}
},
yAxis: {
title: {
text: "Impression"
},
},
tooltip: {
enabled: true
},
series: [{
name: "Impressions",
data: [],
type: "spline"
}]
};
@ViewChild(DaterangepickerDirective) pickerDirective: DaterangepickerDirective;
picker: DaterangepickerComponent;
@ViewChildren('dynamic', {
read: ViewContainerRef
}) viewContainerRef: QueryList < ViewContainerRef >
constructor(
private _script: ScriptLoaderService,
private _apis: ApplicationApiService,
private modalService: NgbModal,
private cfr: ComponentFactoryResolver,
private toastr: ToastrService,
private _configService: ConfigService,
private http: Http,
private cd: ChangeDetectorRef,
private activatedRoute: ActivatedRoute,
@Inject(Service) service,
) {
this.service = service
}
ngOnInit() {
this.picker = this.pickerDirective.picker;
}
ngAfterViewInit() {
Helpers.bodyClass('m-page--wide m-header--fixed m-header--fixed-mobile m-footer--push m-aside--offcanvas-default reports-page');
this._apis.getReportDimensions().subscribe(response => {
if (response.status == 1200) {
this.filterDimentions = response.data;
}
});
// this.updateGraph(this.currentGraphSelection);
this.cd.detectChanges();
}
openFilterSelection(e) {
var xPos;
if (e.clientX > 1024) {
xPos = e.clientX - 250;
} else {
xPos = e.clientX
}
this.posX = xPos - 10 + "px";
this.posY = e.clientY + 10 + "px";
this.filterSelection = true
this.dimentionSelection = true;
this.dValueSelection = false;
}
openSplitSelection(e) {
var xPos;
if (e.clientX > 1024) {
xPos = e.clientX - 250;
} else {
xPos = e.clientX
}
this.posX = xPos - 10 + "px";
this.posY = e.clientY + 10 + "px";
this.splitSelection = true
this.dimentionSelection = true;
this.dValueSelection = false;
}
getFilterValues(d, e) {
if (e) {
var xPos;
if (e.clientX > 1024) {
xPos = e.clientX - 250;
} else {
xPos = e.clientX
}
this.posX = xPos - 10 + "px";
this.posY = e.clientY + 10 + "px";
}
this.filterDimentionsValues = [];
this.appLoading = true;
this.currentSelectedDimension = d.id;
var apiFilters: any = [{}];
var index = this.sFilters.findIndex(function(v) {
return v.id == d.id
});
if (index === -1) {
for (var i = 0; i < this.sFilters.length; i++) {
if (this.sFilters[i].values.length > 0) {
var k;
k = this.sFilters[i].id
apiFilters[0][k] = this.sFilters[i].values;
}
}
} else {
for (var i = 0; i < index; i++) {
if (this.sFilters[i].values.length > 0) {
var k;
k = this.sFilters[i].id
apiFilters[0][k] = this.sFilters[i].values;
}
}
}
delete apiFilters[0][d.id]
this._apis.getFilterDimentionValues(d.id, this.q, apiFilters[0], this.selectedDate).subscribe(response => {
if (response.status == 1200) {
this.filterDimentionsValues = response.data.split_by_data;
this.appLoading = false;
}
})
this.filterSelection = true
this.dValueSelection = true;
this.dimentionSelection = false;
}
onSearchChange() {
var apiFilters: any = [{}];
for (var i = 0; i < this.sFilters.length; i++) {
if (this.sFilters[i][0].values.length > 0) {
var k;
k = this.sFilters[i][0].id
apiFilters[0][k] = this.sFilters[i][0].values;
}
}
this._apis.getFilterDimentionValues(this.currentSelectedDimension, this.q, apiFilters[0], this.selectedDate).subscribe(response => {
if (response.status == 1200) {
this.filterDimentionsValues = response.data.split_by_data;
this.appLoading = false;
}
})
}
hidePopup() {
this.filterSelection = false
this.dValueSelection = false;
this.dimentionSelection = false;
this.splitSelection = false;
}
goBackToDimensions() {
this.filterSelection = true
this.dimentionSelection = true;
this.dValueSelection = false;
}
selectFilters(d) {
var a = this.currentSelectedDimension;
if (this.sFilters.filter(e => e.id === a).length > 0) {
this.sFilters.filter(function(v) {
if (v.id == a) {
if (!v.values.includes(d[a])) {
v.values.push(d[a])
} else {
var index = v.values.indexOf(d[a]);
if (index > -1) {
v.values.splice(index, 1);
}
}
}
});
} else {
let labelText;
for (var i = 0; i < this.filterDimentions.length; i++) {
if (this.filterDimentions[i].id == this.currentSelectedDimension) {
labelText = this.filterDimentions[i].text;
}
}
var obj = {
id: this.currentSelectedDimension,
label: labelText,
values: [d[a]],
}
this.sFilters.push(obj)
}
this.sFilters = this.sFilters.filter(function(v) {
return v.values.length > 0;
});
}
checkIfDimvalueExists(d) {
var a = this.currentSelectedDimension;
if (this.sFilters.length > 0) {
var sel = this.sFilters.filter(function(v) {
if (v.id == a) {
return v
}
});
if (sel.length > 0) {
var index = sel[0]["values"].indexOf(d[a]);
return index;
} else {
return -1;
}
} else {
return -1;
}
}
arrToString(d) {
return d.toString()
}
selectSplitDimention(d) {
if (this.splitOpt.filter(e => e.id === d.id).length == 0) {
this.splitOpt.push(d);
if (this.splitOpt.length === 1) {
this.getReport();
}
if (this.splitOpt.length === 0) {
this.reportData = [];
} else {
// do nothing
}
} else {
this.splitOpt = this.splitOpt.filter(function(obj) {
return obj.id !== d.id;
});
if (this.splitOpt.length === 0) {
this.reportData = [];
} else {
this.reportData = [];
this.getReport();
}
}
this.hidePopup();
}
getReport() {
this.hidePopup();
if (this.splitOpt.length === 0) {
//this.updateGraph(this.currentGraphSelection);
return false;
}
var apiFilters: any = [{}];
for (var i = 0; i < this.sFilters.length; i++) {
if (this.sFilters[i].values.length > 0) {
var k;
k = this.sFilters[i].id
apiFilters[0][k] = this.sFilters[i].values
}
}
var split = this.splitOpt[0].id;
this.reportData = [];
this.reportLoading = true;
this._apis.getReportData(split, apiFilters[0], this.selectedDate).subscribe(response => {
if (response.status == 1200) {
this.reportData = response.data.split_by_data;
this.reportData.map(function(obj) {
obj.isCollapsed = true;
return obj;
});
this.reportLoading = false;
//this.cd.detectChanges();
}
});
}
checkIfHaveMoreSplits(c) {
if (this.splitOpt.length > 0) {
var index = this.splitOpt.findIndex(function(v) {
return v.id == c
})
if (typeof(this.splitOpt[index + 1]) != "undefined") {
return this.splitOpt[index + 1];
} else {
return 0;
}
}
}
splitData(obj, cSplit, sF, splitOptions, index, rowData, selectedDate) {
var nextSplit = this.checkIfHaveMoreSplits(cSplit);
this.service.setRootViewContainerRef(this.viewContainerRef.toArray()[index]);
this.service.addDynamicComponent(obj, nextSplit, sF, splitOptions, rowData, selectedDate);
}
removeDynamicComponent(rowData, index) {
this.viewContainerRef.toArray()[index].clear();
rowData.isCollapsed = true;
}
rowWiseFilterObj(row, split) {
var arr = [];
var obj = {
id: split,
label: split,
values: [row[split]]
}
arr.push(obj);
return arr;
}
convertToDecimals(input, decimals) {
var exp, rounded,
suffixes = ['K', 'M', 'B', 'T', 'P', 'E'];
if (input < 1000) {
return parseFloat(input).toFixed(2);;
}
exp = Math.floor(Math.log(input) / Math.log(1000));
return (input / Math.pow(1000, exp)).toFixed(decimals) + suffixes[exp - 1];
}
altrows(firstcolor, secondcolor) {
var tableElements = document.getElementsByClassName("report-row");
for (var j = 0; j < tableElements.length; j++) {
if (j % 2 == 0) {
( < any > tableElements[j]).style.backgroundColor = firstcolor;
} else {
( < any > tableElements[j]).style.backgroundColor = secondcolor;
}
}
}
updateFilters(d, o) {
if (o === "s") {
this.splitOpt = this.splitOpt.filter(function(obj) {
return obj.id !== d.id;
});
if (this.splitOpt.length > 0) {
this.getReport();
} else {
this.reportData = [];
}
}
if (o === "d") {
this.sFilters = this.sFilters.filter(function(obj) {
return obj.id !== d.id;
});
if (this.sFilters.length > 0) {
this.getReport();
} else {
//this.reportData = [];
}
}
}
openDatePicker(e) {
this.pickerDirective.open(e);
}
datesUpdated(e) {
if (e.startDate != null && e.endDate != null) {
if (this.splitOpt.length !== 0) {
console.log(1)
this.getReport();
}
if (this.splitOpt.length === 0) {
//this.updateGraph(this.currentGraphSelection);
}
}
}
updateGraph(t) {
this.currentGraphSelection = t;
var apiFilters: any = [{}];
for (var i = 0; i < this.sFilters.length; i++) {
if (this.sFilters[i].values.length > 0) {
var k;
k = this.sFilters[i].id
apiFilters[0][k] = this.sFilters[i].values;
}
}
this.reportLoading = true;
this._apis.getReportGraph(t, apiFilters[0], this.selectedDate).subscribe(response => {
if (response.status == 1200) {
this.chartOptions = {
chart: {
type: "spline"
},
title: {
text: t
},
plotOptions: {
area: {
fillColor: "rgba(92, 205, 222,0.2)",
lineColor: "#5ccdde",
},
series: {
marker: {
fillColor: '#FFFFFF',
lineWidth: 1,
lineColor: null // inherit from series
},
fillOpacity: 0.5
}
},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: { // don't display the dummy year
month: '%e. %b',
year: '%b'
},
title: {
text: 'Time'
}
},
yAxis: {
title: {
text: t
},
},
tooltip: {
enabled: true
},
series: [{
name: t,
data: response.data,
type: "spline"
}]
};
this.reportLoading = false;
}
});
}
}
服务文件:
export class Service {
factoryResolver;
rootViewContainer;
reportLoading: boolean = false;
constructor(
@Inject(ComponentFactoryResolver) factoryResolver,
private _apis: ApplicationApiService,
) {
this.factoryResolver = factoryResolver
}
setRootViewContainerRef(viewContainerRef) {
this.rootViewContainer = viewContainerRef
}
addDynamicComponent(selectedRow, nextSplit, selectedFilters, splitOptions, rowData, selectedDate) {
const factory = this.factoryResolver
.resolveComponentFactory(DynamicComponent)
const component = factory
.create(this.rootViewContainer.parentInjector)
component.instance.selectedRow = selectedRow;
component.instance.nextSplit = nextSplit;
component.instance.selectedFilters = selectedFilters;
component.instance.splitOptions = splitOptions;
component.instance.rowData = rowData;
component.instance.reportLoading = true;
component.instance.selectedDate = selectedDate;
var a = JSON.parse(JSON.stringify(selectedFilters));
var b = JSON.parse(JSON.stringify(selectedRow));
a.filter(function(o1) {
return b.some(function(o2) {
if (o1.id === o2.id) {
o1.values = o2.values;
}
});
});
//Find values that are in result1 but not in result2
var uniqueResultOne = a.filter(function(obj) {
return !b.some(function(obj2) {
return obj.id == obj2.id;
});
});
//Find values that are in result2 but not in result1
var uniqueResultTwo = b.filter(function(obj) {
return !a.some(function(obj2) {
return obj.id == obj2.id;
});
});
//Combine the two arrays of unique entries
var result = a.concat(uniqueResultOne.concat(uniqueResultTwo));
result = result.filter((s1, pos, arr) => arr.findIndex((s2) => s2.id === s1.id) === pos);
this.reportLoading = true;
this._apis.getReportData(nextSplit.id, this.getApiFilters(result), selectedDate).subscribe(response => {
if (response.status == 1200) {
response.data.split_by_data.map(function(obj) {
obj.isCollapsed = true;
return obj;
})
component.instance.splitByData = response.data.split_by_data;
component.instance.selectedDate = selectedDate;
component.instance.reportLoading = false;
rowData.isCollapsed = false;
}
})
this.rootViewContainer.insert(component.hostView)
}
getApiFilters(selectedFilters) {
var apiFilters: any = [{}];
for (var i = 0; i < selectedFilters.length; i++) {
if (selectedFilters[i].values.length > 0) {
var k;
k = selectedFilters[i].id
apiFilters[0][k] = selectedFilters[i].values
}
}
return apiFilters[0];
}
}
动态组件:
@Injectable()
@Component({
selector: 'dynamic-component',
template: `
<div class="snippet" *ngIf="reportLoading === true">
<div class="stage">
<div class="dot-typing"></div>
</div>
</div>
<ng-container *ngFor="let rData of splitByData; let i = index; last as isLast">
<div class="row report-row" >
<div class="col-4" [ngStyle]="{'padding-left': calculateTextPadding(nextSplit.id) }">
<button
class="btn btn-sm"
*ngIf="checkIfHaveMoreSplits(nextSplit.id,splitOptions) !== 0 && rData.isCollapsed == true"
(click)="splitData(rowWiseFilterObj(rData,nextSplit.id,selectedRow),nextSplit.id,selectedFilters,splitOptions,i,rData,selectedDate)">+</button>
<button
class="btn btn-sm"
*ngIf="checkIfHaveMoreSplits(nextSplit.id,splitOptions) !== 0 && rData.isCollapsed == false"
(click)="removeDynamicComponent(rData,i,selectedRow,nextSplit.id)"
>-</button>
<span *ngIf="nextSplit.id !== '__time'">{{rData[nextSplit.id]}}</span>
<span *ngIf="nextSplit.id === '__time'">{{ rData[nextSplit.id] | date:'dd-MM-yyyy HH:mm:ss Z'}}</span>
</div>
<div class="col-2">{{convertToDecimals(rData.impressions,2)}}</div>
<div class="col-2">{{convertToDecimals(rData.conversions,2)}}</div>
<div class="col-1">{{convertToDecimals(rData.bids,2)}}</div>
<div class="col-1">{{convertToDecimals(rData.wins,2)}}</div>
<div class="col-1">{{convertToDecimals(rData.cost,2)}}</div>
<div class="col-1">{{convertToDecimals(rData.rev_payout,2)}}</div>
</div>
<div *ngIf="isLast">{{altrows("#ffffff","#f5f5f5")}}</div>
<ng-template #dynamic ></ng-template>
</ng-container>
<div> Load more </div>
`
})
export class DynamicComponent implements OnInit, AfterViewInit {
@Input() selectedRow: any;
@Input() nextSplit: any;
@Input() selectedFilters: any;
@Input() splitOptions: any;
@Input() splitByData: any;
@Input() rowData: any;
@Input() reportLoading: any;
@Input() selectedDate: any;
factoryResolver;
rootViewContainer;
@ViewChildren('dynamic', {
read: ViewContainerRef
}) viewContainerRef: QueryList < ViewContainerRef >
constructor(
@Inject(ComponentFactoryResolver) factoryResolver,
private _apis: ApplicationApiService,
private cd: ChangeDetectorRef
) {
this.factoryResolver = factoryResolver
}
ngOnInit() {
}
ngAfterViewInit() {
}
setRootViewContainerRef(viewContainerRef) {
this.rootViewContainer = viewContainerRef
}
addDynamicComponent(selectedRow, nextSplit, selectedFilters, splitOptions, sRow, selectedDate) {
const factory = this.factoryResolver
.resolveComponentFactory(DynamicComponent)
const component = factory
.create(this.rootViewContainer.parentInjector)
component.instance.selectedRow = selectedRow;
component.instance.nextSplit = nextSplit;
component.instance.selectedFilters = selectedFilters;
component.instance.splitOptions = splitOptions;
component.instance.selectedDate = selectedDate;
var a = JSON.parse(JSON.stringify(selectedFilters));
var b = JSON.parse(JSON.stringify(selectedRow));
a.filter(function(o1) {
return b.some(function(o2) {
if (o1.id === o2.id) {
o1.values = o2.values;
}
});
});
//Find values that are in result1 but not in result2
var uniqueResultOne = a.filter(function(obj) {
return !b.some(function(obj2) {
return obj.id == obj2.id;
});
});
//Find values that are in result2 but not in result1
var uniqueResultTwo = b.filter(function(obj) {
return !a.some(function(obj2) {
return obj.id == obj2.id;
});
});
//Combine the two arrays of unique entries
var result = a.concat(uniqueResultOne.concat(uniqueResultTwo));
result = result.filter((s1, pos, arr) => arr.findIndex((s2) => s2.id === s1.id) === pos);
this.reportLoading = true;
this._apis.getReportData(nextSplit.id, this.getApiFilters(result), selectedDate).subscribe(response => {
if (response.status == 1200) {
response.data.split_by_data.map(function(obj) {
obj.isCollapsed = true;
return obj;
});
sRow.isCollapsed = false;
component.instance.splitByData = response.data.split_by_data;
this.reportLoading = false;
}
})
this.rootViewContainer.insert(component.hostView)
}
getApiFilters(selectedFilters) {
var apiFilters: any = [{}];
for (var i = 0; i < selectedFilters.length; i++) {
if (selectedFilters[i].values.length > 0) {
var k;
k = selectedFilters[i].id
apiFilters[0][k] = selectedFilters[i].values
}
}
return apiFilters[0];
}
checkIfHaveMoreSplits(c, splitOptions) {
if (splitOptions.length > 0) {
var index = splitOptions.findIndex(function(v) {
return v.id == c
})
if (typeof(splitOptions[index + 1]) != "undefined") {
return splitOptions[index + 1];
} else {
return 0;
}
}
}
splitData(obj, cSplit, sFilters, splitOptions, index, sRow, selectedDate) {
var nextSplit = this.checkIfHaveMoreSplits(cSplit, splitOptions);
this.setRootViewContainerRef(this.viewContainerRef.toArray()[index]);
this.addDynamicComponent(obj, nextSplit, sFilters, splitOptions, sRow, selectedDate);
}
rowWiseFilterObj(row, split, prev) {
if (prev.length == 0) {
var arr = [];
var obj = {
id: split,
label: split,
values: [row[split]]
}
arr.push(obj);
return arr;
} else {
var obj = {
id: split,
label: split,
values: [row[split]]
}
prev.push(obj);
return prev
}
}
removeDynamicComponent(rowData, index, sFilters, nextSplit) {
this.viewContainerRef.toArray()[index].clear();
rowData.isCollapsed = true;
this.altrows("#ffffff", "#f5f5f5");
var index = sFilters.findIndex(function(o) {
return o.id === nextSplit;
})
if (index !== -1) {
sFilters.splice(index, 1);
}
}
convertToDecimals(input, decimals) {
var exp, rounded,
suffixes = ['K', 'M', 'B', 'T', 'P', 'E'];
if (input < 1000) {
return parseFloat(input).toFixed(2);;
}
exp = Math.floor(Math.log(input) / Math.log(1000));
return (input / Math.pow(1000, exp)).toFixed(decimals) + suffixes[exp - 1];
}
calculateTextPadding(id) {
var index = this.splitOptions.findIndex(function(v) {
return v.id == id
})
if (typeof(index) != "undefined") {
return index * 40 + "px"
} else {
return "0px";
}
}
altrows(firstcolor, secondcolor) {
var tableElements = document.getElementsByClassName("report-row");
for (var j = 0; j < tableElements.length; j++) {
if (j % 2 == 0) {
(tableElements[j]).className = "row report-row odd";
} else {
(tableElements[j]).className = "row report-row even";
}
}
}
}
您还可以找到我创建的示例的视频教程:https://www.youtube.com/watch?v=m1a2uxhoNqc