This commit is contained in:
MaxKey 2022-04-23 10:08:40 +08:00
parent b6d30a8730
commit ba518828f8
1024 changed files with 142635 additions and 0 deletions

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

View 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

View 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

View 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'
}
}
]
};

View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: cipchk
open_collective: ng-alain
custom: # Replace with a single custom sponsorship URL

View File

@ -0,0 +1,15 @@
<!--
IMPORTANT: Please use the following link to create a new issue:
https://ng-alain.com/issue-helper/index.html#en
If your issue was not created using the app above, it will be closed immediately.
-->
<!--
注意:请使用下面的链接来新建 issue
https://ng-alain.com/issue-helper/index.html#zh
不是用上面的链接创建的 issue 会被立即关闭。
-->

View File

@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Create new issue
url: https://ng-alain.com/issue-helper/index.html#en
about: The issue which is not created via issue-helper will be closed immediately.
- name: 报告问题
url: https://ng-alain.com/issue-helper/index.html#zh
about: 注意:不是用 issue-helper 创建的 issue 会被立即关闭。使用问题请加QQ Group428749721

View File

@ -0,0 +1,43 @@
## PR Checklist
Please check if your PR fulfills the following requirements:
- [ ] The commit message follows our guidelines: https://github.com/ng-alain/ng-alain/blob/master/CONTRIBUTING.md#commit
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Application (the showcase website) / infrastructure changes
[ ] Other... Please describe:
```
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
```
[ ] Yes
[ ] No
```
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information

View File

@ -0,0 +1,28 @@
issue:
translate:
replay: |
Translation of this issue:
---
## {title}
{body}
needReproduce:
label: Need Reproduce
afterLabel: Need More Info
replay: |
Hello @{user}. Please provide a online reproduction by forking this link https://stackblitz.com/edit/ng-alain-setup or a minimal GitHub repository.
Issues labeled by `Need Reproduce` will be closed if no activities in 7 days.
你好 @{user}, 我们需要你提供一个在线的重现实例以便于我们帮你排查问题。你可以通过点击 [此处](https://stackblitz.com/edit/ng-alain-setup) 创建一个 stackblitz 或者提供一个最小化的 GitHub 仓库
被标记为 `Need Reproduce` 的 issue 7 天内未跟进将会被自动关闭。
![](https://gw.alipayobjects.com/zos/antfincdn/y9kwg7DVCd/reproduce.gif)
invalid:
mark: ng-alain-issue-helper
labels: Invalid
replay: |
Hello @{user}, your issue has been closed because it does not conform to our issue requirements.
Please use the [Issue Helper](https://ng-alain.com/issue-helper/index.html#en) to create an issue, thank you!
你好 @{user},为了能够进行高效沟通,我们对 issue 有一定的格式要求,你的 issue 因为不符合要求而被自动关闭。
你可以通过 [issue 助手](https://ng-alain.com/issue-helper/index.html#zh) 来创建 issue 以方便我们定位错误。谢谢配合!

View File

@ -0,0 +1,14 @@
# Configuration for lock-threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 365
# Comment to post before locking. Set to `false` to disable
lockComment: >
This thread has been automatically locked because it has not had recent
activity. Please open a new issue for related bugs and link to relevant
comments in this thread.
# Issues or pull requests with these labels will not be locked
# exemptLabels:
# - no-locking
# Limit to only `issues` or `pulls`
only: issues

View File

@ -0,0 +1,10 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 7
# Label requiring a response
responseRequiredLabel: Need More Info
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response to our request for more information from the original author. With only the information that is currently in the issue, we don't have enough information to take action. If you can provide more information, feel free to ping anyone of our maintainers to reopen this issue. Thank you for your contributions. --- 这个 issue 已经被自动关闭,因为您没有向我们提供更多的信息。仅凭目前的描述我们无法采取任 何行动,如果您能提供更多的信息请随时联系我们的开发人员重新打开这个 issue。 感谢您的贡献。

View File

@ -0,0 +1,14 @@
titleAndCommits: true
types:
- feat
- fix
- docs
- style
- refactor
- perf
- test
- build
- ci
- chore
- revert
- release

View File

@ -0,0 +1,41 @@
name: Deploy website
# on: push
on:
push:
branches:
- 'publish-**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@master
- uses: borales/actions-yarn@v2.3.0
with:
cmd: install
- name: build
shell: bash
run: bash ./scripts/_ci/deploy-pipelines.sh
- name: deploy-to-surge
uses: dswistowski/surge-sh-action@v1
with:
login: ${{ secrets.SURGE_LOGIN }}
token: ${{ secrets.SURGE_TOKEN }}
domain: https://ng-alain.surge.sh
project: ./dist
- name: deploy-to-gh-pages
uses: peaceiris/actions-gh-pages@v2
env:
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PUBLISH_BRANCH: gh-pages
PUBLISH_DIR: ./dist
with:
emptyCommits: false

View File

@ -0,0 +1,13 @@
name: GiteeMirror
on: [push]
jobs:
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@gitee.com:ng-alain/ng-alain.git
ssh_private_key: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}

View File

@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
export NODE_OPTIONS="--max-old-space-size=4096"
npx --no-install tsc -p tsconfig.app.json --noEmit
npx --no-install lint-staged

View File

@ -0,0 +1,5 @@
.github
node_modules
dist
tmp

View File

@ -0,0 +1 @@
12.14.1

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

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

View File

@ -0,0 +1,38 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-rational-order",
"stylelint-config-prettier"
],
"customSyntax": "postcss-less",
"plugins": [
"stylelint-order",
"stylelint-declaration-block-no-ignored-properties"
],
"rules": {
"function-no-unknown": null,
"no-descending-specificity": null,
"plugin/declaration-block-no-ignored-properties": true,
"selector-type-no-unknown": [
true,
{
"ignoreTypes": [
"/^g2-/",
"/^nz-/",
"/^app-/"
]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": [
"ng-deep"
]
}
]
},
"ignoreFiles": [
"src/assets/**/*"
]
}

View File

@ -0,0 +1,5 @@
{
"recommendations": [
"cipchk.ng-alain-extension-pack"
]
}

View File

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceRoot}",
"sourceMaps": true
}
]
}

View File

@ -0,0 +1,37 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
// For ESLint
"source.fixAll.eslint": true,
// For Stylelint
"source.fixAll.stylelint": true
},
"[markdown]": {
"editor.formatOnSave": false
},
"[javascript]": {
"editor.formatOnSave": false
},
"[json]": {
"editor.formatOnSave": false
},
"[jsonc]": {
"editor.formatOnSave": false
},
"files.watcherExclude": {
"**/.git/*/**": true,
"**/node_modules/*/**": true,
"**/dist/*/**": true,
"**/coverage/*/**": true
},
"files.associations": {
"*.json": "jsonc",
".prettierrc": "jsonc",
".stylelintrc": "jsonc"
},
// Angular schematics : https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics
"ngschematics.schematics": [
"ng-alain"
]
}

View File

@ -0,0 +1 @@
[Document](https://ng-alain.com/cli/generate#Custom-template-page)

View File

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

View File

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

View File

@ -0,0 +1 @@
[Document](https://ng-alain.com/mock)

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

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

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

View 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,
},
],
},
};

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

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

View 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 passwordadmin/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',
},
};

View File

@ -0,0 +1,7 @@
export * from './_profile';
export * from './_rule';
export * from './_api';
export * from './_chart';
export * from './_pois';
export * from './_user';
export * from './_geo';

View File

@ -0,0 +1,131 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"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": ["src/styles.less"],
"scripts": [],
"allowedCommonJsDependencies": ["ajv", "ajv-formats"],
"stylePreprocessorOptions": {
"includePaths": [
"node_modules/"
]
}
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "6mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"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",
"disableHostCheck": true,
"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": [],
"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",
"cli": {
"packageManager": "yarn"
}
}

View File

@ -0,0 +1,156 @@
name: ng-alain
trigger:
- master
pool:
vmImage: 'ubuntu-latest'
pr:
autoCancel: true
branches:
exclude:
- gh-pages
stages:
- stage: Env
jobs:
- job: Nodes
steps:
- task: NodeTool@0
inputs:
versionSpec: '12.14.1'
displayName: 'Install Node.js'
- stage: build
dependsOn: env
jobs:
- job: Build
steps:
- script: yarn install
displayName: 'Install'
- script: |
node ./scripts/_ci/github-comment.js "RELEASE" "[Using release @delon, Preview Preparing...](https://dev.azure.com/ng-alain/ng-alain/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
- task: Bash@3
displayName: 'Build'
inputs:
targetType: 'filePath'
filePath: './scripts/_ci/deploy-pipelines.sh'
- script: ls -al dist/
displayName: 'List build'
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ng-alain.surge.sh
echo "Deploy to $DEPLOY_DOMAIN"
cp ./dist/index.html ./dist/404.html
npx surge --project ./dist --domain $DEPLOY_DOMAIN
displayName: 'Deploy Site'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
SURGE_LOGIN: $(SURGE_LOGIN)
SURGE_TOKEN: $(SURGE_TOKEN)
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-ng-alain.surge.sh
node ./scripts/_ci/github-comment.js "RELEASE" "[Using release @delon, Preview is ready!]($DEPLOY_DOMAIN)"
displayName: 'Update comment on github'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
- job: Build_Failed
dependsOn: Build
condition: failed()
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- script: yarn install
displayName: 'Install'
- script: |
node ./scripts/_ci/github-comment.js "RELEASE" "[Using release @delon, Preview Failed](https://dev.azure.com/ng-alain/delon/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
- stage: build_day
dependsOn: env
jobs:
- job: Build
steps:
- script: yarn install
displayName: 'Install'
- script: |
node ./scripts/_ci/github-comment.js "RELEASE_DAY" "[Using day release @delon, Preview Preparing...](https://dev.azure.com/ng-alain/ng-alain/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
- task: Bash@3
displayName: 'Build'
inputs:
targetType: 'filePath'
filePath: './scripts/_ci/deploy-pipelines.sh'
arguments: '-day'
- script: ls -al dist/
displayName: 'List build'
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-day-ng-alain.surge.sh
echo "Deploy to $DEPLOY_DOMAIN"
cp ./dist/index.html ./dist/404.html
npx surge --project ./dist --domain $DEPLOY_DOMAIN
displayName: 'Deploy Site'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
SURGE_LOGIN: $(SURGE_LOGIN)
SURGE_TOKEN: $(SURGE_TOKEN)
- script: |
export DEPLOY_DOMAIN=https://preview-${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-day-ng-alain.surge.sh
node ./scripts/_ci/github-comment.js "RELEASE_DAY" "[Using day release @delon, Preview is ready!]($DEPLOY_DOMAIN)"
displayName: 'Update comment on github'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
- job: Build_Failed
dependsOn: Build
condition: failed()
steps:
- checkout: self
displayName: 'Checkout'
clean: true
fetchDepth: 1
- script: yarn install
displayName: 'Install'
- script: |
node ./scripts/_ci/github-comment.js "RELEASE_DAY" "[Using day release @delon, Preview Failed](https://dev.azure.com/ng-alain/delon/_build/results?buildId=$(Build.BuildId))"
displayName: 'Comment on github'
env:
ACCESS_REPO: $(ACCESS_REPO)
ACCESS_TOKEN: $(ACCESS_TOKEN)
- stage: lint
dependsOn:
- env
jobs:
- job: site
steps:
- script: yarn install
displayName: 'Install'
- script: |
npx stylelint --version
yarn run lint
- stage: test
dependsOn:
- env
jobs:
- job: site
steps:
- script: yarn install
displayName: 'Install'
- script: npx ng test --no-progress --browsers=ChromeHeadlessCI --code-coverage --no-watch

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

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

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

View 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"]
}
}

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

View File

@ -0,0 +1,13 @@
{
"$schema": "./node_modules/ng-alain/schema.json",
"theme": {
"list": [
{
"theme": "dark"
},
{
"theme": "compact"
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
{
"name": "ng-alain",
"version": "13.4.0",
"description": "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"
},
"bugs": {
"url": "https://github.com/ng-alain/ng-alain/issues"
},
"homepage": "https://ng-alain.com",
"license": "MIT",
"keywords": [
"delon",
"antd",
"ng-zorro-antd",
"angular",
"component",
"scaffold"
],
"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": "npx stylelint 'src/**/*.less'",
"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": {
"@angular/animations": "~13.3.0",
"@angular/common": "~13.3.0",
"@angular/compiler": "~13.3.0",
"@angular/core": "~13.3.0",
"@angular/forms": "~13.3.0",
"@angular/platform-browser": "~13.3.0",
"@angular/platform-browser-dynamic": "~13.3.0",
"@angular/router": "~13.3.0",
"@delon/abc": "^13.4.0",
"@delon/acl": "^13.4.0",
"@delon/auth": "^13.4.0",
"@delon/cache": "^13.4.0",
"@delon/chart": "^13.4.0",
"@delon/form": "^13.4.0",
"@delon/mock": "^13.4.0",
"@delon/theme": "^13.4.0",
"@delon/util": "^13.4.0",
"ajv": "^8.10.0",
"ajv-formats": "^2.1.1",
"crypto-js": "^4.1.1",
"ng-zorro-antd": "^13.1.1",
"ngx-cookie-service": "^13.2.0",
"ngx-tinymce": "^13.0.0",
"ngx-ueditor": "^13.0.0",
"rxjs": "~7.5.0",
"screenfull": "^6.0.1",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.3.0",
"@angular-eslint/builder": "~13.1.0",
"@angular-eslint/eslint-plugin": "~13.1.0",
"@angular-eslint/eslint-plugin-template": "~13.1.0",
"@angular-eslint/schematics": "~13.1.0",
"@angular-eslint/template-parser": "~13.1.0",
"@angular/cli": "~13.3.0",
"@angular/compiler-cli": "~13.3.0",
"@angular/language-service": "~13.3.0",
"@delon/testing": "^13.4.0",
"@types/crypto-js": "^4.1.1",
"@types/jasmine": "~3.10.0",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "~5.15.0",
"@typescript-eslint/parser": "~5.15.0",
"eslint": "^8.11.0",
"eslint-config-prettier": "~8.5.0",
"eslint-plugin-import": "~2.25.4",
"eslint-plugin-jsdoc": "~38.0.4",
"eslint-plugin-prefer-arrow": "~1.2.3",
"eslint-plugin-prettier": "~4.0.0",
"husky": "^7.0.4",
"jasmine-core": "~4.0.0",
"jasmine-spec-reporter": "^7.0.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"lint-staged": "^12.3.7",
"ng-alain": "13.4.0",
"ng-alain-plugin-theme": "^13.0.3",
"ng-alain-sts": "^0.0.2",
"node-fetch": "^2.6.1",
"prettier": "^2.6.0",
"protractor": "~7.0.0",
"source-map-explorer": "^2.5.2",
"stylelint": "^14.6.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^25.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.5.0",
"stylelint-order": "^5.0.0",
"ts-node": "~9.1.1",
"typescript": "~4.6.2"
},
"lint-staged": {
"(src)/**/*.{html,ts}": [
"eslint --fix"
],
"(src)/**/*.less": [
"npm run lint:style"
]
}
}

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

View File

@ -0,0 +1 @@
# Only for CI, you can delete it

View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# bash ./scripts/_ci/delon.sh
set -e
cd $(dirname $0)/../..
echo "Download latest @delon version"
rm -rf delon-builds
git clone --depth 1 https://github.com/ng-alain/delon-builds.git
rm -rf node_modules/@delon
rm -rf node_modules/ng-alain
rsync -am delon-builds/ node_modules/
NG_ALAIN_VERSION=$(node -p "require('./node_modules/ng-alain/package.json').version")
rm -rf delon-builds
echo "Using ng-alain version: ${NG_ALAIN_VERSION}"

View File

@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -e
GH=false
DAY=false
for ARG in "$@"; do
case "$ARG" in
-gh)
GH=true
;;
-day)
DAY=true
;;
esac
done
echo "List:"
ls -al
ROOT_DIR="$(pwd)"
DIST_DIR="$(pwd)/dist"
VERSION=$(node -p "require('./package.json').version")
echo "Start build version: ${VERSION}"
if [[ ${DAY} == true ]]; then
echo ""
echo "Download day @delon/* libs"
echo ""
bash ./scripts/_ci/delon.sh
fi
echo ""
echo "Generate color less"
echo ""
npm run color-less
echo ""
echo "Generate theme files"
echo ""
npm run theme
echo '===== need mock'
cp -f ${ROOT_DIR}/src/environments/environment.ts ${ROOT_DIR}/src/environments/environment.prod.ts
sed -i 's/production: false/production: true/g' ${ROOT_DIR}/src/environments/environment.prod.ts
sed -i 's/showSettingDrawer = !environment.production;/showSettingDrawer = true;/g' ${ROOT_DIR}/src/app/layout/basic/basic.component.ts
if [[ ${GH} == true ]]; then
echo "Build angular [github gh-pages]"
node --max_old_space_size=5120 ./node_modules/@angular/cli/bin/ng build --base-href /ng-alain/
else
echo "Build angular"
node --max_old_space_size=5120 ./node_modules/@angular/cli/bin/ng build
fi
cp -f ${DIST_DIR}/index.html ${DIST_DIR}/404.html
echo "Finished"

View File

@ -0,0 +1,61 @@
const fetch = require('node-fetch');
const REPO = process.env.ACCESS_REPO;
const TOKEN = process.env.ACCESS_TOKEN;
const PR = process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER;
const argv = process.argv;
const tag = argv[argv.length - 2];
const comment = argv[argv.length - 1];
const REPLACE_MARK = `<!-- AZURE_UPDATE_COMMENT_${tag} -->`;
const wrappedComment = `
${REPLACE_MARK}
${comment}
`.trim();
async function withGithub(url, json, method) {
const res = await fetch(url, {
method: method || (json ? 'POST' : 'GET'),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Basic ${Buffer.from(TOKEN).toString('base64')}`,
},
body: json ? JSON.stringify(json) : undefined,
});
return res.json();
}
(async function run() {
if (PR == null) {
console.log('未获取到PR忽略处理')
return;
}
const comments = await withGithub(`https://api.github.com/repos/${REPO}/issues/${PR}/comments`);
// Find my comment
const updateComment = comments.find(({ body }) => body.includes(REPLACE_MARK));
// eslint-disable-next-line no-console
console.log('Origin comment:', updateComment);
// Update
let res;
if (!updateComment) {
res = await withGithub(`https://api.github.com/repos/${REPO}/issues/${PR}/comments`, {
body: wrappedComment,
});
} else {
res = await withGithub(
`https://api.github.com/repos/${REPO}/issues/comments/${updateComment.id}`,
{
body: wrappedComment,
},
'PATCH',
);
}
// eslint-disable-next-line no-console
console.log(res);
})();

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

View File

@ -0,0 +1,109 @@
/* 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 { DELON_LOCALE, zh_CN as delonLang, ALAIN_I18N_TOKEN } from '@delon/theme';
import { NZ_DATE_LOCALE, NZ_I18N, zh_CN as zorroLang } from 'ng-zorro-antd/i18n';
import { NzNotificationModule } from 'ng-zorro-antd/notification';
// #region default language
// 参考https://ng-alain.com/docs/i18n
import { I18NService } from '@core';
import { zhCN as dateLang } from 'date-fns/locale';
const LANG = {
abbr: 'zh',
ng: ngLang,
zorro: zorroLang,
date: dateLang,
delon: delonLang
};
// register angular
import { registerLocaleData } from '@angular/common';
registerLocaleData(LANG.ng, LANG.abbr);
const LANG_PROVIDES = [
{ provide: LOCALE_ID, useValue: LANG.abbr },
{ provide: NZ_I18N, useValue: LANG.zorro },
{ provide: NZ_DATE_LOCALE, useValue: LANG.date },
{ provide: DELON_LOCALE, useValue: LANG.delon }
];
// #endregion
// #region i18n services
const I18NSERVICE_PROVIDES = [{ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false }];
// #endregion
// #region global third module
import { BidiModule } from '@angular/cdk/bidi';
const GLOBAL_THIRD_MODULES: Array<Type<any>> = [BidiModule];
// #endregion
// #region JSON Schema form (using @delon/form)
import { JsonSchemaModule } from '@shared';
const FORM_MODULES = [JsonSchemaModule];
// #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): () => Observable<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/st-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,
...FORM_MODULES
],
providers: [...LANG_PROVIDES, ...INTERCEPTOR_PROVIDES, ...I18NSERVICE_PROVIDES, ...APPINIT_PROVIDES],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@ -0,0 +1,5 @@
### CoreModule
**应** 仅只留 `providers` 属性。
**作用:** 一些通用服务例如用户消息、HTTP数据访问。

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

View File

@ -0,0 +1,84 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { TestBed, TestBedStatic } from '@angular/core/testing';
import { DelonLocaleService, SettingsService } from '@delon/theme';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzI18nService } from 'ng-zorro-antd/i18n';
import { of } from 'rxjs';
import { I18NService } from './i18n.service';
describe('Service: I18n', () => {
let injector: TestBedStatic;
let srv: I18NService;
const MockSettingsService: NzSafeAny = {
layout: {
lang: null
}
};
const MockNzI18nService = {
setLocale: () => {},
setDateLocale: () => {}
};
const MockDelonLocaleService = {
setLocale: () => {}
};
const MockTranslateService = {
getBrowserLang: jasmine.createSpy('getBrowserLang'),
addLangs: () => {},
setLocale: () => {},
getDefaultLang: () => '',
use: (lang: string) => of(lang),
instant: jasmine.createSpy('instant')
};
function genModule(): void {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
I18NService,
{ provide: SettingsService, useValue: MockSettingsService },
{ provide: NzI18nService, useValue: MockNzI18nService },
{ provide: DelonLocaleService, useValue: MockDelonLocaleService }
]
});
srv = TestBed.inject(I18NService);
}
it('should working', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['zh-CN']);
genModule();
expect(srv).toBeTruthy();
expect(srv.defaultLang).toBe('zh-CN');
srv.fanyi('a');
srv.fanyi('a', {});
});
it('should be used layout as default language', () => {
MockSettingsService.layout.lang = 'en-US';
const navSpy = spyOnProperty(navigator, 'languages');
genModule();
expect(navSpy).not.toHaveBeenCalled();
expect(srv.defaultLang).toBe('en-US');
MockSettingsService.layout.lang = null;
});
it('should be used browser as default language', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['zh-TW']);
genModule();
expect(srv.defaultLang).toBe('zh-TW');
});
it('should be use default language when the browser language is not in the list', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['es-419']);
genModule();
expect(srv.defaultLang).toBe('zh-CN');
});
it('should be trigger notify when changed language', () => {
genModule();
srv.use('en-US', {});
srv.change.subscribe(lang => {
expect(lang).toBe('en-US');
});
});
});

View File

@ -0,0 +1,109 @@
// 请参考https://ng-alain.com/docs/i18n
import { Platform } from '@angular/cdk/platform';
import { registerLocaleData } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import ngEn from '@angular/common/locales/en';
import ngZh from '@angular/common/locales/zh';
import ngZhTw from '@angular/common/locales/zh-Hant';
import { Injectable } from '@angular/core';
import {
DelonLocaleService,
en_US as delonEnUS,
SettingsService,
zh_CN as delonZhCn,
zh_TW as delonZhTw,
_HttpClient,
AlainI18nBaseService
} from '@delon/theme';
import { AlainConfigService } from '@delon/util/config';
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw } from 'date-fns/locale';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { en_US as zorroEnUS, NzI18nService, zh_CN as zorroZhCN, zh_TW as zorroZhTW } from 'ng-zorro-antd/i18n';
import { Observable } from 'rxjs';
interface LangConfigData {
abbr: string;
text: string;
ng: NzSafeAny;
zorro: NzSafeAny;
date: NzSafeAny;
delon: NzSafeAny;
}
const DEFAULT = 'zh-CN';
const LANGS: { [key: string]: LangConfigData } = {
'zh-CN': {
text: '简体中文',
ng: ngZh,
zorro: zorroZhCN,
date: dfZhCn,
delon: delonZhCn,
abbr: '🇨🇳'
},
'en-US': {
text: 'English',
ng: ngEn,
zorro: zorroEnUS,
date: dfEn,
delon: delonEnUS,
abbr: '🇬🇧'
}
};
@Injectable({ providedIn: 'root' })
export class I18NService extends AlainI18nBaseService {
protected override _defaultLang = DEFAULT;
private _langs = Object.keys(LANGS).map(code => {
const item = LANGS[code];
return { code, text: item.text, abbr: item.abbr };
});
constructor(
private http: HttpClient,
private settings: SettingsService,
private nzI18nService: NzI18nService,
private delonLocaleService: DelonLocaleService,
private platform: Platform,
cogSrv: AlainConfigService
) {
super(cogSrv);
const defaultLang = this.getDefaultLang();
this._defaultLang = this._langs.findIndex(w => w.code === defaultLang) === -1 ? DEFAULT : defaultLang;
}
private getDefaultLang(): string {
if (!this.platform.isBrowser) {
return DEFAULT;
}
if (this.settings.layout.lang) {
return this.settings.layout.lang;
}
let res = (navigator.languages ? navigator.languages[0] : null) || navigator.language;
const arr = res.split('-');
return arr.length <= 1 ? res : `${arr[0]}-${arr[1].toUpperCase()}`;
}
loadLangData(lang: string): Observable<NzSafeAny> {
return this.http.get(`./assets/i18n/${lang}.json`);
}
use(lang: string, data: Record<string, unknown>): void {
if (this._currentLang === lang) return;
this._data = this.flatData(data, []);
const item = LANGS[lang];
registerLocaleData(item.ng);
this.nzI18nService.setLocale(item.zorro);
this.nzI18nService.setDateLocale(item.date);
this.delonLocaleService.setLocale(item.delon);
this._currentLang = lang;
this._change$.next(lang);
}
getLangs(): Array<{ code: string; text: string; abbr: string }> {
return this._langs;
}
}

View File

@ -0,0 +1,4 @@
export * from './i18n/i18n.service';
export * from './module-import-guard';
export * from './net/default.interceptor';
export * from './startup/startup.service';

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

View File

@ -0,0 +1,268 @@
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 { ALAIN_I18N_TOKEN, _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: {
Authorization: `Bearer ${token}`,
hostname: window.location.hostname,
AuthServer: 'MaxKey'
}
});
}
// #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}?url=${req.urlWithParams}`);
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;
}
let jwtAuthn = this.tokenSrv.get();
if (jwtAuthn !== null) {
res['Authorization'] = `Bearer ${jwtAuthn.token}`;
}
res['hostname'] = window.location.hostname;
res['AuthServer'] = 'MaxKey';
return res;
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 统一加上服务端前缀
let url = req.url;
if (!url.startsWith('https://') && !url.startsWith('http://') && !url.startsWith('.')) {
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))
);
}
}

View File

@ -0,0 +1,61 @@
import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { ACLService } from '@delon/acl';
import { ALAIN_I18N_TOKEN, MenuService, SettingsService, TitleService } 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 { I18NService } from '../i18n/i18n.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,
@Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
private settingService: SettingsService,
private aclService: ACLService,
private titleService: TitleService,
private httpClient: HttpClient,
private router: Router
) {
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
}
load(): Observable<void> {
const defaultLang = this.i18n.defaultLang;
return zip(this.i18n.loadLangData(defaultLang), this.httpClient.get('./assets/app-data.json')).pipe(
// 接收其他拦截器后产生的异常消息
catchError(res => {
console.warn(`StartupService.load: Network request failed`, res);
setTimeout(() => this.router.navigateByUrl(`/exception/500`));
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;
})
);
}
}

View File

@ -0,0 +1,37 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class Accounts extends BaseEntity {
strategyId!: String;
strategyName!: String;
appId!: String;
appName!: String;
userId!: String;
username!: String;
displayName!: String;
relatedUsername!: String;
relatedPassword!: String;
createType!: String;
confirmPassword!: String;
constructor() {
super();
this.createType = 'manual';
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,38 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class AccountsStrategy extends BaseEntity {
name!: String;
appId!: String;
appName!: String;
filters!: String;
orgIdsList!: String;
mapping!: String;
suffixes!: String;
createType!: String;
switch_dynamic: boolean = false;
picker_resumeTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
picker_suspendTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
constructor() {
super();
this.createType = 'manual';
this.mapping = 'username';
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,28 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class Adapters extends BaseEntity {
name!: String;
protocol!: String;
adapter!: String;
switch_dynamic: boolean = false;
constructor() {
super();
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,59 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class Apps extends BaseEntity {
name!: String;
loginUrl!: String;
category!: String;
protocol!: String;
secret!: String;
iconBase64!: String;
visible!: String;
inducer!: String;
vendor!: String;
vendorUrl!: String;
credential!: String;
sharedUsername!: String;
sharedPassword!: String;
systemUserAttr!: String;
principal!: String;
credentials!: String;
logoutUrl!: String;
logoutType!: String;
isExtendAttr!: String;
extendAttr!: String;
userPropertys!: String;
isSignature!: String;
isAdapter!: String;
adapterId!: String;
adapterName!: String;
adapter!: String;
iconId!: String;
select_userPropertys!: String[];
constructor() {
super();
this.category = 'none';
this.visible = '0';
this.isAdapter = '0';
this.logoutType = '0';
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,26 @@
export class BaseEntity {
id!: String;
instId!: String;
instName!: String;
sortIndex!: Number;
status!: Number;
description!: String;
switch_status: boolean = false;
constructor() {
this.status = 1;
}
init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,13 @@
import { BaseEntity } from './BaseEntity';
export class ChangePassword extends BaseEntity {
userId!: String;
username!: String;
email!: String;
mobile!: String;
displayName!: String;
oldPassword!: String;
password!: String;
confirmPassword!: String;
decipherable!: String;
}

View File

@ -0,0 +1,36 @@
import { BaseEntity } from './BaseEntity';
export class EmailSenders extends BaseEntity {
account!: String;
credentials!: String;
smtpHost!: String;
port!: Number;
sslSwitch!: Number;
sender!: String;
encoding!: String;
protocol!: String;
switch_sslSwitch: boolean = false;
override init(data: any): void {
Object.assign(this, data);
if (this.sslSwitch == 1) {
this.switch_sslSwitch = true;
}
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_sslSwitch) {
this.sslSwitch = 1;
} else {
this.sslSwitch = 0;
}
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,3 @@
import { BaseEntity } from './BaseEntity';
export class GroupMembers extends BaseEntity { }

View File

@ -0,0 +1,56 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class Groups extends BaseEntity {
name!: String;
dynamic!: String;
filters!: String;
orgIdsList!: String;
resumeTime!: String;
suspendTime!: String;
isdefault!: String;
switch_dynamic: boolean = false;
picker_resumeTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
picker_suspendTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
constructor() {
super();
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
if (this.dynamic == '1') {
this.switch_dynamic = true;
}
if (this.resumeTime != '') {
this.picker_resumeTime = new Date(format(new Date(), `yyyy-MM-dd ${this.resumeTime}:00`));
}
if (this.suspendTime != '') {
this.picker_suspendTime = new Date(format(new Date(), `yyyy-MM-dd ${this.suspendTime}:00`));
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
if (this.switch_dynamic) {
this.dynamic = '1';
} else {
this.dynamic = '0';
}
if (this.picker_resumeTime) {
this.resumeTime = format(this.picker_resumeTime, 'HH:mm');
}
if (this.picker_suspendTime) {
this.suspendTime = format(this.picker_suspendTime, 'HH:mm');
}
}
}

View File

@ -0,0 +1,17 @@
import { BaseEntity } from './BaseEntity';
export class Institutions extends BaseEntity {
name!: String;
fullName!: String;
contact!: String;
email!: String;
phone!: String;
address!: String;
logo!: String;
frontTitle!: String;
consoleTitle!: String;
domain!: String;
captchaType!: String;
captchaSupport!: String;
defaultUri!: String;
}

View File

@ -0,0 +1,48 @@
import { BaseEntity } from './BaseEntity';
export class LdapContext extends BaseEntity {
product!: String;
providerUrl!: String;
principal!: String;
credentials!: String;
sslSwitch!: Number;
filters!: String;
basedn!: String;
msadDomain!: String;
accountMapping!: String;
trustStore!: String;
trustStorePassword!: String;
switch_sslSwitch: boolean = false;
switch_accountMapping: boolean = false;
override init(data: any): void {
Object.assign(this, data);
if (this.sslSwitch == 1) {
this.switch_sslSwitch = true;
}
if (this.status == 1) {
this.switch_status = true;
}
if (this.accountMapping == 'YES') {
this.switch_accountMapping = true;
}
}
override trans(): void {
if (this.switch_sslSwitch) {
this.sslSwitch = 1;
} else {
this.sslSwitch = 0;
}
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
if (this.switch_accountMapping) {
this.accountMapping = 'YES';
} else {
this.accountMapping = 'NO';
}
}
}

View File

@ -0,0 +1,8 @@
import format from 'date-fns/format';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
export class Message<NzSafeAny> {
code: number = 0;
message: string = '';
data: any;
}

View File

@ -0,0 +1,45 @@
import { BaseEntity } from './BaseEntity';
export class Organizations extends BaseEntity {
code!: String;
name!: String;
fullName!: String;
parentId!: String;
parentCode!: string;
parentName!: String;
type!: String;
codePath!: String;
namePath!: String;
level!: Number;
division!: String;
country!: String;
region!: String;
locality!: String;
street!: String;
address!: String;
contact!: String;
postalCode!: String;
phone!: String;
fax!: String;
email!: String;
switch_dynamic: boolean = false;
constructor() {
super();
this.status = 1;
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,14 @@
import format from 'date-fns/format';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
export class PageResults {
page: number = 0;
total: number = 0;
totalPage: number = 0;
records: number = 0;
rows: any[] = [];
init(data: any): void {
Object.assign(this, data);
}
}

View File

@ -0,0 +1,73 @@
import { BaseEntity } from './BaseEntity';
export class PasswordPolicy extends BaseEntity {
minLength!: Number;
maxLength!: Number;
lowerCase!: Number;
upperCase!: Number;
digits!: Number;
specialChar!: Number;
attempts!: Number;
duration!: Number;
expiration!: Number;
username!: Number;
history!: Number;
dictionary!: Number;
alphabetical!: Number;
numerical!: Number;
qwerty!: Number;
occurances!: Number;
switch_username: boolean = false;
switch_dictionary: boolean = false;
switch_alphabetical: boolean = false;
switch_numerical: boolean = false;
switch_qwerty: boolean = false;
override init(data: any): void {
Object.assign(this, data);
if (this.alphabetical == 1) {
this.switch_alphabetical = true;
}
if (this.username == 1) {
this.switch_username = true;
}
if (this.qwerty == 1) {
this.switch_qwerty = true;
}
if (this.numerical == 1) {
this.switch_numerical = true;
}
if (this.dictionary == 1) {
this.switch_dictionary = true;
}
}
override trans(): void {
if (this.switch_alphabetical) {
this.alphabetical = 1;
} else {
this.alphabetical = 0;
}
if (this.switch_username) {
this.username = 1;
} else {
this.username = 0;
}
if (this.switch_qwerty) {
this.qwerty = 1;
} else {
this.qwerty = 0;
}
if (this.switch_numerical) {
this.numerical = 1;
} else {
this.numerical = 0;
}
if (this.switch_dictionary) {
this.dictionary = 1;
} else {
this.dictionary = 0;
}
}
}

View File

@ -0,0 +1,34 @@
import { BaseEntity } from './BaseEntity';
export class Resources extends BaseEntity {
name!: String;
appId!: String;
appName!: String;
parentId!: String;
parentName!: String;
resourceType!: String;
resourceIcon!: String;
resourceStyle!: String;
resourceUrl!: String;
resourceAction!: String;
switch_dynamic: boolean = false;
constructor() {
super();
this.status = 1;
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,3 @@
import { BaseEntity } from './BaseEntity';
export class RoleMembers extends BaseEntity { }

View File

@ -0,0 +1,57 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class Roles extends BaseEntity {
name!: String;
dynamic!: String;
filters!: String;
orgIdsList!: String;
resumeTime!: String;
suspendTime!: String;
isdefault!: String;
switch_dynamic: boolean = false;
picker_resumeTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
picker_suspendTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
constructor() {
super();
this.status = 1;
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
if (this.dynamic == '1') {
this.switch_dynamic = true;
}
if (this.resumeTime != '') {
this.picker_resumeTime = new Date(format(new Date(), `yyyy-MM-dd ${this.resumeTime}:00`));
}
if (this.suspendTime != '') {
this.picker_suspendTime = new Date(format(new Date(), `yyyy-MM-dd ${this.suspendTime}:00`));
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
if (this.switch_dynamic) {
this.dynamic = '1';
} else {
this.dynamic = '0';
}
if (this.picker_resumeTime) {
this.resumeTime = format(this.picker_resumeTime, 'HH:mm');
}
if (this.picker_suspendTime) {
this.suspendTime = format(this.picker_suspendTime, 'HH:mm');
}
}
}

View File

@ -0,0 +1,27 @@
import { BaseEntity } from './BaseEntity';
export class SmsProvider extends BaseEntity {
provider!: String;
providerName!: String;
message!: String;
appKey!: String;
appSecret!: String;
templateId!: String;
signName!: String;
smsSdkAppId!: String;
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
}
}

View File

@ -0,0 +1,22 @@
import { BaseEntity } from './BaseEntity';
import { SocialsProvider } from './SocialsProvider';
export class SocialsAssociate extends SocialsProvider {
redirectUri!: String;
accountId!: String;
bindTime!: String;
unBindTime!: String;
lastLoginTime!: String;
state!: String;
userBind!: String;
constructor() {
super();
}
override init(data: any): void {
Object.assign(this, data);
}
override trans(): void { }
}

View File

@ -0,0 +1,42 @@
import { BaseEntity } from './BaseEntity';
export class SocialsProvider extends BaseEntity {
provider!: String;
providerName!: String;
icon!: String;
clientId!: String;
clientSecret!: String;
agentId!: String;
hidden!: String;
scanCode!: String;
switch_hidden: boolean = false;
constructor() {
super();
this.status = 1;
this.scanCode = 'none';
this.sortIndex = 1;
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
if (this.hidden == 'true') {
this.switch_hidden = true;
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
if (this.switch_hidden) {
this.hidden = 'true';
}
}
}

View File

@ -0,0 +1,67 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class Synchronizers extends BaseEntity {
name!: String;
filters!: String;
sourceType!: String;
service!: String;
resumeTime!: String;
suspendTime!: String;
scheduler!: String;
syncStartTime!: Number;
providerUrl!: String;
driverClass!: String;
principal!: String;
credentials!: String;
sslSwitch!: Number;
basedn!: String;
msadDomain!: String;
trustStore!: String;
trustStorePassword!: String;
switch_sslSwitch: boolean = false;
picker_resumeTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
picker_suspendTime: Date = new Date(format(new Date(), 'yyyy-MM-dd 00:00:00'));
constructor() {
super();
this.status = 1;
this.sourceType != 'API';
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
if (this.sslSwitch == 1) {
this.switch_sslSwitch = true;
}
if (this.resumeTime != '') {
this.picker_resumeTime = new Date(format(new Date(), `yyyy-MM-dd ${this.resumeTime}:00`));
}
if (this.suspendTime != '') {
this.picker_suspendTime = new Date(format(new Date(), `yyyy-MM-dd ${this.suspendTime}:00`));
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
if (this.switch_sslSwitch) {
this.sslSwitch = 1;
} else {
this.sslSwitch = 0;
}
if (this.picker_resumeTime) {
this.resumeTime = format(this.picker_resumeTime, 'HH:mm');
}
if (this.picker_suspendTime) {
this.suspendTime = format(this.picker_suspendTime, 'HH:mm');
}
}
}

View File

@ -0,0 +1,13 @@
import format from 'date-fns/format';
import { BaseEntity } from './BaseEntity';
export class TimeBased extends BaseEntity {
displayName!: String;
username!: String;
digits!: String;
period!: String;
sharedSecret!: String;
hexSharedSecret!: String;
rqCode!: String;
}

View File

@ -0,0 +1,38 @@
import { NzFormatEmitEvent, NzTreeNode, NzTreeNodeOptions } from 'ng-zorro-antd/tree';
export class TreeNodes {
activated!: NzTreeNode;
request!: any[];
nodes!: any[];
checkable!: boolean;
checkedKeys!: any[];
selectedKeys!: any[];
_rootNode!: any;
constructor(checkable: boolean) {
this.checkable = checkable;
this.checkedKeys = [];
this.selectedKeys = [];
}
init(treeAttrs: any) {
this._rootNode = { title: treeAttrs.rootNode.title, key: treeAttrs.rootNode.key, expanded: true, isLeaf: false };
this.request = treeAttrs.nodes;
}
build(): any[] {
return this.buildTree(this._rootNode);
}
buildTree(rootNode: any): any[] {
let treeNodes: any[] = [];
for (let node of this.request) {
if (node.key != rootNode.key && node.parentKey == rootNode.key) {
let treeNode = { title: node.title, key: node.key, expanded: false, isLeaf: true };
this.buildTree(treeNode);
treeNodes.push(treeNode);
rootNode.isLeaf = false;
}
}
rootNode.children = treeNodes;
return [rootNode];
}
}

View File

@ -0,0 +1,164 @@
import { BaseEntity } from './BaseEntity';
export class Users extends BaseEntity {
username!: String;
password!: String;
decipherable!: String;
sharedSecret!: String;
sharedCounter!: String;
/**
* "Employee", "Supplier","Dealer","Contractor",Partner,Customer "Intern",
* "Temp", "External", and "Unknown" .
*/
userType!: String;
userState!: String;
windowsAccount!: String;
// for user name
displayName!: String;
nickName!: String;
nameZhSpell!: String;
nameZhShortSpell!: String;
givenName!: String;
middleName!: String;
familyName!: String;
honorificPrefix!: String;
honorificSuffix!: String;
formattedName!: String;
married!: Number;
gender!: Number;
birthDate!: String;
picture!: String;
pictureId!: String;
pictureBase64!: string;
idType!: Number;
idCardNo!: String;
webSite!: String;
startWorkDate!: String;
// for security
authnType!: String;
email!: String;
emailVerified!: Number;
mobile!: String;
mobileVerified!: String;
passwordQuestion!: String;
passwordAnswer!: String;
// for apps login protected
appLoginAuthnType!: String;
appLoginPassword!: String;
protectedApps!: String;
protectedAppsMap!: String;
passwordLastSetTime!: String;
badPasswordCount!: Number;
badPasswordTime!: String;
unLockTime!: String;
isLocked!: Number;
lastLoginTime!: String;
lastLoginIp!: String;
lastLogoffTime!: String;
passwordSetType!: Number;
loginCount!: Number;
regionHistory!: String;
passwordHistory!: String;
locale!: String;
timeZone!: String;
preferredLanguage!: String;
// for work
workCountry!: String;
workRegion!: String; // province;
workLocality!: String; // city;
workStreetAddress!: String;
workAddressFormatted!: String;
workEmail!: String;
workPhoneNumber!: String;
workPostalCode!: String;
workFax!: String;
workOfficeName!: String;
// for home
homeCountry!: String;
homeRegion!: String; // province;
homeLocality!: String; // city;
homeStreetAddress!: String;
homeAddressFormatted!: String;
homeEmail!: String;
homePhoneNumber!: String;
homePostalCode!: String;
homeFax!: String;
// for company
employeeNumber!: String;
costCenter!: String;
organization!: String;
division!: String;
departmentId!: String;
department!: String;
jobTitle!: String;
jobLevel!: String;
managerId!: String;
manager!: String;
assistantId!: String;
assistant!: String;
entryDate!: String;
quitDate!: String;
// for social contact
defineIm!: String;
theme!: String;
/*
* for extended Attribute from userType extraAttribute for database
* extraAttributeName & extraAttributeValue for page submit
*/
//protected String extraAttribute;
//protected String extraAttributeName;
//protected String extraAttributeValue;
//@JsonIgnore
//protected HashMap<String, String> extraAttributeMap;
online!: Number;
gridList!: Number;
switch_dynamic: boolean = false;
gender_select!: String;
constructor() {
super();
this.status = 1;
this.sortIndex = 1;
this.gender = 1;
this.userType = 'EMPLOYEE';
this.userState = 'RESIDENT';
this.gender_select = '1';
}
override init(data: any): void {
Object.assign(this, data);
if (this.status == 1) {
this.switch_status = true;
}
if (this.gender == 1) {
this.gender_select = '1';
} else {
this.gender_select = '2';
}
}
override trans(): void {
if (this.switch_status) {
this.status = 1;
} else {
this.status = 0;
}
if (this.gender_select == '1') {
this.gender = 1;
} else {
this.gender = 2;
}
}
}

View File

@ -0,0 +1,77 @@
/* eslint-disable import/order */
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { DelonACLModule } from '@delon/acl';
import { AlainThemeModule } from '@delon/theme';
import { AlainConfig, ALAIN_CONFIG } from '@delon/util/config';
import { throwIfAlreadyLoaded } from '@core';
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' },
lodop: {
license: `A59B099A586B3851E0F0D7FDBF37B603`,
licenseA: `C94CEE276DB2187AE6B65D56B3FC2848`
},
auth: { login_url: '/passport/login' }
};
const alainModules: any[] = [AlainThemeModule.forRoot(), DelonACLModule.forRoot()];
const alainProvides = [{ provide: ALAIN_CONFIG, useValue: alainConfig }];
// #region reuse-tab
/**
* [](https://ng-alain.com/components/reuse-tab)需要:
* 1 `shared-delon.module.ts` `ReuseTabModule`
* 2 `RouteReuseStrategy`
* 3 `src/app/layout/default/default.component.html`
* ```html
* <section class="alain-default__content">
* <reuse-tab #reuseTab></reuse-tab>
* <router-outlet (activate)="reuseTab.activate($event)"></router-outlet>
* </section>
* ```
*/
// import { RouteReuseStrategy } from '@angular/router';
// import { ReuseTabService, ReuseTabStrategy } from '@delon/abc/reuse-tab';
// 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) {
throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule');
}
static forRoot(): ModuleWithProviders<GlobalConfigModule> {
return {
ngModule: GlobalConfigModule,
providers: [...alainProvides, ...zorroProvides]
};
}
}

View File

@ -0,0 +1 @@
[Document](https://ng-alain.com/theme/default)

View File

@ -0,0 +1,115 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { SettingsService, User } from '@delon/theme';
import { environment } from '@env/environment';
import { LayoutDefaultOptions } from '../../theme/layout-default';
@Component({
selector: 'layout-basic',
styles: [
`
.alain-default__collapsed .alain-default__aside-user {
width: 64px;
margin-left: 16px;
}
`
],
template: `
<layout-default [options]="options" [asideUser]="asideUserTpl" [content]="contentTpl" [customError]="null">
<layout-default-header-item direction="left">
<a href="#">
<img src="../assets/logo.jpg" alt="logo" style="height: 50px;height: 50px;float: left;" />
<div
style="letter-spacing: 2px;
font-size: 20px;
font-weight: bolder;
color:white;
width: 450px;
margin-top: 12px;"
>
Max<span style="color: #FFD700;">Key</span>{{ 'mxk.title' | i18n }}
</div>
</a>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<div layout-default-header-item-trigger (click)="profile()"> {{ user.name }}</div>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<div layout-default-header-item-trigger nz-dropdown [nzDropdownMenu]="settingsMenu" nzTrigger="click" nzPlacement="bottomRight">
<i nz-icon nzType="setting"></i>
</div>
<nz-dropdown-menu #settingsMenu="nzDropdownMenu">
<div nz-menu style="width: 200px;">
<div nz-menu-item (click)="changePassword()">
<i nz-icon nzType="key" nzTheme="outline"></i>
{{ 'mxk.menu.config.password' | i18n }}
</div>
<div nz-menu-item>
<header-rtl></header-rtl>
</div>
<div nz-menu-item>
<header-fullscreen></header-fullscreen>
</div>
<div nz-menu-item>
<header-clear-storage></header-clear-storage>
</div>
<div nz-menu-item>
<header-i18n></header-i18n>
</div>
</div>
</nz-dropdown-menu>
</layout-default-header-item>
<layout-default-header-item direction="right">
<header-user></header-user>
</layout-default-header-item>
<ng-template #asideUserTpl>
<div nz-dropdown nzTrigger="click" [nzDropdownMenu]="userMenu" class="alain-default__aside-user">
<div class="alain-default__aside-user-info">
<!--<strong>{{ user.name }}</strong>
<p class="mb0">{{ user.email }}</p>-->
</div>
<!--
<nz-avatar class="alain-default__aside-user-avatar" [nzSrc]="user.avatar"></nz-avatar>
-->
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<ul nz-menu>
<!--
<li nz-menu-item routerLink="/pro/account/center">{{ 'menu.account.center' | i18n }}</li>
<li nz-menu-item routerLink="/pro/account/settings">{{ 'menu.account.settings' | i18n }}</li>
-->
</ul>
</nz-dropdown-menu>
</ng-template>
<ng-template #contentTpl>
<router-outlet></router-outlet>
</ng-template>
</layout-default>
<setting-drawer *ngIf="showSettingDrawer"></setting-drawer>
<theme-btn></theme-btn>
`
})
export class LayoutBasicComponent {
options: LayoutDefaultOptions = {
logoExpanded: `./assets/logo-full.svg`,
logoCollapsed: `./assets/logo.svg`,
hideAside: false
};
searchToggleStatus = false;
showSettingDrawer = !environment.production;
get user(): User {
return this.settingsService.user;
}
profile(): void {
this.router.navigateByUrl('/config/profile');
}
changePassword(): void {
this.router.navigateByUrl('/config/password');
}
constructor(private settingsService: SettingsService, private router: Router) { }
}

View File

@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
@Component({
selector: 'header-clear-storage',
template: `
<i nz-icon nzType="tool"></i>
{{ 'menu.clear.local.storage' | i18n }}
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderClearStorageComponent {
constructor(private modalSrv: NzModalService, private messageSrv: NzMessageService) {}
@HostListener('click')
_click(): void {
this.modalSrv.confirm({
nzTitle: 'Make sure clear all local storage?',
nzOnOk: () => {
localStorage.clear();
this.messageSrv.success('Clear Finished!');
}
});
}
}

View File

@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import screenfull from 'screenfull';
@Component({
selector: 'header-fullscreen',
template: `
<i nz-icon [nzType]="status ? 'fullscreen-exit' : 'fullscreen'"></i>
{{ (status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | i18n }}
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderFullScreenComponent {
status = false;
@HostListener('window:resize')
_resize(): void {
this.status = screenfull.isFullscreen;
}
@HostListener('click')
_click(): void {
if (screenfull.isEnabled) {
screenfull.toggle();
}
}
}

View File

@ -0,0 +1,57 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, SettingsService } from '@delon/theme';
import { BooleanInput, InputBoolean } from '@delon/util/decorator';
@Component({
selector: 'header-i18n',
template: `
<div *ngIf="showLangText" nz-dropdown [nzDropdownMenu]="langMenu" nzPlacement="bottomRight">
<i nz-icon nzType="global"></i>
{{ 'menu.lang' | i18n }}
<i nz-icon nzType="down"></i>
</div>
<i *ngIf="!showLangText" nz-dropdown [nzDropdownMenu]="langMenu" nzPlacement="bottomRight" nz-icon nzType="global"></i>
<nz-dropdown-menu #langMenu="nzDropdownMenu">
<ul nz-menu>
<li nz-menu-item *ngFor="let item of langs" [nzSelected]="item.code === curLangCode" (click)="change(item.code)">
<span role="img" [attr.aria-label]="item.text" class="pr-xs">{{ item.abbr }}</span>
{{ item.text }}
</li>
</ul>
</nz-dropdown-menu>
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderI18nComponent {
static ngAcceptInputType_showLangText: BooleanInput;
/** Whether to display language text */
@Input() @InputBoolean() showLangText = true;
get langs(): Array<{ code: string; text: string; abbr: string }> {
return this.i18n.getLangs();
}
get curLangCode(): string {
return this.settings.layout.lang;
}
constructor(private settings: SettingsService, @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService, @Inject(DOCUMENT) private doc: any) {}
change(lang: string): void {
const spinEl = this.doc.createElement('div');
spinEl.setAttribute('class', `page-loading ant-spin ant-spin-lg ant-spin-spinning`);
spinEl.innerHTML = `<span class="ant-spin-dot ant-spin-dot-spin"><i></i><i></i><i></i><i></i></span>`;
this.doc.body.appendChild(spinEl);
this.i18n.loadLangData(lang).subscribe(res => {
this.i18n.use(lang, res);
this.settings.setLayout('lang', lang);
setTimeout(() => this.doc.location.reload());
});
}
}

View File

@ -0,0 +1,70 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
@Component({
selector: 'header-icon',
template: `
<div
class="alain-default__nav-item"
nz-dropdown
[nzDropdownMenu]="iconMenu"
nzTrigger="click"
nzPlacement="bottomRight"
(nzVisibleChange)="change()"
>
<i nz-icon nzType="appstore"></i>
</div>
<nz-dropdown-menu #iconMenu="nzDropdownMenu">
<div nz-menu class="wd-xl animated jello">
<nz-spin [nzSpinning]="loading" [nzTip]="'正在读取数据...'">
<div nz-row [nzJustify]="'center'" [nzAlign]="'middle'" class="app-icons">
<div nz-col [nzSpan]="6">
<i nz-icon nzType="calendar" class="bg-error text-white"></i>
<small>Calendar</small>
</div>
<div nz-col [nzSpan]="6">
<i nz-icon nzType="file" class="bg-geekblue text-white"></i>
<small>Files</small>
</div>
<div nz-col [nzSpan]="6">
<i nz-icon nzType="cloud" class="bg-success text-white"></i>
<small>Cloud</small>
</div>
<div nz-col [nzSpan]="6">
<i nz-icon nzType="star" class="bg-magenta text-white"></i>
<small>Star</small>
</div>
<div nz-col [nzSpan]="6">
<i nz-icon nzType="team" class="bg-purple text-white"></i>
<small>Team</small>
</div>
<div nz-col [nzSpan]="6">
<i nz-icon nzType="scan" class="bg-warning text-white"></i>
<small>QR</small>
</div>
<div nz-col [nzSpan]="6">
<i nz-icon nzType="pay-circle" class="bg-cyan text-white"></i>
<small>Pay</small>
</div>
<div nz-col [nzSpan]="6">
<i nz-icon nzType="printer" class="bg-grey text-white"></i>
<small>Print</small>
</div>
</div>
</nz-spin>
</div>
</nz-dropdown-menu>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderIconComponent {
loading = true;
constructor(private cdr: ChangeDetectorRef) {}
change(): void {
setTimeout(() => {
this.loading = false;
this.cdr.detectChanges();
}, 500);
}
}

View File

@ -0,0 +1,193 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { NoticeIconList, NoticeIconSelect, NoticeItem } from '@delon/abc/notice-icon';
import { add, formatDistanceToNow, parse } from 'date-fns';
import { NzI18nService } from 'ng-zorro-antd/i18n';
import { NzMessageService } from 'ng-zorro-antd/message';
@Component({
selector: 'header-notify',
template: `
<notice-icon
[data]="data"
[count]="count"
[loading]="loading"
btnClass="alain-default__nav-item"
btnIconClass="alain-default__nav-item-icon"
(select)="select($event)"
(clear)="clear($event)"
(popoverVisibleChange)="loadData()"
></notice-icon>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderNotifyComponent {
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 nzI18n: NzI18nService, private cdr: ChangeDetectorRef) {}
private updateNoticeData(notices: NoticeIconList[]): NoticeItem[] {
const data = this.data.slice();
data.forEach(i => (i.list = []));
notices.forEach(item => {
const newItem = { ...item } as NoticeIconList;
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, { locale: this.nzI18n.getDateLocale() });
}
if (newItem.extra && newItem['status']) {
newItem['color'] = (
{
todo: undefined,
processing: 'blue',
urgent: 'red',
doing: 'gold'
} as { [key: string]: string | undefined }
)[newItem['status']];
}
data.find(w => w.title === newItem['type'])!.list.push(newItem);
});
return data;
}
loadData(): void {
if (this.loading) {
return;
}
this.loading = true;
setTimeout(() => {
const now = new Date();
this.data = this.updateNoticeData([
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: add(now, { days: 10 }),
type: '通知'
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: add(now, { days: -3 }),
type: '通知'
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: add(now, { months: -3 }),
read: true,
type: '通知'
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: add(now, { years: -1 }),
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();
}, 500);
}
clear(type: string): void {
this.msg.success(`清空了 ${type}`);
}
select(res: NoticeIconSelect): void {
this.msg.success(`点击了 ${res.title}${res.item.title}`);
}
}

View File

@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import { RTLService } from '@delon/theme';
@Component({
selector: 'header-rtl',
template: `
<i nz-icon [nzType]="rtl.nextDir === 'rtl' ? 'border-left' : 'border-right'"></i>
{{ rtl.nextDir | uppercase }}
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderRTLComponent {
constructor(public rtl: RTLService) {}
@HostListener('click')
toggleDirection(): void {
this.rtl.toggle();
}
}

View File

@ -0,0 +1,108 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
Output
} from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
@Component({
selector: 'header-search',
template: `
<nz-input-group [nzPrefix]="iconTpl" [nzSuffix]="loadingTpl">
<ng-template #iconTpl>
<i nz-icon [nzType]="focus ? 'arrow-down' : 'search'"></i>
</ng-template>
<ng-template #loadingTpl>
<i *ngIf="loading" nz-icon nzType="loading"></i>
</ng-template>
<input
type="text"
nz-input
[(ngModel)]="q"
[nzAutocomplete]="auto"
(input)="search($event)"
(focus)="qFocus()"
(blur)="qBlur()"
[attr.placeholder]="'menu.search.placeholder' | i18n"
/>
</nz-input-group>
<nz-autocomplete nzBackfill #auto>
<nz-auto-option *ngFor="let i of options" [nzValue]="i">{{ i }}</nz-auto-option>
</nz-autocomplete>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderSearchComponent implements AfterViewInit, OnDestroy {
q = '';
qIpt: HTMLInputElement | null = null;
options: string[] = [];
search$ = new BehaviorSubject('');
loading = false;
@HostBinding('class.alain-default__search-focus')
focus = false;
@HostBinding('class.alain-default__search-toggled')
searchToggled = false;
@Input()
set toggleChange(value: boolean) {
if (typeof value === 'undefined') {
return;
}
this.searchToggled = value;
this.focus = value;
if (value) {
setTimeout(() => this.qIpt!.focus());
}
}
@Output() readonly toggleChangeChange = new EventEmitter<boolean>();
constructor(private el: ElementRef<HTMLElement>, private cdr: ChangeDetectorRef) {}
ngAfterViewInit(): void {
this.qIpt = this.el.nativeElement.querySelector('.ant-input') as HTMLInputElement;
this.search$
.pipe(
debounceTime(500),
distinctUntilChanged(),
tap({
complete: () => {
this.loading = true;
}
})
)
.subscribe(value => {
this.options = value ? [value, value + value, value + value + value] : [];
this.loading = false;
this.cdr.detectChanges();
});
}
qFocus(): void {
this.focus = true;
}
qBlur(): void {
this.focus = false;
this.searchToggled = false;
this.options.length = 0;
this.toggleChangeChange.emit(false);
}
search(ev: Event): void {
this.search$.next((ev.target as HTMLInputElement).value);
}
ngOnDestroy(): void {
this.search$.complete();
this.search$.unsubscribe();
}
}

View File

@ -0,0 +1,88 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
@Component({
selector: 'header-task',
template: `
<div
class="alain-default__nav-item"
nz-dropdown
[nzDropdownMenu]="taskMenu"
nzTrigger="click"
nzPlacement="bottomRight"
(nzVisibleChange)="change()"
>
<nz-badge [nzDot]="true">
<i nz-icon nzType="bell" class="alain-default__nav-item-icon"></i>
</nz-badge>
</div>
<nz-dropdown-menu #taskMenu="nzDropdownMenu">
<div nz-menu class="wd-lg">
<div *ngIf="loading" class="mx-lg p-lg"><nz-spin></nz-spin></div>
<nz-card *ngIf="!loading" nzTitle="Notifications" nzBordered="false" class="ant-card__body-nopadding">
<ng-template #extra><i nz-icon nzType="plus"></i></ng-template>
<div nz-row [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm pr-md point bg-grey-lighter-h">
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/1.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>cipchk</strong>
<p class="mb0">Please tell me what happened in a few words, don't go into details.</p>
</div>
</div>
<div nz-row [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm pr-md point bg-grey-lighter-h">
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/2.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong></strong>
<p class="mb0"></p>
</div>
</div>
<div nz-row [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm pr-md point bg-grey-lighter-h">
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/3.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong></strong>
<p class="mb0"></p>
</div>
</div>
<div nz-row [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm pr-md point bg-grey-lighter-h">
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/4.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>Kent</strong>
<p class="mb0">Please tell me what happened in a few words, don't go into details.</p>
</div>
</div>
<div nz-row [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm pr-md point bg-grey-lighter-h">
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/5.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>Jefferson</strong>
<p class="mb0">Please tell me what happened in a few words, don't go into details.</p>
</div>
</div>
<div nz-row>
<div nz-col [nzSpan]="24" class="pt-md border-top-1 text-center text-grey point">See All</div>
</div>
</nz-card>
</div>
</nz-dropdown-menu>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderTaskComponent {
loading = true;
constructor(private cdr: ChangeDetectorRef) {}
change(): void {
setTimeout(() => {
this.loading = false;
this.cdr.detectChanges();
}, 500);
}
}

View File

@ -0,0 +1,66 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { SettingsService, User } from '@delon/theme';
@Component({
selector: 'header-user',
template: `
<!--
<div class="alain-default__nav-item d-flex align-items-center px-sm" nz-dropdown nzPlacement="bottomRight" [nzDropdownMenu]="userMenu">
<nz-avatar [nzSrc]="user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
{{ 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>
{{ 'menu.account.center' | i18n }}
</div>
<div nz-menu-item routerLink="/pro/account/settings">
<i nz-icon nzType="setting" class="mr-sm"></i>
{{ 'menu.account.settings' | i18n }}
</div>
<div nz-menu-item routerLink="/exception/trigger">
<i nz-icon nzType="close-circle" class="mr-sm"></i>
{{ 'menu.account.trigger' | i18n }}
</div>
<li nz-menu-divider></li>
<div nz-menu-item (click)="logout()">
<i nz-icon nzType="logout" class="mr-sm"></i>
{{ 'menu.account.logout' | i18n }}
</div>
</div>
</nz-dropdown-menu>
<div class="alain-default__nav-item d-flex align-items-center px-sm" nz-dropdown nzPlacement="bottomRight" [nzDropdownMenu]="userMenu">
<nz-avatar [nzSrc]="user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
{{ user.name }}
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<div nz-menu-item (click)="logout()">
<i nz-icon nzType="logout" class="mr-sm"></i>
{{ 'menu.account.logout' | i18n }}
</div>
</nz-dropdown-menu>-->
<div layout-default-header-item-trigger>
<div (click)="logout()">
<i nz-icon nzType="logout" class="mr-sm"></i>
<!--{{ 'menu.account.logout' | i18n }}-->
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderUserComponent {
get user(): User {
return this.settings.user;
}
constructor(private settings: SettingsService, private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
logout(): void {
this.tokenService.clear();
this.router.navigateByUrl(this.tokenService.login_url!);
}
}

View File

@ -0,0 +1 @@
[Document](https://ng-alain.com/theme/blank)

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'layout-blank',
template: `<router-outlet></router-outlet> `,
host: {
'[class.alain-blank]': 'true'
}
})
export class LayoutBlankComponent { }

Some files were not shown because too many files have changed in this diff Show More