项目初始化

This commit is contained in:
Taric Xin
2021-11-26 16:34:35 +08:00
parent 66644bcf0a
commit 5287578452
354 changed files with 45736 additions and 0 deletions

View 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>

View 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) {}
}

View 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>

View 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();
}
}

View 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>

View 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) {}
}

View 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>

View 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();
}
}

View 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>

View 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}`);
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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>

View 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();
}
}

View File

@ -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();
}
}

View File

@ -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>

View 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);
}
}

View File

@ -0,0 +1 @@
<i nz-tooltip="Quick panel" nz-icon nzType="appstore" class="alain-pro__header-item-icon"></i>

View 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();
}
}

View 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();
}
}

View File

@ -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>

View 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();
}
}

View 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>

View 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!);
}
}

View 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> -->

View 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!);
}
}