mirror of
https://gitee.com/dromara/MaxKey.git
synced 2025-12-07 17:38:32 +08:00
frontend
This commit is contained in:
parent
b6d30a8730
commit
ba518828f8
17
maxkey-web-frontend/maxkey-web-app/.browserslistrc
Normal file
17
maxkey-web-frontend/maxkey-web-app/.browserslistrc
Normal file
@ -0,0 +1,17 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
16
maxkey-web-frontend/maxkey-web-app/.editorconfig
Normal file
16
maxkey-web-frontend/maxkey-web-app/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
34
maxkey-web-frontend/maxkey-web-app/.eslintignore
Normal file
34
maxkey-web-frontend/maxkey-web-app/.eslintignore
Normal file
@ -0,0 +1,34 @@
|
||||
_cli-tpl/
|
||||
dist/
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
.cache/
|
||||
|
||||
# yarn v2
|
||||
.yarn
|
||||
126
maxkey-web-frontend/maxkey-web-app/.eslintrc.js
Normal file
126
maxkey-web-frontend/maxkey-web-app/.eslintrc.js
Normal file
@ -0,0 +1,126 @@
|
||||
const prettierConfig = require('./.prettierrc.js');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: { ecmaVersion: 2021 },
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['tsconfig.json'],
|
||||
createDefaultProgram: true
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'jsdoc', 'import'],
|
||||
extends: [
|
||||
'plugin:@angular-eslint/recommended',
|
||||
'plugin:@angular-eslint/template/process-inline-templates',
|
||||
'plugin:prettier/recommended'
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': ['error', prettierConfig],
|
||||
'jsdoc/newline-after-description': 1,
|
||||
'@angular-eslint/component-class-suffix': [
|
||||
'error',
|
||||
{
|
||||
suffixes: ['Directive', 'Component', 'Base', 'Widget']
|
||||
}
|
||||
],
|
||||
'@angular-eslint/directive-class-suffix': [
|
||||
'error',
|
||||
{
|
||||
suffixes: ['Directive', 'Component', 'Base', 'Widget']
|
||||
}
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'off',
|
||||
{
|
||||
type: ['element', 'attribute'],
|
||||
prefix: ['app', 'test'],
|
||||
style: 'kebab-case'
|
||||
}
|
||||
],
|
||||
'@angular-eslint/directive-selector': [
|
||||
'off',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: ['app']
|
||||
}
|
||||
],
|
||||
'@angular-eslint/no-attribute-decorator': 'error',
|
||||
'@angular-eslint/no-conflicting-lifecycle': 'off',
|
||||
'@angular-eslint/no-forward-ref': 'off',
|
||||
'@angular-eslint/no-host-metadata-property': 'off',
|
||||
'@angular-eslint/no-lifecycle-call': 'off',
|
||||
'@angular-eslint/no-pipe-impure': 'error',
|
||||
'@angular-eslint/prefer-output-readonly': 'error',
|
||||
'@angular-eslint/use-component-selector': 'off',
|
||||
'@angular-eslint/use-component-view-encapsulation': 'off',
|
||||
'@angular-eslint/no-input-rename': 'off',
|
||||
'@angular-eslint/no-output-native': 'off',
|
||||
'@typescript-eslint/array-type': [
|
||||
'error',
|
||||
{
|
||||
default: 'array-simple'
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/ban-types': [
|
||||
'off',
|
||||
{
|
||||
types: {
|
||||
String: {
|
||||
message: 'Use string instead.'
|
||||
},
|
||||
Number: {
|
||||
message: 'Use number instead.'
|
||||
},
|
||||
Boolean: {
|
||||
message: 'Use boolean instead.'
|
||||
},
|
||||
Function: {
|
||||
message: 'Use specific callable interface instead.'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
'import/no-duplicates': 'error',
|
||||
'import/no-unused-modules': 'error',
|
||||
'import/no-unassigned-import': 'error',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
alphabetize: { order: 'asc', caseInsensitive: false },
|
||||
'newlines-between': 'always',
|
||||
groups: ['external', 'internal', ['parent', 'sibling', 'index']],
|
||||
pathGroups: [],
|
||||
pathGroupsExcludedImportTypes: []
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-this-alias': 'error',
|
||||
'@typescript-eslint/member-ordering': 'off',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-multiple-empty-lines': 'error',
|
||||
'no-sparse-arrays': 'error',
|
||||
'prefer-object-spread': 'error',
|
||||
'prefer-template': 'error',
|
||||
'prefer-const': 'off',
|
||||
'max-len': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['*.html'],
|
||||
extends: ['plugin:@angular-eslint/template/recommended'],
|
||||
rules: {}
|
||||
},
|
||||
{
|
||||
files: ['*.html'],
|
||||
excludedFiles: ['*inline-template-*.component.html'],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': ['error', { parser: 'angular' }],
|
||||
'@angular-eslint/template/eqeqeq': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
5
maxkey-web-frontend/maxkey-web-app/.github/FUNDING.yml
vendored
Normal file
5
maxkey-web-frontend/maxkey-web-app/.github/FUNDING.yml
vendored
Normal 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
|
||||
15
maxkey-web-frontend/maxkey-web-app/.github/ISSUE_TEMPLATE.md
vendored
Normal file
15
maxkey-web-frontend/maxkey-web-app/.github/ISSUE_TEMPLATE.md
vendored
Normal 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 会被立即关闭。
|
||||
-->
|
||||
8
maxkey-web-frontend/maxkey-web-app/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
maxkey-web-frontend/maxkey-web-app/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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 Group:428749721
|
||||
43
maxkey-web-frontend/maxkey-web-app/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
43
maxkey-web-frontend/maxkey-web-app/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
|
||||
28
maxkey-web-frontend/maxkey-web-app/.github/alain-bot.yml
vendored
Normal file
28
maxkey-web-frontend/maxkey-web-app/.github/alain-bot.yml
vendored
Normal 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 天内未跟进将会被自动关闭。
|
||||

|
||||
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 以方便我们定位错误。谢谢配合!
|
||||
14
maxkey-web-frontend/maxkey-web-app/.github/lock.yml
vendored
Normal file
14
maxkey-web-frontend/maxkey-web-app/.github/lock.yml
vendored
Normal 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
|
||||
10
maxkey-web-frontend/maxkey-web-app/.github/no-response.yml
vendored
Normal file
10
maxkey-web-frontend/maxkey-web-app/.github/no-response.yml
vendored
Normal 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。 感谢您的贡献。
|
||||
|
||||
14
maxkey-web-frontend/maxkey-web-app/.github/semantic.yml
vendored
Normal file
14
maxkey-web-frontend/maxkey-web-app/.github/semantic.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
titleAndCommits: true
|
||||
types:
|
||||
- feat
|
||||
- fix
|
||||
- docs
|
||||
- style
|
||||
- refactor
|
||||
- perf
|
||||
- test
|
||||
- build
|
||||
- ci
|
||||
- chore
|
||||
- revert
|
||||
- release
|
||||
41
maxkey-web-frontend/maxkey-web-app/.github/workflows/deploy-site.yml
vendored
Normal file
41
maxkey-web-frontend/maxkey-web-app/.github/workflows/deploy-site.yml
vendored
Normal 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
|
||||
13
maxkey-web-frontend/maxkey-web-app/.github/workflows/mirror.yml
vendored
Normal file
13
maxkey-web-frontend/maxkey-web-app/.github/workflows/mirror.yml
vendored
Normal 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 }}
|
||||
5
maxkey-web-frontend/maxkey-web-app/.husky/pre-commit
Normal file
5
maxkey-web-frontend/maxkey-web-app/.husky/pre-commit
Normal 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
|
||||
5
maxkey-web-frontend/maxkey-web-app/.npmignore
Normal file
5
maxkey-web-frontend/maxkey-web-app/.npmignore
Normal file
@ -0,0 +1,5 @@
|
||||
.github
|
||||
node_modules
|
||||
|
||||
dist
|
||||
tmp
|
||||
1
maxkey-web-frontend/maxkey-web-app/.nvmrc
Normal file
1
maxkey-web-frontend/maxkey-web-app/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
12.14.1
|
||||
18
maxkey-web-frontend/maxkey-web-app/.prettierignore
Normal file
18
maxkey-web-frontend/maxkey-web-app/.prettierignore
Normal file
@ -0,0 +1,18 @@
|
||||
# add files you wish to ignore here
|
||||
**/*.md
|
||||
**/*.svg
|
||||
**/test.ts
|
||||
|
||||
.stylelintrc
|
||||
.prettierrc
|
||||
|
||||
src/assets/*
|
||||
src/index.html
|
||||
node_modules/
|
||||
.vscode/
|
||||
coverage/
|
||||
dist/
|
||||
package.json
|
||||
tslint.json
|
||||
|
||||
_cli-tpl/**/*
|
||||
13
maxkey-web-frontend/maxkey-web-app/.prettierrc.js
Normal file
13
maxkey-web-frontend/maxkey-web-app/.prettierrc.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
useTabs: false,
|
||||
printWidth: 140,
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
htmlWhitespaceSensitivity: 'strict',
|
||||
arrowParens: 'avoid',
|
||||
bracketSpacing: true,
|
||||
proseWrap: 'preserve',
|
||||
trailingComma: 'none',
|
||||
endOfLine: 'lf'
|
||||
};
|
||||
38
maxkey-web-frontend/maxkey-web-app/.stylelintrc
Normal file
38
maxkey-web-frontend/maxkey-web-app/.stylelintrc
Normal 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/**/*"
|
||||
]
|
||||
}
|
||||
5
maxkey-web-frontend/maxkey-web-app/.vscode/extensions.json
vendored
Normal file
5
maxkey-web-frontend/maxkey-web-app/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"cipchk.ng-alain-extension-pack"
|
||||
]
|
||||
}
|
||||
16
maxkey-web-frontend/maxkey-web-app/.vscode/launch.json
vendored
Normal file
16
maxkey-web-frontend/maxkey-web-app/.vscode/launch.json
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
37
maxkey-web-frontend/maxkey-web-app/.vscode/settings.json
vendored
Normal file
37
maxkey-web-frontend/maxkey-web-app/.vscode/settings.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
1
maxkey-web-frontend/maxkey-web-app/_cli-tpl/README.md
Normal file
1
maxkey-web-frontend/maxkey-web-app/_cli-tpl/README.md
Normal file
@ -0,0 +1 @@
|
||||
[Document](https://ng-alain.com/cli/generate#Custom-template-page)
|
||||
@ -0,0 +1 @@
|
||||
<page-header></page-header>
|
||||
@ -0,0 +1,24 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { <%= componentName %> } from './<%= dasherize(name) %>.component';
|
||||
|
||||
describe('<%= componentName %>', () => {
|
||||
let component: <%= componentName %>;
|
||||
let fixture: ComponentFixture<<%= componentName %>>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ <%= componentName %> ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(<%= componentName %>);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
|
||||
@Component({
|
||||
selector: '<%= selector %>',
|
||||
templateUrl: './<%= dasherize(name) %>.component.html',<% if(!inlineStyle) { %><% } else { %>
|
||||
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
|
||||
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
|
||||
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
|
||||
})
|
||||
export class <%= componentName %> implements OnInit {
|
||||
|
||||
constructor(private http: _HttpClient, private msg: NzMessageService) { }
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
}
|
||||
1
maxkey-web-frontend/maxkey-web-app/_mock/README.md
Normal file
1
maxkey-web-frontend/maxkey-web-app/_mock/README.md
Normal file
@ -0,0 +1 @@
|
||||
[Document](https://ng-alain.com/mock)
|
||||
265
maxkey-web-frontend/maxkey-web-app/_mock/_api.ts
Normal file
265
maxkey-web-frontend/maxkey-web-app/_mock/_api.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { MockRequest, MockStatusError } from '@delon/mock';
|
||||
|
||||
// region: mock data
|
||||
|
||||
const titles = ['Alipay', 'Angular', 'Ant Design', 'Ant Design Pro', 'Bootstrap', 'React', 'Vue', 'Webpack'];
|
||||
|
||||
const avatars = [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
|
||||
];
|
||||
const covers = [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/HrxcVbrKnCJOZvtzSqjN.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/alaPpKWajEbIYEUvvVNf.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/RLwlKSYGSXGHuWSojyvp.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
|
||||
];
|
||||
const desc = [
|
||||
'那是一种内在的东西, 他们到达不了,也无法触及的',
|
||||
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
|
||||
'生命就像一盒巧克力,结果往往出人意料',
|
||||
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
|
||||
'那时候我只会想自己想要什么,从不想自己拥有什么',
|
||||
];
|
||||
|
||||
const user = ['卡色', 'cipchk', '付小小', '曲丽丽', '林东东', '周星星', '吴加好', '朱偏右', '鱼酱', '乐哥', '谭小仪', '仲尼'];
|
||||
|
||||
// endregion
|
||||
|
||||
function getFakeList(count: number = 20): any[] {
|
||||
const list: any[] = [];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
list.push({
|
||||
id: `fake-list-${i}`,
|
||||
owner: user[i % 10],
|
||||
title: titles[i % 8],
|
||||
avatar: avatars[i % 8],
|
||||
cover: parseInt((i / 4).toString(), 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
|
||||
status: ['active', 'exception', 'normal'][i % 3],
|
||||
percent: Math.ceil(Math.random() * 50) + 50,
|
||||
logo: avatars[i % 8],
|
||||
href: 'https://ant.design',
|
||||
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
|
||||
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
|
||||
subDescription: desc[i % 5],
|
||||
description:
|
||||
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
|
||||
activeUser: Math.ceil(Math.random() * 100000) + 100000,
|
||||
newUser: Math.ceil(Math.random() * 1000) + 1000,
|
||||
star: Math.ceil(Math.random() * 100) + 100,
|
||||
like: Math.ceil(Math.random() * 100) + 100,
|
||||
message: Math.ceil(Math.random() * 10) + 10,
|
||||
content:
|
||||
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
|
||||
members: [
|
||||
{
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
|
||||
name: '曲丽丽',
|
||||
},
|
||||
{
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
|
||||
name: '王昭君',
|
||||
},
|
||||
{
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
|
||||
name: '董娜娜',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function getNotice(): any[] {
|
||||
return [
|
||||
{
|
||||
id: 'xxx1',
|
||||
title: titles[0],
|
||||
logo: avatars[0],
|
||||
description: '那是一种内在的东西, 他们到达不了,也无法触及的',
|
||||
updatedAt: new Date(),
|
||||
member: '科学搬砖组',
|
||||
href: '',
|
||||
memberLink: '',
|
||||
},
|
||||
{
|
||||
id: 'xxx2',
|
||||
title: titles[1],
|
||||
logo: avatars[1],
|
||||
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
|
||||
updatedAt: new Date('2017-07-24'),
|
||||
member: '全组都是吴彦祖',
|
||||
href: '',
|
||||
memberLink: '',
|
||||
},
|
||||
{
|
||||
id: 'xxx3',
|
||||
title: titles[2],
|
||||
logo: avatars[2],
|
||||
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
|
||||
updatedAt: new Date(),
|
||||
member: '中二少女团',
|
||||
href: '',
|
||||
memberLink: '',
|
||||
},
|
||||
{
|
||||
id: 'xxx4',
|
||||
title: titles[3],
|
||||
logo: avatars[3],
|
||||
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
|
||||
updatedAt: new Date('2017-07-23'),
|
||||
member: '程序员日常',
|
||||
href: '',
|
||||
memberLink: '',
|
||||
},
|
||||
{
|
||||
id: 'xxx5',
|
||||
title: titles[4],
|
||||
logo: avatars[4],
|
||||
description: '凛冬将至',
|
||||
updatedAt: new Date('2017-07-23'),
|
||||
member: '高逼格设计天团',
|
||||
href: '',
|
||||
memberLink: '',
|
||||
},
|
||||
{
|
||||
id: 'xxx6',
|
||||
title: titles[5],
|
||||
logo: avatars[5],
|
||||
description: '生命就像一盒巧克力,结果往往出人意料',
|
||||
updatedAt: new Date('2017-07-23'),
|
||||
member: '骗你来学计算机',
|
||||
href: '',
|
||||
memberLink: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getActivities(): any[] {
|
||||
return [
|
||||
{
|
||||
id: 'trend-1',
|
||||
updatedAt: new Date(),
|
||||
user: {
|
||||
name: '林东东',
|
||||
avatar: avatars[0],
|
||||
},
|
||||
group: {
|
||||
name: '高逼格设计天团',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
project: {
|
||||
name: '六月迭代',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
template: '在 @{group} 新建项目 @{project}',
|
||||
},
|
||||
{
|
||||
id: 'trend-2',
|
||||
updatedAt: new Date(),
|
||||
user: {
|
||||
name: '付小小',
|
||||
avatar: avatars[1],
|
||||
},
|
||||
group: {
|
||||
name: '高逼格设计天团',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
project: {
|
||||
name: '六月迭代',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
template: '在 @{group} 新建项目 @{project}',
|
||||
},
|
||||
{
|
||||
id: 'trend-3',
|
||||
updatedAt: new Date(),
|
||||
user: {
|
||||
name: '曲丽丽',
|
||||
avatar: avatars[2],
|
||||
},
|
||||
group: {
|
||||
name: '中二少女团',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
project: {
|
||||
name: '六月迭代',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
template: '在 @{group} 新建项目 @{project}',
|
||||
},
|
||||
{
|
||||
id: 'trend-4',
|
||||
updatedAt: new Date(),
|
||||
user: {
|
||||
name: '周星星',
|
||||
avatar: avatars[3],
|
||||
},
|
||||
project: {
|
||||
name: '5 月日常迭代',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
template: '将 @{project} 更新至已发布状态',
|
||||
},
|
||||
{
|
||||
id: 'trend-5',
|
||||
updatedAt: new Date(),
|
||||
user: {
|
||||
name: '朱偏右',
|
||||
avatar: avatars[4],
|
||||
},
|
||||
project: {
|
||||
name: '工程效能',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
comment: {
|
||||
name: '留言',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
template: '在 @{project} 发布了 @{comment}',
|
||||
},
|
||||
{
|
||||
id: 'trend-6',
|
||||
updatedAt: new Date(),
|
||||
user: {
|
||||
name: '乐哥',
|
||||
avatar: avatars[5],
|
||||
},
|
||||
group: {
|
||||
name: '程序员日常',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
project: {
|
||||
name: '品牌迭代',
|
||||
link: 'http://github.com/',
|
||||
},
|
||||
template: '在 @{group} 新建项目 @{project}',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const APIS = {
|
||||
'/api/list': (req: MockRequest) => getFakeList(req.queryString.count),
|
||||
'/api/notice': () => getNotice(),
|
||||
'/api/activities': () => getActivities(),
|
||||
'POST /api/auth/refresh': { msg: 'ok', token: 'new-token-by-refresh' },
|
||||
'/api/401': () => {
|
||||
throw new MockStatusError(401);
|
||||
},
|
||||
'/api/403': () => {
|
||||
throw new MockStatusError(403);
|
||||
},
|
||||
'/api/404': () => {
|
||||
throw new MockStatusError(404);
|
||||
},
|
||||
'/api/500': () => {
|
||||
throw new MockStatusError(500);
|
||||
},
|
||||
};
|
||||
205
maxkey-web-frontend/maxkey-web-app/_mock/_chart.ts
Normal file
205
maxkey-web-frontend/maxkey-web-app/_mock/_chart.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { format } from 'date-fns';
|
||||
import * as Mock from 'mockjs';
|
||||
|
||||
// region: mock data
|
||||
|
||||
const visitData: any[] = [];
|
||||
const beginDay = new Date().getTime();
|
||||
|
||||
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
|
||||
for (let i = 0; i < fakeY.length; i += 1) {
|
||||
visitData.push({
|
||||
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
|
||||
y: fakeY[i]
|
||||
});
|
||||
}
|
||||
|
||||
const visitData2: any[] = [];
|
||||
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
|
||||
for (let i = 0; i < fakeY2.length; i += 1) {
|
||||
visitData2.push({
|
||||
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
|
||||
y: fakeY2[i]
|
||||
});
|
||||
}
|
||||
|
||||
const salesData: any[] = [];
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
salesData.push({
|
||||
x: `${i + 1}月`,
|
||||
y: Math.floor(Math.random() * 1000) + 200
|
||||
});
|
||||
}
|
||||
const searchData: any[] = [];
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
searchData.push({
|
||||
index: i + 1,
|
||||
keyword: `搜索关键词-${i}`,
|
||||
count: Math.floor(Math.random() * 1000),
|
||||
range: Math.floor(Math.random() * 100),
|
||||
status: Math.floor((Math.random() * 10) % 2)
|
||||
});
|
||||
}
|
||||
const salesTypeData = [
|
||||
{
|
||||
x: '家用电器',
|
||||
y: 4544
|
||||
},
|
||||
{
|
||||
x: '食用酒水',
|
||||
y: 3321
|
||||
},
|
||||
{
|
||||
x: '个护健康',
|
||||
y: 3113
|
||||
},
|
||||
{
|
||||
x: '服饰箱包',
|
||||
y: 2341
|
||||
},
|
||||
{
|
||||
x: '母婴产品',
|
||||
y: 1231
|
||||
},
|
||||
{
|
||||
x: '其他',
|
||||
y: 1231
|
||||
}
|
||||
];
|
||||
|
||||
const salesTypeDataOnline = [
|
||||
{
|
||||
x: '家用电器',
|
||||
y: 244
|
||||
},
|
||||
{
|
||||
x: '食用酒水',
|
||||
y: 321
|
||||
},
|
||||
{
|
||||
x: '个护健康',
|
||||
y: 311
|
||||
},
|
||||
{
|
||||
x: '服饰箱包',
|
||||
y: 41
|
||||
},
|
||||
{
|
||||
x: '母婴产品',
|
||||
y: 121
|
||||
},
|
||||
{
|
||||
x: '其他',
|
||||
y: 111
|
||||
}
|
||||
];
|
||||
|
||||
const salesTypeDataOffline = [
|
||||
{
|
||||
x: '家用电器',
|
||||
y: 99
|
||||
},
|
||||
{
|
||||
x: '个护健康',
|
||||
y: 188
|
||||
},
|
||||
{
|
||||
x: '服饰箱包',
|
||||
y: 344
|
||||
},
|
||||
{
|
||||
x: '母婴产品',
|
||||
y: 255
|
||||
},
|
||||
{
|
||||
x: '其他',
|
||||
y: 65
|
||||
}
|
||||
];
|
||||
|
||||
const offlineData: any[] = [];
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
offlineData.push({
|
||||
name: `门店${i}`,
|
||||
cvr: Math.ceil(Math.random() * 9) / 10
|
||||
});
|
||||
}
|
||||
const offlineChartData: any[] = [];
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
offlineChartData.push({
|
||||
time: new Date().getTime() + 1000 * 60 * 30 * i,
|
||||
y1: Math.floor(Math.random() * 100) + 10,
|
||||
y2: Math.floor(Math.random() * 100) + 10
|
||||
});
|
||||
}
|
||||
|
||||
const radarOriginData = [
|
||||
{
|
||||
name: '个人',
|
||||
ref: 10,
|
||||
koubei: 8,
|
||||
output: 4,
|
||||
contribute: 5,
|
||||
hot: 7
|
||||
},
|
||||
{
|
||||
name: '团队',
|
||||
ref: 3,
|
||||
koubei: 9,
|
||||
output: 6,
|
||||
contribute: 3,
|
||||
hot: 1
|
||||
},
|
||||
{
|
||||
name: '部门',
|
||||
ref: 4,
|
||||
koubei: 1,
|
||||
output: 6,
|
||||
contribute: 5,
|
||||
hot: 7
|
||||
}
|
||||
];
|
||||
|
||||
//
|
||||
const radarData: any[] = [];
|
||||
const radarTitleMap: any = {
|
||||
ref: '引用',
|
||||
koubei: '口碑',
|
||||
output: '产量',
|
||||
contribute: '贡献',
|
||||
hot: '热度'
|
||||
};
|
||||
radarOriginData.forEach((item: any) => {
|
||||
Object.keys(item).forEach(key => {
|
||||
if (key !== 'name') {
|
||||
radarData.push({
|
||||
name: item.name,
|
||||
label: radarTitleMap[key],
|
||||
value: item[key]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// endregion
|
||||
|
||||
export const CHARTS = {
|
||||
'/chart': JSON.parse(
|
||||
JSON.stringify({
|
||||
visitData,
|
||||
visitData2,
|
||||
salesData,
|
||||
searchData,
|
||||
offlineData,
|
||||
offlineChartData,
|
||||
salesTypeData,
|
||||
salesTypeDataOnline,
|
||||
salesTypeDataOffline,
|
||||
radarData
|
||||
})
|
||||
),
|
||||
'/chart/visit': JSON.parse(JSON.stringify(visitData)),
|
||||
'/chart/tags': Mock.mock({
|
||||
'list|100': [{ name: '@city', 'value|1-100': 150 }]
|
||||
})
|
||||
};
|
||||
76
maxkey-web-frontend/maxkey-web-app/_mock/_geo.ts
Normal file
76
maxkey-web-frontend/maxkey-web-app/_mock/_geo.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { MockRequest } from '@delon/mock';
|
||||
|
||||
const DATA = [
|
||||
{
|
||||
name: '上海',
|
||||
id: '310000',
|
||||
},
|
||||
{
|
||||
name: '市辖区',
|
||||
id: '310100',
|
||||
},
|
||||
{
|
||||
name: '北京',
|
||||
id: '110000',
|
||||
},
|
||||
{
|
||||
name: '市辖区',
|
||||
id: '110100',
|
||||
},
|
||||
{
|
||||
name: '浙江省',
|
||||
id: '330000',
|
||||
},
|
||||
{
|
||||
name: '杭州市',
|
||||
id: '330100',
|
||||
},
|
||||
{
|
||||
name: '宁波市',
|
||||
id: '330200',
|
||||
},
|
||||
{
|
||||
name: '温州市',
|
||||
id: '330300',
|
||||
},
|
||||
{
|
||||
name: '嘉兴市',
|
||||
id: '330400',
|
||||
},
|
||||
{
|
||||
name: '湖州市',
|
||||
id: '330500',
|
||||
},
|
||||
{
|
||||
name: '绍兴市',
|
||||
id: '330600',
|
||||
},
|
||||
{
|
||||
name: '金华市',
|
||||
id: '330700',
|
||||
},
|
||||
{
|
||||
name: '衢州市',
|
||||
id: '330800',
|
||||
},
|
||||
{
|
||||
name: '舟山市',
|
||||
id: '330900',
|
||||
},
|
||||
{
|
||||
name: '台州市',
|
||||
id: '331000',
|
||||
},
|
||||
{
|
||||
name: '丽水市',
|
||||
id: '331100',
|
||||
},
|
||||
];
|
||||
|
||||
export const GEOS = {
|
||||
'/geo/province': () => DATA.filter(w => w.id.endsWith('0000')),
|
||||
'/geo/:id': (req: MockRequest) => {
|
||||
const pid = (req.params.id || '310000').slice(0, 2);
|
||||
return DATA.filter(w => w.id.slice(0, 2) === pid && !w.id.endsWith('0000'));
|
||||
},
|
||||
};
|
||||
61
maxkey-web-frontend/maxkey-web-app/_mock/_pois.ts
Normal file
61
maxkey-web-frontend/maxkey-web-app/_mock/_pois.ts
Normal file
@ -0,0 +1,61 @@
|
||||
export const POIS = {
|
||||
'/pois': {
|
||||
total: 2,
|
||||
list: [
|
||||
{
|
||||
id: 10000,
|
||||
user_id: 1,
|
||||
name: '测试品牌',
|
||||
branch_name: '测试分店',
|
||||
geo: 310105,
|
||||
country: '中国',
|
||||
province: '上海',
|
||||
city: '上海市',
|
||||
district: '长宁区',
|
||||
address: '中山公园',
|
||||
tel: '15900000000',
|
||||
categories: '美食,粤菜,湛江菜',
|
||||
lng: 121.41707989151003,
|
||||
lat: 31.218656214644792,
|
||||
recommend: '推荐品',
|
||||
special: '特色服务',
|
||||
introduction: '商户简介',
|
||||
open_time: '营业时间',
|
||||
avg_price: 260,
|
||||
reason: null,
|
||||
status: 1,
|
||||
status_str: '待审核',
|
||||
status_wx: 1,
|
||||
modified: 1505826527288,
|
||||
created: 1505826527288,
|
||||
},
|
||||
{
|
||||
id: 10001,
|
||||
user_id: 2,
|
||||
name: '测试品牌2',
|
||||
branch_name: '测试分店2',
|
||||
geo: 310105,
|
||||
country: '中国',
|
||||
province: '上海',
|
||||
city: '上海市',
|
||||
district: '长宁区',
|
||||
address: '中山公园',
|
||||
tel: '15900000000',
|
||||
categories: '美食,粤菜,湛江菜',
|
||||
lng: 121.41707989151003,
|
||||
lat: 31.218656214644792,
|
||||
recommend: '推荐品',
|
||||
special: '特色服务',
|
||||
introduction: '商户简介',
|
||||
open_time: '营业时间',
|
||||
avg_price: 260,
|
||||
reason: null,
|
||||
status: 1,
|
||||
status_str: '待审核',
|
||||
status_wx: 1,
|
||||
modified: 1505826527288,
|
||||
created: 1505826527288,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
152
maxkey-web-frontend/maxkey-web-app/_mock/_profile.ts
Normal file
152
maxkey-web-frontend/maxkey-web-app/_mock/_profile.ts
Normal file
@ -0,0 +1,152 @@
|
||||
const basicGoods = [
|
||||
{
|
||||
id: '1234561',
|
||||
name: '矿泉水 550ml',
|
||||
barcode: '12421432143214321',
|
||||
price: '2.00',
|
||||
num: '1',
|
||||
amount: '2.00',
|
||||
},
|
||||
{
|
||||
id: '1234562',
|
||||
name: '凉茶 300ml',
|
||||
barcode: '12421432143214322',
|
||||
price: '3.00',
|
||||
num: '2',
|
||||
amount: '6.00',
|
||||
},
|
||||
{
|
||||
id: '1234563',
|
||||
name: '好吃的薯片',
|
||||
barcode: '12421432143214323',
|
||||
price: '7.00',
|
||||
num: '4',
|
||||
amount: '28.00',
|
||||
},
|
||||
{
|
||||
id: '1234564',
|
||||
name: '特别好吃的蛋卷',
|
||||
barcode: '12421432143214324',
|
||||
price: '8.50',
|
||||
num: '3',
|
||||
amount: '25.50',
|
||||
},
|
||||
];
|
||||
|
||||
const basicProgress = [
|
||||
{
|
||||
key: '1',
|
||||
time: '2017-10-01 14:10',
|
||||
rate: '联系客户',
|
||||
status: 'processing',
|
||||
operator: '取货员 ID1234',
|
||||
cost: '5mins',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
time: '2017-10-01 14:05',
|
||||
rate: '取货员出发',
|
||||
status: 'success',
|
||||
operator: '取货员 ID1234',
|
||||
cost: '1h',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
time: '2017-10-01 13:05',
|
||||
rate: '取货员接单',
|
||||
status: 'success',
|
||||
operator: '取货员 ID1234',
|
||||
cost: '5mins',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
time: '2017-10-01 13:00',
|
||||
rate: '申请审批通过',
|
||||
status: 'success',
|
||||
operator: '系统',
|
||||
cost: '1h',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
time: '2017-10-01 12:00',
|
||||
rate: '发起退货申请',
|
||||
status: 'success',
|
||||
operator: '用户',
|
||||
cost: '5mins',
|
||||
},
|
||||
];
|
||||
|
||||
const advancedOperation1 = [
|
||||
{
|
||||
key: 'op1',
|
||||
type: '订购关系生效',
|
||||
name: '曲丽丽',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
{
|
||||
key: 'op2',
|
||||
type: '财务复审',
|
||||
name: '付小小',
|
||||
status: 'reject',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '不通过原因',
|
||||
},
|
||||
{
|
||||
key: 'op3',
|
||||
type: '部门初审',
|
||||
name: '周毛毛',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
{
|
||||
key: 'op4',
|
||||
type: '提交订单',
|
||||
name: '林东东',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '很棒',
|
||||
},
|
||||
{
|
||||
key: 'op5',
|
||||
type: '创建订单',
|
||||
name: '汗牙牙',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
];
|
||||
|
||||
const advancedOperation2 = [
|
||||
{
|
||||
key: 'op1',
|
||||
type: '订购关系生效',
|
||||
name: '曲丽丽',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
];
|
||||
|
||||
const advancedOperation3 = [
|
||||
{
|
||||
key: 'op1',
|
||||
type: '创建订单',
|
||||
name: '汗牙牙',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
];
|
||||
|
||||
export const PROFILES = {
|
||||
'GET /profile/progress': basicProgress,
|
||||
'GET /profile/goods': basicGoods,
|
||||
'GET /profile/advanced': {
|
||||
advancedOperation1,
|
||||
advancedOperation2,
|
||||
advancedOperation3,
|
||||
},
|
||||
};
|
||||
82
maxkey-web-frontend/maxkey-web-app/_mock/_rule.ts
Normal file
82
maxkey-web-frontend/maxkey-web-app/_mock/_rule.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { HttpRequest } from '@angular/common/http';
|
||||
import { MockRequest } from '@delon/mock';
|
||||
|
||||
const list: any[] = [];
|
||||
|
||||
for (let i = 0; i < 46; i += 1) {
|
||||
list.push({
|
||||
key: i,
|
||||
disabled: i % 6 === 0,
|
||||
href: 'https://ant.design',
|
||||
avatar: [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||
][i % 2],
|
||||
no: `TradeCode ${i}`,
|
||||
title: `一个任务名称 ${i}`,
|
||||
owner: '曲丽丽',
|
||||
description: '这是一段描述',
|
||||
callNo: Math.floor(Math.random() * 1000),
|
||||
status: Math.floor(Math.random() * 10) % 4,
|
||||
updatedAt: new Date(`2017-07-${i < 18 ? '0' + (Math.floor(i / 2) + 1) : Math.floor(i / 2) + 1}`),
|
||||
createdAt: new Date(`2017-07-${i < 18 ? '0' + (Math.floor(i / 2) + 1) : Math.floor(i / 2) + 1}`),
|
||||
progress: Math.ceil(Math.random() * 100),
|
||||
});
|
||||
}
|
||||
|
||||
function getRule(params: any): any[] {
|
||||
let ret = [...list];
|
||||
if (params.sorter) {
|
||||
const s = params.sorter.split('_');
|
||||
ret = ret.sort((prev, next) => {
|
||||
if (s[1] === 'descend') {
|
||||
return next[s[0]] - prev[s[0]];
|
||||
}
|
||||
return prev[s[0]] - next[s[0]];
|
||||
});
|
||||
}
|
||||
if (params.statusList && params.statusList.length > 0) {
|
||||
ret = ret.filter((data) => params.statusList.indexOf(data.status) > -1);
|
||||
}
|
||||
if (params.no) {
|
||||
ret = ret.filter((data) => data.no.indexOf(params.no) > -1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function removeRule(nos: string): boolean {
|
||||
nos.split(',').forEach((no) => {
|
||||
const idx = list.findIndex((w) => w.no === no);
|
||||
if (idx !== -1) {
|
||||
list.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function saveRule(description: string): void {
|
||||
const i = Math.ceil(Math.random() * 10000);
|
||||
list.unshift({
|
||||
key: i,
|
||||
href: 'https://ant.design',
|
||||
avatar: [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||
][i % 2],
|
||||
no: `TradeCode ${i}`,
|
||||
title: `一个任务名称 ${i}`,
|
||||
owner: '曲丽丽',
|
||||
description,
|
||||
callNo: Math.floor(Math.random() * 1000),
|
||||
status: Math.floor(Math.random() * 10) % 2,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
progress: Math.ceil(Math.random() * 100),
|
||||
});
|
||||
}
|
||||
|
||||
export const RULES = {
|
||||
'/rule': (req: MockRequest) => getRule(req.queryString),
|
||||
'DELETE /rule': (req: MockRequest) => removeRule(req.queryString.nos),
|
||||
'POST /rule': (req: MockRequest) => saveRule(req.body.description),
|
||||
};
|
||||
122
maxkey-web-frontend/maxkey-web-app/_mock/_user.ts
Normal file
122
maxkey-web-frontend/maxkey-web-app/_mock/_user.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { MockRequest } from '@delon/mock';
|
||||
|
||||
const list: any[] = [];
|
||||
const total = 50;
|
||||
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
list.push({
|
||||
id: i + 1,
|
||||
disabled: i % 6 === 0,
|
||||
href: 'https://ant.design',
|
||||
avatar: [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||
][i % 2],
|
||||
no: `TradeCode ${i}`,
|
||||
title: `一个任务名称 ${i}`,
|
||||
owner: '曲丽丽',
|
||||
description: '这是一段描述',
|
||||
callNo: Math.floor(Math.random() * 1000),
|
||||
status: Math.floor(Math.random() * 10) % 4,
|
||||
updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
|
||||
createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
|
||||
progress: Math.ceil(Math.random() * 100),
|
||||
});
|
||||
}
|
||||
|
||||
function genData(params: any): { total: number; list: any[] } {
|
||||
let ret = [...list];
|
||||
const pi = +params.pi;
|
||||
const ps = +params.ps;
|
||||
const start = (pi - 1) * ps;
|
||||
|
||||
if (params.no) {
|
||||
ret = ret.filter((data) => data.no.indexOf(params.no) > -1);
|
||||
}
|
||||
|
||||
return { total: ret.length, list: ret.slice(start, ps * pi) };
|
||||
}
|
||||
|
||||
function saveData(id: number, value: any): { msg: string } {
|
||||
const item = list.find((w) => w.id === id);
|
||||
if (!item) {
|
||||
return { msg: '无效用户信息' };
|
||||
}
|
||||
Object.assign(item, value);
|
||||
return { msg: 'ok' };
|
||||
}
|
||||
|
||||
export const USERS = {
|
||||
'/user': (req: MockRequest) => genData(req.queryString),
|
||||
'/user/:id': (req: MockRequest) => list.find((w) => w.id === +req.params.id),
|
||||
'POST /user/:id': (req: MockRequest) => saveData(+req.params.id, req.body),
|
||||
'/user/current': {
|
||||
name: 'Cipchk',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
|
||||
userid: '00000001',
|
||||
email: 'cipchk@qq.com',
|
||||
signature: '海纳百川,有容乃大',
|
||||
title: '交互专家',
|
||||
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
|
||||
tags: [
|
||||
{
|
||||
key: '0',
|
||||
label: '很有想法的',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: '专注撩妹',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '帅~',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '通吃',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
label: '专职后端',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
label: '海纳百川',
|
||||
},
|
||||
],
|
||||
notifyCount: 12,
|
||||
country: 'China',
|
||||
geographic: {
|
||||
province: {
|
||||
label: '上海',
|
||||
key: '330000',
|
||||
},
|
||||
city: {
|
||||
label: '市辖区',
|
||||
key: '330100',
|
||||
},
|
||||
},
|
||||
address: 'XX区XXX路 XX 号',
|
||||
phone: '你猜-你猜你猜猜猜',
|
||||
},
|
||||
'POST /user/avatar': 'ok',
|
||||
'POST /login/account': (req: MockRequest) => {
|
||||
const data = req.body;
|
||||
if (!(data.userName === 'admin' || data.userName === 'user') || data.password !== 'ng-alain.com') {
|
||||
return { msg: `Invalid username or password(admin/ng-alain.com)` };
|
||||
}
|
||||
return {
|
||||
msg: 'ok',
|
||||
user: {
|
||||
token: '123456789',
|
||||
name: data.userName,
|
||||
email: `${data.userName}@qq.com`,
|
||||
id: 10000,
|
||||
time: +new Date(),
|
||||
},
|
||||
};
|
||||
},
|
||||
'POST /register': {
|
||||
msg: 'ok',
|
||||
},
|
||||
};
|
||||
7
maxkey-web-frontend/maxkey-web-app/_mock/index.ts
Normal file
7
maxkey-web-frontend/maxkey-web-app/_mock/index.ts
Normal 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';
|
||||
131
maxkey-web-frontend/maxkey-web-app/angular.json
Normal file
131
maxkey-web-frontend/maxkey-web-app/angular.json
Normal 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"
|
||||
}
|
||||
}
|
||||
156
maxkey-web-frontend/maxkey-web-app/azure-pipelines.yml
Normal file
156
maxkey-web-frontend/maxkey-web-app/azure-pipelines.yml
Normal 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
|
||||
32
maxkey-web-frontend/maxkey-web-app/e2e/protractor.conf.js
Normal file
32
maxkey-web-frontend/maxkey-web-app/e2e/protractor.conf.js
Normal file
@ -0,0 +1,32 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
28
maxkey-web-frontend/maxkey-web-app/e2e/src/app.e2e-spec.ts
Normal file
28
maxkey-web-frontend/maxkey-web-app/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { browser, logging } from 'protractor';
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to ng8!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser
|
||||
.manage()
|
||||
.logs()
|
||||
.get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(
|
||||
jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry),
|
||||
);
|
||||
});
|
||||
});
|
||||
11
maxkey-web-frontend/maxkey-web-app/e2e/src/app.po.ts
Normal file
11
maxkey-web-frontend/maxkey-web-app/e2e/src/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo(): Promise<any> {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
}
|
||||
|
||||
getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
||||
10
maxkey-web-frontend/maxkey-web-app/e2e/tsconfig.json
Normal file
10
maxkey-web-frontend/maxkey-web-app/e2e/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": ["jasmine", "jasminewd2", "node"]
|
||||
}
|
||||
}
|
||||
38
maxkey-web-frontend/maxkey-web-app/karma.conf.js
Normal file
38
maxkey-web-frontend/maxkey-web-app/karma.conf.js
Normal file
@ -0,0 +1,38 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
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
|
||||
});
|
||||
};
|
||||
13
maxkey-web-frontend/maxkey-web-app/ng-alain.json
Normal file
13
maxkey-web-frontend/maxkey-web-app/ng-alain.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "./node_modules/ng-alain/schema.json",
|
||||
"theme": {
|
||||
"list": [
|
||||
{
|
||||
"theme": "dark"
|
||||
},
|
||||
{
|
||||
"theme": "compact"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
38140
maxkey-web-frontend/maxkey-web-app/package-lock.json
generated
Normal file
38140
maxkey-web-frontend/maxkey-web-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
128
maxkey-web-frontend/maxkey-web-app/package.json
Normal file
128
maxkey-web-frontend/maxkey-web-app/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
17
maxkey-web-frontend/maxkey-web-app/proxy.conf.js
Normal file
17
maxkey-web-frontend/maxkey-web-app/proxy.conf.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* For more configuration, please refer to https://angular.io/guide/build#proxying-to-a-backend-server
|
||||
*
|
||||
* 更多配置描述请参考 https://angular.cn/guide/build#proxying-to-a-backend-server
|
||||
*
|
||||
* Note: The proxy is only valid for real requests, Mock does not actually generate requests, so the priority of Mock will be higher than the proxy
|
||||
*/
|
||||
module.exports = {
|
||||
/**
|
||||
* The following means that all requests are directed to the backend `https://localhost:9000/`
|
||||
*/
|
||||
// '/': {
|
||||
// target: 'https://localhost:9000/',
|
||||
// secure: false, // Ignore invalid SSL certificates
|
||||
// changeOrigin: true
|
||||
// }
|
||||
};
|
||||
1
maxkey-web-frontend/maxkey-web-app/scripts/_ci/README.md
Normal file
1
maxkey-web-frontend/maxkey-web-app/scripts/_ci/README.md
Normal file
@ -0,0 +1 @@
|
||||
# Only for CI, you can delete it
|
||||
17
maxkey-web-frontend/maxkey-web-app/scripts/_ci/delon.sh
Normal file
17
maxkey-web-frontend/maxkey-web-app/scripts/_ci/delon.sh
Normal 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}"
|
||||
@ -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"
|
||||
@ -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);
|
||||
})();
|
||||
46
maxkey-web-frontend/maxkey-web-app/src/app/app.component.ts
Normal file
46
maxkey-web-frontend/maxkey-web-app/src/app/app.component.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Component, ElementRef, OnInit, Renderer2 } from '@angular/core';
|
||||
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router } from '@angular/router';
|
||||
import { TitleService, VERSION as VERSION_ALAIN } from '@delon/theme';
|
||||
import { environment } from '@env/environment';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: ` <router-outlet></router-outlet> `
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(
|
||||
el: ElementRef,
|
||||
renderer: Renderer2,
|
||||
private router: Router,
|
||||
private titleSrv: TitleService,
|
||||
private modalSrv: NzModalService
|
||||
) {
|
||||
renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full);
|
||||
renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
let configLoad = false;
|
||||
this.router.events.subscribe(ev => {
|
||||
if (ev instanceof RouteConfigLoadStart) {
|
||||
configLoad = true;
|
||||
}
|
||||
if (configLoad && ev instanceof NavigationError) {
|
||||
this.modalSrv.confirm({
|
||||
nzTitle: `提醒`,
|
||||
nzContent: environment.production ? `应用可能已发布新版本,请点击刷新才能生效。` : `无法加载路由:${ev.url}`,
|
||||
nzCancelDisabled: false,
|
||||
nzOkText: '刷新',
|
||||
nzCancelText: '忽略',
|
||||
nzOnOk: () => location.reload()
|
||||
});
|
||||
}
|
||||
if (ev instanceof NavigationEnd) {
|
||||
this.titleSrv.setTitle();
|
||||
this.modalSrv.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
109
maxkey-web-frontend/maxkey-web-app/src/app/app.module.ts
Normal file
109
maxkey-web-frontend/maxkey-web-app/src/app/app.module.ts
Normal 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 {}
|
||||
@ -0,0 +1,5 @@
|
||||
### CoreModule
|
||||
|
||||
**应** 仅只留 `providers` 属性。
|
||||
|
||||
**作用:** 一些通用服务,例如:用户消息、HTTP数据访问。
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
4
maxkey-web-frontend/maxkey-web-app/src/app/core/index.ts
Normal file
4
maxkey-web-frontend/maxkey-web-app/src/app/core/index.ts
Normal 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';
|
||||
@ -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.`);
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
maxkey-web-frontend/maxkey-web-app/src/app/entity/Apps.ts
Normal file
59
maxkey-web-frontend/maxkey-web-app/src/app/entity/Apps.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import { BaseEntity } from './BaseEntity';
|
||||
|
||||
export class GroupMembers extends BaseEntity { }
|
||||
56
maxkey-web-frontend/maxkey-web-app/src/app/entity/Groups.ts
Normal file
56
maxkey-web-frontend/maxkey-web-app/src/app/entity/Groups.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import { BaseEntity } from './BaseEntity';
|
||||
|
||||
export class RoleMembers extends BaseEntity { }
|
||||
57
maxkey-web-frontend/maxkey-web-app/src/app/entity/Roles.ts
Normal file
57
maxkey-web-frontend/maxkey-web-app/src/app/entity/Roles.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 { }
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
164
maxkey-web-frontend/maxkey-web-app/src/app/entity/Users.ts
Normal file
164
maxkey-web-frontend/maxkey-web-app/src/app/entity/Users.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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]
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
[Document](https://ng-alain.com/theme/default)
|
||||
@ -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) { }
|
||||
}
|
||||
@ -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!');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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!);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
[Document](https://ng-alain.com/theme/blank)
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user