项目初始化
This commit is contained in:
16
src/app/layout/pro/components/footer/footer.component.html
Normal file
16
src/app/layout/pro/components/footer/footer.component.html
Normal file
@ -0,0 +1,16 @@
|
||||
<global-footer>
|
||||
<global-footer-item href="https://e.ng-alain.com/theme/pro" blankTarget>
|
||||
Pro 首页
|
||||
</global-footer-item>
|
||||
<global-footer-item href="https://github.com/ng-alain" blankTarget>
|
||||
<i nz-icon nzType="github"></i>
|
||||
</global-footer-item>
|
||||
<global-footer-item href="https://ng-alain.github.io/ng-alain/" blankTarget>
|
||||
Alain Pro
|
||||
</global-footer-item>
|
||||
Copyright
|
||||
<i nz-icon nzType="copyright" class="mx-sm"></i>
|
||||
{{ year }}
|
||||
<a href="//github.com/cipchk" target="_blank" class="mx-sm">卡色</a>
|
||||
出品
|
||||
</global-footer>
|
||||
15
src/app/layout/pro/components/footer/footer.component.ts
Normal file
15
src/app/layout/pro/components/footer/footer.component.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { SettingsService } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProFooterComponent {
|
||||
get year(): number {
|
||||
return this.setting.app.year;
|
||||
}
|
||||
|
||||
constructor(private setting: SettingsService) {}
|
||||
}
|
||||
25
src/app/layout/pro/components/header/header.component.html
Normal file
25
src/app/layout/pro/components/header/header.component.html
Normal file
@ -0,0 +1,25 @@
|
||||
<div *ngIf="pro.isTopMenu" class="alain-pro__top-nav">
|
||||
<div class="alain-pro__top-nav-main" [ngClass]="{ 'alain-pro__top-nav-main-wide': pro.isFixed }">
|
||||
<div class="alain-pro__top-nav-main-left">
|
||||
<layout-pro-logo class="alain-pro__top-nav-logo"></layout-pro-logo>
|
||||
<div class="alain-pro__menu-wrap">
|
||||
<div layout-pro-menu mode="horizontal"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alain-pro__top-nav-main-right" layout-pro-header-widget></div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!pro.isTopMenu" class="alain-pro__header">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-container *ngIf="pro.isMobile">
|
||||
<a [routerLink]="['/']" class="alain-pro__header-logo">
|
||||
<img src="./assets/logo-color.svg" width="32" />
|
||||
</a>
|
||||
<div class="ant-divider ant-divider-vertical"></div>
|
||||
</ng-container>
|
||||
<div class="alain-pro__header-item alain-pro__header-trigger" (click)="pro.setCollapsed()">
|
||||
<i nz-icon [nzType]="collapsedIcon" class="alain-pro__header-item-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div layout-pro-header-widget></div>
|
||||
</div>
|
||||
68
src/app/layout/pro/components/header/header.component.ts
Normal file
68
src/app/layout/pro/components/header/header.component.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { RTL, RTLService } from '@delon/theme';
|
||||
import { combineLatest, fromEvent, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||
|
||||
import { BrandService } from '../../pro.service';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-header',
|
||||
templateUrl: './header.component.html',
|
||||
host: {
|
||||
'[class.ant-layout-header]': 'true',
|
||||
'[class.alain-pro__header-fixed]': 'pro.fixedHeader',
|
||||
'[class.alain-pro__header-hide]': 'hideHeader',
|
||||
'[style.padding.px]': '0'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProHeaderComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
hideHeader = false;
|
||||
|
||||
@HostBinding('style.width')
|
||||
get getHeadWidth(): string {
|
||||
const { isMobile, fixedHeader, menu, collapsed, width, widthInCollapsed } = this.pro;
|
||||
if (isMobile || !fixedHeader || menu === 'top') {
|
||||
return '100%';
|
||||
}
|
||||
return collapsed ? `calc(100% - ${widthInCollapsed}px)` : `calc(100% - ${width}px)`;
|
||||
}
|
||||
|
||||
get collapsedIcon(): string {
|
||||
let type = this.pro.collapsed ? 'unfold' : 'fold';
|
||||
if (this.rtl.dir === RTL) {
|
||||
type = this.pro.collapsed ? 'fold' : 'unfold';
|
||||
}
|
||||
return `menu-${type}`;
|
||||
}
|
||||
|
||||
constructor(public pro: BrandService, @Inject(DOCUMENT) private doc: any, private cdr: ChangeDetectorRef, private rtl: RTLService) {}
|
||||
|
||||
private handScroll(): void {
|
||||
if (!this.pro.autoHideHeader) {
|
||||
this.hideHeader = false;
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.hideHeader = this.doc.body.scrollTop + this.doc.documentElement.scrollTop > this.pro.autoHideHeaderTop;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
combineLatest([
|
||||
this.pro.notify.pipe(tap(() => this.cdr.markForCheck())),
|
||||
fromEvent(window, 'scroll', { passive: false }).pipe(throttleTime(50), distinctUntilChanged())
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => this.handScroll());
|
||||
|
||||
this.rtl.change.pipe(takeUntil(this.destroy$)).subscribe(() => this.cdr.detectChanges());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
4
src/app/layout/pro/components/logo/logo.component.html
Normal file
4
src/app/layout/pro/components/logo/logo.component.html
Normal file
@ -0,0 +1,4 @@
|
||||
<a [routerLink]="['/']" class="d-flex align-items-center">
|
||||
<img src="./assets/logo-color.svg" alt="{{ name }}" height="32" />
|
||||
<h1>{{ name }}</h1>
|
||||
</a>
|
||||
15
src/app/layout/pro/components/logo/logo.component.ts
Normal file
15
src/app/layout/pro/components/logo/logo.component.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { SettingsService } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-logo',
|
||||
templateUrl: './logo.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProLogoComponent {
|
||||
get name(): string {
|
||||
return this.setting.app.name!;
|
||||
}
|
||||
|
||||
constructor(private setting: SettingsService) {}
|
||||
}
|
||||
101
src/app/layout/pro/components/menu/menu.component.html
Normal file
101
src/app/layout/pro/components/menu/menu.component.html
Normal file
@ -0,0 +1,101 @@
|
||||
<ng-template #icon let-i>
|
||||
<ng-container *ngIf="i" [ngSwitch]="i.type">
|
||||
<i *ngSwitchCase="'icon'" nz-icon [nzType]="i.value" class="alain-pro__menu-icon"></i>
|
||||
<i *ngSwitchCase="'iconfont'" nz-icon [nzIconfont]="i.iconfont" class="alain-pro__menu-icon"></i>
|
||||
<img *ngSwitchCase="'img'" src="{{ i.value }}" class="anticon alain-pro__menu-icon alain-pro__menu-img" />
|
||||
<i *ngSwitchDefault class="anticon alain-pro__menu-icon {{ i.value }}"></i>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-template #mainLink let-i>
|
||||
<ng-template [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{ $implicit: i.icon }"></ng-template>
|
||||
<span class="alain-pro__menu-title-text" *ngIf="!pro.onlyIcon">{{ i.text }}</span>
|
||||
<div *ngIf="i.badge" class="alain-pro__menu-title-badge">
|
||||
<em>{{ i.badge }}</em>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #subLink let-i>
|
||||
<a *ngIf="!i.externalLink" [routerLink]="i.link" [target]="i.target">{{ i.text }} </a>
|
||||
<a *ngIf="i.externalLink" [attr.href]="i.externalLink" [attr.target]="i.target">{{ i.text }} </a>
|
||||
</ng-template>
|
||||
<ul *ngIf="menus" nz-menu [nzMode]="mode" [nzTheme]="pro.theme" [nzInlineCollapsed]="pro.isMobile ? false : pro.collapsed">
|
||||
<ng-container *ngFor="let l1 of menus">
|
||||
<li
|
||||
*ngIf="l1.children!.length === 0"
|
||||
nz-menu-item
|
||||
class="alain-pro__menu-item"
|
||||
[class.alain-pro__menu-item--disabled]="l1.disabled"
|
||||
[nzSelected]="l1._selected"
|
||||
[nzDisabled]="l1.disabled"
|
||||
>
|
||||
<a *ngIf="!l1.externalLink" [routerLink]="l1.link" (click)="closeCollapsed()" class="alain-pro__menu-title">
|
||||
<ng-template [ngTemplateOutlet]="mainLink" [ngTemplateOutletContext]="{ $implicit: l1 }"></ng-template>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="l1.externalLink"
|
||||
[attr.href]="l1.externalLink"
|
||||
[attr.target]="l1.target"
|
||||
(click)="closeCollapsed()"
|
||||
class="alain-pro__menu-title"
|
||||
>
|
||||
<ng-template [ngTemplateOutlet]="mainLink" [ngTemplateOutletContext]="{ $implicit: l1 }"></ng-template>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="l1.children!.length > 0"
|
||||
nz-submenu
|
||||
[nzTitle]="l1TitleTpl"
|
||||
class="alain-pro__menu-item"
|
||||
[class.text-white]="pro.theme === 'dark' && l1._selected"
|
||||
[nzOpen]="l1._open"
|
||||
[nzDisabled]="l1.disabled"
|
||||
(nzOpenChange)="openChange(l1, $event)"
|
||||
>
|
||||
<ng-template #l1TitleTpl>
|
||||
<span title class="alain-pro__menu-title">
|
||||
<ng-template [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{ $implicit: l1.icon }"></ng-template>
|
||||
<span class="alain-pro__menu-title-text" *ngIf="pro.isMobile || !pro.onlyIcon">{{ l1.text }}</span>
|
||||
<div *ngIf="l1.badge" class="alain-pro__menu-title-badge">
|
||||
<em>{{ l1.badge }}</em>
|
||||
</div>
|
||||
</span>
|
||||
</ng-template>
|
||||
<ul>
|
||||
<ng-container *ngFor="let l2 of l1.children">
|
||||
<li
|
||||
*ngIf="!l2._hidden && l2.children!.length === 0"
|
||||
nz-menu-item
|
||||
[class.alain-pro__menu-item--disabled]="l2.disabled"
|
||||
[nzSelected]="l2._selected"
|
||||
[nzDisabled]="l2.disabled"
|
||||
(click)="closeCollapsed()"
|
||||
>
|
||||
<ng-template [ngTemplateOutlet]="subLink" [ngTemplateOutletContext]="{ $implicit: l2 }"></ng-template>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="!l2._hidden && l2.children!.length > 0"
|
||||
nz-submenu
|
||||
[nzTitle]="l2.text!"
|
||||
[nzOpen]="l2._open"
|
||||
[nzDisabled]="l2.disabled"
|
||||
(nzOpenChange)="openChange(l2, $event)"
|
||||
>
|
||||
<ul>
|
||||
<ng-container *ngFor="let l3 of l2.children">
|
||||
<li
|
||||
*ngIf="!l3._hidden"
|
||||
nz-menu-item
|
||||
[class.alain-pro__menu-item--disabled]="l3.disabled"
|
||||
[nzSelected]="l3._selected"
|
||||
[nzDisabled]="l3.disabled"
|
||||
(click)="closeCollapsed()"
|
||||
>
|
||||
<ng-template [ngTemplateOutlet]="subLink" [ngTemplateOutletContext]="{ $implicit: l3 }"></ng-template>
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
125
src/app/layout/pro/components/menu/menu.component.ts
Normal file
125
src/app/layout/pro/components/menu/menu.component.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { MenuService } from '@delon/theme';
|
||||
import { InputBoolean } from '@delon/util';
|
||||
import { NzMenuModeType } from 'ng-zorro-antd/menu';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { BrandService } from '../../pro.service';
|
||||
import { ProMenu } from '../../pro.types';
|
||||
|
||||
@Component({
|
||||
selector: '[layout-pro-menu]',
|
||||
templateUrl: './menu.component.html',
|
||||
host: {
|
||||
'[class.alain-pro__menu]': 'true',
|
||||
'[class.alain-pro__menu-only-icon]': 'pro.onlyIcon'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProMenuComponent implements OnInit, OnDestroy {
|
||||
private unsubscribe$ = new Subject<void>();
|
||||
menus?: ProMenu[];
|
||||
|
||||
@Input() @InputBoolean() disabledAcl = false;
|
||||
@Input() mode: NzMenuModeType = 'inline';
|
||||
|
||||
constructor(private menuSrv: MenuService, private router: Router, public pro: BrandService, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
private cd(): void {
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
private genMenus(data: ProMenu[]): void {
|
||||
const res: ProMenu[] = [];
|
||||
// ingores category menus
|
||||
const ingoreCategores = data.reduce((prev, cur) => prev.concat(cur.children as ProMenu[]), [] as ProMenu[]);
|
||||
this.menuSrv.visit(ingoreCategores, (item: ProMenu, parent: ProMenu | null) => {
|
||||
if (!item._aclResult) {
|
||||
if (this.disabledAcl) {
|
||||
item.disabled = true;
|
||||
} else {
|
||||
item._hidden = true;
|
||||
}
|
||||
}
|
||||
if (item._hidden === true) {
|
||||
return;
|
||||
}
|
||||
if (parent === null) {
|
||||
res.push(item);
|
||||
}
|
||||
});
|
||||
this.menus = res;
|
||||
|
||||
this.openStatus();
|
||||
}
|
||||
|
||||
private openStatus(): void {
|
||||
const inFn = (list: ProMenu[]) => {
|
||||
for (const i of list) {
|
||||
i._open = false;
|
||||
i._selected = false;
|
||||
if (i.children!.length > 0) {
|
||||
inFn(i.children!);
|
||||
}
|
||||
}
|
||||
};
|
||||
inFn(this.menus!);
|
||||
|
||||
let item = this.menuSrv.getHit(this.menus!, this.router.url, true);
|
||||
if (!item) {
|
||||
this.cd();
|
||||
return;
|
||||
}
|
||||
do {
|
||||
item._selected = true;
|
||||
if (!this.pro.isTopMenu && !this.pro.collapsed) {
|
||||
item._open = true;
|
||||
}
|
||||
item = item._parent!;
|
||||
} while (item);
|
||||
this.cd();
|
||||
}
|
||||
|
||||
openChange(item: ProMenu, statue: boolean): void {
|
||||
const data = item._parent ? item._parent.children : this.menus;
|
||||
if (data && data.length <= 1) {
|
||||
return;
|
||||
}
|
||||
data!.forEach(i => (i._open = false));
|
||||
item._open = statue;
|
||||
}
|
||||
|
||||
closeCollapsed(): void {
|
||||
const { pro } = this;
|
||||
if (pro.isMobile) {
|
||||
setTimeout(() => pro.setCollapsed(true), 25);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const { unsubscribe$, router, pro } = this;
|
||||
this.menuSrv.change.pipe(takeUntil(unsubscribe$)).subscribe(res => this.genMenus(res));
|
||||
|
||||
router.events
|
||||
.pipe(
|
||||
takeUntil(unsubscribe$),
|
||||
filter(e => e instanceof NavigationEnd)
|
||||
)
|
||||
.subscribe(() => this.openStatus());
|
||||
|
||||
pro.notify
|
||||
.pipe(
|
||||
takeUntil(unsubscribe$),
|
||||
filter(() => !!this.menus)
|
||||
)
|
||||
.subscribe(() => this.cd());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const { unsubscribe$ } = this;
|
||||
unsubscribe$.next();
|
||||
unsubscribe$.complete();
|
||||
}
|
||||
}
|
||||
10
src/app/layout/pro/components/notify/notify.component.html
Normal file
10
src/app/layout/pro/components/notify/notify.component.html
Normal file
@ -0,0 +1,10 @@
|
||||
<notice-icon
|
||||
btnClass="alain-pro__header-item"
|
||||
btnIconClass="alain-pro__header-item-icon"
|
||||
[data]="data"
|
||||
[count]="count"
|
||||
[loading]="loading"
|
||||
(select)="select($event)"
|
||||
(clear)="clear($event)"
|
||||
(popoverVisibleChange)="loadData()"
|
||||
></notice-icon>
|
||||
183
src/app/layout/pro/components/notify/notify.component.ts
Normal file
183
src/app/layout/pro/components/notify/notify.component.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { NoticeIconList, NoticeItem } from '@delon/abc/notice-icon';
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import parse from 'date-fns/parse';
|
||||
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-notify',
|
||||
templateUrl: './notify.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetNotifyComponent {
|
||||
data: NoticeItem[] = [
|
||||
{
|
||||
title: '通知',
|
||||
list: [],
|
||||
emptyText: '你已查看所有通知',
|
||||
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
|
||||
clearText: '清空通知'
|
||||
},
|
||||
{
|
||||
title: '消息',
|
||||
list: [],
|
||||
emptyText: '您已读完所有消息',
|
||||
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg',
|
||||
clearText: '清空消息'
|
||||
},
|
||||
{
|
||||
title: '待办',
|
||||
list: [],
|
||||
emptyText: '你已完成所有待办',
|
||||
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg',
|
||||
clearText: '清空待办'
|
||||
}
|
||||
];
|
||||
count = 5;
|
||||
loading = false;
|
||||
|
||||
constructor(private msg: NzMessageService, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
updateNoticeData(notices: NoticeIconList[]): NoticeItem[] {
|
||||
const data = this.data.slice();
|
||||
data.forEach(i => (i.list = []));
|
||||
|
||||
notices.forEach(item => {
|
||||
const newItem = { ...item };
|
||||
if (typeof newItem.datetime === 'string') {
|
||||
newItem.datetime = parse(newItem.datetime, 'yyyy-MM-dd', new Date());
|
||||
}
|
||||
if (newItem.datetime) {
|
||||
newItem.datetime = formatDistanceToNow(newItem.datetime as Date);
|
||||
}
|
||||
if (newItem.extra && newItem.status) {
|
||||
newItem.color = (
|
||||
{
|
||||
todo: undefined,
|
||||
processing: 'blue',
|
||||
urgent: 'red',
|
||||
doing: 'gold'
|
||||
} as NzSafeAny
|
||||
)[newItem.status];
|
||||
}
|
||||
data.find(w => w.title === newItem.type)?.list.push(newItem);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
loadData(): void {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
this.data = this.updateNoticeData([
|
||||
{
|
||||
id: '000000001',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||
title: '你收到了 14 份新周报',
|
||||
datetime: '2017-08-09',
|
||||
type: '通知'
|
||||
},
|
||||
{
|
||||
id: '000000002',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
|
||||
title: '你推荐的 曲妮妮 已通过第三轮面试',
|
||||
datetime: '2017-08-08',
|
||||
type: '通知'
|
||||
},
|
||||
{
|
||||
id: '000000003',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
|
||||
title: '这种模板可以区分多种通知类型',
|
||||
datetime: '2017-08-07',
|
||||
read: true,
|
||||
type: '通知'
|
||||
},
|
||||
{
|
||||
id: '000000004',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
datetime: '2017-08-07',
|
||||
type: '通知'
|
||||
},
|
||||
{
|
||||
id: '000000005',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||
title: '内容不要超过两行字,超出时自动截断',
|
||||
datetime: '2017-08-07',
|
||||
type: '通知'
|
||||
},
|
||||
{
|
||||
id: '000000006',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '曲丽丽 评论了你',
|
||||
description: '描述信息描述信息描述信息',
|
||||
datetime: '2017-08-07',
|
||||
type: '消息'
|
||||
},
|
||||
{
|
||||
id: '000000007',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '朱偏右 回复了你',
|
||||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||
datetime: '2017-08-07',
|
||||
type: '消息'
|
||||
},
|
||||
{
|
||||
id: '000000008',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '标题',
|
||||
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||
datetime: '2017-08-07',
|
||||
type: '消息'
|
||||
},
|
||||
{
|
||||
id: '000000009',
|
||||
title: '任务名称',
|
||||
description: '任务需要在 2017-01-12 20:00 前启动',
|
||||
extra: '未开始',
|
||||
status: 'todo',
|
||||
type: '待办'
|
||||
},
|
||||
{
|
||||
id: '000000010',
|
||||
title: '第三方紧急代码变更',
|
||||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||
extra: '马上到期',
|
||||
status: 'urgent',
|
||||
type: '待办'
|
||||
},
|
||||
{
|
||||
id: '000000011',
|
||||
title: '信息安全考试',
|
||||
description: '指派竹尔于 2017-01-09 前完成更新并发布',
|
||||
extra: '已耗时 8 天',
|
||||
status: 'doing',
|
||||
type: '待办'
|
||||
},
|
||||
{
|
||||
id: '000000012',
|
||||
title: 'ABCD 版本发布',
|
||||
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||
extra: '进行中',
|
||||
status: 'processing',
|
||||
type: '待办'
|
||||
}
|
||||
]);
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.cdr.detectChanges();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
clear(type: string): void {
|
||||
this.msg.success(`清空了 ${type}`);
|
||||
}
|
||||
|
||||
select(res: any): void {
|
||||
this.msg.success(`点击了 ${res.title} 的 ${res.item.title}`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
<div class="alain-pro__header-item position-relative" (click)="show()">
|
||||
<nz-badge class="brand-top-right" style="right: 3px; line-height: 34px" nzShowDot [nzStatus]="status"></nz-badge>
|
||||
<i nz-tooltip="Public Chat" nz-icon nzType="message" class="alain-pro__header-item-icon"></i>
|
||||
</div>
|
||||
@ -0,0 +1,42 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { LayoutProWidgetQuickChatService } from './quick-chat.service';
|
||||
|
||||
@Component({
|
||||
selector: 'quick-chat-status',
|
||||
templateUrl: './quick-chat-status.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetQuickChatStatusComponent implements OnInit, OnDestroy {
|
||||
private status$!: Subscription;
|
||||
|
||||
status = 'default';
|
||||
|
||||
constructor(private srv: LayoutProWidgetQuickChatService, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
show(): void {
|
||||
if (this.srv.showDialog) {
|
||||
return;
|
||||
}
|
||||
this.srv.showDialog = true;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.status$ = this.srv.status.subscribe(res => {
|
||||
switch (res) {
|
||||
case 'online':
|
||||
this.status = 'success';
|
||||
break;
|
||||
default:
|
||||
this.status = 'default';
|
||||
break;
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.status$.unsubscribe();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<div class="quick-chat__bar">
|
||||
<strong class="quick-chat__bar--title" (click)="toggleCollapsed()">
|
||||
<div [ngClass]="{ 'quick-chat__bar--title-has-message': collapsed && hasMessage }">
|
||||
{{ !collapsed && inited ? 'Connecting...' : 'Ng Alain Pro' }}
|
||||
</div>
|
||||
</strong>
|
||||
<i nz-dropdown [nzDropdownMenu]="quickMenu" nz-icon nzType="ellipsis" class="quick-chat__bar--menu rotate-90"></i>
|
||||
<nz-dropdown-menu #quickMenu="nzDropdownMenu">
|
||||
<ul nz-menu nzSelectable>
|
||||
<li nz-menu-item>Add</li>
|
||||
<li nz-menu-item>Edit</li>
|
||||
<li nz-menu-item>Remove</li>
|
||||
</ul>
|
||||
</nz-dropdown-menu>
|
||||
<i nz-icon nzType="close" class="quick-chat__bar--close" (click)="close()"></i>
|
||||
</div>
|
||||
<div class="quick-chat__body" [ngClass]="{ 'quick-chat__collapsed': collapsed }">
|
||||
<div class="quick-chat__content">
|
||||
<div class="chat__scroll-container chat__message-container" scrollbar #messageScrollbar="scrollbarComp">
|
||||
<div *ngFor="let m of messages" class="chat__message chat__message-{{ m.dir }}">
|
||||
<ng-container [ngSwitch]="m.type">
|
||||
<div *ngSwitchCase="'only-text'" class="chat__message-text" [innerHTML]="m.msg"></div>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<div class="chat__message-avatar" *ngIf="m.dir === 'left'">
|
||||
<img class="chat__user-avatar" src="{{ m.mp }}" />
|
||||
</div>
|
||||
<div class="chat__message-msg">
|
||||
<strong class="chat__message-msg--name" *ngIf="m.name">{{ m.name }}</strong>
|
||||
<div class="chat__message-msg--text" *ngIf="m.type === 'text'" [innerHTML]="m.msg"></div>
|
||||
<div class="chat__message-msg--image" *ngIf="m.type === 'image'">
|
||||
<img height="40" src="{{ m.msg }}" />
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-chat__reply">
|
||||
<textarea
|
||||
class="quick-chat__reply--ipt scrollbar"
|
||||
[(ngModel)]="text"
|
||||
(keydown.enter)="enterSend($event)"
|
||||
placeholder="Type your message..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
131
src/app/layout/pro/components/quick-chat/quick-chat.component.ts
Normal file
131
src/app/layout/pro/components/quick-chat/quick-chat.component.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { BooleanInput, InputBoolean, InputNumber, NumberInput } from '@delon/util';
|
||||
import { ScrollbarDirective } from '@shared';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { LayoutProWidgetQuickChatService } from './quick-chat.service';
|
||||
|
||||
@Component({
|
||||
selector: 'quick-chat',
|
||||
templateUrl: './quick-chat.component.html',
|
||||
host: {
|
||||
'[class.quick-chat]': 'true',
|
||||
'[class.quick-chat__collapsed]': 'collapsed',
|
||||
'[class.d-none]': '!showDialog'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetQuickChatComponent implements OnInit, OnDestroy {
|
||||
static ngAcceptInputType_height: NumberInput;
|
||||
static ngAcceptInputType_width: BooleanInput;
|
||||
static ngAcceptInputType_collapsed: BooleanInput;
|
||||
|
||||
private unsubscribe$ = new Subject<void>();
|
||||
messages: any[] = [
|
||||
{ type: 'only-text', msg: '2018-12-12' },
|
||||
{
|
||||
type: 'text',
|
||||
dir: 'left',
|
||||
mp: './assets/logo-color.svg',
|
||||
msg: '请<span class="text-success">一句话</span>描述您的问题,我们来帮您解决并转到合适的人工服务。😎'
|
||||
}
|
||||
];
|
||||
text = '';
|
||||
inited?: boolean;
|
||||
hasMessage = false;
|
||||
|
||||
@ViewChild('messageScrollbar', { static: true }) messageScrollbar?: ScrollbarDirective;
|
||||
|
||||
// #region fileds
|
||||
@Input() @InputNumber() height = 380;
|
||||
@Input() @InputNumber() @HostBinding('style.width.px') width = 320;
|
||||
@Input() @InputBoolean() collapsed = true;
|
||||
@Output() readonly collapsedChange = new EventEmitter<boolean>();
|
||||
@Output() readonly closed = new EventEmitter<boolean>();
|
||||
// #endregion
|
||||
|
||||
constructor(private srv: LayoutProWidgetQuickChatService, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
get showDialog(): boolean {
|
||||
return this.srv.showDialog;
|
||||
}
|
||||
|
||||
private scrollToBottom(): void {
|
||||
this.cdr.detectChanges();
|
||||
setTimeout(() => this.messageScrollbar!.scrollToBottom());
|
||||
}
|
||||
|
||||
toggleCollapsed(): void {
|
||||
this.hasMessage = false;
|
||||
this.collapsed = !this.collapsed;
|
||||
this.collapsedChange.emit(this.collapsed);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.srv.close();
|
||||
this.closed.emit(true);
|
||||
}
|
||||
|
||||
enterSend(e: Event): void {
|
||||
if ((e as KeyboardEvent).keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
this.send();
|
||||
}
|
||||
|
||||
send(): boolean {
|
||||
if (!this.text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof this.inited === 'undefined') {
|
||||
this.inited = true;
|
||||
}
|
||||
const item = {
|
||||
type: 'text',
|
||||
msg: this.text,
|
||||
dir: 'right'
|
||||
};
|
||||
this.srv.send(item);
|
||||
this.messages.push(item);
|
||||
this.text = '';
|
||||
this.scrollToBottom();
|
||||
return false;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const { srv, messages, unsubscribe$ } = this;
|
||||
srv.message.pipe(takeUntil(unsubscribe$)).subscribe(res => {
|
||||
if (this.collapsed) {
|
||||
this.hasMessage = true;
|
||||
}
|
||||
messages.push(res);
|
||||
this.scrollToBottom();
|
||||
});
|
||||
srv.status.pipe(takeUntil(unsubscribe$)).subscribe(res => {
|
||||
this.inited = res === 'online' ? false : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const { unsubscribe$ } = this;
|
||||
unsubscribe$.next();
|
||||
unsubscribe$.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Observable, Subject, Subscription } from 'rxjs';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
|
||||
export type QuickChatStatus = 'online' | 'offline';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LayoutProWidgetQuickChatService implements OnDestroy {
|
||||
private url = 'wss://echo.websocket.org/?encoding=text';
|
||||
private _ws!: WebSocketSubject<{}>;
|
||||
private $statusOrg = new Subject();
|
||||
private messageOrg$: Subscription | null = null;
|
||||
private $status = new Subject<QuickChatStatus>();
|
||||
private $message = new Subject<{}>();
|
||||
showDialog = true;
|
||||
|
||||
constructor() {
|
||||
this.$statusOrg.subscribe((res: any) => {
|
||||
this.$status.next(res.type === 'open' ? 'online' : 'offline');
|
||||
});
|
||||
}
|
||||
|
||||
get ws(): WebSocketSubject<{}> {
|
||||
return this._ws!;
|
||||
}
|
||||
|
||||
get message(): Observable<{}> {
|
||||
return this.$message.asObservable();
|
||||
}
|
||||
|
||||
get status(): Observable<QuickChatStatus> {
|
||||
return this.$status.asObservable();
|
||||
}
|
||||
|
||||
open(): WebSocketSubject<{}> {
|
||||
if (this._ws) {
|
||||
return this._ws;
|
||||
}
|
||||
|
||||
this._ws = webSocket({
|
||||
url: this.url,
|
||||
serializer: (value: any) => JSON.stringify(value),
|
||||
deserializer: (e: MessageEvent) => {
|
||||
const res = JSON.parse(e.data);
|
||||
res.dir = 'left';
|
||||
res.mp = './assets/logo-color.svg';
|
||||
return res;
|
||||
},
|
||||
openObserver: this.$statusOrg,
|
||||
closeObserver: this.$statusOrg
|
||||
});
|
||||
return this._ws;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.showDialog = false;
|
||||
if (this.messageOrg$) {
|
||||
this.messageOrg$.unsubscribe();
|
||||
this.messageOrg$ = null;
|
||||
}
|
||||
}
|
||||
|
||||
send(msg: {}): void {
|
||||
if (!this._ws) {
|
||||
this.open();
|
||||
}
|
||||
if (!this.messageOrg$) {
|
||||
this.messageOrg$ = this._ws.subscribe(res => this.$message.next(res));
|
||||
}
|
||||
this._ws.next(msg);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const { $statusOrg, $status, $message } = this;
|
||||
this.close();
|
||||
$statusOrg.complete();
|
||||
$status.complete();
|
||||
$message.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
<div class="p-md border-bottom-1">
|
||||
<button (click)="changeType(0)" nz-button [nzType]="type === 0 ? 'primary' : 'default'">Notifications</button>
|
||||
<button (click)="changeType(1)" nz-button [nzType]="type === 1 ? 'primary' : 'default'">Actions</button>
|
||||
<button (click)="changeType(2)" nz-button [nzType]="type === 2 ? 'primary' : 'default'">Settings</button>
|
||||
</div>
|
||||
<nz-spin [nzSpinning]="!data">
|
||||
<div *ngIf="!data" class="brand-page-loading"></div>
|
||||
<div *ngIf="data" class="p-lg min-width-lg">
|
||||
<nz-timeline *ngIf="type === 0" class="d-block pl-md pt-md">
|
||||
<nz-timeline-item *ngFor="let i of data.notifications" [nzDot]="dotTpl">
|
||||
<ng-template #dotTpl>
|
||||
<div class="md-sm p-sm icon-sm rounded-circle text-white bg-{{ i.dot.bg }}">
|
||||
<i nz-icon [nzType]="i.dot.icon"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="pl-lg">
|
||||
<strong>{{ i.time }}</strong>
|
||||
<div class="py-sm" [innerHTML]="i.content | html"></div>
|
||||
<div class="text-grey">{{ i.tags }}</div>
|
||||
</div>
|
||||
</nz-timeline-item>
|
||||
</nz-timeline>
|
||||
<ng-container *ngIf="type === 1">
|
||||
<div
|
||||
*ngFor="let i of data.actions; let last = last"
|
||||
class="rounded-md text-white position-relative bg-{{ i.bg }}"
|
||||
[ngClass]="{ 'mb-md': !last }"
|
||||
>
|
||||
<strong class="d-block p-md">{{ i.title }}</strong>
|
||||
<div class="px-md">{{ i.content }}</div>
|
||||
<div class="p-sm text-right">
|
||||
<button (click)="msg.success('Dismiss')" nz-button class="btn-flat text-white text-hover">Dismiss</button>
|
||||
<button (click)="msg.success('View')" nz-button class="btn-flat text-white text-hover">View</button>
|
||||
</div>
|
||||
<span nz-dropdown [nzDropdownMenu]="actionMenu" nzPlacement="bottomRight" class="dd-btn brand-top-right text-white">
|
||||
<i nz-icon nzType="ellipsis"></i>
|
||||
</span>
|
||||
<nz-dropdown-menu #actionMenu="nzDropdownMenu">
|
||||
<ul nz-menu>
|
||||
<li nz-menu-item (click)="msg.success('Item1')">Item1</li>
|
||||
<li nz-menu-item (click)="msg.success('Item2')">Item2</li>
|
||||
<li nz-menu-divider></li>
|
||||
<li nz-menu-item (click)="msg.success('Item3')">Item3</li>
|
||||
</ul>
|
||||
</nz-dropdown-menu>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="type === 2">
|
||||
<h3 class="setting-drawer__title">Notifications</h3>
|
||||
<div class="setting-drawer__body-item">
|
||||
Enable notifications:
|
||||
<nz-switch [(ngModel)]="data.settings.notification" (ngModelChange)="updateSetting('notification', $event)"></nz-switch>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item">
|
||||
Enable audit log:
|
||||
<nz-switch [(ngModel)]="data.settings.audit_log" (ngModelChange)="updateSetting('audit_log', $event)"></nz-switch>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item">
|
||||
Notify on new orders:
|
||||
<nz-switch [(ngModel)]="data.settings.new_order" (ngModelChange)="updateSetting('new_order', $event)"></nz-switch>
|
||||
</div>
|
||||
<h3 class="setting-drawer__title mt-md">Orders</h3>
|
||||
<div class="setting-drawer__body-item">
|
||||
Enable order tracking:
|
||||
<nz-switch [(ngModel)]="data.settings.tracking_order" (ngModelChange)="updateSetting('tracking_order', $event)"></nz-switch>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item">
|
||||
Enable orders reports:
|
||||
<nz-switch [(ngModel)]="data.settings.reports_order" (ngModelChange)="updateSetting('reports_order', $event)"></nz-switch>
|
||||
</div>
|
||||
<h3 class="setting-drawer__title mt-md">Customers</h3>
|
||||
<div class="setting-drawer__body-item">
|
||||
Enable customer singup:
|
||||
<nz-switch [(ngModel)]="data.settings.new_customer" (ngModelChange)="updateSetting('new_customer', $event)"></nz-switch>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item">
|
||||
Enable customers reporting:
|
||||
<nz-switch [(ngModel)]="data.settings.reporting_customer" (ngModelChange)="updateSetting('reporting_customer', $event)"></nz-switch>
|
||||
</div>
|
||||
<h3 class="setting-drawer__title mt-md">Other</h3>
|
||||
<div class="setting-drawer__body-item">
|
||||
Weak Mode:
|
||||
<nz-switch [(ngModel)]="layout.colorWeak" (ngModelChange)="setLayout('colorWeak', $event)"></nz-switch>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</nz-spin>
|
||||
42
src/app/layout/pro/components/quick/quick-panel.component.ts
Normal file
42
src/app/layout/pro/components/quick/quick-panel.component.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
|
||||
import { BrandService } from '../../pro.service';
|
||||
import { ProLayout } from '../../pro.types';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-quick-panel',
|
||||
templateUrl: './quick-panel.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetQuickPanelComponent implements OnInit {
|
||||
type = 0;
|
||||
data: any;
|
||||
get layout(): ProLayout {
|
||||
return this.pro.layout;
|
||||
}
|
||||
|
||||
constructor(private pro: BrandService, private http: _HttpClient, private cd: ChangeDetectorRef, public msg: NzMessageService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.http.get('/quick').subscribe(res => {
|
||||
this.data = res;
|
||||
this.changeType(0);
|
||||
});
|
||||
}
|
||||
|
||||
changeType(type: number): void {
|
||||
this.type = type;
|
||||
// wait checkbox & switch render
|
||||
setTimeout(() => this.cd.detectChanges());
|
||||
}
|
||||
|
||||
updateSetting(_type: string, _value: any): void {
|
||||
this.msg.success('Success!');
|
||||
}
|
||||
|
||||
setLayout(name: string, value: any): void {
|
||||
this.pro.setLayout(name, value);
|
||||
}
|
||||
}
|
||||
1
src/app/layout/pro/components/quick/quick.component.html
Normal file
1
src/app/layout/pro/components/quick/quick.component.html
Normal file
@ -0,0 +1 @@
|
||||
<i nz-tooltip="Quick panel" nz-icon nzType="appstore" class="alain-pro__header-item-icon"></i>
|
||||
51
src/app/layout/pro/components/quick/quick.component.ts
Normal file
51
src/app/layout/pro/components/quick/quick.component.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Direction, Directionality } from '@angular/cdk/bidi';
|
||||
import { ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||
import { DrawerHelper } from '@delon/theme';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { LayoutProWidgetQuickPanelComponent } from './quick-panel.component';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-quick',
|
||||
templateUrl: './quick.component.html',
|
||||
host: {
|
||||
'[class.alain-pro__header-item]': 'true'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetQuickComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private dir: Direction = 'ltr';
|
||||
|
||||
constructor(private drawerHelper: DrawerHelper, @Optional() private directionality: Directionality) {}
|
||||
|
||||
@HostListener('click')
|
||||
show(): void {
|
||||
this.drawerHelper
|
||||
.create(``, LayoutProWidgetQuickPanelComponent, null, {
|
||||
size: 480,
|
||||
drawerOptions: {
|
||||
nzTitle: undefined,
|
||||
nzPlacement: this.dir === 'rtl' ? 'left' : 'right',
|
||||
nzBodyStyle: {
|
||||
'min-height': '100%',
|
||||
padding: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dir = this.directionality.value;
|
||||
this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => {
|
||||
this.dir = direction;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
23
src/app/layout/pro/components/rtl/rtl.component.ts
Normal file
23
src/app/layout/pro/components/rtl/rtl.component.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
|
||||
import { RTLService } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-rtl',
|
||||
template: `
|
||||
<button nz-button nzType="link" class="alain-pro__header-item-icon">
|
||||
{{ rtl.nextDir | uppercase }}
|
||||
</button>
|
||||
`,
|
||||
host: {
|
||||
'[class.alain-pro__header-item]': 'true'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetRTLComponent {
|
||||
constructor(public rtl: RTLService) {}
|
||||
|
||||
@HostListener('click')
|
||||
toggleDirection(): void {
|
||||
this.rtl.toggle();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<i nz-icon nzType="search"></i>
|
||||
<div class="alain-pro__header-search-input ant-select-auto-complete ant-select">
|
||||
<input #ipt placeholder="站内搜索" nz-input [nzAutocomplete]="searchAuto" [(ngModel)]="q" (input)="onSearch()" (blur)="show = false" />
|
||||
</div>
|
||||
<nz-autocomplete #searchAuto>
|
||||
<nz-auto-option *ngFor="let item of list" [nzValue]="item.no">
|
||||
{{ item.no }}
|
||||
</nz-auto-option>
|
||||
</nz-autocomplete>
|
||||
49
src/app/layout/pro/components/search/search.component.ts
Normal file
49
src/app/layout/pro/components/search/search.component.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-search',
|
||||
templateUrl: 'search.component.html',
|
||||
host: {
|
||||
'[class.alain-pro__header-item]': 'true',
|
||||
'[class.alain-pro__header-search]': 'true',
|
||||
'[class.alain-pro__header-search-show]': 'show'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetSearchComponent implements OnDestroy {
|
||||
@ViewChild('ipt', { static: true }) private ipt!: ElementRef<HTMLInputElement>;
|
||||
show = false;
|
||||
q = '';
|
||||
search$ = new Subject<string>();
|
||||
list: any[] = [];
|
||||
|
||||
constructor(http: _HttpClient, cdr: ChangeDetectorRef) {
|
||||
this.search$
|
||||
.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
switchMap((q: string) => http.get('/user', { no: q, pi: 1, ps: 5 }))
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.list = res.list;
|
||||
cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
onSearch(): void {
|
||||
this.search$.next(this.ipt.nativeElement.value);
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
_click(): void {
|
||||
this.ipt.nativeElement.focus();
|
||||
this.show = true;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.search$.unsubscribe();
|
||||
}
|
||||
}
|
||||
25
src/app/layout/pro/components/user/user.component.html
Normal file
25
src/app/layout/pro/components/user/user.component.html
Normal file
@ -0,0 +1,25 @@
|
||||
<div nz-dropdown [nzDropdownMenu]="userMenu" nzPlacement="bottomRight" class="alain-pro__header-item">
|
||||
<nz-avatar [nzSrc]="settings.user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
|
||||
{{ settings.user.name }}
|
||||
</div>
|
||||
<nz-dropdown-menu #userMenu="nzDropdownMenu">
|
||||
<div nz-menu class="width-sm">
|
||||
<div nz-menu-item routerLink="/pro/account/center">
|
||||
<i nz-icon nzType="user" class="mr-sm"></i>
|
||||
个人中心
|
||||
</div>
|
||||
<div nz-menu-item routerLink="/pro/account/settings">
|
||||
<i nz-icon nzType="setting" class="mr-sm"></i>
|
||||
个人设置
|
||||
</div>
|
||||
<div nz-menu-item routerLink="/exception/trigger">
|
||||
<i nz-icon nzType="close-circle" class="mr-sm"></i>
|
||||
触发错误
|
||||
</div>
|
||||
<li nz-menu-divider></li>
|
||||
<div nz-menu-item (click)="logout()">
|
||||
<i nz-icon nzType="logout" class="mr-sm"></i>
|
||||
退出登录
|
||||
</div>
|
||||
</div>
|
||||
</nz-dropdown-menu>
|
||||
29
src/app/layout/pro/components/user/user.component.ts
Normal file
29
src/app/layout/pro/components/user/user.component.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { SettingsService } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro-user',
|
||||
templateUrl: 'user.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProWidgetUserComponent implements OnInit {
|
||||
constructor(public settings: SettingsService, private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// mock
|
||||
const token = this.tokenService.get() || {
|
||||
token: 'nothing',
|
||||
name: 'Admin',
|
||||
avatar: './assets/logo-color.svg',
|
||||
email: 'cipchk@qq.com'
|
||||
};
|
||||
this.tokenService.set(token);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.tokenService.clear();
|
||||
this.router.navigateByUrl(this.tokenService.login_url!);
|
||||
}
|
||||
}
|
||||
19
src/app/layout/pro/components/widget/widget.component.html
Normal file
19
src/app/layout/pro/components/widget/widget.component.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!--Search-->
|
||||
<layout-pro-search class="hidden-xs"></layout-pro-search>
|
||||
<!--Link-->
|
||||
<!-- <a nz-tooltip nzTooltipTitle="使用文档" nzTooltipPlacement="bottom" class="hidden-xs" target="_blank"
|
||||
href="https://e.ng-alain.com/theme/pro" rel="noopener noreferrer" class="alain-pro__header-item">
|
||||
<i nz-icon nzType="question-circle"></i>
|
||||
</a> -->
|
||||
<!--Quick chat status-->
|
||||
<!-- <quick-chat-status class="hidden-xs"></quick-chat-status> -->
|
||||
<!--Notify-->
|
||||
<layout-pro-notify class="hidden-xs"></layout-pro-notify>
|
||||
<!--RTL-->
|
||||
<!-- <layout-pro-rtl></layout-pro-rtl> -->
|
||||
<!--User-->
|
||||
<layout-pro-user></layout-pro-user>
|
||||
<!--Languages-->
|
||||
<!-- <pro-langs></pro-langs> -->
|
||||
<!--Quick panel-->
|
||||
<!-- <layout-pro-quick class="hidden-xs"></layout-pro-quick> -->
|
||||
20
src/app/layout/pro/components/widget/widget.component.ts
Normal file
20
src/app/layout/pro/components/widget/widget.component.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
|
||||
@Component({
|
||||
selector: '[layout-pro-header-widget]',
|
||||
templateUrl: './widget.component.html',
|
||||
host: {
|
||||
'[class.alain-pro__header-right]': 'true'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProHeaderWidgetComponent {
|
||||
constructor(private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||
|
||||
logout(): void {
|
||||
this.tokenService.clear();
|
||||
this.router.navigateByUrl(this.tokenService.login_url!);
|
||||
}
|
||||
}
|
||||
1
src/app/layout/pro/index.md
Normal file
1
src/app/layout/pro/index.md
Normal file
@ -0,0 +1 @@
|
||||
[Document](https://e.ng-alain.com/theme/pro)
|
||||
67
src/app/layout/pro/index.ts
Normal file
67
src/app/layout/pro/index.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/* eslint-disable import/order */
|
||||
// #region exports
|
||||
|
||||
export * from './pro.types';
|
||||
export * from './pro.service';
|
||||
export * from './pro.component';
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region widgets
|
||||
|
||||
import { LayoutProFooterComponent } from './components/footer/footer.component';
|
||||
import { LayoutProHeaderComponent } from './components/header/header.component';
|
||||
import { LayoutProLogoComponent } from './components/logo/logo.component';
|
||||
import { LayoutProMenuComponent } from './components/menu/menu.component';
|
||||
import { LayoutProWidgetNotifyComponent } from './components/notify/notify.component';
|
||||
import { LayoutProWidgetQuickChatStatusComponent } from './components/quick-chat/quick-chat-status.component';
|
||||
import { LayoutProWidgetQuickChatComponent } from './components/quick-chat/quick-chat.component';
|
||||
import { LayoutProWidgetQuickComponent } from './components/quick/quick.component';
|
||||
import { LayoutProWidgetRTLComponent } from './components/rtl/rtl.component';
|
||||
import { LayoutProWidgetSearchComponent } from './components/search/search.component';
|
||||
import { LayoutProWidgetUserComponent } from './components/user/user.component';
|
||||
import { LayoutProHeaderWidgetComponent } from './components/widget/widget.component';
|
||||
import { LayoutProWidgetQuickPanelComponent } from './components/quick/quick-panel.component';
|
||||
import { ProSettingDrawerComponent } from './setting-drawer/setting-drawer.component';
|
||||
|
||||
const PRO_WIDGETS = [
|
||||
LayoutProHeaderWidgetComponent,
|
||||
LayoutProWidgetNotifyComponent,
|
||||
LayoutProWidgetSearchComponent,
|
||||
LayoutProWidgetUserComponent,
|
||||
LayoutProWidgetQuickComponent,
|
||||
LayoutProWidgetQuickChatComponent,
|
||||
LayoutProWidgetQuickChatStatusComponent,
|
||||
LayoutProWidgetRTLComponent
|
||||
];
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region entry components
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region components
|
||||
|
||||
import { LayoutProComponent } from './pro.component';
|
||||
|
||||
export const PRO_COMPONENTS: Array<Type<void>> = [
|
||||
LayoutProComponent,
|
||||
LayoutProMenuComponent,
|
||||
LayoutProLogoComponent,
|
||||
LayoutProHeaderComponent,
|
||||
LayoutProFooterComponent,
|
||||
LayoutProWidgetQuickPanelComponent,
|
||||
ProSettingDrawerComponent,
|
||||
...PRO_WIDGETS
|
||||
];
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region shared components
|
||||
|
||||
import { ProPageModule } from './shared/page';
|
||||
import { Type } from '@angular/core';
|
||||
export const PRO_SHARED_MODULES = [ProPageModule];
|
||||
|
||||
// #endregion
|
||||
40
src/app/layout/pro/pro.component.html
Normal file
40
src/app/layout/pro/pro.component.html
Normal file
@ -0,0 +1,40 @@
|
||||
<ng-template #sideTpl>
|
||||
<nz-sider [nzTrigger]="null" [nzCollapsible]="true" [nzCollapsed]="isMobile ? false : pro.collapsed"
|
||||
[nzWidth]="pro.width" [nzCollapsedWidth]="pro.widthInCollapsed" class="alain-pro__sider"
|
||||
[ngClass]="{ 'alain-pro__sider-fixed': pro.fixSiderbar }">
|
||||
<layout-pro-logo class="alain-pro__sider-logo"></layout-pro-logo>
|
||||
<div class="alain-pro__side-nav" style="width: 100%; padding: 16px 0">
|
||||
<div class="alain-pro__side-nav-wrap" layout-pro-menu></div>
|
||||
</div>
|
||||
</nz-sider>
|
||||
</ng-template>
|
||||
<div class="ant-layout ant-layout-has-sider">
|
||||
<ng-container *ngIf="pro.menu === 'side' || isMobile">
|
||||
<nz-drawer *ngIf="isMobile" [nzWidth]="pro.width" nzWrapClassName="alain-pro__drawer" [nzVisible]="!pro.collapsed"
|
||||
[nzClosable]="false" nzPlacement="left" (nzOnClose)="pro.setCollapsed(true)">
|
||||
<ng-template nzDrawerContent>
|
||||
<ng-template [ngTemplateOutlet]="sideTpl"></ng-template>
|
||||
</ng-template>
|
||||
</nz-drawer>
|
||||
<ng-container *ngIf="!isMobile">
|
||||
<ng-template [ngTemplateOutlet]="sideTpl"></ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<div class="ant-layout alain-pro__main" [ngStyle]="getLayoutStyle">
|
||||
<layout-pro-header></layout-pro-header>
|
||||
<!--
|
||||
NOTICE: Route reuse strategy tag placeholder, please refer to: https://ng-alain.com/components/reuse-tab
|
||||
- Not supported top header fixed mode
|
||||
```html
|
||||
<reuse-tab></reuse-tab>
|
||||
```
|
||||
-->
|
||||
<div class="ant-layout-content alain-pro__body" [class.alain-pro__fetching]="isFetching"
|
||||
[ngStyle]="getContentStyle">
|
||||
<nz-spin class="alain-pro__fetching-icon" nzSpinning></nz-spin>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #settingHost></ng-template>
|
||||
<theme-btn></theme-btn>
|
||||
167
src/app/layout/pro/pro.component.ts
Normal file
167
src/app/layout/pro/pro.component.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { BreakpointObserver, MediaMatcher } from '@angular/cdk/layout';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router } from '@angular/router';
|
||||
import { ReuseTabService } from '@delon/abc/reuse-tab';
|
||||
import { RTL, RTLService } from '@delon/theme';
|
||||
import { ScrollService, updateHostClass } from '@delon/util/browser';
|
||||
import { environment } from '@env/environment';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { BrandService } from './pro.service';
|
||||
import { ProSettingDrawerComponent } from './setting-drawer/setting-drawer.component';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-pro',
|
||||
templateUrl: './pro.component.html'
|
||||
// NOTICE: If all pages using OnPush mode, you can turn it on and all `cdr.detectChanges()` codes
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LayoutProComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private queryCls?: string;
|
||||
@ViewChild('settingHost', { read: ViewContainerRef, static: false }) private settingHost!: ViewContainerRef;
|
||||
|
||||
isFetching = false;
|
||||
|
||||
get isMobile(): boolean {
|
||||
return this.pro.isMobile;
|
||||
}
|
||||
|
||||
get getLayoutStyle(): { [key: string]: string } | null {
|
||||
const { isMobile, fixSiderbar, collapsed, menu, width, widthInCollapsed } = this.pro;
|
||||
if (fixSiderbar && menu !== 'top' && !isMobile) {
|
||||
return {
|
||||
[this.rtl.dir === RTL ? 'paddingRight' : 'paddingLeft']: `${collapsed ? widthInCollapsed : width}px`
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get getContentStyle(): { [key: string]: string } {
|
||||
const { fixedHeader, headerHeight } = this.pro;
|
||||
return {
|
||||
margin: '24px 24px 0',
|
||||
'padding-top': `${fixedHeader ? headerHeight : 0}px`
|
||||
};
|
||||
}
|
||||
|
||||
private get body(): HTMLElement {
|
||||
return this.doc.body;
|
||||
}
|
||||
|
||||
constructor(
|
||||
bm: BreakpointObserver,
|
||||
mediaMatcher: MediaMatcher,
|
||||
router: Router,
|
||||
msg: NzMessageService,
|
||||
scroll: ScrollService,
|
||||
reuseTabSrv: ReuseTabService,
|
||||
private resolver: ComponentFactoryResolver,
|
||||
private renderer: Renderer2,
|
||||
public pro: BrandService,
|
||||
@Inject(DOCUMENT) private doc: any,
|
||||
// private cdr: ChangeDetectorRef
|
||||
private rtl: RTLService
|
||||
) {
|
||||
// scroll to top in change page
|
||||
router.events.pipe(takeUntil(this.destroy$)).subscribe(evt => {
|
||||
if (!this.isFetching && evt instanceof RouteConfigLoadStart) {
|
||||
this.isFetching = true;
|
||||
scroll.scrollToTop();
|
||||
}
|
||||
if (evt instanceof NavigationError) {
|
||||
this.isFetching = false;
|
||||
msg.error(`无法加载${evt.url}路由`, { nzDuration: 1000 * 3 });
|
||||
return;
|
||||
}
|
||||
if (!(evt instanceof NavigationEnd)) {
|
||||
return;
|
||||
}
|
||||
this.isFetching = false;
|
||||
// If have already cached router, should be don't need scroll to top
|
||||
if (!reuseTabSrv.exists(evt.url)) {
|
||||
scroll.scrollToTop();
|
||||
}
|
||||
});
|
||||
|
||||
// media
|
||||
const query: { [key: string]: string } = {
|
||||
'screen-xs': '(max-width: 575px)',
|
||||
'screen-sm': '(min-width: 576px) and (max-width: 767px)',
|
||||
'screen-md': '(min-width: 768px) and (max-width: 991px)',
|
||||
'screen-lg': '(min-width: 992px) and (max-width: 1199px)',
|
||||
'screen-xl': '(min-width: 1200px)'
|
||||
};
|
||||
bm.observe([
|
||||
'(min-width: 1200px)',
|
||||
'(min-width: 992px) and (max-width: 1199px)',
|
||||
'(min-width: 768px) and (max-width: 991px)',
|
||||
'(min-width: 576px) and (max-width: 767px)',
|
||||
'(max-width: 575px)'
|
||||
]).subscribe(() => {
|
||||
this.queryCls = Object.keys(query).find(key => mediaMatcher.matchMedia(query[key]).matches);
|
||||
this.setClass();
|
||||
});
|
||||
}
|
||||
|
||||
private setClass(): void {
|
||||
const { body, renderer, queryCls, pro } = this;
|
||||
updateHostClass(body, renderer, {
|
||||
['color-weak']: pro.layout.colorWeak,
|
||||
[`layout-fixed`]: pro.layout.fixed,
|
||||
[`aside-collapsed`]: pro.collapsed,
|
||||
['alain-pro']: true,
|
||||
[queryCls!]: true,
|
||||
[`alain-pro__content-${pro.layout.contentWidth}`]: true,
|
||||
[`alain-pro__fixed`]: pro.layout.fixedHeader,
|
||||
[`alain-pro__wide`]: pro.isFixed,
|
||||
[`alain-pro__dark`]: pro.theme === 'dark',
|
||||
[`alain-pro__light`]: pro.theme === 'light',
|
||||
[`alain-pro__menu-side`]: pro.isSideMenu,
|
||||
[`alain-pro__menu-top`]: pro.isTopMenu
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Setting componet for only developer
|
||||
if (!environment.production) {
|
||||
setTimeout(() => {
|
||||
const settingFactory = this.resolver.resolveComponentFactory(ProSettingDrawerComponent);
|
||||
this.settingHost.createComponent(settingFactory);
|
||||
}, 22);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const { pro, destroy$ } = this;
|
||||
pro.notify.pipe(takeUntil(destroy$)).subscribe(() => {
|
||||
this.setClass();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const { destroy$, body, pro } = this;
|
||||
destroy$.next();
|
||||
destroy$.complete();
|
||||
body.classList.remove(
|
||||
`alain-pro__content-${pro.layout.contentWidth}`,
|
||||
`alain-pro__fixed`,
|
||||
`alain-pro__wide`,
|
||||
`alain-pro__dark`,
|
||||
`alain-pro__light`
|
||||
);
|
||||
}
|
||||
}
|
||||
139
src/app/layout/pro/pro.service.ts
Normal file
139
src/app/layout/pro/pro.service.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Layout, SettingsService } from '@delon/theme';
|
||||
import { environment } from '@env/environment';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import { ProLayout, ProLayoutContentWidth, ProLayoutMenu, ProLayoutTheme } from './pro.types';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BrandService {
|
||||
private notify$ = new BehaviorSubject<string | null>(null);
|
||||
private _isMobile = false;
|
||||
|
||||
// #region fields
|
||||
|
||||
get notify(): Observable<string | null> {
|
||||
return this.notify$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify width of the sidebar, If you change it, muse be synchronize change less parameter:
|
||||
* ```less
|
||||
* @alain-pro-sider-menu-width: 256px;
|
||||
* ```
|
||||
*/
|
||||
readonly width = 256;
|
||||
|
||||
/**
|
||||
* Specify width of the sidebar after collapsed, If you change it, muse be synchronize change less parameter:
|
||||
* ```less
|
||||
* @menu-collapsed-width: 80px;
|
||||
* ```
|
||||
*/
|
||||
readonly widthInCollapsed = 80;
|
||||
|
||||
/**
|
||||
* Specify height of the header, If you change it, muse be synchronize change less parameter:
|
||||
* ```less
|
||||
* @alain-pro-header-height: 64px;
|
||||
* ```
|
||||
*/
|
||||
readonly headerHeight = 64;
|
||||
|
||||
/**
|
||||
* Specify distance from top for automatically hidden header
|
||||
*/
|
||||
readonly autoHideHeaderTop = 300;
|
||||
|
||||
get isMobile(): boolean {
|
||||
return this._isMobile;
|
||||
}
|
||||
|
||||
get layout(): ProLayout {
|
||||
return this.settings.layout as ProLayout;
|
||||
}
|
||||
|
||||
get collapsed(): boolean {
|
||||
return this.layout.collapsed;
|
||||
}
|
||||
|
||||
get theme(): ProLayoutTheme {
|
||||
return this.layout.theme;
|
||||
}
|
||||
|
||||
get menu(): ProLayoutMenu {
|
||||
return this.layout.menu;
|
||||
}
|
||||
|
||||
get contentWidth(): ProLayoutContentWidth {
|
||||
return this.layout.contentWidth;
|
||||
}
|
||||
|
||||
get fixedHeader(): boolean {
|
||||
return this.layout.fixedHeader;
|
||||
}
|
||||
|
||||
get autoHideHeader(): boolean {
|
||||
return this.layout.autoHideHeader;
|
||||
}
|
||||
|
||||
get fixSiderbar(): boolean {
|
||||
return this.layout.fixSiderbar;
|
||||
}
|
||||
|
||||
get onlyIcon(): boolean {
|
||||
return this.menu === 'side' ? false : this.layout.onlyIcon;
|
||||
}
|
||||
|
||||
/** Whether the top menu */
|
||||
get isTopMenu(): boolean {
|
||||
return this.menu === 'top' && !this.isMobile;
|
||||
}
|
||||
|
||||
/** Whether the side menu */
|
||||
get isSideMenu(): boolean {
|
||||
return this.menu === 'side' && !this.isMobile;
|
||||
}
|
||||
|
||||
/** Whether the fixed content */
|
||||
get isFixed(): boolean {
|
||||
return this.contentWidth === 'fixed';
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
constructor(bm: BreakpointObserver, private settings: SettingsService) {
|
||||
// fix layout data
|
||||
settings.setLayout({
|
||||
theme: 'dark',
|
||||
menu: 'side',
|
||||
contentWidth: 'fluid',
|
||||
fixedHeader: false,
|
||||
autoHideHeader: false,
|
||||
fixSiderbar: false,
|
||||
onlyIcon: true,
|
||||
...(environment as any).pro,
|
||||
...settings.layout // Browser cache
|
||||
});
|
||||
|
||||
const mobileMedia = 'only screen and (max-width: 767.99px)';
|
||||
bm.observe(mobileMedia).subscribe(state => this.checkMedia(state.matches));
|
||||
this.checkMedia(bm.isMatched(mobileMedia));
|
||||
}
|
||||
|
||||
private checkMedia(value: boolean): void {
|
||||
this._isMobile = value;
|
||||
this.layout.collapsed = this._isMobile;
|
||||
this.notify$.next('mobile');
|
||||
}
|
||||
|
||||
setLayout(name: string | Layout, value?: any): void {
|
||||
this.settings.setLayout(name, value);
|
||||
this.notify$.next('layout');
|
||||
}
|
||||
|
||||
setCollapsed(status?: boolean): void {
|
||||
this.setLayout('collapsed', typeof status !== 'undefined' ? status : !this.collapsed);
|
||||
}
|
||||
}
|
||||
43
src/app/layout/pro/pro.types.ts
Normal file
43
src/app/layout/pro/pro.types.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Layout, MenuInner } from '@delon/theme';
|
||||
|
||||
export type ProLayoutTheme = 'light' | 'dark';
|
||||
export type ProLayoutMenu = 'side' | 'top';
|
||||
export type ProLayoutContentWidth = 'fluid' | 'fixed';
|
||||
|
||||
export interface ProLayout extends Layout {
|
||||
theme: ProLayoutTheme;
|
||||
/**
|
||||
* menu position
|
||||
*/
|
||||
menu: ProLayoutMenu;
|
||||
/**
|
||||
* layout of content, only works when menu is top
|
||||
*/
|
||||
contentWidth: ProLayoutContentWidth;
|
||||
/**
|
||||
* sticky header
|
||||
*/
|
||||
fixedHeader: boolean;
|
||||
/**
|
||||
* auto hide header
|
||||
*/
|
||||
autoHideHeader: boolean;
|
||||
/**
|
||||
* sticky siderbar
|
||||
*/
|
||||
fixSiderbar: boolean;
|
||||
/**
|
||||
* Only icon of menu
|
||||
* Limited to a temporary solution [#2183](https://github.com/NG-ZORRO/ng-zorro-antd/issues/2183)
|
||||
*/
|
||||
onlyIcon: boolean;
|
||||
/**
|
||||
* Color weak
|
||||
*/
|
||||
colorWeak: boolean;
|
||||
}
|
||||
|
||||
export interface ProMenu extends MenuInner {
|
||||
_parent?: ProMenu | null;
|
||||
children?: ProMenu[];
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
<nz-drawer [(nzVisible)]="collapse" [nzPlacement]="dir === 'rtl' ? 'left' : 'right'" [nzWidth]="300"
|
||||
(nzOnClose)="toggle()">
|
||||
<div *nzDrawerContent class="setting-drawer__content">
|
||||
<div class="setting-drawer__body">
|
||||
<h3 class="setting-drawer__title">整体风格设置</h3>
|
||||
<div class="setting-drawer__blockChecbox">
|
||||
<div *ngFor="let t of themes" class="setting-drawer__blockChecbox-item" (click)="setLayout('theme', t.key)"
|
||||
[nz-tooltip]="t.title">
|
||||
<img src="{{ t.img }}" alt="{{ t.key }}" />
|
||||
<div *ngIf="layout.theme === t.key" class="setting-drawer__blockChecbox-selectIcon">
|
||||
<i nz-icon nzType="check"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-drawer__body setting-drawer__theme">
|
||||
<h3 class="setting-drawer__title">主题色</h3>
|
||||
<span *ngFor="let c of colors" (click)="changeColor(c.color)" nz-tooltip="c.key" class="setting-drawer__theme-tag"
|
||||
[ngStyle]="{ 'background-color': c.color }">
|
||||
<i *ngIf="color === c.color" nz-icon nzType="check"></i>
|
||||
</span>
|
||||
</div>
|
||||
<nz-divider></nz-divider>
|
||||
<div class="setting-drawer__body">
|
||||
<h3 class="setting-drawer__title">导航模式</h3>
|
||||
<div class="setting-drawer__blockChecbox">
|
||||
<div *ngFor="let t of menuModes" class="setting-drawer__blockChecbox-item" (click)="setLayout('menu', t.key)"
|
||||
nz-tooltip="{{ t.title }}">
|
||||
<img src="{{ t.img }}" alt="{{ t.key }}" />
|
||||
<div *ngIf="layout.menu === t.key" class="setting-drawer__blockChecbox-selectIcon">
|
||||
<i nz-icon nzType="check"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item">
|
||||
内容区域宽度
|
||||
<nz-select [(ngModel)]="layout.contentWidth" (ngModelChange)="setLayout('contentWidth', layout.contentWidth)"
|
||||
nzSize="small">
|
||||
<nz-option *ngFor="let i of contentWidths" [nzLabel]="i.title" [nzValue]="i.key" [nzDisabled]="i.disabled">
|
||||
</nz-option>
|
||||
</nz-select>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item">
|
||||
固定 Header
|
||||
<nz-switch nzSize="small" [(ngModel)]="layout.fixedHeader"
|
||||
(ngModelChange)="setLayout('fixedHeader', layout.fixedHeader)"></nz-switch>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item" nz-tooltip="{{ !brand.fixedHeader ? '固定 Header 时可配置' : '' }}"
|
||||
nzTooltipPlacement="left">
|
||||
<span [style.opacity]="!brand.fixedHeader ? 0.5 : 1">下滑时隐藏 Header</span>
|
||||
<nz-switch [nzDisabled]="!brand.fixedHeader" nzSize="small" [(ngModel)]="layout.autoHideHeader"
|
||||
(ngModelChange)="setLayout('autoHideHeader', layout.autoHideHeader)"></nz-switch>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item" nz-tooltip="{{ brand.menu === 'top' ? '侧边菜单布局时可配置' : '' }}"
|
||||
nzTooltipPlacement="left">
|
||||
<span [style.opacity]="brand.menu === 'top' ? 0.5 : 1">固定侧边菜单</span>
|
||||
<nz-switch [nzDisabled]="brand.menu === 'top'" nzSize="small" [(ngModel)]="layout.fixSiderbar"
|
||||
(ngModelChange)="setLayout('fixSiderbar', layout.fixSiderbar)"></nz-switch>
|
||||
</div>
|
||||
<div class="setting-drawer__body-item" nz-tooltip="{{ brand.menu === 'top' ? '' : '顶部菜单布局时可配置' }}"
|
||||
nzTooltipPlacement="left">
|
||||
<span [style.opacity]="brand.menu !== 'top' ? 0.5 : 1">只显示图标</span>
|
||||
<nz-switch [nzDisabled]="brand.menu !== 'top'" nzSize="small" [(ngModel)]="layout.onlyIcon"
|
||||
(ngModelChange)="setLayout('onlyIcon', layout.onlyIcon)"></nz-switch>
|
||||
</div>
|
||||
</div>
|
||||
<nz-divider></nz-divider>
|
||||
<div class="setting-drawer__body">
|
||||
<h3 class="setting-drawer__title">其他设置</h3>
|
||||
<div class="setting-drawer__body-item">
|
||||
色弱模式
|
||||
<nz-switch nzSize="small" [(ngModel)]="layout.colorWeak"
|
||||
(ngModelChange)="setLayout('colorWeak', layout.colorWeak)"></nz-switch>
|
||||
</div>
|
||||
</div>
|
||||
<nz-divider></nz-divider>
|
||||
<button (click)="copy()" type="button" nz-button nzBlock>拷贝设置</button>
|
||||
<nz-alert class="mt-md" nzType="warning" nzMessage="配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件"></nz-alert>
|
||||
</div>
|
||||
</nz-drawer>
|
||||
<div class="setting-drawer__handle" [ngClass]="{ 'setting-drawer__handle-opened': collapse }" (click)="toggle()">
|
||||
<i nz-icon [nzType]="!collapse ? 'setting' : 'close'" class="setting-drawer__handle-icon"></i>
|
||||
</div>
|
||||
240
src/app/layout/pro/setting-drawer/setting-drawer.component.ts
Normal file
240
src/app/layout/pro/setting-drawer/setting-drawer.component.ts
Normal file
@ -0,0 +1,240 @@
|
||||
import { Direction, Directionality } from '@angular/cdk/bidi';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, NgZone, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||
import { copy, LazyService } from '@delon/util';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { BrandService } from '../pro.service';
|
||||
import { ProLayout } from '../pro.types';
|
||||
|
||||
@Component({
|
||||
selector: 'pro-setting-drawer',
|
||||
templateUrl: './setting-drawer.component.html',
|
||||
preserveWhitespaces: false,
|
||||
host: {
|
||||
'[class.setting-drawer]': 'true',
|
||||
'[class.setting-drawer-rtl]': `dir === 'rtl'`
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ProSettingDrawerComponent implements OnInit, OnDestroy {
|
||||
private loadedLess = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
get layout(): ProLayout {
|
||||
return this.brand.layout;
|
||||
}
|
||||
|
||||
collapse = false;
|
||||
dir: Direction = 'ltr';
|
||||
|
||||
themes = [
|
||||
{
|
||||
key: 'dark',
|
||||
title: '暗色菜单风格',
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg'
|
||||
},
|
||||
{
|
||||
key: 'light',
|
||||
title: '亮色菜单风格',
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg'
|
||||
}
|
||||
];
|
||||
|
||||
color = '#2F54EB';
|
||||
colors = [
|
||||
{
|
||||
key: '薄暮',
|
||||
color: '#F5222D'
|
||||
},
|
||||
{
|
||||
key: '火山',
|
||||
color: '#FA541C'
|
||||
},
|
||||
{
|
||||
key: '日暮',
|
||||
color: '#FAAD14'
|
||||
},
|
||||
{
|
||||
key: '明青',
|
||||
color: '#13C2C2'
|
||||
},
|
||||
{
|
||||
key: '极光绿',
|
||||
color: '#52C41A'
|
||||
},
|
||||
{
|
||||
key: '拂晓蓝(默认)',
|
||||
color: '#1890ff'
|
||||
},
|
||||
{
|
||||
key: '极客蓝',
|
||||
color: '#2F54EB'
|
||||
},
|
||||
{
|
||||
key: '酱紫',
|
||||
color: '#722ED1'
|
||||
}
|
||||
];
|
||||
|
||||
menuModes = [
|
||||
{
|
||||
key: 'side',
|
||||
title: '侧边菜单布局',
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg'
|
||||
},
|
||||
{
|
||||
key: 'top',
|
||||
title: '顶部菜单布局',
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg'
|
||||
}
|
||||
];
|
||||
|
||||
contentWidths = [
|
||||
{
|
||||
key: 'fixed',
|
||||
title: '定宽',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
key: 'fluid',
|
||||
title: '流式',
|
||||
disabled: false
|
||||
}
|
||||
];
|
||||
|
||||
constructor(
|
||||
public brand: BrandService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private msg: NzMessageService,
|
||||
private lazy: LazyService,
|
||||
private zone: NgZone,
|
||||
@Inject(DOCUMENT) private doc: any,
|
||||
@Optional() private directionality: Directionality
|
||||
) {
|
||||
this.setLayout('menu', this.brand.menu, false);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dir = this.directionality.value;
|
||||
this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => {
|
||||
this.dir = direction;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private loadLess(): Promise<void> {
|
||||
if (this.loadedLess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.lazy
|
||||
.loadStyle('./assets/color.less', 'stylesheet/less')
|
||||
.then(() => {
|
||||
const lessConfigNode = this.doc.createElement('script');
|
||||
lessConfigNode.innerHTML = `
|
||||
window.less = {
|
||||
async: true,
|
||||
env: 'production',
|
||||
javascriptEnabled: true
|
||||
};
|
||||
`;
|
||||
this.doc.body.appendChild(lessConfigNode);
|
||||
})
|
||||
.then(() => this.lazy.loadScript('https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'))
|
||||
.then(() => {
|
||||
this.loadedLess = true;
|
||||
});
|
||||
}
|
||||
|
||||
private runLess(): void {
|
||||
const { color, zone, msg, cdr: cd } = this;
|
||||
const msgId = msg.loading(`正在编译主题!`, { nzDuration: 0 }).messageId;
|
||||
setTimeout(() => {
|
||||
zone.runOutsideAngular(() => {
|
||||
this.loadLess().then(() => {
|
||||
(window as any).less
|
||||
.modifyVars({
|
||||
[`@primary-color`]: color
|
||||
})
|
||||
.then(() => {
|
||||
msg.success('成功');
|
||||
msg.remove(msgId);
|
||||
zone.run(() => cd.detectChanges());
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.collapse = !this.collapse;
|
||||
}
|
||||
|
||||
changeColor(color: string): void {
|
||||
this.color = color;
|
||||
this.runLess();
|
||||
}
|
||||
|
||||
setLayout(name: string, value: any, cd: boolean = true): void {
|
||||
switch (name) {
|
||||
case 'menu':
|
||||
const isTop = value === 'top';
|
||||
this.contentWidths.find(w => w.key === 'fixed')!.disabled = !isTop;
|
||||
const newLayout = {
|
||||
...this.brand.layout,
|
||||
contentWidth: isTop ? 'fixed' : 'fluid',
|
||||
onlyIcon: isTop,
|
||||
collapsed: isTop && !this.brand.isMobile ? false : this.brand.layout.collapsed
|
||||
};
|
||||
this.brand.setLayout(newLayout);
|
||||
break;
|
||||
case 'fixedHeader':
|
||||
this.brand.setLayout('autoHideHeader', false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.brand.setLayout(name, value);
|
||||
if (cd) {
|
||||
setTimeout(() => {
|
||||
// Re-render G2 muse be trigger window resize
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
copy(): void {
|
||||
const { color, layout } = this;
|
||||
const vars: { [key: string]: string } = {
|
||||
[`@primary-color`]: color
|
||||
};
|
||||
const colorVars = Object.keys(vars)
|
||||
.map(key => `${key}: ${vars[key]};`)
|
||||
.join('\n');
|
||||
const layoutVars = Object.keys(layout)
|
||||
.filter(
|
||||
key => ~['theme', 'menu', 'contentWidth', 'fixedHeader', 'autoHideHeader', 'fixSiderbar', 'colorWeak', 'onlyIcon'].indexOf(key)
|
||||
)
|
||||
.map(key => {
|
||||
const value = layout[key];
|
||||
if (typeof value === 'boolean') {
|
||||
return ` ${key}: ${value},`;
|
||||
} else {
|
||||
return ` ${key}: '${value}',`;
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
copy(
|
||||
`在 [src/styles/theme.less] 配置以下:\n{{colorVars}}\n\n在 [src/environments/*] 的 pro 配置以下:\nexport const environment = {\n ...\n pro: {\n{{layoutVars}}\n }\n}`
|
||||
);
|
||||
this.msg.success(`拷贝成功,请根据剪切板内容进行替换`);
|
||||
}
|
||||
}
|
||||
3
src/app/layout/pro/shared/page/index.ts
Normal file
3
src/app/layout/pro/shared/page/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './page-grid.component';
|
||||
export * from './page-header-wrapper.component';
|
||||
export * from './page.module';
|
||||
4
src/app/layout/pro/shared/page/page-grid.component.html
Normal file
4
src/app/layout/pro/shared/page/page-grid.component.html
Normal file
@ -0,0 +1,4 @@
|
||||
<nz-spin [nzSpinning]="loading">
|
||||
<div *ngIf="loading" class="brand-page-loading"></div>
|
||||
<ng-content></ng-content>
|
||||
</nz-spin>
|
||||
80
src/app/layout/pro/shared/page/page-grid.component.ts
Normal file
80
src/app/layout/pro/shared/page/page-grid.component.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Optional,
|
||||
Renderer2
|
||||
} from '@angular/core';
|
||||
import { ReuseTabService } from '@delon/abc/reuse-tab';
|
||||
import { TitleService } from '@delon/theme';
|
||||
import { BooleanInput, InputBoolean } from '@delon/util';
|
||||
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { BrandService } from '../../pro.service';
|
||||
|
||||
@Component({
|
||||
selector: 'page-grid',
|
||||
templateUrl: './page-grid.component.html',
|
||||
host: {
|
||||
'[class.alain-pro__page-grid]': 'true',
|
||||
'[class.alain-pro__page-grid-no-spacing]': 'noSpacing',
|
||||
'[class.alain-pro__page-grid-wide]': 'isFixed'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ProPageGridComponent implements AfterViewInit, OnDestroy {
|
||||
static ngAcceptInputType_loading: BooleanInput;
|
||||
static ngAcceptInputType_noSpacing: BooleanInput;
|
||||
|
||||
private unsubscribe$ = new Subject<void>();
|
||||
|
||||
@Input() @InputBoolean() loading = false;
|
||||
@Input() @InputBoolean() noSpacing = false;
|
||||
@Input() style: NzSafeAny;
|
||||
@Input()
|
||||
set title(value: string) {
|
||||
if (value) {
|
||||
if (this.titleSrv) {
|
||||
this.titleSrv.setTitle(value);
|
||||
}
|
||||
if (this.reuseSrv) {
|
||||
this.reuseSrv.title = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isFixed(): boolean {
|
||||
return this.pro.isFixed;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
private rend: Renderer2,
|
||||
private pro: BrandService,
|
||||
@Optional() @Inject(TitleService) private titleSrv: TitleService,
|
||||
@Optional() @Inject(ReuseTabService) private reuseSrv: ReuseTabService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.style) {
|
||||
Object.keys(this.style).forEach((key: string) => {
|
||||
this.rend.setStyle(this.el.nativeElement, key, this.style[key]);
|
||||
});
|
||||
}
|
||||
this.pro.notify.pipe(takeUntil(this.unsubscribe$)).subscribe(() => this.cdr.markForCheck());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const { unsubscribe$ } = this;
|
||||
unsubscribe$.next();
|
||||
unsubscribe$.complete();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<nz-spin [nzSpinning]="loading">
|
||||
<ng-template [ngTemplateOutlet]="top"></ng-template>
|
||||
<page-header
|
||||
[wide]="pro.isFixed"
|
||||
[fixed]="false"
|
||||
[title]="title"
|
||||
[home]="home"
|
||||
[homeLink]="homeLink"
|
||||
[homeI18n]="homeI18n"
|
||||
[autoBreadcrumb]="autoBreadcrumb"
|
||||
[autoTitle]="autoTitle"
|
||||
[syncTitle]="syncTitle"
|
||||
[action]="action"
|
||||
[breadcrumb]="breadcrumb"
|
||||
[logo]="logo"
|
||||
[content]="content"
|
||||
[extra]="extra"
|
||||
[tab]="tab"
|
||||
><ng-template [ngTemplateOutlet]="phContent"></ng-template
|
||||
></page-header>
|
||||
<div class="alain-pro__page-header-content">
|
||||
<page-grid [noSpacing]="noSpacing" [style]="style">
|
||||
<ng-content></ng-content>
|
||||
</page-grid>
|
||||
</div>
|
||||
</nz-spin>
|
||||
@ -0,0 +1,74 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, TemplateRef } from '@angular/core';
|
||||
import { AlainConfigService, BooleanInput, InputBoolean } from '@delon/util';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { BrandService } from '../../pro.service';
|
||||
|
||||
@Component({
|
||||
selector: 'page-header-wrapper',
|
||||
templateUrl: './page-header-wrapper.component.html',
|
||||
host: {
|
||||
'[class.alain-pro__page-header-wrapper]': 'true'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ProPageHeaderWrapperComponent implements AfterViewInit, OnDestroy {
|
||||
static ngAcceptInputType_loading: BooleanInput;
|
||||
static ngAcceptInputType_autoBreadcrumb: BooleanInput;
|
||||
static ngAcceptInputType_autoTitle: BooleanInput;
|
||||
static ngAcceptInputType_syncTitle: BooleanInput;
|
||||
static ngAcceptInputType_noSpacing: BooleanInput;
|
||||
|
||||
private unsubscribe$ = new Subject<void>();
|
||||
|
||||
// #region page-header fields
|
||||
|
||||
@Input() title!: string | null | TemplateRef<void>;
|
||||
@Input() @InputBoolean() loading = false;
|
||||
@Input() home!: string;
|
||||
@Input() homeLink!: string;
|
||||
@Input() homeI18n!: string;
|
||||
/**
|
||||
* 自动生成导航,以当前路由从主菜单中定位
|
||||
*/
|
||||
@Input() @InputBoolean() autoBreadcrumb = true;
|
||||
/**
|
||||
* 自动生成标题,以当前路由从主菜单中定位
|
||||
*/
|
||||
@Input() @InputBoolean() autoTitle = true;
|
||||
/**
|
||||
* 是否自动将标题同步至 `TitleService`、`ReuseService` 下,仅 `title` 为 `string` 类型时有效
|
||||
*/
|
||||
@Input() @InputBoolean() syncTitle = true;
|
||||
@Input() breadcrumb!: TemplateRef<void>;
|
||||
@Input() logo!: TemplateRef<void>;
|
||||
@Input() action!: TemplateRef<void>;
|
||||
@Input() content!: TemplateRef<void>;
|
||||
@Input() extra!: TemplateRef<void>;
|
||||
@Input() tab!: TemplateRef<void>;
|
||||
@Input() phContent!: TemplateRef<void>;
|
||||
// #endregion
|
||||
|
||||
// #region fields
|
||||
|
||||
@Input() top!: TemplateRef<void>;
|
||||
@Input() @InputBoolean() noSpacing = false;
|
||||
@Input() style?: {};
|
||||
|
||||
// #endregion
|
||||
|
||||
constructor(public pro: BrandService, cog: AlainConfigService, private cdr: ChangeDetectorRef) {
|
||||
cog.attach(this, 'pageHeader', { syncTitle: true });
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.pro.notify.pipe(takeUntil(this.unsubscribe$)).subscribe(() => this.cdr.markForCheck());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const { unsubscribe$ } = this;
|
||||
unsubscribe$.next();
|
||||
unsubscribe$.complete();
|
||||
}
|
||||
}
|
||||
16
src/app/layout/pro/shared/page/page.module.ts
Normal file
16
src/app/layout/pro/shared/page/page.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PageHeaderModule } from '@delon/abc/page-header';
|
||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||
|
||||
import { ProPageGridComponent } from './page-grid.component';
|
||||
import { ProPageHeaderWrapperComponent } from './page-header-wrapper.component';
|
||||
|
||||
const COMPONENTS = [ProPageGridComponent, ProPageHeaderWrapperComponent];
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, NzSpinModule, PageHeaderModule],
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class ProPageModule {}
|
||||
58
src/app/layout/pro/styles/app/_content.less
Normal file
58
src/app/layout/pro/styles/app/_content.less
Normal file
@ -0,0 +1,58 @@
|
||||
@{alain-pro-prefix} {
|
||||
&__main {
|
||||
min-height: 100vh;
|
||||
background-color: @alain-pro-main-bg;
|
||||
|
||||
> .ant-layout-header {
|
||||
height: @alain-pro-header-height;
|
||||
line-height: @alain-pro-header-height;
|
||||
}
|
||||
|
||||
.router-ant();
|
||||
}
|
||||
|
||||
&__page {
|
||||
&-header {
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: -@alain-pro-content-margin -@alain-pro-content-margin 0;
|
||||
}
|
||||
&-content {
|
||||
margin: @alain-pro-content-margin @alain-pro-content-margin 0;
|
||||
}
|
||||
}
|
||||
&-grid {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
transition: 0.3s;
|
||||
&-wide {
|
||||
max-width: @alain-pro-wide;
|
||||
margin: 0 auto;
|
||||
}
|
||||
&-no-spacing {
|
||||
width: initial;
|
||||
margin: -@alain-pro-content-margin -@alain-pro-content-margin 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@{alain-pro-prefix}__fetching {
|
||||
&-icon {
|
||||
display: none;
|
||||
}
|
||||
& {
|
||||
@{alain-pro-prefix}__fetching-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @mobile-max) {
|
||||
@{alain-pro-prefix}__page-header-content {
|
||||
margin: @alain-pro-content-margin 10px 0;
|
||||
}
|
||||
}
|
||||
3
src/app/layout/pro/styles/app/_footer.less
Normal file
3
src/app/layout/pro/styles/app/_footer.less
Normal file
@ -0,0 +1,3 @@
|
||||
@{alain-pro-prefix}__footer {
|
||||
padding: 0;
|
||||
}
|
||||
104
src/app/layout/pro/styles/app/_header.less
Normal file
104
src/app/layout/pro/styles/app/_header.less
Normal file
@ -0,0 +1,104 @@
|
||||
@header-prefix: ~'@{alain-pro-prefix}__header';
|
||||
|
||||
@{header-prefix} {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: @alain-pro-header-height;
|
||||
padding: 0 12px 0 0;
|
||||
background: @alain-pro-header-bg;
|
||||
box-shadow: @alain-pro-header-box-shadow;
|
||||
&-logo {
|
||||
padding: 0 24px;
|
||||
}
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
&-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
height: @alain-pro-header-height;
|
||||
padding: 0 12px;
|
||||
line-height: @alain-pro-header-height;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
> i,
|
||||
&-icon {
|
||||
// fix dropdown
|
||||
font-size: @alain-pro-header-widgets-icon-fs !important;
|
||||
transform: none !important;
|
||||
}
|
||||
&,
|
||||
&-icon {
|
||||
color: @alain-pro-header-color;
|
||||
}
|
||||
&:hover {
|
||||
background: @alain-pro-header-hover-bg;
|
||||
&,
|
||||
@{header-prefix}-item-icon {
|
||||
color: @alain-pro-header-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-trigger {
|
||||
padding: 0 24px;
|
||||
@{header-prefix}-item-icon {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
}
|
||||
&-search {
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
&-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
&-hide {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @mobile-max) {
|
||||
@{header-prefix} {
|
||||
&-name {
|
||||
display: none;
|
||||
}
|
||||
&-trigger {
|
||||
padding: 0 12px;
|
||||
}
|
||||
&-logo {
|
||||
position: relative;
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layout-pro-header {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.layout-pro-header-rtl-mixin(@enabled) when(@enabled=true) {
|
||||
[dir='rtl'] {
|
||||
@{header-prefix} {
|
||||
&-fixed {
|
||||
right: inherit;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout-pro-header-rtl-mixin(@rtl-enabled);
|
||||
68
src/app/layout/pro/styles/app/_menu.less
Normal file
68
src/app/layout/pro/styles/app/_menu.less
Normal file
@ -0,0 +1,68 @@
|
||||
@{alain-pro-prefix} {
|
||||
&__menu {
|
||||
display: block;
|
||||
&-item {
|
||||
&--disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
&-only-icon {
|
||||
@{alain-pro-prefix}__menu-item {
|
||||
padding-right: 8px !important;
|
||||
padding-left: 8px !important;
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
@{alain-pro-prefix}__menu-icon {
|
||||
margin-right: 0;
|
||||
font-size: @alain-pro-top-nav-only-icon-fs;
|
||||
}
|
||||
}
|
||||
&-title {
|
||||
position: relative;
|
||||
&-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
background: @alain-pro-header-title-badge-bg;
|
||||
border-radius: 50%;
|
||||
> em {
|
||||
color: @alain-pro-header-title-badge-color;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-img {
|
||||
width: @alain-pro-sider-menu-img-wh !important;
|
||||
height: @alain-pro-sider-menu-img-wh !important;
|
||||
}
|
||||
}
|
||||
&__side-nav {
|
||||
@{alain-pro-prefix}__menu {
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&-text {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.@{ant-prefix}-menu-inline-collapsed {
|
||||
@{alain-pro-prefix}__menu-title-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -16px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
> em {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
148
src/app/layout/pro/styles/app/_sider.less
Normal file
148
src/app/layout/pro/styles/app/_sider.less
Normal file
@ -0,0 +1,148 @@
|
||||
@sider-prefix: ~'@{alain-pro-prefix}__sider';
|
||||
|
||||
@{sider-prefix} {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
&-logo {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: @alain-pro-header-height;
|
||||
padding-left: ((@menu-collapsed-width - 32px) / 2);
|
||||
overflow: hidden;
|
||||
line-height: @alain-pro-header-height;
|
||||
background: @alain-pro-header-logo-bg;
|
||||
transition: all 0.3s;
|
||||
img {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
h1 {
|
||||
display: inline-block;
|
||||
margin: 0 0 0 12px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: @alain-pro-logo-font-size;
|
||||
font-family: @alain-pro-logo-font-family;
|
||||
vertical-align: middle;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
&-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@{alain-pro-prefix}__side-nav-wrap {
|
||||
height: ~'calc(100vh - @{alain-pro-header-height})';
|
||||
padding-bottom: 24px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕 drawer 配置信息
|
||||
@{alain-pro-prefix}__drawer {
|
||||
.@{ant-prefix}-drawer {
|
||||
&-wrapper-body {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
&-body {
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当缩进时隐藏站点名称
|
||||
@{aside-collapsed-prefix} {
|
||||
@{sider-prefix}-logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
h1 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@{alain-pro-prefix}__light {
|
||||
@{sider-prefix} {
|
||||
background-color: @alain-pro-light-color;
|
||||
box-shadow: @alain-pro-light-slider-shadow;
|
||||
&-logo {
|
||||
background: @alain-pro-light-color;
|
||||
box-shadow: 1px 1px 0 0 @border-color-split;
|
||||
h1 {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
.@{ant-prefix}-menu-light {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alain-pro-sider-fixed-scroll-mixin(@mode) when(@mode='show') {
|
||||
.scrollbar-mixin(~'@{ider-prefix}-fixed @{alain-pro-prefix}__side-nav-wrap');
|
||||
}
|
||||
|
||||
.alain-pro-sider-fixed-scroll-mixin(@mode) when(@mode='hide') {
|
||||
@{sider-prefix}-fixed @{alain-pro-prefix}__side-nav-wrap {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
// IE
|
||||
-ms-scroll-chaining: chained;
|
||||
-ms-content-zooming: zoom;
|
||||
-ms-scroll-rails: none;
|
||||
-ms-content-zoom-limit-min: 100%;
|
||||
-ms-content-zoom-limit-max: 500%;
|
||||
-ms-scroll-snap-type: proximity;
|
||||
-ms-scroll-snap-points-x: snaplist(100%, 200%, 300%, 400%, 500%);
|
||||
-ms-overflow-style: none;
|
||||
// Firefox
|
||||
scrollbar-width: none;
|
||||
// Chrome
|
||||
&::-webkit-scrollbar {
|
||||
width: @alain-pro-sider-scrollbar-height;
|
||||
height: @alain-pro-sider-scrollbar-width;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 @alain-pro-sider-scrollbar-width @alain-pro-sider-scrollbar-track-color;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: @alain-pro-sider-scrollbar-thumb-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alain-pro-sider-fixed-scroll-mixin(@alain-pro-sider-fixed-scroll-mode);
|
||||
|
||||
.layout-pro-sider-rtl-mixin(@enabled) when(@enabled=true) {
|
||||
[dir='rtl'] {
|
||||
@{sider-prefix} {
|
||||
&-logo {
|
||||
padding-right: ((@menu-collapsed-width - 32px) / 2);
|
||||
padding-left: 0;
|
||||
h1 {
|
||||
margin-right: 12px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
&-fixed {
|
||||
right: 0;
|
||||
left: inherit;
|
||||
}
|
||||
}
|
||||
@{aside-collapsed-prefix} {
|
||||
@{sider-prefix}-logo {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout-pro-sider-rtl-mixin(@rtl-enabled);
|
||||
101
src/app/layout/pro/styles/app/_top-nav.less
Normal file
101
src/app/layout/pro/styles/app/_top-nav.less
Normal file
@ -0,0 +1,101 @@
|
||||
@top-nav-prefix: ~'@{alain-pro-prefix}__top-nav';
|
||||
|
||||
@{top-nav-prefix} {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: @alain-pro-header-height;
|
||||
padding: 0 12px 0 0;
|
||||
box-shadow: @alain-pro-header-box-shadow;
|
||||
transition: background 0.3s, width 0.2s;
|
||||
@{alain-pro-prefix}__menu {
|
||||
.@{ant-prefix}-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: @alain-pro-header-height;
|
||||
border: none;
|
||||
}
|
||||
&-wrap {
|
||||
flex: 1;
|
||||
padding-right: 8px;
|
||||
}
|
||||
&-item {
|
||||
height: 100%;
|
||||
.@{ant-prefix}-menu-submenu-title {
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-main {
|
||||
display: flex;
|
||||
height: @alain-pro-header-height;
|
||||
padding-left: 24px;
|
||||
&-wide {
|
||||
max-width: @alain-pro-wide;
|
||||
margin: auto;
|
||||
padding-left: 4px;
|
||||
}
|
||||
&-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
&-logo {
|
||||
width: 165px;
|
||||
h1 {
|
||||
margin: 0 0 0 12px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@{alain-pro-prefix}__menu-title-badge {
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
right: -16px;
|
||||
}
|
||||
}
|
||||
|
||||
@{alain-pro-prefix} {
|
||||
&__dark {
|
||||
@{top-nav-prefix} {
|
||||
@{alain-pro-prefix}__header-item {
|
||||
&,
|
||||
&-icon {
|
||||
color: @menu-dark-color;
|
||||
}
|
||||
&:hover,
|
||||
.@{ant-prefix}-popover-open {
|
||||
background: @menu-dark-item-active-bg;
|
||||
@{alain-pro-prefix}__header-item {
|
||||
&,
|
||||
&-icon {
|
||||
color: @menu-dark-highlight-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__light {
|
||||
@{top-nav-prefix} {
|
||||
background-color: #fff;
|
||||
h1 {
|
||||
color: #002140;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-pro-top-nav-rtl-mixin(@enabled) when(@enabled=true) {
|
||||
[dir='rtl'] {
|
||||
@{top-nav-prefix} {
|
||||
&-logo {
|
||||
h1 {
|
||||
margin-right: 12px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout-pro-top-nav-rtl-mixin(@rtl-enabled);
|
||||
6
src/app/layout/pro/styles/app/fix/_antd.less
Normal file
6
src/app/layout/pro/styles/app/fix/_antd.less
Normal file
@ -0,0 +1,6 @@
|
||||
@layout-body-background: #f8f8f8;
|
||||
|
||||
// Remoed card transition
|
||||
.@{ant-prefix}-card {
|
||||
transition: none;
|
||||
}
|
||||
5
src/app/layout/pro/styles/app/fix/_btn.less
Normal file
5
src/app/layout/pro/styles/app/fix/_btn.less
Normal file
@ -0,0 +1,5 @@
|
||||
.btn-flat {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
55
src/app/layout/pro/styles/app/fix/_delon.less
Normal file
55
src/app/layout/pro/styles/app/fix/_delon.less
Normal file
@ -0,0 +1,55 @@
|
||||
@{header-prefix}-item {
|
||||
.@{ant-prefix}-badge-count {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// full-content component
|
||||
@{full-content-prefix} {
|
||||
&__opened {
|
||||
layout-pro-header,
|
||||
@{alain-pro-prefix}__sider,
|
||||
reuse-tab {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
&__hidden-title {
|
||||
page-header {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// footer-toolbar component
|
||||
@{footer-toolbar-prefix} {
|
||||
z-index: 99;
|
||||
width: auto;
|
||||
&__body {
|
||||
@{alain-pro-prefix}__body {
|
||||
margin-bottom: @layout-gutter + @footer-toolbar-height !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@{alain-pro-prefix}__menu-side {
|
||||
@{footer-toolbar-prefix} {
|
||||
left: @alain-pro-sider-menu-width;
|
||||
}
|
||||
}
|
||||
@{alain-pro-prefix}__menu-top {
|
||||
@{footer-toolbar-prefix} {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@{aside-collapsed-prefix} {
|
||||
@{footer-toolbar-prefix} {
|
||||
left: @menu-collapsed-width;
|
||||
}
|
||||
}
|
||||
|
||||
@{page-header-prefix} {
|
||||
padding-right: @alain-pro-content-margin;
|
||||
padding-left: @alain-pro-content-margin;
|
||||
}
|
||||
3
src/app/layout/pro/styles/app/fix/_menu.less
Normal file
3
src/app/layout/pro/styles/app/fix/_menu.less
Normal file
@ -0,0 +1,3 @@
|
||||
.@{ant-prefix}-dropdown-menu-item {
|
||||
outline: none;
|
||||
}
|
||||
3
src/app/layout/pro/styles/app/fix/_print.less
Normal file
3
src/app/layout/pro/styles/app/fix/_print.less
Normal file
@ -0,0 +1,3 @@
|
||||
@page {
|
||||
size: a3;
|
||||
}
|
||||
8
src/app/layout/pro/styles/app/fix/index.less
Normal file
8
src/app/layout/pro/styles/app/fix/index.less
Normal file
@ -0,0 +1,8 @@
|
||||
@import './_antd.less';
|
||||
@import './_delon.less';
|
||||
|
||||
// components
|
||||
@import './_btn.less';
|
||||
@import './_menu.less';
|
||||
|
||||
@import './_print.less';
|
||||
8
src/app/layout/pro/styles/app/index.less
Normal file
8
src/app/layout/pro/styles/app/index.less
Normal file
@ -0,0 +1,8 @@
|
||||
@import './_menu.less';
|
||||
@import './_content.less';
|
||||
@import './_header.less';
|
||||
@import './_top-nav.less';
|
||||
@import './_sider.less';
|
||||
@import './_footer.less';
|
||||
|
||||
@import './fix/index.less';
|
||||
4
src/app/layout/pro/styles/index.less
Normal file
4
src/app/layout/pro/styles/index.less
Normal file
@ -0,0 +1,4 @@
|
||||
@import './theme-default.less';
|
||||
|
||||
@import './app/index.less';
|
||||
@import './widgets/index.less';
|
||||
5
src/app/layout/pro/styles/theme-compact.less
Normal file
5
src/app/layout/pro/styles/theme-compact.less
Normal file
@ -0,0 +1,5 @@
|
||||
@import '~@delon/theme/theme-compact.less';
|
||||
@import './theme-default.less';
|
||||
|
||||
@alain-pro-header-height: 56px;
|
||||
@alain-pro-header-widgets-icon-fs: 14px;
|
||||
9
src/app/layout/pro/styles/theme-dark.less
Normal file
9
src/app/layout/pro/styles/theme-dark.less
Normal file
@ -0,0 +1,9 @@
|
||||
@import '~@delon/theme/theme-dark.less';
|
||||
@import './theme-default.less';
|
||||
|
||||
@alain-pro-header-bg: @component-background;
|
||||
@alain-pro-header-logo-bg: @popover-background;
|
||||
@alain-pro-header-box-shadow: @shadow-1-down; // 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
@alain-pro-header-color: @text-color;
|
||||
@alain-pro-header-hover-color: #fff;
|
||||
@alain-pro-header-hover-bg: rgba(0, 0, 0, 0.025);
|
||||
53
src/app/layout/pro/styles/theme-default.less
Normal file
53
src/app/layout/pro/styles/theme-default.less
Normal file
@ -0,0 +1,53 @@
|
||||
@import '~@delon/theme/theme-default.less';
|
||||
|
||||
// Optimization for NG-ALAIN theme system
|
||||
// 针对 NG-ALAIN 主题系统的优化
|
||||
@badge-enabled: false;
|
||||
|
||||
// PRO
|
||||
@alain-pro-prefix: ~'.alain-pro';
|
||||
@aside-collapsed-prefix: ~'.aside-collapsed';
|
||||
@alain-pro-zindex: @zindex-base;
|
||||
@alain-pro-ease: cubic-bezier(0.25, 0, 0.15, 1);
|
||||
|
||||
@alain-pro-header-height: 64px;
|
||||
@alain-pro-header-bg: #fff;
|
||||
@alain-pro-header-logo-bg: #002140;
|
||||
@alain-pro-header-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
@alain-pro-header-color: rgba(0, 0, 0, 0.65);
|
||||
@alain-pro-header-hover-color: #000;
|
||||
@alain-pro-header-hover-bg: rgba(0, 0, 0, 0.025);
|
||||
@alain-pro-header-search-width: 210px;
|
||||
@alain-pro-header-widgets-icon-fs: 16px;
|
||||
@alain-pro-header-title-badge-bg: @primary-color;
|
||||
@alain-pro-header-title-badge-color: #fff;
|
||||
|
||||
@alain-pro-main-bg: @layout-body-background;
|
||||
|
||||
@alain-pro-top-nav-only-icon-fs: 16px;
|
||||
|
||||
@alain-pro-light-color: #fff;
|
||||
@alain-pro-light-slider-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
|
||||
|
||||
@alain-pro-logo-font-size: 20px;
|
||||
@alain-pro-logo-font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
|
||||
@alain-pro-content-margin: 24px;
|
||||
@alain-pro-wide: 1200px;
|
||||
|
||||
@alain-pro-sider-menu-width: 256px;
|
||||
@alain-pro-sider-menu-img-wh: 14px;
|
||||
|
||||
/**
|
||||
* 侧边栏固定时滚动条显示风格:
|
||||
* - hide: 隐藏滚动条(默认)
|
||||
* - show: 显示美化后滚动条
|
||||
*/
|
||||
@alain-pro-sider-fixed-scroll-mode: 'hide';
|
||||
@alain-pro-sider-scrollbar-width: 0;
|
||||
@alain-pro-sider-scrollbar-height: 0;
|
||||
@alain-pro-sider-scrollbar-track-color: transparent;
|
||||
@alain-pro-sider-scrollbar-thumb-color: transparent;
|
||||
|
||||
@brand-bordered-color: rgba(24, 28, 33, 0.06);
|
||||
@brand-scroll-width: 20px;
|
||||
34
src/app/layout/pro/styles/widgets/_bordered.less
Normal file
34
src/app/layout/pro/styles/widgets/_bordered.less
Normal file
@ -0,0 +1,34 @@
|
||||
.brand {
|
||||
// 让 `nz-row` & `nz-col` 包含有边框效果
|
||||
&-bordered {
|
||||
overflow: hidden;
|
||||
border: 1px solid @brand-bordered-color;
|
||||
|
||||
> [class*='ant-col-']::before,
|
||||
> [class*='ant-col-']::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
}
|
||||
|
||||
> [class*='ant-col-']::before {
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid @brand-bordered-color;
|
||||
}
|
||||
|
||||
> [class*='ant-col-']::after {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -1px;
|
||||
width: 0;
|
||||
border-left: 1px solid @brand-bordered-color;
|
||||
}
|
||||
}
|
||||
// 边框大小为 `2px`
|
||||
&-border-width-2 {
|
||||
border-width: 2px !important;
|
||||
}
|
||||
}
|
||||
27
src/app/layout/pro/styles/widgets/_brand.less
Normal file
27
src/app/layout/pro/styles/widgets/_brand.less
Normal file
@ -0,0 +1,27 @@
|
||||
.brand {
|
||||
// 加载容器
|
||||
&-page-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
min-height: 200px;
|
||||
}
|
||||
// `position: absolute` 定位到右上角
|
||||
&-top-right {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
// `position: absolute` 定位到左上角
|
||||
&-top-left {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
// `nz-range-picker` 日期宽度
|
||||
&-range-picker__date {
|
||||
display: inline-block;
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
13
src/app/layout/pro/styles/widgets/_collapse.less
Normal file
13
src/app/layout/pro/styles/widgets/_collapse.less
Normal file
@ -0,0 +1,13 @@
|
||||
.brand {
|
||||
// 将 `nz-ollapse` 的 arrow 图标转化为右边
|
||||
&-collapse-arrow-reverse {
|
||||
.@{ant-prefix}-collapse > .@{ant-prefix}-collapse-item > .@{ant-prefix}-collapse-header {
|
||||
padding-right: 40px;
|
||||
padding-left: 12px;
|
||||
.arrow {
|
||||
right: 16px;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/app/layout/pro/styles/widgets/_search.less
Normal file
51
src/app/layout/pro/styles/widgets/_search.less
Normal file
@ -0,0 +1,51 @@
|
||||
@header-search-prefix: ~'@{alain-pro-prefix}__header-search';
|
||||
|
||||
@{header-search-prefix} {
|
||||
display: flex;
|
||||
.anticon-search {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
&-input {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
transition: width 0.3s, margin-left 0.3s;
|
||||
.@{ant-prefix}-select-selection {
|
||||
background: transparent;
|
||||
}
|
||||
input {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
}
|
||||
}
|
||||
&-show {
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
@{header-search-prefix}-input {
|
||||
width: @alain-pro-header-search-width;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@{alain-pro-prefix}__dark {
|
||||
@{alain-pro-prefix}__top-nav {
|
||||
@{header-search-prefix}-show {
|
||||
.@{ant-prefix}-input {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/app/layout/pro/styles/widgets/_setting-drawer.less
Normal file
53
src/app/layout/pro/styles/widgets/_setting-drawer.less
Normal file
@ -0,0 +1,53 @@
|
||||
@{setting-drawer-prefix} {
|
||||
&__handle-opened {
|
||||
right: 300px !important;
|
||||
}
|
||||
|
||||
&__blockChecbox {
|
||||
display: flex;
|
||||
&-item {
|
||||
position: relative;
|
||||
margin-right: 16px;
|
||||
// box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: @border-radius-base;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
&-selectIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 15px;
|
||||
padding-left: 24px;
|
||||
color: @primary-color;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__handle {
|
||||
top: 114px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(0, 0, 0, 0.38);
|
||||
&-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-pro-setting-drawer-rtl-mixin(@enabled) when(@enabled=true) {
|
||||
@{setting-drawer-prefix}-rtl {
|
||||
@{setting-drawer-prefix} {
|
||||
&__handle-opened {
|
||||
right: inherit !important;
|
||||
left: 300px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout-pro-setting-drawer-rtl-mixin(@rtl-enabled);
|
||||
7
src/app/layout/pro/styles/widgets/index.less
Normal file
7
src/app/layout/pro/styles/widgets/index.less
Normal file
@ -0,0 +1,7 @@
|
||||
// For pro brand style
|
||||
@import './_brand.less';
|
||||
|
||||
@import './_setting-drawer.less';
|
||||
@import './_search.less';
|
||||
@import './_bordered.less';
|
||||
@import './_collapse.less';
|
||||
89
src/app/layout/pro/test/components/menu.component.spec.ts
Normal file
89
src/app/layout/pro/test/components/menu.component.spec.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { Component, DebugElement, ViewChild } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createTestContext } from '@delon/testing';
|
||||
import { AlainThemeModule, Menu, MenuService } from '@delon/theme';
|
||||
|
||||
import { LayoutModule } from '../../../layout.module';
|
||||
import { LayoutProMenuComponent } from '../../components/menu/menu.component';
|
||||
import { BrandService } from '../../pro.service';
|
||||
|
||||
describe('pro: layout-pro-menu', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let dl: DebugElement;
|
||||
let context: TestComponent;
|
||||
let srv: BrandService;
|
||||
let menuSrv: MenuService;
|
||||
let page: PageObject;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, RouterTestingModule.withRoutes([]), AlainThemeModule.forRoot(), LayoutModule],
|
||||
declarations: [TestComponent]
|
||||
});
|
||||
({ fixture, dl, context } = createTestContext(TestComponent));
|
||||
srv = TestBed.inject(BrandService);
|
||||
menuSrv = TestBed.inject(MenuService);
|
||||
page = new PageObject();
|
||||
});
|
||||
|
||||
describe('#menu', () => {
|
||||
it('should be ingored category menus', () => {
|
||||
page.appendMenu([{ text: '1', children: [{ text: '1-1' }, { text: '1-2' }] }]).checkCount(2);
|
||||
});
|
||||
|
||||
it('should be ingored menu item when _hidden is true', () => {
|
||||
page.appendMenu([{ text: '1', children: [{ text: '1-1', hide: true }, { text: '1-2' }] }]).checkCount(1);
|
||||
});
|
||||
|
||||
it('should be navigate router', fakeAsync(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
spyOn(router, 'navigateByUrl');
|
||||
page.appendMenu([{ text: '1', children: [{ text: '1-1', link: '/' }] }]);
|
||||
page.click();
|
||||
tick(100);
|
||||
fixture.detectChanges();
|
||||
expect(router.navigateByUrl).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should be auto closed collapse when router changed of mobile', fakeAsync(() => {
|
||||
spyOnProperty(srv, 'isMobile').and.returnValue(true);
|
||||
const setCollapsedSpy = spyOn(srv, 'setCollapsed');
|
||||
page.appendMenu([{ text: '1', children: [{ text: '1-1', link: '/' }] }]).click();
|
||||
tick(100);
|
||||
fixture.detectChanges();
|
||||
expect(setCollapsedSpy).toHaveBeenCalled();
|
||||
expect(setCollapsedSpy.calls.mostRecent().args[0]).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
class PageObject {
|
||||
appendMenu(menus: Menu[]): this {
|
||||
menuSrv.add(menus);
|
||||
fixture.detectChanges();
|
||||
return this;
|
||||
}
|
||||
checkCount(count: number = 0): this {
|
||||
expect(context.comp.menus!.length).toBe(count);
|
||||
return this;
|
||||
}
|
||||
click(pos: number = 0): this {
|
||||
const el = document.querySelectorAll('.alain-pro__menu-item')[pos] as HTMLElement;
|
||||
el.querySelector('a')!.click();
|
||||
fixture.detectChanges();
|
||||
return this;
|
||||
}
|
||||
whenStable(): Promise<void> {
|
||||
return fixture.whenStable();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: ` <div layout-pro-menu></div> `
|
||||
})
|
||||
class TestComponent {
|
||||
@ViewChild(LayoutProMenuComponent, { static: true }) comp!: LayoutProMenuComponent;
|
||||
}
|
||||
111
src/app/layout/pro/test/pro.component.spec.ts
Normal file
111
src/app/layout/pro/test/pro.component.spec.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { Component, DebugElement, ViewChild } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, TestBedStatic } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createTestContext } from '@delon/testing';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
|
||||
import { LayoutModule } from '../../layout.module';
|
||||
import { LayoutProComponent } from '../pro.component';
|
||||
import { BrandService } from '../pro.service';
|
||||
|
||||
describe('pro: layout-pro', () => {
|
||||
let injector: TestBedStatic;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let dl: DebugElement;
|
||||
let context: TestComponent;
|
||||
let srv: BrandService;
|
||||
let page: PageObject;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NoopAnimationsModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
AlainThemeModule.forRoot(),
|
||||
LayoutModule
|
||||
],
|
||||
declarations: [TestComponent]
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
({ fixture, dl, context } = createTestContext(TestComponent));
|
||||
srv = injector.inject(BrandService);
|
||||
page = new PageObject();
|
||||
});
|
||||
|
||||
describe('should set the body style', () => {
|
||||
it('when inited', () => {
|
||||
srv.setCollapsed(true);
|
||||
srv.setLayout('theme', 'dark');
|
||||
fixture.detectChanges();
|
||||
page.checkBodyClass('alain-pro__dark').checkBodyClass('aside-collapsed');
|
||||
});
|
||||
it('when destroy', () => {
|
||||
srv.setCollapsed(true);
|
||||
srv.setLayout('theme', 'dark');
|
||||
fixture.detectChanges();
|
||||
page.checkBodyClass('alain-pro__dark').checkBodyClass('aside-collapsed');
|
||||
context.comp.ngOnDestroy();
|
||||
page.checkBodyClass('alain-pro__dark', false);
|
||||
});
|
||||
it('when layout changed', () => {
|
||||
srv.setLayout('contentWidth', 'fixed');
|
||||
fixture.detectChanges();
|
||||
page.checkBodyClass('alain-pro__wide');
|
||||
srv.setLayout('contentWidth', 'fluid');
|
||||
fixture.detectChanges();
|
||||
page.checkBodyClass('alain-pro__wide', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#layout', () => {
|
||||
it('should be hide slider when screen is mobile', () => {
|
||||
const siderCls = '.alain-pro__sider';
|
||||
const isMobile = spyOnProperty(srv, 'isMobile', 'get');
|
||||
// Show sider when not mobile
|
||||
isMobile.and.returnValue(false);
|
||||
srv.setCollapsed(true);
|
||||
fixture.detectChanges();
|
||||
fixture
|
||||
.whenStable()
|
||||
.then(() => {
|
||||
page.checkCls(siderCls, true);
|
||||
// Hide sider when is mobile
|
||||
isMobile.and.returnValue(true);
|
||||
srv.setCollapsed(true);
|
||||
fixture.detectChanges();
|
||||
return fixture.whenStable();
|
||||
})
|
||||
.then(() => {
|
||||
page.checkCls(siderCls, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class PageObject {
|
||||
checkBodyClass(cls: string, status: boolean = true): this {
|
||||
expect(document.body.classList.contains(cls)).toBe(status);
|
||||
return this;
|
||||
}
|
||||
checkCls(cls: string, status: boolean = true): this {
|
||||
const nodes = document.querySelectorAll(cls);
|
||||
if (status) {
|
||||
expect(nodes.length).toBe(1);
|
||||
} else {
|
||||
expect(nodes.length).toBe(0);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: ` <layout-pro #comp></layout-pro> `
|
||||
})
|
||||
class TestComponent {
|
||||
@ViewChild('comp', { static: true }) comp!: LayoutProComponent;
|
||||
}
|
||||
81
src/app/layout/pro/test/pro.service.spec.ts
Normal file
81
src/app/layout/pro/test/pro.service.spec.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { TestBed, TestBedStatic } from '@angular/core/testing';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
import { environment } from '@env/environment';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
import { BrandService } from '../pro.service';
|
||||
|
||||
describe('pro: BrandService', () => {
|
||||
let injector: TestBedStatic;
|
||||
let srv: BrandService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = TestBed.configureTestingModule({
|
||||
imports: [AlainThemeModule.forRoot()],
|
||||
providers: [BrandService]
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => ((environment as any).pro = null));
|
||||
|
||||
describe('should be initialized configuration', () => {
|
||||
it('with default', () => {
|
||||
(environment as any).pro = null;
|
||||
spyOn(localStorage, 'getItem').and.returnValue(`null`);
|
||||
srv = injector.get(BrandService);
|
||||
expect(srv.theme).toBe('dark');
|
||||
expect(srv.menu).toBe('side');
|
||||
expect(srv.contentWidth).toBe('fluid');
|
||||
});
|
||||
it('with environment', () => {
|
||||
(environment as any).pro = { theme: 'light' };
|
||||
spyOn(localStorage, 'getItem').and.returnValue(`null`);
|
||||
srv = injector.get(BrandService);
|
||||
expect(srv.theme).toBe('light');
|
||||
});
|
||||
it('with settings', () => {
|
||||
(environment as any).pro = null;
|
||||
spyOn(localStorage, 'getItem').and.returnValue(JSON.stringify({ theme: 'light', menu: 'top' }));
|
||||
srv = injector.get(BrandService);
|
||||
expect(srv.theme).toBe('light');
|
||||
expect(srv.menu).toBe('top');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should be trigger notify', () => {
|
||||
beforeEach(() => (srv = injector.get(BrandService)));
|
||||
|
||||
it('when mobile changed in constructor', done => {
|
||||
srv.notify.pipe(filter(v => v != null && v === 'mobile')).subscribe(type => {
|
||||
expect(true).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('when layout changed', done => {
|
||||
srv.notify.pipe(filter(v => v != null && v === 'layout')).subscribe(type => {
|
||||
expect(true).toBe(true);
|
||||
done();
|
||||
});
|
||||
srv.setLayout('a', 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('#setCollapsed', () => {
|
||||
srv = injector.get(BrandService);
|
||||
srv.setCollapsed(false);
|
||||
expect(srv.collapsed).toBe(false);
|
||||
srv.setCollapsed();
|
||||
expect(srv.collapsed).toBe(true);
|
||||
srv.setCollapsed();
|
||||
expect(srv.collapsed).toBe(false);
|
||||
});
|
||||
|
||||
it('should be onlyIcon always be false when menu is side', () => {
|
||||
srv = injector.get(BrandService);
|
||||
srv.setLayout('menu', 'top');
|
||||
srv.setLayout('onlyIcon', true);
|
||||
expect(srv.onlyIcon).toBe(true);
|
||||
srv.setLayout('menu', 'side');
|
||||
expect(srv.onlyIcon).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user