通常開設商店都會有收入跟費用,
現在業主打開後台,
都會希望在第一時間看到一些數據,
也就是常聽到的 Dashboard。
有出現哪些資訊要看各產業,Demo這邊舉了兩個例子:
-src
|-app
|-cms
|-index
|-rank-order
|-rank-order.component.html
|-rank-order.component.css
|-rank-order.component.ts
|-rank-product
|-rank-product.component.html
|-rank-product.component.css
|-rank-product.component.ts
|-index-routing.module.ts
|-index.component.css
|-index.component.html
|-index.component.ts
|-index.module.ts
圖表的套件使用ngx-charts
,請先安裝
ngx-charts
的Demo網:
https://swimlane.github.io/ngx-charts/#/ngx-charts/bar-vertical
通常圖表的套件都是用canvas
繪製的,
在RWD會有個困擾,
就是瀏覽的時候如果任意拉長寬高,canvas
繪製的圖表不會自適應。
所以我們需要先做一層 目前螢幕的寬高,
以及要給 canvas的寬高。
index.component.ts
:import {
Component,
OnInit,
HostListener,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef
} from "@angular/core";
import { BreakpointObserver } from "@angular/cdk/layout";
@Component({
selector: "app-index",
templateUrl: "./index.component.html",
styleUrls: ["./index.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class IndexComponent implements OnInit, AfterViewInit {
innerWidth = 0;
innerHeight = 0;
isDevice = "pc";
constructor(
private breakpointObserver: BreakpointObserver,
private changeDetectorRef: ChangeDetectorRef,
) {}
ngOnInit() {
this.breakpointObserver.observe("(max-width: 1199px)")
.subscribe(r => {
this.isDevice = r.matches ? "mb" : "pc";
});
}
ngAfterViewInit() {
setInterval(() => {
this.changeDetectorRef.markForCheck();
}, 500);
this.initSize();
}
@HostListener("window:resize", ["$event"]) onResize(event: Event) {
this.initSize();
}
initSize() {
this.innerWidth = window.innerWidth;
this.innerHeight = window.innerHeight;
this.reviseSize();
}
reviseSize() {
if (this.isDevice === "pc") {
this.innerWidth = this.innerWidth - 270;
this.innerHeight = (this.innerHeight - 125) / 2;
} else {
this.innerWidth = this.innerWidth - 40;
this.innerHeight = (this.innerHeight - 140) / 2;
}
}
}
說明幾個部分:
@HostListener("window:resize", ["$event"]) onResize(event: Event) {
this.initSize();
}
--
breakpointObserver
是Angular的CDKobserve("(max-width: 1199px)")
但是
breakpointObserver
無法直接告訴我目前寬高尺寸,
所以才用@HostListener去監聽。
--
有分兩種版面:
而header
、footer
、menu
各有它的尺寸。
一種為PC版:
container 需要放置的寬度就必須要減掉menu
的寬度(230px),
以及兩邊留白的各20px(40px)。
container 需要放置的高度就必須要減掉header
的高度(50px),
以及footer
的高度(35px)。
以及上下兩邊留白的各20px(40px)。
--
一種為MB版:
container 需要放置的寬度就必須要減掉兩邊留白的各20px(40px)。
container 需要放置的高度就必須要減掉header
的高度(65px),
以及footer
的高度(35px)。
以及上下兩邊留白的各20px(40px)。
為什麼高度除以2呢?
那是因為Demo中舉了兩個例子,分別各佔剩餘版面的50%。
--
當畫面任意拉伸,所監聽到的尺寸事件一直變動,
如果沒有設定this.changeDetectorRef.markForCheck();
會一直出現紅字錯誤說明偵測上是沒有變動,但是變數卻被更改了。
想知道更多偵測原理,請參考至 https://ithelp.ithome.com.tw/articles/10209130
--
index.component.html
:<div class="container flex straight-flex">
<ng-container *ngIf="!!innerWidth && !!innerHeight">
<app-rank-order [innerWidth]="innerWidth" [innerHeight]="innerHeight">
</app-rank-order>
</ng-container>
<ng-container *ngIf="!!innerWidth && !!innerHeight">
<app-rank-product [innerWidth]="innerWidth" [innerHeight]="innerHeight">
</app-rank-product>
</ng-container>
</div>
.container > * {
flex: 1 1 50%;
}
rank-order.component.ts
:...
import * as _moment from "moment";
import { Moment } from "moment";
const moment = (_moment as any).default ? (_moment as any).default : _moment;
interface IChartData {
name: string;
value: number;
}
interface IDateRange {
startDate: Moment;
endDate: Moment;
}
@Component({
selector: "app-rank-order",
templateUrl: "./rank-order.component.html",
styleUrls: ["./rank-order.component.css"]
})
export class RankOrderComponent implements OnInit {
@Input() innerWidth: number = 0;
@Input() innerHeight: number = 0;
rankOrders: IChartData[] = [];//畫面資料
max = 3; //最多到前幾個月
constructor(private dataService: DataService) {}
/*開始跑流程 setDatas()為遞迴 */
ngOnInit() {
this.setDatas(0);
}
/*最多跑到max設置的圈數 圈數為0.1.2*/
setDatas(tag: number) {
if (tag <= this.max) {
this.setOrders(tag, this.setDate(tag));
}
}
/*根據第幾圈給予前幾個月的 時間區間 */
setDate(lastIndex?: number): IDateRange {
let obj = <IDateRange>{
startDate: null,
endDate: null
};
obj.startDate = moment()
.month(moment().month() - lastIndex)
.startOf("month");
obj.endDate = moment()
.month(moment().month() - lastIndex)
.endOf("month");
return obj;
}
/*
根據前一個月或是前兩個月或是前三個月的
已結單訂單來查詢資料(orders)。
*/
setOrders(tag: number, range: IDateRange) {
let url = this.dataService.setUrl("orders", [
{ key: "status", val: 5 },
{ key: "inserted_gte", val: range.startDate.valueOf() },
{ key: "inserted_lte", val: range.endDate.valueOf() }
]);
this.dataService.getData(url).subscribe((data: IData) => {
if (!data.errorcode && !!data.res) {
let orders = <IOrder[]>data.res;
this.settingOrder(tag, orders);
} else {
this.setDatas(++tag);
}
});
}
/*
如果查詢完的資料為空,直接跑一下圈
如果有查詢完的資料(orders),直接進行orders的計算
*/
settingOrder(tag: number, orders: IOrder[]) {
if (!!orders.length) {
this.runPerOrder(tag, 0, orders);
} else {
this.setDatas(++tag);
}
}
/*
orders開始進行每張order的計算
此function為遞迴
*/
runPerOrder(tag: number, orderIndex: number, orders: IOrder[]) {
let leng = orders.length;
if (orderIndex < leng) {
this.setPerOrder(tag, orderIndex, orders);
} else {
//計算此圈月份orders的合計,進入塞資料
this.setRankOrders(tag, orders);
}
}
/*每張訂單的購物車明細*/
setPerOrder(tag: number, orderIndex: number, orders: IOrder[]) {
let order = orders[orderIndex];
let carUrl = this.dataService.setUrl("cars", [
{ key: "orderId", val: order.id },
{ key: "_expand", val: "product" }
]);
this.dataService.getData(carUrl).subscribe((carData: IData) => {
if (!carData.errorcode && !!carData.res) {
let cars = <IOrderCar[]>carData.res;
let sum = this.sumCars(cars);
order["sum"] = sum;
} else {
order["sum"] = 0;
}
this.runPerOrder(tag, ++orderIndex, orders);
});
}
/*每張訂單的購物車總和*/
sumCars(cars: IOrderCar[]): number {
let sum = 0;
if (!!cars && !!cars.length) {
cars.forEach((car: IOrderCar) => {
sum += car.product.price * car.amount;
});
}
return sum;
}
/*裝載資料*/
setRankOrders(tag: number, orders: IOrder[]) {
let total = orders
.map((order: IOrder) => {
return order["sum"];
})
.reduce((a: number, c: number) => {
return a + c;
}, 0);
let obj = <IChartData>{
name: this.setRankName(tag),
value: total
};
this.rankOrders.push(obj);
this.setDatas(++tag);
}
/*每筆資料的項目名稱 如:7月、8月..*/
setRankName(tag: number): string {
let nowMonth = moment().month() + 1;
let tagMonth = nowMonth - tag;
return `${tagMonth}`;
}
}
注意的是有兩個遞迴function,
因為異步加載的關係,
一定要等上一個結果回應後才能進行下一個。
--
rank-order.component.html
:<ngx-charts-bar-horizontal
*ngIf="!!rankOrders && !!rankOrders.length"
[view]="[innerWidth, innerHeight]"
[results]="rankOrders"
[xAxis]="true"
[yAxis]="true"
[barPadding]="40"
[xAxisLabel]="'revenue' | translate"
[yAxisLabel]="'month' | translate"
[showXAxisLabel]="true"
[showYAxisLabel]="true"
>
</ngx-charts-bar-horizontal>
rank-product.component.ts
:...
import * as _moment from "moment";
import { Moment } from "moment";
const moment = (_moment as any).default ? (_moment as any).default : _moment;
interface IChartData {
name: string;
value: number;
}
interface IDateRange {
startDate: Moment;
endDate: Moment;
}
interface IAmountProduct {
productName: string;
totalAmount: number;
}
@Component({
selector: "app-rank-product",
templateUrl: "./rank-product.component.html",
styleUrls: ["./rank-product.component.css"]
})
export class RankProductComponent implements OnInit {
@Input() innerWidth: number = 0;
@Input() innerHeight: number = 0;
rankProducts: IChartData[] = [];//畫面資料
//每件商品的銷售量
sumProducts: { [key: number]: number } = {};
max = 5; //最多幾個商品
constructor(private dataService: DataService) {}
ngOnInit() {
this.setDatas();
}
/*目前月份的時間區間 */
setDate(): IDateRange {
let obj = <IDateRange>{
startDate: null,
endDate: null
};
obj.startDate = moment().startOf("month");
obj.endDate = moment()
.startOf("month")
.add(1, "months");
return obj;
}
/*開始跑流程 */
setDatas() {
this.setOrders(this.setDate());
}
/*
根據當月的已結單訂單來查詢資料(orders)。
*/
setOrders(range: IDateRange) {
let url = this.dataService.setUrl("orders", [
{ key: "status", val: 5 },
{ key: "inserted_gte", val: range.startDate.valueOf() },
{ key: "inserted_lte", val: range.endDate.valueOf() }
]);
this.dataService.getData(url).subscribe((data: IData) => {
if (!data.errorcode && !!data.res) {
let orders = <IOrder[]>data.res;
this.runPerOrder(0, orders);
}
});
}
/*
orders開始進行每張order的計算
此function為遞迴
*/
runPerOrder(orderIndex: number, orders: IOrder[]) {
let leng = orders.length;
if (orderIndex < leng) {
this.setPerOrder(orderIndex, orders);
} else {
this.setRankProducts();
}
}
/*每張訂單的購物車明細*/
setPerOrder(orderIndex: number, orders: IOrder[]) {
let order = orders[orderIndex];
let carUrl = this.dataService.setUrl("cars", [
{ key: "orderId", val: order.id },
{ key: "_expand", val: "product" }
]);
this.dataService.getData(carUrl).subscribe((carData: IData) => {
if (!carData.errorcode && !!carData.res) {
let cars = <IOrderCar[]>carData.res;
this.setPerProduct(cars);
}
this.runPerOrder(++orderIndex, orders);
});
}
/*確認每個產品在每張訂單的銷售量*/
setPerProduct(cars: IOrderCar[]) {
cars.forEach((car: IOrderCar) => {
if (!this.sumProducts[car.product.name]) {
this.sumProducts[car.product.name] = 0;
}
this.sumProducts[car.product.name] =
this.sumProducts[car.product.name] + car.amount;
});
}
/*裝載資料*/
setRankProducts() {
let arrs: IAmountProduct[] = [];
arrs = this.sortRankProducts(arrs);
if (!!arrs && !!arrs.length) {
this.rankProducts = [];
let max = arrs.length - 1 < this.max ?
arrs.length - 1 : this.max;
for (let i = 0; i <= max; i++) {
this.rankProducts.push({
name: arrs[i].productName,
value: arrs[i].totalAmount
});
}
}
}
/*所有商品的銷售量排序*/
sortRankProducts(arrs: IAmountProduct[]): IAmountProduct[] {
let keys = Object.keys(this.sumProducts);
keys.forEach((k: string) => {
let obj = <IAmountProduct>{
productName: k,
totalAmount: this.sumProducts[k]
};
arrs.push(obj);
});
arrs.sort((a, b) => {
if (a.totalAmount < b.totalAmount) {
return 1;
}
if (a.totalAmount > b.totalAmount) {
return -1;
}
return 0;
});
return arrs;
}
}
--
rank-product.component.ts
:<ngx-charts-bar-vertical
*ngIf="!!rankProducts && !!rankProducts.length"
[view]="[innerWidth, innerHeight]"
[results]="rankProducts"
[xAxis]="true"
[yAxis]="true"
[barPadding]="80"
[xAxisLabel]="'product_name' | translate"
[yAxisLabel]="'product_amount' | translate"
[showXAxisLabel]="true"
[showYAxisLabel]="true"
>
</ngx-charts-bar-vertical>
此為完整專案範例碼,連線方式為json-server。
https://stackblitz.com/edit/ngcms-json-server
一開始會跳出提示視窗顯示fail為正常,
請先從範例專案裡下載或是複製db.json
到本地端,
並下指令:
json-server db.json
json-server開啟成功後請連結此網址:
https://ngcms-json-server.stackblitz.io/cms?token=bc6e113d26ce620066237d5e43f14690