소스 검색

#init

develop
OCEAN 1 년 전
부모
커밋
5f4ac0362f
100개의 변경된 파일22566개의 추가작업 그리고 2개의 파일을 삭제
  1. +16
    -0
      .editorconfig
  2. +36
    -0
      .eslintignore
  3. +130
    -0
      .eslintrc.js
  4. +42
    -0
      .gitignore
  5. +5
    -0
      .husky/pre-commit
  6. +1
    -0
      .nvmrc
  7. +18
    -0
      .prettierignore
  8. +13
    -0
      .prettierrc.js
  9. +63
    -0
      .stylelintrc.js
  10. +4
    -0
      .vscode/extensions.json
  11. +20
    -0
      .vscode/launch.json
  12. +31
    -0
      .vscode/settings.json
  13. +42
    -0
      .vscode/tasks.json
  14. +7
    -0
      Dockerfile
  15. +21
    -0
      LICENSE
  16. +76
    -0
      README-zh_CN.md
  17. +36
    -0
      README.en.md
  18. +89
    -2
      README.md
  19. +1
    -0
      _mock/README.md
  20. +122
    -0
      _mock/_user.ts
  21. +1
    -0
      _mock/index.ts
  22. +168
    -0
      angular.json
  23. +0
    -0
      assets/.keep
  24. BIN
      assets/微信图片_20240122220954.png
  25. BIN
      assets/微信图片_20240122220959.png
  26. BIN
      assets/微信图片_20240122221002.png
  27. BIN
      assets/微信图片_20240122221006.png
  28. +16
    -0
      ng-alain.json
  29. +52
    -0
      nginx.conf
  30. +18141
    -0
      package-lock.json
  31. +90
    -0
      package.json
  32. +17
    -0
      proxy.conf.js
  33. +49
    -0
      src/app/app.component.ts
  34. +70
    -0
      src/app/app.config.ts
  35. +65
    -0
      src/app/conf/message.ts
  36. +5
    -0
      src/app/core/README.md
  37. +3
    -0
      src/app/core/index.ts
  38. +90
    -0
      src/app/core/net/default.interceptor.ts
  39. +66
    -0
      src/app/core/net/helper.ts
  40. +2
    -0
      src/app/core/net/index.ts
  41. +103
    -0
      src/app/core/net/refresh-token.ts
  42. +18
    -0
      src/app/core/start-page.guard.ts
  43. +133
    -0
      src/app/core/startup/startup.service.ts
  44. +80
    -0
      src/app/core/utils/app-utils.ts
  45. +97
    -0
      src/app/core/utils/app-validators.ts
  46. +164
    -0
      src/app/core/utils/base.service.ts
  47. +1
    -0
      src/app/layout/basic/README.md
  48. +146
    -0
      src/app/layout/basic/basic.component.ts
  49. +34
    -0
      src/app/layout/basic/widgets/clear-storage.component.ts
  50. +33
    -0
      src/app/layout/basic/widgets/fullscreen.component.ts
  51. +228
    -0
      src/app/layout/basic/widgets/notify.component.ts
  52. +121
    -0
      src/app/layout/basic/widgets/search.component.ts
  53. +55
    -0
      src/app/layout/basic/widgets/user.component.ts
  54. +1
    -0
      src/app/layout/blank/README.md
  55. +13
    -0
      src/app/layout/blank/blank.component.ts
  56. +3
    -0
      src/app/layout/index.ts
  57. +108
    -0
      src/app/layout/passport/passport.component.less
  58. +52
    -0
      src/app/layout/passport/passport.component.ts
  59. +10
    -0
      src/app/routes/dashboard/dashboard.component.html
  60. +12
    -0
      src/app/routes/dashboard/dashboard.component.less
  61. +16
    -0
      src/app/routes/dashboard/dashboard.component.ts
  62. +33
    -0
      src/app/routes/dashboard/header/index.less
  63. +20
    -0
      src/app/routes/dashboard/header/index.ts
  64. +17
    -0
      src/app/routes/dashboard/menu/index.less
  65. +35
    -0
      src/app/routes/dashboard/menu/index.ts
  66. +6
    -0
      src/app/routes/data-v/date/date.component.less
  67. +30
    -0
      src/app/routes/data-v/date/date.component.ts
  68. +15
    -0
      src/app/routes/data-v/home/home.component.html
  69. +18
    -0
      src/app/routes/data-v/home/home.component.less
  70. +31
    -0
      src/app/routes/data-v/home/home.component.ts
  71. +33
    -0
      src/app/routes/data-v/navigation/navigation.component.less
  72. +38
    -0
      src/app/routes/data-v/navigation/navigation.component.ts
  73. +17
    -0
      src/app/routes/data-v/routes.ts
  74. +0
    -0
      src/app/routes/data-v/s1/s1.component.html
  75. +22
    -0
      src/app/routes/data-v/s1/s1.component.ts
  76. +26
    -0
      src/app/routes/data-v/user/user.component.html
  77. +29
    -0
      src/app/routes/data-v/user/user.component.ts
  78. +27
    -0
      src/app/routes/data-v/workstation/workstation.component.html
  79. +43
    -0
      src/app/routes/data-v/workstation/workstation.component.less
  80. +58
    -0
      src/app/routes/data-v/workstation/workstation.component.ts
  81. +17
    -0
      src/app/routes/exception/exception.component.ts
  82. +11
    -0
      src/app/routes/exception/routes.ts
  83. +42
    -0
      src/app/routes/exception/trigger.component.ts
  84. +34
    -0
      src/app/routes/passport/callback.component.ts
  85. +21
    -0
      src/app/routes/passport/lock/lock.component.html
  86. +13
    -0
      src/app/routes/passport/lock/lock.component.less
  87. +44
    -0
      src/app/routes/passport/lock/lock.component.ts
  88. +45
    -0
      src/app/routes/passport/login/login.component.html
  89. +107
    -0
      src/app/routes/passport/login/login.component.less
  90. +210
    -0
      src/app/routes/passport/login/login.component.ts
  91. +13
    -0
      src/app/routes/passport/register-result/register-result.component.html
  92. +17
    -0
      src/app/routes/passport/register-result/register-result.component.ts
  93. +115
    -0
      src/app/routes/passport/register/register.component.html
  94. +51
    -0
      src/app/routes/passport/register/register.component.less
  95. +152
    -0
      src/app/routes/passport/register/register.component.ts
  96. +40
    -0
      src/app/routes/passport/routes.ts
  97. +27
    -0
      src/app/routes/routes.ts
  98. +27
    -0
      src/app/routes/sys/audit-log/audit-log-detl/audit-log-detl.component.html
  99. +38
    -0
      src/app/routes/sys/audit-log/audit-log-detl/audit-log-detl.component.ts
  100. +8
    -0
      src/app/routes/sys/audit-log/audit-log.component.html

+ 16
- 0
.editorconfig 파일 보기

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

+ 36
- 0
.eslintignore 파일 보기

@@ -0,0 +1,36 @@
_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

**/src/index.html

+ 130
- 0
.eslintrc.js 파일 보기

@@ -0,0 +1,130 @@
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', 'deprecation'],
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',
'deprecation/deprecation': 'warn',
'jsdoc/newline-after-description': 'off'
}
},
{
files: ['*.html'],
extends: ['plugin:@angular-eslint/template/recommended'],
rules: {
"@angular-eslint/template/prefer-self-closing-tags": "error"
}
},
{
files: ['*.html'],
excludedFiles: ['*inline-template-*.component.html'],
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': ['error', { parser: 'angular' }],
'@angular-eslint/template/eqeqeq': 'off'
}
}
]
};

+ 42
- 0
.gitignore 파일 보기

@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.

# Compiled output
/dist
/tmp
/out-tsc
/bazel-out

# Node
/node_modules
npm-debug.log
yarn-error.log

# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings

# System files
.DS_Store
Thumbs.db

+ 5
- 0
.husky/pre-commit 파일 보기

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

+ 1
- 0
.nvmrc 파일 보기

@@ -0,0 +1 @@
18.18.0

+ 18
- 0
.prettierignore 파일 보기

@@ -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
- 0
.prettierrc.js 파일 보기

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

+ 63
- 0
.stylelintrc.js 파일 보기

@@ -0,0 +1,63 @@
const { propertyGroups } = require('stylelint-config-clean-order');

const propertiesOrder = propertyGroups.map(properties => ({
noEmptyLineBetween: true,
emptyLineBefore: 'never',
properties
}));

module.exports = {
extends: ['stylelint-config-standard'],
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']
}
],
'import-notation': 'string',
'media-feature-range-notation': 'prefix',
'media-query-no-invalid': null,
'order/order': [
[
'dollar-variables',
'at-variables',
'custom-properties',
{ type: 'at-rule', name: 'custom-media' },
{ type: 'at-rule', name: 'function' },
{ type: 'at-rule', name: 'mixin' },
{ type: 'at-rule', name: 'extend' },
{ type: 'at-rule', name: 'include' },
'declarations',
'less-mixins',
{
type: 'rule',
selector: /^&::[\w-]+/,
hasBlock: true
},
'rules',
{ type: 'at-rule', name: 'media', hasBlock: true }
],
{ severity: 'warning' }
],
'order/properties-order': [
propertiesOrder,
{
severity: 'warning',
unspecified: 'bottomAlphabetical'
}
]
},
ignoreFiles: ['src/assets/**/*']
};

+ 4
- 0
.vscode/extensions.json 파일 보기

@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["cipchk.ng-alain-extension-pack"]
}

+ 20
- 0
.vscode/launch.json 파일 보기

@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

+ 31
- 0
.vscode/settings.json 파일 보기

@@ -0,0 +1,31 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"[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"
},
"typescript.referencesCodeLens.enabled": true
}

+ 42
- 0
.vscode/tasks.json 파일 보기

@@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

+ 7
- 0
Dockerfile 파일 보기

@@ -0,0 +1,7 @@
# nginx
FROM nginx:1.21.1-alpine as final
#RUN mkdir -p /usr/share/nginx/html/dist
COPY ./dist/himp.platform.angular/browser /usr/share/nginx/html/
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx","-g","daemon off;"]

+ 21
- 0
LICENSE 파일 보기

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018-present 卡色<cipchk@qq.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 76
- 0
README-zh_CN.md 파일 보기

@@ -0,0 +1,76 @@
# 云派

云派是基于微服务+领域驱动设计的医疗云基座解决方案。

## 关于 Himp.Platform.Angular

Himp.Platform.Angular 是使用基于Angular17 + Ng-alain 的后端管理网站。 整体方案采用微服务+领域驱动设计,当前项目为基座服务前端,项目会逐步补充各个模块功能。感兴趣的同学们帮忙点点关注!

### 系统需求

* [Node v18](https://nodejs.org/en)

### 环境部署

#### 在根目录安装程序依赖包

`npm install`

#### 安装成功后启动程序(需启动后端服务),默认启动地址 http://localhost:4200

`npm start`

### 产品功能

#### 系统管理

+ 租户管理
+ 用户管理
+ 角色管理
+ 菜单管理
+ 机构管理
+ 部门管理
+ 人员管理
+ 数据字典
+ 审计日志

### 页面截图

![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240122220954.png)

![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240122221006.png)

![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240122220959.png)

![输入图片说明](assets/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240122221002.png)

### 产品特点

#### Saas平台化

云端部署和远程支持 采用云计算技术,租户模式支持,实现系统的远程部署和管理,减少硬件成本并提供灵活性。同时,远程支持可以更快地响应和解决用户的问题。

#### 模块化设计

采用模块化设计可以使系统更具灵活性和可扩展性,便于根据医院的具体需求进行定制和集成,业务流程配置化。

#### 数据分析和决策支持

集成智能分析工具,对医院的数据进行分析和挖掘,为管理层提供决策支持和改进意见。

#### 开放API

其他医疗系统的集成:接口开放、数据开放,与电子病历系统、影像系统、LAB系统等医疗系统的集成,使信息共享更加便捷、无缝,提高工作效率。

#### 云监控支持

实施监控软件关键指标,提早响应及发现解决问题。

#### 自动化流程

引入自动化流程可以提高工作效率并降低人为错误的发生。例如,自动化测试、自动化更新及自动化部署等

#### 数据整合与应用

注重数据整合和应用,将更多来源的数据进行整合,包括患者数据、医学影像、实验室检测结果、药品信息等,利用人工智能、大数据等技术进行数据分析和挖掘,帮助医生和医护人员做出更好的医疗决策。


+ 36
- 0
README.en.md 파일 보기

@@ -0,0 +1,36 @@
# 云派前端基座

#### Description
基于angular+ng-alain的医疗云微前端解决方案

#### Software Architecture
Software architecture description

#### Installation

1. xxxx
2. xxxx
3. xxxx

#### Instructions

1. xxxx
2. xxxx
3. xxxx

#### Contribution

1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request


#### Gitee Feature

1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 89
- 2
README.md 파일 보기

@@ -1,3 +1,90 @@
# auseft.platform.datav
<p align="center">
<a href="https://ng-alain.com">
<img width="100" src="https://ng-alain.com/assets/img/logo-color.svg">
</a>
</p>

数据可视化大屏
<h1 align="center">NG-ALAIN</h1>

<div align="center">
Out-of-box UI solution for enterprise applications, Let developers focus on business.

[![CI](https://github.com/ng-alain/ng-alain/actions/workflows/ci.yml/badge.svg)](https://github.com/ng-alain/ng-alain/actions/workflows/ci.yml)
[![Dependency Status](https://david-dm.org/ng-alain/ng-alain/status.svg?style=flat-square)](https://david-dm.org/ng-alain/ng-alain)
[![GitHub Release Date](https://img.shields.io/github/release-date/ng-alain/ng-alain.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/releases)
[![NPM version](https://img.shields.io/npm/v/ng-alain.svg?style=flat-square)](https://www.npmjs.com/package/ng-alain)
[![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://prettier.io/)
[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/blob/master/LICENSE)
[![Gitter](https://img.shields.io/gitter/room/ng-alain/ng-alain.svg?style=flat-square)](https://gitter.im/ng-alain/ng-alain)
[![ng-zorro-vscode](https://img.shields.io/badge/ng--zorro-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-zorro-vscode)
[![ng-alain-vscode](https://img.shields.io/badge/ng--alain-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-alain-vscode)

</div>

English | [简体中文](README-zh_CN.md)

## Quickstart

- [Getting Started](https://ng-alain.com/docs/getting-started)

## Links

+ [Document](https://ng-alain.com) ([Surge Mirror](https://ng-alain-doc.surge.sh))
+ [@delon Source](https://github.com/ng-alain/delon)
+ [DEMO](https://ng-alain.surge.sh) ([国内镜像](https://ng-alain.gitee.io/))

## Features

+ `ng-zorro-antd` based
+ Responsive Layout
+ I18n
+ [@delon](https://github.com/ng-alain/delon)
+ Lazy load Assets
+ UI Router States
+ Customize Theme
+ Less preprocessor
+ RTL
+ Well organized & commented code
+ Simple upgrade
+ Support Docker deploy

## Architecture

![Architecture](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/architecture.png)

> [delon](https://github.com/ng-alain/delon) is a production-ready solution for admin business components packages, Built on the design principles developed by Ant Design.

## App Shots

![desktop](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/desktop.png)
![ipad](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/ipad.png)
![iphone](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/iphone.png)

## Contributing

[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/pulls)

We welcome all contributions. Please read our [CONTRIBUTING.md](https://github.com/ng-alain/ng-alain/blob/master/CONTRIBUTING.md) first. You can submit any ideas as [pull requests](https://github.com/ng-alain/ng-alain/pulls) or as [GitHub issues](https://github.com/ng-alain/ng-alain/issues).

> If you're new to posting issues, we ask that you read [*How To Ask Questions The Smart Way*](http://www.catb.org/~esr/faqs/smart-questions.html) (**This guide does not provide actual support services for this project!**), [How to Ask a Question in Open Source Community](https://github.com/seajs/seajs/issues/545) and [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) prior to posting. Well written bug reports help us help you!

## Donation

ng-alain is an MIT-licensed open source project. In order to achieve better and sustainable development of the project, we expect to gain more backers. You can support us in any of the following ways:

- [patreon](https://www.patreon.com/cipchk)
- [opencollective](https://opencollective.com/ng-alain)
- [paypal](https://www.paypal.me/cipchk)
- [支付宝或微信](https://ng-alain.com/assets/donate.png)

Or purchasing our [business theme](https://e.ng-alain.com/).

## Backers

Thank you to all our backers! 🙏

<a href="https://opencollective.com/ng-alain#backers" target="_blank"><img src="https://opencollective.com/ng-alain/backers.svg?width=890"></a>

### License

The MIT License (see the [LICENSE](https://github.com/ng-alain/ng-alain/blob/master/LICENSE) file for the full text)

+ 1
- 0
_mock/README.md 파일 보기

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

+ 122
- 0
_mock/_user.ts 파일 보기

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

+ 1
- 0
_mock/index.ts 파일 보기

@@ -0,0 +1 @@
export * from './_user';

+ 168
- 0
angular.json 파일 보기

@@ -0,0 +1,168 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"himp.platform.angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"skipTests": false,
"flat": false,
"inlineStyle": true,
"inlineTemplate": false,
"style": "less"
},
"ng-alain:module": {
"routing": true
},
"ng-alain:list": {
"skipTests": false
},
"ng-alain:edit": {
"skipTests": false,
"modal": true
},
"ng-alain:view": {
"skipTests": false,
"modal": true
},
"ng-alain:curd": {
"skipTests": false
},
"@schematics/angular:module": {
"routing": true
},
"@schematics/angular:directive": {
"skipTests": false
},
"@schematics/angular:service": {
"skipTests": false
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/himp.platform.angular",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.less"
],
"scripts": [],
"allowedCommonJsDependencies": [
"ajv",
"ajv-formats",
"extend",
"file-saver",
"mockjs"
],
"stylePreprocessorOptions": {
"includePaths": [
"node_modules/"
]
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "6mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "himp.platform.angular:build:production"
},
"development": {
"buildTarget": "himp.platform.angular:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"proxyConfig": "proxy.conf.js"
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "himp.platform.angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.less"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"analytics": false,
"schematicCollections": [
"@schematics/angular",
"ng-alain"
]
}
}

+ 0
- 0
assets/.keep 파일 보기


BIN
assets/微信图片_20240122220954.png 파일 보기

Before After
Width: 1714  |  Height: 1235  |  Size: 64KB

BIN
assets/微信图片_20240122220959.png 파일 보기

Before After
Width: 1714  |  Height: 1235  |  Size: 68KB

BIN
assets/微信图片_20240122221002.png 파일 보기

Before After
Width: 1714  |  Height: 1235  |  Size: 62KB

BIN
assets/微信图片_20240122221006.png 파일 보기

Before After
Width: 1714  |  Height: 1235  |  Size: 80KB

+ 16
- 0
ng-alain.json 파일 보기

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

+ 52
- 0
nginx.conf 파일 보기

@@ -0,0 +1,52 @@
upstream api{
server 112.33.111.160:8081;
}

server {
listen 80;
listen [::]:80;
server_name localhost;

#access_log /var/log/nginx/host.access.log main;
location /api/ {
proxy_pass http://api;
}

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


+ 18141
- 0
package-lock.json
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 90
- 0
package.json 파일 보기

@@ -0,0 +1,90 @@
{
"name": "himp.platform.angular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng s -o",
"build": "npm run ng-high-memory build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"ng-high-memory": "node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng",
"hmr": "ng s -o --hmr",
"analyze": "npm run ng-high-memory build -- --source-map",
"analyze:view": "source-map-explorer dist/**/*.js",
"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",
"lint": "npm run lint:ts && npm run lint:style",
"lint:ts": "ng lint --fix",
"lint:style": "npx stylelint \"src/**/*.less\" --fix"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.1.0",
"@angular/common": "^17.1.0",
"@angular/compiler": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/forms": "^17.1.0",
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/router": "^17.1.0",
"@delon/abc": "^17.1.0",
"@delon/acl": "^17.1.0",
"@delon/auth": "^17.1.0",
"@delon/cache": "^17.1.0",
"@delon/chart": "^17.1.0",
"@delon/form": "^17.1.0",
"@delon/mock": "^17.1.0",
"@delon/theme": "^17.1.0",
"@delon/util": "^17.1.0",
"ag-grid-angular": "^31.0.2",
"moment": "^2.30.1",
"ng-alain": "^17.1.0",
"ng-zorro-antd": "^17.1.0",
"rxjs": "~7.8.0",
"screenfull": "^6.0.2",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.0",
"@angular-eslint/builder": "^17.2.0",
"@angular-eslint/eslint-plugin": "^17.2.0",
"@angular-eslint/eslint-plugin-template": "^17.2.0",
"@angular-eslint/schematics": "^17.2.0",
"@angular-eslint/template-parser": "^17.2.0",
"@angular/cli": "^17.1.0",
"@angular/compiler-cli": "^17.1.0",
"@angular/language-service": "^17.1.0",
"@delon/testing": "^17.1.0",
"@ng-util/monaco-editor": "^17.0.1",
"@types/jasmine": "~5.1.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "~9.1.0",
"eslint-plugin-deprecation": "~2.0.0",
"eslint-plugin-import": "~2.29.1",
"eslint-plugin-jsdoc": "~48.0.2",
"eslint-plugin-prefer-arrow": "~1.2.3",
"eslint-plugin-prettier": "~5.1.3",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"lint-staged": "^15.2.0",
"ng-alain": "^17.1.0",
"ng-alain-plugin-theme": "^16.0.2",
"ngx-tinymce": "^17.0.0",
"prettier": "^3.2.4",
"source-map-explorer": "^2.5.3",
"stylelint": "^16.1.0",
"stylelint-config-clean-order": "^5.4.0",
"stylelint-config-standard": "^36.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
"typescript": "~5.3.2"
}
}

+ 17
- 0
proxy.conf.js 파일 보기

@@ -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/`
*/
// '/api': {
// target: 'https://localhost:9000/',
// secure: false, // Ignore invalid SSL certificates
// changeOrigin: true
// }
};

+ 49
- 0
src/app/app.component.ts 파일 보기

@@ -0,0 +1,49 @@
import { Component, ElementRef, OnInit, Renderer2, inject } from '@angular/core';
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router';
import { TitleService, VERSION as VERSION_ALAIN, stepPreloader } 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 />`,
standalone: true,
imports: [RouterOutlet]
})
export class AppComponent implements OnInit {
private readonly router = inject(Router);
private readonly titleSrv = inject(TitleService);
private readonly modalSrv = inject(NzModalService);

private donePreloader = stepPreloader();

constructor(el: ElementRef, renderer: Renderer2) {
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.donePreloader();
this.titleSrv.setTitle();
this.modalSrv.closeAll();
}
});
}
}

+ 70
- 0
src/app/app.config.ts 파일 보기

@@ -0,0 +1,70 @@
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { default as ngLang } from '@angular/common/locales/zh';
import { ApplicationConfig, EnvironmentProviders, Provider } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, withComponentInputBinding, withViewTransitions, withInMemoryScrolling, withHashLocation, RouterFeatures } from '@angular/router';
import { defaultInterceptor, provideStartup } from '@core';
import { provideCellWidgets } from '@delon/abc/cell';
import { provideSTWidgets } from '@delon/abc/st';
import { authSimpleInterceptor, provideAuth } from '@delon/auth';
import { provideSFConfig } from '@delon/form';
import { AlainProvideLang, provideAlain, zh_CN as delonLang } from '@delon/theme';
import { AlainConfig } from '@delon/util/config';
import { environment } from '@env/environment';
import { CELL_WIDGETS, ST_WIDGETS, SF_WIDGETS } from '@shared';
import { zhCN as dateLang } from 'date-fns/locale';
import { NzConfig, provideNzConfig } from 'ng-zorro-antd/core/config';
import { zh_CN as zorroLang } from 'ng-zorro-antd/i18n';



import { provideBindAuthRefresh } from './core/net';
import { routes } from './routes/routes';
import { ICONS } from '../style-icons';
import { ICONS_AUTO } from '../style-icons-auto';

const defaultLang: AlainProvideLang = {
abbr: 'zh-CN',
ng: ngLang,
zorro: zorroLang,
date: dateLang,
delon: delonLang
};

const alainConfig: AlainConfig = {
auth: { login_url: '/passport/login' }
};

const ngZorroConfig: NzConfig = {};

const routerFeatures: RouterFeatures[] = [
withComponentInputBinding(),
withViewTransitions(),
withInMemoryScrolling({ scrollPositionRestoration: 'top' })
];
if (environment.useHash) routerFeatures.push(withHashLocation());

const providers: Array<Provider | EnvironmentProviders> = [
provideHttpClient(withInterceptors([...(environment.interceptorFns ?? []), authSimpleInterceptor, defaultInterceptor])),
provideAnimations(),
provideRouter(routes, ...routerFeatures),
provideAlain({ config: alainConfig, defaultLang, icons: [...ICONS_AUTO, ...ICONS] }),
provideNzConfig(ngZorroConfig),
provideAuth(),
provideCellWidgets(...CELL_WIDGETS),
provideSTWidgets(...ST_WIDGETS),
provideSFConfig({
widgets: [...SF_WIDGETS]
}),
provideStartup(),
...(environment.providers || [])
];

// If you use `@delon/auth` to refresh the token, additional registration `provideBindAuthRefresh` is required
if (environment.api?.refreshTokenEnabled && environment.api.refreshTokenType === 'auth-refresh') {
providers.push(provideBindAuthRefresh());
}

export const appConfig: ApplicationConfig = {
providers: providers
};

+ 65
- 0
src/app/conf/message.ts 파일 보기

@@ -0,0 +1,65 @@
// tslint:disable-next-line:ban-types
const Messages: any = {
C0001: '确定要删除这条数据吗?',
C0002: '确定要把这条数据无效吗?',
C0003: '保存成功',
C0004: '确定要作废该药品及药品目录信息吗?',
C0005: '确定要启用该药品信息吗?',
C0006: '删除成功',
C0007: '确定要进行审核操作吗?',
E0001: '请输入{0}',
E0002: '{0}的最大长度是{1}',
E0003: '{0}的格式不正确',
E0004: '请输入数字',
E0005: '请输入{0}',
E0006: '{0}的最小长度是{1}',
E0007: '',
I0001: '',
W0001: ''
};

class MessageUtil {
public devMode = false;

public getMessage(messageId: string, params: string[]): string {
const message = this.replaceMessage(messageId, params);
if (this.devMode) {
return `${message}(${messageId})`;
} else {
return message;
}
}

public replaceMessage(messageId: string, params: string[]): string {
let message: string = Messages[messageId] || messageId;
if (params) {
params.forEach((param: string, index: number) => {
const reg = new RegExp(`\\{${index}\\}`, 'g');
message = message.replace(reg, param);
});
}
return message;
}

public convertToXML(): any {
if (document.implementation && document.implementation.createDocument) {
const xmlDom = document.implementation.createDocument('', '', null);
const messageListEl = xmlDom.createElement('MessageList');
// tslint:disable-next-line: forin
for (const key in Messages) {
const messageEl = xmlDom.createElement('Message');
const idEl = xmlDom.createElement('Id');
idEl.textContent = key;
const textEl = xmlDom.createElement('Text');
textEl.textContent = Messages[key];
messageEl.appendChild(idEl);
messageEl.appendChild(textEl);
messageListEl.appendChild(messageEl);
}
return messageListEl;
}
return null;
}
}

export const MESSAGE_UTIL: MessageUtil = new MessageUtil();

+ 5
- 0
src/app/core/README.md 파일 보기

@@ -0,0 +1,5 @@
### CoreModule

**应** 仅只留 `providers` 属性。

**作用:** 一些通用服务,例如:用户消息、HTTP数据访问。

+ 3
- 0
src/app/core/index.ts 파일 보기

@@ -0,0 +1,3 @@
export * from './net/default.interceptor';
export * from './startup/startup.service';
export * from './start-page.guard';

+ 90
- 0
src/app/core/net/default.interceptor.ts 파일 보기

@@ -0,0 +1,90 @@
import { HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest, HttpResponseBase } from '@angular/common/http';
import { Injector, inject } from '@angular/core';
import { IGNORE_BASE_URL, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { Observable, of, throwError, mergeMap, catchError } from 'rxjs';

import { ReThrowHttpError, checkStatus, getAdditionalHeaders, toLogin } from './helper';
import { tryRefreshToken } from './refresh-token';
import { NzNotificationService } from 'ng-zorro-antd/notification';

function handleData(injector: Injector, ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandlerFn): Observable<any> {
checkStatus(injector, 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) {
// const customError = req.context.get(CUSTOM_ERROR);
// if (customError) injector.get(NzMessageService).error(body.msg);
// return customError ? throwError(() => ({ body, _throw: true }) as ReThrowHttpError) : of({});
// } else {
// // 返回原始返回体
// if (req.context.get(RAW_BODY) || ev.body instanceof Blob) {
// return of(ev);
// }
// // 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
// return of(new HttpResponse({ ...ev, body: body.response } as any));
// // 或者依然保持完整的格式
// return of(ev);
// }
// }
break;
case 401:
if (environment.api.refreshTokenEnabled && environment.api.refreshTokenType === 're-request') {
return tryRefreshToken(injector, ev, req, next);
}
toLogin(injector);
break;
case 403:
case 404:
case 500:
// goTo(injector, `/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 if ((ev as unknown as ReThrowHttpError)._throw === true) {
return throwError(() => (ev as unknown as ReThrowHttpError).body);
} else {
return of(ev);
}
}

export const defaultInterceptor: HttpInterceptorFn = (req, next) => {
// 统一加上服务端前缀
let url = req.url;
if (!req.context.get(IGNORE_BASE_URL) && !url.startsWith('https://') && !url.startsWith('http://')) {
const { baseUrl } = environment.api;
url = baseUrl + (baseUrl.endsWith('/') && url.startsWith('/') ? url.substring(1) : url);
}

const newReq = req.clone({ url, setHeaders: getAdditionalHeaders(req.headers) });
const injector = inject(Injector);
const notification = inject(NzNotificationService);

return next(newReq).pipe(
mergeMap(ev => {
// 允许统一对请求错误处理
if (ev instanceof HttpResponseBase) {
return handleData(injector, ev, newReq, next);
}
// 若一切都正常,则后续操作
return of(ev);
}),
catchError((err: HttpErrorResponse) => handleData(injector, err, newReq, next))
);
};

+ 66
- 0
src/app/core/net/helper.ts 파일 보기

@@ -0,0 +1,66 @@
import { HttpErrorResponse, HttpHeaders, HttpResponseBase } from '@angular/common/http';
import { Injector, inject } from '@angular/core';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzNotificationService } from 'ng-zorro-antd/notification';

export interface ReThrowHttpError {
body: any;
_throw: true;
}

export const CODEMESSAGE: { [key: number]: string } = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
};

export function goTo(injector: Injector, url: string): void {
setTimeout(() => injector.get(Router).navigateByUrl(url));
}

export function toLogin(injector: Injector): void {
injector.get(NzNotificationService).error(`未登录或登录已过期,请重新登录。`, ``);
goTo(injector, injector.get(DA_SERVICE_TOKEN).login_url!);
}

export function getAdditionalHeaders(headers?: HttpHeaders): { [name: string]: string } {
const res: { [name: string]: string } = {};
const lang = inject(ALAIN_I18N_TOKEN).currentLang;
if (!headers?.has('Accept-Language') && lang) {
res['Accept-Language'] = lang;
}

if (headers?.has('Token')) {
res['Authorization'] = `Bearer ${headers.get('Token')}`;
}

return res;
}

export function checkStatus(injector: Injector, ev: HttpResponseBase): void {
if ((ev.status >= 200 && ev.status < 300) || ev.status === 401) {
return;
}

const errortext = CODEMESSAGE[ev.status] || ev.statusText;

if (ev instanceof HttpErrorResponse && ev.status === 403) {
injector.get(NzNotificationService).error(`${ev.error.error.code} - ${ev.error.error.message}`, errortext);
} else {
injector.get(NzNotificationService).error(`请求错误 ${ev.status}: ${ev.url}`, errortext);
}
}

+ 2
- 0
src/app/core/net/index.ts 파일 보기

@@ -0,0 +1,2 @@
export { provideBindAuthRefresh } from './refresh-token';
export * from './default.interceptor';

+ 103
- 0
src/app/core/net/refresh-token.ts 파일 보기

@@ -0,0 +1,103 @@
import { HttpClient, HttpHandlerFn, HttpRequest, HttpResponseBase } from '@angular/common/http';
import { APP_INITIALIZER, Injector, Provider } from '@angular/core';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { BehaviorSubject, Observable, catchError, filter, switchMap, take, throwError } from 'rxjs';

import { toLogin } from './helper';

let refreshToking = false;
let refreshToken$: BehaviorSubject<any> = new BehaviorSubject<any>(null);

/**
* 重新附加新 Token 信息
*
* > 由于已经发起的请求,不会再走一遍 `@delon/auth` 因此需要结合业务情况重新附加新的 Token
*/
function reAttachToken(injector: Injector, req: HttpRequest<any>): HttpRequest<any> {
const token = injector.get(DA_SERVICE_TOKEN).get()?.token;
return req.clone({
setHeaders: {
token: `Bearer ${token}`
}
});
}

function refreshTokenRequest(injector: Injector): Observable<any> {
const model = injector.get(DA_SERVICE_TOKEN).get();
return injector.get(HttpClient).post(`/api/auth/refresh`, { headers: { refresh_token: model?.['refresh_token'] || '' } });
}

/**
* 刷新Token方式一:使用 401 重新刷新 Token
*/
export function tryRefreshToken(injector: Injector, ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandlerFn): Observable<any> {
// 1、若请求为刷新Token请求,表示来自刷新Token可以直接跳转登录页
if ([`/api/auth/refresh`].some(url => req.url.includes(url))) {
toLogin(injector);
return throwError(() => ev);
}
// 2、如果 `refreshToking` 为 `true` 表示已经在请求刷新 Token 中,后续所有请求转入等待状态,直至结果返回后再重新发起请求
if (refreshToking) {
return refreshToken$.pipe(
filter(v => !!v),
take(1),
switchMap(() => next(reAttachToken(injector, req)))
);
}
// 3、尝试调用刷新 Token
refreshToking = true;
refreshToken$.next(null);

return refreshTokenRequest(injector).pipe(
switchMap(res => {
// 通知后续请求继续执行
refreshToking = false;
refreshToken$.next(res);
// 重新保存新 token
injector.get(DA_SERVICE_TOKEN).set(res);
// 重新发起请求
return next(reAttachToken(injector, req));
}),
catchError(err => {
refreshToking = false;
toLogin(injector);
return throwError(() => err);
})
);
}

function buildAuthRefresh(injector: Injector) {
const tokenSrv = injector.get(DA_SERVICE_TOKEN);
tokenSrv.refresh
.pipe(
filter(() => !refreshToking),
switchMap(res => {
console.log(res);
refreshToking = true;
return refreshTokenRequest(injector);
})
)
.subscribe({
next: res => {
// TODO: Mock expired value
res.expired = +new Date() + 1000 * 60 * 5;
refreshToking = false;
tokenSrv.set(res);
},
error: () => toLogin(injector)
});
}

/**
* 刷新Token方式二:使用 `@delon/auth` 的 `refresh` 接口,需要在 `app.config.ts` 中注册 `provideBindAuthRefresh`
*/
export function provideBindAuthRefresh(): Provider[] {
return [
{
provide: APP_INITIALIZER,
useFactory: (injector: Injector) => () => buildAuthRefresh(injector),
deps: [Injector],
multi: true
}
];
}

+ 18
- 0
src/app/core/start-page.guard.ts 파일 보기

@@ -0,0 +1,18 @@
import { CanActivateFn } from '@angular/router';
import { Observable } from 'rxjs';

/**
* Dynamically load the start page
*
* 动态加载启动页
*/
export const startPageGuard: CanActivateFn = (_, __): boolean | Observable<boolean> => {
// Re-jump according to the first item of the menu, you can re-customize the logic
// 以下代码是根据菜单的第一项进行重新跳转,你可以重新定制逻辑
// const menuSrv = inject(MenuService);
// if (menuSrv.find({ url: state.url }) == null) {
// inject(Router).navigateByUrl(menuSrv.menus[0].link!);
// return false;
// }
return true;
};

+ 133
- 0
src/app/core/startup/startup.service.ts 파일 보기

@@ -0,0 +1,133 @@
import { APP_INITIALIZER, Injectable, Provider, inject } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { ALAIN_I18N_TOKEN, MenuService, SettingsService, TitleService } from '@delon/theme';
import { ACLService } from '@delon/acl';
import { Observable, zip, of, catchError, map } from 'rxjs';
import type { NzSafeAny } from 'ng-zorro-antd/core/types';

/**
* Used for application startup
* Generally used to get the basic data of the application, like: Menu Data, User Data, etc.
*/
export function provideStartup(): Provider[] {
return [
StartupService,
{
provide: APP_INITIALIZER,
useFactory: (startupService: StartupService) => () => startupService.load(),
deps: [StartupService],
multi: true
}
];
}

@Injectable()
export class StartupService {
private menuService = inject(MenuService);
private settingService = inject(SettingsService);
private tokenService = inject(DA_SERVICE_TOKEN);
private aclService = inject(ACLService);
private titleService = inject(TitleService);
private httpClient = inject(HttpClient);
private router = inject(Router);
// If http request allows anonymous access, you need to add `ALLOW_ANONYMOUS`:
// this.httpClient.get('assets/tmp/app-data.json', { context: new HttpContext().set(ALLOW_ANONYMOUS, true) })
private appData$ = this.httpClient.get('assets/tmp/app-data.json').pipe(
catchError((res: NzSafeAny) => {
console.warn(`StartupService.load: Network request failed`, res);
setTimeout(() => this.router.navigateByUrl(`/exception/500`));
return of({});
})
);

private initData$ = zip(this.httpClient.get('/api/main/role-permission'), this.httpClient.get('/api/main/menu')).pipe(
catchError((res: NzSafeAny) => {
console.warn(`StartupService.load: Network request failed`, res);
setTimeout(() => this.router.navigateByUrl(`/passport/login`));
return of([]);
})
);

private handleAppData(res: NzSafeAny): void {
// Application information: including site name, description, year
const app: any = {
name: `NG-ALAIN`,
description: `NG-ZORRO admin panel front-end framework`
};

this.settingService.setApp(app);

if (res.length == 2) {
this.aclService.setAbility(res[0].items);
this.menuService.add(res[1]);
}

// Can be set page suffix title, https://ng-alain.com/theme/title
//this.titleService.suffix = res.app?.name;
}

// private viaHttp(): Observable<void> {
// return this.appData$.pipe(map((res: NzSafeAny) => this.handleAppData(res)));
// }

private viaHttp(): Observable<void> {
return this.initData$.pipe(
map((res: NzSafeAny) => {
this.handleAppData(res);
})
);
}

private viaMock(): Observable<void> {
// const tokenData = this.tokenService.get();
// if (!tokenData.token) {
// this.router.navigateByUrl(this.tokenService.login_url!);
// return;
// }
// mock
const app: any = {
name: `NG-ALAIN`,
description: `NG-ZORRO admin panel front-end framework`
};
const user: any = {
name: 'Admin',
avatar: './assets/tmp/img/avatar.jpg',
email: 'cipchk@qq.com',
token: '123456789'
};
// Application information: including site name, description, year
this.settingService.setApp(app);
// User information: including name, avatar, email address
this.settingService.setUser(user);
// ACL: Set the permissions to full, https://ng-alain.com/acl/getting-started
this.aclService.setFull(true);
// Menu data, https://ng-alain.com/theme/menu
this.menuService.add([
{
text: 'Main',
group: true,
children: [
{
text: 'Dashboard',
link: '/dashboard',
icon: { type: 'icon', value: 'appstore' }
}
]
}
]);
// Can be set page suffix title, https://ng-alain.com/theme/title
this.titleService.suffix = app.name;

return of(void 0);
}

load(): Observable<void> {
// http
return this.viaHttp();
// mock: Don’t use it in a production environment. ViaMock is just to simulate some data to make the scaffolding work normally
// mock:请勿在生产环境中这么使用,viaMock 单纯只是为了模拟一些数据使脚手架一开始能正常运行
// return this.viaMock();
}
}

+ 80
- 0
src/app/core/utils/app-utils.ts 파일 보기

@@ -0,0 +1,80 @@
import moment from 'moment';

export class AppUtils {
public static dateFormat(date: Date, format: string): any {
if (!date) {
return null;
}
return moment(date).format(format);
}

public static convertDate(str: string, format?: string, substring?: boolean) {
if (str) {
const data = moment(substring && format ? str.substring(0, format.length) : str, format);
if (data.isValid()) {
return data.toDate();
}
}

return null;
}

public static calAge(birth: string | Date, format?: string) {
let birthday;
if (typeof birth == 'string') {
birthday = this.convertDate(birth, format);
} else {
birthday = birth;
}

if (birthday) {
const now = new Date();
let age = now.getFullYear() - birthday.getFullYear();
if (now.getMonth() < birthday.getMonth() || (now.getMonth() == birthday.getMonth() && now.getDay() < birthday.getDay())) {
age--;
}

return age < 0 ? 0 : age;
}
return null;
}

public static isNull(val: any) {
if (val === null || val === undefined || val === '') {
return true;
}
return false;
}

public static convertToCheck(val: any) {
if (val) {
return '√';
}
return '';
}

public static convertToBool(val: any) {
if (val) {
return true;
}
return false;
}

public static boolConvertToNumber(val: any) {
if (val) {
return 1;
}
return 0;
}

public static convertEnumToList(obj: any) {
const list: any = [];
if (obj) {
for (const key in obj) {
const val = obj[key];
list.push({ value: Number.parseInt(key), label: val });
}
}
return list;
}
}

+ 97
- 0
src/app/core/utils/app-validators.ts 파일 보기

@@ -0,0 +1,97 @@
import { AbstractControl, Validators, ValidatorFn } from '@angular/forms';
import { _Validators } from '@delon/util';
import { MESSAGE_UTIL } from 'src/app/conf/message';
import { AppUtils } from './app-utils';

export type AppValidatorsOptions = { errorTip: string } & Record<string, any>;
export type AppValidatorsErrors = Record<string, AppValidatorsOptions>;

export class AppValidators {
static idCard(id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
if (AppUtils.isNull(control.value)) {
return {};
}
if (_Validators.idCard(control) === null) {
return null;
}
return { idcard: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
};
}

static maxLength(maxLength: number, id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [maxLength.toString()] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
if (Validators.maxLength(maxLength)(control) === null) {
return null;
}
return { maxlength: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
};
}

static minLength(minLength: number, id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [minLength.toString()] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
if (Validators.minLength(minLength)(control) === null) {
return null;
}
return { minLength: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
};
}

static required(id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
if (Validators.required(control) === null) {
return null;
}
return { required: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
};
}

static email(id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
if (Validators.required(control) !== null) {
return { required: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}

if (Validators.email(control) !== null) {
return { email: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}

return null;
};
}

static mobile(id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
if (!control.value) {
return null;
}
if (_Validators.mobile(control) === null) {
return null;
}
return { mobile: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
};
}

static pattern(patternStr: RegExp, id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
patternStr.lastIndex = 0;
if (Validators.pattern(patternStr)(control) === null) {
return null;
}
return { pattern: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
};
}

static other(fn: (control: AbstractControl) => AppValidatorsErrors | null): ValidatorFn {
return (control: AbstractControl): AppValidatorsErrors | null => {
return fn(control);
};
}
}

+ 164
- 0
src/app/core/utils/base.service.ts 파일 보기

@@ -0,0 +1,164 @@
import { EventEmitter, Injectable, TemplateRef, Type } from '@angular/core';
import { FormArray, FormGroup } from '@angular/forms';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { ModalButtonOptions, NzModalService } from 'ng-zorro-antd/modal';
import { MESSAGE_UTIL } from '../../conf/message';
import { FormProperty, retrieveSchema } from '@delon/form';
import { NzMessageService } from 'ng-zorro-antd/message';
import { _HttpClient } from '@delon/theme';

declare var FakeServer: any;

@Injectable({
providedIn: 'root'
})
export class BaseService {
public drawerWidth = 550;

deptmentServiceUrl = ''; //

constructor(
private http: _HttpClient,
private modalService: NzModalService,
private message: NzMessageService
) {}

hasError(...args: (FormGroup | FormProperty | null)[]): boolean {
const result = this.hasErrorBase(args);

return result;
}

hasErrorBase(args: (FormGroup | FormProperty | null)[]): boolean {
let result = false;
if (!args) {
return result;
}
args.forEach((val: any) => {
if (val instanceof FormGroup) {
for (const key in val.controls) {
const ctl = val.controls[key];
if (ctl instanceof FormGroup) {
result = this.hasErrorBase([ctl]) || result;
} else if (ctl instanceof FormArray) {
(ctl as FormArray).controls.forEach(c => {
c.markAsDirty();
c.updateValueAndValidity();
result = c.invalid || result;
});
} else {
ctl.markAsDirty();
ctl.updateValueAndValidity();
result = ctl.invalid || result;
}
}
} else if (val instanceof FormProperty) {
const properties = val.root.properties as FormProperty[];
if (properties) {
for (const key in properties) {
const ctl = properties[key];
if (ctl instanceof FormProperty) {
ctl.updateValueAndValidity();
result = ctl.valid || result;
}
}
}
}
});
return result;
}

public getMessage(messageId: string, ...params: string[]): string {
return this.getMessageBase(messageId, params);
}

private getMessageBase(messageId: string, params: string[]): string {
return MESSAGE_UTIL.getMessage(messageId, params);
}

/**
* ocean 2023-4-17
* message提示框
*/
public showMessage(state: { messageId: string }) {
this.message.info(this.getMessage(state.messageId));
}

public showConfirm(state: { title?: string; message?: string; okCallback?: () => void; cancelCallback?: () => void }) {
this.modalService.create({
nzTitle: state.title,
nzContent: state.message,
nzClosable: false,
nzBodyStyle: { 'font-size': '1.5rem' },
nzOnOk: () => {
if (state.okCallback) {
state.okCallback();
}
},
nzOnCancel: () => {
if (state.cancelCallback) {
state.cancelCallback();
}
}
});
}

public showModal(config: AppModalConfig) {
let width = 600;
switch (config.widthClass) {
case 'xxl':
width = 1200;
break;
case 'xl':
width = 1000;
break;
case 'lg':
width = 800;
break;
case 'sm':
width = 600;
break;
case 'xs':
width = 400;
break;
}
const modal = this.modalService.create({
nzTitle: config.title,
nzContent: config.component,
nzData: config.componentParams,
nzClosable: true,
nzWidth: width,
nzOnOk: config.okCallBack,
nzFooter: config.footer,
nzStyle: config.style
});
return modal;
}

public async post(url: string, method: 'put' | 'post' | 'get' | 'delete', params?: any) {
switch (method) {
case 'put':
return this.http.put(url, params).subscribe(res => {});
break;
case 'post':
return this.http.post(url, params).subscribe(res => {});
break;
case 'get':
return this.http.get(url, params).subscribe(res => {});
break;
case 'delete':
return this.http.delete(url, params).subscribe(res => {});
break;
}
}
}

export class AppModalConfig {
title?: string | TemplateRef<{}>;
component?: string | TemplateRef<NzSafeAny> | Type<any>;
componentParams?: any;
okCallBack?: () => void;
widthClass?: 'xxl' | 'xl' | 'lg' | 'sm' | 'xs';
footer?: string | TemplateRef<{}> | Array<ModalButtonOptions<any>> | null;
style?: Object;
}

+ 1
- 0
src/app/layout/basic/README.md 파일 보기

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

+ 146
- 0
src/app/layout/basic/basic.component.ts 파일 보기

@@ -0,0 +1,146 @@
import { Component, inject } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { I18nPipe, SettingsService, User } from '@delon/theme';
import { LayoutDefaultModule, LayoutDefaultOptions } from '@delon/theme/layout-default';
import { SettingDrawerModule } from '@delon/theme/setting-drawer';
import { ThemeBtnComponent } from '@delon/theme/theme-btn';
import { environment } from '@env/environment';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzMenuModule } from 'ng-zorro-antd/menu';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { SharedModule } from '@shared';

import { HeaderClearStorageComponent } from './widgets/clear-storage.component';
import { HeaderFullScreenComponent } from './widgets/fullscreen.component';
import { HeaderSearchComponent } from './widgets/search.component';
import { HeaderUserComponent } from './widgets/user.component';

@Component({
selector: 'layout-basic',
template: `
<layout-default [options]="options" [asideUser]="asideUserTpl" [content]="contentTpl" [customError]="null">
<!-- <layout-default-header-item direction="left">
<a layout-default-header-item-trigger href="//github.com/ng-alain/ng-alain" target="_blank">
<i nz-icon nzType="github"></i>
</a>
</layout-default-header-item>
<layout-default-header-item direction="left" hidden="mobile">
<a layout-default-header-item-trigger routerLink="/passport/lock">
<i nz-icon nzType="lock"></i>
</a>
</layout-default-header-item> -->
<layout-default-header-item direction="left">
<nz-select
class="width-xs"
style="color: white;"
[nzBorderless]="true"
[nzShowArrow]="false"
[(ngModel)]="currentOrgan"
nzPlaceHolder="请选择机构"
(ngModelChange)="organSelectOptionChange($event)"
>
<nz-option *ngFor="let organ of organs" [nzLabel]="organ.organName" [nzValue]="organ"></nz-option>
</nz-select>
</layout-default-header-item>
<layout-default-header-item direction="left" hidden="pc">
<div layout-default-header-item-trigger (click)="searchToggleStatus = !searchToggleStatus">
<i nz-icon nzType="search"></i>
</div>
</layout-default-header-item>
<layout-default-header-item direction="middle">
<header-search class="alain-default__search" [toggleChange]="searchToggleStatus" />
</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>
<header-fullscreen />
</div>
<div nz-menu-item>
<header-clear-storage />
</div>
</div>
</nz-dropdown-menu>
</layout-default-header-item>
<layout-default-header-item direction="right">
<header-user />
</layout-default-header-item>
<ng-template #asideUserTpl>
<div nz-dropdown nzTrigger="click" [nzDropdownMenu]="userMenu" class="alain-default__aside-user">
<nz-avatar class="alain-default__aside-user-avatar" [nzSrc]="user.avatar" />
<div class="alain-default__aside-user-info">
<strong>{{ user.name }}</strong>
<p class="mb0">{{ user.email }}</p>
</div>
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<ul nz-menu>
<li nz-menu-item routerLink="/pro/account/center">个人中心</li>
<li nz-menu-item routerLink="/pro/account/settings">个人设置</li>
</ul>
</nz-dropdown-menu>
</ng-template>
<ng-template #contentTpl>
<router-outlet />
</ng-template>
</layout-default>
@if (showSettingDrawer) {
<setting-drawer />
}
<theme-btn />
`,
standalone: true,
imports: [
RouterOutlet,
RouterLink,
I18nPipe,
LayoutDefaultModule,
SettingDrawerModule,
SharedModule,
NzSelectModule,
ThemeBtnComponent,
NzIconModule,
NzMenuModule,
NzDropDownModule,
NzAvatarModule,
HeaderSearchComponent,
HeaderClearStorageComponent,
HeaderFullScreenComponent,
HeaderUserComponent
]
})
export class LayoutBasicComponent {
private readonly settings = inject(SettingsService);
options: LayoutDefaultOptions = {
logoExpanded: `./assets/logo-full.svg`,
logoCollapsed: `./assets/logo.svg`
};
searchToggleStatus = false;
currentOrgan: any;
organs: any;
showSettingDrawer = !environment.production;
get user(): User {
return this.settings.user;
}

constructor() {
this.organs = this.settings.user['organs'];
const c = this.settings.user['currentOrgan'];

if (c) {
const organ = this.organs.find((o: { id: any }) => o.id == c.id);
this.currentOrgan = organ;
}
}

organSelectOptionChange(e: any) {
const u = this.settings.user;
u['currentOrgan'] = e;
this.settings.setUser(u);
}
}

+ 34
- 0
src/app/layout/basic/widgets/clear-storage.component.ts 파일 보기

@@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component, HostListener, inject } from '@angular/core';
import { I18nPipe } from '@delon/theme';
import { NzIconModule } from 'ng-zorro-antd/icon';
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>
清理本地缓存
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NzIconModule, I18nPipe]
})
export class HeaderClearStorageComponent {
private readonly modalSrv = inject(NzModalService);
private readonly messageSrv = inject(NzMessageService);

@HostListener('click')
_click(): void {
this.modalSrv.confirm({
nzTitle: 'Make sure clear all local storage?',
nzOnOk: () => {
localStorage.clear();
this.messageSrv.success('Clear Finished!');
}
});
}
}

+ 33
- 0
src/app/layout/basic/widgets/fullscreen.component.ts 파일 보기

@@ -0,0 +1,33 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import { I18nPipe } from '@delon/theme';
import { NzIconModule } from 'ng-zorro-antd/icon';
import screenfull from 'screenfull';

@Component({
selector: 'header-fullscreen',
template: `
<i nz-icon [nzType]="status ? 'fullscreen-exit' : 'fullscreen'"></i>
{{ status ? '退出全屏' : '全屏' }}
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NzIconModule, I18nPipe]
})
export class HeaderFullScreenComponent {
status = false;

@HostListener('window:resize')
_resize(): void {
this.status = screenfull.isFullscreen;
}

@HostListener('click')
_click(): void {
if (screenfull.isEnabled) {
screenfull.toggle();
}
}
}

+ 228
- 0
src/app/layout/basic/widgets/notify.component.ts 파일 보기

@@ -0,0 +1,228 @@
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';
import { NoticeIconModule } from '@delon/abc/notice-icon';
import { SignalRService } from 'src/app/core/utils/signalR.service';
import { _HttpClient } from '@delon/theme';

@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,
standalone: true,
imports: [NoticeIconModule]
})
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 signalRService: SignalRService,
private httpClient: _HttpClient
) {
this.signalRService.startConnection();

this.signalRService.getReceivedMessage().subscribe(data => {
this.count++;
this.cdr.detectChanges();
});
}

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;
this.httpClient.get('/api/main/msg-send-rec').subscribe(res => {
const now = new Date();
const noticeDatas = res.items.map((m: { id: any; }) => {
return {
id: m.id,
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: add(now, { days: 10 }),
type: '通知'
}
});

this.data = this.updateNoticeData(noticeDatas);
this.loading = false;
this.cdr.detectChanges();
});

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

+ 121
- 0
src/app/layout/basic/widgets/search.component.ts 파일 보기

@@ -0,0 +1,121 @@
import { NgTemplateOutlet } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
Output,
inject
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { I18nPipe } from '@delon/theme';
import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { BehaviorSubject, debounceTime, distinctUntilChanged, tap } from 'rxjs';

@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>
@if (loading) {
<i nz-icon nzType="loading"></i>
}
</ng-template>
<input
type="text"
nz-input
[(ngModel)]="q"
[nzAutocomplete]="auto"
(input)="search($event)"
(focus)="qFocus()"
(blur)="qBlur()"
hotkey="F1"
[attr.placeholder]="'搜索:关键字'"
/>
</nz-input-group>
<nz-autocomplete nzBackfill #auto>
@for (i of options; track $index) {
<nz-auto-option [nzValue]="i">{{ i }}</nz-auto-option>
}
</nz-autocomplete>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FormsModule, I18nPipe, NgTemplateOutlet, NzInputModule, NzIconModule, NzAutocompleteModule]
})
export class HeaderSearchComponent implements AfterViewInit, OnDestroy {
private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
private readonly cdr = inject(ChangeDetectorRef);
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>();

ngAfterViewInit(): void {
this.qIpt = this.el.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();
}
}

+ 55
- 0
src/app/layout/basic/widgets/user.component.ts 파일 보기

@@ -0,0 +1,55 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { I18nPipe, SettingsService, User } from '@delon/theme';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzMenuModule } from 'ng-zorro-antd/menu';

@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" />
{{ user.name }}
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<div nz-menu class="width-sm">
<div nz-menu-item routerLink="/pro/account/center">
<i nz-icon nzType="user" class="mr-sm"></i>
个人中心
</div>
<div nz-menu-item routerLink="/pro/account/settings">
<i nz-icon nzType="setting" class="mr-sm"></i>
个人设置
</div>
<div nz-menu-item routerLink="/exception/trigger">
<i nz-icon nzType="close-circle" class="mr-sm"></i>
触发错误
</div>
<li nz-menu-divider></li>
<div nz-menu-item (click)="logout()">
<i nz-icon nzType="logout" class="mr-sm"></i>
退出登录
</div>
</div>
</nz-dropdown-menu>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NzDropDownModule, NzMenuModule, NzIconModule, I18nPipe, NzAvatarModule]
})
export class HeaderUserComponent {
private readonly settings = inject(SettingsService);
private readonly router = inject(Router);
private readonly tokenService = inject(DA_SERVICE_TOKEN);
get user(): User {
return this.settings.user;
}

logout(): void {
this.tokenService.clear();
this.router.navigateByUrl(this.tokenService.login_url!);
}
}

+ 1
- 0
src/app/layout/blank/README.md 파일 보기

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

+ 13
- 0
src/app/layout/blank/blank.component.ts 파일 보기

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
selector: 'layout-blank',
template: `<router-outlet />`,
host: {
'[class.alain-blank]': 'false'
},
standalone: true,
imports: [RouterOutlet]
})
export class LayoutBlankComponent { }

+ 3
- 0
src/app/layout/index.ts 파일 보기

@@ -0,0 +1,3 @@
export * from './basic/basic.component';
export * from './blank/blank.component';
export * from './passport/passport.component';

+ 108
- 0
src/app/layout/passport/passport.component.less 파일 보기

@@ -0,0 +1,108 @@
@import '@delon/theme/index';

:host ::ng-deep {
.container {
display: flex;
flex-direction: column;
min-height: 100%;
background: #f0f2f5;
}

.langs {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;

.anticon {
cursor: pointer;
margin-top: 24px;
margin-right: 24px;
font-size: 14px;
vertical-align: top;
}
}

.wrap {
flex: 1;
padding: 32px 0;
}

.ant-form-item {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
}

.top {
margin-top: 10rem;
text-align: center;
}

.footer {
position: fixed;
/* 相对于浏览器窗口固定位置 */
bottom: 0.8rem;
left: 50%;
/* 左侧距离为50% */
transform: translate(-50%, -50%);
color: #f0f2f5;
/* 平移元素自身宽高的50%,实现水平垂直居中 */
/* 距离底部为0 */
}



.desc {
margin-top: 12px;
margin-bottom: 40px;
font-size: @font-size-base;
color: @text-color-secondary;
}

.container {
//background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-image: url('/assets/bg.jpg');
//background-size: contain;
/* 让背景图片完全覆盖整个容器 */
background-repeat: no-repeat;
// background-position: center 110px;
background-size: cover;
}

@media (min-width: @screen-md-min) {
.wrap {
padding: 32px 0 24px;
}
}
}

[data-theme='dark'] {
:host ::ng-deep {
.container {
background: #141414;
}

.title {
color: fade(@white, 85%);
}

.desc {
color: fade(@white, 45%);
}

@media (min-width: @screen-md-min) {
.container {
background-image: none;
}
}
}
}

[data-theme='compact'] {
:host ::ng-deep {
.ant-form-item {
margin-bottom: 16px;
}
}
}

+ 52
- 0
src/app/layout/passport/passport.component.ts 파일 보기

@@ -0,0 +1,52 @@
import { Component, OnInit, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { GlobalFooterModule } from '@delon/abc/global-footer';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { ThemeBtnComponent } from '@delon/theme/theme-btn';
import { NzIconModule } from 'ng-zorro-antd/icon';

@Component({
selector: 'layout-passport',
template: `
<div class="container">
<div class="wrap">
<div class="top">
<!-- <div class="head">
<img class="logo" src="./assets/logo-color.svg" />
<span class="title">云派医疗基座平台</span>
</div> -->
<div class="desc"></div>
</div>
<router-outlet />
<div class="footer">
上海发电设备成套设计研究院有限责任公司
</div>
</div>
</div>
`,
styleUrls: ['./passport.component.less'],
standalone: true,
imports: [RouterOutlet, GlobalFooterModule, NzIconModule, ThemeBtnComponent]
})
export class LayoutPassportComponent implements OnInit {
private tokenService = inject(DA_SERVICE_TOKEN);

links = [
{
title: '帮助',
href: ''
},
{
title: '隐私',
href: ''
},
{
title: '条款',
href: ''
}
];

ngOnInit(): void {
this.tokenService.clear();
}
}

+ 10
- 0
src/app/routes/dashboard/dashboard.component.html 파일 보기

@@ -0,0 +1,10 @@
<div class="container">
<!-- <dashboard-header></dashboard-header>

<div class="menu-container">
<dashboard-menu></dashboard-menu>
</div> -->


123
</div>

+ 12
- 0
src/app/routes/dashboard/dashboard.component.less 파일 보기

@@ -0,0 +1,12 @@
.container {
height: 768px;
//background-image: url('assets/dashboard/dashboard_bg_1.jpg');
background-size: cover;
/* 背景图片尺寸适应 */
background-position: center;
/* 背景图片居中 */
}

.menu-container {
display: flex;
}

+ 16
- 0
src/app/routes/dashboard/dashboard.component.ts 파일 보기

@@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { PageHeaderModule } from '@delon/abc/page-header';
import { DashboardHeaderComponent } from './header';
import { DashboardMenuComponent } from './menu';

@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [DashboardHeaderComponent, DashboardMenuComponent, PageHeaderModule],
styleUrls: ['./dashboard.component.less']
})
export class DashboardComponent {

}

+ 33
- 0
src/app/routes/dashboard/header/index.less 파일 보기

@@ -0,0 +1,33 @@
.header-container {
position: relative;
text-align: center;
/* Centers the title text horizontally */
}

.header-image-container {
width: 100%;
overflow: hidden;
background-color: #041028; // 设置背景色为红色
}

.header-image {
width: 100%;
height: auto; // 保持图片纵横比
display: block; // 防止出现空隙
}

.header-title {
position: absolute;
top: 50%;
/* Position the title halfway down the image */
left: 50%;
/* Center horizontally */
transform: translate(-50%, -50%);
/* Offset the title to truly center it */
color: #fff;
/* Adjust text color as needed */
font-size: 28px;
/* Adjust font size as needed */
color: #B9D5E5;
/* Additional styling for title text, such as font-family, etc. */
}

+ 20
- 0
src/app/routes/dashboard/header/index.ts 파일 보기

@@ -0,0 +1,20 @@
// index.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'dashboard-header',
template: `
<div class="header-container">
<div class="header-image-container">
<img src="{{imageUrl}}" alt="Header Image" class="header-image"/>
<div class="header-title">{{title}}</div>
</div>
</div>
`,
standalone: true,
styleUrls: ['./index.less']
})
export class DashboardHeaderComponent {
@Input() title: string = "";
@Input() imageUrl: string = 'assets/dashboard/screen_header2.png';
}

+ 17
- 0
src/app/routes/dashboard/menu/index.less 파일 보기

@@ -0,0 +1,17 @@
.menu-container {
display: flex;
flex-direction: row;
/* Centers the title text horizontally */
}

.menu-image-container {
width: 8rem;
overflow: hidden;
}

.menu-image {
width: 100%;
height: auto; // 保持图片纵横比
}

.menu-title {}

+ 35
- 0
src/app/routes/dashboard/menu/index.ts 파일 보기

@@ -0,0 +1,35 @@
// index.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'dashboard-menu',
template: `
<div class="menu-container">
<div class="menu-image-container">
<img src="{{imageUrl}}" class="menu-image"/>
<div class="menu-title">{{title}}</div>
</div>

<div class="menu-image-container">
<img src="{{imageUrl}}" class="menu-image"/>
<div class="menu-title">{{title}}</div>
</div>

<div class="menu-image-container">
<img src="{{imageUrl}}" class="menu-image"/>
<div class="menu-title">{{title}}</div>
</div>

<div class="menu-image-container">
<img src="{{imageUrl}}" class="menu-image"/>
<div class="menu-title">{{title}}</div>
</div>
</div>
`,
standalone: true,
styleUrls: ['./index.less']
})
export class DashboardMenuComponent {
@Input() title: string = "";
@Input() imageUrl: string = 'assets/dashboard/menu_btn_l.png';
}

+ 6
- 0
src/app/routes/data-v/date/date.component.less 파일 보기

@@ -0,0 +1,6 @@
.containers {
align-items: center;
color: white;

/* 可以根据需要设置高度 */
}

+ 30
- 0
src/app/routes/data-v/date/date.component.ts 파일 보기

@@ -0,0 +1,30 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { STColumn, STComponent } from '@delon/abc/st';
import { SFSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { SHARED_IMPORTS } from '@shared';

@Component({
selector: 'data-v-date',
standalone: true,
imports: [...SHARED_IMPORTS],
template: `
<div class="containers">
{{context}}
</div>`,
styleUrls: ['./date.component.less']
})
export class DataVDateComponent implements OnInit {
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);

context: string = '2024-02-29 10:00';

ngOnInit(): void { }

add(): void {
// this.modal
// .createStatic(FormEditComponent, { i: { id: 0 } })
// .subscribe(() => this.st.reload());
}
}

+ 15
- 0
src/app/routes/data-v/home/home.component.html 파일 보기

@@ -0,0 +1,15 @@
<div class="home-container">
<div class="data-v-sysinfo mr-sm">
<data-v-user></data-v-user>
<data-v-date></data-v-date>
</div>

<div>
<data-v-navigation></data-v-navigation>
</div>

<div>
<router-outlet />
</div>

</div>

+ 18
- 0
src/app/routes/data-v/home/home.component.less 파일 보기

@@ -0,0 +1,18 @@
.home-container {
width: 100vw;
/* 水平宽度占据整个视口宽度 */
height: 100vh;
/* 垂直高度占据整个视口高度 */
background-image: url('../../../../assets/dashboard/dashboard_bg_1.jpg');
background-size: cover;
/* 背景图片尺寸适应 */
background-position: top;
/* 图片居中显示 */
}

.data-v-sysinfo {
align-items: center;
display: flex;
text-align: right;
justify-content: right;
}

+ 31
- 0
src/app/routes/data-v/home/home.component.ts 파일 보기

@@ -0,0 +1,31 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { STColumn, STComponent } from '@delon/abc/st';
import { SFSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { SHARED_IMPORTS } from '@shared';
import { DataVUserComponent } from '../user/user.component';
import { LayoutDefaultModule } from '@delon/theme/layout-default';
import { DataVDateComponent } from '../date/date.component';
import { DataVNavigationComponent } from '../navigation/navigation.component';
import { RouterOutlet } from '@angular/router';

@Component({
selector: 'app-data-v-home',
standalone: true,
imports: [DataVUserComponent, DataVDateComponent, DataVNavigationComponent, LayoutDefaultModule, RouterOutlet, ...SHARED_IMPORTS],
templateUrl: './home.component.html',
styleUrls: ['./home.component.less']
})
export class DataVHomeComponent implements OnInit {
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);

ngOnInit(): void { }

add(): void {
// this.modal
// .createStatic(FormEditComponent, { i: { id: 0 } })
// .subscribe(() => this.st.reload());
}

}

+ 33
- 0
src/app/routes/data-v/navigation/navigation.component.less 파일 보기

@@ -0,0 +1,33 @@
.navigation-container {
width: 100%;
margin-top: 2.5rem;
height: 4rem;
/* 水平宽度占据整个视口宽度 */
background-image: url('../../../../assets/dashboard/dashboard_menu.jpg');
background-size: cover;
/* 背景图片尺寸适应 */
background-position: top;
display: flex;
flex-direction: row;
padding-left: 5rem;
}

.menu-image-container {
overflow: hidden;
position: relative;
text-align: center;
}

.menu-image {
height: 1.6rem; // 保持图片纵横比
background-size: cover;
margin-left: 0.5rem;
}

.menu-title {
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: 600;
}

+ 38
- 0
src/app/routes/data-v/navigation/navigation.component.ts 파일 보기

@@ -0,0 +1,38 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { STColumn, STComponent } from '@delon/abc/st';
import { SFSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { SHARED_IMPORTS } from '@shared';

@Component({
selector: 'data-v-navigation',
standalone: true,
imports: [RouterOutlet, ...SHARED_IMPORTS],
template: `
<div class="navigation-container">
<div class="menu-image-container">
<img src="{{imageUrl}}" class="menu-image"/>
<div class="menu-title">{{title}}</div>
</div>
<div class="menu-image-container">
<img src="{{imageUrl}}" class="menu-image"/>
<div class="menu-title">{{title}}</div>
</div>
</div>`,
styleUrls: ['./navigation.component.less']
})
export class DataVNavigationComponent implements OnInit {
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);

imageUrl: string = 'assets/dashboard/menu_btn_l.png';
title: string = 'Label';

context: string = '2024-02-29 10:00';

ngOnInit(): void { }

add(): void {
}
}

+ 17
- 0
src/app/routes/data-v/routes.ts 파일 보기

@@ -0,0 +1,17 @@
import { Routes } from '@angular/router';
import { DataVHomeComponent } from './home/home.component';
import { DataVUserComponent } from './user/user.component';
import { DataVDateComponent } from './date/date.component';
import { DataVWorkstationComponent } from './workstation/workstation.component';
import { DataVS1Component } from './s1/s1.component';

export const routes: Routes = [
{
path: 'home',
component: DataVHomeComponent,
children: [
{ path: '', component: DataVWorkstationComponent },
{ path: 's1', component: DataVS1Component }
]
}];


+ 0
- 0
src/app/routes/data-v/s1/s1.component.html 파일 보기


+ 22
- 0
src/app/routes/data-v/s1/s1.component.ts 파일 보기

@@ -0,0 +1,22 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { STColumn, STComponent } from '@delon/abc/st';
import { SFSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { SHARED_IMPORTS } from '@shared';

@Component({
selector: 'app-data-v-s1',
standalone: true,
imports: [...SHARED_IMPORTS],
templateUrl: './s1.component.html',
})
export class DataVS1Component implements OnInit {
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);

ngOnInit(): void { }

add(): void {

}
}

+ 26
- 0
src/app/routes/data-v/user/user.component.html 파일 보기

@@ -0,0 +1,26 @@
<div class="alain-default__nav-item d-flex align-items-center px-sm mr-sm" nz-dropdown nzPlacement="bottomRight"
[nzDropdownMenu]="userMenu">
<nz-avatar [nzSrc]="user.avatar" nzSize="small" class="mr-sm" />
{{ user.name }}
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<div nz-menu class="width-sm">
<div nz-menu-item routerLink="/pro/account/center">
<i nz-icon nzType="user" class="mr-sm"></i>
个人中心
</div>
<div nz-menu-item routerLink="/pro/account/settings">
<i nz-icon nzType="setting" class="mr-sm"></i>
个人设置
</div>
<div nz-menu-item routerLink="/exception/trigger">
<i nz-icon nzType="close-circle" class="mr-sm"></i>
触发错误
</div>
<li nz-menu-divider></li>
<div nz-menu-item (click)="logout()">
<i nz-icon nzType="logout" class="mr-sm"></i>
退出登录
</div>
</div>
</nz-dropdown-menu>

+ 29
- 0
src/app/routes/data-v/user/user.component.ts 파일 보기

@@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { I18nPipe, SettingsService, User } from '@delon/theme';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzMenuModule } from 'ng-zorro-antd/menu';

@Component({
selector: 'data-v-user',
standalone: true,
templateUrl: './user.component.html',
styleUrls: [],
imports: [NzDropDownModule, NzMenuModule, NzIconModule, I18nPipe, NzAvatarModule]
})
export class DataVUserComponent {
private readonly settings = inject(SettingsService);
private readonly router = inject(Router);
private readonly tokenService = inject(DA_SERVICE_TOKEN);
get user(): User {
return this.settings.user;
}

logout(): void {
this.tokenService.clear();
this.router.navigateByUrl(this.tokenService.login_url!);
}
}

+ 27
- 0
src/app/routes/data-v/workstation/workstation.component.html 파일 보기

@@ -0,0 +1,27 @@
<div class="workstation-header">
</div>

<div nz-row>
<div class="workstation-content" nz-col nzSpan="12">
<div class="workstation-content-title">待办任务</div>
<ag-grid-angular [rowData]="rowData" [columnDefs]="colDefs" class="ag-theme-datav">
</ag-grid-angular>
</div>
<div class="workstation-content" nz-col nzSpan="12">
<div class="workstation-content-title">已办任务</div>
<ag-grid-angular [rowData]="rowData" [columnDefs]="colDefs" class="ag-theme-datav">
</ag-grid-angular>
</div>
</div>
<div nz-row>
<div class="workstation-content" nz-col nzSpan="12">
<div class="workstation-content-title">报警信息</div>
<ag-grid-angular [rowData]="rowData" [columnDefs]="colDefs" class="ag-theme-datav">
</ag-grid-angular>
</div>
<div class="workstation-content" nz-col nzSpan="12">
<div class="workstation-content-title">通知</div>
<ag-grid-angular [rowData]="rowData" [columnDefs]="colDefs" class="ag-theme-datav">
</ag-grid-angular>
</div>
</div>

+ 43
- 0
src/app/routes/data-v/workstation/workstation.component.less 파일 보기

@@ -0,0 +1,43 @@
.workstation-header {
width: 100%;
height: 3rem;
/* 水平宽度占据整个视口宽度 */
background-image: url('../../../../assets/dashboard/dashboard_workstation.jpg');
background-size: cover;
/* 背景图片尺寸适应 */
background-position: top;
}

.workstation-content {
height: 18rem;
padding: 1rem 1.5rem;
background-image: url('../../../../assets/dashboard/dashboard_card_bg.jpg');
background-size: cover;
/* 背景图片尺寸适应 */
background-position: top;
}

.workstation-content-title {
font-size: 18px;
color: #94DDF3;
font-weight: 600;
margin-bottom: 0.5rem;
}

.ag-theme-datav {
--ag-border-color: #74FAFB;
--ag-foreground-color: #74FAFB;
--ag-background-color: #273069;
--ag-header-foreground-color: #74FAFB;
--ag-header-background-color: #273069;
--ag-odd-row-background-color: #273069;
--ag-header-column-resize-handle-color: rgb(126, 46, 132);
--ag-font-size: 17px;
--ag-font-family: monospace;
height: 14rem;
}

.inner-box {
background: #0092ff;
padding: 8px 0;
}

+ 58
- 0
src/app/routes/data-v/workstation/workstation.component.ts 파일 보기

@@ -0,0 +1,58 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { STColumn, STComponent } from '@delon/abc/st';
import { SFSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { SHARED_IMPORTS } from '@shared';
import { AgGridAngular } from 'ag-grid-angular'; // AG Grid Component
import { ColDef } from 'ag-grid-community'; // Column Definition Type Interface

@Component({
selector: 'app-data-v-workstation',
standalone: true,
imports: [AgGridAngular, ...SHARED_IMPORTS],
templateUrl: './workstation.component.html',
styleUrls: ['./workstation.component.less']
})
export class DataVWorkstationComponent implements OnInit {
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);

// Row Data: The data to be displayed.
rowData = [
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
{ make: "Tesla", model: "Model Y", price: 64950, electric: true },
{ make: "Ford", model: "F-Series", price: 33850, electric: false },
{ make: "Toyota", model: "Corolla", price: 29600, electric: false },
];

// Column Definitions: Defines the columns to be displayed.
colDefs: ColDef[] = [
{ field: "make" },
{ field: "model" },
{ field: "price" },
{ field: "electric" }
];

ngOnInit(): void {
}

add(): void {
// this.modal
// .createStatic(FormEditComponent, { i: { id: 0 } })
// .subscribe(() => this.st.reload());
}
}

+ 17
- 0
src/app/routes/exception/exception.component.ts 파일 보기

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ExceptionModule, ExceptionType } from '@delon/abc/exception';

@Component({
selector: 'app-exception',
template: ` <exception [type]="type" style="min-height: 500px; height: 80%;" />`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ExceptionModule]
})
export class ExceptionComponent {
private readonly route = inject(ActivatedRoute);
get type(): ExceptionType {
return this.route.snapshot.data['type'];
}
}

+ 11
- 0
src/app/routes/exception/routes.ts 파일 보기

@@ -0,0 +1,11 @@
import { Routes } from '@angular/router';

import { ExceptionComponent } from './exception.component';
import { ExceptionTriggerComponent } from './trigger.component';

export const routes: Routes = [
{ path: '403', component: ExceptionComponent, data: { type: 403 } },
{ path: '404', component: ExceptionComponent, data: { type: 404 } },
{ path: '500', component: ExceptionComponent, data: { type: 500 } },
{ path: 'trigger', component: ExceptionTriggerComponent }
];

+ 42
- 0
src/app/routes/exception/trigger.component.ts 파일 보기

@@ -0,0 +1,42 @@
import { Component, inject } from '@angular/core';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { _HttpClient } from '@delon/theme';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCardModule } from 'ng-zorro-antd/card';

@Component({
selector: 'exception-trigger',
template: `
<div class="pt-lg">
<nz-card>
@for (t of types; track $index) {
<button (click)="go(t)" nz-button nzDanger>触发{{ t }}</button>
}
<button nz-button nzType="link" (click)="refresh()">触发刷新Token</button>
</nz-card>
</div>
`,
standalone: true,
imports: [NzCardModule, NzButtonModule]
})
export class ExceptionTriggerComponent {
private readonly http = inject(_HttpClient);
private readonly tokenService = inject(DA_SERVICE_TOKEN);

types = [401, 403, 404, 500];

go(type: number): void {
this.http.get(`/api/${type}`).subscribe();
}

refresh(): void {
this.tokenService.set({ token: 'invalid-token' });
// 必须提供一个后端地址,无法通过 Mock 来模拟
this.http.post(`https://localhost:5001/auth`).subscribe({
next: res => console.warn('成功', res),
error: err => {
console.log('最后结果失败', err);
}
});
}
}

+ 34
- 0
src/app/routes/passport/callback.component.ts 파일 보기

@@ -0,0 +1,34 @@
import { Component, Input, OnInit, inject } from '@angular/core';
import { SocialService } from '@delon/auth';
import { SettingsService } from '@delon/theme';

@Component({
selector: 'app-callback',
template: ``,
providers: [SocialService],
standalone: true
})
export class CallbackComponent implements OnInit {
private readonly socialService = inject(SocialService);
private readonly settingsSrv = inject(SettingsService);
@Input() type = '';

ngOnInit(): void {
this.mockModel();
}

private mockModel(): void {
const info = {
token: '123456789',
name: 'cipchk',
email: `${this.type}@${this.type}.com`,
id: 10000,
time: +new Date()
};
this.settingsSrv.setUser({
...this.settingsSrv.user,
...info
});
this.socialService.callback(info);
}
}

+ 21
- 0
src/app/routes/passport/lock/lock.component.html 파일 보기

@@ -0,0 +1,21 @@
<div class="ant-card width-lg" style="margin: 0 auto">
<div class="ant-card-body">
<div class="avatar">
<nz-avatar [nzSrc]="user.avatar" nzIcon="user" nzSize="large" />
</div>
<form nz-form [formGroup]="f" (ngSubmit)="submit()" role="form" class="mt-md">
<nz-form-item>
<nz-form-control [nzErrorTip]="'请输入密码!'">
<nz-input-group nzSuffixIcon="lock">
<input type="password" nz-input formControlName="password" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<div nz-row nzType="flex" nzAlign="middle">
<div nz-col [nzOffset]="12" [nzSpan]="12" style="text-align: right">
<button nz-button [disabled]="!f.valid" nzType="primary">锁屏</button>
</div>
</div>
</form>
</div>
</div>

+ 13
- 0
src/app/routes/passport/lock/lock.component.less 파일 보기

@@ -0,0 +1,13 @@
:host ::ng-deep {
.ant-card-body {
position: relative;
margin-top: 80px;
}

.avatar {
position: absolute;
top: -20px;
left: 50%;
margin-left: -20px;
}
}

+ 44
- 0
src/app/routes/passport/lock/lock.component.ts 파일 보기

@@ -0,0 +1,44 @@
import { Component, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { I18nPipe, SettingsService, User } from '@delon/theme';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzGridModule } from 'ng-zorro-antd/grid';
import { NzInputModule } from 'ng-zorro-antd/input';

@Component({
selector: 'passport-lock',
templateUrl: './lock.component.html',
styleUrls: ['./lock.component.less'],
standalone: true,
imports: [ReactiveFormsModule, I18nPipe, NzAvatarModule, NzFormModule, NzGridModule, NzButtonModule, NzInputModule]
})
export class UserLockComponent {
private readonly tokenService = inject(DA_SERVICE_TOKEN);
private readonly settings = inject(SettingsService);
private readonly router = inject(Router);

f = new FormGroup({
password: new FormControl('', { nonNullable: true, validators: [Validators.required] })
});

get user(): User {
return this.settings.user;
}

submit(): void {
this.f.controls.password.markAsDirty();
this.f.controls.password.updateValueAndValidity();
if (this.f.valid) {
console.log('Valid!');
console.log(this.f.value);
this.tokenService.set({
token: '123'
});
this.router.navigate(['dashboard']);
}
}
}

+ 45
- 0
src/app/routes/passport/login/login.component.html 파일 보기

@@ -0,0 +1,45 @@
<div class="login-container">
<div class="head">
<img class="logo" src="./assets/lh-logo.png" />
<span class="title">国家电投临河电厂全过程智能燃料管理系统</span>
</div>
<div class="login-form">
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
@if (error) {
<nz-alert [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg" />
}
<nz-form-item>
<nz-form-control nzErrorTip="Please enter user, yunpai">
<nz-input-group nzSize="large" nzPrefixIcon="user">
<input nz-input formControlName="userName" placeholder="登录账户" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzErrorTip="Please enter password, 1q2w3E*">
<nz-input-group nzSize="large" nzPrefixIcon="lock">
<input nz-input type="password" formControlName="password" placeholder="登录密码" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item />
<nz-form-item>
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock> 登录 </button>
</nz-form-item>
</form>
</div>
</div>



<ng-template #coverTemplate>
<img style="height: 100px;" alt="example" src="assets/bg2.jpg" />
</ng-template>

<!-- <div class="other">
其他登录方式
<i nz-tooltip nzTooltipTitle="in fact Auth0 via window" (click)="open('auth0', 'window')" nz-icon nzType="alipay-circle" class="icon"></i>
<i nz-tooltip nzTooltipTitle="in fact Github via redirect" (click)="open('github')" nz-icon nzType="taobao-circle" class="icon"></i>
<i (click)="open('weibo', 'window')" nz-icon nzType="weibo-circle" class="icon"></i>
<a class="register" routerLink="/passport/register">注册账户</a>
</div> -->

+ 107
- 0
src/app/routes/passport/login/login.component.less 파일 보기

@@ -0,0 +1,107 @@
@import '@delon/theme/index';

:host {
display: block;
width: 538px;
margin: 0 auto;

::ng-deep {
.ant-tabs .ant-tabs-bar {
margin-bottom: 24px;
text-align: center;
border-bottom: 0;
}

.ant-tabs-tab {
font-size: 16px;
line-height: 24px;
}

.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 4px;
}

.icon {
cursor: pointer;
margin-left: 16px;
font-size: 24px;
color: rgb(0 0 0 / 20%);
vertical-align: middle;
transition: color 0.3s;

&:hover {
color: @primary-color;
}
}

.other {
margin-top: 24px;
line-height: 22px;
text-align: left;

nz-tooltip {
vertical-align: middle;
}

.register {
float: right;
}
}

.login-container {
border-radius: 10px;
/* 设置所有四个角都为 10px 的圆角 */
background-image: url('/assets/bg2.jpg');
background-size: 100%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 10px 0 0 0;
height: 300px;
}

.head {
margin: 0px 0 0 20px;
}

.login-form {
justify-content: center;
padding: 30px 120px 0px 120px;
/* 水平方向居中对齐 */
//width: 400px;
}

.header {
height: 46px;
line-height: 46px;

a {
text-decoration: none;
}
}

.logo {
height: 46px;
margin-right: 6px;
}

.title {
position: relative;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-size: 20px;
font-weight: 400;
color: #515151;
vertical-align: middle;
}
}
}

[data-theme='dark'] {
:host ::ng-deep {
.icon {
color: rgb(255 255 255 / 20%);

&:hover {
color: #fff;
}
}
}
}

+ 210
- 0
src/app/routes/passport/login/login.component.ts 파일 보기

@@ -0,0 +1,210 @@
import { HttpContext } from '@angular/common/http';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { StartupService } from '@core';
import { ReuseTabService } from '@delon/abc/reuse-tab';
import { ALLOW_ANONYMOUS, DA_SERVICE_TOKEN, SocialOpenType, SocialService } from '@delon/auth';
import { I18nPipe, SettingsService, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { SharedModule } from '@shared';
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzTabChangeEvent, NzTabsModule } from 'ng-zorro-antd/tabs';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { finalize } from 'rxjs';

@Component({
selector: 'passport-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.less'],
providers: [SocialService],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
RouterLink,
ReactiveFormsModule,
I18nPipe,
NzCheckboxModule,
NzTabsModule,
NzAlertModule,
NzFormModule,
NzInputModule,
NzButtonModule,
NzToolTipModule,
NzIconModule,
SharedModule
]
})
export class UserLoginComponent implements OnDestroy {
private readonly router = inject(Router);
private readonly settingsService = inject(SettingsService);
private readonly socialService = inject(SocialService);
private readonly reuseTabService = inject(ReuseTabService, { optional: true });
private readonly tokenService = inject(DA_SERVICE_TOKEN);
private readonly startupSrv = inject(StartupService);
private readonly http = inject(_HttpClient);
private readonly cdr = inject(ChangeDetectorRef);

form = inject(FormBuilder).nonNullable.group({
userName: ['', [Validators.required]],
password: ['', [Validators.required]],
mobile: ['', [Validators.required, Validators.pattern(/^1\d{10}$/)]],
captcha: ['', [Validators.required]],
remember: [true]
});
error = '';
type = 0;
loading = false;

count = 0;
interval$: any;

switch({ index }: NzTabChangeEvent): void {
this.type = index!;
}

getCaptcha(): void {
const mobile = this.form.controls.mobile;
if (mobile.invalid) {
mobile.markAsDirty({ onlySelf: true });
mobile.updateValueAndValidity({ onlySelf: true });
return;
}
this.count = 59;
this.interval$ = setInterval(() => {
this.count -= 1;
if (this.count <= 0) {
clearInterval(this.interval$);
}
}, 1000);
}

submit(): void {
this.error = '';
if (this.type === 0) {
const { userName, password } = this.form.controls;
userName.markAsDirty();
userName.updateValueAndValidity();
password.markAsDirty();
password.updateValueAndValidity();
if (userName.invalid || password.invalid) {
return;
}
} else {
const { mobile, captcha } = this.form.controls;
mobile.markAsDirty();
mobile.updateValueAndValidity();
captcha.markAsDirty();
captcha.updateValueAndValidity();
if (mobile.invalid || captcha.invalid) {
return;
}
}

// 默认配置中对所有HTTP请求都会强制 [校验](https://ng-alain.com/auth/getting-started) 用户 Token
// 然一般来说登录请求不需要校验,因此加上 `ALLOW_ANONYMOUS` 表示不触发用户 Token 校验
this.loading = true;
this.cdr.detectChanges();
this.http
.post(
'/api/main/login/account',
{
type: this.type,
userName: this.form.value.userName,
password: this.form.value.password
},
null,
{
context: new HttpContext().set(ALLOW_ANONYMOUS, true)
}
)
.pipe(
finalize(() => {
this.loading = false;
this.cdr.detectChanges();
})
)
.subscribe(res => {
if (res.msg !== 'ok') {
this.error = res.msg;
this.cdr.detectChanges();
return;
}
// 清空路由复用信息
this.reuseTabService?.clear();
// 设置用户Token信息
this.settingsService.setUser({
name: res.user.name,
avatar: './assets/pai.svg',
email: res.user.email,
staffId: res.staff?.id,
organs: res.organ,
currentOrgan: res.organ.find((_: any) => true)
});
//res.user.token = 'Bearer ' + res.user.token;
this.tokenService.set(res.user);
// 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响
this.startupSrv.load().subscribe(() => {
let url = this.tokenService.referrer!.url || '/data-v/home';
if (url.includes('/passport')) {
url = '/data-v/home';
}

url = '/data-v/home';

this.router.navigateByUrl(url);
//window.location.href = 'http://112.33.111.160:8084';
});
});
}

open(type: string, openType: SocialOpenType = 'href'): void {
let url = ``;
let callback = ``;
if (environment.production) {
callback = `https://ng-alain.github.io/ng-alain/#/passport/callback/${type}`;
} else {
callback = `http://localhost:4200/#/passport/callback/${type}`;
}
switch (type) {
case 'auth0':
url = `//cipchk.auth0.com/login?client=8gcNydIDzGBYxzqV0Vm1CX_RXH-wsWo5&redirect_uri=${decodeURIComponent(callback)}`;
break;
case 'github':
url = `//github.com/login/oauth/authorize?client_id=9d6baae4b04a23fcafa2&response_type=code&redirect_uri=${decodeURIComponent(
callback
)}`;
break;
case 'weibo':
url = `https://api.weibo.com/oauth2/authorize?client_id=1239507802&response_type=code&redirect_uri=${decodeURIComponent(callback)}`;
break;
}
if (openType === 'window') {
this.socialService
.login(url, '/', {
type: 'window'
})
.subscribe(res => {
if (res) {
this.settingsService.setUser(res);
this.router.navigateByUrl('/');
}
});
} else {
this.socialService.login(url, '/', {
type: 'href'
});
}
}

ngOnDestroy(): void {
if (this.interval$) {
clearInterval(this.interval$);
}
}
}

+ 13
- 0
src/app/routes/passport/register-result/register-result.component.html 파일 보기

@@ -0,0 +1,13 @@
<result type="success" [title]="title" description="激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。">
<ng-template #title>
<div class="title" style="font-size: 20px">
你的账户:{{email}} 注册成功
</div>
</ng-template>
<button (click)="msg.success('email')" nz-button nzSize="large" [nzType]="'primary'">
查看邮箱
</button>
<button routerLink="/" nz-button nzSize="large">
返回首页
</button>
</result>

+ 17
- 0
src/app/routes/passport/register-result/register-result.component.ts 파일 보기

@@ -0,0 +1,17 @@
import { Component, Input, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ResultModule } from '@delon/abc/result';
import { I18nPipe } from '@delon/theme';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzMessageService } from 'ng-zorro-antd/message';

@Component({
selector: 'passport-register-result',
templateUrl: './register-result.component.html',
standalone: true,
imports: [RouterLink, I18nPipe, NzButtonModule, ResultModule]
})
export class UserRegisterResultComponent {
readonly msg = inject(NzMessageService);
@Input() email = '';
}

+ 115
- 0
src/app/routes/passport/register/register.component.html 파일 보기

@@ -0,0 +1,115 @@
<h3>注册</h3>
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
@if (error) {
<nz-alert [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg" />
}
<nz-form-item>
<nz-form-control [nzErrorTip]="mailErrorTip">
<nz-input-group nzSize="large" nzAddonBeforeIcon="user">
<input nz-input formControlName="mail" placeholder="Email" />
</nz-input-group>
<ng-template #mailErrorTip let-i>
@if (i.errors?.required) {
请输入邮箱地址!
}
@if (i.errors?.email) {
邮箱地址格式错误!
}
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="'请输入密码!'">
<nz-input-group
nzSize="large"
nzAddonBeforeIcon="lock"
nz-popover
nzPopoverPlacement="right"
nzPopoverTrigger="focus"
[(nzPopoverVisible)]="visible"
nzPopoverOverlayClassName="register-password-cdk"
[nzPopoverOverlayStyle]="{ 'width.px': 240 }"
[nzPopoverContent]="pwdCdkTpl"
>
<input nz-input type="password" formControlName="password" placeholder="Password" />
</nz-input-group>
<ng-template #pwdCdkTpl>
<div style="padding: 4px 0">
@switch (status) {
@case ('ok') {
<div class="success">强度:强</div>
}
@case ('pass') {
<div class="warning">强度:中</div>
}
@default {
<div class="error">强度:太短</div>
}
}
<div class="progress-{{ status }}">
<nz-progress [nzPercent]="progress" [nzStatus]="passwordProgressMap[status]" [nzStrokeWidth]="6" [nzShowInfo]="false" />
</div>
<p class="mt-sm">请至少输入 6 个字符。请不要使用容易被猜到的密码。</p>
</div>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="confirmErrorTip">
<nz-input-group nzSize="large" nzAddonBeforeIcon="lock">
<input nz-input type="password" formControlName="confirm" placeholder="Confirm Password" />
</nz-input-group>
<ng-template #confirmErrorTip let-i>
@if (i.errors?.required) {
请确认密码!
}
@if (i.errors?.matchControl) {
两次输入的密码不匹配!
}
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="mobileErrorTip">
<nz-input-group nzSize="large" [nzAddOnBefore]="addOnBeforeTemplate">
<ng-template #addOnBeforeTemplate>
<nz-select formControlName="mobilePrefix" style="width: 100px">
<nz-option [nzLabel]="'+86'" [nzValue]="'+86'" />
<nz-option [nzLabel]="'+87'" [nzValue]="'+87'" />
</nz-select>
</ng-template>
<input formControlName="mobile" nz-input placeholder="Phone number" />
</nz-input-group>
<ng-template #mobileErrorTip let-i>
@if (i.errors?.required) {
请输入手机号!
}
@if (i.errors?.pattern) {
手机号格式错误!
}
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="'请输入验证码!'">
<div nz-row [nzGutter]="8">
<div nz-col [nzSpan]="16">
<nz-input-group nzSize="large" nzAddonBeforeIcon="mail">
<input nz-input formControlName="captcha" placeholder="Captcha" />
</nz-input-group>
</div>
<div nz-col [nzSpan]="8">
<button type="button" nz-button nzSize="large" (click)="getCaptcha()" [disabled]="count > 0" nzBlock [nzLoading]="loading">
{{ count ? count + 's' : ('获取验证码') }}
</button>
</div>
</div>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<button nz-button nzType="primary" nzSize="large" type="submit" [nzLoading]="loading" class="submit">
注册
</button>
<a class="login" routerLink="/passport/login">使用已有账户登录</a>
</nz-form-item>
</form>

+ 51
- 0
src/app/routes/passport/register/register.component.less 파일 보기

@@ -0,0 +1,51 @@
@import '@delon/theme/index';

:host {
display: block;
width: 368px;
margin: 0 auto;

::ng-deep {
h3 {
margin-bottom: 20px;
font-size: 16px;
}

.submit {
width: 50%;
}

.login {
float: right;
line-height: @btn-height-lg;
}
}
}

::ng-deep {
.register-password-cdk {
.success,
.warning,
.error {
transition: color 0.3s;
}

.success {
color: @success-color;
}

.warning {
color: @warning-color;
}

.error {
color: @error-color;
}

.progress-pass > .progress {
.ant-progress-bg {
background-color: @warning-color;
}
}
}
}

+ 152
- 0
src/app/routes/passport/register/register.component.ts 파일 보기

@@ -0,0 +1,152 @@
import { HttpContext } from '@angular/common/http';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, inject } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { ALLOW_ANONYMOUS } from '@delon/auth';
import { I18nPipe, _HttpClient } from '@delon/theme';
import { MatchControl } from '@delon/util/form';
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzGridModule } from 'ng-zorro-antd/grid';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzPopoverModule } from 'ng-zorro-antd/popover';
import { NzProgressModule } from 'ng-zorro-antd/progress';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { finalize } from 'rxjs';

@Component({
selector: 'passport-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
ReactiveFormsModule,
I18nPipe,
RouterLink,
NzAlertModule,
NzFormModule,
NzInputModule,
NzPopoverModule,
NzProgressModule,
NzSelectModule,
NzGridModule,
NzButtonModule
]
})
export class UserRegisterComponent implements OnDestroy {
private readonly router = inject(Router);
private readonly http = inject(_HttpClient);
private readonly cdr = inject(ChangeDetectorRef);

// #region fields

form = inject(FormBuilder).nonNullable.group(
{
mail: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6), UserRegisterComponent.checkPassword.bind(this)]],
confirm: ['', [Validators.required, Validators.minLength(6)]],
mobilePrefix: ['+86'],
mobile: ['', [Validators.required, Validators.pattern(/^1\d{10}$/)]],
captcha: ['', [Validators.required]]
},
{
validators: MatchControl('password', 'confirm')
}
);
error = '';
type = 0;
loading = false;
visible = false;
status = 'pool';
progress = 0;
passwordProgressMap: { [key: string]: 'success' | 'normal' | 'exception' } = {
ok: 'success',
pass: 'normal',
pool: 'exception'
};

// #endregion

// #region get captcha

count = 0;
interval$: NzSafeAny;

static checkPassword(control: FormControl): NzSafeAny {
if (!control) {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: NzSafeAny = this;
self.visible = !!control.value;
if (control.value && control.value.length > 9) {
self.status = 'ok';
} else if (control.value && control.value.length > 5) {
self.status = 'pass';
} else {
self.status = 'pool';
}

if (self.visible) {
self.progress = control.value.length * 10 > 100 ? 100 : control.value.length * 10;
}
}

getCaptcha(): void {
const { mobile } = this.form.controls;
if (mobile.invalid) {
mobile.markAsDirty({ onlySelf: true });
mobile.updateValueAndValidity({ onlySelf: true });
return;
}
this.count = 59;
this.cdr.detectChanges();
this.interval$ = setInterval(() => {
this.count -= 1;
this.cdr.detectChanges();
if (this.count <= 0) {
clearInterval(this.interval$);
}
}, 1000);
}

// #endregion

submit(): void {
this.error = '';
Object.keys(this.form.controls).forEach(key => {
const control = (this.form.controls as NzSafeAny)[key] as AbstractControl;
control.markAsDirty();
control.updateValueAndValidity();
});
if (this.form.invalid) {
return;
}

const data = this.form.value;
this.loading = true;
this.cdr.detectChanges();
this.http
.post('/register', data, null, {
context: new HttpContext().set(ALLOW_ANONYMOUS, true)
})
.pipe(
finalize(() => {
this.loading = false;
this.cdr.detectChanges();
})
)
.subscribe(() => {
this.router.navigate(['passport', 'register-result'], { queryParams: { email: data.mail } });
});
}

ngOnDestroy(): void {
if (this.interval$) {
clearInterval(this.interval$);
}
}
}

+ 40
- 0
src/app/routes/passport/routes.ts 파일 보기

@@ -0,0 +1,40 @@
import { Routes } from '@angular/router';

import { CallbackComponent } from './callback.component';
import { UserLockComponent } from './lock/lock.component';
import { UserLoginComponent } from './login/login.component';
import { UserRegisterComponent } from './register/register.component';
import { UserRegisterResultComponent } from './register-result/register-result.component';
import { LayoutPassportComponent } from '../../layout';

export const routes: Routes = [
// passport
{
path: 'passport',
component: LayoutPassportComponent,
children: [
{
path: 'login',
component: UserLoginComponent,
data: { title: '登录', titleI18n: 'app.login.login' }
},
{
path: 'register',
component: UserRegisterComponent,
data: { title: '注册', titleI18n: 'app.register.register' }
},
{
path: 'register-result',
component: UserRegisterResultComponent,
data: { title: '注册结果', titleI18n: 'app.register.register' }
},
{
path: 'lock',
component: UserLockComponent,
data: { title: '锁屏', titleI18n: 'app.lock' }
}
]
},
// 单页不包裹Layout
{ path: 'passport/callback/:type', component: CallbackComponent }
];

+ 27
- 0
src/app/routes/routes.ts 파일 보기

@@ -0,0 +1,27 @@
import { Routes } from '@angular/router';
import { startPageGuard } from '@core';
import { authSimpleCanActivate, authSimpleCanActivateChild } from '@delon/auth';

import { DashboardComponent } from './dashboard/dashboard.component';
import { LayoutBasicComponent, LayoutBlankComponent } from '../layout';

export const routes: Routes = [
{
path: '',
component: LayoutBasicComponent,
canActivate: [startPageGuard, authSimpleCanActivate],
canActivateChild: [authSimpleCanActivateChild],
data: {},
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'sys', loadChildren: () => import('./sys/routes').then((m) => m.routes) }]
},
{
path: 'data-v',
component: LayoutBlankComponent,
children: [{ path: '', loadChildren: () => import('./data-v/routes').then(m => m.routes) }]
},
{ path: '', loadChildren: () => import('./passport/routes').then(m => m.routes) },
{ path: 'exception', loadChildren: () => import('./exception/routes').then(m => m.routes) },
{ path: '**', redirectTo: 'exception/404' }
];

+ 27
- 0
src/app/routes/sys/audit-log/audit-log-detl/audit-log-detl.component.html 파일 보기

@@ -0,0 +1,27 @@
<sv-container labelWidth="120">
<sv-title>AuditLogInfo</sv-title>
<sv label="应用程序">{{ auditlog.applicationName }}</sv>
<sv label="用户的Id">{{ auditlog.userId }}</sv>
<sv label="用户名">{{ auditlog.userName }}</sv>
<sv label="当前租户">{{ auditlog.tenantId }}</sv>
<sv label="租户的名称">{{ auditlog.tenantName }}</sv>
<sv label="创建的时间">{{ auditlog.executionTime }}</sv>
<sv label="执行时间">{{ auditlog.executionDuration }}</sv>
<sv label="客户端的Id">{{ auditlog.clientId }}</sv>
<sv label="客户端的名称">{{ auditlog.clientName }}</sv>
<sv label="客户端IP地址">{{ auditlog.clientIpAddress }}</sv>
<sv label="当前相关Id">{{ auditlog.correlationId }}</sv>
<sv label="浏览器名称">{{ auditlog.browserInfo }}</sv>
<sv label="HTTP请求的方法">{{ auditlog.httpMethod }}</sv>
<sv label="响应状态码">{{ auditlog.httpStatusCode }}</sv>
<sv label="请求的URL">{{ auditlog.url }}</sv>
<nz-divider></nz-divider>
<sv-title>AuditLogActionInfo</sv-title>
<sv col="1" hideLabel>
<st [data]="auditlog.actions" [columns]="auditLogActionInfoColumns" size="small" style="width: 100%;"></st>
</sv>
<sv-title>EntityChangeInfo</sv-title>
<sv hideLabel col="1">
<st [data]="auditlog.entityChanges" [columns]="entityChangeInfoColumns" size="small" style="width: 100%;"></st>
</sv>
</sv-container>

+ 38
- 0
src/app/routes/sys/audit-log/audit-log-detl/audit-log-detl.component.ts 파일 보기

@@ -0,0 +1,38 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { STColumn, STComponent } from '@delon/abc/st';
import { SFSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { SHARED_IMPORTS } from '@shared';
import { NZ_MODAL_DATA } from 'ng-zorro-antd/modal';

@Component({
selector: 'app-sys-audit-log-detl',
standalone: true,
imports: [...SHARED_IMPORTS],
templateUrl: './audit-log-detl.component.html'
})
export class SysAuditLogDetlComponent implements OnInit {
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);
auditlog: any = inject(NZ_MODAL_DATA);

auditLogActionInfoColumns: STColumn[] = [
{ title: '服务的名称', index: 'serviceName' },
{ title: '方法的名称', index: 'methodName' },
{ title: '参数的JSON', index: 'parameters' },
{ title: '执行的时间', index: 'executionTime' },
{ title: '方法执行时长', index: 'executionDuration' }
];

entityChangeInfoColumns: STColumn[] = [
{ title: '实体被改变的时间', index: 'changeTime' },
{ title: '方法的名称', index: 'changeType' },
{ title: '更改实体的Id', index: 'entityId' },
{ title: '实体所属的租户Id', index: 'entityTenantId' },
{ title: '实体的类型', index: 'entityTypeFullName' }
];

ngOnInit(): void {

}
}

+ 8
- 0
src/app/routes/sys/audit-log/audit-log.component.html 파일 보기

@@ -0,0 +1,8 @@
<page-header [action]="phActionTpl">
<ng-template #phActionTpl>
</ng-template>
</page-header>
<nz-card>
<sf #sf mode="search" [schema]="searchSchema" (formSubmit)="st.reset($event)" (formReset)="st.reset($event)"></sf>
<st #st [data]="url" [columns]="columns" [res]="{ reName: { list: 'items' ,total: 'totalCount'} }" size="small"></st>
</nz-card>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.

불러오는 중...
취소
저장