项目初始化
This commit is contained in:
17
.browserslistrc
Normal file
17
.browserslistrc
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
last 1 Chrome version
|
||||||
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
|
Firefox ESR
|
||||||
|
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||||
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
34
.eslintignore
Normal file
34
.eslintignore
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
_cli-tpl/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn
|
||||||
126
.eslintrc.js
Normal file
126
.eslintrc.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
const prettierConfig = require('./.prettierrc.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parserOptions: { ecmaVersion: 2021 },
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.ts'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['tsconfig.json'],
|
||||||
|
createDefaultProgram: true
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint', 'jsdoc', 'import'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@angular-eslint/recommended',
|
||||||
|
'plugin:@angular-eslint/template/process-inline-templates',
|
||||||
|
'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': ['error', prettierConfig],
|
||||||
|
'jsdoc/newline-after-description': 1,
|
||||||
|
'@angular-eslint/component-class-suffix': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
suffixes: ['Directive', 'Component', 'Base', 'Widget']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@angular-eslint/directive-class-suffix': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
suffixes: ['Directive', 'Component', 'Base', 'Widget']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'off',
|
||||||
|
{
|
||||||
|
type: ['element', 'attribute'],
|
||||||
|
prefix: ['app', 'test'],
|
||||||
|
style: 'kebab-case'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'off',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: ['app']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@angular-eslint/no-attribute-decorator': 'error',
|
||||||
|
'@angular-eslint/no-conflicting-lifecycle': 'off',
|
||||||
|
'@angular-eslint/no-forward-ref': 'off',
|
||||||
|
'@angular-eslint/no-host-metadata-property': 'off',
|
||||||
|
'@angular-eslint/no-lifecycle-call': 'off',
|
||||||
|
'@angular-eslint/no-pipe-impure': 'error',
|
||||||
|
'@angular-eslint/prefer-output-readonly': 'error',
|
||||||
|
'@angular-eslint/use-component-selector': 'off',
|
||||||
|
'@angular-eslint/use-component-view-encapsulation': 'off',
|
||||||
|
'@angular-eslint/no-input-rename': 'off',
|
||||||
|
'@angular-eslint/no-output-native': 'off',
|
||||||
|
'@typescript-eslint/array-type': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
default: 'array-simple'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/ban-types': [
|
||||||
|
'off',
|
||||||
|
{
|
||||||
|
types: {
|
||||||
|
String: {
|
||||||
|
message: 'Use string instead.'
|
||||||
|
},
|
||||||
|
Number: {
|
||||||
|
message: 'Use number instead.'
|
||||||
|
},
|
||||||
|
Boolean: {
|
||||||
|
message: 'Use boolean instead.'
|
||||||
|
},
|
||||||
|
Function: {
|
||||||
|
message: 'Use specific callable interface instead.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'import/no-duplicates': 'error',
|
||||||
|
'import/no-unused-modules': 'error',
|
||||||
|
'import/no-unassigned-import': 'error',
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
alphabetize: { order: 'asc', caseInsensitive: false },
|
||||||
|
'newlines-between': 'always',
|
||||||
|
groups: ['external', 'internal', ['parent', 'sibling', 'index']],
|
||||||
|
pathGroups: [],
|
||||||
|
pathGroupsExcludedImportTypes: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-this-alias': 'error',
|
||||||
|
'@typescript-eslint/member-ordering': 'off',
|
||||||
|
'no-irregular-whitespace': 'error',
|
||||||
|
'no-multiple-empty-lines': 'error',
|
||||||
|
'no-sparse-arrays': 'error',
|
||||||
|
'prefer-object-spread': 'error',
|
||||||
|
'prefer-template': 'error',
|
||||||
|
'prefer-const': 'off',
|
||||||
|
'max-len': 'off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.html'],
|
||||||
|
extends: ['plugin:@angular-eslint/template/recommended'],
|
||||||
|
rules: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.html'],
|
||||||
|
excludedFiles: ['*inline-template-*.component.html'],
|
||||||
|
extends: ['plugin:prettier/recommended'],
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': ['error', { parser: 'angular' }],
|
||||||
|
'@angular-eslint/template/eqeqeq': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# /src/assets/color.less
|
||||||
|
delon-builds
|
||||||
|
scripts/var.less
|
||||||
|
# src/assets/style.dark.css
|
||||||
|
# src/assets/style.compact.css
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/yarn.lock
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
yarn-error.log
|
||||||
|
/_all.less
|
||||||
|
|
||||||
|
# e2e
|
||||||
|
/e2e/*.map
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.github
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
dist
|
||||||
|
tmp
|
||||||
18
.prettierignore
Normal file
18
.prettierignore
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# add files you wish to ignore here
|
||||||
|
**/*.md
|
||||||
|
**/*.svg
|
||||||
|
**/test.ts
|
||||||
|
|
||||||
|
.stylelintrc
|
||||||
|
.prettierrc
|
||||||
|
|
||||||
|
src/assets/*
|
||||||
|
src/index.html
|
||||||
|
node_modules/
|
||||||
|
.vscode/
|
||||||
|
coverage/
|
||||||
|
dist/
|
||||||
|
package.json
|
||||||
|
tslint.json
|
||||||
|
|
||||||
|
_cli-tpl/**/*
|
||||||
13
.prettierrc.js
Normal file
13
.prettierrc.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
useTabs: false,
|
||||||
|
printWidth: 140,
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
htmlWhitespaceSensitivity: 'strict',
|
||||||
|
arrowParens: 'avoid',
|
||||||
|
bracketSpacing: true,
|
||||||
|
proseWrap: 'preserve',
|
||||||
|
trailingComma: 'none',
|
||||||
|
endOfLine: 'lf'
|
||||||
|
};
|
||||||
28
.stylelintrc
Normal file
28
.stylelintrc
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"stylelint-config-standard",
|
||||||
|
"stylelint-config-rational-order",
|
||||||
|
"stylelint-config-prettier"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"stylelint-order",
|
||||||
|
"stylelint-declaration-block-no-ignored-properties"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-descending-specificity": null,
|
||||||
|
"plugin/declaration-block-no-ignored-properties": true,
|
||||||
|
"selector-type-no-unknown": null,
|
||||||
|
"selector-pseudo-element-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignorePseudoElements": [
|
||||||
|
"ng-deep"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"at-rule-no-unknown": null
|
||||||
|
},
|
||||||
|
"ignoreFiles": [
|
||||||
|
"src/assets/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
_cli-tpl/README.md
Normal file
1
_cli-tpl/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Document](https://ng-alain.com/cli/generate#Custom-template-page)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<page-header-wrapper>
|
||||||
|
<nz-card>
|
||||||
|
|
||||||
|
</nz-card>
|
||||||
|
</page-header-wrapper>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { <%= componentName %> } from './<%= dasherize(name) %>.component';
|
||||||
|
|
||||||
|
describe('<%= componentName %>', () => {
|
||||||
|
let component: <%= componentName %>;
|
||||||
|
let fixture: ComponentFixture<<%= componentName %>>;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ <%= componentName %> ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(<%= componentName %>);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
|
||||||
|
import { _HttpClient } from '@delon/theme';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '<%= selector %>',
|
||||||
|
templateUrl: './<%= dasherize(name) %>.component.html',<% if(!inlineStyle) { %><% } else { %>
|
||||||
|
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
|
||||||
|
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
|
||||||
|
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
|
||||||
|
})
|
||||||
|
export class <%= componentName %> implements OnInit {
|
||||||
|
|
||||||
|
constructor(private http: _HttpClient, private msg: NzMessageService) { }
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
<page-grid>
|
||||||
|
<nz-card> </nz-card>
|
||||||
|
</page-grid>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { <%= componentName %> } from './<%= dasherize(name) %>.component';
|
||||||
|
|
||||||
|
describe('<%= componentName %>', () => {
|
||||||
|
let component: <%= componentName %>;
|
||||||
|
let fixture: ComponentFixture<<%= componentName %>>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ <%= componentName %> ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(<%= componentName %>);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
|
||||||
|
import { _HttpClient } from '@delon/theme';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '<%= selector %>',
|
||||||
|
templateUrl: './<%= dasherize(name) %>.component.html',<% if(!inlineStyle) { %><% } else { %>
|
||||||
|
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
|
||||||
|
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
|
||||||
|
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
|
||||||
|
})
|
||||||
|
export class <%= componentName %> implements OnInit {
|
||||||
|
|
||||||
|
constructor(private http: _HttpClient, private msg: NzMessageService) { }
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<page-header></page-header>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { <%= componentName %> } from './<%= dasherize(name) %>.component';
|
||||||
|
|
||||||
|
describe('<%= componentName %>', () => {
|
||||||
|
let component: <%= componentName %>;
|
||||||
|
let fixture: ComponentFixture<<%= componentName %>>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ <%= componentName %> ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(<%= componentName %>);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
|
||||||
|
import { _HttpClient } from '@delon/theme';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '<%= selector %>',
|
||||||
|
templateUrl: './<%= dasherize(name) %>.component.html',<% if(!inlineStyle) { %><% } else { %>
|
||||||
|
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
|
||||||
|
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
|
||||||
|
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
|
||||||
|
})
|
||||||
|
export class <%= componentName %> implements OnInit {
|
||||||
|
|
||||||
|
constructor(private http: _HttpClient, private msg: NzMessageService) { }
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
}
|
||||||
1
_mock/README.md
Normal file
1
_mock/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Document](https://ng-alain.com/mock)
|
||||||
265
_mock/_api.ts
Normal file
265
_mock/_api.ts
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
|
||||||
|
// region: mock data
|
||||||
|
|
||||||
|
const titles = ['Alipay', 'Angular', 'Ant Design', 'Ant Design Pro', 'Bootstrap', 'React', 'Vue', 'Webpack'];
|
||||||
|
|
||||||
|
const avatars = [
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
|
||||||
|
];
|
||||||
|
const covers = [
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/HrxcVbrKnCJOZvtzSqjN.png',
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/alaPpKWajEbIYEUvvVNf.png',
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/RLwlKSYGSXGHuWSojyvp.png',
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
|
||||||
|
];
|
||||||
|
const desc = [
|
||||||
|
'那是一种内在的东西, 他们到达不了,也无法触及的',
|
||||||
|
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
|
||||||
|
'生命就像一盒巧克力,结果往往出人意料',
|
||||||
|
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
|
||||||
|
'那时候我只会想自己想要什么,从不想自己拥有什么',
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = ['卡色', 'cipchk', '付小小', '曲丽丽', '林东东', '周星星', '吴加好', '朱偏右', '鱼酱', '乐哥', '谭小仪', '仲尼'];
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
function getFakeList(count: number = 20): any[] {
|
||||||
|
const list: any[] = [];
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
list.push({
|
||||||
|
id: `fake-list-${i}`,
|
||||||
|
owner: user[i % 10],
|
||||||
|
title: titles[i % 8],
|
||||||
|
avatar: avatars[i % 8],
|
||||||
|
cover: parseInt((i / 4).toString(), 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
|
||||||
|
status: ['active', 'exception', 'normal'][i % 3],
|
||||||
|
percent: Math.ceil(Math.random() * 50) + 50,
|
||||||
|
logo: avatars[i % 8],
|
||||||
|
href: 'https://ant.design',
|
||||||
|
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
|
||||||
|
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
|
||||||
|
subDescription: desc[i % 5],
|
||||||
|
description:
|
||||||
|
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
|
||||||
|
activeUser: Math.ceil(Math.random() * 100000) + 100000,
|
||||||
|
newUser: Math.ceil(Math.random() * 1000) + 1000,
|
||||||
|
star: Math.ceil(Math.random() * 100) + 100,
|
||||||
|
like: Math.ceil(Math.random() * 100) + 100,
|
||||||
|
message: Math.ceil(Math.random() * 10) + 10,
|
||||||
|
content:
|
||||||
|
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
|
||||||
|
name: '曲丽丽',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
|
||||||
|
name: '王昭君',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
|
||||||
|
name: '董娜娜',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNotice(): any[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'xxx1',
|
||||||
|
title: titles[0],
|
||||||
|
logo: avatars[0],
|
||||||
|
description: '那是一种内在的东西, 他们到达不了,也无法触及的',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
member: '科学搬砖组',
|
||||||
|
href: '',
|
||||||
|
memberLink: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xxx2',
|
||||||
|
title: titles[1],
|
||||||
|
logo: avatars[1],
|
||||||
|
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
|
||||||
|
updatedAt: new Date('2017-07-24'),
|
||||||
|
member: '全组都是吴彦祖',
|
||||||
|
href: '',
|
||||||
|
memberLink: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xxx3',
|
||||||
|
title: titles[2],
|
||||||
|
logo: avatars[2],
|
||||||
|
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
member: '中二少女团',
|
||||||
|
href: '',
|
||||||
|
memberLink: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xxx4',
|
||||||
|
title: titles[3],
|
||||||
|
logo: avatars[3],
|
||||||
|
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
|
||||||
|
updatedAt: new Date('2017-07-23'),
|
||||||
|
member: '程序员日常',
|
||||||
|
href: '',
|
||||||
|
memberLink: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xxx5',
|
||||||
|
title: titles[4],
|
||||||
|
logo: avatars[4],
|
||||||
|
description: '凛冬将至',
|
||||||
|
updatedAt: new Date('2017-07-23'),
|
||||||
|
member: '高逼格设计天团',
|
||||||
|
href: '',
|
||||||
|
memberLink: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xxx6',
|
||||||
|
title: titles[5],
|
||||||
|
logo: avatars[5],
|
||||||
|
description: '生命就像一盒巧克力,结果往往出人意料',
|
||||||
|
updatedAt: new Date('2017-07-23'),
|
||||||
|
member: '骗你来学计算机',
|
||||||
|
href: '',
|
||||||
|
memberLink: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivities(): any[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'trend-1',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
user: {
|
||||||
|
name: '林东东',
|
||||||
|
avatar: avatars[0],
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
name: '高逼格设计天团',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: '六月迭代',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
template: '在 @{group} 新建项目 @{project}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trend-2',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
user: {
|
||||||
|
name: '付小小',
|
||||||
|
avatar: avatars[1],
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
name: '高逼格设计天团',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: '六月迭代',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
template: '在 @{group} 新建项目 @{project}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trend-3',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
user: {
|
||||||
|
name: '曲丽丽',
|
||||||
|
avatar: avatars[2],
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
name: '中二少女团',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: '六月迭代',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
template: '在 @{group} 新建项目 @{project}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trend-4',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
user: {
|
||||||
|
name: '周星星',
|
||||||
|
avatar: avatars[3],
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: '5 月日常迭代',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
template: '将 @{project} 更新至已发布状态',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trend-5',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
user: {
|
||||||
|
name: '朱偏右',
|
||||||
|
avatar: avatars[4],
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: '工程效能',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
name: '留言',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
template: '在 @{project} 发布了 @{comment}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trend-6',
|
||||||
|
updatedAt: new Date(),
|
||||||
|
user: {
|
||||||
|
name: '乐哥',
|
||||||
|
avatar: avatars[5],
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
name: '程序员日常',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: '品牌迭代',
|
||||||
|
link: 'http://github.com/',
|
||||||
|
},
|
||||||
|
template: '在 @{group} 新建项目 @{project}',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APIS = {
|
||||||
|
'/api/list': (req: MockRequest) => getFakeList(req.queryString.count),
|
||||||
|
'/api/notice': () => getNotice(),
|
||||||
|
'/api/activities': () => getActivities(),
|
||||||
|
'POST /api/auth/refresh': { msg: 'ok', token: 'new-token-by-refresh' },
|
||||||
|
'/api/401': () => {
|
||||||
|
throw new MockStatusError(401);
|
||||||
|
},
|
||||||
|
'/api/403': () => {
|
||||||
|
throw new MockStatusError(403);
|
||||||
|
},
|
||||||
|
'/api/404': () => {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
},
|
||||||
|
'/api/500': () => {
|
||||||
|
throw new MockStatusError(500);
|
||||||
|
},
|
||||||
|
};
|
||||||
205
_mock/_chart.ts
Normal file
205
_mock/_chart.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { format } from 'date-fns';
|
||||||
|
import * as Mock from 'mockjs';
|
||||||
|
|
||||||
|
// region: mock data
|
||||||
|
|
||||||
|
const visitData: any[] = [];
|
||||||
|
const beginDay = new Date().getTime();
|
||||||
|
|
||||||
|
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
|
||||||
|
for (let i = 0; i < fakeY.length; i += 1) {
|
||||||
|
visitData.push({
|
||||||
|
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
|
||||||
|
y: fakeY[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitData2: any[] = [];
|
||||||
|
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
|
||||||
|
for (let i = 0; i < fakeY2.length; i += 1) {
|
||||||
|
visitData2.push({
|
||||||
|
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
|
||||||
|
y: fakeY2[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const salesData: any[] = [];
|
||||||
|
for (let i = 0; i < 12; i += 1) {
|
||||||
|
salesData.push({
|
||||||
|
x: `${i + 1}月`,
|
||||||
|
y: Math.floor(Math.random() * 1000) + 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const searchData: any[] = [];
|
||||||
|
for (let i = 0; i < 50; i += 1) {
|
||||||
|
searchData.push({
|
||||||
|
index: i + 1,
|
||||||
|
keyword: `搜索关键词-${i}`,
|
||||||
|
count: Math.floor(Math.random() * 1000),
|
||||||
|
range: Math.floor(Math.random() * 100),
|
||||||
|
status: Math.floor((Math.random() * 10) % 2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const salesTypeData = [
|
||||||
|
{
|
||||||
|
x: '家用电器',
|
||||||
|
y: 4544,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '食用酒水',
|
||||||
|
y: 3321,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '个护健康',
|
||||||
|
y: 3113,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '服饰箱包',
|
||||||
|
y: 2341,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '母婴产品',
|
||||||
|
y: 1231,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '其他',
|
||||||
|
y: 1231,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const salesTypeDataOnline = [
|
||||||
|
{
|
||||||
|
x: '家用电器',
|
||||||
|
y: 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '食用酒水',
|
||||||
|
y: 321,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '个护健康',
|
||||||
|
y: 311,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '服饰箱包',
|
||||||
|
y: 41,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '母婴产品',
|
||||||
|
y: 121,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '其他',
|
||||||
|
y: 111,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const salesTypeDataOffline = [
|
||||||
|
{
|
||||||
|
x: '家用电器',
|
||||||
|
y: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '个护健康',
|
||||||
|
y: 188,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '服饰箱包',
|
||||||
|
y: 344,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '母婴产品',
|
||||||
|
y: 255,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: '其他',
|
||||||
|
y: 65,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const offlineData: any[] = [];
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
offlineData.push({
|
||||||
|
name: `门店${i}`,
|
||||||
|
cvr: Math.ceil(Math.random() * 9) / 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const offlineChartData: any[] = [];
|
||||||
|
for (let i = 0; i < 20; i += 1) {
|
||||||
|
offlineChartData.push({
|
||||||
|
time: new Date().getTime() + 1000 * 60 * 30 * i,
|
||||||
|
y1: Math.floor(Math.random() * 100) + 10,
|
||||||
|
y2: Math.floor(Math.random() * 100) + 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const radarOriginData = [
|
||||||
|
{
|
||||||
|
name: '个人',
|
||||||
|
ref: 10,
|
||||||
|
koubei: 8,
|
||||||
|
output: 4,
|
||||||
|
contribute: 5,
|
||||||
|
hot: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '团队',
|
||||||
|
ref: 3,
|
||||||
|
koubei: 9,
|
||||||
|
output: 6,
|
||||||
|
contribute: 3,
|
||||||
|
hot: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '部门',
|
||||||
|
ref: 4,
|
||||||
|
koubei: 1,
|
||||||
|
output: 6,
|
||||||
|
contribute: 5,
|
||||||
|
hot: 7,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
//
|
||||||
|
const radarData: any[] = [];
|
||||||
|
const radarTitleMap: any = {
|
||||||
|
ref: '引用',
|
||||||
|
koubei: '口碑',
|
||||||
|
output: '产量',
|
||||||
|
contribute: '贡献',
|
||||||
|
hot: '热度',
|
||||||
|
};
|
||||||
|
radarOriginData.forEach((item: any) => {
|
||||||
|
Object.keys(item).forEach((key) => {
|
||||||
|
if (key !== 'name') {
|
||||||
|
radarData.push({
|
||||||
|
name: item.name,
|
||||||
|
label: radarTitleMap[key],
|
||||||
|
value: item[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
export const CHARTS = {
|
||||||
|
'/chart': JSON.parse(
|
||||||
|
JSON.stringify({
|
||||||
|
visitData,
|
||||||
|
visitData2,
|
||||||
|
salesData,
|
||||||
|
searchData,
|
||||||
|
offlineData,
|
||||||
|
offlineChartData,
|
||||||
|
salesTypeData,
|
||||||
|
salesTypeDataOnline,
|
||||||
|
salesTypeDataOffline,
|
||||||
|
radarData,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
'/chart/visit': JSON.parse(JSON.stringify(visitData)),
|
||||||
|
'/chart/tags': Mock.mock({
|
||||||
|
'list|100': [{ name: '@city', 'value|1-100': 150 }],
|
||||||
|
}),
|
||||||
|
};
|
||||||
126
_mock/_file-manager.ts
Normal file
126
_mock/_file-manager.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
import { Random } from 'mockjs';
|
||||||
|
|
||||||
|
import { genMp } from './utils';
|
||||||
|
|
||||||
|
interface FileItem {
|
||||||
|
id?: number;
|
||||||
|
parent_id?: number;
|
||||||
|
type?: 'folder' | 'file';
|
||||||
|
title?: string;
|
||||||
|
mp?: string;
|
||||||
|
ext?: string;
|
||||||
|
size?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
created?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = 1;
|
||||||
|
let DATA: FileItem[] = [];
|
||||||
|
DATA = DATA.concat(...genFolds(0, 3), ...genFiles(1, 6), ...genFiles(2, 3), ...genFiles(0, 1, 'zip'), ...genFiles(0, 10));
|
||||||
|
|
||||||
|
function genFolds(parent_id: number, count: number): FileItem[] {
|
||||||
|
return new Array(count).fill({}).map(() => {
|
||||||
|
return {
|
||||||
|
id: point++,
|
||||||
|
parent_id,
|
||||||
|
type: 'folder',
|
||||||
|
ext: 'folder',
|
||||||
|
title: Random.ctitle(3, 5),
|
||||||
|
created: new Date()
|
||||||
|
} as FileItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genFiles(parent_id: number, count: number, ext: string = 'png'): FileItem[] {
|
||||||
|
return new Array(count).fill({}).map(() => {
|
||||||
|
return {
|
||||||
|
id: point++,
|
||||||
|
parent_id,
|
||||||
|
type: 'file',
|
||||||
|
title: `${Random.ctitle(3, 5)}.${ext}`,
|
||||||
|
mp: genMp(),
|
||||||
|
is_img: ext === 'png',
|
||||||
|
ext,
|
||||||
|
size: Random.natural(10, 10000),
|
||||||
|
width: Random.natural(100, 1000),
|
||||||
|
height: Random.natural(100, 1000),
|
||||||
|
created: new Date()
|
||||||
|
} as FileItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(DATA) as FileItem[];
|
||||||
|
const parent_id = +(params.parent_id || '0');
|
||||||
|
ret = ret.filter(data => data.parent_id === parent_id);
|
||||||
|
if (params.type) {
|
||||||
|
ret = ret.filter(data => data.type!.indexOf(params.type) > -1);
|
||||||
|
}
|
||||||
|
if (params.q) {
|
||||||
|
ret = ret.filter(data => data.title!.indexOf(params.q) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = DATA.findIndex(w => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FILES = {
|
||||||
|
'/file/folder': () => deepCopy(DATA).filter((w: any) => w.type === 'folder'),
|
||||||
|
'/file': (req: MockRequest) => {
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
const data = get(req.queryString);
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'POST /file': (req: MockRequest) => {
|
||||||
|
const file = req.body.get('file') as File;
|
||||||
|
const parent_id = +req.body.get('parent_id');
|
||||||
|
const item = Object.assign(genFiles(req.body.parent_id, 1)[0], {
|
||||||
|
parent_id,
|
||||||
|
title: file.name,
|
||||||
|
size: file.size
|
||||||
|
});
|
||||||
|
DATA.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'/file/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
const item = { ...DATA[idx], ...req.body };
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
'POST /file/rename': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.id || 0);
|
||||||
|
DATA[idx].title = req.body.title;
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
},
|
||||||
|
'POST /file/move': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.id || 0);
|
||||||
|
DATA[idx].parent_id = req.body.moveId;
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
},
|
||||||
|
'POST /file/copy/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
const item = { ...DATA[idx], id: point++ };
|
||||||
|
item.title += ' - Copy';
|
||||||
|
DATA.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'DELETE /file/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
DATA.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
}
|
||||||
|
};
|
||||||
258
_mock/_forum.ts
Normal file
258
_mock/_forum.ts
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
import { Random } from 'mockjs';
|
||||||
|
import { genLabel, genMp } from './utils';
|
||||||
|
|
||||||
|
let id = 1;
|
||||||
|
const CATEGORY = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'General',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Getting started',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Announcements',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Guides',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Shopping',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Guides',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'Payments',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'Products',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: 'Refund',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Support',
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
title: 'Common questions',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: 'Site issues',
|
||||||
|
threads: Random.natural(1, 20),
|
||||||
|
replies: Random.natural(6, 100),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const THREAD: any[] = new Array(20).fill({}).map((v, idx) => ({
|
||||||
|
id: id++,
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
replies: Random.natural(1, 1000),
|
||||||
|
label: idx % 2 === 0 && Random.boolean() ? genLabel() : null,
|
||||||
|
category_id: Random.natural(1, 9),
|
||||||
|
last: {
|
||||||
|
id: id++,
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
user_name: Random.name(),
|
||||||
|
time: '1d ago',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const REPLIES: any[] = new Array(20).fill({}).map((v, idx) => ({
|
||||||
|
id: id++,
|
||||||
|
content: Random.paragraph(),
|
||||||
|
user: {
|
||||||
|
name: Random.name(),
|
||||||
|
mp: genMp(),
|
||||||
|
posts: Random.natural(0, 1000),
|
||||||
|
},
|
||||||
|
time: Random.time(),
|
||||||
|
like: Random.natural(0, 100),
|
||||||
|
dislike: Random.natural(0, 10),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(THREAD);
|
||||||
|
if (params.q) {
|
||||||
|
ret = ret.filter((data: any) => data.name.indexOf(params.q) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(itemId: number): number {
|
||||||
|
itemId = +itemId;
|
||||||
|
const idx = THREAD.findIndex((w) => w.id === itemId);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCate(itemId: number): any {
|
||||||
|
let item: any;
|
||||||
|
const category: any = deepCopy(CATEGORY).find((w: any) => {
|
||||||
|
item = w.list.find((l: any) => l.id === itemId);
|
||||||
|
if (item) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
delete category.list;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FORUMS = {
|
||||||
|
'/forum/category': CATEGORY,
|
||||||
|
'/forum/thread/:id': (req: MockRequest) => {
|
||||||
|
// list
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
const data = get(req.queryString);
|
||||||
|
return {
|
||||||
|
category: getCate(+req.params.id),
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'/forum/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
const item = {
|
||||||
|
...THREAD[idx],
|
||||||
|
time: '3 days ago',
|
||||||
|
like: Random.natural(0, 100),
|
||||||
|
view: Random.natural(0, 10000),
|
||||||
|
user: {
|
||||||
|
name: Random.name(),
|
||||||
|
mp: genMp(),
|
||||||
|
posts: Random.natural(0, 1000),
|
||||||
|
},
|
||||||
|
desc:
|
||||||
|
'<p>' +
|
||||||
|
new Array(Random.natural(1, 3))
|
||||||
|
.fill('')
|
||||||
|
.map((v) => Random.paragraph())
|
||||||
|
.join('</p><p>') +
|
||||||
|
'</p>',
|
||||||
|
};
|
||||||
|
item.category = getCate(item.category_id);
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
'/forum/:id/replies': (req: MockRequest) => {
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
return {
|
||||||
|
total: REPLIES.length,
|
||||||
|
list: REPLIES.slice((pi - 1) * ps, pi * ps),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
'POST /forum': (req: MockRequest) => {
|
||||||
|
const itemId = req.body.id || 0;
|
||||||
|
if (itemId > 0) {
|
||||||
|
const idx = getIdx(itemId);
|
||||||
|
THREAD[idx] = { ...THREAD[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: THREAD[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...req.body, id: THREAD.sort((a, b) => b.id - a.id)[0].id + 1 };
|
||||||
|
THREAD.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'DELETE /forum/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
THREAD.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
};
|
||||||
76
_mock/_geo.ts
Normal file
76
_mock/_geo.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { MockRequest } from '@delon/mock';
|
||||||
|
|
||||||
|
const DATA = [
|
||||||
|
{
|
||||||
|
name: '上海',
|
||||||
|
id: '310000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '市辖区',
|
||||||
|
id: '310100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '北京',
|
||||||
|
id: '110000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '市辖区',
|
||||||
|
id: '110100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '浙江省',
|
||||||
|
id: '330000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '杭州市',
|
||||||
|
id: '330100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '宁波市',
|
||||||
|
id: '330200',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '温州市',
|
||||||
|
id: '330300',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '嘉兴市',
|
||||||
|
id: '330400',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '湖州市',
|
||||||
|
id: '330500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '绍兴市',
|
||||||
|
id: '330600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '金华市',
|
||||||
|
id: '330700',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '衢州市',
|
||||||
|
id: '330800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '舟山市',
|
||||||
|
id: '330900',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '台州市',
|
||||||
|
id: '331000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '丽水市',
|
||||||
|
id: '331100',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const GEOS = {
|
||||||
|
'/geo/province': () => DATA.filter(w => w.id.endsWith('0000')),
|
||||||
|
'/geo/:id': (req: MockRequest) => {
|
||||||
|
const pid = (req.params.id || '310000').slice(0, 2);
|
||||||
|
return DATA.filter(w => w.id.slice(0, 2) === pid && !w.id.endsWith('0000'));
|
||||||
|
},
|
||||||
|
};
|
||||||
103
_mock/_img.ts
Normal file
103
_mock/_img.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
import { genMp } from './utils';
|
||||||
|
|
||||||
|
interface ImgCat {
|
||||||
|
id: number;
|
||||||
|
parent_id: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
interface Img {
|
||||||
|
cat_id: number;
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
mp: string;
|
||||||
|
size: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
created: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CAT: ImgCat[] = [
|
||||||
|
{ id: 1, parent_id: 0, title: '店铺' },
|
||||||
|
{ id: 2, parent_id: 1, title: '产品图' },
|
||||||
|
{ id: 3, parent_id: 1, title: '品牌图' },
|
||||||
|
{ id: 4, parent_id: 0, title: '营销' },
|
||||||
|
{ id: 5, parent_id: 4, title: '双11' },
|
||||||
|
{ id: 6, parent_id: 4, title: '日常' },
|
||||||
|
{ id: 7, parent_id: 0, title: '其他' },
|
||||||
|
];
|
||||||
|
const DATA: Img[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 50; i += 1) {
|
||||||
|
DATA.push(gen(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
function gen(i: number): any {
|
||||||
|
return {
|
||||||
|
cat_id: [1, 2, 3, 4, 5, 6, 7][Math.floor(Math.random() * 10) % 7],
|
||||||
|
id: i * 10000,
|
||||||
|
title: `title ${i}`,
|
||||||
|
mp: genMp(),
|
||||||
|
size: Math.floor(Math.random() * 1000) % 1000,
|
||||||
|
width: Math.floor(Math.random() * 1000) % 1000,
|
||||||
|
height: Math.floor(Math.random() * 1000) % 1000,
|
||||||
|
created: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(DATA);
|
||||||
|
const cat_id = +(params.cat_id || '0');
|
||||||
|
if (cat_id > 0) {
|
||||||
|
ret = ret.filter((data: any) => data.cat_id === cat_id);
|
||||||
|
}
|
||||||
|
if (params.q) {
|
||||||
|
ret = ret.filter((data: any) => data.title.indexOf(params.q) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = DATA.findIndex((w) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IMGS = {
|
||||||
|
'/img/cat': () => deepCopy(CAT),
|
||||||
|
'/img': (req: MockRequest) => {
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
const data = get(req.queryString);
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'POST /img': (req: MockRequest) => {
|
||||||
|
const id = req.body.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(id);
|
||||||
|
DATA[idx] = { ...DATA[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = Object.assign(gen(DATA.sort((a, b) => b.id - a.id)[0].id + 1), req.body);
|
||||||
|
DATA.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'/img/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
const item = { ...DATA[idx], ...req.body };
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
'DELETE /img/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
DATA.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
};
|
||||||
62
_mock/_log.ts
Normal file
62
_mock/_log.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
|
||||||
|
const DATA: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 20; i += 1) {
|
||||||
|
DATA.push({
|
||||||
|
id: i,
|
||||||
|
name: 'cms',
|
||||||
|
level: ['error', 'warning', 'info'][Math.floor(Math.random() * 10) % 3],
|
||||||
|
path: `/home/${i}`,
|
||||||
|
title: `未知报告 ${i}`,
|
||||||
|
data: `Uncaught Error: test-${i}\nat <anonymous>:1:7\nat <anonymous>:1:7\nat <anonymous>:1:7`,
|
||||||
|
created: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
const idx = DATA.findIndex((w) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(DATA);
|
||||||
|
if (params.q) {
|
||||||
|
ret = ret.filter((data: any) => data.title.indexOf(params.q) > -1);
|
||||||
|
}
|
||||||
|
if (params.level) {
|
||||||
|
ret = ret.filter((data: any) => data.level.indexOf(params.level) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(params: any): any {
|
||||||
|
const id = params.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
DATA.splice(getIdx(id), 1);
|
||||||
|
} else {
|
||||||
|
get(params).forEach((w: any) => {
|
||||||
|
del({ id: w.id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LOGS = {
|
||||||
|
'/log': (req: MockRequest) => {
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
const data = get(req.queryString);
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'DELETE /log': (req: MockRequest) => {
|
||||||
|
del(req.queryString);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
};
|
||||||
109
_mock/_menu.ts
Normal file
109
_mock/_menu.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { Menu } from '@delon/theme';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
|
||||||
|
const DATA: Menu[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
parent_id: 0,
|
||||||
|
text: '主导航',
|
||||||
|
i18n: 'menu.main',
|
||||||
|
group: true,
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
parent_id: 1,
|
||||||
|
text: '仪表盘',
|
||||||
|
i18n: 'menu.dashboard',
|
||||||
|
icon: 'dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
parent_id: 2,
|
||||||
|
text: '分析页',
|
||||||
|
link: '/dashboard/analysis',
|
||||||
|
i18n: 'menu.dashboard.analysis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
parent_id: 2,
|
||||||
|
text: '监控页',
|
||||||
|
link: '/dashboard/monitor',
|
||||||
|
i18n: 'menu.dashboard.monitor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
parent_id: 2,
|
||||||
|
text: '工作台',
|
||||||
|
link: '/dashboard/workplace',
|
||||||
|
i18n: 'menu.dashboard.workplace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
parent_id: 0,
|
||||||
|
text: 'Pro',
|
||||||
|
i18n: 'menu.pro',
|
||||||
|
group: true,
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
parent_id: 6,
|
||||||
|
text: 'Form Page',
|
||||||
|
i18n: 'menu.form',
|
||||||
|
link: '/pro/form',
|
||||||
|
icon: 'anticon anticon-edit',
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
parent_id: 6,
|
||||||
|
text: 'Basic Form',
|
||||||
|
link: '/pro/form/basic-form',
|
||||||
|
i18n: 'menu.form.basicform',
|
||||||
|
shortcut: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
parent_id: 6,
|
||||||
|
text: 'Step Form',
|
||||||
|
link: '/pro/form/step-form',
|
||||||
|
i18n: 'menu.form.stepform',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = DATA.findIndex((w) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MENUS = {
|
||||||
|
'/menus': () => deepCopy(DATA),
|
||||||
|
'POST /menus': (req: MockRequest) => {
|
||||||
|
const id = req.body.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(id);
|
||||||
|
DATA[idx] = { ...DATA[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...req.body, id: DATA.sort((a, b) => b.id - a.id)[0].id + 1 };
|
||||||
|
DATA.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'DELETE /menus/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
DATA.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
'POST /menus/move': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.from || 0);
|
||||||
|
DATA[idx].parent_id = req.body.to || 0;
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
},
|
||||||
|
};
|
||||||
917
_mock/_other.ts
Normal file
917
_mock/_other.ts
Normal file
@ -0,0 +1,917 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import getDaysInMonth from 'date-fns/getDaysInMonth';
|
||||||
|
import startOfMonth from 'date-fns/startOfMonth';
|
||||||
|
import { Random } from 'mockjs';
|
||||||
|
|
||||||
|
import { genArr, genBigMp, genColorName, genData, genLabel, genMp, genName, genTag } from './utils';
|
||||||
|
|
||||||
|
let ID = 1;
|
||||||
|
const DATA: any = {
|
||||||
|
kanban: null,
|
||||||
|
task: null,
|
||||||
|
email: null,
|
||||||
|
project: null,
|
||||||
|
client: null,
|
||||||
|
contact: null,
|
||||||
|
pricing: null,
|
||||||
|
billing: null,
|
||||||
|
course: null,
|
||||||
|
chat: null,
|
||||||
|
gallery: null,
|
||||||
|
article: null,
|
||||||
|
voting: null,
|
||||||
|
invoice: null,
|
||||||
|
faq: null,
|
||||||
|
calendar: null,
|
||||||
|
quick: null,
|
||||||
|
dd: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIdx(type: string, id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = DATA[type].findIndex((w: any) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(type: string, body: any): any {
|
||||||
|
const id = body.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(type, id);
|
||||||
|
DATA[type][idx] = { ...DATA[type][idx], ...body };
|
||||||
|
return { msg: 'ok', item: DATA[type][idx], type: 'edit' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...body, id: DATA[type].sort((a: any, b: any) => b.id - a.id)[0].id + 1 };
|
||||||
|
(DATA[type] as any[]).splice(0, 0, item);
|
||||||
|
return { msg: 'ok', item, type: 'add' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(type: string, p: any): any {
|
||||||
|
const cid = +(p.cid || '0');
|
||||||
|
let list: any[] = DATA[type];
|
||||||
|
if (cid > 0) {
|
||||||
|
list = DATA[type].find((w: any) => w.id === cid).list;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = list.findIndex(w => w.id === p.id);
|
||||||
|
list.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function genHtml(): string {
|
||||||
|
return `<p>${new Array(Random.natural(1, 3))
|
||||||
|
.fill('')
|
||||||
|
.map(v => Random.sentence())
|
||||||
|
.join('</p><p>')}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachements(): any {
|
||||||
|
return new Array(Random.natural(2, 6)).fill({}).map((v, idx) => {
|
||||||
|
const item = {
|
||||||
|
url: Random.url(),
|
||||||
|
type: genArr(['jpg', 'zip', 'pdf']),
|
||||||
|
filename: Random.name(false),
|
||||||
|
size: genArr(['100KB', '980KB', '1.56MB'])
|
||||||
|
};
|
||||||
|
if (item.type === 'jpg') {
|
||||||
|
item.url = genBigMp();
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genPage(type: string, queryString: any, qField: string = 'name'): any {
|
||||||
|
const pi = +(queryString.pi || 1);
|
||||||
|
const ps = +(queryString.ps || 10);
|
||||||
|
// data
|
||||||
|
let data = deepCopy(DATA[type]);
|
||||||
|
if (queryString.q) {
|
||||||
|
data = data.filter((data: any) => data[qField].indexOf(queryString.q) > -1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// #region kanban
|
||||||
|
|
||||||
|
function kanbanList(): any {
|
||||||
|
if (DATA.kanban) {
|
||||||
|
return DATA.kanban;
|
||||||
|
}
|
||||||
|
const res: any[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'To Do',
|
||||||
|
list: [],
|
||||||
|
color: '#fadb14',
|
||||||
|
icon: 'warning'
|
||||||
|
},
|
||||||
|
{ id: 2, title: 'In progress', color: '#1890ff', icon: 'tool', list: [] },
|
||||||
|
{ id: 3, title: 'Done', color: '#52c41a', icon: 'check-circle', list: [] },
|
||||||
|
{ id: 4, title: 'Gone', color: '#f5222d', icon: 'delete', list: [] }
|
||||||
|
];
|
||||||
|
for (const i of res) {
|
||||||
|
i.list = new Array(Random.natural(2, 6)).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
title: Random.ctitle(3, 4),
|
||||||
|
content: Random.ctitle(0, 50),
|
||||||
|
attachement: Random.boolean() && Random.boolean() && Random.boolean()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// new
|
||||||
|
if (res[0].list.length > 0) {
|
||||||
|
res[0].list[Random.natural(0, res[0].list.length - 1)].label = {
|
||||||
|
color: 'green',
|
||||||
|
text: 'Clients'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (res[1].list.length > 0) {
|
||||||
|
res[1].list[Random.natural(0, res[1].list.length - 1)].label = {
|
||||||
|
color: 'red',
|
||||||
|
text: 'Important'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (res[2].list.length > 0) {
|
||||||
|
res[2].list[Random.natural(0, res[2].list.length - 1)].label = {
|
||||||
|
color: 'blue',
|
||||||
|
text: 'Other'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// labels
|
||||||
|
DATA.kanban = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region task
|
||||||
|
|
||||||
|
function taskList(): any {
|
||||||
|
if (DATA.task) {
|
||||||
|
return DATA.task;
|
||||||
|
}
|
||||||
|
const res: any[] = [
|
||||||
|
{ id: 1, title: 'Today', list: [] },
|
||||||
|
{ id: 2, title: 'Tomorrow', list: [] },
|
||||||
|
{ id: 3, title: 'Next week', list: [] }
|
||||||
|
];
|
||||||
|
for (const i of res) {
|
||||||
|
i.list = new Array(Random.natural(2, 8)).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
title: Random.ctitle(3, 16),
|
||||||
|
due: i.id === 1 && Random.boolean() ? `${Random.natural(1, 59)} ${Random.boolean() ? 'mins' : 'hours'} left` : null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// new
|
||||||
|
if (res[0].list.length > 0) {
|
||||||
|
res[0].list[Random.natural(0, res[0].list.length - 1)].label = {
|
||||||
|
color: 'green',
|
||||||
|
text: 'Clients'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (res[1].list.length > 0) {
|
||||||
|
res[1].list[Random.natural(0, res[1].list.length - 1)].label = {
|
||||||
|
color: 'red',
|
||||||
|
text: 'Important'
|
||||||
|
};
|
||||||
|
res[0].list[Random.natural(0, res[0].list.length - 1)].done = true;
|
||||||
|
}
|
||||||
|
if (res[2].list.length > 0) {
|
||||||
|
res[2].list[Random.natural(0, res[2].list.length - 1)].label = {
|
||||||
|
color: 'blue',
|
||||||
|
text: 'Other'
|
||||||
|
};
|
||||||
|
res[0].list[Random.natural(0, res[0].list.length - 1)].done = true;
|
||||||
|
}
|
||||||
|
// labels
|
||||||
|
DATA.task = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region email
|
||||||
|
|
||||||
|
function emailList(queryString: any): any {
|
||||||
|
if (DATA.email) {
|
||||||
|
return genPage('email', queryString, 'subject');
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(20).fill({}).map((v, idx) => ({
|
||||||
|
id: ID++,
|
||||||
|
from: Random.email(),
|
||||||
|
from_name: genName(),
|
||||||
|
to: Random.email(),
|
||||||
|
to_name: Random.name(),
|
||||||
|
cc: [Random.email(), Random.email()],
|
||||||
|
subject: Random.title(1, 6),
|
||||||
|
body: Random.paragraph(),
|
||||||
|
read: idx % 2 === 0 && Random.boolean(),
|
||||||
|
star: Random.boolean(),
|
||||||
|
label: genLabel(),
|
||||||
|
attach: idx % 3 === 0 && Random.boolean(),
|
||||||
|
time: `1${Random.boolean() ? 'h' : 'd'} ago`
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.email = res;
|
||||||
|
return genPage('email', queryString, 'subject');
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailGet(id: number): any {
|
||||||
|
const idx = getIdx('email', id || 0);
|
||||||
|
const item = { ...DATA.email[idx], mp: genMp(), desc: genHtml(), attachements: attachements() };
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region project
|
||||||
|
|
||||||
|
function projectList(): any {
|
||||||
|
if (DATA.project) {
|
||||||
|
return DATA.project;
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(5).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
title: genArr(['UI update', 'Web Design', 'Pro Design', 'Ng Alain', 'Delon', 'SEO']),
|
||||||
|
status: idx % 2 ? genArr(['active', 'pending', 'complete']) : 'active',
|
||||||
|
task: {
|
||||||
|
process: Random.natural(1, 100),
|
||||||
|
opened: Random.natural(1, 100),
|
||||||
|
completed: Random.natural(1, 100)
|
||||||
|
},
|
||||||
|
remark: Random.title(5, 10),
|
||||||
|
created: Random.date(),
|
||||||
|
deadline: Random.date(),
|
||||||
|
tean: new Array(Random.natural(1, 6)).fill({}).map(() => ({
|
||||||
|
name: genName(),
|
||||||
|
mp: genMp()
|
||||||
|
})),
|
||||||
|
leaders: new Array(Random.natural(1, 2)).fill({}).map(() => ({
|
||||||
|
name: genName(),
|
||||||
|
mp: genMp()
|
||||||
|
})),
|
||||||
|
participants: new Array(Random.natural(1, 6)).fill({}).map(() => ({
|
||||||
|
name: Random.name(),
|
||||||
|
mp: genMp()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.project = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectGet(id: number): any {
|
||||||
|
const idx = getIdx('project', id || 0);
|
||||||
|
const item = {
|
||||||
|
...DATA.project[idx],
|
||||||
|
user_name: genName(),
|
||||||
|
desc: genHtml(),
|
||||||
|
attachements: attachements(),
|
||||||
|
tasks: DATA.task,
|
||||||
|
discussions: new Array(Random.natural(3, 8)).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
user_avatar: genMp(),
|
||||||
|
user_name: genName(),
|
||||||
|
time: Random.datetime(),
|
||||||
|
content: Random.paragraph(1, 1)
|
||||||
|
})),
|
||||||
|
activities: new Array(Random.natural(3, 12)).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
user_avatar: genMp(),
|
||||||
|
user_name: genName(),
|
||||||
|
time: Random.datetime(),
|
||||||
|
type: idx % 2 === 0 ? genArr(['add', 'completed', 'assigned']) : 'push',
|
||||||
|
commit: Random.natural(10000, 99999),
|
||||||
|
assigne_name: Random.name(),
|
||||||
|
message: Random.title()
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region billing
|
||||||
|
|
||||||
|
function billingList(queryString: any): any {
|
||||||
|
if (DATA.billing) {
|
||||||
|
return genPage('billing', queryString, 'client');
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(11).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
order: `FR0${Random.natural(10, 99)}`,
|
||||||
|
client: genArr(['Google', 'Alibaba', 'Tencent']),
|
||||||
|
fee: Random.float(0, 9.0),
|
||||||
|
amount: Random.float(0.1, 99999.0),
|
||||||
|
date: Random.now('day'),
|
||||||
|
status: idx % 2 ? genArr(['Completed', 'Pending', 'Rejected']) : 'Completed',
|
||||||
|
auth_code: Random.natural(100000000),
|
||||||
|
address: (Random as any).county(true),
|
||||||
|
first_name: Random.first(),
|
||||||
|
last_name: Random.last(),
|
||||||
|
country: 'China'
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.billing = res;
|
||||||
|
return genPage('billing', queryString, 'client');
|
||||||
|
}
|
||||||
|
|
||||||
|
function billingGet(id: number): any {
|
||||||
|
const idx = getIdx('billing', id || 0);
|
||||||
|
const item = {
|
||||||
|
...DATA.billing[idx],
|
||||||
|
messages: new Array(Random.natural(0, 5)).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
time: `${Random.natural(1, 6)} day ago`,
|
||||||
|
message: Random.paragraph(1, 1)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region contact
|
||||||
|
|
||||||
|
function contactList(queryString: any): any {
|
||||||
|
if (DATA.contact) {
|
||||||
|
return genPage('contact', queryString, 'contact');
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(11).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
mp: genMp(),
|
||||||
|
name: genName(),
|
||||||
|
user_name: Random.name(false),
|
||||||
|
company: Random.title(1, 3),
|
||||||
|
email: Random.email(),
|
||||||
|
tel: Random.natural(10000000000, 16000000000)
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.contact = res;
|
||||||
|
return genPage('contact', queryString, 'company');
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region pricing
|
||||||
|
|
||||||
|
function pricingList(): any {
|
||||||
|
if (DATA.pricing) {
|
||||||
|
return DATA.pricing;
|
||||||
|
}
|
||||||
|
const res: any = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Basic',
|
||||||
|
icon: 'shop',
|
||||||
|
mo: 12,
|
||||||
|
yr: 12 * 12,
|
||||||
|
user: 5,
|
||||||
|
project: 5,
|
||||||
|
space: '100GB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Company',
|
||||||
|
icon: 'bank',
|
||||||
|
mo: 25,
|
||||||
|
yr: 25 * 12,
|
||||||
|
user: 30,
|
||||||
|
project: 150,
|
||||||
|
space: '300GB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Enterprise',
|
||||||
|
icon: 'crown',
|
||||||
|
mo: 50,
|
||||||
|
yr: 50 * 12,
|
||||||
|
user: -1,
|
||||||
|
project: -1,
|
||||||
|
space: '1000GB'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
faq: [
|
||||||
|
{
|
||||||
|
q: 'Can I cancel at anytime?',
|
||||||
|
a: 'Yes, you can cancel anytime no questions are asked while you cancel but we would highly appreciate if you will give us some feedback.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'My team has credits. How do we use them?',
|
||||||
|
a: 'Once your team signs up for a subscription plan. enim eiusmod high life accusamus eoset dignissimos.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: `How does Front's pricing work?`,
|
||||||
|
a: 'Our subscriptions are tiered. based on the number of people enim eiusmod high life accusamus terry richardson ad squid.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'How secure is Front?',
|
||||||
|
a: 'Protecting the data you trust to Front is our first priority. at vero eoset dignissimos ducimus qui blanditiis.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Do you offer discounts?',
|
||||||
|
a: `We've built in discounts at each tier for teams. leggings occaecat craft beer farm-to-table. raw denim aesthetic synth nesciunt.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'What is your refund policy?',
|
||||||
|
a: 'We do not offer refunds apart leggings occaecat craft beer farm-to-table. raw leggings occaecat craft.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
// labels
|
||||||
|
DATA.pricing = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region client
|
||||||
|
|
||||||
|
function clientList(queryString: any): any {
|
||||||
|
if (DATA.client) {
|
||||||
|
return genPage('client', queryString, 'company');
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(11).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
mp: genMp(),
|
||||||
|
name: genName(),
|
||||||
|
user_name: Random.name(false),
|
||||||
|
company: Random.title(1, 3),
|
||||||
|
email: Random.email(),
|
||||||
|
tel: Random.natural(10000000000, 16000000000),
|
||||||
|
status: idx % 2 ? genArr(['active', 'pending', 'progress']) : 'active'
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.client = res;
|
||||||
|
return genPage('client', queryString, 'company');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientGet(id: number): any {
|
||||||
|
const idx = getIdx('client', id || 0);
|
||||||
|
const item = {
|
||||||
|
...DATA.client[idx],
|
||||||
|
messages: new Array(Random.natural(0, 5)).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
time: `${Random.natural(1, 6)} day ago`,
|
||||||
|
message: Random.paragraph(1, 1)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region course
|
||||||
|
|
||||||
|
function courseList(queryString: any): any {
|
||||||
|
if (DATA.course) {
|
||||||
|
return genPage('course', queryString, 'title');
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(10).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
mp: genBigMp(),
|
||||||
|
tags: genTag(),
|
||||||
|
price: idx === 0 ? 0 : Random.natural(0, 100),
|
||||||
|
title: Random.title(2, 5),
|
||||||
|
remark: Random.paragraph(1, 1),
|
||||||
|
star: genArr([4, 4.5, 5]),
|
||||||
|
hour: Random.natural(10, 99)
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.course = res;
|
||||||
|
return genPage('course', queryString, 'title');
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region chat
|
||||||
|
|
||||||
|
function chatList(): any {
|
||||||
|
if (DATA.chat) {
|
||||||
|
return DATA.chat;
|
||||||
|
}
|
||||||
|
const res: any = {
|
||||||
|
users: new Array(10).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
mp: genMp(),
|
||||||
|
name: genName(),
|
||||||
|
count: idx % 3 === 0 ? Random.natural(0, 5) : 0,
|
||||||
|
online: idx < 5 ? true : false,
|
||||||
|
unread: Random.boolean() && Random.boolean() ? Random.natural(0, 5) : 0
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
// labels
|
||||||
|
DATA.chat = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region gallery
|
||||||
|
|
||||||
|
function galleryList(): any {
|
||||||
|
if (DATA.gallery) {
|
||||||
|
return DATA.gallery;
|
||||||
|
}
|
||||||
|
const res: any = new Array(16).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
url: genBigMp(),
|
||||||
|
title: Random.title(),
|
||||||
|
type: genArr(['Nature', 'Beach', 'Animal', 'Other'])
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.gallery = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region article
|
||||||
|
|
||||||
|
function articleList(queryString: any): any {
|
||||||
|
if (DATA.article) {
|
||||||
|
return genPage('article', queryString, 'title');
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(11).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
mp: genMp(),
|
||||||
|
name: genName(),
|
||||||
|
title: Random.ctitle(),
|
||||||
|
likes: Random.natural(0, 1000),
|
||||||
|
comments: Random.natural(0, 1000),
|
||||||
|
created: Random.now('day'),
|
||||||
|
status: idx % 2 ? genArr(['Published', 'Draft', 'Deleted']) : 'Published'
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.article = res;
|
||||||
|
return genPage('article', queryString, 'title');
|
||||||
|
}
|
||||||
|
|
||||||
|
function articleGet(id: number): any {
|
||||||
|
const idx = getIdx('article', id || 0);
|
||||||
|
const item = { ...DATA.article[idx] };
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region voting
|
||||||
|
|
||||||
|
function votingList(queryString: any): any {
|
||||||
|
if (DATA.voting) {
|
||||||
|
return genPage('voting', queryString, 'title');
|
||||||
|
}
|
||||||
|
const res: any[] = new Array(11).fill({}).map((v, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
voting: Random.integer(-10, 10000),
|
||||||
|
title: Random.title(5, 10),
|
||||||
|
content: Random.paragraph(),
|
||||||
|
likes: Random.natural(0, 1000),
|
||||||
|
created: Random.now('day')
|
||||||
|
}));
|
||||||
|
// labels
|
||||||
|
DATA.voting = res;
|
||||||
|
return genPage('voting', queryString, 'title');
|
||||||
|
}
|
||||||
|
|
||||||
|
function votingSave(req: any): any {
|
||||||
|
const idx = getIdx('voting', req.id || 0);
|
||||||
|
DATA.voting[idx].value += req.voting;
|
||||||
|
return { msg: 'ok', item: DATA.voting[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region voting
|
||||||
|
|
||||||
|
function invoice(): any {
|
||||||
|
if (DATA.invoice) {
|
||||||
|
return deepCopy(DATA.invoice);
|
||||||
|
}
|
||||||
|
const res: any = {
|
||||||
|
id: Random.integer(10000, 99999),
|
||||||
|
zone: 'Mountain View, CA 94043 United States',
|
||||||
|
address: '1600 Amphitheatre Parkway',
|
||||||
|
tel: '15900000000, +86 (021) 99999999',
|
||||||
|
date: genData(0),
|
||||||
|
to: {
|
||||||
|
company: 'XXX Company LTD',
|
||||||
|
zone: 'Mountain View, CA 94043 United States',
|
||||||
|
address: '1600 Amphitheatre Parkway',
|
||||||
|
tel: '15900000000, +86 (021) 99999999',
|
||||||
|
email: 'cipchk@qq.com'
|
||||||
|
},
|
||||||
|
payment: {
|
||||||
|
total: 0,
|
||||||
|
bank: 'XXXX Bank',
|
||||||
|
country: 'China',
|
||||||
|
city: 'Shang Hai',
|
||||||
|
address: 'xxx xxxx',
|
||||||
|
code: '012384'
|
||||||
|
},
|
||||||
|
wares: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: Random.title(),
|
||||||
|
remark: Random.title(),
|
||||||
|
price: +Random.float(0.1, 999).toFixed(2),
|
||||||
|
num: +Random.natural(1, 10)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: Random.title(),
|
||||||
|
remark: Random.title(),
|
||||||
|
price: +Random.float(0.1, 999).toFixed(2),
|
||||||
|
num: +Random.natural(1, 10)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
note: Random.paragraph()
|
||||||
|
};
|
||||||
|
// total
|
||||||
|
res.wares.forEach((i: any) => {
|
||||||
|
i.total = +(i.price * i.num).toFixed(2);
|
||||||
|
});
|
||||||
|
res.tax_rate = 0.2;
|
||||||
|
res.total = +(res.wares as any[]).reduce((a, b) => (a += b.total), 0).toFixed(2);
|
||||||
|
res.tax = +(res.total * 0.2).toFixed(2);
|
||||||
|
res.payment_total = +(res.total + res.tax).toFixed(2);
|
||||||
|
DATA.invoice = res;
|
||||||
|
|
||||||
|
return deepCopy(DATA.invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region faq
|
||||||
|
|
||||||
|
function faq(): any {
|
||||||
|
if (DATA.faq) {
|
||||||
|
return deepCopy(DATA.faq);
|
||||||
|
}
|
||||||
|
DATA.faq = new Array(6).fill({}).map((v, idx) => ({
|
||||||
|
title: `Knowledge ${idx + 1}`,
|
||||||
|
icon: 'question-circle',
|
||||||
|
primary: idx < 3,
|
||||||
|
remark: 'The list of FAQ',
|
||||||
|
children: new Array(Random.natural(3, 6)).fill({}).map((v, idx) => ({
|
||||||
|
active: idx === 0,
|
||||||
|
q: 'What is a product key?',
|
||||||
|
a: genHtml()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
return deepCopy(DATA.faq);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region calendar
|
||||||
|
|
||||||
|
function calendar(req: any): any {
|
||||||
|
const cur = new Date(+req.time || new Date());
|
||||||
|
const startDate = startOfMonth(cur);
|
||||||
|
const max = getDaysInMonth(cur);
|
||||||
|
const start = format(startDate, 'yyyy-MM');
|
||||||
|
const now = format(new Date(), 'yyyy-MM-dd');
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'All Day Event',
|
||||||
|
start: `${start}-1`,
|
||||||
|
className: 'fc-event-danger fc-event-fill-warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Reporting',
|
||||||
|
start: `${start}-7T13:30:00`,
|
||||||
|
end: `${start}-7`,
|
||||||
|
className: 'fc-event-success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Company Trip',
|
||||||
|
start: `${start}-12`,
|
||||||
|
end: `${start}-14`,
|
||||||
|
className: 'fc-event-primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Product Release',
|
||||||
|
start: `${start}-3`,
|
||||||
|
end: `${start}-5`,
|
||||||
|
className: 'fc-event-light fc-event-fill-primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'Repeating Event',
|
||||||
|
start: `${start}-09T16:00:00`,
|
||||||
|
className: 'fc-event-purple'
|
||||||
|
},
|
||||||
|
{ id: 6, title: 'Repeating Event', start: `${start}-11T16:00:00` },
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: 'Meeting',
|
||||||
|
start: `${now}T10:00:00`,
|
||||||
|
end: `${now}T11:30:00`
|
||||||
|
},
|
||||||
|
{ id: 8, title: 'Lunch', start: `${now}T12:00:00` },
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: 'Meeting',
|
||||||
|
start: `${now}T14:00:00`,
|
||||||
|
className: 'fc-event-warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: 'Happy Hour',
|
||||||
|
start: `${now}T17:30:00`,
|
||||||
|
className: 'fc-event-success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'Dinner',
|
||||||
|
start: `${now}T18:30:00`,
|
||||||
|
className: 'fc-event-fill-danger fc-event-light'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'Birthday Party',
|
||||||
|
start: `${now}T21:00:00`,
|
||||||
|
className: 'fc-event-primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
title: 'Click for Ng Alain',
|
||||||
|
url: 'https://ng-alain.com',
|
||||||
|
start: `${start}-27`,
|
||||||
|
className: 'fc-event-fill-success fc-event-light'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
title: 'Repeating Event',
|
||||||
|
start: `${start}-09T08:00:00`,
|
||||||
|
className: 'fc-event-magenta'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region quick
|
||||||
|
|
||||||
|
function quick(): any {
|
||||||
|
if (DATA.quick) {
|
||||||
|
return deepCopy(DATA.quick);
|
||||||
|
}
|
||||||
|
DATA.quick = {
|
||||||
|
notifications: new Array(6).fill({}).map(() => ({
|
||||||
|
dot: genArr([
|
||||||
|
{ icon: 'warning', bg: 'error' },
|
||||||
|
{ icon: 'pie-chart', bg: 'primary' },
|
||||||
|
{ icon: 'message', bg: 'success' },
|
||||||
|
{ icon: 'bell', bg: 'cyan' }
|
||||||
|
]),
|
||||||
|
content: Random.title(5, 15),
|
||||||
|
time: genArr(['01:01 PM', '09:00 AM', '18:56']),
|
||||||
|
tags: genTag().join(',')
|
||||||
|
})),
|
||||||
|
actions: new Array(6).fill({}).map(() => ({
|
||||||
|
bg: genColorName(),
|
||||||
|
title: Random.title(2, 3),
|
||||||
|
content: Random.title(5, 15)
|
||||||
|
})),
|
||||||
|
settings: {
|
||||||
|
notification: true,
|
||||||
|
audit_log: false,
|
||||||
|
new_order: false,
|
||||||
|
tracking_order: false,
|
||||||
|
reports_order: true,
|
||||||
|
new_customer: true,
|
||||||
|
reporting_customer: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return deepCopy(DATA.quick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region dd
|
||||||
|
|
||||||
|
function ddList(): any {
|
||||||
|
if (DATA.dd) {
|
||||||
|
return DATA.dd;
|
||||||
|
}
|
||||||
|
DATA.dd = [
|
||||||
|
{
|
||||||
|
name: 'total-sales',
|
||||||
|
title: '总数-总销售额',
|
||||||
|
enabled: true,
|
||||||
|
params: { title: '总销售额', total: 100, week: 10, day: 11, daySales: 1000 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total-sales',
|
||||||
|
title: '总数-总订单量',
|
||||||
|
enabled: false,
|
||||||
|
params: { title: '总订单量', total: 5500, week: 320, day: 5, daySales: 5422 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total-sales',
|
||||||
|
title: '总数-总用户量',
|
||||||
|
enabled: false,
|
||||||
|
params: { title: '总用户量', total: 500, week: 80, day: 23, daySales: 6666 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total-sales',
|
||||||
|
title: '总数-其他',
|
||||||
|
enabled: false,
|
||||||
|
params: { title: '其他', total: 200, week: 80, day: 23, daySales: 77777 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'visits',
|
||||||
|
title: '访问量',
|
||||||
|
enabled: true,
|
||||||
|
params: { url: '/chart' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'effect',
|
||||||
|
title: '访问量',
|
||||||
|
enabled: true,
|
||||||
|
params: { percent: 66, week: 11, day: 23 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gauge',
|
||||||
|
title: '核销率',
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'radar',
|
||||||
|
title: '指数',
|
||||||
|
enabled: true,
|
||||||
|
params: { title: '贡献指数', url: '/chart' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'activities',
|
||||||
|
title: '动态',
|
||||||
|
enabled: true,
|
||||||
|
params: { title: '动态', url: '/api/activities' }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return DATA.dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ddSave(req: any): any {
|
||||||
|
DATA.dd = req;
|
||||||
|
return { msg: 'ok' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
export const OTHERS = {
|
||||||
|
'/kanban-board': kanbanList(),
|
||||||
|
'DELETE /kanban-board': (req: MockRequest) => del('kanban', req.queryString),
|
||||||
|
'/task': taskList(),
|
||||||
|
'DELETE /task': (req: MockRequest) => del('task', req.queryString),
|
||||||
|
'/email': (req: MockRequest) => emailList(req.queryString),
|
||||||
|
'/email/:id': (req: MockRequest) => emailGet(+req.params.id),
|
||||||
|
'DELETE /email': (req: MockRequest) => del('email', req.queryString),
|
||||||
|
'/project': projectList(),
|
||||||
|
'/project/:id': (req: MockRequest) => projectGet(+req.params.id),
|
||||||
|
'DELETE /project': (req: MockRequest) => del('project', req.queryString),
|
||||||
|
'/client': (req: MockRequest) => clientList(req.queryString),
|
||||||
|
'/client/:id': (req: MockRequest) => clientGet(+req.params.id),
|
||||||
|
'/contact': (req: MockRequest) => contactList(req.queryString),
|
||||||
|
'DELETE /contact': (req: MockRequest) => del('contact', req.queryString),
|
||||||
|
'/pricing': () => pricingList(),
|
||||||
|
'/billing': (req: MockRequest) => billingList(req.queryString),
|
||||||
|
'/course': (req: MockRequest) => courseList(req.queryString),
|
||||||
|
'/chat': () => chatList(),
|
||||||
|
'/chat/message': () => {
|
||||||
|
const item: any = {
|
||||||
|
type: Random.boolean() && Random.boolean() ? 'image' : 'text',
|
||||||
|
dir: Random.boolean() ? 'left' : 'right'
|
||||||
|
};
|
||||||
|
item.msg = item.type === 'text' ? Random.paragraph(1, 1) : genBigMp();
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
'/gallery': () => galleryList(),
|
||||||
|
'/article': (req: MockRequest) => articleList(req.queryString),
|
||||||
|
'/article/:id': (req: MockRequest) => articleGet(+req.params.id),
|
||||||
|
'DELETE /article': (req: MockRequest) => del('article', req.queryString),
|
||||||
|
'POST /article': (req: MockRequest) => save('article', req.body),
|
||||||
|
'/voting': (req: MockRequest) => votingList(req.queryString),
|
||||||
|
'POST /voting': (req: MockRequest) => votingSave(req.body),
|
||||||
|
'/invoice': () => invoice(),
|
||||||
|
'/faq': () => faq(),
|
||||||
|
'/calendar': (req: MockRequest) => calendar(req.queryString),
|
||||||
|
'/quick': () => quick(),
|
||||||
|
'/dd': () => ddList(),
|
||||||
|
'POST /dd': (req: MockRequest) => ddSave(req.body)
|
||||||
|
};
|
||||||
58
_mock/_permission.ts
Normal file
58
_mock/_permission.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
id: number;
|
||||||
|
parent_id: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionData: Permission[] = [
|
||||||
|
{ id: 1, parent_id: 0, text: '超级权限' },
|
||||||
|
{ id: 2, parent_id: 0, text: '系统' },
|
||||||
|
{ id: 3, parent_id: 2, text: '员工' },
|
||||||
|
{ id: 4, parent_id: 2, text: '菜单' },
|
||||||
|
{ id: 5, parent_id: 2, text: '权限' },
|
||||||
|
{ id: 6, parent_id: 0, text: '订单' },
|
||||||
|
{ id: 7, parent_id: 6, text: '列表' },
|
||||||
|
{ id: 8, parent_id: 6, text: '导入订单' },
|
||||||
|
{ id: 9, parent_id: 6, text: '打印快递单' },
|
||||||
|
{ id: 10, parent_id: 9, text: '批量打印' },
|
||||||
|
{ id: 11, parent_id: 6, text: '发货' },
|
||||||
|
{ id: 12, parent_id: 11, text: '批量发货' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = PermissionData.findIndex((w) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PERMISSION = {
|
||||||
|
'/permission': () => deepCopy(PermissionData),
|
||||||
|
'POST /permission': (req: MockRequest) => {
|
||||||
|
const id = req.body.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(id);
|
||||||
|
PermissionData[idx] = { ...PermissionData[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: PermissionData[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...req.body, id: PermissionData.sort((a, b) => b.id - a.id)[0].id + 1 };
|
||||||
|
PermissionData.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'DELETE /permission/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
PermissionData.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
'POST /permission/move': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.from || 0);
|
||||||
|
PermissionData[idx].parent_id = req.body.to || 0;
|
||||||
|
return { msg: 'ok', item: PermissionData[idx] };
|
||||||
|
},
|
||||||
|
};
|
||||||
61
_mock/_pois.ts
Normal file
61
_mock/_pois.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
export const POIS = {
|
||||||
|
'/pois': {
|
||||||
|
total: 2,
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 10000,
|
||||||
|
user_id: 1,
|
||||||
|
name: '测试品牌',
|
||||||
|
branch_name: '测试分店',
|
||||||
|
geo: 310105,
|
||||||
|
country: '中国',
|
||||||
|
province: '上海',
|
||||||
|
city: '上海市',
|
||||||
|
district: '长宁区',
|
||||||
|
address: '中山公园',
|
||||||
|
tel: '15900000000',
|
||||||
|
categories: '美食,粤菜,湛江菜',
|
||||||
|
lng: 121.41707989151003,
|
||||||
|
lat: 31.218656214644792,
|
||||||
|
recommend: '推荐品',
|
||||||
|
special: '特色服务',
|
||||||
|
introduction: '商户简介',
|
||||||
|
open_time: '营业时间',
|
||||||
|
avg_price: 260,
|
||||||
|
reason: null,
|
||||||
|
status: 1,
|
||||||
|
status_str: '待审核',
|
||||||
|
status_wx: 1,
|
||||||
|
modified: 1505826527288,
|
||||||
|
created: 1505826527288,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10001,
|
||||||
|
user_id: 2,
|
||||||
|
name: '测试品牌2',
|
||||||
|
branch_name: '测试分店2',
|
||||||
|
geo: 310105,
|
||||||
|
country: '中国',
|
||||||
|
province: '上海',
|
||||||
|
city: '上海市',
|
||||||
|
district: '长宁区',
|
||||||
|
address: '中山公园',
|
||||||
|
tel: '15900000000',
|
||||||
|
categories: '美食,粤菜,湛江菜',
|
||||||
|
lng: 121.41707989151003,
|
||||||
|
lat: 31.218656214644792,
|
||||||
|
recommend: '推荐品',
|
||||||
|
special: '特色服务',
|
||||||
|
introduction: '商户简介',
|
||||||
|
open_time: '营业时间',
|
||||||
|
avg_price: 260,
|
||||||
|
reason: null,
|
||||||
|
status: 1,
|
||||||
|
status_str: '待审核',
|
||||||
|
status_wx: 1,
|
||||||
|
modified: 1505826527288,
|
||||||
|
created: 1505826527288,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
152
_mock/_profile.ts
Normal file
152
_mock/_profile.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
const basicGoods = [
|
||||||
|
{
|
||||||
|
id: '1234561',
|
||||||
|
name: '矿泉水 550ml',
|
||||||
|
barcode: '12421432143214321',
|
||||||
|
price: '2.00',
|
||||||
|
num: '1',
|
||||||
|
amount: '2.00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1234562',
|
||||||
|
name: '凉茶 300ml',
|
||||||
|
barcode: '12421432143214322',
|
||||||
|
price: '3.00',
|
||||||
|
num: '2',
|
||||||
|
amount: '6.00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1234563',
|
||||||
|
name: '好吃的薯片',
|
||||||
|
barcode: '12421432143214323',
|
||||||
|
price: '7.00',
|
||||||
|
num: '4',
|
||||||
|
amount: '28.00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1234564',
|
||||||
|
name: '特别好吃的蛋卷',
|
||||||
|
barcode: '12421432143214324',
|
||||||
|
price: '8.50',
|
||||||
|
num: '3',
|
||||||
|
amount: '25.50',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const basicProgress = [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
time: '2017-10-01 14:10',
|
||||||
|
rate: '联系客户',
|
||||||
|
status: 'processing',
|
||||||
|
operator: '取货员 ID1234',
|
||||||
|
cost: '5mins',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
time: '2017-10-01 14:05',
|
||||||
|
rate: '取货员出发',
|
||||||
|
status: 'success',
|
||||||
|
operator: '取货员 ID1234',
|
||||||
|
cost: '1h',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
time: '2017-10-01 13:05',
|
||||||
|
rate: '取货员接单',
|
||||||
|
status: 'success',
|
||||||
|
operator: '取货员 ID1234',
|
||||||
|
cost: '5mins',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4',
|
||||||
|
time: '2017-10-01 13:00',
|
||||||
|
rate: '申请审批通过',
|
||||||
|
status: 'success',
|
||||||
|
operator: '系统',
|
||||||
|
cost: '1h',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '5',
|
||||||
|
time: '2017-10-01 12:00',
|
||||||
|
rate: '发起退货申请',
|
||||||
|
status: 'success',
|
||||||
|
operator: '用户',
|
||||||
|
cost: '5mins',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const advancedOperation1 = [
|
||||||
|
{
|
||||||
|
key: 'op1',
|
||||||
|
type: '订购关系生效',
|
||||||
|
name: '曲丽丽',
|
||||||
|
status: 'agree',
|
||||||
|
updatedAt: '2017-10-03 19:23:12',
|
||||||
|
memo: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'op2',
|
||||||
|
type: '财务复审',
|
||||||
|
name: '付小小',
|
||||||
|
status: 'reject',
|
||||||
|
updatedAt: '2017-10-03 19:23:12',
|
||||||
|
memo: '不通过原因',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'op3',
|
||||||
|
type: '部门初审',
|
||||||
|
name: '周毛毛',
|
||||||
|
status: 'agree',
|
||||||
|
updatedAt: '2017-10-03 19:23:12',
|
||||||
|
memo: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'op4',
|
||||||
|
type: '提交订单',
|
||||||
|
name: '林东东',
|
||||||
|
status: 'agree',
|
||||||
|
updatedAt: '2017-10-03 19:23:12',
|
||||||
|
memo: '很棒',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'op5',
|
||||||
|
type: '创建订单',
|
||||||
|
name: '汗牙牙',
|
||||||
|
status: 'agree',
|
||||||
|
updatedAt: '2017-10-03 19:23:12',
|
||||||
|
memo: '-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const advancedOperation2 = [
|
||||||
|
{
|
||||||
|
key: 'op1',
|
||||||
|
type: '订购关系生效',
|
||||||
|
name: '曲丽丽',
|
||||||
|
status: 'agree',
|
||||||
|
updatedAt: '2017-10-03 19:23:12',
|
||||||
|
memo: '-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const advancedOperation3 = [
|
||||||
|
{
|
||||||
|
key: 'op1',
|
||||||
|
type: '创建订单',
|
||||||
|
name: '汗牙牙',
|
||||||
|
status: 'agree',
|
||||||
|
updatedAt: '2017-10-03 19:23:12',
|
||||||
|
memo: '-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PROFILES = {
|
||||||
|
'GET /profile/progress': basicProgress,
|
||||||
|
'GET /profile/goods': basicGoods,
|
||||||
|
'GET /profile/advanced': {
|
||||||
|
advancedOperation1,
|
||||||
|
advancedOperation2,
|
||||||
|
advancedOperation3,
|
||||||
|
},
|
||||||
|
};
|
||||||
73
_mock/_role.ts
Normal file
73
_mock/_role.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
id: number;
|
||||||
|
parent_id: number;
|
||||||
|
text: string;
|
||||||
|
permission: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RuleData: Permission[] = [
|
||||||
|
{ id: 1, parent_id: 0, text: '管理', permission: [1] },
|
||||||
|
{ id: 2, parent_id: 0, text: '仓储', permission: [] },
|
||||||
|
{ id: 3, parent_id: 0, text: '营销', permission: [] },
|
||||||
|
{ id: 4, parent_id: 0, text: '第三方', permission: [7] },
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
parent_id: 2,
|
||||||
|
text: '仓储经理',
|
||||||
|
permission: [6, 7, 8, 9, 10, 11, 12],
|
||||||
|
},
|
||||||
|
{ id: 6, parent_id: 2, text: '出库员', permission: [6, 7, 9, 10, 11, 12] },
|
||||||
|
{ id: 7, parent_id: 2, text: '入库员', permission: [6, 7] },
|
||||||
|
{ id: 8, parent_id: 3, text: '市场经理', permission: [6, 7] },
|
||||||
|
{ id: 9, parent_id: 3, text: '销售人员', permission: [6, 7] },
|
||||||
|
];
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(RuleData);
|
||||||
|
if (params.q) {
|
||||||
|
ret = ret.filter((data: any) => data.text.indexOf(params.q) > -1);
|
||||||
|
}
|
||||||
|
const p = +(params.permission || '0');
|
||||||
|
if (p > 0) {
|
||||||
|
ret = ret.filter((data: any) => data.permission.includes(p));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = RuleData.findIndex((w) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROLES_PRO = {
|
||||||
|
'/role': (req: MockRequest) => get(req.queryString),
|
||||||
|
'POST /role': (req: MockRequest) => {
|
||||||
|
const id = req.body.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(id);
|
||||||
|
RuleData[idx] = { ...RuleData[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: RuleData[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...req.body, id: RuleData.sort((a, b) => b.id - a.id)[0].id + 1 };
|
||||||
|
RuleData.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'DELETE /role/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
RuleData.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
'POST /role/move': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.from || 0);
|
||||||
|
RuleData[idx].parent_id = req.body.to || 0;
|
||||||
|
return { msg: 'ok', item: RuleData[idx] };
|
||||||
|
},
|
||||||
|
};
|
||||||
82
_mock/_rule.ts
Normal file
82
_mock/_rule.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { HttpRequest } from '@angular/common/http';
|
||||||
|
import { MockRequest } from '@delon/mock';
|
||||||
|
|
||||||
|
const list: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 46; i += 1) {
|
||||||
|
list.push({
|
||||||
|
key: i,
|
||||||
|
disabled: i % 6 === 0,
|
||||||
|
href: 'https://ant.design',
|
||||||
|
avatar: [
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||||
|
][i % 2],
|
||||||
|
no: `TradeCode ${i}`,
|
||||||
|
title: `一个任务名称 ${i}`,
|
||||||
|
owner: '曲丽丽',
|
||||||
|
description: '这是一段描述',
|
||||||
|
callNo: Math.floor(Math.random() * 1000),
|
||||||
|
status: Math.floor(Math.random() * 10) % 4,
|
||||||
|
updatedAt: new Date(`2017-07-${i < 18 ? '0' + (Math.floor(i / 2) + 1) : Math.floor(i / 2) + 1}`),
|
||||||
|
createdAt: new Date(`2017-07-${i < 18 ? '0' + (Math.floor(i / 2) + 1) : Math.floor(i / 2) + 1}`),
|
||||||
|
progress: Math.ceil(Math.random() * 100),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRule(params: any): any[] {
|
||||||
|
let ret = [...list];
|
||||||
|
if (params.sorter) {
|
||||||
|
const s = params.sorter.split('_');
|
||||||
|
ret = ret.sort((prev, next) => {
|
||||||
|
if (s[1] === 'descend') {
|
||||||
|
return next[s[0]] - prev[s[0]];
|
||||||
|
}
|
||||||
|
return prev[s[0]] - next[s[0]];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (params.statusList && params.statusList.length > 0) {
|
||||||
|
ret = ret.filter((data) => params.statusList.indexOf(data.status) > -1);
|
||||||
|
}
|
||||||
|
if (params.no) {
|
||||||
|
ret = ret.filter((data) => data.no.indexOf(params.no) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRule(nos: string): boolean {
|
||||||
|
nos.split(',').forEach((no) => {
|
||||||
|
const idx = list.findIndex((w) => w.no === no);
|
||||||
|
if (idx !== -1) {
|
||||||
|
list.splice(idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRule(description: string): void {
|
||||||
|
const i = Math.ceil(Math.random() * 10000);
|
||||||
|
list.unshift({
|
||||||
|
key: i,
|
||||||
|
href: 'https://ant.design',
|
||||||
|
avatar: [
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||||
|
][i % 2],
|
||||||
|
no: `TradeCode ${i}`,
|
||||||
|
title: `一个任务名称 ${i}`,
|
||||||
|
owner: '曲丽丽',
|
||||||
|
description,
|
||||||
|
callNo: Math.floor(Math.random() * 1000),
|
||||||
|
status: Math.floor(Math.random() * 10) % 2,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
progress: Math.ceil(Math.random() * 100),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RULES = {
|
||||||
|
'/rule': (req: MockRequest) => getRule(req.queryString),
|
||||||
|
'DELETE /rule': (req: MockRequest) => removeRule(req.queryString.nos),
|
||||||
|
'POST /rule': (req: MockRequest) => saveRule(req.body.description),
|
||||||
|
};
|
||||||
172
_mock/_trade.ts
Normal file
172
_mock/_trade.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
import { Random } from 'mockjs';
|
||||||
|
|
||||||
|
import { genMp, genName } from './utils';
|
||||||
|
|
||||||
|
interface Trade {
|
||||||
|
id: number;
|
||||||
|
buyer_id: number;
|
||||||
|
buyer_nick: string;
|
||||||
|
price: number;
|
||||||
|
discount_fee: number;
|
||||||
|
post_fee: number;
|
||||||
|
payment: number;
|
||||||
|
pay_time?: Date;
|
||||||
|
status: number;
|
||||||
|
status_str: string;
|
||||||
|
logistics_name?: string;
|
||||||
|
logistics_no?: string;
|
||||||
|
finished?: Date;
|
||||||
|
created: Date;
|
||||||
|
wares: TradeWare[];
|
||||||
|
memo?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TradeWare {
|
||||||
|
ware_id: number;
|
||||||
|
sku_id: number;
|
||||||
|
num: number;
|
||||||
|
price: number;
|
||||||
|
mp: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA: Trade[] = [];
|
||||||
|
const STATUS: { [key: string]: string } = {
|
||||||
|
CANCELED: '取消',
|
||||||
|
WAIT_BUYER_PAY: '等待买家付款',
|
||||||
|
WAIT_PAY_CONFIRM: '支付确认中',
|
||||||
|
WAIT_SELLER_STOCK_OUT: '等待出库',
|
||||||
|
WAIT_GOODS_RECEIVE_CONFIRM: '等待确认收货',
|
||||||
|
FINISHED: '交易成功'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 1; i <= 30; i += 1) {
|
||||||
|
const buyer_nick = genName();
|
||||||
|
const status: any = Object.keys(STATUS)[Random.natural(0, 5)];
|
||||||
|
let pay_time: Date | null = null;
|
||||||
|
let finished: Date | null = null;
|
||||||
|
if (status !== 'WAIT_BUYER_PAY' || status !== 'CANCELED') {
|
||||||
|
pay_time = new Date();
|
||||||
|
}
|
||||||
|
if (status !== 'FINISHED') {
|
||||||
|
finished = new Date();
|
||||||
|
}
|
||||||
|
const price = Random.natural(100, 10000);
|
||||||
|
const discount_fee = Random.natural(0, 100);
|
||||||
|
const post_fee = Random.natural(6, 20);
|
||||||
|
|
||||||
|
DATA.push({
|
||||||
|
id: 10000 * i,
|
||||||
|
buyer_id: Random.natural(10000, 100000),
|
||||||
|
buyer_nick,
|
||||||
|
buyer_tel: Random.natural(10000000000, 19999999999),
|
||||||
|
buyer_adr: Random.city(true),
|
||||||
|
buyer_message: Random.title(5, 20),
|
||||||
|
price,
|
||||||
|
discount_fee,
|
||||||
|
post_fee,
|
||||||
|
payment: price - discount_fee + post_fee,
|
||||||
|
pay_time,
|
||||||
|
finished,
|
||||||
|
status,
|
||||||
|
status_str: STATUS[status],
|
||||||
|
logistics_name: '',
|
||||||
|
logistics_no: '',
|
||||||
|
created: new Date(),
|
||||||
|
wares: genWare(Random.natural(1, 5))
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
function genWare(count: number): TradeWare[] {
|
||||||
|
return new Array(count).fill({}).map((v, idx) => ({
|
||||||
|
ware_id: Random.natural(10000, 10020),
|
||||||
|
sku_id: (idx + 1) * 1000,
|
||||||
|
num: Random.natural(1, 10),
|
||||||
|
price: Random.natural(10, 500),
|
||||||
|
mp: genMp(),
|
||||||
|
title: Random.ctitle(5, 10)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(DATA);
|
||||||
|
if (params.sorter) {
|
||||||
|
const s = params.sorter.split('_');
|
||||||
|
ret = ret.sort((prev: any, next: any) => {
|
||||||
|
if (s[1] === 'descend') {
|
||||||
|
return next[s[0]] - prev[s[0]];
|
||||||
|
}
|
||||||
|
return prev[s[0]] - next[s[0]];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (params.id) {
|
||||||
|
ret = ret.filter((data: any) => data.id == params.id);
|
||||||
|
}
|
||||||
|
if (params.statusList && params.statusList.length > 0) {
|
||||||
|
ret = ret.filter((data: any) => params.statusList.indexOf(data.status) > -1);
|
||||||
|
}
|
||||||
|
if (params.ware_id) {
|
||||||
|
ret = ret.filter((data: any) => data.wares.find((w: any) => w.ware_id == params.ware_id));
|
||||||
|
}
|
||||||
|
if (params.buyer_nick) {
|
||||||
|
ret = ret.filter((data: any) => data.buyer_nick.indexOf(params.buyer_nick) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = DATA.findIndex(w => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TRADES = {
|
||||||
|
'/trade': (req: MockRequest) => {
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
const data = get(req.queryString);
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'POST /trade': (req: MockRequest) => {
|
||||||
|
const id = req.body.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(id);
|
||||||
|
DATA[idx] = { ...DATA[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...req.body, id: DATA.sort((a, b) => b.id - a.id)[0].id + 1 };
|
||||||
|
DATA.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'/trade/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
const item = { ...DATA[idx] };
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
'DELETE /trade/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
DATA.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
'POST /trade/status': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.id || 0);
|
||||||
|
DATA[idx].status = req.body.status;
|
||||||
|
DATA[idx].status_str = STATUS[req.body.status];
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
},
|
||||||
|
'POST /trade/memo': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.id || 0);
|
||||||
|
DATA[idx].memo = req.body.memo;
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
}
|
||||||
|
};
|
||||||
91
_mock/_user-pro.ts
Normal file
91
_mock/_user-pro.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
import { Random } from 'mockjs';
|
||||||
|
import { genName } from './utils';
|
||||||
|
import { RuleData } from './_role';
|
||||||
|
|
||||||
|
interface UserPro {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
userName: string;
|
||||||
|
email: string;
|
||||||
|
verified: boolean;
|
||||||
|
status: 'active' | 'banned' | 'deleted';
|
||||||
|
role?: any;
|
||||||
|
permission?: any[];
|
||||||
|
created: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA: UserPro[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 20; i += 1) {
|
||||||
|
const name = genName();
|
||||||
|
DATA.push({
|
||||||
|
id: i,
|
||||||
|
name,
|
||||||
|
userName: `user name ${i}`,
|
||||||
|
email: `${name}` + ['@qq.com', '@gmail.com', '@163.com'][Math.floor(Math.random() * 10) % 3],
|
||||||
|
verified: [true, false][Math.floor(Math.random() * 10) % 2],
|
||||||
|
status: ['active', 'banned', 'deleted'][Math.floor(Math.random() * 10) % 3] as any,
|
||||||
|
role: deepCopy(RuleData[Random.natural(0, RuleData.length - 1)]),
|
||||||
|
permission: [],
|
||||||
|
created: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(DATA);
|
||||||
|
if (params.q) {
|
||||||
|
ret = ret.filter((data: any) => data.name.indexOf(params.q) > -1);
|
||||||
|
}
|
||||||
|
if (params.email) {
|
||||||
|
ret = ret.filter((data: any) => data.email.indexOf(params.email) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = DATA.findIndex((w) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const USERS_PRO = {
|
||||||
|
'/user-pro': (req: MockRequest) => {
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
const data = get(req.queryString);
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'POST /user-pro': (req: MockRequest) => {
|
||||||
|
const id = req.body.id || 0;
|
||||||
|
// fix role data
|
||||||
|
req.body.role = RuleData.find((w) => w.id === req.body.role.id);
|
||||||
|
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(id);
|
||||||
|
DATA[idx] = { ...DATA[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...req.body, id: DATA.sort((a, b) => b.id - a.id)[0].id + 1 };
|
||||||
|
DATA.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'/user-pro/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
const item = { ...DATA[idx], ...req.body };
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
'DELETE /user-pro/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
DATA.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
};
|
||||||
122
_mock/_user.ts
Normal file
122
_mock/_user.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { MockRequest } from '@delon/mock';
|
||||||
|
|
||||||
|
const list: any[] = [];
|
||||||
|
const total = 50;
|
||||||
|
|
||||||
|
for (let i = 0; i < total; i += 1) {
|
||||||
|
list.push({
|
||||||
|
id: i + 1,
|
||||||
|
disabled: i % 6 === 0,
|
||||||
|
href: 'https://ant.design',
|
||||||
|
avatar: [
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||||
|
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||||
|
][i % 2],
|
||||||
|
no: `TradeCode ${i}`,
|
||||||
|
title: `一个任务名称 ${i}`,
|
||||||
|
owner: '曲丽丽',
|
||||||
|
description: '这是一段描述',
|
||||||
|
callNo: Math.floor(Math.random() * 1000),
|
||||||
|
status: Math.floor(Math.random() * 10) % 4,
|
||||||
|
updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
|
||||||
|
createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
|
||||||
|
progress: Math.ceil(Math.random() * 100),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genData(params: any): { total: number; list: any[] } {
|
||||||
|
let ret = [...list];
|
||||||
|
const pi = +params.pi;
|
||||||
|
const ps = +params.ps;
|
||||||
|
const start = (pi - 1) * ps;
|
||||||
|
|
||||||
|
if (params.no) {
|
||||||
|
ret = ret.filter((data) => data.no.indexOf(params.no) > -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total: ret.length, list: ret.slice(start, ps * pi) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveData(id: number, value: any): { msg: string } {
|
||||||
|
const item = list.find((w) => w.id === id);
|
||||||
|
if (!item) {
|
||||||
|
return { msg: '无效用户信息' };
|
||||||
|
}
|
||||||
|
Object.assign(item, value);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const USERS = {
|
||||||
|
'/user': (req: MockRequest) => genData(req.queryString),
|
||||||
|
'/user/:id': (req: MockRequest) => list.find((w) => w.id === +req.params.id),
|
||||||
|
'POST /user/:id': (req: MockRequest) => saveData(+req.params.id, req.body),
|
||||||
|
'/user/current': {
|
||||||
|
name: 'Cipchk',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
|
||||||
|
userid: '00000001',
|
||||||
|
email: 'cipchk@qq.com',
|
||||||
|
signature: '海纳百川,有容乃大',
|
||||||
|
title: '交互专家',
|
||||||
|
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
key: '0',
|
||||||
|
label: '很有想法的',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
label: '专注撩妹',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
label: '帅~',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
label: '通吃',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4',
|
||||||
|
label: '专职后端',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '5',
|
||||||
|
label: '海纳百川',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
notifyCount: 12,
|
||||||
|
country: 'China',
|
||||||
|
geographic: {
|
||||||
|
province: {
|
||||||
|
label: '上海',
|
||||||
|
key: '330000',
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
label: '市辖区',
|
||||||
|
key: '330100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
address: 'XX区XXX路 XX 号',
|
||||||
|
phone: '你猜-你猜你猜猜猜',
|
||||||
|
},
|
||||||
|
'POST /user/avatar': 'ok',
|
||||||
|
'POST /login/account': (req: MockRequest) => {
|
||||||
|
const data = req.body;
|
||||||
|
if (!(data.userName === 'admin' || data.userName === 'user') || data.password !== 'ng-alain.com') {
|
||||||
|
return { msg: `Invalid username or password(admin/ng-alain.com)` };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
msg: 'ok',
|
||||||
|
user: {
|
||||||
|
token: '123456789',
|
||||||
|
name: data.userName,
|
||||||
|
email: `${data.userName}@qq.com`,
|
||||||
|
id: 10000,
|
||||||
|
time: +new Date(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'POST /register': {
|
||||||
|
msg: 'ok',
|
||||||
|
},
|
||||||
|
};
|
||||||
163
_mock/_ware.ts
Normal file
163
_mock/_ware.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||||
|
import { deepCopy } from '@delon/util';
|
||||||
|
import { genMp } from './utils';
|
||||||
|
|
||||||
|
interface UserPro {
|
||||||
|
cid: number;
|
||||||
|
cname: string;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mp: string;
|
||||||
|
stock: number;
|
||||||
|
outer_id: string;
|
||||||
|
market_price: number;
|
||||||
|
price: number;
|
||||||
|
sale_num: number;
|
||||||
|
status: string;
|
||||||
|
modified: Date;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA: UserPro[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 20; i += 1) {
|
||||||
|
const name = ['HUAWEI Mate 20 Pro', '小米MAX3', 'IPhone X', 'Gree 8,000 BTU Portable Air Conditioner'][
|
||||||
|
Math.floor(Math.random() * 10) % 4
|
||||||
|
];
|
||||||
|
DATA.push({
|
||||||
|
cid: i * 1000,
|
||||||
|
cname: '',
|
||||||
|
id: i + 10000,
|
||||||
|
name,
|
||||||
|
mp: genMp(),
|
||||||
|
stock: Math.floor(Math.random() * 1000) % 1000,
|
||||||
|
outer_id: `S50-${Math.floor(Math.random() * 100) % 100}`,
|
||||||
|
market_price: Math.floor(Math.random() * 1000) % 1000,
|
||||||
|
price: Math.floor(Math.random() * 1000) % 1000,
|
||||||
|
sale_num: Math.floor(Math.random() * 200) % 200,
|
||||||
|
modified: new Date(),
|
||||||
|
status: ['CUSTORMER_DOWN', 'ON_SALE', 'AUDIT_AWAIT', 'DELETED'][Math.floor(Math.random() * 10) % 4],
|
||||||
|
brand: 1,
|
||||||
|
place: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(params: any): any {
|
||||||
|
let ret = deepCopy(DATA);
|
||||||
|
if (params.q) {
|
||||||
|
ret = ret.filter((data: any) => data.name.indexOf(params.q) > -1);
|
||||||
|
}
|
||||||
|
if (params.email) {
|
||||||
|
ret = ret.filter((data: any) => data.email.indexOf(params.email) > -1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdx(id: number): number {
|
||||||
|
id = +id;
|
||||||
|
const idx = DATA.findIndex((w) => w.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new MockStatusError(404);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WARES = {
|
||||||
|
'/ware': (req: MockRequest) => {
|
||||||
|
const pi = +(req.queryString.pi || 1);
|
||||||
|
const ps = +(req.queryString.ps || 10);
|
||||||
|
const data = get(req.queryString);
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
list: data.slice((pi - 1) * ps, pi * ps),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'POST /ware': (req: MockRequest) => {
|
||||||
|
const id = req.body.id || 0;
|
||||||
|
if (id > 0) {
|
||||||
|
const idx = getIdx(id);
|
||||||
|
DATA[idx] = { ...DATA[idx], ...req.body };
|
||||||
|
return { msg: 'ok', item: DATA[idx] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = { ...req.body, id: DATA.sort((a, b) => b.id - a.id)[0].id + 1 };
|
||||||
|
DATA.push(item);
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'/ware/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
const item = {
|
||||||
|
id: 0,
|
||||||
|
brand: 1,
|
||||||
|
is_7return: true,
|
||||||
|
prop: {
|
||||||
|
1: '是',
|
||||||
|
2: '24天',
|
||||||
|
3: '0.5克',
|
||||||
|
},
|
||||||
|
place: 1,
|
||||||
|
weight: 10,
|
||||||
|
skus: [
|
||||||
|
{
|
||||||
|
id: 10001,
|
||||||
|
attributes: '1:10',
|
||||||
|
names: [`红色`, `S`],
|
||||||
|
price: 1000,
|
||||||
|
stock: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10002,
|
||||||
|
attributes: '1:11',
|
||||||
|
names: [`红色`, `M`],
|
||||||
|
price: 1000,
|
||||||
|
stock: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10003,
|
||||||
|
attributes: '3:10',
|
||||||
|
names: [`蓝色1`, `S`],
|
||||||
|
price: 1000,
|
||||||
|
stock: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10004,
|
||||||
|
attributes: '3:11',
|
||||||
|
names: [`蓝色1`, `M`],
|
||||||
|
price: 1000,
|
||||||
|
stock: 13,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imgs: {
|
||||||
|
0: ['https://randomuser.me/api/portraits/lego/0.jpg'],
|
||||||
|
1: ['https://randomuser.me/api/portraits/lego/1.jpg'],
|
||||||
|
3: ['https://randomuser.me/api/portraits/lego/3.jpg'],
|
||||||
|
},
|
||||||
|
desc: `<p>Test</p>`,
|
||||||
|
...(DATA[idx] as any),
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
'DELETE /ware/:id': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.params.id || 0);
|
||||||
|
DATA.splice(idx, 1);
|
||||||
|
return { msg: 'ok' };
|
||||||
|
},
|
||||||
|
'POST /ware/status': (req: MockRequest) => {
|
||||||
|
const idx = getIdx(req.body.id || 0);
|
||||||
|
const item = DATA[idx];
|
||||||
|
item.status = req.body.status;
|
||||||
|
return { msg: 'ok', item };
|
||||||
|
},
|
||||||
|
'/ware/cat': [
|
||||||
|
{ id: 1, name: '颜色', value: '红色', color: '#f5222d', type: 'color' },
|
||||||
|
{ id: 2, name: '颜色', value: '绿色', color: '#a0d911', type: 'color' },
|
||||||
|
{ id: 3, name: '颜色', value: '蓝色', color: '#1890ff', type: 'color' },
|
||||||
|
{ id: 4, name: '颜色', value: '洋红', color: '#eb2f96', type: 'color' },
|
||||||
|
{ id: 10, name: '尺寸', value: 'S', type: 'size' },
|
||||||
|
{ id: 11, name: '尺寸', value: 'M', type: 'size' },
|
||||||
|
{ id: 12, name: '尺寸', value: 'L', type: 'size' },
|
||||||
|
{ id: 13, name: '尺寸', value: 'XL', type: 'size' },
|
||||||
|
{ id: 14, name: '尺寸', value: 'XXL', type: 'size' },
|
||||||
|
{ id: 15, name: '尺寸', value: 'XXXL', type: 'size' },
|
||||||
|
],
|
||||||
|
};
|
||||||
21
_mock/index.ts
Normal file
21
_mock/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export * from './_profile';
|
||||||
|
export * from './_rule';
|
||||||
|
export * from './_api';
|
||||||
|
export * from './_chart';
|
||||||
|
export * from './_pois';
|
||||||
|
export * from './_user';
|
||||||
|
export * from './_geo';
|
||||||
|
|
||||||
|
export * from './_file-manager';
|
||||||
|
export * from './_img';
|
||||||
|
export * from './_log';
|
||||||
|
export * from './_menu';
|
||||||
|
export * from './_permission';
|
||||||
|
export * from './_role';
|
||||||
|
export * from './_user-pro';
|
||||||
|
export * from './utils';
|
||||||
|
|
||||||
|
export * from './_forum';
|
||||||
|
export * from './_other';
|
||||||
|
export * from './_trade';
|
||||||
|
export * from './_ware';
|
||||||
160
_mock/utils.ts
Normal file
160
_mock/utils.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import addDays from 'date-fns/addDays';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import { Random } from 'mockjs';
|
||||||
|
|
||||||
|
export const COLOR_NAMES = ['red', 'volcano', 'orange', 'gold', 'yellow', 'lime', 'green', 'cyan', 'blue', 'geekblue', 'purple', 'magenta'];
|
||||||
|
|
||||||
|
export function genName(): any {
|
||||||
|
return genArr(['asdf', 'cipchk', '卡色']);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genProvince(): any {
|
||||||
|
return genArr([
|
||||||
|
'台湾',
|
||||||
|
'河北',
|
||||||
|
'山西',
|
||||||
|
'内蒙古',
|
||||||
|
'辽宁',
|
||||||
|
'吉林',
|
||||||
|
'黑龙江',
|
||||||
|
'江苏',
|
||||||
|
'浙江',
|
||||||
|
'安徽',
|
||||||
|
'福建',
|
||||||
|
'江西',
|
||||||
|
'山东',
|
||||||
|
'河南',
|
||||||
|
'湖北',
|
||||||
|
'湖南',
|
||||||
|
'广东',
|
||||||
|
'广西',
|
||||||
|
'海南',
|
||||||
|
'四川',
|
||||||
|
'贵州',
|
||||||
|
'云南',
|
||||||
|
'西藏',
|
||||||
|
'陕西',
|
||||||
|
'甘肃',
|
||||||
|
'青海',
|
||||||
|
'宁夏',
|
||||||
|
'新疆',
|
||||||
|
'北京',
|
||||||
|
'天津',
|
||||||
|
'上海',
|
||||||
|
'重庆',
|
||||||
|
'香港',
|
||||||
|
'澳门',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genArr(arr: any[], count: number = 1): any {
|
||||||
|
if (count === 1) {
|
||||||
|
return arr[Random.natural(0, arr.length - 1)];
|
||||||
|
}
|
||||||
|
return new Array(count <= -1 ? Random.natural(0, -count) : count).fill({}).map(() => {
|
||||||
|
return arr[Random.natural(0, arr.length - 1)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genColorName(): any {
|
||||||
|
return genArr(COLOR_NAMES);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genLabel(): any {
|
||||||
|
return genArr([
|
||||||
|
{
|
||||||
|
color: 'green',
|
||||||
|
text: 'Clients',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'red',
|
||||||
|
text: 'Important',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'blue',
|
||||||
|
text: 'Other',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成头像,`id` 只能0-8 */
|
||||||
|
export function genMp(id?: number): string {
|
||||||
|
return `https://randomuser.me/api/portraits/lego/${typeof id === 'undefined' ? Random.natural(0, 8) : id}.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genBigMp(): string {
|
||||||
|
return `./assets/tmp/img-big/${Random.natural(1, 8)}.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genTag(num: number = -3): any {
|
||||||
|
return genArr(['Angular', 'Node', 'HTML5', 'Less', 'Db', 'Python', 'Go'], num);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDate(days: number): Date {
|
||||||
|
return addDays(new Date(), days);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genData(days: number, dateFormat: string = 'yyyy-MM-dd'): string {
|
||||||
|
return format(addDate(days), dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rudeCopy(obj: any): string {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genContent(): string {
|
||||||
|
return `
|
||||||
|
<h2>主标题</h2>
|
||||||
|
<h3>次标题</h3>
|
||||||
|
<p><a>段落</a>,${Random.cparagraph(1, 1)}<p>
|
||||||
|
<p>段落,${Random.paragraph(1, 1)}<p>
|
||||||
|
<h2>列表</h2>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p>段落,${Random.paragraph(1, 1)}<p>
|
||||||
|
<ul>
|
||||||
|
<li><a>${Random.ctitle(5, 10)}</a></li>
|
||||||
|
<li><a>${Random.ctitle(5, 10)}</a></li>
|
||||||
|
<li><a>${Random.ctitle(5, 10)}</a></li>
|
||||||
|
<li><a>${Random.ctitle(5, 10)}</a></li>
|
||||||
|
<li><a>${Random.ctitle(5, 10)}</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>段落,${Random.paragraph(1, 1)}<p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>段落,${Random.paragraph(1, 1)}<p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<h2>图像</h2>
|
||||||
|
<p><img src="${Random.image()}"></p>
|
||||||
|
<h2>表格</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>列</th>
|
||||||
|
<th>列</th>
|
||||||
|
<th>列</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>列</td>
|
||||||
|
<td>列</td>
|
||||||
|
<td>列</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>列</td>
|
||||||
|
<td>列</td>
|
||||||
|
<td>列</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>列</td>
|
||||||
|
<td>列</td>
|
||||||
|
<td>列</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
141
angular.json
Normal file
141
angular.json
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"cli": {
|
||||||
|
"analytics": "0398a482-260e-4ce3-b33e-91ecf55f0703"
|
||||||
|
},
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"ng-alain": {
|
||||||
|
"projectType": "application",
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "less"
|
||||||
|
},
|
||||||
|
"@schematics/angular:application": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"assets": [
|
||||||
|
"src/assets",
|
||||||
|
"src/favicon.ico"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"node_modules/perfect-scrollbar/css/perfect-scrollbar.css",
|
||||||
|
"node_modules/quill/dist/quill.snow.css",
|
||||||
|
"src/styles.less"
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/quill/dist/quill.min.js",
|
||||||
|
"node_modules/perfect-scrollbar/dist/perfect-scrollbar.js"
|
||||||
|
],
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"ajv",
|
||||||
|
"ajv-formats"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all",
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "ng-alain:build",
|
||||||
|
"proxyConfig": "proxy.conf.js"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "ng-alain:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "ng-alain:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "ng-alain:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/quill/dist/quill.min.js",
|
||||||
|
"node_modules/perfect-scrollbar/dist/perfect-scrollbar.js"
|
||||||
|
],
|
||||||
|
"styles": [],
|
||||||
|
"assets": [
|
||||||
|
"src/assets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-eslint/builder:lint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "ng-alain:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "ng-alain:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "ng-alain"
|
||||||
|
}
|
||||||
32
e2e/protractor.conf.js
Normal file
32
e2e/protractor.conf.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Protractor configuration file, see link for more information
|
||||||
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
|
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { import("protractor").Config }
|
||||||
|
*/
|
||||||
|
exports.config = {
|
||||||
|
allScriptsTimeout: 11000,
|
||||||
|
specs: [
|
||||||
|
'./src/**/*.e2e-spec.ts'
|
||||||
|
],
|
||||||
|
capabilities: {
|
||||||
|
'browserName': 'chrome'
|
||||||
|
},
|
||||||
|
directConnect: true,
|
||||||
|
baseUrl: 'http://localhost:4200/',
|
||||||
|
framework: 'jasmine',
|
||||||
|
jasmineNodeOpts: {
|
||||||
|
showColors: true,
|
||||||
|
defaultTimeoutInterval: 30000,
|
||||||
|
print: function() {}
|
||||||
|
},
|
||||||
|
onPrepare() {
|
||||||
|
require('ts-node').register({
|
||||||
|
project: require('path').join(__dirname, './tsconfig.json')
|
||||||
|
});
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
28
e2e/src/app.e2e-spec.ts
Normal file
28
e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { browser, logging } from 'protractor';
|
||||||
|
import { AppPage } from './app.po';
|
||||||
|
|
||||||
|
describe('workspace-project App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display welcome message', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getTitleText()).toEqual('Welcome to ng8!');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Assert that there are no errors emitted from the browser
|
||||||
|
const logs = await browser
|
||||||
|
.manage()
|
||||||
|
.logs()
|
||||||
|
.get(logging.Type.BROWSER);
|
||||||
|
expect(logs).not.toContain(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
level: logging.Level.SEVERE,
|
||||||
|
} as logging.Entry),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
11
e2e/src/app.po.ts
Normal file
11
e2e/src/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { browser, by, element } from 'protractor';
|
||||||
|
|
||||||
|
export class AppPage {
|
||||||
|
navigateTo(): Promise<any> {
|
||||||
|
return browser.get(browser.baseUrl) as Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitleText(): Promise<string> {
|
||||||
|
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
e2e/tsconfig.json
Normal file
10
e2e/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/e2e",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2018",
|
||||||
|
"types": ["jasmine", "jasminewd2", "node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
38
karma.conf.js
Normal file
38
karma.conf.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Karma configuration file, see link for more information
|
||||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
basePath: '',
|
||||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage-istanbul-reporter'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
coverageIstanbulReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage'),
|
||||||
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
|
fixWebpackSourcePaths: true
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
customLaunchers: {
|
||||||
|
ChromeHeadlessCI: {
|
||||||
|
base: 'ChromeHeadless',
|
||||||
|
flags: ['--no-sandbox']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
||||||
17
ng-alain.json
Normal file
17
ng-alain.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/ng-alain/schema.json",
|
||||||
|
"theme": {
|
||||||
|
"additionalThemeVars": [
|
||||||
|
"./src/app/layout/pro/styles/theme-#NAME#.less",
|
||||||
|
"./src/styles/fix/theme-#NAME#.less"
|
||||||
|
],
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"theme": "dark"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"theme": "compact"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24244
package-lock.json
generated
Normal file
24244
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
135
package.json
Normal file
135
package.json
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"name": "ng-alain-pro",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Ng-alain business theme, ng-zorro-antd admin panel front-end framework",
|
||||||
|
"author": "cipchk <cipchk@qq.com>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/ng-alain/ng-alain.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://e.ng-alain.com/theme/pro",
|
||||||
|
"scripts": {
|
||||||
|
"ng-high-memory": "node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng",
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng s -o",
|
||||||
|
"hmr": "ng s -o --hmr",
|
||||||
|
"build": "npm run ng-high-memory build",
|
||||||
|
"analyze": "npm run ng-high-memory build -- --source-map",
|
||||||
|
"analyze:view": "source-map-explorer dist/**/*.js",
|
||||||
|
"lint": "npm run lint:ts && npm run lint:style",
|
||||||
|
"lint:ts": "ng lint --fix",
|
||||||
|
"lint:style": "stylelint \"src/**/*.less\" --syntax less --fix",
|
||||||
|
"e2e": "ng e2e",
|
||||||
|
"test": "ng test --watch",
|
||||||
|
"test-coverage": "ng test --code-coverage --watch=false",
|
||||||
|
"color-less": "ng-alain-plugin-theme -t=colorLess",
|
||||||
|
"theme": "ng-alain-plugin-theme -t=themeCss",
|
||||||
|
"icon": "ng g ng-alain:plugin icon",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@agm/core": "^1.1.0",
|
||||||
|
"@angular/animations": "~12.2.0",
|
||||||
|
"@angular/common": "~12.2.0",
|
||||||
|
"@angular/compiler": "~12.2.0",
|
||||||
|
"@angular/core": "~12.2.0",
|
||||||
|
"@angular/forms": "~12.2.0",
|
||||||
|
"@angular/platform-browser": "~12.2.0",
|
||||||
|
"@angular/platform-browser-dynamic": "~12.2.0",
|
||||||
|
"@angular/router": "~12.2.0",
|
||||||
|
"@delon/abc": "^12.3.0",
|
||||||
|
"@delon/acl": "^12.3.0",
|
||||||
|
"@delon/auth": "^12.3.0",
|
||||||
|
"@delon/cache": "^12.3.0",
|
||||||
|
"@delon/chart": "^12.3.0",
|
||||||
|
"@delon/form": "^12.3.0",
|
||||||
|
"@delon/mock": "^12.3.0",
|
||||||
|
"@delon/theme": "^12.3.0",
|
||||||
|
"@delon/util": "^12.3.0",
|
||||||
|
"@fullcalendar/core": "^5.9.0",
|
||||||
|
"@fullcalendar/daygrid": "^5.9.0",
|
||||||
|
"@fullcalendar/interaction": "^5.9.0",
|
||||||
|
"@fullcalendar/list": "^5.9.0",
|
||||||
|
"@fullcalendar/timegrid": "^5.9.0",
|
||||||
|
"@swimlane/ngx-charts": "^18.0.1",
|
||||||
|
"ajv": "^8.6.2",
|
||||||
|
"angular-baidu-maps": "^12.0.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"js-base64": "^3.6.1",
|
||||||
|
"masonry-layout": "^4.2.2",
|
||||||
|
"ng-gallery": "^5.0.0",
|
||||||
|
"ng-zorro-antd": "^12.0.1",
|
||||||
|
"ngx-tinymce": "^12.0.0",
|
||||||
|
"ngx-trend": "^7.0.0",
|
||||||
|
"perfect-scrollbar": "^1.5.2",
|
||||||
|
"quill": "^1.3.7",
|
||||||
|
"quill-image-resize-module": "^3.0.0",
|
||||||
|
"rxjs": "~6.6.0",
|
||||||
|
"screenfull": "^5.1.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.11.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~12.2.0",
|
||||||
|
"@angular/cli": "~12.2.0",
|
||||||
|
"@angular/compiler-cli": "~12.2.0",
|
||||||
|
"@types/jasmine": "~3.8.0",
|
||||||
|
"@types/node": "^12.11.1",
|
||||||
|
"jasmine-core": "~3.8.0",
|
||||||
|
"karma": "~6.3.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage": "~2.0.3",
|
||||||
|
"karma-jasmine": "~4.0.0",
|
||||||
|
"karma-jasmine-html-reporter": "~1.7.0",
|
||||||
|
"typescript": "~4.3.5",
|
||||||
|
"@angular-eslint/builder": "~12.3.1",
|
||||||
|
"@angular-eslint/eslint-plugin": "~12.3.1",
|
||||||
|
"@angular-eslint/eslint-plugin-template": "~12.3.1",
|
||||||
|
"@angular-eslint/schematics": "~12.3.1",
|
||||||
|
"@angular-eslint/template-parser": "~12.3.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "~4.29.2",
|
||||||
|
"@typescript-eslint/parser": "~4.29.2",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-config-prettier": "~8.3.0",
|
||||||
|
"eslint-plugin-import": "~2.24.1",
|
||||||
|
"eslint-plugin-jsdoc": "~36.0.7",
|
||||||
|
"eslint-plugin-prefer-arrow": "~1.2.3",
|
||||||
|
"eslint-plugin-prettier": "~3.4.1",
|
||||||
|
"@angular/language-service": "~12.2.0",
|
||||||
|
"source-map-explorer": "^2.5.2",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"stylelint": "^13.13.1",
|
||||||
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
|
"stylelint-config-rational-order": "^0.1.2",
|
||||||
|
"stylelint-config-standard": "^22.0.0",
|
||||||
|
"stylelint-declaration-block-no-ignored-properties": "^2.4.0",
|
||||||
|
"stylelint-order": "^4.1.0",
|
||||||
|
"@delon/testing": "^12.3.0",
|
||||||
|
"ng-alain": "^12.3.0",
|
||||||
|
"ng-alain-plugin-theme": "^12.0.0",
|
||||||
|
"ng-alain-sts": "^0.0.1",
|
||||||
|
"@types/jasminewd2": "~2.0.3",
|
||||||
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
|
"protractor": "~7.0.0",
|
||||||
|
"ts-node": "~8.3.0",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"husky": "^6.0.0",
|
||||||
|
"lint-staged": "^11.1.2",
|
||||||
|
"@types/file-saver": "^2.0.3",
|
||||||
|
"@types/js-base64": "^3.0.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"(src)/**/*.{html,ts}": [
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"(src)/**/*.less": [
|
||||||
|
"stylelint --syntax less --fix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"theme": {
|
||||||
|
"name": "pro",
|
||||||
|
"version": "12.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
proxy.conf.js
Normal file
17
proxy.conf.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* For more configuration, please refer to https://angular.io/guide/build#proxying-to-a-backend-server
|
||||||
|
*
|
||||||
|
* 更多配置描述请参考 https://angular.cn/guide/build#proxying-to-a-backend-server
|
||||||
|
*
|
||||||
|
* Note: The proxy is only valid for real requests, Mock does not actually generate requests, so the priority of Mock will be higher than the proxy
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* The following means that all requests are directed to the backend `https://localhost:9000/`
|
||||||
|
*/
|
||||||
|
// '/': {
|
||||||
|
// target: 'https://localhost:9000/',
|
||||||
|
// secure: false, // Ignore invalid SSL certificates
|
||||||
|
// changeOrigin: true
|
||||||
|
// }
|
||||||
|
};
|
||||||
46
src/app/app.component.ts
Normal file
46
src/app/app.component.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Component, ElementRef, OnInit, Renderer2 } from '@angular/core';
|
||||||
|
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router } from '@angular/router';
|
||||||
|
import { TitleService, VERSION as VERSION_ALAIN } from '@delon/theme';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||||
|
import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
template: ` <router-outlet></router-outlet> `
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnInit {
|
||||||
|
constructor(
|
||||||
|
el: ElementRef,
|
||||||
|
renderer: Renderer2,
|
||||||
|
private router: Router,
|
||||||
|
private titleSrv: TitleService,
|
||||||
|
private modalSrv: NzModalService
|
||||||
|
) {
|
||||||
|
renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full);
|
||||||
|
renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
let configLoad = false;
|
||||||
|
this.router.events.subscribe(ev => {
|
||||||
|
if (ev instanceof RouteConfigLoadStart) {
|
||||||
|
configLoad = true;
|
||||||
|
}
|
||||||
|
if (configLoad && ev instanceof NavigationError) {
|
||||||
|
this.modalSrv.confirm({
|
||||||
|
nzTitle: `提醒`,
|
||||||
|
nzContent: environment.production ? `应用可能已发布新版本,请点击刷新才能生效。` : `无法加载路由:${ev.url}`,
|
||||||
|
nzCancelDisabled: false,
|
||||||
|
nzOkText: '刷新',
|
||||||
|
nzCancelText: '忽略',
|
||||||
|
nzOnOk: () => location.reload()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ev instanceof NavigationEnd) {
|
||||||
|
this.titleSrv.setTitle();
|
||||||
|
this.modalSrv.closeAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/app/app.module.ts
Normal file
72
src/app/app.module.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable import/order */
|
||||||
|
/* eslint-disable import/no-duplicates */
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { default as ngLang } from '@angular/common/locales/zh';
|
||||||
|
import { APP_INITIALIZER, LOCALE_ID, NgModule, Type } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { SimpleInterceptor } from '@delon/auth';
|
||||||
|
import { NzNotificationModule } from 'ng-zorro-antd/notification';
|
||||||
|
|
||||||
|
// #region global third module
|
||||||
|
|
||||||
|
import { BidiModule } from '@angular/cdk/bidi';
|
||||||
|
const GLOBAL_THIRD_MODULES: Array<Type<any>> = [BidiModule];
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Http Interceptors
|
||||||
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { DefaultInterceptor } from '@core';
|
||||||
|
|
||||||
|
const INTERCEPTOR_PROVIDES = [
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true },
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }
|
||||||
|
];
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Startup Service
|
||||||
|
import { StartupService } from '@core';
|
||||||
|
export function StartupServiceFactory(startupService: StartupService): () => Promise<void> {
|
||||||
|
return () => startupService.load();
|
||||||
|
}
|
||||||
|
const APPINIT_PROVIDES = [
|
||||||
|
StartupService,
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: StartupServiceFactory,
|
||||||
|
deps: [StartupService],
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { CoreModule } from './core/core.module';
|
||||||
|
import { GlobalConfigModule } from './global-config.module';
|
||||||
|
import { LayoutModule } from './layout/layout.module';
|
||||||
|
import { RoutesModule } from './routes/routes.module';
|
||||||
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
import { STWidgetModule } from './shared/widget/st-widget.module';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
GlobalConfigModule.forRoot(),
|
||||||
|
CoreModule,
|
||||||
|
SharedModule,
|
||||||
|
LayoutModule,
|
||||||
|
RoutesModule,
|
||||||
|
STWidgetModule,
|
||||||
|
NzNotificationModule,
|
||||||
|
...GLOBAL_THIRD_MODULES
|
||||||
|
],
|
||||||
|
providers: [...INTERCEPTOR_PROVIDES, ...APPINIT_PROVIDES],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
5
src/app/core/README.md
Normal file
5
src/app/core/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
### CoreModule
|
||||||
|
|
||||||
|
**应** 仅只留 `providers` 属性。
|
||||||
|
|
||||||
|
**作用:** 一些通用服务,例如:用户消息、HTTP数据访问。
|
||||||
12
src/app/core/core.module.ts
Normal file
12
src/app/core/core.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
|
|
||||||
|
import { throwIfAlreadyLoaded } from './module-import-guard';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class CoreModule {
|
||||||
|
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
|
||||||
|
throwIfAlreadyLoaded(parentModule, 'CoreModule');
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/app/core/core.service.ts
Normal file
90
src/app/core/core.service.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* @Author: Maple
|
||||||
|
* @Date: 2021-03-22 11:42:26
|
||||||
|
* @LastEditors: Do not edit
|
||||||
|
* @LastEditTime: 2021-05-27 14:06:18
|
||||||
|
* @Description: 全局核心服务
|
||||||
|
*/
|
||||||
|
import { Injectable, Injector } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ReuseTabService } from '@delon/abc/reuse-tab';
|
||||||
|
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||||
|
import { CacheService } from '@delon/cache';
|
||||||
|
import { SettingsService } from '@delon/theme';
|
||||||
|
import { EnvironmentService } from '@env/environment.service';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class CoreService {
|
||||||
|
// 获取当前登录用户信息
|
||||||
|
public $api_get_current_user_info = `/scm/cuc/cuc/user/getUserDetail`;
|
||||||
|
|
||||||
|
// 获取当前用户所拥有的菜单
|
||||||
|
public $api_get_current_user_menus = `/scm/cuc/cuc/functionInfo/getUserHaveFunctionsList`;
|
||||||
|
|
||||||
|
constructor(private injector: Injector) {}
|
||||||
|
// 注入路由
|
||||||
|
public get router(): Router {
|
||||||
|
return this.injector.get(Router);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入全局设置服务
|
||||||
|
public get settingSrv(): SettingsService {
|
||||||
|
return this.injector.get(SettingsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入缓存服务
|
||||||
|
public get cacheSrv(): CacheService {
|
||||||
|
return this.injector.get(CacheService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入令牌服务
|
||||||
|
public get tokenSrv(): ITokenService {
|
||||||
|
return this.injector.get(DA_SERVICE_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入消息服务
|
||||||
|
public get msgSrv(): NzMessageService {
|
||||||
|
return this.injector.get(NzMessageService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入环境服务
|
||||||
|
public get envSrv(): EnvironmentService {
|
||||||
|
return this.injector.get(EnvironmentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入路由复用服务
|
||||||
|
private get reuseTabService(): ReuseTabService {
|
||||||
|
return this.injector.get(ReuseTabService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录状态
|
||||||
|
public get loginStatus(): boolean {
|
||||||
|
try {
|
||||||
|
return !!this.tokenSrv.get()?.token;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限认证凭据(TOKEN)
|
||||||
|
public get token(): string {
|
||||||
|
return this.tokenSrv.get()?.token || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出系统
|
||||||
|
* @param showMsg 是否显示登录过期弹窗
|
||||||
|
*/
|
||||||
|
logout(showMsg: boolean = false): void {
|
||||||
|
if (showMsg) {
|
||||||
|
this.msgSrv.warning('未登录或登录信息已过期,请重新登录!');
|
||||||
|
}
|
||||||
|
this.settingSrv.setUser({});
|
||||||
|
this.tokenSrv.clear();
|
||||||
|
this.cacheSrv.clear();
|
||||||
|
this.router.navigate([this.tokenSrv.login_url]);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/app/core/index.ts
Normal file
6
src/app/core/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './module-import-guard';
|
||||||
|
export * from './net/default.interceptor';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
export * from './core.service';
|
||||||
|
export * from './startup/startup.service';
|
||||||
6
src/app/core/module-import-guard.ts
Normal file
6
src/app/core/module-import-guard.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// https://angular.io/guide/styleguide#style-04-12
|
||||||
|
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string): void {
|
||||||
|
if (parentModule) {
|
||||||
|
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/app/core/net/default.interceptor.ts
Normal file
261
src/app/core/net/default.interceptor.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import {
|
||||||
|
HttpErrorResponse,
|
||||||
|
HttpEvent,
|
||||||
|
HttpHandler,
|
||||||
|
HttpHeaders,
|
||||||
|
HttpInterceptor,
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponseBase
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import { Injectable, Injector } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||||
|
import { _HttpClient } from '@delon/theme';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { NzNotificationService } from 'ng-zorro-antd/notification';
|
||||||
|
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
||||||
|
import { catchError, filter, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
const CODEMESSAGE: { [key: number]: string } = {
|
||||||
|
200: '服务器成功返回请求的数据。',
|
||||||
|
201: '新建或修改数据成功。',
|
||||||
|
202: '一个请求已经进入后台排队(异步任务)。',
|
||||||
|
204: '删除数据成功。',
|
||||||
|
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
|
||||||
|
401: '用户没有权限(令牌、用户名、密码错误)。',
|
||||||
|
403: '用户得到授权,但是访问是被禁止的。',
|
||||||
|
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
|
||||||
|
406: '请求的格式不可得。',
|
||||||
|
410: '请求的资源被永久删除,且不会再得到的。',
|
||||||
|
422: '当创建一个对象时,发生一个验证错误。',
|
||||||
|
500: '服务器发生错误,请检查服务器。',
|
||||||
|
502: '网关错误。',
|
||||||
|
503: '服务不可用,服务器暂时过载或维护。',
|
||||||
|
504: '网关超时。'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认HTTP拦截器,其注册细节见 `app.module.ts`
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DefaultInterceptor implements HttpInterceptor {
|
||||||
|
private refreshTokenEnabled = environment.api.refreshTokenEnabled;
|
||||||
|
private refreshTokenType: 're-request' | 'auth-refresh' = environment.api.refreshTokenType;
|
||||||
|
private refreshToking = false;
|
||||||
|
private refreshToken$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||||
|
|
||||||
|
constructor(private injector: Injector) {
|
||||||
|
if (this.refreshTokenType === 'auth-refresh') {
|
||||||
|
this.buildAuthRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get notification(): NzNotificationService {
|
||||||
|
return this.injector.get(NzNotificationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get tokenSrv(): ITokenService {
|
||||||
|
return this.injector.get(DA_SERVICE_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get http(): _HttpClient {
|
||||||
|
return this.injector.get(_HttpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private goTo(url: string): void {
|
||||||
|
setTimeout(() => this.injector.get(Router).navigateByUrl(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkStatus(ev: HttpResponseBase): void {
|
||||||
|
if ((ev.status >= 200 && ev.status < 300) || ev.status === 401) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errortext = CODEMESSAGE[ev.status] || ev.statusText;
|
||||||
|
this.notification.error(`请求错误 ${ev.status}: ${ev.url}`, errortext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新 Token 请求
|
||||||
|
*/
|
||||||
|
private refreshTokenRequest(): Observable<any> {
|
||||||
|
const model = this.tokenSrv.get();
|
||||||
|
return this.http.post(`/api/auth/refresh`, null, null, { headers: { refresh_token: model?.refresh_token || '' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// #region 刷新Token方式一:使用 401 重新刷新 Token
|
||||||
|
|
||||||
|
private tryRefreshToken(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
|
||||||
|
// 1、若请求为刷新Token请求,表示来自刷新Token可以直接跳转登录页
|
||||||
|
if ([`/api/auth/refresh`].some(url => req.url.includes(url))) {
|
||||||
|
this.toLogin();
|
||||||
|
return throwError(ev);
|
||||||
|
}
|
||||||
|
// 2、如果 `refreshToking` 为 `true` 表示已经在请求刷新 Token 中,后续所有请求转入等待状态,直至结果返回后再重新发起请求
|
||||||
|
if (this.refreshToking) {
|
||||||
|
return this.refreshToken$.pipe(
|
||||||
|
filter(v => !!v),
|
||||||
|
take(1),
|
||||||
|
switchMap(() => next.handle(this.reAttachToken(req)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 3、尝试调用刷新 Token
|
||||||
|
this.refreshToking = true;
|
||||||
|
this.refreshToken$.next(null);
|
||||||
|
|
||||||
|
return this.refreshTokenRequest().pipe(
|
||||||
|
switchMap(res => {
|
||||||
|
// 通知后续请求继续执行
|
||||||
|
this.refreshToking = false;
|
||||||
|
this.refreshToken$.next(res);
|
||||||
|
// 重新保存新 token
|
||||||
|
this.tokenSrv.set(res);
|
||||||
|
// 重新发起请求
|
||||||
|
return next.handle(this.reAttachToken(req));
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
this.refreshToking = false;
|
||||||
|
this.toLogin();
|
||||||
|
return throwError(err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新附加新 Token 信息
|
||||||
|
*
|
||||||
|
* > 由于已经发起的请求,不会再走一遍 `@delon/auth` 因此需要结合业务情况重新附加新的 Token
|
||||||
|
*/
|
||||||
|
private reAttachToken(req: HttpRequest<any>): HttpRequest<any> {
|
||||||
|
// 以下示例是以 NG-ALAIN 默认使用 `SimpleInterceptor`
|
||||||
|
const token = this.tokenSrv.get()?.token;
|
||||||
|
return req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
token: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region 刷新Token方式二:使用 `@delon/auth` 的 `refresh` 接口
|
||||||
|
|
||||||
|
private buildAuthRefresh(): void {
|
||||||
|
if (!this.refreshTokenEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tokenSrv.refresh
|
||||||
|
.pipe(
|
||||||
|
filter(() => !this.refreshToking),
|
||||||
|
switchMap(res => {
|
||||||
|
console.log(res);
|
||||||
|
this.refreshToking = true;
|
||||||
|
return this.refreshTokenRequest();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
res => {
|
||||||
|
// TODO: Mock expired value
|
||||||
|
res.expired = +new Date() + 1000 * 60 * 5;
|
||||||
|
this.refreshToking = false;
|
||||||
|
this.tokenSrv.set(res);
|
||||||
|
},
|
||||||
|
() => this.toLogin()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
private toLogin(): void {
|
||||||
|
this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
|
||||||
|
this.goTo(this.tokenSrv.login_url!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleData(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
|
||||||
|
this.checkStatus(ev);
|
||||||
|
// 业务处理:一些通用操作
|
||||||
|
switch (ev.status) {
|
||||||
|
case 200:
|
||||||
|
// 业务层级错误处理,以下是假定restful有一套统一输出格式(指不管成功与否都有相应的数据格式)情况下进行处理
|
||||||
|
// 例如响应内容:
|
||||||
|
// 错误内容:{ status: 1, msg: '非法参数' }
|
||||||
|
// 正确内容:{ status: 0, response: { } }
|
||||||
|
// 则以下代码片断可直接适用
|
||||||
|
// if (ev instanceof HttpResponse) {
|
||||||
|
// const body = ev.body;
|
||||||
|
// if (body && body.status !== 0) {
|
||||||
|
// this.injector.get(NzMessageService).error(body.msg);
|
||||||
|
// // 注意:这里如果继续抛出错误会被行254的 catchError 二次拦截,导致外部实现的 Pipe、subscribe 操作被中断,例如:this.http.get('/').subscribe() 不会触发
|
||||||
|
// // 如果你希望外部实现,需要手动移除行254
|
||||||
|
// return throwError({});
|
||||||
|
// } else {
|
||||||
|
// // 忽略 Blob 文件体
|
||||||
|
// if (ev.body instanceof Blob) {
|
||||||
|
// return of(ev);
|
||||||
|
// }
|
||||||
|
// // 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
|
||||||
|
// return of(new HttpResponse(Object.assign(ev, { body: body.response })));
|
||||||
|
// // 或者依然保持完整的格式
|
||||||
|
// return of(ev);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
if (this.refreshTokenEnabled && this.refreshTokenType === 're-request') {
|
||||||
|
return this.tryRefreshToken(ev, req, next);
|
||||||
|
}
|
||||||
|
this.toLogin();
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
case 404:
|
||||||
|
case 500:
|
||||||
|
this.goTo(`/exception/${ev.status}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (ev instanceof HttpErrorResponse) {
|
||||||
|
console.warn(
|
||||||
|
'未可知错误,大部分是由于后端不支持跨域CORS或无效配置引起,请参考 https://ng-alain.com/docs/server 解决跨域问题',
|
||||||
|
ev
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ev instanceof HttpErrorResponse) {
|
||||||
|
return throwError(ev);
|
||||||
|
} else {
|
||||||
|
return of(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAdditionalHeaders(headers?: HttpHeaders): { [name: string]: string } {
|
||||||
|
const res: { [name: string]: string } = {};
|
||||||
|
// const lang = this.injector.get(ALAIN_I18N_TOKEN).currentLang;
|
||||||
|
// if (!headers?.has('Accept-Language') && lang) {
|
||||||
|
// res['Accept-Language'] = lang;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
// 统一加上服务端前缀
|
||||||
|
let url = req.url;
|
||||||
|
if (!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||||
|
const { baseUrl } = environment.api;
|
||||||
|
url = baseUrl + (baseUrl.endsWith('/') && url.startsWith('/') ? url.substring(1) : url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newReq = req.clone({ url, setHeaders: this.getAdditionalHeaders(req.headers) });
|
||||||
|
return next.handle(newReq).pipe(
|
||||||
|
mergeMap(ev => {
|
||||||
|
// 允许统一对请求错误处理
|
||||||
|
if (ev instanceof HttpResponseBase) {
|
||||||
|
return this.handleData(ev, newReq, next);
|
||||||
|
}
|
||||||
|
// 若一切都正常,则后续操作
|
||||||
|
return of(ev);
|
||||||
|
}),
|
||||||
|
catchError((err: HttpErrorResponse) => this.handleData(err, newReq, next))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
src/app/core/startup/startup.service.ts
Normal file
158
src/app/core/startup/startup.service.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { ACLService } from '@delon/acl';
|
||||||
|
import { MenuService, SettingsService, TitleService, _HttpClient } from '@delon/theme';
|
||||||
|
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||||
|
import { NzIconService } from 'ng-zorro-antd/icon';
|
||||||
|
import { Observable, zip } from 'rxjs';
|
||||||
|
import { catchError, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ICONS } from '../../../style-icons';
|
||||||
|
import { ICONS_AUTO } from '../../../style-icons-auto';
|
||||||
|
import { CoreService } from '../core.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for application startup
|
||||||
|
* Generally used to get the basic data of the application, like: Menu Data, User Data, etc.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class StartupService {
|
||||||
|
constructor(
|
||||||
|
iconSrv: NzIconService,
|
||||||
|
private menuService: MenuService,
|
||||||
|
private settingService: SettingsService,
|
||||||
|
private aclService: ACLService,
|
||||||
|
private titleService: TitleService,
|
||||||
|
private httpClient: _HttpClient,
|
||||||
|
private coreSrv: CoreService
|
||||||
|
) {
|
||||||
|
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 退出登录时需要清理用户信息
|
||||||
|
|
||||||
|
load(): Promise<void> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let data;
|
||||||
|
if (this.coreSrv.loginStatus) {
|
||||||
|
// 本地菜单
|
||||||
|
// data = this.loadMockData();
|
||||||
|
// 远程菜单
|
||||||
|
data = this.loadRemoteData();
|
||||||
|
} else {
|
||||||
|
data = this.loadMockData();
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
.pipe(
|
||||||
|
catchError(res => {
|
||||||
|
console.warn(`StartupService.load: Network request failed`, res);
|
||||||
|
resolve();
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
([appData, userData, menuData]) => this.initSystem(appData, userData, menuData),
|
||||||
|
err => {
|
||||||
|
console.log(err);
|
||||||
|
},
|
||||||
|
() => resolve()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统初始化
|
||||||
|
*
|
||||||
|
* @param langData 翻译数据
|
||||||
|
* @param appData App应用数据
|
||||||
|
* @param userData 用户数据
|
||||||
|
* @param menuData 菜单数据
|
||||||
|
*/
|
||||||
|
private initSystem(appData: NzSafeAny, userData: NzSafeAny, menuData: NzSafeAny): void {
|
||||||
|
// 应用信息:包括站点名、描述、年份
|
||||||
|
this.settingService.setApp(appData);
|
||||||
|
// 用户信息:包括姓名、头像、邮箱地址
|
||||||
|
this.settingService.setUser(userData);
|
||||||
|
// ACL:设置权限为全量
|
||||||
|
this.aclService.setFull(true);
|
||||||
|
// 初始化菜单
|
||||||
|
this.menuService.add(menuData);
|
||||||
|
// 设置页面标题的后缀
|
||||||
|
this.titleService.default = '';
|
||||||
|
this.titleService.suffix = appData.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 加载本地模拟数据
|
||||||
|
* @returns 程序初始化数据
|
||||||
|
*/
|
||||||
|
loadMockData(): Observable<[object, object, object]> {
|
||||||
|
// 登录时调用远程数据, 非登录状态下调用Mock数据
|
||||||
|
|
||||||
|
// App数据
|
||||||
|
const appData = this.httpClient.get(`assets/mocks/app-data.json`).pipe(map((res: any) => res.app));
|
||||||
|
|
||||||
|
// 用户数据
|
||||||
|
const userData = this.coreSrv.loginStatus
|
||||||
|
? this.httpClient.post(this.coreSrv.$api_get_current_user_info, {}).pipe(map((res: any) => res.data))
|
||||||
|
: this.httpClient.get('assets/mocks/user-data.json').pipe(map((res: any) => res.user));
|
||||||
|
|
||||||
|
// 菜单数据
|
||||||
|
const menuData = this.httpClient.get('assets/mocks/menu-data.json').pipe(map((res: any) => res.menu));
|
||||||
|
|
||||||
|
return zip(appData, userData, menuData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 加载远程数据
|
||||||
|
* @returns 程序初始化数据
|
||||||
|
*/
|
||||||
|
loadRemoteData(): Observable<[object, object, object]> {
|
||||||
|
// 登录时调用远程数据, 非登录状态下调用Mock数据
|
||||||
|
|
||||||
|
// App数据
|
||||||
|
const appData = this.httpClient.get(`assets/mocks/app-data.json`).pipe(map((res: any) => res.app));
|
||||||
|
|
||||||
|
// 用户数据
|
||||||
|
const userData = this.coreSrv.loginStatus
|
||||||
|
? this.httpClient.post(this.coreSrv.$api_get_current_user_info, {}).pipe(map((res: any) => res.data))
|
||||||
|
: this.httpClient.get('assets/mocks/user-data.json').pipe(map((res: any) => res.user));
|
||||||
|
|
||||||
|
// 菜单数据
|
||||||
|
const menuData = this.httpClient
|
||||||
|
.post(this.coreSrv.$api_get_current_user_menus, {
|
||||||
|
appId: this.coreSrv.envSrv.getEnvironment().appId
|
||||||
|
})
|
||||||
|
.pipe(map((res: any) => res.data));
|
||||||
|
|
||||||
|
return zip(appData, userData, menuData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load(): Observable<void> {
|
||||||
|
// const defaultLang = this.i18n.defaultLang;
|
||||||
|
// return zip(this.i18n.loadLangData(defaultLang), this.httpClient.get('assets/tmp/app-data.json')).pipe(
|
||||||
|
// // 接收其他拦截器后产生的异常消息
|
||||||
|
// catchError(res => {
|
||||||
|
// console.warn(`StartupService.load: Network request failed`, res);
|
||||||
|
// return [];
|
||||||
|
// }),
|
||||||
|
// map(([langData, appData]: [Record<string, string>, NzSafeAny]) => {
|
||||||
|
// // setting language data
|
||||||
|
// this.i18n.use(defaultLang, langData);
|
||||||
|
|
||||||
|
// // 应用信息:包括站点名、描述、年份
|
||||||
|
// this.settingService.setApp(appData.app);
|
||||||
|
// // 用户信息:包括姓名、头像、邮箱地址
|
||||||
|
// this.settingService.setUser(appData.user);
|
||||||
|
// // ACL:设置权限为全量
|
||||||
|
// this.aclService.setFull(true);
|
||||||
|
// // 初始化菜单
|
||||||
|
// this.menuService.add(appData.menu);
|
||||||
|
// // 设置页面标题的后缀
|
||||||
|
// this.titleService.default = '';
|
||||||
|
// this.titleService.suffix = appData.app.name;
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
65
src/app/global-config.module.ts
Normal file
65
src/app/global-config.module.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/* eslint-disable import/order */
|
||||||
|
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
|
import { throwIfAlreadyLoaded } from '@core';
|
||||||
|
import { ReuseTabMatchMode, ReuseTabService, ReuseTabStrategy } from '@delon/abc/reuse-tab';
|
||||||
|
import { DelonACLModule } from '@delon/acl';
|
||||||
|
import { AlainThemeModule } from '@delon/theme';
|
||||||
|
import { AlainConfig, ALAIN_CONFIG } from '@delon/util';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
|
||||||
|
// Please refer to: https://ng-alain.com/docs/global-config
|
||||||
|
// #region NG-ALAIN Config
|
||||||
|
|
||||||
|
const alainConfig: AlainConfig = {
|
||||||
|
st: { modal: { size: 'lg' } },
|
||||||
|
pageHeader: { homeI18n: 'home', recursiveBreadcrumb: true },
|
||||||
|
auth: { login_url: '/passport/login' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const alainModules = [AlainThemeModule.forRoot(), DelonACLModule.forRoot()];
|
||||||
|
const alainProvides = [{ provide: ALAIN_CONFIG, useValue: alainConfig }];
|
||||||
|
|
||||||
|
// #region reuse-tab
|
||||||
|
|
||||||
|
import { RouteReuseStrategy } from '@angular/router';
|
||||||
|
alainProvides.push({
|
||||||
|
provide: RouteReuseStrategy,
|
||||||
|
useClass: ReuseTabStrategy,
|
||||||
|
deps: [ReuseTabService]
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// Please refer to: https://ng.ant.design/docs/global-config/en#how-to-use
|
||||||
|
// #region NG-ZORRO Config
|
||||||
|
|
||||||
|
import { NzConfig, NZ_CONFIG } from 'ng-zorro-antd/core/config';
|
||||||
|
|
||||||
|
const ngZorroConfig: NzConfig = {};
|
||||||
|
|
||||||
|
const zorroProvides = [{ provide: NZ_CONFIG, useValue: ngZorroConfig }];
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [...alainModules, ...(environment.modules || [])]
|
||||||
|
})
|
||||||
|
export class GlobalConfigModule {
|
||||||
|
constructor(@Optional() @SkipSelf() parentModule: GlobalConfigModule, reuseTabService: ReuseTabService) {
|
||||||
|
throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule');
|
||||||
|
// NOTICE: Only valid for menus with reuse property
|
||||||
|
// Pls refer to the E-Mail demo effect
|
||||||
|
reuseTabService.mode = ReuseTabMatchMode.MenuForce;
|
||||||
|
// Shouled be trigger init, you can ingore when used `reuse-tab` component in layout component
|
||||||
|
reuseTabService.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
static forRoot(): ModuleWithProviders<GlobalConfigModule> {
|
||||||
|
return {
|
||||||
|
ngModule: GlobalConfigModule,
|
||||||
|
providers: [...alainProvides, ...zorroProvides]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/app/layout/layout.module.ts
Normal file
64
src/app/layout/layout.module.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { LayoutModule as CDKLayoutModule } from '@angular/cdk/layout';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GlobalFooterModule } from '@delon/abc/global-footer';
|
||||||
|
import { NoticeIconModule } from '@delon/abc/notice-icon';
|
||||||
|
import { AlainThemeModule } from '@delon/theme';
|
||||||
|
import { ThemeBtnModule } from '@delon/theme/theme-btn';
|
||||||
|
import { ScrollbarModule } from '@shared';
|
||||||
|
import { NzAlertModule } from 'ng-zorro-antd/alert';
|
||||||
|
import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete';
|
||||||
|
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
|
||||||
|
import { NzBadgeModule } from 'ng-zorro-antd/badge';
|
||||||
|
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||||
|
import { NzDividerModule } from 'ng-zorro-antd/divider';
|
||||||
|
import { NzDrawerModule } from 'ng-zorro-antd/drawer';
|
||||||
|
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
|
||||||
|
import { NzIconModule } from 'ng-zorro-antd/icon';
|
||||||
|
import { NzLayoutModule } from 'ng-zorro-antd/layout';
|
||||||
|
import { NzMessageModule } from 'ng-zorro-antd/message';
|
||||||
|
import { NzSelectModule } from 'ng-zorro-antd/select';
|
||||||
|
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||||
|
import { NzSwitchModule } from 'ng-zorro-antd/switch';
|
||||||
|
import { NzTimelineModule } from 'ng-zorro-antd/timeline';
|
||||||
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
|
|
||||||
|
import { LayoutPassportComponent } from './passport/passport.component';
|
||||||
|
import { PRO_COMPONENTS } from './pro/index';
|
||||||
|
|
||||||
|
const COMPONENTS: Array<Type<any>> = [...PRO_COMPONENTS, LayoutPassportComponent];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterModule,
|
||||||
|
FormsModule,
|
||||||
|
AlainThemeModule,
|
||||||
|
GlobalFooterModule,
|
||||||
|
CDKLayoutModule,
|
||||||
|
NzSpinModule,
|
||||||
|
NzDropDownModule,
|
||||||
|
NzIconModule,
|
||||||
|
NzDrawerModule,
|
||||||
|
NzAutocompleteModule,
|
||||||
|
NzAvatarModule,
|
||||||
|
NzSwitchModule,
|
||||||
|
NzToolTipModule,
|
||||||
|
NzSelectModule,
|
||||||
|
NzDividerModule,
|
||||||
|
NzAlertModule,
|
||||||
|
NzLayoutModule,
|
||||||
|
NzButtonModule,
|
||||||
|
NzBadgeModule,
|
||||||
|
NzTimelineModule,
|
||||||
|
NoticeIconModule,
|
||||||
|
ThemeBtnModule,
|
||||||
|
ScrollbarModule,
|
||||||
|
NzMessageModule
|
||||||
|
],
|
||||||
|
declarations: COMPONENTS,
|
||||||
|
exports: COMPONENTS
|
||||||
|
})
|
||||||
|
export class LayoutModule {}
|
||||||
13
src/app/layout/passport/passport.component.html
Normal file
13
src/app/layout/passport/passport.component.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div class="container">
|
||||||
|
<!-- <pro-langs class="langs" btnClass></pro-langs> -->
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="top">
|
||||||
|
<div class="head">
|
||||||
|
<img class="logo" src="./assets/logo-color.svg">
|
||||||
|
<span class="title">ng-alain pro</span>
|
||||||
|
</div>
|
||||||
|
<div class="desc">武林中最有影响力的《葵花宝典》;欲练神功,挥刀自宫</div>
|
||||||
|
</div>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
74
src/app/layout/passport/passport.component.less
Normal file
74
src/app/layout/passport/passport.component.less
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
@import '~@delon/theme/index';
|
||||||
|
:host {
|
||||||
|
::ng-deep {
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
.langs {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 44px;
|
||||||
|
text-align: right;
|
||||||
|
.ant-dropdown-trigger {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.anticon {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-right: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
flex: 1;
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: @screen-md-min) {
|
||||||
|
.container {
|
||||||
|
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center 110px;
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
padding: 32px 0 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
height: 44px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
position: relative;
|
||||||
|
color: @heading-color;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 33px;
|
||||||
|
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
color: @text-color-secondary;
|
||||||
|
font-size: @font-size-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app/layout/passport/passport.component.ts
Normal file
30
src/app/layout/passport/passport.component.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-passport',
|
||||||
|
templateUrl: './passport.component.html',
|
||||||
|
styleUrls: ['./passport.component.less']
|
||||||
|
})
|
||||||
|
export class LayoutPassportComponent implements OnInit {
|
||||||
|
links = [
|
||||||
|
{
|
||||||
|
title: '帮助',
|
||||||
|
href: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '隐私',
|
||||||
|
href: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '条款',
|
||||||
|
href: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.tokenService.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/layout/pro/components/footer/footer.component.html
Normal file
16
src/app/layout/pro/components/footer/footer.component.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<global-footer>
|
||||||
|
<global-footer-item href="https://e.ng-alain.com/theme/pro" blankTarget>
|
||||||
|
Pro 首页
|
||||||
|
</global-footer-item>
|
||||||
|
<global-footer-item href="https://github.com/ng-alain" blankTarget>
|
||||||
|
<i nz-icon nzType="github"></i>
|
||||||
|
</global-footer-item>
|
||||||
|
<global-footer-item href="https://ng-alain.github.io/ng-alain/" blankTarget>
|
||||||
|
Alain Pro
|
||||||
|
</global-footer-item>
|
||||||
|
Copyright
|
||||||
|
<i nz-icon nzType="copyright" class="mx-sm"></i>
|
||||||
|
{{ year }}
|
||||||
|
<a href="//github.com/cipchk" target="_blank" class="mx-sm">卡色</a>
|
||||||
|
出品
|
||||||
|
</global-footer>
|
||||||
15
src/app/layout/pro/components/footer/footer.component.ts
Normal file
15
src/app/layout/pro/components/footer/footer.component.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { SettingsService } from '@delon/theme';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-footer',
|
||||||
|
templateUrl: './footer.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProFooterComponent {
|
||||||
|
get year(): number {
|
||||||
|
return this.setting.app.year;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private setting: SettingsService) {}
|
||||||
|
}
|
||||||
25
src/app/layout/pro/components/header/header.component.html
Normal file
25
src/app/layout/pro/components/header/header.component.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<div *ngIf="pro.isTopMenu" class="alain-pro__top-nav">
|
||||||
|
<div class="alain-pro__top-nav-main" [ngClass]="{ 'alain-pro__top-nav-main-wide': pro.isFixed }">
|
||||||
|
<div class="alain-pro__top-nav-main-left">
|
||||||
|
<layout-pro-logo class="alain-pro__top-nav-logo"></layout-pro-logo>
|
||||||
|
<div class="alain-pro__menu-wrap">
|
||||||
|
<div layout-pro-menu mode="horizontal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alain-pro__top-nav-main-right" layout-pro-header-widget></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!pro.isTopMenu" class="alain-pro__header">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<ng-container *ngIf="pro.isMobile">
|
||||||
|
<a [routerLink]="['/']" class="alain-pro__header-logo">
|
||||||
|
<img src="./assets/logo-color.svg" width="32" />
|
||||||
|
</a>
|
||||||
|
<div class="ant-divider ant-divider-vertical"></div>
|
||||||
|
</ng-container>
|
||||||
|
<div class="alain-pro__header-item alain-pro__header-trigger" (click)="pro.setCollapsed()">
|
||||||
|
<i nz-icon [nzType]="collapsedIcon" class="alain-pro__header-item-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div layout-pro-header-widget></div>
|
||||||
|
</div>
|
||||||
68
src/app/layout/pro/components/header/header.component.ts
Normal file
68
src/app/layout/pro/components/header/header.component.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { RTL, RTLService } from '@delon/theme';
|
||||||
|
import { combineLatest, fromEvent, Subject } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BrandService } from '../../pro.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-header',
|
||||||
|
templateUrl: './header.component.html',
|
||||||
|
host: {
|
||||||
|
'[class.ant-layout-header]': 'true',
|
||||||
|
'[class.alain-pro__header-fixed]': 'pro.fixedHeader',
|
||||||
|
'[class.alain-pro__header-hide]': 'hideHeader',
|
||||||
|
'[style.padding.px]': '0'
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProHeaderComponent implements OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
hideHeader = false;
|
||||||
|
|
||||||
|
@HostBinding('style.width')
|
||||||
|
get getHeadWidth(): string {
|
||||||
|
const { isMobile, fixedHeader, menu, collapsed, width, widthInCollapsed } = this.pro;
|
||||||
|
if (isMobile || !fixedHeader || menu === 'top') {
|
||||||
|
return '100%';
|
||||||
|
}
|
||||||
|
return collapsed ? `calc(100% - ${widthInCollapsed}px)` : `calc(100% - ${width}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get collapsedIcon(): string {
|
||||||
|
let type = this.pro.collapsed ? 'unfold' : 'fold';
|
||||||
|
if (this.rtl.dir === RTL) {
|
||||||
|
type = this.pro.collapsed ? 'fold' : 'unfold';
|
||||||
|
}
|
||||||
|
return `menu-${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(public pro: BrandService, @Inject(DOCUMENT) private doc: any, private cdr: ChangeDetectorRef, private rtl: RTLService) {}
|
||||||
|
|
||||||
|
private handScroll(): void {
|
||||||
|
if (!this.pro.autoHideHeader) {
|
||||||
|
this.hideHeader = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hideHeader = this.doc.body.scrollTop + this.doc.documentElement.scrollTop > this.pro.autoHideHeaderTop;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
combineLatest([
|
||||||
|
this.pro.notify.pipe(tap(() => this.cdr.markForCheck())),
|
||||||
|
fromEvent(window, 'scroll', { passive: false }).pipe(throttleTime(50), distinctUntilChanged())
|
||||||
|
])
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(() => this.handScroll());
|
||||||
|
|
||||||
|
this.rtl.change.pipe(takeUntil(this.destroy$)).subscribe(() => this.cdr.detectChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/app/layout/pro/components/logo/logo.component.html
Normal file
4
src/app/layout/pro/components/logo/logo.component.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<a [routerLink]="['/']" class="d-flex align-items-center">
|
||||||
|
<img src="./assets/logo-color.svg" alt="{{ name }}" height="32" />
|
||||||
|
<h1>{{ name }}</h1>
|
||||||
|
</a>
|
||||||
15
src/app/layout/pro/components/logo/logo.component.ts
Normal file
15
src/app/layout/pro/components/logo/logo.component.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { SettingsService } from '@delon/theme';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-logo',
|
||||||
|
templateUrl: './logo.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProLogoComponent {
|
||||||
|
get name(): string {
|
||||||
|
return this.setting.app.name!;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private setting: SettingsService) {}
|
||||||
|
}
|
||||||
101
src/app/layout/pro/components/menu/menu.component.html
Normal file
101
src/app/layout/pro/components/menu/menu.component.html
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<ng-template #icon let-i>
|
||||||
|
<ng-container *ngIf="i" [ngSwitch]="i.type">
|
||||||
|
<i *ngSwitchCase="'icon'" nz-icon [nzType]="i.value" class="alain-pro__menu-icon"></i>
|
||||||
|
<i *ngSwitchCase="'iconfont'" nz-icon [nzIconfont]="i.iconfont" class="alain-pro__menu-icon"></i>
|
||||||
|
<img *ngSwitchCase="'img'" src="{{ i.value }}" class="anticon alain-pro__menu-icon alain-pro__menu-img" />
|
||||||
|
<i *ngSwitchDefault class="anticon alain-pro__menu-icon {{ i.value }}"></i>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #mainLink let-i>
|
||||||
|
<ng-template [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{ $implicit: i.icon }"></ng-template>
|
||||||
|
<span class="alain-pro__menu-title-text" *ngIf="!pro.onlyIcon">{{ i.text }}</span>
|
||||||
|
<div *ngIf="i.badge" class="alain-pro__menu-title-badge">
|
||||||
|
<em>{{ i.badge }}</em>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #subLink let-i>
|
||||||
|
<a *ngIf="!i.externalLink" [routerLink]="i.link" [target]="i.target">{{ i.text }} </a>
|
||||||
|
<a *ngIf="i.externalLink" [attr.href]="i.externalLink" [attr.target]="i.target">{{ i.text }} </a>
|
||||||
|
</ng-template>
|
||||||
|
<ul *ngIf="menus" nz-menu [nzMode]="mode" [nzTheme]="pro.theme" [nzInlineCollapsed]="pro.isMobile ? false : pro.collapsed">
|
||||||
|
<ng-container *ngFor="let l1 of menus">
|
||||||
|
<li
|
||||||
|
*ngIf="l1.children!.length === 0"
|
||||||
|
nz-menu-item
|
||||||
|
class="alain-pro__menu-item"
|
||||||
|
[class.alain-pro__menu-item--disabled]="l1.disabled"
|
||||||
|
[nzSelected]="l1._selected"
|
||||||
|
[nzDisabled]="l1.disabled"
|
||||||
|
>
|
||||||
|
<a *ngIf="!l1.externalLink" [routerLink]="l1.link" (click)="closeCollapsed()" class="alain-pro__menu-title">
|
||||||
|
<ng-template [ngTemplateOutlet]="mainLink" [ngTemplateOutletContext]="{ $implicit: l1 }"></ng-template>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
*ngIf="l1.externalLink"
|
||||||
|
[attr.href]="l1.externalLink"
|
||||||
|
[attr.target]="l1.target"
|
||||||
|
(click)="closeCollapsed()"
|
||||||
|
class="alain-pro__menu-title"
|
||||||
|
>
|
||||||
|
<ng-template [ngTemplateOutlet]="mainLink" [ngTemplateOutletContext]="{ $implicit: l1 }"></ng-template>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
*ngIf="l1.children!.length > 0"
|
||||||
|
nz-submenu
|
||||||
|
[nzTitle]="l1TitleTpl"
|
||||||
|
class="alain-pro__menu-item"
|
||||||
|
[class.text-white]="pro.theme === 'dark' && l1._selected"
|
||||||
|
[nzOpen]="l1._open"
|
||||||
|
[nzDisabled]="l1.disabled"
|
||||||
|
(nzOpenChange)="openChange(l1, $event)"
|
||||||
|
>
|
||||||
|
<ng-template #l1TitleTpl>
|
||||||
|
<span title class="alain-pro__menu-title">
|
||||||
|
<ng-template [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{ $implicit: l1.icon }"></ng-template>
|
||||||
|
<span class="alain-pro__menu-title-text" *ngIf="pro.isMobile || !pro.onlyIcon">{{ l1.text }}</span>
|
||||||
|
<div *ngIf="l1.badge" class="alain-pro__menu-title-badge">
|
||||||
|
<em>{{ l1.badge }}</em>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
<ul>
|
||||||
|
<ng-container *ngFor="let l2 of l1.children">
|
||||||
|
<li
|
||||||
|
*ngIf="!l2._hidden && l2.children!.length === 0"
|
||||||
|
nz-menu-item
|
||||||
|
[class.alain-pro__menu-item--disabled]="l2.disabled"
|
||||||
|
[nzSelected]="l2._selected"
|
||||||
|
[nzDisabled]="l2.disabled"
|
||||||
|
(click)="closeCollapsed()"
|
||||||
|
>
|
||||||
|
<ng-template [ngTemplateOutlet]="subLink" [ngTemplateOutletContext]="{ $implicit: l2 }"></ng-template>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
*ngIf="!l2._hidden && l2.children!.length > 0"
|
||||||
|
nz-submenu
|
||||||
|
[nzTitle]="l2.text!"
|
||||||
|
[nzOpen]="l2._open"
|
||||||
|
[nzDisabled]="l2.disabled"
|
||||||
|
(nzOpenChange)="openChange(l2, $event)"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<ng-container *ngFor="let l3 of l2.children">
|
||||||
|
<li
|
||||||
|
*ngIf="!l3._hidden"
|
||||||
|
nz-menu-item
|
||||||
|
[class.alain-pro__menu-item--disabled]="l3.disabled"
|
||||||
|
[nzSelected]="l3._selected"
|
||||||
|
[nzDisabled]="l3.disabled"
|
||||||
|
(click)="closeCollapsed()"
|
||||||
|
>
|
||||||
|
<ng-template [ngTemplateOutlet]="subLink" [ngTemplateOutletContext]="{ $implicit: l3 }"></ng-template>
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
125
src/app/layout/pro/components/menu/menu.component.ts
Normal file
125
src/app/layout/pro/components/menu/menu.component.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { MenuService } from '@delon/theme';
|
||||||
|
import { InputBoolean } from '@delon/util';
|
||||||
|
import { NzMenuModeType } from 'ng-zorro-antd/menu';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { filter, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BrandService } from '../../pro.service';
|
||||||
|
import { ProMenu } from '../../pro.types';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '[layout-pro-menu]',
|
||||||
|
templateUrl: './menu.component.html',
|
||||||
|
host: {
|
||||||
|
'[class.alain-pro__menu]': 'true',
|
||||||
|
'[class.alain-pro__menu-only-icon]': 'pro.onlyIcon'
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProMenuComponent implements OnInit, OnDestroy {
|
||||||
|
private unsubscribe$ = new Subject<void>();
|
||||||
|
menus?: ProMenu[];
|
||||||
|
|
||||||
|
@Input() @InputBoolean() disabledAcl = false;
|
||||||
|
@Input() mode: NzMenuModeType = 'inline';
|
||||||
|
|
||||||
|
constructor(private menuSrv: MenuService, private router: Router, public pro: BrandService, private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
private cd(): void {
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private genMenus(data: ProMenu[]): void {
|
||||||
|
const res: ProMenu[] = [];
|
||||||
|
// ingores category menus
|
||||||
|
const ingoreCategores = data.reduce((prev, cur) => prev.concat(cur.children as ProMenu[]), [] as ProMenu[]);
|
||||||
|
this.menuSrv.visit(ingoreCategores, (item: ProMenu, parent: ProMenu | null) => {
|
||||||
|
if (!item._aclResult) {
|
||||||
|
if (this.disabledAcl) {
|
||||||
|
item.disabled = true;
|
||||||
|
} else {
|
||||||
|
item._hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item._hidden === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parent === null) {
|
||||||
|
res.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.menus = res;
|
||||||
|
|
||||||
|
this.openStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private openStatus(): void {
|
||||||
|
const inFn = (list: ProMenu[]) => {
|
||||||
|
for (const i of list) {
|
||||||
|
i._open = false;
|
||||||
|
i._selected = false;
|
||||||
|
if (i.children!.length > 0) {
|
||||||
|
inFn(i.children!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
inFn(this.menus!);
|
||||||
|
|
||||||
|
let item = this.menuSrv.getHit(this.menus!, this.router.url, true);
|
||||||
|
if (!item) {
|
||||||
|
this.cd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
item._selected = true;
|
||||||
|
if (!this.pro.isTopMenu && !this.pro.collapsed) {
|
||||||
|
item._open = true;
|
||||||
|
}
|
||||||
|
item = item._parent!;
|
||||||
|
} while (item);
|
||||||
|
this.cd();
|
||||||
|
}
|
||||||
|
|
||||||
|
openChange(item: ProMenu, statue: boolean): void {
|
||||||
|
const data = item._parent ? item._parent.children : this.menus;
|
||||||
|
if (data && data.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data!.forEach(i => (i._open = false));
|
||||||
|
item._open = statue;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCollapsed(): void {
|
||||||
|
const { pro } = this;
|
||||||
|
if (pro.isMobile) {
|
||||||
|
setTimeout(() => pro.setCollapsed(true), 25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const { unsubscribe$, router, pro } = this;
|
||||||
|
this.menuSrv.change.pipe(takeUntil(unsubscribe$)).subscribe(res => this.genMenus(res));
|
||||||
|
|
||||||
|
router.events
|
||||||
|
.pipe(
|
||||||
|
takeUntil(unsubscribe$),
|
||||||
|
filter(e => e instanceof NavigationEnd)
|
||||||
|
)
|
||||||
|
.subscribe(() => this.openStatus());
|
||||||
|
|
||||||
|
pro.notify
|
||||||
|
.pipe(
|
||||||
|
takeUntil(unsubscribe$),
|
||||||
|
filter(() => !!this.menus)
|
||||||
|
)
|
||||||
|
.subscribe(() => this.cd());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
const { unsubscribe$ } = this;
|
||||||
|
unsubscribe$.next();
|
||||||
|
unsubscribe$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/layout/pro/components/notify/notify.component.html
Normal file
10
src/app/layout/pro/components/notify/notify.component.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<notice-icon
|
||||||
|
btnClass="alain-pro__header-item"
|
||||||
|
btnIconClass="alain-pro__header-item-icon"
|
||||||
|
[data]="data"
|
||||||
|
[count]="count"
|
||||||
|
[loading]="loading"
|
||||||
|
(select)="select($event)"
|
||||||
|
(clear)="clear($event)"
|
||||||
|
(popoverVisibleChange)="loadData()"
|
||||||
|
></notice-icon>
|
||||||
183
src/app/layout/pro/components/notify/notify.component.ts
Normal file
183
src/app/layout/pro/components/notify/notify.component.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
|
||||||
|
import { NoticeIconList, NoticeItem } from '@delon/abc/notice-icon';
|
||||||
|
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||||
|
import parse from 'date-fns/parse';
|
||||||
|
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-notify',
|
||||||
|
templateUrl: './notify.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetNotifyComponent {
|
||||||
|
data: NoticeItem[] = [
|
||||||
|
{
|
||||||
|
title: '通知',
|
||||||
|
list: [],
|
||||||
|
emptyText: '你已查看所有通知',
|
||||||
|
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
|
||||||
|
clearText: '清空通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息',
|
||||||
|
list: [],
|
||||||
|
emptyText: '您已读完所有消息',
|
||||||
|
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg',
|
||||||
|
clearText: '清空消息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '待办',
|
||||||
|
list: [],
|
||||||
|
emptyText: '你已完成所有待办',
|
||||||
|
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg',
|
||||||
|
clearText: '清空待办'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
count = 5;
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
constructor(private msg: NzMessageService, private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
updateNoticeData(notices: NoticeIconList[]): NoticeItem[] {
|
||||||
|
const data = this.data.slice();
|
||||||
|
data.forEach(i => (i.list = []));
|
||||||
|
|
||||||
|
notices.forEach(item => {
|
||||||
|
const newItem = { ...item };
|
||||||
|
if (typeof newItem.datetime === 'string') {
|
||||||
|
newItem.datetime = parse(newItem.datetime, 'yyyy-MM-dd', new Date());
|
||||||
|
}
|
||||||
|
if (newItem.datetime) {
|
||||||
|
newItem.datetime = formatDistanceToNow(newItem.datetime as Date);
|
||||||
|
}
|
||||||
|
if (newItem.extra && newItem.status) {
|
||||||
|
newItem.color = (
|
||||||
|
{
|
||||||
|
todo: undefined,
|
||||||
|
processing: 'blue',
|
||||||
|
urgent: 'red',
|
||||||
|
doing: 'gold'
|
||||||
|
} as NzSafeAny
|
||||||
|
)[newItem.status];
|
||||||
|
}
|
||||||
|
data.find(w => w.title === newItem.type)?.list.push(newItem);
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData(): void {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.data = this.updateNoticeData([
|
||||||
|
{
|
||||||
|
id: '000000001',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||||
|
title: '你收到了 14 份新周报',
|
||||||
|
datetime: '2017-08-09',
|
||||||
|
type: '通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000002',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
|
||||||
|
title: '你推荐的 曲妮妮 已通过第三轮面试',
|
||||||
|
datetime: '2017-08-08',
|
||||||
|
type: '通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000003',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
|
||||||
|
title: '这种模板可以区分多种通知类型',
|
||||||
|
datetime: '2017-08-07',
|
||||||
|
read: true,
|
||||||
|
type: '通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000004',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||||
|
title: '左侧图标用于区分不同的类型',
|
||||||
|
datetime: '2017-08-07',
|
||||||
|
type: '通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000005',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||||
|
title: '内容不要超过两行字,超出时自动截断',
|
||||||
|
datetime: '2017-08-07',
|
||||||
|
type: '通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000006',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||||
|
title: '曲丽丽 评论了你',
|
||||||
|
description: '描述信息描述信息描述信息',
|
||||||
|
datetime: '2017-08-07',
|
||||||
|
type: '消息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000007',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||||
|
title: '朱偏右 回复了你',
|
||||||
|
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||||
|
datetime: '2017-08-07',
|
||||||
|
type: '消息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000008',
|
||||||
|
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||||
|
title: '标题',
|
||||||
|
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
|
||||||
|
datetime: '2017-08-07',
|
||||||
|
type: '消息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000009',
|
||||||
|
title: '任务名称',
|
||||||
|
description: '任务需要在 2017-01-12 20:00 前启动',
|
||||||
|
extra: '未开始',
|
||||||
|
status: 'todo',
|
||||||
|
type: '待办'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000010',
|
||||||
|
title: '第三方紧急代码变更',
|
||||||
|
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||||
|
extra: '马上到期',
|
||||||
|
status: 'urgent',
|
||||||
|
type: '待办'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000011',
|
||||||
|
title: '信息安全考试',
|
||||||
|
description: '指派竹尔于 2017-01-09 前完成更新并发布',
|
||||||
|
extra: '已耗时 8 天',
|
||||||
|
status: 'doing',
|
||||||
|
type: '待办'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '000000012',
|
||||||
|
title: 'ABCD 版本发布',
|
||||||
|
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
|
||||||
|
extra: '进行中',
|
||||||
|
status: 'processing',
|
||||||
|
type: '待办'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(type: string): void {
|
||||||
|
this.msg.success(`清空了 ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
select(res: any): void {
|
||||||
|
this.msg.success(`点击了 ${res.title} 的 ${res.item.title}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<div class="alain-pro__header-item position-relative" (click)="show()">
|
||||||
|
<nz-badge class="brand-top-right" style="right: 3px; line-height: 34px" nzShowDot [nzStatus]="status"></nz-badge>
|
||||||
|
<i nz-tooltip="Public Chat" nz-icon nzType="message" class="alain-pro__header-item-icon"></i>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
import { LayoutProWidgetQuickChatService } from './quick-chat.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'quick-chat-status',
|
||||||
|
templateUrl: './quick-chat-status.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetQuickChatStatusComponent implements OnInit, OnDestroy {
|
||||||
|
private status$!: Subscription;
|
||||||
|
|
||||||
|
status = 'default';
|
||||||
|
|
||||||
|
constructor(private srv: LayoutProWidgetQuickChatService, private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
show(): void {
|
||||||
|
if (this.srv.showDialog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.srv.showDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.status$ = this.srv.status.subscribe(res => {
|
||||||
|
switch (res) {
|
||||||
|
case 'online':
|
||||||
|
this.status = 'success';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.status = 'default';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.status$.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<div class="quick-chat__bar">
|
||||||
|
<strong class="quick-chat__bar--title" (click)="toggleCollapsed()">
|
||||||
|
<div [ngClass]="{ 'quick-chat__bar--title-has-message': collapsed && hasMessage }">
|
||||||
|
{{ !collapsed && inited ? 'Connecting...' : 'Ng Alain Pro' }}
|
||||||
|
</div>
|
||||||
|
</strong>
|
||||||
|
<i nz-dropdown [nzDropdownMenu]="quickMenu" nz-icon nzType="ellipsis" class="quick-chat__bar--menu rotate-90"></i>
|
||||||
|
<nz-dropdown-menu #quickMenu="nzDropdownMenu">
|
||||||
|
<ul nz-menu nzSelectable>
|
||||||
|
<li nz-menu-item>Add</li>
|
||||||
|
<li nz-menu-item>Edit</li>
|
||||||
|
<li nz-menu-item>Remove</li>
|
||||||
|
</ul>
|
||||||
|
</nz-dropdown-menu>
|
||||||
|
<i nz-icon nzType="close" class="quick-chat__bar--close" (click)="close()"></i>
|
||||||
|
</div>
|
||||||
|
<div class="quick-chat__body" [ngClass]="{ 'quick-chat__collapsed': collapsed }">
|
||||||
|
<div class="quick-chat__content">
|
||||||
|
<div class="chat__scroll-container chat__message-container" scrollbar #messageScrollbar="scrollbarComp">
|
||||||
|
<div *ngFor="let m of messages" class="chat__message chat__message-{{ m.dir }}">
|
||||||
|
<ng-container [ngSwitch]="m.type">
|
||||||
|
<div *ngSwitchCase="'only-text'" class="chat__message-text" [innerHTML]="m.msg"></div>
|
||||||
|
<ng-container *ngSwitchDefault>
|
||||||
|
<div class="chat__message-avatar" *ngIf="m.dir === 'left'">
|
||||||
|
<img class="chat__user-avatar" src="{{ m.mp }}" />
|
||||||
|
</div>
|
||||||
|
<div class="chat__message-msg">
|
||||||
|
<strong class="chat__message-msg--name" *ngIf="m.name">{{ m.name }}</strong>
|
||||||
|
<div class="chat__message-msg--text" *ngIf="m.type === 'text'" [innerHTML]="m.msg"></div>
|
||||||
|
<div class="chat__message-msg--image" *ngIf="m.type === 'image'">
|
||||||
|
<img height="40" src="{{ m.msg }}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-chat__reply">
|
||||||
|
<textarea
|
||||||
|
class="quick-chat__reply--ipt scrollbar"
|
||||||
|
[(ngModel)]="text"
|
||||||
|
(keydown.enter)="enterSend($event)"
|
||||||
|
placeholder="Type your message..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
131
src/app/layout/pro/components/quick-chat/quick-chat.component.ts
Normal file
131
src/app/layout/pro/components/quick-chat/quick-chat.component.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
HostBinding,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { BooleanInput, InputBoolean, InputNumber, NumberInput } from '@delon/util';
|
||||||
|
import { ScrollbarDirective } from '@shared';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { LayoutProWidgetQuickChatService } from './quick-chat.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'quick-chat',
|
||||||
|
templateUrl: './quick-chat.component.html',
|
||||||
|
host: {
|
||||||
|
'[class.quick-chat]': 'true',
|
||||||
|
'[class.quick-chat__collapsed]': 'collapsed',
|
||||||
|
'[class.d-none]': '!showDialog'
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetQuickChatComponent implements OnInit, OnDestroy {
|
||||||
|
static ngAcceptInputType_height: NumberInput;
|
||||||
|
static ngAcceptInputType_width: BooleanInput;
|
||||||
|
static ngAcceptInputType_collapsed: BooleanInput;
|
||||||
|
|
||||||
|
private unsubscribe$ = new Subject<void>();
|
||||||
|
messages: any[] = [
|
||||||
|
{ type: 'only-text', msg: '2018-12-12' },
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
dir: 'left',
|
||||||
|
mp: './assets/logo-color.svg',
|
||||||
|
msg: '请<span class="text-success">一句话</span>描述您的问题,我们来帮您解决并转到合适的人工服务。😎'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
text = '';
|
||||||
|
inited?: boolean;
|
||||||
|
hasMessage = false;
|
||||||
|
|
||||||
|
@ViewChild('messageScrollbar', { static: true }) messageScrollbar?: ScrollbarDirective;
|
||||||
|
|
||||||
|
// #region fileds
|
||||||
|
@Input() @InputNumber() height = 380;
|
||||||
|
@Input() @InputNumber() @HostBinding('style.width.px') width = 320;
|
||||||
|
@Input() @InputBoolean() collapsed = true;
|
||||||
|
@Output() readonly collapsedChange = new EventEmitter<boolean>();
|
||||||
|
@Output() readonly closed = new EventEmitter<boolean>();
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
constructor(private srv: LayoutProWidgetQuickChatService, private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
get showDialog(): boolean {
|
||||||
|
return this.srv.showDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToBottom(): void {
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
setTimeout(() => this.messageScrollbar!.scrollToBottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCollapsed(): void {
|
||||||
|
this.hasMessage = false;
|
||||||
|
this.collapsed = !this.collapsed;
|
||||||
|
this.collapsedChange.emit(this.collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.srv.close();
|
||||||
|
this.closed.emit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
enterSend(e: Event): void {
|
||||||
|
if ((e as KeyboardEvent).keyCode !== 13) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
this.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
send(): boolean {
|
||||||
|
if (!this.text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.inited === 'undefined') {
|
||||||
|
this.inited = true;
|
||||||
|
}
|
||||||
|
const item = {
|
||||||
|
type: 'text',
|
||||||
|
msg: this.text,
|
||||||
|
dir: 'right'
|
||||||
|
};
|
||||||
|
this.srv.send(item);
|
||||||
|
this.messages.push(item);
|
||||||
|
this.text = '';
|
||||||
|
this.scrollToBottom();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const { srv, messages, unsubscribe$ } = this;
|
||||||
|
srv.message.pipe(takeUntil(unsubscribe$)).subscribe(res => {
|
||||||
|
if (this.collapsed) {
|
||||||
|
this.hasMessage = true;
|
||||||
|
}
|
||||||
|
messages.push(res);
|
||||||
|
this.scrollToBottom();
|
||||||
|
});
|
||||||
|
srv.status.pipe(takeUntil(unsubscribe$)).subscribe(res => {
|
||||||
|
this.inited = res === 'online' ? false : undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
const { unsubscribe$ } = this;
|
||||||
|
unsubscribe$.next();
|
||||||
|
unsubscribe$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
|
import { Observable, Subject, Subscription } from 'rxjs';
|
||||||
|
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||||
|
|
||||||
|
export type QuickChatStatus = 'online' | 'offline';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class LayoutProWidgetQuickChatService implements OnDestroy {
|
||||||
|
private url = 'wss://echo.websocket.org/?encoding=text';
|
||||||
|
private _ws!: WebSocketSubject<{}>;
|
||||||
|
private $statusOrg = new Subject();
|
||||||
|
private messageOrg$: Subscription | null = null;
|
||||||
|
private $status = new Subject<QuickChatStatus>();
|
||||||
|
private $message = new Subject<{}>();
|
||||||
|
showDialog = true;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.$statusOrg.subscribe((res: any) => {
|
||||||
|
this.$status.next(res.type === 'open' ? 'online' : 'offline');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get ws(): WebSocketSubject<{}> {
|
||||||
|
return this._ws!;
|
||||||
|
}
|
||||||
|
|
||||||
|
get message(): Observable<{}> {
|
||||||
|
return this.$message.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
get status(): Observable<QuickChatStatus> {
|
||||||
|
return this.$status.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
open(): WebSocketSubject<{}> {
|
||||||
|
if (this._ws) {
|
||||||
|
return this._ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._ws = webSocket({
|
||||||
|
url: this.url,
|
||||||
|
serializer: (value: any) => JSON.stringify(value),
|
||||||
|
deserializer: (e: MessageEvent) => {
|
||||||
|
const res = JSON.parse(e.data);
|
||||||
|
res.dir = 'left';
|
||||||
|
res.mp = './assets/logo-color.svg';
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
openObserver: this.$statusOrg,
|
||||||
|
closeObserver: this.$statusOrg
|
||||||
|
});
|
||||||
|
return this._ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.showDialog = false;
|
||||||
|
if (this.messageOrg$) {
|
||||||
|
this.messageOrg$.unsubscribe();
|
||||||
|
this.messageOrg$ = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(msg: {}): void {
|
||||||
|
if (!this._ws) {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
if (!this.messageOrg$) {
|
||||||
|
this.messageOrg$ = this._ws.subscribe(res => this.$message.next(res));
|
||||||
|
}
|
||||||
|
this._ws.next(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
const { $statusOrg, $status, $message } = this;
|
||||||
|
this.close();
|
||||||
|
$statusOrg.complete();
|
||||||
|
$status.complete();
|
||||||
|
$message.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
<div class="p-md border-bottom-1">
|
||||||
|
<button (click)="changeType(0)" nz-button [nzType]="type === 0 ? 'primary' : 'default'">Notifications</button>
|
||||||
|
<button (click)="changeType(1)" nz-button [nzType]="type === 1 ? 'primary' : 'default'">Actions</button>
|
||||||
|
<button (click)="changeType(2)" nz-button [nzType]="type === 2 ? 'primary' : 'default'">Settings</button>
|
||||||
|
</div>
|
||||||
|
<nz-spin [nzSpinning]="!data">
|
||||||
|
<div *ngIf="!data" class="brand-page-loading"></div>
|
||||||
|
<div *ngIf="data" class="p-lg min-width-lg">
|
||||||
|
<nz-timeline *ngIf="type === 0" class="d-block pl-md pt-md">
|
||||||
|
<nz-timeline-item *ngFor="let i of data.notifications" [nzDot]="dotTpl">
|
||||||
|
<ng-template #dotTpl>
|
||||||
|
<div class="md-sm p-sm icon-sm rounded-circle text-white bg-{{ i.dot.bg }}">
|
||||||
|
<i nz-icon [nzType]="i.dot.icon"></i>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<div class="pl-lg">
|
||||||
|
<strong>{{ i.time }}</strong>
|
||||||
|
<div class="py-sm" [innerHTML]="i.content | html"></div>
|
||||||
|
<div class="text-grey">{{ i.tags }}</div>
|
||||||
|
</div>
|
||||||
|
</nz-timeline-item>
|
||||||
|
</nz-timeline>
|
||||||
|
<ng-container *ngIf="type === 1">
|
||||||
|
<div
|
||||||
|
*ngFor="let i of data.actions; let last = last"
|
||||||
|
class="rounded-md text-white position-relative bg-{{ i.bg }}"
|
||||||
|
[ngClass]="{ 'mb-md': !last }"
|
||||||
|
>
|
||||||
|
<strong class="d-block p-md">{{ i.title }}</strong>
|
||||||
|
<div class="px-md">{{ i.content }}</div>
|
||||||
|
<div class="p-sm text-right">
|
||||||
|
<button (click)="msg.success('Dismiss')" nz-button class="btn-flat text-white text-hover">Dismiss</button>
|
||||||
|
<button (click)="msg.success('View')" nz-button class="btn-flat text-white text-hover">View</button>
|
||||||
|
</div>
|
||||||
|
<span nz-dropdown [nzDropdownMenu]="actionMenu" nzPlacement="bottomRight" class="dd-btn brand-top-right text-white">
|
||||||
|
<i nz-icon nzType="ellipsis"></i>
|
||||||
|
</span>
|
||||||
|
<nz-dropdown-menu #actionMenu="nzDropdownMenu">
|
||||||
|
<ul nz-menu>
|
||||||
|
<li nz-menu-item (click)="msg.success('Item1')">Item1</li>
|
||||||
|
<li nz-menu-item (click)="msg.success('Item2')">Item2</li>
|
||||||
|
<li nz-menu-divider></li>
|
||||||
|
<li nz-menu-item (click)="msg.success('Item3')">Item3</li>
|
||||||
|
</ul>
|
||||||
|
</nz-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="type === 2">
|
||||||
|
<h3 class="setting-drawer__title">Notifications</h3>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Enable notifications:
|
||||||
|
<nz-switch [(ngModel)]="data.settings.notification" (ngModelChange)="updateSetting('notification', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Enable audit log:
|
||||||
|
<nz-switch [(ngModel)]="data.settings.audit_log" (ngModelChange)="updateSetting('audit_log', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Notify on new orders:
|
||||||
|
<nz-switch [(ngModel)]="data.settings.new_order" (ngModelChange)="updateSetting('new_order', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<h3 class="setting-drawer__title mt-md">Orders</h3>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Enable order tracking:
|
||||||
|
<nz-switch [(ngModel)]="data.settings.tracking_order" (ngModelChange)="updateSetting('tracking_order', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Enable orders reports:
|
||||||
|
<nz-switch [(ngModel)]="data.settings.reports_order" (ngModelChange)="updateSetting('reports_order', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<h3 class="setting-drawer__title mt-md">Customers</h3>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Enable customer singup:
|
||||||
|
<nz-switch [(ngModel)]="data.settings.new_customer" (ngModelChange)="updateSetting('new_customer', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Enable customers reporting:
|
||||||
|
<nz-switch [(ngModel)]="data.settings.reporting_customer" (ngModelChange)="updateSetting('reporting_customer', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<h3 class="setting-drawer__title mt-md">Other</h3>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
Weak Mode:
|
||||||
|
<nz-switch [(ngModel)]="layout.colorWeak" (ngModelChange)="setLayout('colorWeak', $event)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</nz-spin>
|
||||||
42
src/app/layout/pro/components/quick/quick-panel.component.ts
Normal file
42
src/app/layout/pro/components/quick/quick-panel.component.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { _HttpClient } from '@delon/theme';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
|
||||||
|
import { BrandService } from '../../pro.service';
|
||||||
|
import { ProLayout } from '../../pro.types';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-quick-panel',
|
||||||
|
templateUrl: './quick-panel.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetQuickPanelComponent implements OnInit {
|
||||||
|
type = 0;
|
||||||
|
data: any;
|
||||||
|
get layout(): ProLayout {
|
||||||
|
return this.pro.layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private pro: BrandService, private http: _HttpClient, private cd: ChangeDetectorRef, public msg: NzMessageService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.http.get('/quick').subscribe(res => {
|
||||||
|
this.data = res;
|
||||||
|
this.changeType(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeType(type: number): void {
|
||||||
|
this.type = type;
|
||||||
|
// wait checkbox & switch render
|
||||||
|
setTimeout(() => this.cd.detectChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSetting(_type: string, _value: any): void {
|
||||||
|
this.msg.success('Success!');
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayout(name: string, value: any): void {
|
||||||
|
this.pro.setLayout(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/app/layout/pro/components/quick/quick.component.html
Normal file
1
src/app/layout/pro/components/quick/quick.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<i nz-tooltip="Quick panel" nz-icon nzType="appstore" class="alain-pro__header-item-icon"></i>
|
||||||
51
src/app/layout/pro/components/quick/quick.component.ts
Normal file
51
src/app/layout/pro/components/quick/quick.component.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Direction, Directionality } from '@angular/cdk/bidi';
|
||||||
|
import { ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||||
|
import { DrawerHelper } from '@delon/theme';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { LayoutProWidgetQuickPanelComponent } from './quick-panel.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-quick',
|
||||||
|
templateUrl: './quick.component.html',
|
||||||
|
host: {
|
||||||
|
'[class.alain-pro__header-item]': 'true'
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetQuickComponent implements OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private dir: Direction = 'ltr';
|
||||||
|
|
||||||
|
constructor(private drawerHelper: DrawerHelper, @Optional() private directionality: Directionality) {}
|
||||||
|
|
||||||
|
@HostListener('click')
|
||||||
|
show(): void {
|
||||||
|
this.drawerHelper
|
||||||
|
.create(``, LayoutProWidgetQuickPanelComponent, null, {
|
||||||
|
size: 480,
|
||||||
|
drawerOptions: {
|
||||||
|
nzTitle: undefined,
|
||||||
|
nzPlacement: this.dir === 'rtl' ? 'left' : 'right',
|
||||||
|
nzBodyStyle: {
|
||||||
|
'min-height': '100%',
|
||||||
|
padding: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.dir = this.directionality.value;
|
||||||
|
this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => {
|
||||||
|
this.dir = direction;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/layout/pro/components/rtl/rtl.component.ts
Normal file
23
src/app/layout/pro/components/rtl/rtl.component.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
|
||||||
|
import { RTLService } from '@delon/theme';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-rtl',
|
||||||
|
template: `
|
||||||
|
<button nz-button nzType="link" class="alain-pro__header-item-icon">
|
||||||
|
{{ rtl.nextDir | uppercase }}
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
host: {
|
||||||
|
'[class.alain-pro__header-item]': 'true'
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetRTLComponent {
|
||||||
|
constructor(public rtl: RTLService) {}
|
||||||
|
|
||||||
|
@HostListener('click')
|
||||||
|
toggleDirection(): void {
|
||||||
|
this.rtl.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<i nz-icon nzType="search"></i>
|
||||||
|
<div class="alain-pro__header-search-input ant-select-auto-complete ant-select">
|
||||||
|
<input #ipt placeholder="站内搜索" nz-input [nzAutocomplete]="searchAuto" [(ngModel)]="q" (input)="onSearch()" (blur)="show = false" />
|
||||||
|
</div>
|
||||||
|
<nz-autocomplete #searchAuto>
|
||||||
|
<nz-auto-option *ngFor="let item of list" [nzValue]="item.no">
|
||||||
|
{{ item.no }}
|
||||||
|
</nz-auto-option>
|
||||||
|
</nz-autocomplete>
|
||||||
49
src/app/layout/pro/components/search/search.component.ts
Normal file
49
src/app/layout/pro/components/search/search.component.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';
|
||||||
|
import { _HttpClient } from '@delon/theme';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-search',
|
||||||
|
templateUrl: 'search.component.html',
|
||||||
|
host: {
|
||||||
|
'[class.alain-pro__header-item]': 'true',
|
||||||
|
'[class.alain-pro__header-search]': 'true',
|
||||||
|
'[class.alain-pro__header-search-show]': 'show'
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetSearchComponent implements OnDestroy {
|
||||||
|
@ViewChild('ipt', { static: true }) private ipt!: ElementRef<HTMLInputElement>;
|
||||||
|
show = false;
|
||||||
|
q = '';
|
||||||
|
search$ = new Subject<string>();
|
||||||
|
list: any[] = [];
|
||||||
|
|
||||||
|
constructor(http: _HttpClient, cdr: ChangeDetectorRef) {
|
||||||
|
this.search$
|
||||||
|
.pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((q: string) => http.get('/user', { no: q, pi: 1, ps: 5 }))
|
||||||
|
)
|
||||||
|
.subscribe((res: any) => {
|
||||||
|
this.list = res.list;
|
||||||
|
cdr.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(): void {
|
||||||
|
this.search$.next(this.ipt.nativeElement.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('click')
|
||||||
|
_click(): void {
|
||||||
|
this.ipt.nativeElement.focus();
|
||||||
|
this.show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.search$.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/app/layout/pro/components/user/user.component.html
Normal file
25
src/app/layout/pro/components/user/user.component.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<div nz-dropdown [nzDropdownMenu]="userMenu" nzPlacement="bottomRight" class="alain-pro__header-item">
|
||||||
|
<nz-avatar [nzSrc]="settings.user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
|
||||||
|
{{ settings.user.name }}
|
||||||
|
</div>
|
||||||
|
<nz-dropdown-menu #userMenu="nzDropdownMenu">
|
||||||
|
<div nz-menu class="width-sm">
|
||||||
|
<div nz-menu-item routerLink="/pro/account/center">
|
||||||
|
<i nz-icon nzType="user" class="mr-sm"></i>
|
||||||
|
个人中心
|
||||||
|
</div>
|
||||||
|
<div nz-menu-item routerLink="/pro/account/settings">
|
||||||
|
<i nz-icon nzType="setting" class="mr-sm"></i>
|
||||||
|
个人设置
|
||||||
|
</div>
|
||||||
|
<div nz-menu-item routerLink="/exception/trigger">
|
||||||
|
<i nz-icon nzType="close-circle" class="mr-sm"></i>
|
||||||
|
触发错误
|
||||||
|
</div>
|
||||||
|
<li nz-menu-divider></li>
|
||||||
|
<div nz-menu-item (click)="logout()">
|
||||||
|
<i nz-icon nzType="logout" class="mr-sm"></i>
|
||||||
|
退出登录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nz-dropdown-menu>
|
||||||
29
src/app/layout/pro/components/user/user.component.ts
Normal file
29
src/app/layout/pro/components/user/user.component.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||||
|
import { SettingsService } from '@delon/theme';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro-user',
|
||||||
|
templateUrl: 'user.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProWidgetUserComponent implements OnInit {
|
||||||
|
constructor(public settings: SettingsService, private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// mock
|
||||||
|
const token = this.tokenService.get() || {
|
||||||
|
token: 'nothing',
|
||||||
|
name: 'Admin',
|
||||||
|
avatar: './assets/logo-color.svg',
|
||||||
|
email: 'cipchk@qq.com'
|
||||||
|
};
|
||||||
|
this.tokenService.set(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.tokenService.clear();
|
||||||
|
this.router.navigateByUrl(this.tokenService.login_url!);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/layout/pro/components/widget/widget.component.html
Normal file
19
src/app/layout/pro/components/widget/widget.component.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!--Search-->
|
||||||
|
<layout-pro-search class="hidden-xs"></layout-pro-search>
|
||||||
|
<!--Link-->
|
||||||
|
<!-- <a nz-tooltip nzTooltipTitle="使用文档" nzTooltipPlacement="bottom" class="hidden-xs" target="_blank"
|
||||||
|
href="https://e.ng-alain.com/theme/pro" rel="noopener noreferrer" class="alain-pro__header-item">
|
||||||
|
<i nz-icon nzType="question-circle"></i>
|
||||||
|
</a> -->
|
||||||
|
<!--Quick chat status-->
|
||||||
|
<!-- <quick-chat-status class="hidden-xs"></quick-chat-status> -->
|
||||||
|
<!--Notify-->
|
||||||
|
<layout-pro-notify class="hidden-xs"></layout-pro-notify>
|
||||||
|
<!--RTL-->
|
||||||
|
<!-- <layout-pro-rtl></layout-pro-rtl> -->
|
||||||
|
<!--User-->
|
||||||
|
<layout-pro-user></layout-pro-user>
|
||||||
|
<!--Languages-->
|
||||||
|
<!-- <pro-langs></pro-langs> -->
|
||||||
|
<!--Quick panel-->
|
||||||
|
<!-- <layout-pro-quick class="hidden-xs"></layout-pro-quick> -->
|
||||||
20
src/app/layout/pro/components/widget/widget.component.ts
Normal file
20
src/app/layout/pro/components/widget/widget.component.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: '[layout-pro-header-widget]',
|
||||||
|
templateUrl: './widget.component.html',
|
||||||
|
host: {
|
||||||
|
'[class.alain-pro__header-right]': 'true'
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProHeaderWidgetComponent {
|
||||||
|
constructor(private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.tokenService.clear();
|
||||||
|
this.router.navigateByUrl(this.tokenService.login_url!);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/app/layout/pro/index.md
Normal file
1
src/app/layout/pro/index.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Document](https://e.ng-alain.com/theme/pro)
|
||||||
67
src/app/layout/pro/index.ts
Normal file
67
src/app/layout/pro/index.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/* eslint-disable import/order */
|
||||||
|
// #region exports
|
||||||
|
|
||||||
|
export * from './pro.types';
|
||||||
|
export * from './pro.service';
|
||||||
|
export * from './pro.component';
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region widgets
|
||||||
|
|
||||||
|
import { LayoutProFooterComponent } from './components/footer/footer.component';
|
||||||
|
import { LayoutProHeaderComponent } from './components/header/header.component';
|
||||||
|
import { LayoutProLogoComponent } from './components/logo/logo.component';
|
||||||
|
import { LayoutProMenuComponent } from './components/menu/menu.component';
|
||||||
|
import { LayoutProWidgetNotifyComponent } from './components/notify/notify.component';
|
||||||
|
import { LayoutProWidgetQuickChatStatusComponent } from './components/quick-chat/quick-chat-status.component';
|
||||||
|
import { LayoutProWidgetQuickChatComponent } from './components/quick-chat/quick-chat.component';
|
||||||
|
import { LayoutProWidgetQuickComponent } from './components/quick/quick.component';
|
||||||
|
import { LayoutProWidgetRTLComponent } from './components/rtl/rtl.component';
|
||||||
|
import { LayoutProWidgetSearchComponent } from './components/search/search.component';
|
||||||
|
import { LayoutProWidgetUserComponent } from './components/user/user.component';
|
||||||
|
import { LayoutProHeaderWidgetComponent } from './components/widget/widget.component';
|
||||||
|
import { LayoutProWidgetQuickPanelComponent } from './components/quick/quick-panel.component';
|
||||||
|
import { ProSettingDrawerComponent } from './setting-drawer/setting-drawer.component';
|
||||||
|
|
||||||
|
const PRO_WIDGETS = [
|
||||||
|
LayoutProHeaderWidgetComponent,
|
||||||
|
LayoutProWidgetNotifyComponent,
|
||||||
|
LayoutProWidgetSearchComponent,
|
||||||
|
LayoutProWidgetUserComponent,
|
||||||
|
LayoutProWidgetQuickComponent,
|
||||||
|
LayoutProWidgetQuickChatComponent,
|
||||||
|
LayoutProWidgetQuickChatStatusComponent,
|
||||||
|
LayoutProWidgetRTLComponent
|
||||||
|
];
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region entry components
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region components
|
||||||
|
|
||||||
|
import { LayoutProComponent } from './pro.component';
|
||||||
|
|
||||||
|
export const PRO_COMPONENTS: Array<Type<void>> = [
|
||||||
|
LayoutProComponent,
|
||||||
|
LayoutProMenuComponent,
|
||||||
|
LayoutProLogoComponent,
|
||||||
|
LayoutProHeaderComponent,
|
||||||
|
LayoutProFooterComponent,
|
||||||
|
LayoutProWidgetQuickPanelComponent,
|
||||||
|
ProSettingDrawerComponent,
|
||||||
|
...PRO_WIDGETS
|
||||||
|
];
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region shared components
|
||||||
|
|
||||||
|
import { ProPageModule } from './shared/page';
|
||||||
|
import { Type } from '@angular/core';
|
||||||
|
export const PRO_SHARED_MODULES = [ProPageModule];
|
||||||
|
|
||||||
|
// #endregion
|
||||||
40
src/app/layout/pro/pro.component.html
Normal file
40
src/app/layout/pro/pro.component.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<ng-template #sideTpl>
|
||||||
|
<nz-sider [nzTrigger]="null" [nzCollapsible]="true" [nzCollapsed]="isMobile ? false : pro.collapsed"
|
||||||
|
[nzWidth]="pro.width" [nzCollapsedWidth]="pro.widthInCollapsed" class="alain-pro__sider"
|
||||||
|
[ngClass]="{ 'alain-pro__sider-fixed': pro.fixSiderbar }">
|
||||||
|
<layout-pro-logo class="alain-pro__sider-logo"></layout-pro-logo>
|
||||||
|
<div class="alain-pro__side-nav" style="width: 100%; padding: 16px 0">
|
||||||
|
<div class="alain-pro__side-nav-wrap" layout-pro-menu></div>
|
||||||
|
</div>
|
||||||
|
</nz-sider>
|
||||||
|
</ng-template>
|
||||||
|
<div class="ant-layout ant-layout-has-sider">
|
||||||
|
<ng-container *ngIf="pro.menu === 'side' || isMobile">
|
||||||
|
<nz-drawer *ngIf="isMobile" [nzWidth]="pro.width" nzWrapClassName="alain-pro__drawer" [nzVisible]="!pro.collapsed"
|
||||||
|
[nzClosable]="false" nzPlacement="left" (nzOnClose)="pro.setCollapsed(true)">
|
||||||
|
<ng-template nzDrawerContent>
|
||||||
|
<ng-template [ngTemplateOutlet]="sideTpl"></ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</nz-drawer>
|
||||||
|
<ng-container *ngIf="!isMobile">
|
||||||
|
<ng-template [ngTemplateOutlet]="sideTpl"></ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<div class="ant-layout alain-pro__main" [ngStyle]="getLayoutStyle">
|
||||||
|
<layout-pro-header></layout-pro-header>
|
||||||
|
<!--
|
||||||
|
NOTICE: Route reuse strategy tag placeholder, please refer to: https://ng-alain.com/components/reuse-tab
|
||||||
|
- Not supported top header fixed mode
|
||||||
|
```html
|
||||||
|
<reuse-tab></reuse-tab>
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
<div class="ant-layout-content alain-pro__body" [class.alain-pro__fetching]="isFetching"
|
||||||
|
[ngStyle]="getContentStyle">
|
||||||
|
<nz-spin class="alain-pro__fetching-icon" nzSpinning></nz-spin>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #settingHost></ng-template>
|
||||||
|
<theme-btn></theme-btn>
|
||||||
167
src/app/layout/pro/pro.component.ts
Normal file
167
src/app/layout/pro/pro.component.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { BreakpointObserver, MediaMatcher } from '@angular/cdk/layout';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ComponentFactoryResolver,
|
||||||
|
Inject,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Renderer2,
|
||||||
|
ViewChild,
|
||||||
|
ViewContainerRef
|
||||||
|
} from '@angular/core';
|
||||||
|
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router } from '@angular/router';
|
||||||
|
import { ReuseTabService } from '@delon/abc/reuse-tab';
|
||||||
|
import { RTL, RTLService } from '@delon/theme';
|
||||||
|
import { ScrollService, updateHostClass } from '@delon/util/browser';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BrandService } from './pro.service';
|
||||||
|
import { ProSettingDrawerComponent } from './setting-drawer/setting-drawer.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'layout-pro',
|
||||||
|
templateUrl: './pro.component.html'
|
||||||
|
// NOTICE: If all pages using OnPush mode, you can turn it on and all `cdr.detectChanges()` codes
|
||||||
|
// changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class LayoutProComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private queryCls?: string;
|
||||||
|
@ViewChild('settingHost', { read: ViewContainerRef, static: false }) private settingHost!: ViewContainerRef;
|
||||||
|
|
||||||
|
isFetching = false;
|
||||||
|
|
||||||
|
get isMobile(): boolean {
|
||||||
|
return this.pro.isMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
get getLayoutStyle(): { [key: string]: string } | null {
|
||||||
|
const { isMobile, fixSiderbar, collapsed, menu, width, widthInCollapsed } = this.pro;
|
||||||
|
if (fixSiderbar && menu !== 'top' && !isMobile) {
|
||||||
|
return {
|
||||||
|
[this.rtl.dir === RTL ? 'paddingRight' : 'paddingLeft']: `${collapsed ? widthInCollapsed : width}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get getContentStyle(): { [key: string]: string } {
|
||||||
|
const { fixedHeader, headerHeight } = this.pro;
|
||||||
|
return {
|
||||||
|
margin: '24px 24px 0',
|
||||||
|
'padding-top': `${fixedHeader ? headerHeight : 0}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private get body(): HTMLElement {
|
||||||
|
return this.doc.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
bm: BreakpointObserver,
|
||||||
|
mediaMatcher: MediaMatcher,
|
||||||
|
router: Router,
|
||||||
|
msg: NzMessageService,
|
||||||
|
scroll: ScrollService,
|
||||||
|
reuseTabSrv: ReuseTabService,
|
||||||
|
private resolver: ComponentFactoryResolver,
|
||||||
|
private renderer: Renderer2,
|
||||||
|
public pro: BrandService,
|
||||||
|
@Inject(DOCUMENT) private doc: any,
|
||||||
|
// private cdr: ChangeDetectorRef
|
||||||
|
private rtl: RTLService
|
||||||
|
) {
|
||||||
|
// scroll to top in change page
|
||||||
|
router.events.pipe(takeUntil(this.destroy$)).subscribe(evt => {
|
||||||
|
if (!this.isFetching && evt instanceof RouteConfigLoadStart) {
|
||||||
|
this.isFetching = true;
|
||||||
|
scroll.scrollToTop();
|
||||||
|
}
|
||||||
|
if (evt instanceof NavigationError) {
|
||||||
|
this.isFetching = false;
|
||||||
|
msg.error(`无法加载${evt.url}路由`, { nzDuration: 1000 * 3 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(evt instanceof NavigationEnd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isFetching = false;
|
||||||
|
// If have already cached router, should be don't need scroll to top
|
||||||
|
if (!reuseTabSrv.exists(evt.url)) {
|
||||||
|
scroll.scrollToTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// media
|
||||||
|
const query: { [key: string]: string } = {
|
||||||
|
'screen-xs': '(max-width: 575px)',
|
||||||
|
'screen-sm': '(min-width: 576px) and (max-width: 767px)',
|
||||||
|
'screen-md': '(min-width: 768px) and (max-width: 991px)',
|
||||||
|
'screen-lg': '(min-width: 992px) and (max-width: 1199px)',
|
||||||
|
'screen-xl': '(min-width: 1200px)'
|
||||||
|
};
|
||||||
|
bm.observe([
|
||||||
|
'(min-width: 1200px)',
|
||||||
|
'(min-width: 992px) and (max-width: 1199px)',
|
||||||
|
'(min-width: 768px) and (max-width: 991px)',
|
||||||
|
'(min-width: 576px) and (max-width: 767px)',
|
||||||
|
'(max-width: 575px)'
|
||||||
|
]).subscribe(() => {
|
||||||
|
this.queryCls = Object.keys(query).find(key => mediaMatcher.matchMedia(query[key]).matches);
|
||||||
|
this.setClass();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setClass(): void {
|
||||||
|
const { body, renderer, queryCls, pro } = this;
|
||||||
|
updateHostClass(body, renderer, {
|
||||||
|
['color-weak']: pro.layout.colorWeak,
|
||||||
|
[`layout-fixed`]: pro.layout.fixed,
|
||||||
|
[`aside-collapsed`]: pro.collapsed,
|
||||||
|
['alain-pro']: true,
|
||||||
|
[queryCls!]: true,
|
||||||
|
[`alain-pro__content-${pro.layout.contentWidth}`]: true,
|
||||||
|
[`alain-pro__fixed`]: pro.layout.fixedHeader,
|
||||||
|
[`alain-pro__wide`]: pro.isFixed,
|
||||||
|
[`alain-pro__dark`]: pro.theme === 'dark',
|
||||||
|
[`alain-pro__light`]: pro.theme === 'light',
|
||||||
|
[`alain-pro__menu-side`]: pro.isSideMenu,
|
||||||
|
[`alain-pro__menu-top`]: pro.isTopMenu
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
// Setting componet for only developer
|
||||||
|
if (!environment.production) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const settingFactory = this.resolver.resolveComponentFactory(ProSettingDrawerComponent);
|
||||||
|
this.settingHost.createComponent(settingFactory);
|
||||||
|
}, 22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const { pro, destroy$ } = this;
|
||||||
|
pro.notify.pipe(takeUntil(destroy$)).subscribe(() => {
|
||||||
|
this.setClass();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
const { destroy$, body, pro } = this;
|
||||||
|
destroy$.next();
|
||||||
|
destroy$.complete();
|
||||||
|
body.classList.remove(
|
||||||
|
`alain-pro__content-${pro.layout.contentWidth}`,
|
||||||
|
`alain-pro__fixed`,
|
||||||
|
`alain-pro__wide`,
|
||||||
|
`alain-pro__dark`,
|
||||||
|
`alain-pro__light`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/app/layout/pro/pro.service.ts
Normal file
139
src/app/layout/pro/pro.service.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Layout, SettingsService } from '@delon/theme';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { ProLayout, ProLayoutContentWidth, ProLayoutMenu, ProLayoutTheme } from './pro.types';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class BrandService {
|
||||||
|
private notify$ = new BehaviorSubject<string | null>(null);
|
||||||
|
private _isMobile = false;
|
||||||
|
|
||||||
|
// #region fields
|
||||||
|
|
||||||
|
get notify(): Observable<string | null> {
|
||||||
|
return this.notify$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify width of the sidebar, If you change it, muse be synchronize change less parameter:
|
||||||
|
* ```less
|
||||||
|
* @alain-pro-sider-menu-width: 256px;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
readonly width = 256;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify width of the sidebar after collapsed, If you change it, muse be synchronize change less parameter:
|
||||||
|
* ```less
|
||||||
|
* @menu-collapsed-width: 80px;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
readonly widthInCollapsed = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify height of the header, If you change it, muse be synchronize change less parameter:
|
||||||
|
* ```less
|
||||||
|
* @alain-pro-header-height: 64px;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
readonly headerHeight = 64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify distance from top for automatically hidden header
|
||||||
|
*/
|
||||||
|
readonly autoHideHeaderTop = 300;
|
||||||
|
|
||||||
|
get isMobile(): boolean {
|
||||||
|
return this._isMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
get layout(): ProLayout {
|
||||||
|
return this.settings.layout as ProLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
get collapsed(): boolean {
|
||||||
|
return this.layout.collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get theme(): ProLayoutTheme {
|
||||||
|
return this.layout.theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
get menu(): ProLayoutMenu {
|
||||||
|
return this.layout.menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
get contentWidth(): ProLayoutContentWidth {
|
||||||
|
return this.layout.contentWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fixedHeader(): boolean {
|
||||||
|
return this.layout.fixedHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
get autoHideHeader(): boolean {
|
||||||
|
return this.layout.autoHideHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fixSiderbar(): boolean {
|
||||||
|
return this.layout.fixSiderbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
get onlyIcon(): boolean {
|
||||||
|
return this.menu === 'side' ? false : this.layout.onlyIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the top menu */
|
||||||
|
get isTopMenu(): boolean {
|
||||||
|
return this.menu === 'top' && !this.isMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the side menu */
|
||||||
|
get isSideMenu(): boolean {
|
||||||
|
return this.menu === 'side' && !this.isMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the fixed content */
|
||||||
|
get isFixed(): boolean {
|
||||||
|
return this.contentWidth === 'fixed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
constructor(bm: BreakpointObserver, private settings: SettingsService) {
|
||||||
|
// fix layout data
|
||||||
|
settings.setLayout({
|
||||||
|
theme: 'dark',
|
||||||
|
menu: 'side',
|
||||||
|
contentWidth: 'fluid',
|
||||||
|
fixedHeader: false,
|
||||||
|
autoHideHeader: false,
|
||||||
|
fixSiderbar: false,
|
||||||
|
onlyIcon: true,
|
||||||
|
...(environment as any).pro,
|
||||||
|
...settings.layout // Browser cache
|
||||||
|
});
|
||||||
|
|
||||||
|
const mobileMedia = 'only screen and (max-width: 767.99px)';
|
||||||
|
bm.observe(mobileMedia).subscribe(state => this.checkMedia(state.matches));
|
||||||
|
this.checkMedia(bm.isMatched(mobileMedia));
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkMedia(value: boolean): void {
|
||||||
|
this._isMobile = value;
|
||||||
|
this.layout.collapsed = this._isMobile;
|
||||||
|
this.notify$.next('mobile');
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayout(name: string | Layout, value?: any): void {
|
||||||
|
this.settings.setLayout(name, value);
|
||||||
|
this.notify$.next('layout');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCollapsed(status?: boolean): void {
|
||||||
|
this.setLayout('collapsed', typeof status !== 'undefined' ? status : !this.collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/app/layout/pro/pro.types.ts
Normal file
43
src/app/layout/pro/pro.types.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Layout, MenuInner } from '@delon/theme';
|
||||||
|
|
||||||
|
export type ProLayoutTheme = 'light' | 'dark';
|
||||||
|
export type ProLayoutMenu = 'side' | 'top';
|
||||||
|
export type ProLayoutContentWidth = 'fluid' | 'fixed';
|
||||||
|
|
||||||
|
export interface ProLayout extends Layout {
|
||||||
|
theme: ProLayoutTheme;
|
||||||
|
/**
|
||||||
|
* menu position
|
||||||
|
*/
|
||||||
|
menu: ProLayoutMenu;
|
||||||
|
/**
|
||||||
|
* layout of content, only works when menu is top
|
||||||
|
*/
|
||||||
|
contentWidth: ProLayoutContentWidth;
|
||||||
|
/**
|
||||||
|
* sticky header
|
||||||
|
*/
|
||||||
|
fixedHeader: boolean;
|
||||||
|
/**
|
||||||
|
* auto hide header
|
||||||
|
*/
|
||||||
|
autoHideHeader: boolean;
|
||||||
|
/**
|
||||||
|
* sticky siderbar
|
||||||
|
*/
|
||||||
|
fixSiderbar: boolean;
|
||||||
|
/**
|
||||||
|
* Only icon of menu
|
||||||
|
* Limited to a temporary solution [#2183](https://github.com/NG-ZORRO/ng-zorro-antd/issues/2183)
|
||||||
|
*/
|
||||||
|
onlyIcon: boolean;
|
||||||
|
/**
|
||||||
|
* Color weak
|
||||||
|
*/
|
||||||
|
colorWeak: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProMenu extends MenuInner {
|
||||||
|
_parent?: ProMenu | null;
|
||||||
|
children?: ProMenu[];
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<nz-drawer [(nzVisible)]="collapse" [nzPlacement]="dir === 'rtl' ? 'left' : 'right'" [nzWidth]="300"
|
||||||
|
(nzOnClose)="toggle()">
|
||||||
|
<div *nzDrawerContent class="setting-drawer__content">
|
||||||
|
<div class="setting-drawer__body">
|
||||||
|
<h3 class="setting-drawer__title">整体风格设置</h3>
|
||||||
|
<div class="setting-drawer__blockChecbox">
|
||||||
|
<div *ngFor="let t of themes" class="setting-drawer__blockChecbox-item" (click)="setLayout('theme', t.key)"
|
||||||
|
[nz-tooltip]="t.title">
|
||||||
|
<img src="{{ t.img }}" alt="{{ t.key }}" />
|
||||||
|
<div *ngIf="layout.theme === t.key" class="setting-drawer__blockChecbox-selectIcon">
|
||||||
|
<i nz-icon nzType="check"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body setting-drawer__theme">
|
||||||
|
<h3 class="setting-drawer__title">主题色</h3>
|
||||||
|
<span *ngFor="let c of colors" (click)="changeColor(c.color)" nz-tooltip="c.key" class="setting-drawer__theme-tag"
|
||||||
|
[ngStyle]="{ 'background-color': c.color }">
|
||||||
|
<i *ngIf="color === c.color" nz-icon nzType="check"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<nz-divider></nz-divider>
|
||||||
|
<div class="setting-drawer__body">
|
||||||
|
<h3 class="setting-drawer__title">导航模式</h3>
|
||||||
|
<div class="setting-drawer__blockChecbox">
|
||||||
|
<div *ngFor="let t of menuModes" class="setting-drawer__blockChecbox-item" (click)="setLayout('menu', t.key)"
|
||||||
|
nz-tooltip="{{ t.title }}">
|
||||||
|
<img src="{{ t.img }}" alt="{{ t.key }}" />
|
||||||
|
<div *ngIf="layout.menu === t.key" class="setting-drawer__blockChecbox-selectIcon">
|
||||||
|
<i nz-icon nzType="check"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
内容区域宽度
|
||||||
|
<nz-select [(ngModel)]="layout.contentWidth" (ngModelChange)="setLayout('contentWidth', layout.contentWidth)"
|
||||||
|
nzSize="small">
|
||||||
|
<nz-option *ngFor="let i of contentWidths" [nzLabel]="i.title" [nzValue]="i.key" [nzDisabled]="i.disabled">
|
||||||
|
</nz-option>
|
||||||
|
</nz-select>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
固定 Header
|
||||||
|
<nz-switch nzSize="small" [(ngModel)]="layout.fixedHeader"
|
||||||
|
(ngModelChange)="setLayout('fixedHeader', layout.fixedHeader)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item" nz-tooltip="{{ !brand.fixedHeader ? '固定 Header 时可配置' : '' }}"
|
||||||
|
nzTooltipPlacement="left">
|
||||||
|
<span [style.opacity]="!brand.fixedHeader ? 0.5 : 1">下滑时隐藏 Header</span>
|
||||||
|
<nz-switch [nzDisabled]="!brand.fixedHeader" nzSize="small" [(ngModel)]="layout.autoHideHeader"
|
||||||
|
(ngModelChange)="setLayout('autoHideHeader', layout.autoHideHeader)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item" nz-tooltip="{{ brand.menu === 'top' ? '侧边菜单布局时可配置' : '' }}"
|
||||||
|
nzTooltipPlacement="left">
|
||||||
|
<span [style.opacity]="brand.menu === 'top' ? 0.5 : 1">固定侧边菜单</span>
|
||||||
|
<nz-switch [nzDisabled]="brand.menu === 'top'" nzSize="small" [(ngModel)]="layout.fixSiderbar"
|
||||||
|
(ngModelChange)="setLayout('fixSiderbar', layout.fixSiderbar)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer__body-item" nz-tooltip="{{ brand.menu === 'top' ? '' : '顶部菜单布局时可配置' }}"
|
||||||
|
nzTooltipPlacement="left">
|
||||||
|
<span [style.opacity]="brand.menu !== 'top' ? 0.5 : 1">只显示图标</span>
|
||||||
|
<nz-switch [nzDisabled]="brand.menu !== 'top'" nzSize="small" [(ngModel)]="layout.onlyIcon"
|
||||||
|
(ngModelChange)="setLayout('onlyIcon', layout.onlyIcon)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nz-divider></nz-divider>
|
||||||
|
<div class="setting-drawer__body">
|
||||||
|
<h3 class="setting-drawer__title">其他设置</h3>
|
||||||
|
<div class="setting-drawer__body-item">
|
||||||
|
色弱模式
|
||||||
|
<nz-switch nzSize="small" [(ngModel)]="layout.colorWeak"
|
||||||
|
(ngModelChange)="setLayout('colorWeak', layout.colorWeak)"></nz-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nz-divider></nz-divider>
|
||||||
|
<button (click)="copy()" type="button" nz-button nzBlock>拷贝设置</button>
|
||||||
|
<nz-alert class="mt-md" nzType="warning" nzMessage="配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件"></nz-alert>
|
||||||
|
</div>
|
||||||
|
</nz-drawer>
|
||||||
|
<div class="setting-drawer__handle" [ngClass]="{ 'setting-drawer__handle-opened': collapse }" (click)="toggle()">
|
||||||
|
<i nz-icon [nzType]="!collapse ? 'setting' : 'close'" class="setting-drawer__handle-icon"></i>
|
||||||
|
</div>
|
||||||
240
src/app/layout/pro/setting-drawer/setting-drawer.component.ts
Normal file
240
src/app/layout/pro/setting-drawer/setting-drawer.component.ts
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
import { Direction, Directionality } from '@angular/cdk/bidi';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, NgZone, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||||
|
import { copy, LazyService } from '@delon/util';
|
||||||
|
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BrandService } from '../pro.service';
|
||||||
|
import { ProLayout } from '../pro.types';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pro-setting-drawer',
|
||||||
|
templateUrl: './setting-drawer.component.html',
|
||||||
|
preserveWhitespaces: false,
|
||||||
|
host: {
|
||||||
|
'[class.setting-drawer]': 'true',
|
||||||
|
'[class.setting-drawer-rtl]': `dir === 'rtl'`
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class ProSettingDrawerComponent implements OnInit, OnDestroy {
|
||||||
|
private loadedLess = false;
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
get layout(): ProLayout {
|
||||||
|
return this.brand.layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse = false;
|
||||||
|
dir: Direction = 'ltr';
|
||||||
|
|
||||||
|
themes = [
|
||||||
|
{
|
||||||
|
key: 'dark',
|
||||||
|
title: '暗色菜单风格',
|
||||||
|
img: 'https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'light',
|
||||||
|
title: '亮色菜单风格',
|
||||||
|
img: 'https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
color = '#2F54EB';
|
||||||
|
colors = [
|
||||||
|
{
|
||||||
|
key: '薄暮',
|
||||||
|
color: '#F5222D'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '火山',
|
||||||
|
color: '#FA541C'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '日暮',
|
||||||
|
color: '#FAAD14'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '明青',
|
||||||
|
color: '#13C2C2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '极光绿',
|
||||||
|
color: '#52C41A'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '拂晓蓝(默认)',
|
||||||
|
color: '#1890ff'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '极客蓝',
|
||||||
|
color: '#2F54EB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '酱紫',
|
||||||
|
color: '#722ED1'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
menuModes = [
|
||||||
|
{
|
||||||
|
key: 'side',
|
||||||
|
title: '侧边菜单布局',
|
||||||
|
img: 'https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'top',
|
||||||
|
title: '顶部菜单布局',
|
||||||
|
img: 'https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
contentWidths = [
|
||||||
|
{
|
||||||
|
key: 'fixed',
|
||||||
|
title: '定宽',
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fluid',
|
||||||
|
title: '流式',
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public brand: BrandService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private msg: NzMessageService,
|
||||||
|
private lazy: LazyService,
|
||||||
|
private zone: NgZone,
|
||||||
|
@Inject(DOCUMENT) private doc: any,
|
||||||
|
@Optional() private directionality: Directionality
|
||||||
|
) {
|
||||||
|
this.setLayout('menu', this.brand.menu, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.dir = this.directionality.value;
|
||||||
|
this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => {
|
||||||
|
this.dir = direction;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadLess(): Promise<void> {
|
||||||
|
if (this.loadedLess) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return this.lazy
|
||||||
|
.loadStyle('./assets/color.less', 'stylesheet/less')
|
||||||
|
.then(() => {
|
||||||
|
const lessConfigNode = this.doc.createElement('script');
|
||||||
|
lessConfigNode.innerHTML = `
|
||||||
|
window.less = {
|
||||||
|
async: true,
|
||||||
|
env: 'production',
|
||||||
|
javascriptEnabled: true
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
this.doc.body.appendChild(lessConfigNode);
|
||||||
|
})
|
||||||
|
.then(() => this.lazy.loadScript('https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'))
|
||||||
|
.then(() => {
|
||||||
|
this.loadedLess = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private runLess(): void {
|
||||||
|
const { color, zone, msg, cdr: cd } = this;
|
||||||
|
const msgId = msg.loading(`正在编译主题!`, { nzDuration: 0 }).messageId;
|
||||||
|
setTimeout(() => {
|
||||||
|
zone.runOutsideAngular(() => {
|
||||||
|
this.loadLess().then(() => {
|
||||||
|
(window as any).less
|
||||||
|
.modifyVars({
|
||||||
|
[`@primary-color`]: color
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
msg.success('成功');
|
||||||
|
msg.remove(msgId);
|
||||||
|
zone.run(() => cd.detectChanges());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(): void {
|
||||||
|
this.collapse = !this.collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeColor(color: string): void {
|
||||||
|
this.color = color;
|
||||||
|
this.runLess();
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayout(name: string, value: any, cd: boolean = true): void {
|
||||||
|
switch (name) {
|
||||||
|
case 'menu':
|
||||||
|
const isTop = value === 'top';
|
||||||
|
this.contentWidths.find(w => w.key === 'fixed')!.disabled = !isTop;
|
||||||
|
const newLayout = {
|
||||||
|
...this.brand.layout,
|
||||||
|
contentWidth: isTop ? 'fixed' : 'fluid',
|
||||||
|
onlyIcon: isTop,
|
||||||
|
collapsed: isTop && !this.brand.isMobile ? false : this.brand.layout.collapsed
|
||||||
|
};
|
||||||
|
this.brand.setLayout(newLayout);
|
||||||
|
break;
|
||||||
|
case 'fixedHeader':
|
||||||
|
this.brand.setLayout('autoHideHeader', false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.brand.setLayout(name, value);
|
||||||
|
if (cd) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Re-render G2 muse be trigger window resize
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(): void {
|
||||||
|
const { color, layout } = this;
|
||||||
|
const vars: { [key: string]: string } = {
|
||||||
|
[`@primary-color`]: color
|
||||||
|
};
|
||||||
|
const colorVars = Object.keys(vars)
|
||||||
|
.map(key => `${key}: ${vars[key]};`)
|
||||||
|
.join('\n');
|
||||||
|
const layoutVars = Object.keys(layout)
|
||||||
|
.filter(
|
||||||
|
key => ~['theme', 'menu', 'contentWidth', 'fixedHeader', 'autoHideHeader', 'fixSiderbar', 'colorWeak', 'onlyIcon'].indexOf(key)
|
||||||
|
)
|
||||||
|
.map(key => {
|
||||||
|
const value = layout[key];
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return ` ${key}: ${value},`;
|
||||||
|
} else {
|
||||||
|
return ` ${key}: '${value}',`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
copy(
|
||||||
|
`在 [src/styles/theme.less] 配置以下:\n{{colorVars}}\n\n在 [src/environments/*] 的 pro 配置以下:\nexport const environment = {\n ...\n pro: {\n{{layoutVars}}\n }\n}`
|
||||||
|
);
|
||||||
|
this.msg.success(`拷贝成功,请根据剪切板内容进行替换`);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/app/layout/pro/shared/page/index.ts
Normal file
3
src/app/layout/pro/shared/page/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './page-grid.component';
|
||||||
|
export * from './page-header-wrapper.component';
|
||||||
|
export * from './page.module';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user