项目初始化
This commit is contained in:
30
src/app/shared/README.md
Normal file
30
src/app/shared/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# ShareModule
|
||||
|
||||
**应** 包含定义:
|
||||
|
||||
+ 应用通用自定义业务组件
|
||||
|
||||
**应** 导出所有包含的模块。
|
||||
|
||||
**不应** 有 `providers` 属性。
|
||||
|
||||
## 自定义全局组件或指令
|
||||
|
||||
每一个组件或指令应该有一个完整的说明文件,**建议**一个合理的目录结构应该是:
|
||||
|
||||
```
|
||||
├── components
|
||||
│ ├── comp1
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── README.md
|
||||
│ ├── comp2
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── README.md
|
||||
├── directives
|
||||
│ ├── dire1
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── README.md
|
||||
│ ├── dire2
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── README.md
|
||||
```
|
||||
16
src/app/shared/components/address/address.component.html
Normal file
16
src/app/shared/components/address/address.component.html
Normal file
@ -0,0 +1,16 @@
|
||||
<nz-cascader
|
||||
*ngIf="data"
|
||||
[(ngModel)]="value"
|
||||
(ngModelChange)="change()"
|
||||
[nzOptions]="data"
|
||||
[nzDisabled]="disabled"
|
||||
[nzAllowClear]="allowClear"
|
||||
[nzAutoFocus]="autoFocus"
|
||||
[nzNotFoundContent]="notFoundContent"
|
||||
[nzSize]="size"
|
||||
[nzShowSearch]="showSearch"
|
||||
[nzPlaceHolder]="placeHolder"
|
||||
[nzMouseEnterDelay]="mouseEnterDelay"
|
||||
[nzMouseLeaveDelay]="mouseLeaveDelay"
|
||||
[nzTriggerAction]="triggerAction"
|
||||
></nz-cascader>
|
||||
84
src/app/shared/components/address/address.component.ts
Normal file
84
src/app/shared/components/address/address.component.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { BooleanInput, InputBoolean } from '@delon/util';
|
||||
import {
|
||||
NzCascaderExpandTrigger,
|
||||
NzCascaderOption,
|
||||
NzCascaderSize,
|
||||
NzCascaderTriggerType,
|
||||
NzShowSearchOptions
|
||||
} from 'ng-zorro-antd/cascader';
|
||||
|
||||
import { AddressService, AddressType } from './address.service';
|
||||
|
||||
@Component({
|
||||
selector: 'address',
|
||||
templateUrl: './address.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => AddressComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AddressComponent implements OnInit, ControlValueAccessor {
|
||||
static ngAcceptInputType_allowClear: BooleanInput;
|
||||
static ngAcceptInputType_autoFocus: BooleanInput;
|
||||
static ngAcceptInputType_disabled: BooleanInput;
|
||||
|
||||
private onChangeFn?: (val: string) => void;
|
||||
private onTouchedFn?: () => void;
|
||||
value: string[] = [];
|
||||
data?: NzCascaderOption[];
|
||||
|
||||
// #region fields
|
||||
|
||||
@Input() type: AddressType = 'pca';
|
||||
|
||||
// Original attributes
|
||||
@Input() @InputBoolean() allowClear = true;
|
||||
@Input() @InputBoolean() autoFocus = false;
|
||||
@Input() @InputBoolean() disabled = false;
|
||||
@Input() expandTrigger: NzCascaderExpandTrigger = 'click';
|
||||
@Input() notFoundContent?: string;
|
||||
@Input() size: NzCascaderSize = 'default';
|
||||
@Input() showSearch!: boolean | NzShowSearchOptions;
|
||||
@Input() placeHolder = '请选择所在地';
|
||||
@Input() mouseEnterDelay = 150; // ms
|
||||
@Input() mouseLeaveDelay = 150; // ms
|
||||
@Input() triggerAction: NzCascaderTriggerType | NzCascaderTriggerType[] = ['click'] as NzCascaderTriggerType[];
|
||||
|
||||
// #endregion
|
||||
|
||||
constructor(private srv: AddressService, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
change(): void {
|
||||
this.onChangeFn!(this.value.pop()!);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.srv[this.type].subscribe(res => {
|
||||
this.data = res;
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
writeValue(geo: string): void {
|
||||
if (geo == null) {
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
this.value = this.srv.toValueArr(geo, this.type);
|
||||
}
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChangeFn = fn;
|
||||
}
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouchedFn = fn;
|
||||
}
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
}
|
||||
15
src/app/shared/components/address/address.module.ts
Normal file
15
src/app/shared/components/address/address.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NzCascaderModule } from 'ng-zorro-antd/cascader';
|
||||
|
||||
import { AddressComponent } from './address.component';
|
||||
|
||||
const COMPONENTS = [AddressComponent];
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, NzCascaderModule],
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class AddressModule {}
|
||||
79
src/app/shared/components/address/address.service.ts
Normal file
79
src/app/shared/components/address/address.service.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { ArrayService } from '@delon/util';
|
||||
import { NzCascaderOption } from 'ng-zorro-antd/cascader';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export interface PCCode {
|
||||
code?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
label?: string;
|
||||
isLeaf?: boolean;
|
||||
children?: PCCode[];
|
||||
}
|
||||
|
||||
export type AddressType = 'pc' | 'pca';
|
||||
const MAXLENGTH = 6;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddressService {
|
||||
private _pcCode?: NzCascaderOption[];
|
||||
private _pcaCode?: NzCascaderOption[];
|
||||
|
||||
/**
|
||||
* “省份、城市” 二级联动数据,数据来源于 [pc-code.json](https://github.com/modood/Administrative-divisions-of-China/blob/master/dist/pc-code.json)
|
||||
*/
|
||||
get pc(): Observable<NzCascaderOption[]> {
|
||||
return this._pcCode ? of(this._pcCode) : this.getPcCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* “省份、城市、区县” 三级联动数据,数据来源于 [pc-code.json](https://github.com/modood/Administrative-divisions-of-China/blob/master/dist/pca-code.json)
|
||||
*/
|
||||
get pca(): Observable<NzCascaderOption[]> {
|
||||
return this._pcaCode ? of(this._pcaCode) : this.getPcaCode();
|
||||
}
|
||||
|
||||
constructor(private http: _HttpClient, private arrSrv: ArrayService) {}
|
||||
|
||||
/**
|
||||
* 始终保持 6 位数,不足补 `0`
|
||||
*/
|
||||
fixValue(val: string): string {
|
||||
return `${val}000000`.substr(0, MAXLENGTH);
|
||||
}
|
||||
|
||||
toValueArr(val: string, type: AddressType): string[] {
|
||||
val = this.fixValue(val);
|
||||
const res: string[] = [];
|
||||
if (type === 'pc') {
|
||||
res.push(val.substr(0, 2), val);
|
||||
} else {
|
||||
for (let i = 0; i < MAXLENGTH; i += 2) {
|
||||
res.push(val.substr(0, i + 2));
|
||||
}
|
||||
}
|
||||
return res.map(this.fixValue);
|
||||
}
|
||||
|
||||
private map = (res: PCCode[]): NzCascaderOption[] => {
|
||||
this.arrSrv.visitTree(res, (item: PCCode) => {
|
||||
item.value = this.fixValue(item.code!);
|
||||
item.label = item.name;
|
||||
if (!item.children) {
|
||||
item.isLeaf = true;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
private getPcCode(): Observable<NzCascaderOption[]> {
|
||||
return this.http.get('./assets/tmp/pc-code.json').pipe(map(this.map));
|
||||
}
|
||||
|
||||
private getPcaCode(): Observable<NzCascaderOption[]> {
|
||||
return this.http.get('./assets/tmp/pca-code.json').pipe(map(this.map));
|
||||
}
|
||||
}
|
||||
26
src/app/shared/components/address/index.en-US.md
Normal file
26
src/app/shared/components/address/index.en-US.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
order: 110
|
||||
title: address
|
||||
type: Component
|
||||
---
|
||||
|
||||
China address picker, support two styles of “province, city” or “province, city, district”, refer to the account management example.
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|----------|----|----------|--------|
|
||||
| `[ngModel]` | selected value, return the city or district code | `string` | - |
|
||||
| `[type]` | Type of address | `pc,pca` | `pca` |
|
||||
| `[allowClear]` | whether allow clear | `boolean` | `true` |
|
||||
| `[autoFocus]` | whether auto focus the input box | `boolean` | `false` |
|
||||
| `[disabled]` | whether disabled select | `boolean` | `false` |
|
||||
| `[expandTrigger]` | expand current item when click or hover, one of 'click' 'hover' | `'click'|'hover'` | `'click'` |
|
||||
| `[notFoundContent]` | Specify content to show when no result matches. | `string` | - |
|
||||
| `[placeHolder]` | input placeholder | `string` | `'请选择所在地'` |
|
||||
| `[showSearch]` | Whether support search. Cannot be used with `[nzLoadData]` at the same time | `boolean|NzShowSearchOptions` | `false` |
|
||||
| `[size]` | input size, one of `large` `default` `small` | `'large'|'small'|'default'` | `'default'` |
|
||||
|
||||
## sf widget
|
||||
|
||||
Widget name: `address`.
|
||||
3
src/app/shared/components/address/index.ts
Normal file
3
src/app/shared/components/address/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './address.service';
|
||||
export * from './address.component';
|
||||
export * from './address.module';
|
||||
26
src/app/shared/components/address/index.zh-CN.md
Normal file
26
src/app/shared/components/address/index.zh-CN.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
order: 110
|
||||
title: address
|
||||
type: Component
|
||||
---
|
||||
|
||||
地址选择器,支持“省份、城市”或“省份、城市、区县”两种风格,参考账号管理示例。
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|----|----------|--------|
|
||||
| `[ngModel]` | 指定选中项,返回城市或区县代码 | `string` | - |
|
||||
| `[type]` | 类型 | `pc,pca` | `pca` |
|
||||
| `[allowClear]` | 是否支持清除 | `boolean` | `true` |
|
||||
| `[autoFocus]` | 是否自动聚焦,当存在输入框时 | `boolean` | `false` |
|
||||
| `[disabled]` | 禁用 | `boolean` | `false` |
|
||||
| `[expandTrigger]` | 次级菜单的展开方式,可选 'click' 和 'hover' | `'click'|'hover'` | `'click'` |
|
||||
| `[notFoundContent]` | 当下拉列表为空时显示的内容 | `string` | - |
|
||||
| `[placeHolder]` | 输入框占位文本 | `string` | `'请选择所在地'` |
|
||||
| `[showSearch]` | 是否支持搜索,默认情况下对 `label` 进行全匹配搜索,不能和 `[nzLoadData]` 同时使用 | `boolean|NzShowSearchOptions` | `false` |
|
||||
| `[size]` | 输入框大小,可选 `large` `default` `small` | `'large'|'small'|'default'` | `'default'` |
|
||||
|
||||
## sf 小部件
|
||||
|
||||
小部件名称:`address`。
|
||||
1
src/app/shared/components/captcha/captcha.component.html
Normal file
1
src/app/shared/components/captcha/captcha.component.html
Normal file
@ -0,0 +1 @@
|
||||
<div id="captcha"></div>
|
||||
6
src/app/shared/components/captcha/captcha.component.less
Normal file
6
src/app/shared/components/captcha/captcha.component.less
Normal file
@ -0,0 +1,6 @@
|
||||
:host {
|
||||
::ng-deep {
|
||||
.captcha-box {
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/app/shared/components/captcha/captcha.component.ts
Normal file
79
src/app/shared/components/captcha/captcha.component.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { keysConf } from '@conf/keys.conf';
|
||||
import { Subject } from 'rxjs';
|
||||
import { EACaptchaService } from '../../services';
|
||||
import { initNECaptchaWithFallback } from './dun';
|
||||
@Component({
|
||||
selector: 'app-captcha',
|
||||
templateUrl: './captcha.component.html',
|
||||
styleUrls: ['./captcha.component.less'],
|
||||
})
|
||||
export class CaptchaComponent implements OnInit {
|
||||
@Input() phone!: string; // 手机号
|
||||
@Input() url!: string; // api地址
|
||||
@Output() done = new EventEmitter<any>();
|
||||
captchaIns: any;
|
||||
|
||||
initSubject = new Subject<any>();
|
||||
|
||||
constructor(public captchaService: EACaptchaService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
init() {
|
||||
const _this = this;
|
||||
if (this.captchaIns) {
|
||||
return this.initSubject;
|
||||
}
|
||||
|
||||
initNECaptchaWithFallback(
|
||||
{
|
||||
element: '#captcha',
|
||||
captchaId: keysConf.yidun_capcha_id,
|
||||
mode: 'popup',
|
||||
width: '320px',
|
||||
onClose: () => {
|
||||
// 弹出关闭结束后将会触发该函数
|
||||
},
|
||||
onVerify: (err: any, data: any) => {
|
||||
// console.log('🚀 ~ init ~ data', data);
|
||||
if (data?.validate) {
|
||||
// 验证通过,获取验证码
|
||||
_this.captchaDone(data?.validate);
|
||||
}
|
||||
},
|
||||
},
|
||||
(instance: any) => {
|
||||
// console.log('🚀 ~ initCaptcha ~ instance', instance);
|
||||
// 初始化成功后得到验证实例instance,可以调用实例的方法
|
||||
_this.captchaIns = instance;
|
||||
this.initSubject.next(_this.captchaIns);
|
||||
},
|
||||
(err: any) => {
|
||||
// 初始化失败后触发该函数,err对象描述当前错误信息
|
||||
},
|
||||
);
|
||||
|
||||
return this.initSubject;
|
||||
}
|
||||
/* 网易盾验证通过 */
|
||||
captchaDone(validate: any) {
|
||||
this.captchaService.getCaptchaByDun(this.phone, validate, this.url || undefined).subscribe((res: any) => {
|
||||
// console.log('🚀 ~ 验证通过发送验证码=>', res);
|
||||
if (res) {
|
||||
this.captchaService.msgSrv.success('验证码发送成功!');
|
||||
this.done.emit(null);
|
||||
} else {
|
||||
this.captchaService.msgSrv.warning(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
popUp() {
|
||||
console.log(222222222222222222222222222222222222222);
|
||||
if (!this.captchaIns) {
|
||||
this.init();
|
||||
}
|
||||
this.captchaIns.refresh();
|
||||
this.captchaIns.popUp();
|
||||
}
|
||||
}
|
||||
84
src/app/shared/components/captcha/dun.helper.ts
Normal file
84
src/app/shared/components/captcha/dun.helper.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { ComponentRef, Injectable } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { CaptchaComponent } from './captcha.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DunHelper {
|
||||
captchacontainerRef!: ComponentRef<CaptchaComponent>;
|
||||
userInfo;
|
||||
|
||||
constructor(private overlay: Overlay) {
|
||||
this.userInfo = JSON.parse(localStorage.getItem('user') || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件初始化
|
||||
* @param phone 手机号
|
||||
* @param url 发送验证码请求地址
|
||||
*/
|
||||
init(phone: string, url?: string): Subject<any> {
|
||||
const overlayRef = this.createOverlay();
|
||||
const containerPortal = new ComponentPortal(CaptchaComponent);
|
||||
this.captchacontainerRef = overlayRef.attach<CaptchaComponent>(containerPortal);
|
||||
this.captchacontainerRef.instance.phone = phone;
|
||||
this.captchacontainerRef.instance.url = url || '';
|
||||
return this.captchacontainerRef.instance.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出滑块验证
|
||||
* @param phone 手机号
|
||||
* @param url 发送验证码请求地址
|
||||
*/
|
||||
popUp(phone?: string, url?: string): Observable<any> {
|
||||
if (this.captchacontainerRef) {
|
||||
this.destory();
|
||||
}
|
||||
|
||||
this.init(phone || this.userInfo?.phone, url).subscribe((instance) => {
|
||||
if (instance) {
|
||||
this.captchacontainerRef.instance.popUp();
|
||||
}
|
||||
});
|
||||
|
||||
/* if (!this.captchacontainerRef) {
|
||||
this.init(phone || this.userInfo?.phone, url).subscribe(instance => {
|
||||
if (instance) {
|
||||
this.captchacontainerRef.instance.popUp();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!!phone && !!url) {
|
||||
this.init(phone || this.userInfo?.phone, url).subscribe(instance => {
|
||||
if (instance) {
|
||||
this.captchacontainerRef.instance.popUp();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.captchacontainerRef.instance.popUp();
|
||||
}
|
||||
} */
|
||||
return this.captchacontainerRef.instance.done;
|
||||
}
|
||||
|
||||
/** 组件销毁 */
|
||||
destory() {
|
||||
this.captchacontainerRef.destroy();
|
||||
}
|
||||
|
||||
private createOverlay(): OverlayRef {
|
||||
const overlayConfig = new OverlayConfig({
|
||||
hasBackdrop: false,
|
||||
scrollStrategy: this.overlay.scrollStrategies.block(),
|
||||
positionStrategy: this.overlay.position().global(),
|
||||
backdropClass: 'captcha-back-drop',
|
||||
panelClass: 'captcha-overlay',
|
||||
});
|
||||
|
||||
return this.overlay.create(overlayConfig);
|
||||
}
|
||||
}
|
||||
353
src/app/shared/components/captcha/dun.ts
Normal file
353
src/app/shared/components/captcha/dun.ts
Normal file
@ -0,0 +1,353 @@
|
||||
/* eslint-disable no-undef */
|
||||
let errorCallbackCount: any = 0;
|
||||
|
||||
// 常量
|
||||
const DEFAULT_VALIDATE =
|
||||
'QjGAuvoHrcpuxlbw7cp4WnIbbjzG4rtSlpc7EDovNHQS._ujzPZpeCInSxIT4WunuDDh8dRZYF2GbBGWyHlC6q5uEi9x-TXT9j7J705vSsBXyTar7aqFYyUltKYJ7f4Y2TXm_1Mn6HFkb4M7URQ_rWtpxQ5D6hCgNJYC0HpRE7.2sttqYKLoi7yP1KHzK-PptdHHkVwb77cwS2EJW7Mj_PsOtnPBubTmTZLpnRECJR99dWTVC11xYG0sx8dJNLUxUFxEyzTfX4nSmQz_T5sXATRKHtVAz7nmV0De5unmflfAlUwMGKlCT1khBtewlgN5nHvyxeD8Z1_fPVzi9oznl-sbegj6lKfCWezmLcwft8.4yaVh6SlzXJq-FnSK.euq9OBd5jYc82ge2_hEca1fGU--SkPRzgwkzew4O4qjdS2utdPwFONnhKAIMJRPUmCV4lPHG1OeRDvyNV8sCnuFMw7leasxIhPoycl4pm5bNy70Z1laozEGJgItVNr3'; // 默认validate
|
||||
const FALLBACK_LANG: any = {
|
||||
'zh-CN': '前方拥堵,已自动跳过验证',
|
||||
en: 'captcha error,Verified automatically',
|
||||
};
|
||||
const CACHE_MIN = 1000 * 60; // 缓存时长单位,1分钟
|
||||
const REQUEST_SCRIPT_ERROR = 502;
|
||||
|
||||
const RESOURCE_CACHE: any = {};
|
||||
|
||||
// 工具函数
|
||||
function loadScript(src: any, cb: any) {
|
||||
const head: any = document.head || document.getElementsByTagName('head')[0];
|
||||
const script: any = document.createElement('script');
|
||||
|
||||
cb = cb || function () {};
|
||||
|
||||
script.type = 'text/javascript';
|
||||
script.charset = 'utf8';
|
||||
script.async = true;
|
||||
script.src = src;
|
||||
|
||||
if (!('onload' in script)) {
|
||||
script.onreadystatechange = function () {
|
||||
if (this.readyState !== 'complete' && this.readyState !== 'loaded') {
|
||||
return;
|
||||
}
|
||||
this.onreadystatechange = null;
|
||||
cb(null, script); // there is no way to catch loading errors in IE8
|
||||
};
|
||||
}
|
||||
|
||||
script.onload = function () {
|
||||
this.onerror = this.onload = null;
|
||||
cb(null, script);
|
||||
};
|
||||
script.onerror = function () {
|
||||
// because even IE9 works not like others
|
||||
this.onerror = this.onload = null;
|
||||
cb(new Error('Failed to load ' + this.src), script);
|
||||
};
|
||||
|
||||
head.appendChild(script);
|
||||
}
|
||||
|
||||
function joinUrl(protocol: any, host: any, path: any) {
|
||||
protocol = protocol || '';
|
||||
host = host || '';
|
||||
path = path || '';
|
||||
if (protocol) {
|
||||
protocol = protocol.replace(/:?\/{0,2}$/, '://');
|
||||
}
|
||||
if (host) {
|
||||
const matched = host.match(/^([-0-9a-zA-Z.:]*)(\/.*)?/);
|
||||
host = matched[1];
|
||||
path = (matched[2] || '') + '/' + path;
|
||||
}
|
||||
!host && (protocol = '');
|
||||
|
||||
return protocol + host + path;
|
||||
}
|
||||
|
||||
function setDomText(el: any, value: any) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
const nodeType = el.nodeType;
|
||||
if (nodeType === 1 || nodeType === 11 || nodeType === 9) {
|
||||
if (typeof el.textContent === 'string') {
|
||||
el.textContent = value;
|
||||
} else {
|
||||
el.innerText = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queryAllByClassName(selector: any, node: any) {
|
||||
node = node || document;
|
||||
if (node.querySelectorAll) {
|
||||
return node.querySelectorAll(selector);
|
||||
}
|
||||
if (!/^\.[^.]+$/.test(selector)) {
|
||||
return [];
|
||||
}
|
||||
if (node.getElementsByClassName) {
|
||||
return node.getElementsByClassName(selector);
|
||||
}
|
||||
|
||||
const children = node.getElementsByTagName('*');
|
||||
let current;
|
||||
const result = [];
|
||||
const className = selector.slice(1);
|
||||
for (let i = 0, l = children.length; i < l; i++) {
|
||||
current = children[i];
|
||||
if (~(' ' + current.className + ' ').indexOf(' ' + className + ' ')) {
|
||||
result.push(current);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function assert(condition: any, msg: any) {
|
||||
if (!condition) {
|
||||
throw new Error('[NECaptcha] ' + msg);
|
||||
}
|
||||
}
|
||||
|
||||
function isInteger(val: any) {
|
||||
if (Number.isInteger) {
|
||||
return Number.isInteger(val);
|
||||
}
|
||||
return typeof val === 'number' && isFinite(val) && Math.floor(val) === val;
|
||||
}
|
||||
|
||||
function isArray(val: any) {
|
||||
if (Array.isArray) {
|
||||
return Array.isArray(val);
|
||||
}
|
||||
return Object.prototype.toString.call(val) === '[object Array]';
|
||||
}
|
||||
|
||||
function ObjectAssign(a: any, b: any, c: any) {
|
||||
if (Object.assign) {
|
||||
// return Object.assign.apply(null, arguments);
|
||||
return Object.assign.apply(null, arguments as any);
|
||||
}
|
||||
|
||||
const target: any = {};
|
||||
for (let index = 1; index < arguments.length; index++) {
|
||||
const source = arguments[index];
|
||||
if (source != null) {
|
||||
for (const key in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function getTimestamp(msec: any) {
|
||||
msec = !msec && msec !== 0 ? msec : 1;
|
||||
return parseInt((new Date().valueOf() / msec).toString(), 10);
|
||||
}
|
||||
|
||||
// 降级方案
|
||||
function normalizeFallbackConfig(customConfig: any) {
|
||||
const siteProtocol = window.location.protocol.replace(':', '');
|
||||
const defaultConf: any = {
|
||||
protocol: siteProtocol === 'http' ? 'http' : 'https',
|
||||
lang: 'zh-CN',
|
||||
errorFallbackCount: 3,
|
||||
};
|
||||
const config: any = ObjectAssign({}, defaultConf, customConfig);
|
||||
|
||||
const errorFallbackCount: any = config.errorFallbackCount;
|
||||
assert(
|
||||
errorFallbackCount === undefined || (isInteger(errorFallbackCount) && errorFallbackCount >= 1),
|
||||
"errorFallbackCount must be an integer, and it's value greater than or equal one",
|
||||
);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadResource(config: any, cb: any) {
|
||||
if ((window as any).initNECaptcha) {
|
||||
return cb(null);
|
||||
}
|
||||
function genUrl(server: any) {
|
||||
const path = 'load.min.js';
|
||||
let _urls = [];
|
||||
if (isArray(server)) {
|
||||
for (let i = 0, len = server.length; i < len; i++) {
|
||||
_urls.push(joinUrl(config.protocol, server[i], path));
|
||||
}
|
||||
} else {
|
||||
const url = joinUrl(config.protocol, server, path);
|
||||
_urls = [url, url];
|
||||
}
|
||||
|
||||
return _urls;
|
||||
}
|
||||
const urls = genUrl(config.staticServer || ['cstaticdun.126.net', 'cstaticdun1.126.net', 'cstatic.dun.163yun.com']);
|
||||
|
||||
function step(i: any) {
|
||||
const url = urls[i] + '?v=' + getTimestamp(CACHE_MIN);
|
||||
loadScript(url, function (err: any) {
|
||||
if (err || !(window as any).initNECaptcha) {
|
||||
// loadjs的全局变量
|
||||
i = i + 1;
|
||||
if (i === urls.length) {
|
||||
return cb(new Error('Failed to load script(' + url + ').' + (err ? err.message : 'unreliable script')));
|
||||
}
|
||||
return step(i);
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
step(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* entry: initNECaptchaWithFallback
|
||||
* options:
|
||||
* errorFallbackCount: 触发降级的错误次数,默认第三次错误降级
|
||||
* defaultFallback: 是否开启默认降级
|
||||
* onFallback: 自定义降级方案,参数为默认validate
|
||||
*/
|
||||
export function initNECaptchaWithFallback(options: any, onload: any, onerror: any) {
|
||||
let captchaIns: any = null;
|
||||
|
||||
const config = normalizeFallbackConfig(options);
|
||||
const defaultFallback = config.defaultFallback !== false;
|
||||
const langPkg = FALLBACK_LANG[config.lang === 'zh-CN' ? config.lang : 'en'];
|
||||
const storeKey = window.location.pathname + '_' + config.captchaId + '_NECAPTCHA_ERROR_COUNTS';
|
||||
try {
|
||||
errorCallbackCount = parseInt(localStorage.getItem(storeKey)?.toString() || '0', 10);
|
||||
} catch (error) {}
|
||||
|
||||
const fallbackFn = !defaultFallback
|
||||
? config.onFallback || function () {}
|
||||
: (validate: any) => {
|
||||
function setFallbackTip(instance: any) {
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
setFallbackTip(instance._captchaIns);
|
||||
if (!instance.$el) {
|
||||
return;
|
||||
}
|
||||
const tipEles = queryAllByClassName('.yidun-fallback__tip', instance.$el);
|
||||
if (!tipEles.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保在队列的最后
|
||||
setTimeout(() => {
|
||||
for (let i = 0, l = tipEles.length; i < l; i++) {
|
||||
setDomText(tipEles[i], langPkg);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
setFallbackTip(captchaIns);
|
||||
|
||||
config.onVerify && config.onVerify(null, { validate: validate });
|
||||
};
|
||||
const noFallback = !defaultFallback && !config.onFallback;
|
||||
|
||||
const proxyOnError = (error: any) => {
|
||||
errorCallbackCount++;
|
||||
if (errorCallbackCount < config.errorFallbackCount) {
|
||||
try {
|
||||
localStorage.setItem(storeKey, errorCallbackCount);
|
||||
} catch (err) {}
|
||||
|
||||
onerror(error);
|
||||
} else {
|
||||
fallbackFn(DEFAULT_VALIDATE);
|
||||
proxyRefresh();
|
||||
noFallback && onerror(error);
|
||||
}
|
||||
};
|
||||
|
||||
const proxyRefresh = () => {
|
||||
errorCallbackCount = 0;
|
||||
try {
|
||||
localStorage.setItem(storeKey, '0');
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
const triggerInitError = (error: any) => {
|
||||
if (initialTimer && initialTimer.isError()) {
|
||||
initialTimer.resetError();
|
||||
return;
|
||||
}
|
||||
initialTimer && initialTimer.resetTimer();
|
||||
noFallback ? onerror(error) : proxyOnError(error);
|
||||
};
|
||||
|
||||
config.onError = (error: any) => {
|
||||
if (initialTimer && initialTimer.isError()) {
|
||||
initialTimer.resetError();
|
||||
}
|
||||
proxyOnError(error);
|
||||
};
|
||||
config.onDidRefresh = () => {
|
||||
if (initialTimer && initialTimer.isError()) {
|
||||
initialTimer.resetError();
|
||||
}
|
||||
proxyRefresh();
|
||||
};
|
||||
|
||||
const initialTimer = options.initTimeoutError ? options.initTimeoutError(proxyOnError) : null; // initialTimer is only for mobile.html
|
||||
|
||||
const loadResolve = () => {
|
||||
(window as any).initNECaptcha(
|
||||
config,
|
||||
(instance: any) => {
|
||||
if (initialTimer && initialTimer.isError()) {
|
||||
return;
|
||||
}
|
||||
initialTimer && initialTimer.resetTimer();
|
||||
captchaIns = instance;
|
||||
onload && onload(instance);
|
||||
},
|
||||
triggerInitError,
|
||||
);
|
||||
};
|
||||
const cacheId = 'load-queue';
|
||||
if (!RESOURCE_CACHE[cacheId]) {
|
||||
RESOURCE_CACHE[cacheId] = {
|
||||
rejects: [],
|
||||
resolves: [],
|
||||
status: 'error',
|
||||
};
|
||||
}
|
||||
if (RESOURCE_CACHE[cacheId].status === 'error') {
|
||||
RESOURCE_CACHE[cacheId].status = 'pending';
|
||||
loadResource(config, (error: any) => {
|
||||
if (error) {
|
||||
const err: any = new Error();
|
||||
err.code = REQUEST_SCRIPT_ERROR;
|
||||
err.message = config.staticServer + '/load.min.js error';
|
||||
|
||||
const rejects = RESOURCE_CACHE[cacheId].rejects;
|
||||
for (let i = 0, iLen = rejects.length; i < iLen; i++) {
|
||||
rejects.pop()(err);
|
||||
}
|
||||
RESOURCE_CACHE[cacheId].status = 'error';
|
||||
} else {
|
||||
RESOURCE_CACHE[cacheId].status = 'done';
|
||||
const resolves = RESOURCE_CACHE[cacheId].resolves;
|
||||
for (let j = 0, jLen = resolves.length; j < jLen; j++) {
|
||||
resolves.pop()();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (RESOURCE_CACHE[cacheId].status === 'done') {
|
||||
loadResolve();
|
||||
}
|
||||
if (RESOURCE_CACHE[cacheId].status === 'pending') {
|
||||
RESOURCE_CACHE[cacheId].rejects.push(function loadReject(err: any) {
|
||||
triggerInitError(err);
|
||||
});
|
||||
RESOURCE_CACHE[cacheId].resolves.push(loadResolve);
|
||||
}
|
||||
}
|
||||
2
src/app/shared/components/captcha/index.ts
Normal file
2
src/app/shared/components/captcha/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './captcha.component';
|
||||
export * from './dun.helper';
|
||||
45
src/app/shared/components/delay/delay.directive.ts
Normal file
45
src/app/shared/components/delay/delay.directive.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { AfterViewInit, Directive, EventEmitter, Input, OnDestroy, Optional, Output } from '@angular/core';
|
||||
import { NgModel } from '@angular/forms';
|
||||
import { BooleanInput, InputBoolean, InputNumber, NumberInput } from '@delon/util';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
@Directive({
|
||||
selector: '[delay]:not([noDelay])',
|
||||
exportAs: 'delayComp'
|
||||
})
|
||||
export class DelayDirective implements AfterViewInit, OnDestroy {
|
||||
static ngAcceptInputType_delayTime: NumberInput;
|
||||
static ngAcceptInputType_delayFirstEmit: BooleanInput;
|
||||
|
||||
private data$: Subscription | undefined;
|
||||
private firstEmit = false;
|
||||
|
||||
@Input() @InputNumber() delayTime = 500;
|
||||
@Input() @InputBoolean() delayFirstEmit = false;
|
||||
@Output() readonly delayChange = new EventEmitter<any>();
|
||||
|
||||
constructor(@Optional() private ngModel: NgModel) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const { ngModel, delayFirstEmit, delayTime, delayChange } = this;
|
||||
if (ngModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.firstEmit = delayFirstEmit;
|
||||
this.data$ = ngModel.valueChanges?.pipe(debounceTime(delayTime), distinctUntilChanged()).subscribe(res => {
|
||||
if (this.firstEmit === false) {
|
||||
this.firstEmit = true;
|
||||
return;
|
||||
}
|
||||
delayChange.emit(res);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.data$) {
|
||||
this.data$.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/app/shared/components/delay/delay.module.ts
Normal file
11
src/app/shared/components/delay/delay.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { DelayDirective } from './delay.directive';
|
||||
|
||||
const COMPONENTS = [DelayDirective];
|
||||
|
||||
@NgModule({
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class DelayModule {}
|
||||
19
src/app/shared/components/delay/index.en-US.md
Normal file
19
src/app/shared/components/delay/index.en-US.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
order: 30
|
||||
title: delay
|
||||
type: Component
|
||||
---
|
||||
|
||||
Delay trigger, for title search, [DEMO](https://preview.ng-alain.com/pro/#/other/article).
|
||||
|
||||
```html
|
||||
<input nz-input [(ngModel)]="q" delay (delayChange)="load()" />
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|--------------------|----------------------------------------|---------------------|---------|
|
||||
| `[delayTime]` | Delay time (unit: ms) | `number` | `500` |
|
||||
| `[delayFirstEmit]` | Whether to trigger after `delayChange` | `boolean` | `false` |
|
||||
| `(delayChange)` | Callback event | `EventEmitter<any>` | - |
|
||||
2
src/app/shared/components/delay/index.ts
Normal file
2
src/app/shared/components/delay/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './delay.directive';
|
||||
export * from './delay.module';
|
||||
19
src/app/shared/components/delay/index.zh-CN.md
Normal file
19
src/app/shared/components/delay/index.zh-CN.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
order: 30
|
||||
title: delay
|
||||
type: Component
|
||||
---
|
||||
|
||||
延迟触发,适用于标题搜索,参考[示例](https://preview.ng-alain.com/pro/#/other/article)。
|
||||
|
||||
```html
|
||||
<input nz-input [(ngModel)]="q" delay (delayChange)="load()" />
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|--------------------|----------------------------|---------------------|---------|
|
||||
| `[delayTime]` | 延迟时间(单位:毫秒) | `number` | `500` |
|
||||
| `[delayFirstEmit]` | 是否加载后触发 `delayChange` | `boolean` | `false` |
|
||||
| `(delayChange)` | 回调函数 | `EventEmitter<any>` | - |
|
||||
417
src/app/shared/components/editor/editor.component.ts
Normal file
417
src/app/shared/components/editor/editor.component.ts
Normal file
@ -0,0 +1,417 @@
|
||||
/**
|
||||
* Part of the code comes from https://github.com/KillerCodeMonkey/ngx-quill/
|
||||
*/
|
||||
import { DOCUMENT, isPlatformServer } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Output,
|
||||
PLATFORM_ID,
|
||||
Renderer2,
|
||||
SimpleChanges
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
|
||||
import { ModalHelper } from '@delon/theme';
|
||||
import { BooleanInput, InputBoolean, InputNumber, NumberInput } from '@delon/util';
|
||||
import ImageResize from 'quill-image-resize-module';
|
||||
|
||||
import { FileManagerImgComponent } from '../file-manager/file-manager-img.component';
|
||||
|
||||
declare var Quill: any;
|
||||
const Delta = require('quill-delta');
|
||||
|
||||
export interface CustomOption {
|
||||
import: string;
|
||||
whitelist: any[];
|
||||
}
|
||||
|
||||
Quill.register('modules/imageResize', ImageResize);
|
||||
['align', 'background', 'color', 'direction', 'font'].forEach(type => Quill.register(Quill.import(`attributors/style/${type}`), true));
|
||||
const Size = Quill.import('attributors/style/size');
|
||||
const VALUES = {
|
||||
// NOTICE: Should be sync modify `@ql-sizes` in `styles/fix/_quill.less`
|
||||
size: ['10px', '12px', '14px', '16px', '18px', '20px', '24px']
|
||||
};
|
||||
Size.whitelist = VALUES.size;
|
||||
Quill.register(Size, true);
|
||||
|
||||
@Component({
|
||||
selector: 'editor',
|
||||
template: ``,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => EditorComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => EditorComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
host: {
|
||||
'[class.quill-editor]': 'true'
|
||||
},
|
||||
preserveWhitespaces: false
|
||||
})
|
||||
export class EditorComponent implements AfterViewInit, ControlValueAccessor, OnChanges, OnDestroy, Validator {
|
||||
@Input()
|
||||
set mode(value: 'full' | 'simple') {
|
||||
this._mode = value;
|
||||
const handlers = {
|
||||
image: (state: boolean) => this.image(state)
|
||||
};
|
||||
if (value === 'full') {
|
||||
this.modules = {
|
||||
imageResize: {},
|
||||
toolbar: {
|
||||
handlers,
|
||||
container: [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
|
||||
[{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
||||
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
||||
// [{ direction: 'rtl' }], // text direction
|
||||
|
||||
[{ size: VALUES.size }], // custom dropdown
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||
// [{ font: [] }],
|
||||
[{ align: [] }],
|
||||
|
||||
['clean'], // remove formatting button
|
||||
|
||||
['link', 'image', 'video'] // link and image, video
|
||||
]
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.modules = {
|
||||
imageResize: {},
|
||||
toolbar: {
|
||||
handlers,
|
||||
container: [
|
||||
['bold', 'italic', 'underline', 'strike', 'blockquote'], // toggled buttons
|
||||
[{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||
[{ align: [] }],
|
||||
['clean'], // remove formatting button
|
||||
['link', 'image', 'video'] // link and image, video
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef,
|
||||
@Inject(DOCUMENT) private doc: any,
|
||||
@Inject(PLATFORM_ID) private platformId: {},
|
||||
private renderer: Renderer2,
|
||||
private zone: NgZone,
|
||||
private modalHelper: ModalHelper
|
||||
) {}
|
||||
static ngAcceptInputType_readOnly: BooleanInput;
|
||||
static ngAcceptInputType_maxLength: NumberInput;
|
||||
static ngAcceptInputType_minLength: NumberInput;
|
||||
static ngAcceptInputType_required: BooleanInput;
|
||||
static ngAcceptInputType_strict: BooleanInput;
|
||||
|
||||
quill: any;
|
||||
editorElem!: HTMLElement;
|
||||
emptyArray: any[] = [];
|
||||
content: any;
|
||||
selectionChangeEvent: any;
|
||||
textChangeEvent: any;
|
||||
_mode!: 'full' | 'simple';
|
||||
|
||||
@Input() format: 'object' | 'html' | 'text' | 'json' = 'html';
|
||||
@Input() theme?: string;
|
||||
@Input() modules?: { [index: string]: any };
|
||||
@Input() @InputBoolean() readOnly?: boolean;
|
||||
@Input() placeholder = '';
|
||||
@Input() @InputNumber() maxLength?: number;
|
||||
@Input() @InputNumber() minLength?: number;
|
||||
@Input() @InputBoolean() required?: boolean;
|
||||
@Input() formats?: string[];
|
||||
@Input() style: any = { height: '250px' };
|
||||
@Input() @InputBoolean() strict = true;
|
||||
@Input() scrollingContainer?: HTMLElement | string;
|
||||
@Input() bounds?: HTMLElement | string;
|
||||
@Input() customOptions: CustomOption[] = [];
|
||||
|
||||
@Output() readonly editorCreated = new EventEmitter();
|
||||
@Output() readonly contentChanged = new EventEmitter();
|
||||
@Output() readonly selectionChanged = new EventEmitter();
|
||||
private image(_: boolean): void {
|
||||
this.modalHelper
|
||||
.create(
|
||||
FileManagerImgComponent,
|
||||
{
|
||||
opt: {
|
||||
multiple: true,
|
||||
i: { orderby: 0, cat_id: 0, ps: 20 }
|
||||
}
|
||||
},
|
||||
{
|
||||
size: 1000,
|
||||
modalOptions: {
|
||||
nzClosable: false
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe((res: any[]) => {
|
||||
// delete
|
||||
const range = this.quill.getSelection(true);
|
||||
this.quill.updateContents(new Delta().retain(range.index).delete(range.length));
|
||||
// install all images
|
||||
for (const ii of res) {
|
||||
this.quill.updateContents(new Delta().retain(range.index).delete(range.length).insert({ image: ii.mp }, { alt: ii.title }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Input()
|
||||
valueGetter = (quillEditor: any, editorElement: HTMLElement): any => {
|
||||
let html: string | null = editorElement.children[0].innerHTML;
|
||||
if (html === '<p><br></p>' || html === '<div><br><div>') {
|
||||
html = null;
|
||||
}
|
||||
let modelValue = html;
|
||||
|
||||
if (this.format === 'text') {
|
||||
modelValue = quillEditor.getText();
|
||||
} else if (this.format === 'object') {
|
||||
modelValue = quillEditor.getContents();
|
||||
} else if (this.format === 'json') {
|
||||
try {
|
||||
modelValue = JSON.stringify(quillEditor.getContents());
|
||||
} catch (e) {
|
||||
modelValue = quillEditor.getText();
|
||||
}
|
||||
}
|
||||
|
||||
return modelValue;
|
||||
};
|
||||
|
||||
@Input()
|
||||
valueSetter = (quillEditor: any, value: any, _format: 'object' | 'html' | 'json'): any => {
|
||||
if (this.format === 'html') {
|
||||
return quillEditor.clipboard.convert(value);
|
||||
} else if (this.format === 'json') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
onModelChange = (_: any) => {};
|
||||
onModelTouched = () => {};
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (isPlatformServer(this.platformId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._mode == null) {
|
||||
this.mode = 'full';
|
||||
}
|
||||
|
||||
const modules: any = this.modules;
|
||||
|
||||
this.elementRef.nativeElement.insertAdjacentHTML('beforeend', '<div quill-editor-element></div>');
|
||||
this.editorElem = this.elementRef.nativeElement.querySelector('[quill-editor-element]');
|
||||
|
||||
if (this.style) {
|
||||
Object.keys(this.style).forEach((key: string) => {
|
||||
this.renderer.setStyle(this.editorElem, key, this.style[key]);
|
||||
});
|
||||
}
|
||||
|
||||
this.customOptions.forEach(customOption => {
|
||||
const newCustomOption = Quill.import(customOption.import);
|
||||
newCustomOption.whitelist = customOption.whitelist;
|
||||
Quill.register(newCustomOption, true);
|
||||
});
|
||||
|
||||
this.quill = new Quill(this.editorElem, {
|
||||
modules,
|
||||
placeholder: this.placeholder,
|
||||
readOnly: this.readOnly || false,
|
||||
theme: this.theme || 'snow',
|
||||
formats: this.formats,
|
||||
bounds: this.bounds ? (this.bounds === 'self' ? this.editorElem : this.bounds) : this.doc.body,
|
||||
strict: this.strict,
|
||||
scrollingContainer: this.scrollingContainer
|
||||
});
|
||||
|
||||
if (this.content) {
|
||||
if (this.format === 'object') {
|
||||
this.quill.setContents(this.content, 'silent');
|
||||
} else if (this.format === 'text') {
|
||||
this.quill.setText(this.content, 'silent');
|
||||
} else if (this.format === 'json') {
|
||||
try {
|
||||
this.quill.setContents(JSON.parse(this.content), 'silent');
|
||||
} catch (e) {
|
||||
this.quill.setText(this.content, 'silent');
|
||||
}
|
||||
} else {
|
||||
const contents = this.quill.clipboard.convert(this.content);
|
||||
this.quill.setContents(contents, 'silent');
|
||||
}
|
||||
|
||||
this.quill.history.clear();
|
||||
}
|
||||
|
||||
this.editorCreated.emit(this.quill);
|
||||
|
||||
// mark model as touched if editor lost focus
|
||||
this.selectionChangeEvent = this.quill.on('selection-change', (range: any, oldRange: any, source: string) => {
|
||||
this.zone.run(() => {
|
||||
this.selectionChanged.emit({
|
||||
editor: this.quill,
|
||||
range,
|
||||
oldRange,
|
||||
source
|
||||
});
|
||||
|
||||
if (!range) {
|
||||
this.onModelTouched();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// update model if text changes
|
||||
this.textChangeEvent = this.quill.on('text-change', (delta: any, oldDelta: any, source: string) => {
|
||||
const text = this.quill.getText();
|
||||
const content = this.quill.getContents();
|
||||
|
||||
let html: string | null = this.editorElem.children[0].innerHTML;
|
||||
if (html === '<p><br></p>' || html === '<div><br><div>') {
|
||||
html = null;
|
||||
}
|
||||
|
||||
this.zone.run(() => {
|
||||
this.onModelChange(this.valueGetter(this.quill, this.editorElem));
|
||||
|
||||
this.contentChanged.emit({
|
||||
editor: this.quill,
|
||||
html,
|
||||
text,
|
||||
content,
|
||||
delta,
|
||||
oldDelta,
|
||||
source
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.selectionChangeEvent) {
|
||||
this.selectionChangeEvent.removeListener('selection-change');
|
||||
}
|
||||
if (this.textChangeEvent) {
|
||||
this.textChangeEvent.removeListener('text-change');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (!this.quill) {
|
||||
return;
|
||||
}
|
||||
if (changes.readOnly) {
|
||||
this.quill.enable(!changes.readOnly.currentValue);
|
||||
}
|
||||
if (changes.placeholder) {
|
||||
this.quill.root.dataset.placeholder = changes.placeholder.currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(currentValue: any): void {
|
||||
this.content = currentValue;
|
||||
|
||||
if (this.quill) {
|
||||
if (currentValue) {
|
||||
if (this.format === 'text') {
|
||||
this.quill.setText(currentValue);
|
||||
} else {
|
||||
this.quill.setContents(this.valueSetter(this.quill, this.content, this.format));
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.quill.setText('');
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: any) => void): void {
|
||||
this.onModelChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.onModelTouched = fn;
|
||||
}
|
||||
|
||||
validate(): any {
|
||||
if (!this.quill) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const err: {
|
||||
minLengthError?: { given: number; minLength: number };
|
||||
maxLengthError?: { given: number; maxLength: number };
|
||||
requiredError?: { empty: boolean };
|
||||
} = {};
|
||||
let valid = true;
|
||||
|
||||
const textLength = this.quill.getText().trim().length;
|
||||
|
||||
if (this.minLength && textLength && textLength < this.minLength) {
|
||||
err.minLengthError = {
|
||||
given: textLength,
|
||||
minLength: this.minLength
|
||||
};
|
||||
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (this.maxLength && textLength > this.maxLength) {
|
||||
err.maxLengthError = {
|
||||
given: textLength,
|
||||
maxLength: this.maxLength
|
||||
};
|
||||
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (this.required && !textLength) {
|
||||
err.requiredError = {
|
||||
empty: true
|
||||
};
|
||||
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid ? null : err;
|
||||
}
|
||||
}
|
||||
13
src/app/shared/components/editor/editor.module.ts
Normal file
13
src/app/shared/components/editor/editor.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { FileManagerModule } from '../file-manager';
|
||||
import { EditorComponent } from './editor.component';
|
||||
|
||||
const COMPONENTS = [EditorComponent];
|
||||
|
||||
@NgModule({
|
||||
imports: [FileManagerModule],
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class EditorModule {}
|
||||
39
src/app/shared/components/editor/index.en-US.md
Normal file
39
src/app/shared/components/editor/index.en-US.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
order: 60
|
||||
title: editor
|
||||
type: Component
|
||||
---
|
||||
|
||||
Based on [quill](https://github.com/quilljs/quill) WYSIWYG editor, [DEMO](https://preview.ng-alain.com/pro/#/ec/ware/edit/10001).
|
||||
|
||||
## Feature
|
||||
|
||||
- Integration [file-manager](file-manager)
|
||||
- Integration [quill-image-resize-module](https://github.com/kensnyder/quill-image-resize-module)
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| ---------------------- | -------------------------------------- | ------------------- | --------------------- |
|
||||
| `[(ngModel)]` | Value of quill | `string` | - |
|
||||
| `[mode]` | Mode of quill | `full,simple` | `full` |
|
||||
| `[theme]` | Theme of quill | `string` | `snow` |
|
||||
| `[readOnly]` | Whether to readonly | `boolean` | `false` |
|
||||
| `[required]` | Whether to required | `boolean` | - |
|
||||
| `[maxLength]` | The maximum number of quill characters | `number` | - |
|
||||
| `[minLength]` | The minimum number of quill characters | `number` | - |
|
||||
| `[placeholder]` | Placeholder of quill | `string` | - |
|
||||
| `[style]` | Styles of quill | `any` | `{ height: '250px' }` |
|
||||
| `(editorCreated)` | Quill rendered event | `EventEmitter<any>` | - |
|
||||
| `(contentChanged)` | Quill content change event | `EventEmitter<any>` | - |
|
||||
| `(selectionChanged)` | `selection-change` event | `EventEmitter<any>` | - |
|
||||
|
||||
## sf widget
|
||||
|
||||
Widget name: `editor`.
|
||||
|
||||
### ui
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------------------- | -------------------------- | ------------------------- | ------- |
|
||||
| `(contentChanged)` | Quill content change event | `(value: string) => void` | - |
|
||||
2
src/app/shared/components/editor/index.ts
Normal file
2
src/app/shared/components/editor/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './editor.component';
|
||||
export * from './editor.module';
|
||||
39
src/app/shared/components/editor/index.zh-CN.md
Normal file
39
src/app/shared/components/editor/index.zh-CN.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
order: 60
|
||||
title: editor
|
||||
type: Component
|
||||
---
|
||||
|
||||
基于 [quill](https://github.com/quilljs/quill) 富文本编辑器,参考[示例](https://preview.ng-alain.com/pro/#/ec/ware/edit/10001)。
|
||||
|
||||
## 特性
|
||||
|
||||
- 整合 [file-manager](file-manager)
|
||||
- 整合 [quill-image-resize-module](https://github.com/kensnyder/quill-image-resize-module)
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ---------------------- | -------------------------- | ------------------- | --------------------- |
|
||||
| `[(ngModel)]` | 值 | `string` | - |
|
||||
| `[mode]` | 模式 | `full,simple` | `full` |
|
||||
| `[theme]` | 主题 | `string` | `snow` |
|
||||
| `[readOnly]` | 是否只读 | `boolean` | `false` |
|
||||
| `[required]` | 是否必填 | `boolean` | - |
|
||||
| `[maxLength]` | 最大长度 | `number` | - |
|
||||
| `[minLength]` | 最少长度 | `number` | - |
|
||||
| `[placeholder]` | 文本框默认值 | `string` | - |
|
||||
| `[style]` | 样式,可以决定富文本的高度 | `any` | `{ height: '250px' }` |
|
||||
| `(editorCreated)` | 初始化完成后事件 | `EventEmitter<any>` | - |
|
||||
| `(contentChanged)` | 内容变更事件 | `EventEmitter<any>` | - |
|
||||
| `(selectionChanged)` | `selection-change` 事件 | `EventEmitter<any>` | - |
|
||||
|
||||
## sf 小部件
|
||||
|
||||
小部件名称:`editor`。
|
||||
|
||||
### ui
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| -------------------- | ------------ | ------------------------- | ------ |
|
||||
| `(contentChanged)` | 内容变更事件 | `(value: string) => void` | - |
|
||||
@ -0,0 +1,53 @@
|
||||
<div nz-row [nzGutter]="8">
|
||||
<div nz-col [nzSpan]="6">
|
||||
<nz-tree [nzData]="cat.ls" (nzClick)="changeCat($event)"> </nz-tree>
|
||||
</div>
|
||||
<div nz-col [nzSpan]="18">
|
||||
<file-manager #fm [actions]="fmAction" [multiple]="multiple" (selected)="cho($event)" [params]="params">
|
||||
<ng-template #fmAction>
|
||||
<button *ngIf="result.length > 0" nz-button nz-dropdown [nzDropdownMenu]="copyMenu" class="ml-md">
|
||||
<i nz-icon nzType="setting"></i>
|
||||
<i nz-icon nzType="down"></i>
|
||||
</button>
|
||||
<nz-dropdown-menu #copyMenu="nzDropdownMenu">
|
||||
<ul nz-menu>
|
||||
<li nz-menu-item (click)="copyData('link')">Copy Link</li>
|
||||
<li nz-menu-item (click)="copyData('code')">Copy Code</li>
|
||||
</ul>
|
||||
</nz-dropdown-menu>
|
||||
<nz-input-group nzCompact style="display: inherit; width: 270px" class="ml-md">
|
||||
<nz-select [(ngModel)]="params.orderby" (ngModelChange)="load()">
|
||||
<nz-option [nzValue]="0" nzLabel="按上传时间从晚到早"></nz-option>
|
||||
<nz-option [nzValue]="2" nzLabel="按修改时间从晚到早"></nz-option>
|
||||
<nz-option [nzValue]="4" nzLabel="按修改时间从早到晚"></nz-option>
|
||||
<nz-option [nzValue]="6" nzLabel="按图片名升序"></nz-option>
|
||||
<nz-option [nzValue]="8" nzLabel="按图片名降序"></nz-option>
|
||||
</nz-select>
|
||||
<input [(ngModel)]="params.q" delay (delayChange)="load()" nz-input placeholder="按文件名称" />
|
||||
</nz-input-group>
|
||||
</ng-template>
|
||||
</file-manager>
|
||||
</div>
|
||||
</div>
|
||||
<nz-card
|
||||
[nzTitle]="choTpl"
|
||||
nzType="inner"
|
||||
*ngIf="multiple && result.length > 0"
|
||||
[nzBodyStyle]="{ background: 'rgba(204, 204, 204, 0.33)' }"
|
||||
class="mt-sm"
|
||||
>
|
||||
<ng-template #choTpl>
|
||||
已选图
|
||||
<small class="pl-md text-grey">(按住拖动可调整顺序)</small>
|
||||
<button (click)="ok()" nz-button nzType="primary">确认所选</button>
|
||||
</ng-template>
|
||||
<div class="file-manager" cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="drop($event)">
|
||||
<div class="file-item" *ngFor="let i of result" cdkDrag [title]="i.title">
|
||||
<div class="file-item__img" [ngStyle]="{ 'background-image': 'url(' + i.mp + ')' }"></div>
|
||||
<div class="file-item__name">{{ i.title }}</div>
|
||||
<div class="file-item__pixel">
|
||||
<span *ngIf="i.is_img">{{ i.width }}x{{ i.height }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nz-card>
|
||||
@ -0,0 +1,108 @@
|
||||
import { moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, ViewChild } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { ArrayService, copy } from '@delon/util';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
import { NzModalRef } from 'ng-zorro-antd/modal';
|
||||
import { NzFormatEmitEvent } from 'ng-zorro-antd/tree';
|
||||
|
||||
import { FileManagerComponent } from './file-manager.component';
|
||||
|
||||
@Component({
|
||||
selector: 'file-manager-img',
|
||||
templateUrl: './file-manager-img.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FileManagerImgComponent implements AfterViewInit {
|
||||
result: any[] = [];
|
||||
cat: any = {
|
||||
ls: [],
|
||||
item: {}
|
||||
};
|
||||
|
||||
@Input()
|
||||
params = {
|
||||
type: 'file',
|
||||
q: '',
|
||||
is_img: true,
|
||||
parent_id: 0,
|
||||
orderby: 0
|
||||
};
|
||||
@Input() multiple: boolean | number = false;
|
||||
@ViewChild('fm', { static: false }) fm!: FileManagerComponent;
|
||||
|
||||
constructor(
|
||||
private http: _HttpClient,
|
||||
private arrSrv: ArrayService,
|
||||
private msg: NzMessageService,
|
||||
private modal: NzModalRef,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.loadCat();
|
||||
}
|
||||
|
||||
copyData(type: 'link' | 'code'): void {
|
||||
copy(this.result.map(v => this.fm.getCode(v.mp, type)).join('\n')).then(() => this.msg.success('Copy Success'));
|
||||
}
|
||||
|
||||
// #region category
|
||||
|
||||
changeCat(e: NzFormatEmitEvent): void {
|
||||
this.cat.item = e.node!.origin;
|
||||
this.params.parent_id = e.node!.origin.id;
|
||||
this.fm.load(1);
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
loadCat(): void {
|
||||
this.http.get('/file/folder').subscribe((res: any[]) => {
|
||||
res.splice(0, 0, { id: 0, title: '所有图片' });
|
||||
this.cat.ls = this.arrSrv.arrToTreeNode(res, {
|
||||
cb: (item, parent, deep) => {
|
||||
item.expanded = deep <= 1;
|
||||
item.selected = item.id === 0;
|
||||
}
|
||||
});
|
||||
this.cat.item = res[0];
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
load(): void {
|
||||
this.fm.load(1);
|
||||
}
|
||||
|
||||
cho(i: any): void {
|
||||
if (i.on === true) {
|
||||
this.result.splice(this.result.indexOf(i), 1);
|
||||
i.on = false;
|
||||
return;
|
||||
}
|
||||
if (!this.multiple) {
|
||||
this.result.push(i);
|
||||
this.ok();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.multiple === 'number' && this.result.length >= this.multiple) {
|
||||
this.msg.error(`最多只能选取${this.multiple}张`);
|
||||
return;
|
||||
}
|
||||
i.on = true;
|
||||
this.result.push(i);
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
drop(e: any): void {
|
||||
moveItemInArray(this.result, e.previousIndex, e.currentIndex);
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
ok(): void {
|
||||
this.modal.close(this.result);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
|
||||
import { ModalHelper } from '@delon/theme';
|
||||
|
||||
import { FileManagerImgComponent } from './file-manager-img.component';
|
||||
|
||||
@Directive({ selector: '[dialog-img]' })
|
||||
export class FileManagerImgDirective {
|
||||
@Input() multiple: boolean | number = false;
|
||||
@Input() field?: string;
|
||||
@Output() readonly selected = new EventEmitter<any>();
|
||||
|
||||
constructor(private modalHelper: ModalHelper) {}
|
||||
|
||||
@HostListener('click')
|
||||
_click(): void {
|
||||
this.modalHelper
|
||||
.create(
|
||||
FileManagerImgComponent,
|
||||
{
|
||||
multiple: this.multiple
|
||||
},
|
||||
{
|
||||
size: 1000,
|
||||
modalOptions: {
|
||||
nzClosable: false
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
if (Array.isArray(res)) {
|
||||
let ret = res.length > 0 && !this.multiple ? res[0] : res;
|
||||
if (this.field && ret) {
|
||||
ret = ret[this.field];
|
||||
}
|
||||
this.selected.emit(ret);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center flex-1">
|
||||
<nz-upload nzAction="/file" [nzShowUploadList]="false" [nzData]="uploadData" nzMultiple (nzChange)="uploadChange($event)">
|
||||
<button nz-button nzType="primary" [nzLoading]="loading">
|
||||
<i nz-icon nzType="upload"></i>
|
||||
<span>{{ loading ? '上传中' : '选择图像' }}</span>
|
||||
</button>
|
||||
</nz-upload>
|
||||
<ng-template [ngTemplateOutlet]="actions"></ng-template>
|
||||
</div>
|
||||
<nz-button-group>
|
||||
<button nz-button (click)="showType = 'big'" [disabled]="showType === 'big'">
|
||||
<i nz-icon nzType="appstore"></i>
|
||||
</button>
|
||||
<button nz-button (click)="showType = 'small'" [disabled]="showType === 'small'">
|
||||
<i nz-icon nzType="bars"></i>
|
||||
</button>
|
||||
</nz-button-group>
|
||||
</div>
|
||||
<nz-spin [nzSpinning]="loading">
|
||||
<div *ngIf="showType === 'small'" class="file-manager__header">
|
||||
<div class="file-manager__header-name">Filename</div>
|
||||
<div class="file-manager__header-pixel">Pixel</div>
|
||||
<div class="file-manager__header-time">Changed</div>
|
||||
</div>
|
||||
<div class="file-manager" [ngClass]="{ 'file-manager__small': showType === 'small' }">
|
||||
<div *ngIf="path.length > 1" (click)="back()" class="file-item">
|
||||
<i class="file-item__icon" nz-icon nzType="rollback"></i>
|
||||
<div class="file-item__name">..</div>
|
||||
</div>
|
||||
<div *ngFor="let i of list; let idx = index" class="file-item" [ngClass]="{ 'file-item__selected': i.selected }" (click)="cho(i)">
|
||||
<i *ngIf="i.type === 'folder'" class="file-item__icon" nz-icon nzType="folder"></i>
|
||||
<ng-container *ngIf="i.type === 'file'">
|
||||
<i *ngIf="!i.is_img" class="file-item__icon" nz-icon nzType="file-{{ i.ext }}"></i>
|
||||
<div class="file-item__img" *ngIf="i.is_img" [ngStyle]="{ 'background-image': 'url(' + i.mp + ')' }"></div>
|
||||
</ng-container>
|
||||
<div class="file-item__name">{{ i.title }}</div>
|
||||
<div class="file-item__pixel">
|
||||
<span *ngIf="i.is_img">{{ i.width }}x{{ i.height }}</span>
|
||||
</div>
|
||||
<div class="file-item__time">{{ i.created | _date }}</div>
|
||||
<span nz-dropdown [nzDropdownMenu]="actionMenu" class="dd-btn file-item__actions">
|
||||
<i nz-icon nzType="ellipsis"></i>
|
||||
</span>
|
||||
<nz-dropdown-menu #actionMenu="nzDropdownMenu">
|
||||
<ul nz-menu>
|
||||
<li nz-menu-item nz-popconfirm nzPopconfirmTitle="确定吗?" (nzOnConfirm)="copyImg(i.id)">Copy</li>
|
||||
<li nz-menu-item (click)="copyData(i.mp, 'link')">Copy Link</li>
|
||||
<li nz-menu-item (click)="copyData(i.mp, 'code')">Copy Code</li>
|
||||
<li nz-menu-item (click)="rename(i)">Rename</li>
|
||||
<li nz-menu-item (click)="move(i)">Move</li>
|
||||
<li nz-menu-item nz-popconfirm nzPopconfirmTitle="确定吗?" (nzOnConfirm)="remove(i.id, idx)">Remove</li>
|
||||
</ul>
|
||||
</nz-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-md">
|
||||
<nz-pagination
|
||||
[(nzPageIndex)]="s.pi"
|
||||
(nzPageIndexChange)="load(s.pi)"
|
||||
[nzPageSize]="s.ps"
|
||||
[nzTotal]="total"
|
||||
nzHideOnSinglePage
|
||||
></nz-pagination>
|
||||
</div>
|
||||
<div class="no-data" *ngIf="total === 0">暂无</div>
|
||||
</nz-spin>
|
||||
<nz-modal [(nzVisible)]="renameModel" nzTitle="重命名" (nzOnOk)="renameOk()" (nzOnCancel)="renameModel = false">
|
||||
<input nz-input [(ngModel)]="renameTitle" name="renameTitle" />
|
||||
</nz-modal>
|
||||
<nz-modal [(nzVisible)]="moveModel" nzTitle="移动" (nzOnOk)="moveOk()" (nzOnCancel)="moveModel = false">
|
||||
<nz-tree-select class="d-block" [nzNodes]="folderNodes" nzShowSearch nzExpandAll [(ngModel)]="moveId"> </nz-tree-select>
|
||||
</nz-modal>
|
||||
188
src/app/shared/components/file-manager/file-manager.component.ts
Normal file
188
src/app/shared/components/file-manager/file-manager.component.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { ArrayService, copy } from '@delon/util';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
import { NzUploadFile } from 'ng-zorro-antd/upload';
|
||||
|
||||
@Component({
|
||||
selector: 'file-manager',
|
||||
templateUrl: './file-manager.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FileManagerComponent implements OnInit {
|
||||
private get parent_id(): number {
|
||||
return this.path[this.path.length - 1];
|
||||
}
|
||||
|
||||
constructor(private http: _HttpClient, private cdr: ChangeDetectorRef, private arrSrv: ArrayService, private msg: NzMessageService) {}
|
||||
|
||||
showType: 'big' | 'small' = 'big';
|
||||
s: any = { orderby: 0, ps: 20, pi: 1, q: '' };
|
||||
loading = false;
|
||||
list: any[] = [];
|
||||
item: any;
|
||||
path: number[] = [0];
|
||||
total = 0;
|
||||
|
||||
@Input() params: any;
|
||||
@Input() actions!: TemplateRef<any>;
|
||||
@Input() multiple: boolean | number = false;
|
||||
@Output() readonly selected = new EventEmitter<any>();
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region rename
|
||||
|
||||
renameModel = false;
|
||||
renameTitle = '';
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region move
|
||||
moveModel = false;
|
||||
moveId = '';
|
||||
folderNodes: any[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load(1);
|
||||
}
|
||||
|
||||
getCode(mp: string, type: 'link' | 'code'): string {
|
||||
return type === 'link' ? mp : `<img src="${mp}">`;
|
||||
}
|
||||
|
||||
// #region op
|
||||
|
||||
back(): void {
|
||||
this.path.pop();
|
||||
this.load(1);
|
||||
}
|
||||
|
||||
next(i: any): void {
|
||||
this.path.push(i.id);
|
||||
this.load(1);
|
||||
}
|
||||
|
||||
load(pi: number): void {
|
||||
const data = {
|
||||
...this.s,
|
||||
pi,
|
||||
parent_id: this.parent_id,
|
||||
...this.params
|
||||
};
|
||||
this.loading = true;
|
||||
this.cdr.markForCheck();
|
||||
this.http.get('/file', data).subscribe((res: any) => {
|
||||
this.loading = false;
|
||||
this.list = res.list;
|
||||
this.total = res.total;
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
cho(i: any): void {
|
||||
if (i.type === 'folder') {
|
||||
this.next(i);
|
||||
return;
|
||||
}
|
||||
i.selected = !i.selected;
|
||||
this.selected.emit(i);
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region upload
|
||||
|
||||
uploadData = () => {
|
||||
return {
|
||||
parent_id: this.parent_id
|
||||
};
|
||||
};
|
||||
|
||||
uploadChange({ file }: { file: NzUploadFile }): void {
|
||||
if (file.status === 'done') {
|
||||
this.load(1);
|
||||
}
|
||||
}
|
||||
rename(i: any): void {
|
||||
this.renameModel = true;
|
||||
this.item = i;
|
||||
this.renameTitle = i.title;
|
||||
}
|
||||
renameOk(): void {
|
||||
this.http
|
||||
.post(`/file/rename`, {
|
||||
id: this.item.id,
|
||||
title: this.renameTitle
|
||||
})
|
||||
.subscribe(() => {
|
||||
this.msg.success('Success');
|
||||
this.item.title = this.renameTitle;
|
||||
this.renameModel = false;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
move(i: any): void {
|
||||
this.moveModel = true;
|
||||
this.item = i;
|
||||
this.moveId = i.parent_id;
|
||||
this.http.get(`/file/folder`).subscribe((res: any[]) => {
|
||||
res.splice(0, 0, { id: 0, title: '根目录' });
|
||||
this.folderNodes = this.arrSrv.arrToTree(res, {
|
||||
cb: item => {
|
||||
item.key = item.id;
|
||||
if (item.id === this.moveId) {
|
||||
item.disabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
moveOk(): void {
|
||||
this.http
|
||||
.post(`/file/move`, {
|
||||
id: this.item.id,
|
||||
moveId: this.moveId
|
||||
})
|
||||
.subscribe(() => {
|
||||
this.msg.success('Success');
|
||||
this.moveModel = false;
|
||||
this.list.splice(
|
||||
this.list.findIndex(w => w.id === this.item.id),
|
||||
1
|
||||
);
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region copy
|
||||
|
||||
copyImg(id: number): void {
|
||||
this.http.post(`/file/copy/${id}`).subscribe((res: any) => {
|
||||
this.msg.success('Success');
|
||||
this.list.push(res.item);
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
copyData(mp: string, type: 'link' | 'code'): void {
|
||||
copy(this.getCode(mp, type)).then(() => this.msg.success('Copy Success'));
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region remove
|
||||
|
||||
remove(id: number, idx: number): void {
|
||||
this.http.delete(`/file/${id}`).subscribe(() => {
|
||||
this.msg.success('Success');
|
||||
this.list.splice(idx, 1);
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||
import { NzCardModule } from 'ng-zorro-antd/card';
|
||||
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
|
||||
import { NzGridModule } from 'ng-zorro-antd/grid';
|
||||
import { NzIconModule } from 'ng-zorro-antd/icon';
|
||||
import { NzInputModule } from 'ng-zorro-antd/input';
|
||||
import { NzModalModule } from 'ng-zorro-antd/modal';
|
||||
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
|
||||
import { NzSelectModule } from 'ng-zorro-antd/select';
|
||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||
import { NzTreeModule } from 'ng-zorro-antd/tree';
|
||||
import { NzTreeSelectModule } from 'ng-zorro-antd/tree-select';
|
||||
import { NzUploadModule } from 'ng-zorro-antd/upload';
|
||||
|
||||
import { FileManagerImgComponent } from './file-manager-img.component';
|
||||
import { FileManagerImgDirective } from './file-manager-img.directive';
|
||||
import { FileManagerComponent } from './file-manager.component';
|
||||
|
||||
const COMPONENTS = [FileManagerComponent, FileManagerImgComponent, FileManagerImgDirective];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
AlainThemeModule.forChild(),
|
||||
NzUploadModule,
|
||||
NzButtonModule,
|
||||
NzIconModule,
|
||||
NzSpinModule,
|
||||
NzPaginationModule,
|
||||
NzDropDownModule,
|
||||
NzModalModule,
|
||||
NzInputModule,
|
||||
NzTreeSelectModule,
|
||||
NzGridModule,
|
||||
NzTreeModule,
|
||||
NzSelectModule,
|
||||
NzCardModule
|
||||
],
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class FileManagerModule {}
|
||||
50
src/app/shared/components/file-manager/index.en-US.md
Normal file
50
src/app/shared/components/file-manager/index.en-US.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
order: 70
|
||||
title: file-manager
|
||||
type: Component
|
||||
---
|
||||
|
||||
File manager, [DEMO](https://preview.ng-alain.com/pro/#/sys/file-manager).
|
||||
|
||||
## API
|
||||
|
||||
### file-manager
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| ------------ | ------------------------------------------- | ------------------- | ------- |
|
||||
| `[params]` | Extra QueryString request parameter | `any` | - |
|
||||
| `[actions]` | Custom action template | `TemplateRef<any>` | - |
|
||||
| `[multiple]` | Whether to mulitple, or specified number | `boolean, number` | `false` |
|
||||
| `(selected)` | Resource selected event, not include folder | `EventEmitter<any>` | - |
|
||||
|
||||
### dialog-img
|
||||
|
||||
`dialog-img` is modal dialog based on the `file-manager` component, [DEMO](https://preview.ng-alain.com/pro/#/ec/ware/edit/10001).
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| ------------ | ------------------------------------------- | ------------------- | ------- |
|
||||
| `[multiple]` | Whether to mulitple, or specified number | `boolean, number` | `false` |
|
||||
| `[field]` | Specify to return a field data | `string` | - |
|
||||
| `(selected)` | Resource selected event, not include folder | `EventEmitter<any>` | - |
|
||||
|
||||
**DEMO**
|
||||
|
||||
You can only chooses 5 at most, trigger the `cho` event after confirmation.
|
||||
|
||||
```html
|
||||
<button dialog-img [multiple]="5" (selected)="cho(i, $event)"
|
||||
nz-button type="button" nzType="primary" nzSize="small">
|
||||
Choose
|
||||
</button>
|
||||
```
|
||||
|
||||
## sf widget
|
||||
|
||||
Widget name: `img`.
|
||||
|
||||
### ui
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|--------------|---------------------------------------------|------------------------|---------|
|
||||
| `[multiple]` | Whether to mulitple, or specified number | `boolean, number` | `false` |
|
||||
| `(selected)` | Resource selected event, not include folder | `(value: any) => void` | - |
|
||||
4
src/app/shared/components/file-manager/index.ts
Normal file
4
src/app/shared/components/file-manager/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './file-manager.component';
|
||||
export * from './file-manager-img.component';
|
||||
export * from './file-manager-img.directive';
|
||||
export * from './file-manager.module';
|
||||
50
src/app/shared/components/file-manager/index.zh-CN.md
Normal file
50
src/app/shared/components/file-manager/index.zh-CN.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
order: 70
|
||||
title: file-manager
|
||||
type: Component
|
||||
---
|
||||
|
||||
文件管理器,参考[示例](https://preview.ng-alain.com/pro/#/sys/file-manager)。
|
||||
|
||||
## API
|
||||
|
||||
### file-manager
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------------ | ------------------------------ | ------------------- | ------- |
|
||||
| `[params]` | 额外 QueryString 请求参数 | `any` | - |
|
||||
| `[actions]` | 自定义动作 | `TemplateRef<any>` | - |
|
||||
| `[multiple]` | 是否多选,若最多 N 张 | `boolean, number` | `false` |
|
||||
| `(selected)` | 当前资源选中事件,不包含文件夹 | `EventEmitter<any>` | - |
|
||||
|
||||
### dialog-img
|
||||
|
||||
`dialog-img` 是在 `file-manager` 组件的基础上对话框化,参考[示例](https://preview.ng-alain.com/pro/#/ec/ware/edit/10001)。
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------------ | ------------------------------ | ------------------- | ------- |
|
||||
| `[multiple]` | 是否多选,若最多 N 张 | `boolean, number` | `false` |
|
||||
| `[field]` | 指定返回某字段的数据 | `string` | - |
|
||||
| `(selected)` | 当前资源选中事件,不包含文件夹 | `EventEmitter<any>` | - |
|
||||
|
||||
**示例**
|
||||
|
||||
最多只能选择 5 张,确认后触发 `cho` 事件。
|
||||
|
||||
```html
|
||||
<button dialog-img [multiple]="5" (selected)="cho(i, $event)"
|
||||
nz-button type="button" nzType="primary" nzSize="small">
|
||||
选择照片
|
||||
</button>
|
||||
```
|
||||
|
||||
## sf 小部件
|
||||
|
||||
小部件名称:`img`。
|
||||
|
||||
### ui
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------------ | ------------------------------ | ---------------------- | ------- |
|
||||
| `[multiple]` | 是否多选,若最多 N 张 | `boolean, number` | `false` |
|
||||
| `(selected)` | 当前资源选中事件,不包含文件夹 | `(value: any) => void` | - |
|
||||
45
src/app/shared/components/masonry/index.en-US.md
Normal file
45
src/app/shared/components/masonry/index.en-US.md
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
order: 50
|
||||
title: masonry
|
||||
type: Component
|
||||
---
|
||||
|
||||
Based on [masonry](https://masonry.desandro.com/) grid layout, [DEMO](https://preview.ng-alain.com/pro/#/other/gallery).
|
||||
|
||||
## Container structure
|
||||
|
||||
```html
|
||||
<div nz-row nzGutter="8" masonry [disabled]="masonryDisabled">
|
||||
<div
|
||||
class="masonry__sizer ant-col-md-12 ant-col-xl-6 position-absolute"
|
||||
></div>
|
||||
<div
|
||||
*ngFor="let i of images; let idx=index"
|
||||
(click)="gallery.open(idx)"
|
||||
class="masonry__thm mb-sm"
|
||||
nz-col
|
||||
nzMd="12"
|
||||
nzXl="6"
|
||||
>
|
||||
<a class="img-thm img-thm__zoom-in">
|
||||
<i class="img-thm__mask"></i>
|
||||
<i class="img-thm__icon" nz-icon nzType="search"></i>
|
||||
<img
|
||||
class="img-thm__img"
|
||||
src="{{i.url}}"
|
||||
(load)="imgLoaded()"
|
||||
style="min-height: 150px"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
The `masonry__` prefix class styles is required part.
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|--------------|------------------------------------------------------|-----------|---------|
|
||||
| `[masonry]` | [Options](https://masonry.desandro.com/options.html) | `any` | - |
|
||||
| `[disabled]` | Whether to disable | `boolean` | `false` |
|
||||
2
src/app/shared/components/masonry/index.ts
Normal file
2
src/app/shared/components/masonry/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './masonry.directive';
|
||||
export * from './masonry.module';
|
||||
32
src/app/shared/components/masonry/index.zh-CN.md
Normal file
32
src/app/shared/components/masonry/index.zh-CN.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
order: 50
|
||||
title: masonry
|
||||
type: Component
|
||||
---
|
||||
|
||||
基于 [masonry](https://masonry.desandro.com/) 瀑布流布局,参考[示例](https://preview.ng-alain.com/pro/#/other/gallery)。
|
||||
|
||||
## 容器结构
|
||||
|
||||
```html
|
||||
<div nz-row nzGutter="8" masonry [disabled]="masonryDisabled">
|
||||
<div class="masonry__sizer ant-col-md-12 ant-col-xl-6 position-absolute"></div>
|
||||
<div *ngFor="let i of images; let idx=index" (click)="gallery.open(idx)"
|
||||
class="masonry__thm mb-sm" nz-col nzMd="12" nzXl="6">
|
||||
<a class="img-thm img-thm__zoom-in">
|
||||
<i class="img-thm__mask"></i>
|
||||
<i class="img-thm__icon" nz-icon nzType="search"></i>
|
||||
<img class="img-thm__img" src="{{i.url}}" (load)="imgLoaded()" style="min-height: 150px">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
其中 `masonry__` 前缀类样式是必须部分。
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|--------------|-------------------------------------------------|-----------|---------|
|
||||
| `[masonry]` | [选项](https://masonry.desandro.com/options.html) | `any` | - |
|
||||
| `[disabled]` | 是否禁用 | `boolean` | `false` |
|
||||
92
src/app/shared/components/masonry/masonry.directive.ts
Normal file
92
src/app/shared/components/masonry/masonry.directive.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnChanges, OnDestroy } from '@angular/core';
|
||||
import { BooleanInput, InputBoolean } from '@delon/util';
|
||||
import { fromEvent, Subscription } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
|
||||
declare var Masonry: any;
|
||||
|
||||
@Directive({
|
||||
selector: '[masonry]',
|
||||
exportAs: 'masonryComp'
|
||||
})
|
||||
export class MasonryDirective implements AfterViewInit, OnChanges, OnDestroy {
|
||||
static ngAcceptInputType_disabled: BooleanInput;
|
||||
|
||||
private masonry: any;
|
||||
private observer?: MutationObserver;
|
||||
private resize$: Subscription | null = null;
|
||||
|
||||
@Input('masonry') options: any;
|
||||
@Input() @InputBoolean() disabled = false;
|
||||
|
||||
constructor(private el: ElementRef, private zone: NgZone) {}
|
||||
|
||||
private outsideRender(cb: () => void): void {
|
||||
this.zone.runOutsideAngular(() => cb());
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.destroy();
|
||||
this.outsideRender(() => {
|
||||
this.masonry = new Masonry(this.el.nativeElement, {
|
||||
originLeft: true,
|
||||
transitionDuration: '0.3s',
|
||||
itemSelector: '.masonry__thm',
|
||||
columnWidth: '.masonry__sizer',
|
||||
...this.options
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.outsideRender(() => {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this.masonry.reloadItems();
|
||||
this.masonry.layout();
|
||||
});
|
||||
}
|
||||
|
||||
private destroy(): void {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
if (this.masonry) {
|
||||
this.masonry.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private initElChange(): void {
|
||||
if (this.observer || typeof MutationObserver === 'undefined') {
|
||||
return;
|
||||
}
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.observer = new MutationObserver(() => this.reload());
|
||||
this.observer.observe(this.el.nativeElement, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initElChange();
|
||||
this.resize$ = fromEvent(window, 'resize')
|
||||
.pipe(debounceTime(50))
|
||||
.subscribe(() => this.reload());
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy();
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
if (this.resize$) {
|
||||
this.resize$.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/app/shared/components/masonry/masonry.module.ts
Normal file
11
src/app/shared/components/masonry/masonry.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { MasonryDirective } from './masonry.directive';
|
||||
|
||||
const COMPONENTS = [MasonryDirective];
|
||||
|
||||
@NgModule({
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class MasonryModule {}
|
||||
34
src/app/shared/components/mouse-focus/index.en-US.md
Normal file
34
src/app/shared/components/mouse-focus/index.en-US.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
order: 80
|
||||
title: mouse-focus
|
||||
type: Component
|
||||
---
|
||||
|
||||
The focus of the mouse, Add class to focus element when mouse over it in a set of elements, Keeping last state when leaving the container.
|
||||
|
||||
## DEMO
|
||||
|
||||
```html
|
||||
<ul [mouseFocus]="{ time: 250, itemSelector: 'li', actionClassName: 'active'}">
|
||||
<li>
|
||||
<a href="javascript:;">Books</a>
|
||||
<img src="//qr.liantu.com/api.php?text=https://e.ng-alain.com/">
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="javascript:;">APP</a>
|
||||
<img src="//qr.liantu.com/api.php?text=https://e.ng-alain.com/">
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:;">WeChat</a>
|
||||
<img src="//qr.liantu.com/api.php?text=https://e.ng-alain.com/">
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| ----- | ------ | ----- | ------ |
|
||||
| `[delay]` | Delay (unit: milliseconds) | `number` | `250` |
|
||||
| `[itemSelector]` | Class name of element item | `string` | `li` |
|
||||
| `[actionClassName]` | Class name of focus element | `string` | `active` |
|
||||
2
src/app/shared/components/mouse-focus/index.ts
Normal file
2
src/app/shared/components/mouse-focus/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './mouse-focus.directive';
|
||||
export * from './mouse-focus.module';
|
||||
34
src/app/shared/components/mouse-focus/index.zh-CN.md
Normal file
34
src/app/shared/components/mouse-focus/index.zh-CN.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
order: 80
|
||||
title: mouse-focus
|
||||
type: Component
|
||||
---
|
||||
|
||||
鼠标焦点,在一组元素里鼠标移到某个元素时增加额外一个类名,当离开容器时保留最后一个状态。
|
||||
|
||||
## DEMO
|
||||
|
||||
```html
|
||||
<ul [mouseFocus]="{ time: 250, itemSelector: 'li', actionClassName: 'active'}">
|
||||
<li>
|
||||
<a href="javascript:;">必读</a>
|
||||
<img src="//qr.liantu.com/api.php?text=https://e.ng-alain.com/">
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="javascript:;">APP</a>
|
||||
<img src="//qr.liantu.com/api.php?text=https://e.ng-alain.com/">
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:;">微信</a>
|
||||
<img src="//qr.liantu.com/api.php?text=https://e.ng-alain.com/">
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|---------------------|---------------|----------|----------|
|
||||
| `[delay]` | 延迟(单位:毫秒) | `number` | `250` |
|
||||
| `[itemSelector]` | 项类名 | `string` | `li` |
|
||||
| `[actionClassName]` | 获得焦点时类名 | `string` | `active` |
|
||||
@ -0,0 +1,60 @@
|
||||
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
import { auditTime, takeUntil } from 'rxjs/operators';
|
||||
|
||||
export interface MouseFocusOptions {
|
||||
delay?: number;
|
||||
itemSelector?: string;
|
||||
actionClassName?: string;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: `[mouseFocus]`,
|
||||
exportAs: `mouseFocus`
|
||||
})
|
||||
export class MouseFocusDirective implements AfterViewInit, OnDestroy {
|
||||
private unsubscribe$ = new Subject<void>();
|
||||
private _cog!: MouseFocusOptions;
|
||||
private _curEl: HTMLElement | null = null;
|
||||
|
||||
@Input('mouseFocus')
|
||||
set config(value: MouseFocusOptions) {
|
||||
this._cog = {
|
||||
delay: 250,
|
||||
itemSelector: 'li',
|
||||
actionClassName: 'active',
|
||||
...value
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private el: ElementRef) {
|
||||
this.config = {};
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const { el, unsubscribe$, _cog } = this;
|
||||
let { _curEl } = this;
|
||||
const parentEl = el.nativeElement as HTMLElement;
|
||||
fromEvent(parentEl, 'mouseover')
|
||||
.pipe(takeUntil(unsubscribe$), auditTime(_cog.delay!))
|
||||
.subscribe((e: Event) => {
|
||||
const target = (e.target as HTMLElement).closest(_cog.itemSelector!) as HTMLElement;
|
||||
if (!target || !parentEl.contains(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_curEl) {
|
||||
_curEl.classList.remove(_cog.actionClassName!);
|
||||
}
|
||||
target.classList.add(_cog.actionClassName!);
|
||||
_curEl = target;
|
||||
});
|
||||
_curEl = (parentEl.querySelector(`.${_cog.actionClassName}`) as HTMLElement) || null;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
const { unsubscribe$ } = this;
|
||||
unsubscribe$.next();
|
||||
unsubscribe$.complete();
|
||||
}
|
||||
}
|
||||
11
src/app/shared/components/mouse-focus/mouse-focus.module.ts
Normal file
11
src/app/shared/components/mouse-focus/mouse-focus.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { MouseFocusDirective } from './mouse-focus.directive';
|
||||
|
||||
const COMPONENTS = [MouseFocusDirective];
|
||||
|
||||
@NgModule({
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class MouseFocusModule {}
|
||||
24
src/app/shared/components/scrollbar/index.en-US.md
Normal file
24
src/app/shared/components/scrollbar/index.en-US.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
order: 40
|
||||
title: scrollbar
|
||||
type: Component
|
||||
---
|
||||
|
||||
Based on [perfect-scrollbar](http://utatti.github.io/perfect-scrollbar/) perfect custom scrollbar plugin, [DEMO](https://preview.ng-alain.com/pro/#/other/chat).
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| ----------------- | ------------------------ | ------------------- | ------- |
|
||||
| `[options]` | [Options](https://github.com/utatti/perfect-scrollbar#options) | `ScrollbarOptions` | - |
|
||||
| `[disabled]` | Whether to disable | `boolean` | `false` |
|
||||
| `[psScrollX]` | `ps-scroll-x` event | `EventEmitter<any>` | - |
|
||||
| `[psScrollY]` | `ps-scroll-y` event | `EventEmitter<any>` | - |
|
||||
| `[psScrollUp]` | `ps-scroll-up` event | `EventEmitter<any>` | - |
|
||||
| `[psScrollDown]` | `ps-scroll-down` event | `EventEmitter<any>` | - |
|
||||
| `[psScrollLeft]` | `ps-scroll-left` event | `EventEmitter<any>` | - |
|
||||
| `[psScrollRight]` | `ps-scroll-right` event | `EventEmitter<any>` | - |
|
||||
| `[psXReachStart]` | `ps-x-reach-start` event | `EventEmitter<any>` | - |
|
||||
| `[psXReachEnd]` | `ps-x-reach-end` event | `EventEmitter<any>` | - |
|
||||
| `[psYReachStart]` | `ps-y-reach-start` event | `EventEmitter<any>` | - |
|
||||
| `[psYReachEnd]` | `ps-y-reach-end` event | `EventEmitter<any>` | - |
|
||||
3
src/app/shared/components/scrollbar/index.ts
Normal file
3
src/app/shared/components/scrollbar/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './scrollbar.directive';
|
||||
export * from './scrollbar.interface';
|
||||
export * from './scrollbar.module';
|
||||
24
src/app/shared/components/scrollbar/index.zh-CN.md
Normal file
24
src/app/shared/components/scrollbar/index.zh-CN.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
order: 40
|
||||
title: scrollbar
|
||||
type: Component
|
||||
---
|
||||
|
||||
基于 [perfect-scrollbar](http://utatti.github.io/perfect-scrollbar/) 自定义滚动条插件,参考[示例](https://preview.ng-alain.com/pro/#/other/chat)。
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ----- | ------ | ----- | ------ |
|
||||
| `[options]` | [选项](https://github.com/utatti/perfect-scrollbar#options) | `ScrollbarOptions` | - |
|
||||
| `[disabled]` | 是否禁用 | `boolean` | `false` |
|
||||
| `[psScrollX]` | `ps-scroll-x` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psScrollY]` | `ps-scroll-y` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psScrollUp]` | `ps-scroll-up` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psScrollDown]` | `ps-scroll-down` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psScrollLeft]` | `ps-scroll-left` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psScrollRight]` | `ps-scroll-right` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psXReachStart]` | `ps-x-reach-start` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psXReachEnd]` | `ps-x-reach-end` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psYReachStart]` | `ps-y-reach-start` 事件 | `EventEmitter<any>` | - |
|
||||
| `[psYReachEnd]` | `ps-y-reach-end` 事件 | `EventEmitter<any>` | - |
|
||||
115
src/app/shared/components/scrollbar/scrollbar.directive.ts
Normal file
115
src/app/shared/components/scrollbar/scrollbar.directive.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { AfterViewInit, Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, Output } from '@angular/core';
|
||||
import { toBoolean } from '@delon/util';
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { PerfectScrollbarEvent, PerfectScrollbarEvents, ScrollbarOptions } from './scrollbar.interface';
|
||||
|
||||
@Directive({
|
||||
selector: '[scrollbar]',
|
||||
exportAs: 'scrollbarComp'
|
||||
})
|
||||
export class ScrollbarDirective implements AfterViewInit, OnDestroy {
|
||||
static ngAcceptInputType_options: ScrollbarOptions | string | null | undefined;
|
||||
|
||||
private instance: PerfectScrollbar | null = null;
|
||||
private readonly ngDestroy: Subject<void> = new Subject();
|
||||
private _disabled = false;
|
||||
|
||||
// #region fields
|
||||
|
||||
@Input('scrollbar') options?: ScrollbarOptions;
|
||||
|
||||
@Input()
|
||||
set disabled(value: boolean) {
|
||||
this._disabled = toBoolean(value)!;
|
||||
if (this._disabled) {
|
||||
this.ngOnDestroy();
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
@Output() readonly psScrollX: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() readonly psScrollY: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Output() readonly psScrollUp: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() readonly psScrollDown: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() readonly psScrollLeft: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() readonly psScrollRight: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Output() readonly psXReachStart: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() readonly psXReachEnd: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() readonly psYReachStart: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() readonly psYReachEnd: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
// #endregion
|
||||
|
||||
scrollToBottom(): void {
|
||||
this.el.scrollTop = this.el.scrollHeight - this.el.clientHeight;
|
||||
}
|
||||
|
||||
scrollToTop(): void {
|
||||
this.el.scrollTop = 0;
|
||||
}
|
||||
|
||||
scrollToLeft(): void {
|
||||
this.el.scrollLeft = 0;
|
||||
}
|
||||
|
||||
scrollToRight(): void {
|
||||
this.el.scrollLeft = this.el.scrollWidth - this.el.clientWidth;
|
||||
}
|
||||
|
||||
constructor(private elRef: ElementRef, private zone: NgZone) {}
|
||||
|
||||
private get el(): HTMLElement {
|
||||
return this.elRef.nativeElement as HTMLElement;
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const options = {
|
||||
wheelSpeed: 0.5,
|
||||
swipeEasing: true,
|
||||
wheelPropagation: false,
|
||||
minScrollbarLength: 40,
|
||||
maxScrollbarLength: 300,
|
||||
...this.options
|
||||
};
|
||||
setTimeout(() => {
|
||||
if (this._disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.instance = new PerfectScrollbar(this.el, options);
|
||||
|
||||
PerfectScrollbarEvents.forEach((eventName: PerfectScrollbarEvent) => {
|
||||
const eventType = eventName.replace(/([A-Z])/g, c => `-${c.toLowerCase()}`);
|
||||
|
||||
fromEvent<Event>(this.el, eventType)
|
||||
.pipe(debounceTime(20), takeUntil(this.ngDestroy))
|
||||
.subscribe((event: Event) => {
|
||||
this[eventName].emit(event);
|
||||
});
|
||||
});
|
||||
}, this.options?.delay || 0);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.ngDestroy.next();
|
||||
this.ngDestroy.complete();
|
||||
this.zone.runOutsideAngular(() => {
|
||||
if (this.instance) {
|
||||
this.instance.destroy();
|
||||
}
|
||||
this.instance = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
35
src/app/shared/components/scrollbar/scrollbar.interface.ts
Normal file
35
src/app/shared/components/scrollbar/scrollbar.interface.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
|
||||
export interface ScrollbarOptions extends PerfectScrollbar.Options {
|
||||
/**
|
||||
* 延迟初始化
|
||||
*/
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export type PerfectScrollbarEvent =
|
||||
| 'psScrollY'
|
||||
| 'psScrollX'
|
||||
| 'psScrollUp'
|
||||
| 'psScrollDown'
|
||||
| 'psScrollLeft'
|
||||
| 'psScrollRight'
|
||||
| 'psYReachEnd'
|
||||
| 'psYReachStart'
|
||||
| 'psXReachEnd'
|
||||
| 'psXReachStart';
|
||||
|
||||
export const PerfectScrollbarEvents: PerfectScrollbarEvent[] = [
|
||||
'psScrollY',
|
||||
'psScrollX',
|
||||
|
||||
'psScrollUp',
|
||||
'psScrollDown',
|
||||
'psScrollLeft',
|
||||
'psScrollRight',
|
||||
|
||||
'psYReachEnd',
|
||||
'psYReachStart',
|
||||
'psXReachEnd',
|
||||
'psXReachStart'
|
||||
];
|
||||
11
src/app/shared/components/scrollbar/scrollbar.module.ts
Normal file
11
src/app/shared/components/scrollbar/scrollbar.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { ScrollbarDirective } from './scrollbar.directive';
|
||||
|
||||
const COMPONENTS = [ScrollbarDirective];
|
||||
|
||||
@NgModule({
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class ScrollbarModule {}
|
||||
23
src/app/shared/components/status-label/index.en-US.md
Normal file
23
src/app/shared/components/status-label/index.en-US.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
order: 10
|
||||
title: status-label
|
||||
type: Component
|
||||
---
|
||||
|
||||
Status label, [DEMO](https://preview.ng-alain.com/ms/#/dns/domain)。
|
||||
|
||||
## DEMO
|
||||
|
||||
```html
|
||||
<status-label>Normal</status-label>
|
||||
<status-label type="error" text="Error"></status-label>
|
||||
<span status-label type="error" text="Error"></span>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | --------------------------------------- | ----------------------- | --------- |
|
||||
| `[type]` | Type of status label | `success,error,warning` | `success` |
|
||||
| `[icon]` | Whether show icon | `boolean` | `true` |
|
||||
| `[text]` | Text of status label, or `[ng-content]` | `string` | - |
|
||||
2
src/app/shared/components/status-label/index.ts
Normal file
2
src/app/shared/components/status-label/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './status-label.component';
|
||||
export * from './status-label.module';
|
||||
23
src/app/shared/components/status-label/index.zh-CN.md
Normal file
23
src/app/shared/components/status-label/index.zh-CN.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
order: 10
|
||||
title: status-label
|
||||
type: Component
|
||||
---
|
||||
|
||||
状态标签,参考[示例](https://preview.ng-alain.com/ms/#/dns/domain)。
|
||||
|
||||
## DEMO
|
||||
|
||||
```html
|
||||
<status-label>Normal</status-label>
|
||||
<status-label type="error" text="Error"></status-label>
|
||||
<span status-label type="error" text="Error"></span>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| -------- | ----------------------- | ----------------------- | --------- |
|
||||
| `[type]` | 类型 | `success,error,warning` | `success` |
|
||||
| `[icon]` | 是否显示图标 | `boolean` | `true` |
|
||||
| `[text]` | 文本,或 `[ng-content]` | `string` | - |
|
||||
@ -0,0 +1,55 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core';
|
||||
import { BooleanInput, InputBoolean } from '@delon/util';
|
||||
|
||||
@Component({
|
||||
selector: 'status-label, [status-label]',
|
||||
template: `
|
||||
<i *ngIf="icon" nz-icon [nzType]="iconType" class="pr-xs"></i>
|
||||
{{ text }}
|
||||
<ng-content></ng-content>
|
||||
`,
|
||||
host: {
|
||||
'[class.text-success]': `_t=='success'`,
|
||||
'[class.text-error]': `_t=='error'`,
|
||||
'[class.text-orange]': `_t=='warning'`
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class StatusLabelComponent implements OnChanges {
|
||||
static ngAcceptInputType_icon: BooleanInput;
|
||||
|
||||
_t?: string;
|
||||
|
||||
iconType!: string;
|
||||
|
||||
@Input()
|
||||
set type(v: 'success' | 'error' | 'warning') {
|
||||
let iconType: string;
|
||||
switch (v) {
|
||||
case 'success':
|
||||
iconType = 'check-circle';
|
||||
break;
|
||||
case 'error':
|
||||
iconType = 'close-circle';
|
||||
break;
|
||||
case 'warning':
|
||||
default:
|
||||
iconType = 'exclamation-circle';
|
||||
break;
|
||||
}
|
||||
this._t = v;
|
||||
this.iconType = iconType;
|
||||
}
|
||||
|
||||
@Input() @InputBoolean() icon = true;
|
||||
|
||||
@Input() text?: string;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {
|
||||
this.type = 'success';
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NzIconModule } from 'ng-zorro-antd/icon';
|
||||
|
||||
import { StatusLabelComponent } from './status-label.component';
|
||||
|
||||
const COMPONENTS = [StatusLabelComponent];
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, NzIconModule],
|
||||
declarations: COMPONENTS,
|
||||
exports: COMPONENTS
|
||||
})
|
||||
export class StatusLabelModule {}
|
||||
15
src/app/shared/index.ts
Normal file
15
src/app/shared/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Modules
|
||||
export * from './components/delay/index';
|
||||
export * from './components/editor/index';
|
||||
export * from './components/file-manager/index';
|
||||
export * from './components/masonry/index';
|
||||
export * from './components/mouse-focus/index';
|
||||
export * from './components/status-label/index';
|
||||
export * from './components/scrollbar/index';
|
||||
export * from './components/address/index';
|
||||
|
||||
// Utils
|
||||
export * from './utils/yuan';
|
||||
|
||||
// Module
|
||||
export * from './shared.module';
|
||||
7
src/app/shared/interfaces/api/i-api-response.i.ts
Normal file
7
src/app/shared/interfaces/api/i-api-response.i.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface IApiResponse {
|
||||
success?: boolean;
|
||||
msg?: string;
|
||||
status?: number;
|
||||
state?: number;
|
||||
data?: any;
|
||||
}
|
||||
26
src/app/shared/interfaces/core/i-base.i.ts
Normal file
26
src/app/shared/interfaces/core/i-base.i.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Observable } from 'rxjs';
|
||||
export interface IBase {
|
||||
// 增
|
||||
addOne(params: any, url: string): Observable<any>;
|
||||
asyncAddOne(params: any, url: string): Promise<any>;
|
||||
addMany(params: any[], url: string): Observable<any>;
|
||||
asyncAddMany(params: any[], url: string): Promise<any>;
|
||||
|
||||
// 删
|
||||
delOne(params: any, url: string): Observable<any>;
|
||||
asyncDelOne(params: any, url: string): Promise<any>;
|
||||
delMany(params: any[], url: string): Observable<any>;
|
||||
asyncDelMany(params: any[], url: string): Promise<any>;
|
||||
|
||||
// 改
|
||||
updateOne(params: any, url: string): Observable<any>;
|
||||
asyncUpdateOne(params: any, url: string): Promise<any>;
|
||||
updateMany(params: any[], url: string): Observable<any>;
|
||||
asyncUpdateMany(params: any[], url: string): Promise<any>;
|
||||
|
||||
// 查
|
||||
getOne(params: any, url: string): Observable<any[]>;
|
||||
asyncGetOne(params: any, url: string): Promise<any[]>;
|
||||
getMany(params: any, url: string): Observable<any[]>;
|
||||
asyncGetMany(params: any, url: string): Promise<any[]>;
|
||||
}
|
||||
44
src/app/shared/interfaces/core/i-cache-obj.ts
Normal file
44
src/app/shared/interfaces/core/i-cache-obj.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* .::::.
|
||||
* .::::::::.
|
||||
* :::::::::::
|
||||
* ..:::::::::::'
|
||||
* '::::::::::::'
|
||||
* .::::::::::
|
||||
* '::::::::::::::..
|
||||
* ..::::::::::::.
|
||||
* ``::::::::::::::::
|
||||
* ::::``:::::::::' .:::.
|
||||
* ::::' ':::::' .::::::::.
|
||||
* .::::' :::: .:::::::'::::.
|
||||
* .:::' ::::: .:::::::::' ':::::.
|
||||
* .::' :::::.:::::::::' ':::::.
|
||||
* .::' ::::::::::::::' ``::::.
|
||||
* ...::: ::::::::::::' ``::.
|
||||
* ````':. ':::::::::' ::::..
|
||||
* '.:::::' ':'````..
|
||||
*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-06-10 16:04:50
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-06-11 10:15:26
|
||||
* @Description: 缓存数据对象
|
||||
*/
|
||||
export interface ICacheObj {
|
||||
/**
|
||||
* 生产日期
|
||||
*/
|
||||
pd: number;
|
||||
/**
|
||||
* 缓存数据
|
||||
*/
|
||||
data: any;
|
||||
/**
|
||||
* 有效时间
|
||||
*/
|
||||
vld?: number;
|
||||
/**
|
||||
* 是否加密
|
||||
*/
|
||||
encrypt?: boolean;
|
||||
}
|
||||
6
src/app/shared/interfaces/index.ts
Normal file
6
src/app/shared/interfaces/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// Api
|
||||
export * from './api/i-api-response.i';
|
||||
|
||||
// Core
|
||||
export * from './core/i-base.i';
|
||||
export * from './core/i-cache-obj';
|
||||
112
src/app/shared/services/business/account.service.ts
Normal file
112
src/app/shared/services/business/account.service.ts
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-22 11:42:26
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-05-27 11:22:02
|
||||
* @Description: 全局账号服务
|
||||
*/
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { CoreService, StartupService } from '@core';
|
||||
import { ReuseTabService } from '@delon/abc/reuse-tab';
|
||||
import { BaseService } from '../core/base.service';
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAAccountService extends BaseService {
|
||||
// 验证码登录
|
||||
public $api_login_by_captcha = `/scm/cuc/cuc/user/sms/login?_allow_anonymous=true`;
|
||||
// 账号密码登录
|
||||
public $api_login_by_account = `/scm/cuc/cuc/user/login?_allow_anonymous=true`;
|
||||
// 退出登录
|
||||
public $api_logout = `/chia/user/logout`;
|
||||
|
||||
constructor(public injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
// 注入核心服务
|
||||
private get coreSrv(): CoreService {
|
||||
return this.injector.get(CoreService);
|
||||
}
|
||||
|
||||
// 注入路由
|
||||
private get router(): Router {
|
||||
return this.injector.get(Router);
|
||||
}
|
||||
|
||||
// 注入路由复用服务
|
||||
private get reuseTabService(): ReuseTabService {
|
||||
return this.injector.get(ReuseTabService);
|
||||
}
|
||||
// 注入全局启动服务
|
||||
private get startSrv(): StartupService {
|
||||
return this.injector.get(StartupService);
|
||||
}
|
||||
|
||||
// 登录状态
|
||||
public get loginStatus(): boolean {
|
||||
return this.coreSrv.loginStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号密码登录
|
||||
* @param username 登录账号
|
||||
* @param password 登录密码
|
||||
*/
|
||||
loginByAccount(username: string, password: string): void {
|
||||
this.asyncRequest(this.$api_login_by_account, { username, password, type: 0 }, 'POST', true, 'FORM').then((res) => {
|
||||
console.log(res);
|
||||
this.doAfterLogin(res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
* @param phone 手机号码
|
||||
* @param smsCode 短信验证码
|
||||
*/
|
||||
loginByCaptcha(phone: string, smsCode: string): void {
|
||||
this.asyncRequest(this.$api_login_by_captcha, { phone, smsCode }, 'POST', true, 'FORM').then((res) => {
|
||||
this.doAfterLogin(res);
|
||||
});
|
||||
}
|
||||
|
||||
private doAfterLogin(res: any): void {
|
||||
const token = res?.token;
|
||||
if (token) {
|
||||
// 清空路由复用信息
|
||||
this.reuseTabService.clear();
|
||||
|
||||
// 设置用户Token信息
|
||||
// TODO: Mock expired value
|
||||
// res.user.expired = +new Date() + 1000 * 60 * 5;
|
||||
this.coreSrv.tokenSrv.set({ token });
|
||||
// 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响
|
||||
this.startSrv.load().then(() => {
|
||||
let url = this.coreSrv.tokenSrv.referrer!.url || '/';
|
||||
if (url.includes('/passport')) {
|
||||
url = '/';
|
||||
}
|
||||
this.router.navigateByUrl(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出系统
|
||||
* @param showMsg 是否显示登录过期弹窗
|
||||
*/
|
||||
logout(showMsg: boolean): void {
|
||||
this.coreSrv.logout(showMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向服务器请求登出
|
||||
*/
|
||||
requestLogout(): void {
|
||||
this.asyncRequest(this.$api_logout).finally(() => {
|
||||
this.logout(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
88
src/app/shared/services/business/authorization.service.ts
Normal file
88
src/app/shared/services/business/authorization.service.ts
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-22 11:42:26
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-06-17 14:58:35
|
||||
* @Description: 全局权限服务
|
||||
*/
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { cacheConf } from '@conf/cache.conf';
|
||||
import { StartupService } from '@core';
|
||||
import { ReuseTabService } from '@delon/abc/reuse-tab';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { SettingsService } from '@delon/theme';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { BaseService } from '../core/base.service';
|
||||
import { EACacheService } from '../core/cache.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAAuthorizationService extends BaseService {
|
||||
// 获取我的角色
|
||||
public $api_get_my_roles = `/chia/roleInfo/getMyRoleList`;
|
||||
|
||||
constructor(public injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
// 注入路由
|
||||
private get router(): Router {
|
||||
return this.injector.get(Router);
|
||||
}
|
||||
// 注入缓存服务
|
||||
private get cacheSrv(): EACacheService {
|
||||
return this.injector.get(EACacheService);
|
||||
}
|
||||
// 注入令牌服务
|
||||
private get tokenSrv(): ITokenService {
|
||||
return this.injector.get(DA_SERVICE_TOKEN);
|
||||
}
|
||||
// 注入路由复用服务
|
||||
private get reuseTabService(): ReuseTabService {
|
||||
return this.injector.get(ReuseTabService);
|
||||
}
|
||||
// 注入全局启动服务
|
||||
private get startSrv(): StartupService {
|
||||
return this.injector.get(StartupService);
|
||||
}
|
||||
// 注入全局设置服务
|
||||
private get settingSrv(): SettingsService {
|
||||
return this.injector.get(SettingsService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色列表
|
||||
* @param force 强制刷新 默认值:false
|
||||
* @returns 角色列表, 类型 Array<any> | Observable<Array<any>>
|
||||
*/
|
||||
getMyRoles(force: boolean = false): Observable<Array<any>> {
|
||||
// 强制刷新
|
||||
if (force) {
|
||||
return this.refreshRoles();
|
||||
}
|
||||
|
||||
// 从缓存中提取角色
|
||||
const roles = this.cacheSrv.get(cacheConf.role) || [];
|
||||
|
||||
// 当缓存中不存在角色时,尝试从服务器拉取角色数据
|
||||
if (roles.length > 0) {
|
||||
return of(roles);
|
||||
}
|
||||
|
||||
return this.refreshRoles();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新角色权限
|
||||
*/
|
||||
private refreshRoles(): Observable<Array<any>> {
|
||||
return this.request(this.$api_get_my_roles).pipe(
|
||||
tap((res) => {
|
||||
this.cacheSrv.set(cacheConf.role, res);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
76
src/app/shared/services/business/captcha.service.ts
Normal file
76
src/app/shared/services/business/captcha.service.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-22 11:42:26
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-03-29 14:45:43
|
||||
* @Description: 全局验证码服务
|
||||
*/
|
||||
import { ComponentRef, Injectable, Injector } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { BaseService } from '../core/base.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EACaptchaService extends BaseService {
|
||||
// 通过手机号发送短信验证码
|
||||
private $api_send_sms_by_mobile = `/scm/sms/sms/verification/getSMVerificationCode?_allow_anonymous=true&_allow_badcode=true`;
|
||||
|
||||
// 验证手机号为平台用户后发送短信验证码
|
||||
private $api_send_sms__by_validate_mobile = `/chiauserBasicInfo/forgetPassword/getAccountSMVerificationCode`;
|
||||
|
||||
// 滑块验证码获取短信
|
||||
$api_dun_sms = `/scce/pbc/pbc/verification/verificationSlider?_allow_anonymous=true&_allow_badcode=true`;
|
||||
|
||||
// 根据当前登录用户绑定的手机号码获取短信验证码
|
||||
$api_captcha_sms_code = `/scce/pbc/pbc/verification/getSMVerificationCodeByToken`;
|
||||
|
||||
// 获取应用租户的管理员用户发送验证码
|
||||
$api_getAppLesseeAdminSMVerificationCode = `/chiauserBasicInfo/getAppLesseeAdminSMVerificationCode`;
|
||||
|
||||
/**
|
||||
* 根据当前登录用户绑定的手机号码获取短信验证码
|
||||
*/
|
||||
getCaptchaBySMSNoPhone(): Observable<any> {
|
||||
return this.request(this.$api_captcha_sms_code, { appId: this.envSrv.getEnvironment().appId }, 'POST', true, 'FORM');
|
||||
}
|
||||
/**
|
||||
* 获取应用租户的管理员用户发送验证码
|
||||
*/
|
||||
getAppLesseeAdminSMVerificationCode(): Observable<any> {
|
||||
return this.request(this.$api_getAppLesseeAdminSMVerificationCode, { appId: this.envSrv.getEnvironment().appId }, 'POST', true, 'FORM');
|
||||
}
|
||||
constructor(public injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param mobile 手机号码
|
||||
*/
|
||||
sendSMSCaptchaByMobile(mobile: string): Observable<any> {
|
||||
return this.request(
|
||||
this.$api_send_sms_by_mobile,
|
||||
{ appId: this.envSrv.getEnvironment()?.appId, phoneNumber: mobile },
|
||||
'POST',
|
||||
true,
|
||||
'FORM',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑块验证获取短信码
|
||||
* @param tel 手机号
|
||||
* @param validate 滑动验证通过字符串
|
||||
* @param url api地址
|
||||
*/
|
||||
getCaptchaByDun(mobile: string, validate: string, url?: string): Observable<any> {
|
||||
return this.request(
|
||||
url || this.$api_dun_sms,
|
||||
{ appId: this.envSrv.getEnvironment()?.appId, phoneNumber: mobile, user: mobile, validate },
|
||||
'POST',
|
||||
true,
|
||||
'FORM',
|
||||
);
|
||||
}
|
||||
}
|
||||
125
src/app/shared/services/business/enterprise.service.ts
Normal file
125
src/app/shared/services/business/enterprise.service.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { cacheConf } from '@conf/cache.conf';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { BaseService } from '../core/base.service';
|
||||
import { EACacheService } from '../core/cache.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAEnterpriseService extends BaseService {
|
||||
// 获取当前用户绑定的分销商/代理商/供应商
|
||||
public $api_get_my_enterprises = `/chia/enterpriseInfo/list/queryMyAppInfo`;
|
||||
|
||||
// 向服务器变更默认企业
|
||||
public $api_selectEnterprise = '/chia/userApp/selectEnterprise';
|
||||
constructor(public injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
// 注入缓存服务
|
||||
private get cacheSrv(): EACacheService {
|
||||
return this.injector.get(EACacheService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载企业信息
|
||||
*/
|
||||
loadEnterpises(): Observable<any> {
|
||||
return this.request(this.$api_get_my_enterprises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户ID获取指定的企业
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
getOne(tenantId: string): any {
|
||||
const enterprises = this.getAll();
|
||||
return enterprises.find((r) => r.tenantId === tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有企业
|
||||
*/
|
||||
getAll(): Array<any> {
|
||||
return this.cacheSrv.get(cacheConf.enterprise) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认企业
|
||||
*/
|
||||
getDefault(): any {
|
||||
const list = this.getAll();
|
||||
return list.find((r) => r.defaultState) || list[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认企业
|
||||
*/
|
||||
setDefault(tenantId: string): void {
|
||||
// 获取指定tenantId的企业
|
||||
const enterprise = this.getOne(tenantId);
|
||||
if (enterprise) {
|
||||
let enterpriseList = this.getAll();
|
||||
|
||||
// 设置指定tenantId的企业为默认企业,其他的企业为非默认企业
|
||||
enterpriseList = enterpriseList.map((m) => {
|
||||
if (m.tenantId === enterprise.tenantId) {
|
||||
m.defaultState = true;
|
||||
} else {
|
||||
m.defaultState = false;
|
||||
}
|
||||
return m;
|
||||
});
|
||||
// 重新缓存新的企业列表
|
||||
this.setCache(enterpriseList);
|
||||
|
||||
// 广播当前环境信息
|
||||
this.envSrv.setEnvironment(enterprise.appId, enterprise.tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存企业信息
|
||||
* @param enterpriseData 企业信息信息
|
||||
*/
|
||||
setCache(enterpriseData: any): void {
|
||||
this.cacheSrv.set(cacheConf.enterprise, enterpriseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择企业
|
||||
* @param item 企业实体
|
||||
* 选择企业进入管理后台
|
||||
* 1. 判断当前企业是否被冻结,如企业为冻结状态,则toast提示“供应商已被冻结,无法访问,请联系客服处理”
|
||||
* 2. 向服务器请求变更当前企业身份
|
||||
* 3. 服务器变更当前企业后,刷新本地缓存中的企业信息
|
||||
* 4. 跳转到管理后台首页
|
||||
*/
|
||||
changeDefaultEnterprise(item: any): Observable<any> {
|
||||
// 1. 判断冻结状态
|
||||
// if (item.stateLocked) {
|
||||
// this.msgSrv.warning('代理商已被冻结,无法访问,请联系客服处理');
|
||||
// return of(false);
|
||||
// }
|
||||
// 2.判断用户状态userStatus:0-正常,1-冻结,2-废弃
|
||||
// if (item.userStatus === 1) {
|
||||
// this.msgSrv.warning('您的帐号已被企业冻结,无法访问,请联系客服处理');
|
||||
// return of(false);
|
||||
// }
|
||||
// if (item.userStatus === 2) {
|
||||
// this.msgSrv.warning('您的帐号已被企业废弃,无法访问,请联系客服处理');
|
||||
// return of(false);
|
||||
// }
|
||||
// 3. 向服务器请求变更当前企业身份
|
||||
return this.request(this.$api_selectEnterprise, { tenantId: item.tenantId }).pipe(
|
||||
tap((data) => {
|
||||
if (data) {
|
||||
// 切换成功则广播当前环境信息
|
||||
this.envSrv.setEnvironment(item.appId, item.tenantId);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
306
src/app/shared/services/business/sl-platform.service.ts
Normal file
306
src/app/shared/services/business/sl-platform.service.ts
Normal file
@ -0,0 +1,306 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-22 11:42:26
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-05-27 11:07:18
|
||||
* @Description: 全局系统服务
|
||||
*/
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { Observable, zip } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
import { CoreService } from 'src/app/core/core.service';
|
||||
import { EAEncryptUtil } from '../../utils';
|
||||
import { BaseService } from '../core/base.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAPlatformService extends BaseService {
|
||||
public $api_update_platform_name = `/chia/operationInfo/updatePlatformName`; // 修改当前登录平台名称
|
||||
|
||||
/**
|
||||
* 判断是否已经设置平台名称
|
||||
*/
|
||||
private $api_is_set_platform_name = `/chia/operationInfo/judgeSetPlatformName`;
|
||||
/**
|
||||
* 获取当前登录用户绑定的运营商有效期
|
||||
*/
|
||||
private $api_get_validity_period_of_operator = `/chia/operationInfo/getOperationDetailByToken`;
|
||||
|
||||
/**
|
||||
* 缓存平台状态键名
|
||||
*/
|
||||
private _cachePlatformStatusKey = '_cpsk';
|
||||
/**
|
||||
* 运营商剩余有效期
|
||||
*/
|
||||
private _validityPeriodOfOperator: Observable<number> | null = null;
|
||||
/**
|
||||
* 允许运营商超出有效期时登录
|
||||
*/
|
||||
private _allowLoginBeyondValidity = true;
|
||||
/**
|
||||
* 获取基础配置
|
||||
*/
|
||||
// public $api_get_config = `/scce/pbc/pbc/baseConfig/getBaseConfigList?_allow_anonymous=true`;
|
||||
constructor(public injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
// 注入核心服务
|
||||
private get coreSrv(): CoreService {
|
||||
return this.injector.get(CoreService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台状态(参考Http状态码)
|
||||
* 200:平台状态正常
|
||||
* 401: 未设置平台名称
|
||||
* 403
|
||||
*/
|
||||
getPlatformStatus(): number {
|
||||
const encryptStatus = this.coreSrv.cacheSrv.getNone<string>(this._cachePlatformStatusKey);
|
||||
try {
|
||||
const status = EAEncryptUtil.deencryptByDeAES(encryptStatus);
|
||||
return +status;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前运营商剩余有效期
|
||||
* @returns 有效期天数
|
||||
*/
|
||||
getValidityPeriodOfOperator(): Observable<number> {
|
||||
return this.loadValidityPeriodOfOperator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载平台状态
|
||||
*
|
||||
* @returns 平台状态码
|
||||
*/
|
||||
loadPlatformStatus(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
zip(this.loadIsSetPlatformName(), this.loadValidityPeriodOfOperator())
|
||||
.pipe(
|
||||
catchError((res) => {
|
||||
console.error(`加载平台状态时发生错误:`, res);
|
||||
this.msgSrv.error(`加载平台状态时发生错误!`);
|
||||
resolve();
|
||||
return [];
|
||||
}),
|
||||
)
|
||||
.subscribe(
|
||||
([nameStauts, validityPeriod]) => this.setPlatformStatus(nameStauts, validityPeriod),
|
||||
() => {},
|
||||
() => resolve(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台名称设置状态
|
||||
* @param nameStatus 平台名称设置状态
|
||||
* @param validityPeriod 运营剩余有效期天数
|
||||
*/
|
||||
setPlatformStatus(nameStatus: boolean, validityPeriod: number) {
|
||||
let status = 0;
|
||||
// 判断平台名称
|
||||
if (status !== 200) {
|
||||
status = nameStatus ? 200 : 401;
|
||||
}
|
||||
|
||||
// 判断运营商有效期
|
||||
if (!this._allowLoginBeyondValidity && status === 200) {
|
||||
status = validityPeriod > 0 ? 200 : 402;
|
||||
}
|
||||
|
||||
// 加密并保存平台状态
|
||||
const ciphertext = EAEncryptUtil.encryptByEnAES(status.toString());
|
||||
this.coreSrv.cacheSrv.set(this._cachePlatformStatusKey, ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台是否设置平台名称
|
||||
* @returns true | false
|
||||
*/
|
||||
private loadIsSetPlatformName(): Observable<boolean> {
|
||||
return this.request(this.$api_is_set_platform_name).pipe(switchMap(async (sm) => sm === true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前账户绑定的运营商有效期
|
||||
* @returns 有效期时间(天)
|
||||
*/
|
||||
private loadValidityPeriodOfOperator(): Observable<number> {
|
||||
return this.request(this.$api_get_validity_period_of_operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定平台的地址
|
||||
* @param platformName 平台名称
|
||||
* @param hasOperationCode 是否需要运营商代码
|
||||
*/
|
||||
getPlatfomrUrl(platformName: string, hasOperationCode = true) {
|
||||
const url: string = this.getAllPlatformUrls().find((r: any) => r.name === platformName)?.url || '';
|
||||
if (hasOperationCode) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return url.includes('?') ? url.substring(0, url.indexOf('?')) : url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有平台的地址
|
||||
*/
|
||||
getAllPlatformUrls() {
|
||||
let platforms: any = [];
|
||||
const sc = this.coreSrv.tokenSrv.get()?.sc;
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
switch (hostname) {
|
||||
case 'localhost':
|
||||
platforms = [
|
||||
{
|
||||
name: '运营后台',
|
||||
url: `${protocol}//${hostname}:8001/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '开放平台',
|
||||
url: `${protocol}//localhost:8005/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '供应商平台',
|
||||
url: `${protocol}//localhost:8004/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '分销商平台',
|
||||
url: `${protocol}//localhost:8003/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '代理商平台',
|
||||
url: `${protocol}//localhost:8002/#/?v=${sc}`,
|
||||
},
|
||||
];
|
||||
break;
|
||||
case 'sce-ows-test.380star.com':
|
||||
platforms = [
|
||||
{
|
||||
name: '运营后台',
|
||||
url: `${protocol}//${hostname}${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '开放平台',
|
||||
url: `${protocol}//sce-opc-test.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '供应商平台',
|
||||
url: `${protocol}//sce-cvc-test.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '分销商平台',
|
||||
url: `${protocol}//sce-cdc-test.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '代理商平台',
|
||||
url: `${protocol}//sce-cac-test.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
];
|
||||
break;
|
||||
case 'sce-ows-pre.380star.com':
|
||||
platforms = [
|
||||
{
|
||||
name: '运营后台',
|
||||
url: `${protocol}//${hostname}${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '开放平台',
|
||||
url: `${protocol}//sce-opc-pre.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '供应商平台',
|
||||
url: `${protocol}//sce-cvc-pre.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '分销商平台',
|
||||
url: `${protocol}//sce-cdc-pre.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '代理商平台',
|
||||
url: `${protocol}//sce-cac-pre.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
];
|
||||
break;
|
||||
case 'sce-ows-demo.380star.com':
|
||||
platforms = [
|
||||
{
|
||||
name: '运营后台',
|
||||
url: `${protocol}//${hostname}${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '开放平台',
|
||||
url: `${protocol}//sce-opc-demo.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '供应商平台',
|
||||
url: `${protocol}//sce-cvc-demo.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '分销商平台',
|
||||
url: `${protocol}//sce-cdc-demo.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '代理商平台',
|
||||
url: `${protocol}//sce-cac-demo.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
];
|
||||
break;
|
||||
case 'sce-ows.380star.com':
|
||||
platforms = [
|
||||
{
|
||||
name: '运营后台',
|
||||
url: `${protocol}//${hostname}${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '开放平台',
|
||||
url: `${protocol}//sce-opc.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '供应商平台',
|
||||
url: `${protocol}//sce-cvc.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '分销商平台',
|
||||
url: `${protocol}//sce-cdc.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
{
|
||||
name: '代理商平台',
|
||||
url: `${protocol}//sce-cac.380star.com${port ? ':' + port : ''}/#/?v=${sc}`,
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
return platforms;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运营商代码
|
||||
* @returns 运营商代码
|
||||
*/
|
||||
getOperatorCode() {
|
||||
return this.coreSrv.tokenSrv.get()?.sc || this.coreSrv.settingSrv.getData('app')?.v || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置信息列表
|
||||
*/
|
||||
// getPlatformConfigurationList(): Observable<Array<any>> {
|
||||
// return this.request(this.$api_get_config, {
|
||||
// pageIndex: 1,
|
||||
// pageSize: 999,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
204
src/app/shared/services/business/user.service.ts
Normal file
204
src/app/shared/services/business/user.service.ts
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: wsm
|
||||
* @Date: 2021-06-22 10:25:33
|
||||
* @LastEditTime: 2021-06-23 20:25:05
|
||||
* @LastEditors: Do not edit
|
||||
* @Reference:
|
||||
*/
|
||||
import { Inject, Injectable, Injector } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { cacheConf } from '@conf/cache.conf';
|
||||
import { eventConf } from '@conf/event.conf';
|
||||
import { sysConf } from '@conf/sys.conf';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { SettingsService } from '@delon/theme';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { BaseService } from '../core/base.service';
|
||||
import { EACacheService } from '../core/cache.service';
|
||||
import { EAEventService } from '../core/event.service';
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAUserService extends BaseService {
|
||||
/**
|
||||
* 账号密码登录
|
||||
*/
|
||||
$api_login_by_account = `/scce/cuc/cuc/user/login?_allow_anonymous=true`;
|
||||
/**
|
||||
* 手机号登录
|
||||
*/
|
||||
$api_login_by_mobile = `/scce/cuc/cuc/user/sms/login?_allow_anonymous=true`;
|
||||
// 登录路径
|
||||
private $api_login = `/scce/cuc/cuc/user/login?_allow_anonymous=true`;
|
||||
private $api_captcha_login = `/scce/cuc/cuc/user/sms/login?_allow_anonymous=true`;
|
||||
private $api_register: any = ``;
|
||||
// 获取协议信息
|
||||
public $api_get_agreement_info = `/scce/pbc/pbc/agreementInfo/getAgreementInfoByType?_allow_anonymous=true`;
|
||||
// 未登录验证身份
|
||||
public $forgetPasswordVerifyIdentity = `/scm/cuc/cuc/userBasicInfo/forgetPassword/verifyIdentity?_allow_anonymous=true`;
|
||||
// 未登录账号发送验证码
|
||||
public $getAccountSMVerificationCode = `/scm/cuc/cuc/userBasicInfo/forgetPassword/getAccountSMVerificationCode?_allow_anonymous=true`;
|
||||
// 未设置密码的用户设置用户密码
|
||||
public $api_set_password = `/scce/cuc/cuc/userBasicInfo/setPassword`;
|
||||
// 凭证修改密码
|
||||
public $voucherUpdatePassword = `/scm/cuc/cuc/userBasicInfo/forgetPassword/voucherUpdatePassword?_allow_anonymous=true`;
|
||||
// 检测用户名是否存在
|
||||
public $api_validate_username_exists = `/tms/cuc/cuc/userBasicInfo/checkUserName?_allow_badcode=true`;
|
||||
// 获取当前用户信息
|
||||
public $api_get_current_user = `/scce/cuc/cuc/user/getUserDetail`;
|
||||
// 校验手机号是否可注册
|
||||
private $api_vaild_register: any = ``;
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
$api_logout = `/scce/cuc/cuc/user/logout`;
|
||||
/**
|
||||
* 根据Token获取用户详情
|
||||
*/
|
||||
$api_get_user_by_token = `/scce/cuc/cuc/user/getUserDetail`;
|
||||
/**
|
||||
* 获取用户菜单
|
||||
*/
|
||||
$api_get_user_menus = `/scce/cuc/cuc/functionInfo/queryUserHaveFunctionsList`;
|
||||
/**
|
||||
* 获取用户角色
|
||||
*/
|
||||
$api_get_user_roles = `/scce/cuc/cuc/roleInfo/getMyRoleList`;
|
||||
// 获取一、二、三级地区详情
|
||||
public $api_getRegionToThree = `/scce/pbc/pbc/region/getRegionToThree?_allow_anonymous=true`;
|
||||
constructor(
|
||||
public injector: Injector,
|
||||
public cacheSrv: EACacheService,
|
||||
public eventSrv: EAEventService,
|
||||
public router: Router,
|
||||
public ar: ActivatedRoute,
|
||||
@Inject(DA_SERVICE_TOKEN) public tokenSrv: ITokenService,
|
||||
private settingSrv: SettingsService,
|
||||
) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录状态
|
||||
*/
|
||||
public get loginStatus(): boolean {
|
||||
try {
|
||||
return !!this.tokenSrv.get()?.token;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 手机号登录
|
||||
* @param mobile 手机号
|
||||
* @param captcha 验证码
|
||||
*/
|
||||
loginByMobile(mobile: string, captcha: string, sc: string) {
|
||||
this.asyncRequest(this.$api_login_by_mobile, { phone: mobile, smsCode: captcha, sc }, 'POST', true, 'FORM').then((res) => {
|
||||
if (res?.token) {
|
||||
this.cacheSrv.set(cacheConf.token, res.token);
|
||||
this.doAfterLogin();
|
||||
this.eventSrv.event.emit(eventConf.reflesh_login_status);
|
||||
this.router.navigate([this.ar.snapshot.queryParams.returnUrl || '/']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 账号密码登录
|
||||
* @param account 账号
|
||||
* @param password 密码
|
||||
*/
|
||||
loginByAccount(account: string, password: string, sc: string) {
|
||||
this.request(this.$api_login_by_account, { username: account, password, sc }, 'POST', true, 'FORM').subscribe((res: any) => {
|
||||
if (res?.token) {
|
||||
this.tokenSrv.set({ token: res.token });
|
||||
this.doAfterLogin();
|
||||
this.eventSrv.event.emit(eventConf.reflesh_login_status, this.ar.snapshot.queryParams.returnUrl || '/');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async doAfterLogin() {
|
||||
await this.loadUserInfo();
|
||||
await this.loadUserMenus();
|
||||
await this.loadUserRoles();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户信息
|
||||
*/
|
||||
async loadUserInfo() {
|
||||
return this.asyncRequest(this.$api_get_user_by_token).then((res) => {
|
||||
this.cacheSrv.set(cacheConf.user, res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户菜单
|
||||
*/
|
||||
async loadUserMenus() {
|
||||
return this.asyncRequest(this.$api_get_user_menus, { appId: sysConf.appId }).then((res) => {
|
||||
this.cacheSrv.set(cacheConf.menu, res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户角色
|
||||
*/
|
||||
loadUserRoles() {}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
logout() {
|
||||
this.settingSrv.setApp({});
|
||||
this.tokenSrv.clear();
|
||||
this.cacheSrv.clear();
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
register(params: any): Observable<boolean> {
|
||||
const obj: any = {};
|
||||
obj.appId = sysConf.appId;
|
||||
obj.masterAccount = 0;
|
||||
if (params.regType === 'account') {
|
||||
obj.account = params.account;
|
||||
obj.loginCipher = params.loginCipher;
|
||||
obj.type = 0;
|
||||
} else if (params.regType === 'phone') {
|
||||
obj.phone = params.phone;
|
||||
obj.smsVerificationCode = params.smsVerificationCode;
|
||||
obj.type = 1;
|
||||
}
|
||||
return this.http.post(this.$api_register, obj).pipe(
|
||||
map((res: any): any => {
|
||||
if (res.success === true) {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验手机号是否可注册
|
||||
*/
|
||||
checkPhoneHasRegister(params: any): Observable<boolean> {
|
||||
const formdata = new FormData();
|
||||
formdata.append('appId', sysConf.appId);
|
||||
formdata.append('phoneNumber', params);
|
||||
return this.http.post(this.$api_vaild_register, formdata).pipe(
|
||||
map((res: any): any => {
|
||||
if (res.success === true) {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
516
src/app/shared/services/core/base.service.ts
Normal file
516
src/app/shared/services/core/base.service.ts
Normal file
@ -0,0 +1,516 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-20 16:38:44
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-06-17 15:06:37
|
||||
* @Description: 全局基础服务:
|
||||
* 所有实体类服务必须使用extends扩展此类
|
||||
* 几个特别的Get参数:
|
||||
* 1. _allow_anonymous=true: 匿名访问,可绕开Http拦截器验证拦截
|
||||
* 使用方法: $api_xxx = `xxx?_allow_anonymous=true`
|
||||
*
|
||||
* 2. _allow_badcode=true: 请求坏状态,可使Http拦截放还完整的HttpResponse
|
||||
* 使用方法: $api_xxx = `xxxx?_allow_badcode=true`
|
||||
*
|
||||
* 3. _custom_header="[{key: string, value: string}]": 自定义Header,
|
||||
* 一些特殊的请求,需要额外附加特别的Header时使用
|
||||
* 使用方法:
|
||||
* const headers = [{ key: xxx, value: xxx},
|
||||
* { key: xxx, value: xxxx}];
|
||||
* this.service.request(
|
||||
* this.encodeUrlHeader(
|
||||
* this.service.$api_xxx, headers))
|
||||
*
|
||||
*/
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { environment } from '@env/environment';
|
||||
import { EnvironmentService } from '@env/environment.service';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
import { NzUploadChangeParam } from 'ng-zorro-antd/upload';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { IBase } from '../../interfaces';
|
||||
import { EAFileUtil } from '../../utils';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BaseService implements IBase {
|
||||
// 新增实例接口地址
|
||||
public $api_add_one!: string;
|
||||
// 新增多个实例地址
|
||||
public $api_add_many!: string;
|
||||
// 修改实例接口地址
|
||||
public $api_edit_one!: string;
|
||||
// 修改多个实例地址
|
||||
public $api_edit_many!: string;
|
||||
// 删除单个实例接口地址
|
||||
public $api_del_one!: string;
|
||||
// 删除多个实例接口地址
|
||||
public $api_del_many!: string;
|
||||
// 获取多个实例接口地址
|
||||
public $api_get_many!: string;
|
||||
// 获取单个实例接口地址
|
||||
public $api_get_one!: string;
|
||||
// 获取分页数据
|
||||
public $api_get_page!: string;
|
||||
// 导出数据接口地址
|
||||
public $api_export!: string;
|
||||
// 导入数据接口地址
|
||||
public $api_import!: string;
|
||||
// 导入数据模板下载地址
|
||||
public $api_import_download_tpl!: string;
|
||||
// 获取字典选项
|
||||
public $api_get_dict!: string;
|
||||
|
||||
constructor(public injector: Injector) {}
|
||||
|
||||
get http(): _HttpClient {
|
||||
return this.injector.get(_HttpClient);
|
||||
}
|
||||
|
||||
get msgSrv(): NzMessageService {
|
||||
return this.injector.get(NzMessageService);
|
||||
}
|
||||
|
||||
get fileUtil(): EAFileUtil {
|
||||
return this.injector.get(EAFileUtil);
|
||||
}
|
||||
|
||||
get envSrv(): EnvironmentService {
|
||||
return this.injector.get(EnvironmentService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步请求
|
||||
* @param parameter 请求参数
|
||||
* @param url 请求路径
|
||||
* @param method 请求方法
|
||||
*/
|
||||
private httpRequest(
|
||||
url: string,
|
||||
parameter: any = {},
|
||||
method: 'POST' | 'GET',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
if (paramType === 'FORM') {
|
||||
parameter = this.getFormData(parameter);
|
||||
}
|
||||
// 判断请求是否需要返回完整请求体
|
||||
const allowBadCode = this.getParams('_allow_badcode', url);
|
||||
return this.http
|
||||
.request(method, url, {
|
||||
body: paramInBody ? parameter : null,
|
||||
params: paramInBody ? null : parameter,
|
||||
})
|
||||
.pipe(
|
||||
map((res: any) => {
|
||||
if (res.success === true) {
|
||||
const data = res?.data;
|
||||
if (allowBadCode) {
|
||||
return res;
|
||||
} else {
|
||||
if (data === undefined || data === null) {
|
||||
return true;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
this.msgSrv.warning(res.msg);
|
||||
return allowBadCode ? res : null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把实体对象转换为FormData
|
||||
*/
|
||||
private getFormData(entity: any) {
|
||||
const formdata = new FormData();
|
||||
for (const key in entity) {
|
||||
if (Object.prototype.hasOwnProperty.call(entity, key)) {
|
||||
formdata.append(key, entity[key]);
|
||||
}
|
||||
}
|
||||
return formdata;
|
||||
}
|
||||
|
||||
request(
|
||||
url: string,
|
||||
parameter: any = {},
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.httpRequest(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncRequest(
|
||||
url: string,
|
||||
parameter: any = {},
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
// 增
|
||||
addOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_add_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncAddOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_add_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.addOne(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
addMany(
|
||||
parameter: any[],
|
||||
url: string = this.$api_add_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncAddMany(
|
||||
parameter: any,
|
||||
url: string = this.$api_add_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.addMany(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
// 删
|
||||
delOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_del_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncDelOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_del_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.delOne(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
delMany(
|
||||
parameter: any[],
|
||||
url: string = this.$api_del_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncDelMany(
|
||||
parameter: any[],
|
||||
url: string = this.$api_del_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.delMany(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
// 改
|
||||
updateOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_edit_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncUpdateOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_edit_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.updateOne(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
updateMany(
|
||||
parameter: any[],
|
||||
url: string = this.$api_edit_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncUpdateMany(
|
||||
parameter: any[],
|
||||
url: string = this.$api_edit_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.updateMany(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个实例
|
||||
*/
|
||||
getOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_get_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any> {
|
||||
return this.request(url, parameter, method, paramInBody, paramType);
|
||||
}
|
||||
|
||||
asyncGetOne(
|
||||
parameter: any,
|
||||
url: string = this.$api_get_one,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any> {
|
||||
return this.getOne(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个实例
|
||||
*/
|
||||
getMany(
|
||||
parameter: any,
|
||||
url: string = this.$api_get_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Observable<any[]> {
|
||||
return this.httpRequest(url, parameter, method, paramInBody, paramType).pipe(
|
||||
map((res) => {
|
||||
return (res as any[]) || [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
asyncGetMany(
|
||||
parameter: any,
|
||||
url: string = this.$api_get_many,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
): Promise<any[]> {
|
||||
return this.getMany(parameter, url, method, paramInBody, paramType).toPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 异步导出文件
|
||||
* 采用this.request相同的传参方式调用
|
||||
* 支持多种传参方法及自定义头(推荐)
|
||||
* @param parameter 条件参数
|
||||
* @param url API地址
|
||||
* @param method Http方法,默认:POST
|
||||
* @param paramInBody 参数是否在body内
|
||||
* @param paramType 参数类型: JSON | FORM , 默认:FORM
|
||||
* @param allowBadCode 允许完整响应代码
|
||||
* @param async 是否同步
|
||||
* @returns Http响应结果
|
||||
*/
|
||||
exportStart(
|
||||
parameter: any,
|
||||
url: string = this.$api_export,
|
||||
method: 'POST' | 'GET' = 'POST',
|
||||
paramInBody: boolean = true,
|
||||
paramType: 'JSON' | 'FORM' = 'JSON',
|
||||
allowBadCode: boolean = true,
|
||||
async: boolean = true,
|
||||
): Observable<any> | Promise<any> {
|
||||
if (allowBadCode) {
|
||||
url += `?_allow_badcode=true`;
|
||||
}
|
||||
const response = this.httpRequest(url, parameter, method, paramInBody, paramType).pipe(
|
||||
tap((res) => {
|
||||
if (res.success) {
|
||||
this.msgSrv.success(`创建下载任务成功,请前往下载任务列表下载您的文件!`);
|
||||
window.open('#/download');
|
||||
}
|
||||
return res;
|
||||
}),
|
||||
);
|
||||
return async ? response.toPromise() : response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出文件
|
||||
* @param body Http body参数
|
||||
*/
|
||||
exportFile(body: any = {}, url: string = this.$api_export): void {
|
||||
this.fileUtil.download(url, body, {}, 'POST');
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步导出文件:
|
||||
* 采用HttpClient POST方式调用
|
||||
* 仅支持POST方法,传参仅支持JSON对象(不推荐)
|
||||
* @deprecated 不建议使用,请使用exportStart方法代替
|
||||
*/
|
||||
asyncExport(params: any, url: string = this.$api_export): Promise<any> {
|
||||
return this.http
|
||||
.post(url, params)
|
||||
.pipe(
|
||||
map((m: any) => {
|
||||
if (m.success) {
|
||||
this.msgSrv.success('创建导出文件任务成功,请前往下载管理中下载您的文件!');
|
||||
window.open('#/download');
|
||||
return m.data;
|
||||
} else {
|
||||
this.msgSrv.warning(m.msg);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接下载文件
|
||||
*/
|
||||
openDownload(url: string, body: any = {}): void {
|
||||
if (url.startsWith('/')) {
|
||||
url = url.substr(1);
|
||||
}
|
||||
let newUrl = `${environment.api}/${url}`;
|
||||
if (body && JSON.stringify(body) !== '{}') {
|
||||
newUrl = newUrl + '?';
|
||||
for (const key in body) {
|
||||
if (Object.prototype.hasOwnProperty.call(body, key)) {
|
||||
newUrl = newUrl + key + '=' + body[key] + '&';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newUrl.endsWith('&')) {
|
||||
newUrl = newUrl.substr(0, newUrl.length - 1);
|
||||
}
|
||||
|
||||
window.open(newUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param url 请求路径
|
||||
* @param body Http body参数
|
||||
* @param params Http params参数
|
||||
*/
|
||||
downloadFile(url: string, body: any = {}, params: any = {}, method: 'POST' | 'GET' = 'POST'): void {
|
||||
this.fileUtil.download(url, body, params, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载模版
|
||||
* @param body Http body参数
|
||||
* @param params Http params参数
|
||||
*/
|
||||
downloadTpl(body: any = {}, params: any = {}): void {
|
||||
this.fileUtil.download(this.$api_import_download_tpl, body, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传状态
|
||||
*/
|
||||
handleUpload({ file, fileList }: NzUploadChangeParam): void {
|
||||
const status = file.status;
|
||||
if (status !== 'uploading') {
|
||||
// console.log(file, fileList);
|
||||
}
|
||||
if (status === 'done') {
|
||||
this.msgSrv.success(`${file.name} 上传成功.`);
|
||||
} else if (status === 'error') {
|
||||
this.msgSrv.error(`${file.name} 上传失败.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典
|
||||
*/
|
||||
getDict(key: string): Observable<any[]> {
|
||||
return this.http.post(this.$api_get_dict, { dict: key }).pipe(
|
||||
map((res) => {
|
||||
if (res.success === true) {
|
||||
return res.data.map((r: any) => {
|
||||
return {
|
||||
label: r.itemValue,
|
||||
value: r.itemKey,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.msgSrv.warning(`获取取字典【${key}】时发生错误:${res.msg || '未知错误!'}`);
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码自定义头
|
||||
* @param url 需要请求的URL
|
||||
* @param customHeader 自定义头集合
|
||||
* @returns 编码后的URL
|
||||
*/
|
||||
encodeUrlHeader(url: string, customHeader: { key: string; value: string }[]): string {
|
||||
if (customHeader && customHeader.length > 0) {
|
||||
let newUrl = '';
|
||||
if (url.includes('?')) {
|
||||
newUrl = url + '&_custom_header=' + encodeURI(JSON.stringify(customHeader));
|
||||
} else {
|
||||
newUrl = url + '?_custom_header=' + encodeURI(JSON.stringify(customHeader));
|
||||
}
|
||||
return newUrl;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL指定参数的值
|
||||
*/
|
||||
private getParams(paramName: string, url: string): string {
|
||||
const paramsIndex = url.indexOf('?');
|
||||
if (paramsIndex > -1) {
|
||||
const paramsStr = url.substr(paramsIndex + 1);
|
||||
const params = paramsStr.split('&');
|
||||
const keyMap = params.find((e) => e.includes(paramName));
|
||||
const value = keyMap ? keyMap.split('=')[1] : '';
|
||||
return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
130
src/app/shared/services/core/cache.service.ts
Normal file
130
src/app/shared/services/core/cache.service.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-22 09:35:02
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-06-18 00:34:01
|
||||
* @Description: 全局缓存服务
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import { cacheConf } from '@conf/cache.conf';
|
||||
import { CacheService } from '@delon/cache';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
import { ICacheObj } from '../../interfaces/core/i-cache-obj';
|
||||
import { EADateUtil } from '../../utils';
|
||||
import { EAEncryptUtil } from '../../utils/encrypt.util';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EACacheService {
|
||||
// 监听最后操作时间值变化
|
||||
private listen$ = new BehaviorSubject<number>(0);
|
||||
private listen = this.listen$.asObservable();
|
||||
constructor(private service: CacheService) {
|
||||
this.listen.pipe(distinctUntilChanged()).subscribe((res) => {
|
||||
this.set(cacheConf.last_operation_time, res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 设置缓存
|
||||
* @param key 缓存键值
|
||||
* @param data 缓存数据
|
||||
* @param vld 有效时间
|
||||
* @param encrypt 是否加密
|
||||
*/
|
||||
set(key: string, data: any, vld: number = 0, encrypt: boolean = false) {
|
||||
// 去除缓存数据两侧空格
|
||||
if (typeof data === 'string') {
|
||||
data = data.trim();
|
||||
}
|
||||
const cahceObj: ICacheObj = {
|
||||
pd: new Date().getTime(),
|
||||
data,
|
||||
vld,
|
||||
encrypt,
|
||||
};
|
||||
if (encrypt) {
|
||||
// 加密
|
||||
cahceObj.data = EAEncryptUtil.encryptByEnAES(JSON.stringify(data));
|
||||
}
|
||||
this.service.set(key, cahceObj);
|
||||
// 更新系统最后操作时间
|
||||
this.refleshLastOperationTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取缓存
|
||||
* @param key 缓存键名
|
||||
*/
|
||||
get(key: string) {
|
||||
// 获取缓存对象
|
||||
const cacheObj = this.service.getNone<any>(key);
|
||||
// 判断是否存在
|
||||
if (!cacheObj) {
|
||||
// 更新系统最后操作时间
|
||||
this.refleshLastOperationTime();
|
||||
return null;
|
||||
}
|
||||
// 判断有效期
|
||||
if (cacheObj.vld > 0) {
|
||||
// 获取系统最后操作时间
|
||||
const last_operation_time: number = this.getLastOperationTime();
|
||||
const start = cacheObj.pd > last_operation_time ? cacheObj.pd : last_operation_time;
|
||||
const vld: Date = EADateUtil.timestampToDate(start + cacheObj.vld);
|
||||
const expired = EADateUtil.dateDiff(vld, new Date()) <= 0;
|
||||
if (expired) {
|
||||
// 已过期,清除缓存
|
||||
this.remove(key);
|
||||
// 更新系统最后操作时间
|
||||
this.refleshLastOperationTime();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否加密
|
||||
if (cacheObj.encrypt) {
|
||||
const encryptJson = EAEncryptUtil.deencryptByDeAES(cacheObj.data);
|
||||
// 更新系统最后操作时间
|
||||
this.refleshLastOperationTime();
|
||||
return JSON.parse(encryptJson);
|
||||
}
|
||||
|
||||
// 更新系统最后操作时间
|
||||
this.refleshLastOperationTime();
|
||||
return cacheObj.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 移除缓存
|
||||
* @param key 缓存键名
|
||||
*/
|
||||
remove(key: string) {
|
||||
this.service.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 清除所有缓存
|
||||
*/
|
||||
clear(): void {
|
||||
this.service.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新系统最后操作时间
|
||||
*/
|
||||
refleshLastOperationTime() {
|
||||
this.listen$.next(new Date().getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统最后操作时间
|
||||
*/
|
||||
private getLastOperationTime(): number {
|
||||
// 获取缓存对象
|
||||
const cacheObj: ICacheObj = this.service.getNone<ICacheObj>(cacheConf.last_operation_time);
|
||||
return cacheObj?.data || 0;
|
||||
}
|
||||
}
|
||||
19
src/app/shared/services/core/event.service.ts
Normal file
19
src/app/shared/services/core/event.service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-22 09:35:02
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-06-11 17:00:07
|
||||
* @Description: 全局事件监听服务
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as EventEmitter from 'eventemitter3';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAEventService {
|
||||
event: EventEmitter;
|
||||
constructor() {
|
||||
this.event = new EventEmitter();
|
||||
}
|
||||
}
|
||||
104
src/app/shared/services/core/validate.service.ts
Normal file
104
src/app/shared/services/core/validate.service.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-03-22 11:42:26
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-03-29 14:47:54
|
||||
* @Description: 全局验证服务
|
||||
*/
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { BaseService } from '../core/base.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAValidateService extends BaseService {
|
||||
constructor(public injector: Injector) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过正则表达式验证字符串
|
||||
* @param value 待验证的字符串
|
||||
* @param partern 正则表达式
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validate(value: string, partern: RegExp): boolean {
|
||||
const reg = new RegExp(partern);
|
||||
return reg.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号码
|
||||
* @param value 待验证的值
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validateChineseCharacters(value: string, star: number): boolean {
|
||||
const express = /^[\u4e00-\u9fa5]{2, 4}$/;
|
||||
return this.validate(value, express);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号码
|
||||
* @param value 待验证的值
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validateMobile(value: string): boolean {
|
||||
const express = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
|
||||
return this.validate(value, express);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证固定电话
|
||||
* @param value 待验证的值
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validateTel(value: string): boolean {
|
||||
const express = /^(0\d{2,3})-?(\d{7,8})$/;
|
||||
return this.validate(value, express);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证身份证号码
|
||||
* @param value 待验证的值
|
||||
* @param mode 匹配模式:indistinct - 模糊匹配 | accurate - 精准匹配(默认值:indistinct)
|
||||
* @param length 号码长度 (默认值:18)
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validateIDCard(value: string, mode: 'indistinct' | 'accurate' = 'indistinct', length: 15 | 18 = 18): boolean {
|
||||
// 模糊校验
|
||||
let express = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
|
||||
// 精准校验18位身份证号码
|
||||
const express1 = /^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
|
||||
// 精准校验15位身份证号码
|
||||
const express2 = /^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx]$/;
|
||||
|
||||
if (mode === 'accurate') {
|
||||
if (length === 18) {
|
||||
express = express1;
|
||||
} else if (length === 15) {
|
||||
express = express2;
|
||||
}
|
||||
}
|
||||
return this.validate(value, express);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮政编码
|
||||
* @param value 待验证的值
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validatePostalCode(value: string): boolean {
|
||||
const express = /^[1-9]\d{5}$/;
|
||||
return this.validate(value, express);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱地址
|
||||
* @param value 待验证的值
|
||||
* @returns 验证结果
|
||||
*/
|
||||
validateEmail(value: string): boolean {
|
||||
const express = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
|
||||
return this.validate(value, express);
|
||||
}
|
||||
}
|
||||
12
src/app/shared/services/index.ts
Normal file
12
src/app/shared/services/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// Core
|
||||
export * from './core/base.service';
|
||||
export * from './core/cache.service';
|
||||
export * from './core/validate.service';
|
||||
|
||||
// Bussiness
|
||||
export * from './business/account.service';
|
||||
export * from './business/authorization.service';
|
||||
export * from './business/enterprise.service';
|
||||
export * from './business/captcha.service';
|
||||
export * from './business/user.service';
|
||||
export * from './business/sl-platform.service';
|
||||
6
src/app/shared/shared-delon.module.ts
Normal file
6
src/app/shared/shared-delon.module.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { PageHeaderModule } from '@delon/abc/page-header';
|
||||
import { SEModule } from '@delon/abc/se';
|
||||
import { STModule } from '@delon/abc/st';
|
||||
import { SVModule } from '@delon/abc/sv';
|
||||
|
||||
export const SHARED_DELON_MODULES = [PageHeaderModule, STModule, SVModule, SEModule];
|
||||
27
src/app/shared/shared-zorro.module.ts
Normal file
27
src/app/shared/shared-zorro.module.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { NzAlertModule } from 'ng-zorro-antd/alert';
|
||||
import { NzBadgeModule } from 'ng-zorro-antd/badge';
|
||||
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||
import { NzCardModule } from 'ng-zorro-antd/card';
|
||||
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
|
||||
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
|
||||
import { NzFormModule } from 'ng-zorro-antd/form';
|
||||
import { NzGridModule } from 'ng-zorro-antd/grid';
|
||||
import { NzIconModule } from 'ng-zorro-antd/icon';
|
||||
import { NzInputModule } from 'ng-zorro-antd/input';
|
||||
import { NzSelectModule } from 'ng-zorro-antd/select';
|
||||
import { NzTableModule } from 'ng-zorro-antd/table';
|
||||
|
||||
export const SHARED_ZORRO_MODULES = [
|
||||
NzButtonModule,
|
||||
NzGridModule,
|
||||
NzTableModule,
|
||||
NzFormModule,
|
||||
NzInputModule,
|
||||
NzBadgeModule,
|
||||
NzAlertModule,
|
||||
NzDropDownModule,
|
||||
NzSelectModule,
|
||||
NzDatePickerModule,
|
||||
NzCardModule,
|
||||
NzIconModule
|
||||
];
|
||||
73
src/app/shared/shared.module.ts
Normal file
73
src/app/shared/shared.module.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/* eslint-disable import/order */
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { DelonACLModule } from '@delon/acl';
|
||||
import { DelonFormModule } from '@delon/form';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
|
||||
import { SHARED_DELON_MODULES } from './shared-delon.module';
|
||||
import { SHARED_ZORRO_MODULES } from './shared-zorro.module';
|
||||
|
||||
// #region third libs
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
const THIRDMODULES = [DragDropModule];
|
||||
// #endregion
|
||||
|
||||
// #region your componets & directives
|
||||
import { PRO_SHARED_MODULES } from '../layout/pro';
|
||||
import { AddressModule } from './components/address';
|
||||
import { DelayModule } from './components/delay';
|
||||
import { EditorModule } from './components/editor';
|
||||
import { FileManagerModule } from './components/file-manager';
|
||||
import { MasonryModule } from './components/masonry';
|
||||
import { MouseFocusModule } from './components/mouse-focus';
|
||||
import { ScrollbarModule } from './components/scrollbar';
|
||||
import { StatusLabelModule } from './components/status-label';
|
||||
|
||||
const MODULES = [
|
||||
AddressModule,
|
||||
DelayModule,
|
||||
EditorModule,
|
||||
FileManagerModule,
|
||||
MasonryModule,
|
||||
MouseFocusModule,
|
||||
ScrollbarModule,
|
||||
StatusLabelModule,
|
||||
...PRO_SHARED_MODULES
|
||||
];
|
||||
// #endregion
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
AlainThemeModule.forChild(),
|
||||
DelonACLModule,
|
||||
DelonFormModule,
|
||||
...SHARED_DELON_MODULES,
|
||||
...SHARED_ZORRO_MODULES,
|
||||
...MODULES,
|
||||
// third libs
|
||||
...THIRDMODULES
|
||||
],
|
||||
exports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
AlainThemeModule,
|
||||
DelonACLModule,
|
||||
DelonFormModule,
|
||||
// i18n
|
||||
...SHARED_DELON_MODULES,
|
||||
...SHARED_ZORRO_MODULES,
|
||||
...MODULES,
|
||||
// third libs
|
||||
...THIRDMODULES
|
||||
]
|
||||
})
|
||||
export class SharedModule {}
|
||||
56
src/app/shared/utils/date.util.ts
Normal file
56
src/app/shared/utils/date.util.ts
Normal file
@ -0,0 +1,56 @@
|
||||
export class EADateUtil {
|
||||
/**
|
||||
* @description 时间戳转换日期
|
||||
* @param timestamp 时间戳
|
||||
* @returns 时间戳对应的日期
|
||||
*/
|
||||
static timestampToDate(timestamp: number): Date {
|
||||
let date;
|
||||
// 时间戳为10位需*1000,时间戳为13位的话不需乘1000
|
||||
if (timestamp.toString().length === 10) {
|
||||
date = new Date(timestamp * 1000);
|
||||
} else {
|
||||
date = new Date(timestamp);
|
||||
}
|
||||
|
||||
const Y = date.getFullYear();
|
||||
const M = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
|
||||
const D = date.getDate();
|
||||
const h = date.getHours();
|
||||
const m = date.getMinutes();
|
||||
const s = date.getSeconds();
|
||||
|
||||
const dateStr = `${Y}-${M}-${D} ${h}:${m}:${s}`;
|
||||
return new Date(dateStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 日期相加
|
||||
* @param start 开始日期
|
||||
* @param intervalMS 间隔秒数(毫秒)
|
||||
* @returns 相加后的日期
|
||||
*/
|
||||
static dateAdd(start: Date, intervalMS: number): Date {
|
||||
return this.timestampToDate(start.getTime() + intervalMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 日期相减
|
||||
* @param start 开始日期
|
||||
* @param intervalMS 时间间隔(毫秒)
|
||||
* @returns 相减后的日期
|
||||
*/
|
||||
static dateMinus(start: Date, intervalMS: number): Date {
|
||||
return this.timestampToDate(start.getTime() - intervalMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 计算两个日期的时间差
|
||||
* @param start 开始日期
|
||||
* @param end 结束日期
|
||||
* @returns 两时间相差的毫秒数
|
||||
*/
|
||||
static dateDiff(start: Date, end: Date): number {
|
||||
return start.getTime() - end.getTime();
|
||||
}
|
||||
}
|
||||
76
src/app/shared/utils/file.util.ts
Normal file
76
src/app/shared/utils/file.util.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import * as FileSaver from 'file-saver';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EAFileUtil {
|
||||
constructor(private modal: NzModalService, private http: _HttpClient) {}
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
* @param content 需要保存的内容
|
||||
* @param filename 文件名
|
||||
* @param confirm 显示确认框
|
||||
*/
|
||||
save(content: any, filename: string, confirm: boolean = true): void {
|
||||
const blob = new Blob([JSON.stringify(content)], { type: 'text/plain;charset=utf-8' });
|
||||
if (confirm === true) {
|
||||
this.modal.confirm({
|
||||
nzTitle: '<i>文件下载确认</i>',
|
||||
nzContent: `<b>是否下载文件:<u><i>${filename}</i></u> ?</b>`,
|
||||
nzOnOk: () => {
|
||||
FileSaver.saveAs(blob, filename);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
FileSaver.saveAs(blob, filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url 接口地址
|
||||
* @param body post请求参数
|
||||
* @param params get请求参数
|
||||
* @method 请求方法
|
||||
*/
|
||||
download(url: string, body: any, params: any, method: 'POST' | 'GET' = 'POST'): void {
|
||||
if (method === 'POST') {
|
||||
this.http
|
||||
.post(url, body, params, {
|
||||
responseType: 'blob',
|
||||
observe: 'response',
|
||||
})
|
||||
.subscribe((res) => {
|
||||
this.saveBlob(res);
|
||||
});
|
||||
} else if (method === 'GET') {
|
||||
this.http
|
||||
.get(url, params, {
|
||||
responseType: 'blob',
|
||||
observe: 'response',
|
||||
})
|
||||
.subscribe((res) => {
|
||||
this.saveBlob(res);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建blob对象,并利用浏览器打开url进行下载
|
||||
* @param data 文件流数据
|
||||
*/
|
||||
private saveBlob(res: any): void {
|
||||
const disp = res.headers.get('Content-Disposition');
|
||||
const blob = new Blob([res.body], { type: 'text/plain;charset=utf-8' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const fileName = disp.split(';')[1].split('=')[1];
|
||||
a.href = url;
|
||||
a.download = decodeURI(fileName);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
4
src/app/shared/utils/index.ts
Normal file
4
src/app/shared/utils/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './date.util';
|
||||
export * from './file.util';
|
||||
export * from './processSingleSort.func';
|
||||
export * from './yuan';
|
||||
38
src/app/shared/utils/processSingleSort.func.ts
Normal file
38
src/app/shared/utils/processSingleSort.func.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* @Author: Maple
|
||||
* @Date: 2021-02-27 17:40:54
|
||||
* @LastEditors: Do not edit
|
||||
* @LastEditTime: 2021-03-23 20:58:18
|
||||
* @Description: 处理ST表格的单列排序
|
||||
*/
|
||||
import { STRequestOptions } from '@delon/abc/st';
|
||||
|
||||
/**
|
||||
* 处理ST表格单条件排序
|
||||
* @param reqOtions ST的Req
|
||||
* @param sortField 排序字段,默认为:sortType
|
||||
* @returns ST的Req
|
||||
*/
|
||||
export function processSingleSort(reqOtions: STRequestOptions, sortField: string = 'sort'): STRequestOptions {
|
||||
if (!reqOtions.body) {
|
||||
return reqOtions;
|
||||
}
|
||||
// 获取排序字段
|
||||
const body: any = {};
|
||||
for (const _key of Object.keys(reqOtions.body)) {
|
||||
if (typeof reqOtions.body[_key] === 'string') {
|
||||
const value: string = reqOtions.body[_key]?.trim();
|
||||
if (value && (value === 'ascend' || value === 'descend')) {
|
||||
body[sortField] = `${_key}.${value}`;
|
||||
} else {
|
||||
body[_key] = reqOtions.body[_key];
|
||||
}
|
||||
} else {
|
||||
body[_key] = reqOtions.body[_key];
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(body) !== '{}') {
|
||||
reqOtions.body = body;
|
||||
}
|
||||
return reqOtions;
|
||||
}
|
||||
11
src/app/shared/utils/yuan.ts
Normal file
11
src/app/shared/utils/yuan.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 转化成RMB元字符串
|
||||
*
|
||||
* @param digits 当数字类型时,允许指定小数点后数字的个数,默认2位小数
|
||||
*/
|
||||
export function yuan(value: number | string, digits: number = 2): string {
|
||||
if (typeof value === 'number') {
|
||||
value = value.toFixed(digits);
|
||||
}
|
||||
return `¥ ${value}`;
|
||||
}
|
||||
20
src/app/shared/widget/address/address.widget.ts
Normal file
20
src/app/shared/widget/address/address.widget.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
|
||||
@Component({
|
||||
selector: 'sf-address',
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<address [ngModel]="value" name="sf.address" (ngModelChange)="_change($event)"></address>
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
// tslint:disable-next-line: component-class-suffix
|
||||
export class AddressWidget extends ControlWidget {
|
||||
static readonly KEY = 'address';
|
||||
|
||||
_change(value: string): void {
|
||||
this.setValue(value);
|
||||
}
|
||||
}
|
||||
23
src/app/shared/widget/editor/editor.widget.ts
Normal file
23
src/app/shared/widget/editor/editor.widget.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
|
||||
@Component({
|
||||
selector: 'sf-editor',
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<editor [ngModel]="value" name="sf.editor" (ngModelChange)="_change($event)"></editor>
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
// tslint:disable-next-line: component-class-suffix
|
||||
export class EditorWidget extends ControlWidget {
|
||||
static readonly KEY = 'editor';
|
||||
|
||||
_change(value: string): void {
|
||||
this.setValue(value);
|
||||
if (this.ui.contentChanged) {
|
||||
this.ui.contentChanged(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<ng-container *ngIf="type === 'date'">
|
||||
<div style="display: flex">
|
||||
<nz-date-picker
|
||||
[nzMode]="mode"
|
||||
[nzFormat]="displayFormat"
|
||||
[(ngModel)]="startDate"
|
||||
(ngModelChange)="onChange($event)"
|
||||
nzPlaceHolder="开始时间"
|
||||
></nz-date-picker>
|
||||
<span style="padding: 4px">-</span>
|
||||
<nz-date-picker
|
||||
[nzMode]="mode"
|
||||
[nzFormat]="displayFormat"
|
||||
[(ngModel)]="endDate"
|
||||
(ngModelChange)="onChange($event)"
|
||||
nzPlaceHolder="结束时间"
|
||||
></nz-date-picker>
|
||||
</div>
|
||||
</ng-container>
|
||||
</sf-item-wrap>
|
||||
@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SLFromToSearchWidget } from './from-to-search.widget';
|
||||
|
||||
describe('SLFromToSearchWidget', () => {
|
||||
let component: SLFromToSearchWidget;
|
||||
let fixture: ComponentFixture<SLFromToSearchWidget>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SLFromToSearchWidget],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SLFromToSearchWidget);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
104
src/app/shared/widget/from-to-search/from-to-search.widget.ts
Normal file
104
src/app/shared/widget/from-to-search/from-to-search.widget.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* @Descripttion: Do not edit
|
||||
* @version: 1.0
|
||||
* @Author: Eclipse
|
||||
* @Date: 2021-05-08 15:57:42
|
||||
* @LastEditors: Eclipse
|
||||
* @LastEditTime: 2021-05-08 16:05:26
|
||||
*/
|
||||
import { ChangeDetectorRef, Component, Injector, OnInit } from '@angular/core';
|
||||
import { ControlWidget, SFComponent, SFItemComponent } from '@delon/form';
|
||||
import { format, getISOWeek } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
selector: 'sl-from-to-search',
|
||||
templateUrl: './from-to-search.widget.html',
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class SLFromToSearchWidget extends ControlWidget implements OnInit {
|
||||
static readonly KEY = 'sl-from-to-search';
|
||||
/**
|
||||
* 组件类型
|
||||
*/
|
||||
type: 'date' | 'number' = 'date';
|
||||
/**
|
||||
* 控件模式
|
||||
*/
|
||||
mode: 'date' | 'week' | 'month' | 'year' = 'date';
|
||||
/**
|
||||
* 组件返回值
|
||||
*/
|
||||
data: { start: any; end: any } | null = null;
|
||||
/**
|
||||
* 组件日期显示格式
|
||||
*/
|
||||
displayFormat = 'yyyy-MM-dd';
|
||||
/**
|
||||
* 组件日期格式
|
||||
*/
|
||||
format = 'yyyy-MM-dd';
|
||||
/**
|
||||
* 最小值
|
||||
*/
|
||||
min: any;
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
max: any;
|
||||
/**
|
||||
* 日期区间
|
||||
*/
|
||||
date: Array<any> = [];
|
||||
/**
|
||||
* 开始日期
|
||||
*/
|
||||
startDate: any;
|
||||
/**
|
||||
* 结束日期
|
||||
*/
|
||||
endDate: any;
|
||||
|
||||
constructor(cd: ChangeDetectorRef, injector: Injector, sfItemComp?: SFItemComponent, sfComp?: SFComponent) {
|
||||
super(cd, injector, sfItemComp, sfComp);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.type = this.ui?.type || 'date';
|
||||
this.mode = this.ui?.mode || 'date';
|
||||
this.displayFormat = this.ui?.displayFormat;
|
||||
this.format = this.ui?.format || 'yyyy-MM-dd';
|
||||
}
|
||||
onChange(result: any): void {
|
||||
if (this.startDate && this.endDate) {
|
||||
const date1 = new Date(this.startDate);
|
||||
const date2 = new Date(this.endDate);
|
||||
if (date1.getTime() > date2.getTime()) {
|
||||
const date3 = this.endDate;
|
||||
this.endDate = this.startDate;
|
||||
this.startDate = date3;
|
||||
}
|
||||
}
|
||||
if (this.startDate) {
|
||||
this.date[0] = format(this.startDate, this.format);
|
||||
} else {
|
||||
this.date[0] = '';
|
||||
}
|
||||
if (this.endDate) {
|
||||
this.date[1] = format(this.endDate, this.format);
|
||||
} else {
|
||||
this.date[1] = '';
|
||||
}
|
||||
|
||||
if (this.type === 'date') {
|
||||
this.setValue(this.date);
|
||||
} else if (this.type === 'number') {
|
||||
} else {
|
||||
this.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
reset(value: string) {
|
||||
this.startDate = null;
|
||||
this.endDate = null;
|
||||
}
|
||||
}
|
||||
11
src/app/shared/widget/from-to/from-to.widget.html
Normal file
11
src/app/shared/widget/from-to/from-to.widget.html
Normal file
@ -0,0 +1,11 @@
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<ng-container *ngIf="type === 'date'">
|
||||
<nz-range-picker
|
||||
[(ngModel)]="date"
|
||||
[nzMode]="mode"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[nzFormat]="displayFormat"
|
||||
[nzShowTime]="displayFormat === 'yyyy-MM-dd HH:mm:ss'"
|
||||
></nz-range-picker>
|
||||
</ng-container>
|
||||
</sf-item-wrap>
|
||||
0
src/app/shared/widget/from-to/from-to.widget.less
Normal file
0
src/app/shared/widget/from-to/from-to.widget.less
Normal file
24
src/app/shared/widget/from-to/from-to.widget.spec.ts
Normal file
24
src/app/shared/widget/from-to/from-to.widget.spec.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EAFromToWidget } from './from-to.widget';
|
||||
|
||||
describe('EAFromToWidget', () => {
|
||||
let component: EAFromToWidget;
|
||||
let fixture: ComponentFixture<EAFromToWidget>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [EAFromToWidget],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EAFromToWidget);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
85
src/app/shared/widget/from-to/from-to.widget.ts
Normal file
85
src/app/shared/widget/from-to/from-to.widget.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { ChangeDetectorRef, Component, Injector, OnInit } from '@angular/core';
|
||||
import { ControlWidget, SFComponent, SFItemComponent } from '@delon/form';
|
||||
import { format, getISOWeek } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
selector: 'sl-from-to',
|
||||
templateUrl: './from-to.widget.html',
|
||||
styleUrls: ['./from-to.widget.less'],
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class EAFromToWidget extends ControlWidget implements OnInit {
|
||||
static readonly KEY = 'sl-from-to';
|
||||
/**
|
||||
* 组件类型
|
||||
*/
|
||||
type: 'date' | 'number' = 'date';
|
||||
/**
|
||||
* 控件模式
|
||||
*/
|
||||
mode: 'date' | 'week' | 'month' | 'year' = 'date';
|
||||
/**
|
||||
* 组件返回值
|
||||
*/
|
||||
data: { start: any; end: any } | null = null;
|
||||
/**
|
||||
* 组件日期显示格式
|
||||
*/
|
||||
displayFormat = 'yyyy-MM-dd';
|
||||
/**
|
||||
* 组件日期格式
|
||||
*/
|
||||
format = 'yyyy-MM-dd';
|
||||
/**
|
||||
* 最小值
|
||||
*/
|
||||
min: any;
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
max: any;
|
||||
/**
|
||||
* 日期区间
|
||||
*/
|
||||
date: any;
|
||||
|
||||
constructor(cd: ChangeDetectorRef, injector: Injector, sfItemComp?: SFItemComponent, sfComp?: SFComponent) {
|
||||
super(cd, injector, sfItemComp, sfComp);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.data = this.formProperty?.formData as { start: any; end: any };
|
||||
this.date = this.initData(this.data);
|
||||
this.type = this.ui?.type || 'date';
|
||||
this.mode = this.ui?.mode || 'date';
|
||||
this.displayFormat = this.ui?.displayFormat;
|
||||
this.format = this.ui?.format || 'yyyy-MM-dd';
|
||||
}
|
||||
|
||||
initData(data: { start: any; end: any }): Date[] | null {
|
||||
return data ? [new Date(this.data?.start), new Date(this.data?.end)] : null;
|
||||
}
|
||||
|
||||
onChange(result: Array<any>): void {
|
||||
console.log('onChange: ', result);
|
||||
|
||||
if (this.type === 'date') {
|
||||
if (JSON.stringify(result) !== '[]') {
|
||||
this.setValue({ start: format(result[0], this.format), end: format(result[1], this.format) });
|
||||
} else {
|
||||
this.setValue(null);
|
||||
}
|
||||
} else if (this.type === 'number') {
|
||||
} else {
|
||||
this.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
getWeek(result: Date[]): void {
|
||||
console.log('week: ', result.map(getISOWeek));
|
||||
}
|
||||
|
||||
_change(value: string): void {
|
||||
this.setValue(value);
|
||||
}
|
||||
}
|
||||
63
src/app/shared/widget/img/img.widget.ts
Normal file
63
src/app/shared/widget/img/img.widget.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
|
||||
@Component({
|
||||
selector: 'sf-img',
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<div class="d-flex align-items-center">
|
||||
<ng-container *ngIf="result.length > 0">
|
||||
<img *ngFor="let i of result" src="{{ i.mp }}" height="64" width="64" class="mr-sm" />
|
||||
</ng-container>
|
||||
<button nz-button type="button" nzType="primary" nzSize="small" dialog-img [multiple]="ui.multiple" (selected)="_change($event)">
|
||||
选择
|
||||
</button>
|
||||
<button *ngIf="result.length > 0" class="ml-sm" nz-button type="button" nzSize="small" (click)="_clean()">删除</button>
|
||||
</div>
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
// tslint:disable-next-line: component-class-suffix
|
||||
export class ImgWidget extends ControlWidget {
|
||||
static readonly KEY = 'img';
|
||||
result: any[] = [];
|
||||
|
||||
private notify(value: any): void {
|
||||
const { selected } = this.ui;
|
||||
this.setValue(value);
|
||||
if (selected) {
|
||||
selected(value);
|
||||
}
|
||||
}
|
||||
|
||||
reset(value: any): void {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let res = Array.isArray(value) ? value : [value];
|
||||
if (res.length > 0 && typeof res[0] === 'string') {
|
||||
res = res.map((mp) => mp);
|
||||
}
|
||||
this.result = res;
|
||||
}
|
||||
|
||||
_change(list: any): void {
|
||||
const { multiple, field } = this.ui;
|
||||
if (!Array.isArray(list)) {
|
||||
list = [list];
|
||||
}
|
||||
this.result = list;
|
||||
// get fields
|
||||
list = (list as any[]).map((item) => (field ? item[field] : item)).filter((item) => !!item);
|
||||
const value = list.length > 0 ? (multiple === true ? list : list[0]) : null;
|
||||
|
||||
this.notify(value);
|
||||
}
|
||||
|
||||
_clean(): void {
|
||||
this.result.length = 0;
|
||||
this.notify(null);
|
||||
}
|
||||
}
|
||||
77
src/app/shared/widget/noun/noun.widget.ts
Normal file
77
src/app/shared/widget/noun/noun.widget.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
|
||||
@Component({
|
||||
selector: 'sl-noun',
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<nz-input-group nzCompact>
|
||||
<nz-input-number
|
||||
nzPlaceHolder="从"
|
||||
class="number-input"
|
||||
[nzMin]="ui.min || 0"
|
||||
[nzMax]="ui.max || 100000"
|
||||
[nzStep]="ui.step || 1"
|
||||
[ngModel]="min"
|
||||
(ngModelChange)="_change($event, 0)"
|
||||
></nz-input-number>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
nz-input
|
||||
placeholder="~"
|
||||
style="width: 30px; text-align: center; pointer-events: none; background-color: rgb(255, 255, 255);"
|
||||
/>
|
||||
<nz-input-number
|
||||
nzPlaceHolder="到"
|
||||
class="number-input"
|
||||
[nzMin]="ui.min || 0"
|
||||
[nzMax]="ui.max || 100000"
|
||||
[nzStep]="ui.step || 1"
|
||||
[ngModel]="max"
|
||||
(ngModelChange)="_change($event, 1)"
|
||||
></nz-input-number>
|
||||
</nz-input-group>
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
styles: [
|
||||
`
|
||||
.number-input {
|
||||
width: calc(50% - 15px);
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
|
||||
// tslint:disable-next-line: component-class-suffix
|
||||
export class NounWidget extends ControlWidget {
|
||||
static readonly KEY = 'noun';
|
||||
min!: number | null;
|
||||
max!: number | null;
|
||||
val: any[] = [];
|
||||
|
||||
_change(value: any, index: number) {
|
||||
if (value.toString().trim() === '') {
|
||||
value = null;
|
||||
}
|
||||
if (index === 0) {
|
||||
this.min = value;
|
||||
}
|
||||
if (index === 1) {
|
||||
this.max = value;
|
||||
}
|
||||
|
||||
this.val[index] = value;
|
||||
if (this.val.length === 2 && this.val[0] !== null && this.val[1] !== null) {
|
||||
this.setValue(this.val);
|
||||
} else {
|
||||
this.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
reset(value: string) {
|
||||
this.min = null;
|
||||
this.max = null;
|
||||
}
|
||||
}
|
||||
184
src/app/shared/widget/property-values/property-values.widget.ts
Normal file
184
src/app/shared/widget/property-values/property-values.widget.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
export class PV {
|
||||
_$id?: any;
|
||||
id?: string;
|
||||
propertyId?: string;
|
||||
sortId?: number;
|
||||
stateShow?: number;
|
||||
storeId?: number;
|
||||
value?: string;
|
||||
status?: number;
|
||||
}
|
||||
@Component({
|
||||
selector: 'sl-property-values',
|
||||
styles: [
|
||||
`
|
||||
::ng-deep .cdk-drag-preview {
|
||||
display: table;
|
||||
}
|
||||
|
||||
::ng-deep .cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<button nz-button nzType="primary" (click)="addRow()">
|
||||
<i nz-icon nzType="plus" nzTheme="outline"></i>
|
||||
新增属性值
|
||||
</button>
|
||||
<nz-table
|
||||
#editRowTable
|
||||
nzBordered
|
||||
nzSize="small"
|
||||
[nzData]="listOfData"
|
||||
nzTableLayout="fixed"
|
||||
[nzFrontPagination]="false"
|
||||
[nzShowPagination]="false"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th nzWidth="70px">排序</th>
|
||||
<th nzWidth="200px">属性值</th>
|
||||
<!-- <th nzWidth="200px">状态</th> -->
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<tr *ngFor="let data of editRowTable.data" cdkDrag>
|
||||
<ng-container *ngIf="!editCache[data._$id]?.edit; else editTemplate">
|
||||
<td>{{ data.sortId }}</td>
|
||||
<td>{{ data.value }}</td>
|
||||
<!-- <td>
|
||||
<span *ngIf="data.stateShow == 0 || data.status == 0">隐藏</span><span *ngIf="data.stateShow == 1 || data.status == 1">显示</span>
|
||||
</td> -->
|
||||
<td>
|
||||
<a (click)="startEdit(data._$id)">编辑</a>
|
||||
<nz-divider nzType="vertical"></nz-divider>
|
||||
<a (click)="deleteRow(data._$id)" class="text-danger">删除</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #editTemplate>
|
||||
<td>
|
||||
<input type="text" nz-input [(ngModel)]="editCache[data._$id].data.sortId" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" nz-input [(ngModel)]="editCache[data._$id].data.value" />
|
||||
</td>
|
||||
<!-- <td>
|
||||
<nz-radio-group
|
||||
[(ngModel)]="editCache[data._$id].data.stateShow"
|
||||
(ngModelChange)="changeRadio($event, editCache[data._$id].data)"
|
||||
nzButtonStyle="solid"
|
||||
nzSize="small"
|
||||
>
|
||||
<label nz-radio-button [nzValue]="1">显示</label>
|
||||
<label nz-radio-button [nzValue]="0">隐藏</label>
|
||||
</nz-radio-group>
|
||||
</td> -->
|
||||
<td>
|
||||
<a (click)="saveEdit(data._$id)" class="save">保存</a>
|
||||
<nz-divider nzType="vertical"></nz-divider>
|
||||
<a (click)="cancelEdit(data._$id)">取消</a>
|
||||
</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</nz-table>
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
|
||||
// tslint:disable-next-line: component-class-suffix
|
||||
export class PropertyValuesWidget extends ControlWidget {
|
||||
static readonly KEY = 'property-values';
|
||||
|
||||
i = 0;
|
||||
editId: string | null = null;
|
||||
|
||||
editCache: { [key: string]: { edit: boolean; data: PV } } = {};
|
||||
listOfData: PV[] = [];
|
||||
|
||||
startEdit(id: string): void {
|
||||
this.editCache[id].edit = true;
|
||||
console.log(this.editCache[id]);
|
||||
// 开始编辑时禁止提交表单
|
||||
this.setValue(null);
|
||||
}
|
||||
|
||||
changeRadio(e: any, item: any) {
|
||||
item.status = e;
|
||||
console.log(e, item);
|
||||
}
|
||||
|
||||
cancelEdit(id: string): void {
|
||||
const index = this.listOfData.findIndex((item) => item._$id === id);
|
||||
this.editCache[id] = {
|
||||
data: { ...this.listOfData[index] },
|
||||
edit: false,
|
||||
};
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
saveEdit(id: string): void {
|
||||
const index = this.listOfData.findIndex((item) => item._$id === id);
|
||||
Object.assign(this.listOfData[index], this.editCache[id].data);
|
||||
this.editCache[id].edit = false;
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
updateEditCache(): void {
|
||||
this.listOfData.forEach((item) => {
|
||||
this.editCache[item._$id] = {
|
||||
edit: false,
|
||||
data: { ...item },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
addRow(): void {
|
||||
this.i++;
|
||||
this.listOfData = [
|
||||
...this.listOfData,
|
||||
{
|
||||
_$id: `${this.i}`,
|
||||
stateShow: 1,
|
||||
},
|
||||
];
|
||||
this.updateEditCache();
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
deleteRow(id: string): void {
|
||||
this.listOfData = this.listOfData.filter((d) => d._$id !== id);
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
_change(value: any, index: number) {}
|
||||
|
||||
reset(value: string) {}
|
||||
afterViewInit() {
|
||||
// 初始化数据
|
||||
this.listOfData = [];
|
||||
const formData: any = this.formProperty?.formData;
|
||||
if (formData) {
|
||||
const data = [...formData];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
this.i = i;
|
||||
this.listOfData.push({
|
||||
_$id: `${this.i}`,
|
||||
...data[i],
|
||||
});
|
||||
}
|
||||
this.updateEditCache();
|
||||
}
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>): void {
|
||||
moveItemInArray(this.listOfData, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
}
|
||||
176
src/app/shared/widget/spec-values/spec-values.widget.ts
Normal file
176
src/app/shared/widget/spec-values/spec-values.widget.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
export class PV {
|
||||
_$id?: any;
|
||||
id?: string;
|
||||
specId?: string;
|
||||
sortId?: number;
|
||||
stateShow?: number;
|
||||
storeId?: number;
|
||||
value?: string;
|
||||
}
|
||||
@Component({
|
||||
selector: 'sl-spec-values',
|
||||
styles: [
|
||||
`
|
||||
::ng-deep .cdk-drag-preview {
|
||||
display: table;
|
||||
}
|
||||
|
||||
::ng-deep .cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<button style="margin-bottom: 20px" nz-button nzType="primary" (click)="addRow()">
|
||||
<i nz-icon nzType="plus" nzTheme="outline"></i>
|
||||
新增规格值
|
||||
</button>
|
||||
|
||||
<nz-table
|
||||
#editRowTable
|
||||
nzBordered
|
||||
nzSize="small"
|
||||
[nzData]="listOfData"
|
||||
nzTableLayout="fixed"
|
||||
[nzFrontPagination]="false"
|
||||
[nzShowPagination]="false"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th nzWidth="70px">排序</th>
|
||||
<th nzWidth="200px">规格值</th>
|
||||
<!-- <th nzWidth="200px">状态</th> -->
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<tr *ngFor="let data of editRowTable.data" cdkDrag>
|
||||
<ng-container *ngIf="!editCache[data._$id]?.edit; else editTemplate">
|
||||
<td>{{ data.sortId }}</td>
|
||||
<td>{{ data.value }}</td>
|
||||
<!-- <td><span *ngIf="data.stateShow == 0">隐藏</span><span *ngIf="data.stateShow == 1">显示</span></td> -->
|
||||
<td>
|
||||
<a (click)="startEdit(data._$id)">编辑</a>
|
||||
<nz-divider nzType="vertical"></nz-divider>
|
||||
<a (click)="deleteRow(data._$id)" class="text-danger">删除</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-template #editTemplate>
|
||||
<td>
|
||||
<input type="text" nz-input [(ngModel)]="editCache[data._$id].data.sortId" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" nz-input [(ngModel)]="editCache[data._$id].data.value" />
|
||||
</td>
|
||||
<!-- <td>
|
||||
<nz-radio-group [(ngModel)]="editCache[data._$id].data.stateShow" (ngModelChange)="changeRadio($event, editCache[data._$id].data)" nzButtonStyle="solid" nzSize="small">
|
||||
<label nz-radio-button [nzValue]="1">显示</label>
|
||||
<label nz-radio-button [nzValue]="0">隐藏</label>
|
||||
</nz-radio-group>
|
||||
</td> -->
|
||||
<td>
|
||||
<a (click)="saveEdit(data._$id)" class="save">保存</a>
|
||||
<nz-divider nzType="vertical"></nz-divider>
|
||||
<a (click)="cancelEdit(data._$id)">取消</a>
|
||||
</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</nz-table>
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
|
||||
// tslint:disable-next-line: component-class-suffix
|
||||
export class SpecValuesWidget extends ControlWidget {
|
||||
static readonly KEY = 'spec-values';
|
||||
|
||||
i = 0;
|
||||
editId: string | null = null;
|
||||
|
||||
editCache: { [key: string]: { edit: boolean; data: PV } } = {};
|
||||
listOfData: PV[] = [];
|
||||
|
||||
startEdit(id: string): void {
|
||||
this.editCache[id].edit = true;
|
||||
// 开始编辑时禁止提交表单
|
||||
this.setValue(null);
|
||||
}
|
||||
|
||||
changeRadio(e: any, item: any) {
|
||||
item.status = e;
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
cancelEdit(id: string): void {
|
||||
const index = this.listOfData.findIndex((item) => item._$id === id);
|
||||
this.editCache[id] = {
|
||||
data: { ...this.listOfData[index] },
|
||||
edit: false,
|
||||
};
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
saveEdit(id: string): void {
|
||||
const index = this.listOfData.findIndex((item) => item._$id === id);
|
||||
Object.assign(this.listOfData[index], this.editCache[id].data);
|
||||
this.editCache[id].edit = false;
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
updateEditCache(): void {
|
||||
this.listOfData.forEach((item) => {
|
||||
this.editCache[item._$id] = {
|
||||
edit: false,
|
||||
data: { ...item },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
addRow(): void {
|
||||
this.i++;
|
||||
this.listOfData = [
|
||||
...this.listOfData,
|
||||
{
|
||||
_$id: `${this.i}`,
|
||||
stateShow: 1,
|
||||
},
|
||||
];
|
||||
this.updateEditCache();
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
deleteRow(id: string): void {
|
||||
this.listOfData = this.listOfData.filter((d) => d._$id !== id);
|
||||
this.setValue(this.listOfData);
|
||||
}
|
||||
|
||||
_change(value: any, index: number) {}
|
||||
|
||||
reset(value: string) {}
|
||||
afterViewInit() {
|
||||
this.listOfData = [];
|
||||
// 初始化数据
|
||||
const formData: any = this.formProperty?.formData;
|
||||
if (formData) {
|
||||
const data = [...formData];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
this.i = i;
|
||||
this.listOfData.push({
|
||||
_$id: `${this.i}`,
|
||||
...data[i],
|
||||
});
|
||||
}
|
||||
this.updateEditCache();
|
||||
}
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>): void {
|
||||
moveItemInArray(this.listOfData, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
}
|
||||
72
src/app/shared/widget/st-widget.module.ts
Normal file
72
src/app/shared/widget/st-widget.module.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { STWidgetRegistry } from '@delon/abc/st';
|
||||
import { DelonFormModule } from '@delon/form';
|
||||
import { AddressModule, EditorModule, FileManagerModule } from '@shared';
|
||||
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
|
||||
import { NzDividerModule } from 'ng-zorro-antd/divider';
|
||||
import { NzInputModule } from 'ng-zorro-antd/input';
|
||||
import { NzInputNumberModule } from 'ng-zorro-antd/input-number';
|
||||
import { NzStepsModule } from 'ng-zorro-antd/steps';
|
||||
import { NgxTinymceModule } from 'ngx-tinymce';
|
||||
|
||||
// import { STWidgetRegistry } from '@delon/abc/st';
|
||||
import { SharedModule } from '../shared.module';
|
||||
import { AddressWidget } from './address/address.widget';
|
||||
import { EditorWidget } from './editor/editor.widget';
|
||||
import { SLFromToSearchWidget } from './from-to-search/from-to-search.widget';
|
||||
import { EAFromToWidget } from './from-to/from-to.widget';
|
||||
import { ImgWidget } from './img/img.widget';
|
||||
import { NounWidget } from './noun/noun.widget';
|
||||
import { PropertyValuesWidget } from './property-values/property-values.widget';
|
||||
import { SpecValuesWidget } from './spec-values/spec-values.widget';
|
||||
import { TinymceWidget } from './tinymce/tinymce.widget';
|
||||
|
||||
export const STWIDGET_COMPONENTS = [
|
||||
EditorWidget,
|
||||
ImgWidget,
|
||||
AddressWidget,
|
||||
TinymceWidget,
|
||||
NounWidget,
|
||||
EAFromToWidget,
|
||||
SpecValuesWidget,
|
||||
PropertyValuesWidget,
|
||||
SLFromToSearchWidget
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: STWIDGET_COMPONENTS,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
DelonFormModule.forRoot(),
|
||||
AddressModule,
|
||||
EditorModule,
|
||||
FileManagerModule,
|
||||
NzDividerModule,
|
||||
NzStepsModule,
|
||||
NzButtonModule,
|
||||
NzInputModule,
|
||||
NzDatePickerModule,
|
||||
NzInputNumberModule,
|
||||
NgxTinymceModule,
|
||||
SharedModule
|
||||
],
|
||||
exports: [...STWIDGET_COMPONENTS],
|
||||
entryComponents: STWIDGET_COMPONENTS
|
||||
})
|
||||
export class STWidgetModule {
|
||||
constructor(widgetRegistry: STWidgetRegistry) {
|
||||
widgetRegistry.register(EditorWidget.KEY, EditorWidget);
|
||||
widgetRegistry.register(ImgWidget.KEY, ImgWidget);
|
||||
widgetRegistry.register(AddressWidget.KEY, AddressWidget);
|
||||
widgetRegistry.register(TinymceWidget.KEY, TinymceWidget);
|
||||
widgetRegistry.register(NounWidget.KEY, NounWidget);
|
||||
widgetRegistry.register(EAFromToWidget.KEY, EAFromToWidget);
|
||||
widgetRegistry.register(SpecValuesWidget.KEY, SpecValuesWidget);
|
||||
widgetRegistry.register(PropertyValuesWidget.KEY, PropertyValuesWidget);
|
||||
widgetRegistry.register(SLFromToSearchWidget.KEY, SLFromToSearchWidget);
|
||||
}
|
||||
}
|
||||
31
src/app/shared/widget/tinymce/tinymce.widget.ts
Normal file
31
src/app/shared/widget/tinymce/tinymce.widget.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
|
||||
@Component({
|
||||
selector: 'sf-tinymce',
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
<tinymce [ngModel]="value" (ngModelChange)="change($event)" [config]="config" [loading]="loading"> </tinymce>
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
// tslint:disable-next-line: component-class-suffix
|
||||
export class TinymceWidget extends ControlWidget implements OnInit {
|
||||
static readonly KEY = 'tinymce';
|
||||
|
||||
config: any = {};
|
||||
loading = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = this.ui.loading || '加载中……';
|
||||
this.config = this.ui.config || {};
|
||||
}
|
||||
|
||||
change(value: string): void {
|
||||
if (this.ui.change) {
|
||||
this.ui.change(value);
|
||||
}
|
||||
this.setValue(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user