소스 검색

初始化新框架

master
DESKTOP-AJ87SHJ\h 4 달 전
부모
커밋
fb628f512f
100개의 변경된 파일51899개의 추가작업 그리고 0개의 파일을 삭제
  1. +16
    -0
      .editorconfig
  2. +36
    -0
      .eslintignore
  3. +130
    -0
      .eslintrc.js
  4. +3
    -0
      .gitattributes
  5. +86
    -0
      .gitignore
  6. +3
    -0
      .gitmodules
  7. +29
    -0
      .hintrc
  8. +5
    -0
      .husky/pre-commit
  9. +1
    -0
      .nvmrc
  10. +18
    -0
      .prettierignore
  11. +13
    -0
      .prettierrc.js
  12. +63
    -0
      .stylelintrc.js
  13. +3
    -0
      .vscode/settings.json
  14. +6
    -0
      ;
  15. +7
    -0
      Dockerfile
  16. +21
    -0
      LICENSE
  17. +1
    -0
      _mock/README.md
  18. +122
    -0
      _mock/_user.ts
  19. +1
    -0
      _mock/index.ts
  20. +216
    -0
      angular.json
  21. +19
    -0
      certificate.crt
  22. +2039
    -0
      color.less
  23. +16
    -0
      ng-alain.json
  24. +84
    -0
      nginx.conf
  25. +23522
    -0
      package-lock.json
  26. +139
    -0
      package.json
  27. +14964
    -0
      pnpm-lock.yaml
  28. +17
    -0
      proxy.conf.js
  29. +22
    -0
      routes.ts
  30. +157
    -0
      src/app/app.component.ts
  31. +102
    -0
      src/app/app.config.ts
  32. +18
    -0
      src/app/app.module.ts
  33. +68
    -0
      src/app/conf/message.ts
  34. +5
    -0
      src/app/core/README.md
  35. +3
    -0
      src/app/core/index.ts
  36. +82
    -0
      src/app/core/net/default.interceptor.ts
  37. +68
    -0
      src/app/core/net/helper.ts
  38. +2
    -0
      src/app/core/net/index.ts
  39. +103
    -0
      src/app/core/net/refresh-token.ts
  40. +36
    -0
      src/app/core/route-reuse-strategy.ts
  41. +39
    -0
      src/app/core/start-page.guard.ts
  42. +178
    -0
      src/app/core/startup/startup.service.ts
  43. +80
    -0
      src/app/core/utils/app-utils.ts
  44. +140
    -0
      src/app/core/utils/app-validators.ts
  45. +192
    -0
      src/app/core/utils/base.service.ts
  46. +9
    -0
      src/app/core/utils/nl2br.pipe.ts
  47. +43
    -0
      src/app/custom-reuse-strateg.ts
  48. +1
    -0
      src/app/layout/basic/README.md
  49. +34
    -0
      src/app/layout/basic/widgets/clear-storage.component.ts
  50. +40
    -0
      src/app/layout/basic/widgets/date.component.ts
  51. +33
    -0
      src/app/layout/basic/widgets/fullscreen.component.ts
  52. +121
    -0
      src/app/layout/basic/widgets/search.component.ts
  53. +150
    -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. +77
    -0
      src/app/layout/datav/datav.component.html
  57. +211
    -0
      src/app/layout/datav/datav.component.less
  58. +149
    -0
      src/app/layout/datav/datav.component.ts
  59. +43
    -0
      src/app/layout/datav/menu/datav-menu.component.html
  60. +302
    -0
      src/app/layout/datav/menu/datav-menu.component.less
  61. +286
    -0
      src/app/layout/datav/menu/datav-menu.component.ts
  62. +2
    -0
      src/app/layout/index.ts
  63. +135
    -0
      src/app/layout/overflow/overflow.component.html
  64. +229
    -0
      src/app/layout/overflow/overflow.component.less
  65. +340
    -0
      src/app/layout/overflow/overflow.component.ts
  66. +127
    -0
      src/app/layout/passport/passport.component.less
  67. +75
    -0
      src/app/layout/passport/passport.component.ts
  68. +11
    -0
      src/app/layout/tabs/tabs.component.html
  69. +70
    -0
      src/app/layout/tabs/tabs.component.less
  70. +325
    -0
      src/app/layout/tabs/tabs.component.ts
  71. +111
    -0
      src/app/layout/trad/trad.component.html
  72. +803
    -0
      src/app/layout/trad/trad.component.less
  73. +475
    -0
      src/app/layout/trad/trad.component.ts
  74. +90
    -0
      src/app/routes/approve/leave-apply/leave-apply.component.html
  75. +51
    -0
      src/app/routes/approve/leave-apply/leave-apply.component.less
  76. +385
    -0
      src/app/routes/approve/leave-apply/leave-apply.component.ts
  77. +64
    -0
      src/app/routes/approve/leave-request-detail/leave-request-detail.component.html
  78. +69
    -0
      src/app/routes/approve/leave-request-detail/leave-request-detail.component.less
  79. +190
    -0
      src/app/routes/approve/leave-request-detail/leave-request-detail.component.ts
  80. +51
    -0
      src/app/routes/approve/leave-request-list/leave-request-list.component.html
  81. +313
    -0
      src/app/routes/approve/leave-request-list/leave-request-list.component.less
  82. +180
    -0
      src/app/routes/approve/leave-request-list/leave-request-list.component.ts
  83. +64
    -0
      src/app/routes/approve/manager-approve/manager-approve.component.html
  84. +69
    -0
      src/app/routes/approve/manager-approve/manager-approve.component.less
  85. +188
    -0
      src/app/routes/approve/manager-approve/manager-approve.component.ts
  86. +51
    -0
      src/app/routes/approve/own-leave-request-list/own-leave-request-list.component.html
  87. +305
    -0
      src/app/routes/approve/own-leave-request-list/own-leave-request-list.component.less
  88. +213
    -0
      src/app/routes/approve/own-leave-request-list/own-leave-request-list.component.ts
  89. +64
    -0
      src/app/routes/approve/people-manager-approve/people-manager-approve.component.html
  90. +69
    -0
      src/app/routes/approve/people-manager-approve/people-manager-approve.component.less
  91. +189
    -0
      src/app/routes/approve/people-manager-approve/people-manager-approve.component.ts
  92. +16
    -0
      src/app/routes/approve/routes.ts
  93. +271
    -0
      src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.html
  94. +265
    -0
      src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.less
  95. +24
    -0
      src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.spec.ts
  96. +1544
    -0
      src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.ts
  97. +1
    -0
      src/app/routes/apps/automatic-sample-storage-second/bar-chart.component/bar-chart.component.html
  98. +5
    -0
      src/app/routes/apps/automatic-sample-storage-second/bar-chart.component/bar-chart.component.less
  99. +67
    -0
      src/app/routes/apps/automatic-sample-storage-second/bar-chart.component/bar-chart.component.ts
  100. +10
    -0
      src/app/routes/apps/automatic-sample-storage-second/circle-icon.component/circle-icon.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'
}
}
]
};

+ 3
- 0
.gitattributes 파일 보기

@@ -0,0 +1,3 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf

+ 86
- 0
.gitignore 파일 보기

@@ -0,0 +1,86 @@
# 依赖包
node_modules/

# 打包输出目录
/dist
/dist-server
/tmp
/out-tsc
/bazel-out

# Angular缓存
.angular/
.sass-cache/

# 各种IDE和编辑器配置
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# 环境配置文件
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# 日志文件
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
yarn.*

# 系统文件
.DS_Store
Thumbs.db
desktop.ini

# 测试覆盖率目录
/coverage
/coverage-e2e
/.nyc_output

# 缓存
.eslintcache
.stylelintcache
.cache/
.parcel-cache/
.npm/
.pnpm-store/
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz

# Misc
*.tgz
*.zip
*.rar
*.gz
.temp
.tmp
temp/
tmp/

# 本地配置和密钥文件
*.key
*.pem
*-key.json
*secret*

# 特定于项目的忽略
# 如果有其他特定于您项目的文件/目录需要忽略,请在此处添加

+ 3
- 0
.gitmodules 파일 보기

@@ -0,0 +1,3 @@
[submodule "src/app/routes/wf"]
path = src/app/routes/wf
url = http://112.33.111.155:3000/yuhy/auseft.platform.workflow.git

+ 29
- 0
.hintrc 파일 보기

@@ -0,0 +1,29 @@
{
"extends": [
"development"
],
"hints": {
"axe/name-role-value": [
"default",
{
"button-name": "off"
}
],
"compat-api/css": [
"default",
{
"ignore": [
"user-select"
]
}
],
"axe/forms": [
"default",
{
"label": "off",
"select-name": "off"
}
],
"button-type": "off"
}
}

+ 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/**/*']
};

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

@@ -0,0 +1,3 @@
{
"i18n-ally.localesPaths": []
}

+ 6
- 0
; 파일 보기

@@ -0,0 +1,6 @@
Merge branch 'master' of http://112.33.111.155:3000/hanxs/auseft.newplatform.web
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

+ 7
- 0
Dockerfile 파일 보기

@@ -0,0 +1,7 @@
# nginx
FROM nginx:1.19.0-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.

+ 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';

+ 216
- 0
angular.json 파일 보기

@@ -0,0 +1,216 @@
{
"$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",
"src/assets/menu/",
"src/mobile.html",
"src/assets/menu/diqiu.svg",
{
"glob": "**/*.woff2",
"input": "src/assets/fonts",
"output": "/assets/fonts"
}
],
"styles": [
"src/styles.less",
"src/styles/googleapiscss2.css"
],
"scripts": [

],
"allowedCommonJsDependencies": [
"ajv",
"ajv-formats",
"extend",
"file-saver",
"mockjs"
],
"stylePreprocessorOptions": {
"includePaths": [
"node_modules/"
]
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "30mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "300kb"
}
],
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"linhe": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "30mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "300kb"
}
],
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.linhe.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"
},
"linhe": {
"buildTarget": "himp.platform.angular:build:linhe"
},
"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"
]
}
}














+ 19
- 0
certificate.crt 파일 보기

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJTCCAg0CFHxH0VEVUwceRTXO6UiW7AKa4TLKMA0GCSqGSIb3DQEBCwUAME8x
CzAJBgNVBAYTAmNoMQ4wDAYDVQQIDAVjaGluYTEPMA0GA1UEBwwGc3V6aG91MQ8w
DQYDVQQKDAZhdXNlZnQxDjAMBgNVBAMMBW9jZWFuMB4XDTI0MDUwNjA3MDczNVoX
DTI0MDYwNTA3MDczNVowTzELMAkGA1UEBhMCY2gxDjAMBgNVBAgMBWNoaW5hMQ8w
DQYDVQQHDAZzdXpob3UxDzANBgNVBAoMBmF1c2VmdDEOMAwGA1UEAwwFb2NlYW4w
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHkoVKhHqw7HUq9wEKVID8
/9UbZyZkA61uZ6OlHcKcP4DWxPLACH4yHhkludVuGF0XBh9V+l/CeGRGgzKd7AK+
sNG3MKxUcStIBPQr1WyyDAm62TjuK4frqaeQzV6PRJ/4t/JXcSoKJ7ZpGe7wWjYy
7YzcsAqQ6ZYIbgFccQUJbZ1xUXghyumo5epNlS/pbTajOMbwBG5gom1DwhVSqGGw
iOOynmXS+iZKbWoiYWdVf++SRL3niag2AwUTmxsDwu3FZtjedR4++EK6YRCOIl9P
4tR8yke+JQ+q24H+/5W/4ekwxmYhaAe5dpnThly9x5tS0NrG84PNbr+tgVi10zAB
AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHv3nlXcPLKXKozLStPWkz/kE3nSwSoM
rXCK4qWDbtEr51HiAvEq58A0NEJ8pYl0PowKd2cYBgAONfMQZcUnXqvn0jAHjgvv
QxNJavfzs9IoWwuCx4wxJjWdju3+/Ah4WV3jWUmgQsXzH5s0sExFc76OID09huAU
1dMvW+1JohR3fnKRDTjVsQTRpmAAYY0vq3YCsXhzQoJD/bH0flaTd79MUeQvXEcg
fs86YcnypueNgXVlORIHtNTG6Qyf7wMmSy1Jt378pB5ApVNh+/oVev2t9oVNDftS
WWRAzHxSONuvgv6AHs5BQp8sklwaFe25+hl0ZnScxvjG1ynBrgj/VqU=
-----END CERTIFICATE-----

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


+ 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": {}
}
}

+ 84
- 0
nginx.conf 파일 보기

@@ -0,0 +1,84 @@
upstream api{
server 112.33.111.160:8081;
}
upstream bm{
server 112.33.111.160:8085;
}

upstream pneumatic{
server 112.33.111.160:8085;
}

upstream system{
server 112.33.111.160:8085;
}

upstream pur{
server 112.33.111.160:8085;
}


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

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

location /bm/ {
proxy_pass http://bm;
}

location /pneumatic/ {
proxy_pass http://pneumatic;
}

location /system/ {
proxy_pass http://system;
}

location /pur/ {
proxy_pass http://pur;
}

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


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


+ 139
- 0
package.json 파일 보기

@@ -0,0 +1,139 @@
{
"name": "himp.platform.angular",
"version": "0.0.0",
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead",
"Android >= 4.4"
],
"scripts": {
"ng": "ng",
"start": "ng s -o",
"dev": "ng serve --host 0.0.0.0 --port 4200",
"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": "^18.0.1",
"@angular/common": "^18.0.1",
"@angular/compiler": "^18.0.1",
"@angular/core": "^18.0.1",
"@angular/forms": "^18.0.1",
"@angular/platform-browser": "^18.0.1",
"@angular/platform-browser-dynamic": "^18.0.1",
"@angular/router": "^18.0.1",
"@antv/x6": "^2.18.1",
"@antv/x6-plugin-clipboard": "^2.1.6",
"@antv/x6-plugin-dnd": "^2.1.1",
"@antv/x6-plugin-history": "^2.2.4",
"@antv/x6-plugin-keyboard": "^2.2.3",
"@antv/x6-plugin-selection": "^2.2.2",
"@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-stencil": "^2.1.5",
"@antv/x6-plugin-transform": "^2.1.8",
"@ctrl/tinycolor": "^4.1.0",
"@delon/abc": "^18.2.0",
"@delon/acl": "^17.1.0",
"@delon/auth": "^17.1.0",
"@delon/cache": "^17.1.0",
"@delon/chart": "^18.2.0",
"@delon/form": "^18.2.0",
"@delon/mock": "^17.1.0",
"@delon/theme": "^18.2.0",
"@delon/util": "^17.1.0",
"@microsoft/signalr": "^8.0.0",
"@ngx-translate/core": "^15.0.0",
"@ngx-translate/http-loader": "^8.0.0",
"@scena/guides": "^0.29.2",
"@tauri-apps/api": "^2.2.0",
"@types/d3": "^7.4.3",
"@types/uuid": "^10.0.0",
"ag-grid-angular": "^31.2.1",
"ag-grid-community": "^31.2.1",
"autofit.js": "^3.2.8",
"chart.js": "^4.4.2",
"d3": "^7.9.0",
"dayjs": "^1.11.11",
"echarts": "^5.5.0",
"echarts-gl": "^2.0.9",
"fabric": "^6.4.3",
"highcharts": "^11.4.1",
"highcharts-angular": "^4.0.0",
"hls.js": "^1.5.8",
"i": "^0.3.7",
"infinite-viewer": "^0.29.1",
"jsbarcode": "^3.12.1",
"konva": "^9.3.15",
"lz-string": "^1.5.0",
"mermaid": "^11.6.0",
"moment": "^2.30.1",
"ng-alain": "^17.1.0",
"ng-zorro-antd": "^18.1.1",
"ng2-konva": "^9.0.0",
"ngx-mqtt": "^17.0.0",
"react-device-detect": "^2.2.3",
"rxjs": "~7.8.0",
"screenfull": "^6.0.2",
"tslib": "^2.3.0",
"uninstall": "^0.0.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.19",
"@angular-eslint/builder": "^17.2.0",
"@angular-eslint/eslint-plugin": "^17.2.0",
"@angular-eslint/eslint-plugin-template": "^17.2.0",
"@angular-eslint/schematics": "^18.2.0",
"@angular-eslint/template-parser": "^17.2.0",
"@angular/cli": "^18.0.2",
"@angular/compiler-cli": "^18.0.1",
"@angular/language-service": "^18.0.1",
"@delon/testing": "^17.1.0",
"@ng-util/monaco-editor": "^17.0.1",
"@types/file-saver": "^2.0.7",
"@types/jasmine": "~5.1.0",
"@types/jsbarcode": "^3.11.4",
"@types/opentype.js": "^1.3.8",
"@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",
"raw-loader": "^4.0.2",
"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.4.5"
}
}

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


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

+ 22
- 0
routes.ts 파일 보기

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

import { WfBizflowConfigComponent } from './bizflow-config/bizflow-config.component';
import { ChouYangComponent } from './chouyang/chouyang.component';
import { WorkflowInstanceStartComponent } from './instance-start/instance-start.component';
import { WorkflowProcessDefinitionComponent } from './process-definition/process-definition.component';
import { WorkflowProcessInstanceComponent } from './process-instance/process-instance.component';
import { ProcessMonitoringComponent } from './process-monitoring/process-monitoring.component';
import { WorkflowProcessNodeComponent } from './process-node/process-node.component';
import { WorkflowProcessTypeComponent } from './process-type/process-type.component';
import { TodoTasksComponent } from './todo-tasks/todo-tasks.component'; // xxxx
export const routes: Routes = [
{ path: 'process-definition', component: WorkflowProcessDefinitionComponent },
{ path: 'process-type', component: WorkflowProcessTypeComponent },
{ path: 'process-instance', component: WorkflowProcessInstanceComponent },
{ path: 'process-node', component: WorkflowProcessNodeComponent },
{ path: 'instance-start', component: WorkflowInstanceStartComponent },
{ path: 'bizflow-config', component: WfBizflowConfigComponent },
{ path: 'process-monitoring', component: ProcessMonitoringComponent },
{ path: 'todo-tasks', component: TodoTasksComponent },
{ path: 'chouyang', component: ChouYangComponent }
];

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

@@ -0,0 +1,157 @@
import { NgComponentOutlet, CommonModule } from '@angular/common';
import { Component, ElementRef, OnInit, Renderer2, inject, RendererType2 } from '@angular/core';
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router';
import { TitleService, VERSION as VERSION_ALAIN, stepPreloader, SettingsService } from '@delon/theme';
import { environment } from '@env/environment';
import { NzIconModule, NzIconService } from 'ng-zorro-antd/icon';
import { NzModalService } from 'ng-zorro-antd/modal';
import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version';
// 引入阿里矢量图标库的链接
const ICONFONT_URL = '../assets/fonts/zi/iconfont.js';
// <router-outlet (activate)="onActivate($event)" (deactivate)="onDeactivate($event)">
// <ng-container *ngComponentOutlet="currentComponent"></ng-container>
// </router-outlet>

@Component({
selector: 'app-root',
template: ` <router-outlet /> `,
standalone: true,
imports: [RouterOutlet, NgComponentOutlet, CommonModule]
})
export class AppComponent implements OnInit {
private readonly router = inject(Router);
private readonly titleSrv = inject(TitleService);
private readonly modalSrv = inject(NzModalService);

private donePreloader = stepPreloader();
routeHistory: string[] = [];

private readonly settings = inject(SettingsService);

// 用于 keep-alive 的组件缓存
private componentCache = new Map<string, any>();
currentComponent: any = null;

constructor(
el: ElementRef,
renderer: Renderer2,
private iconService: NzIconService
) {
// 在根模块的构造函数中加载图标字体
this.iconService.fetchFromIconfont({
scriptUrl: ICONFONT_URL
});
renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full);
renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full);
}
getHistory(): string[] {
return this.routeHistory;
}
ngOnInit(): void {
const xhr = new XMLHttpRequest();
const url = `${environment.api['fuelUrl']}system/config/configKey/pcSystemTitile`;
xhr.open('GET', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
const res = JSON.parse(xhr.responseText);
console.log(res.msg, 'ress');
if (res.code === 200) {
this.titleSrv.setTitle(res.msg);
}
} catch (error) {
console.error('解析响应数据时出错:', error);
}
} else {
console.error('请求失败,状态码:', xhr.status);
}
}
};
xhr.send();
let configLoad = false;
if (window.performance.navigation.type === window.performance.navigation.TYPE_RELOAD) {
if (window.location.search === '') {
if (!this.settings.user.name) {
var ua = navigator.userAgent.toLowerCase();
// 简易判断移动端关键字
var isMobile = /android|iphone|ipod|ipad|windows phone|blackberry|mobile/.test(ua);

if (isMobile) {
if (!window.sessionStorage.getItem('token')) {
this.router.navigateByUrl('/mobile');
}
} else {
this.router.navigateByUrl('/passport/login');
}
}
}
} else {
// 记录来源
if (document.referrer) {
console.log('页面是通过链接跳转访问的,来源是:', document.referrer);
} else {
console.log('页面可能是通过直接输入地址或书签访问的');

// **🔹 判断是否为移动端**
var ua = navigator.userAgent.toLowerCase();
var isMobile = /android|iphone|ipod|ipad|windows phone|blackberry|mobile/.test(ua);

// **🔹 进行路由跳转**
if (isMobile) {
this.router.navigateByUrl('/mobile').then(success => {
if (success) {
console.log('成功跳转到 /mobile');
} else {
console.error('跳转失败,可能是路由未正确加载');
}
});
} else {
this.router.navigate(['/passport/login']).then(success => {
if (success) {
console.log('成功跳转到 /passport/login');
} else {
console.error('跳转失败,可能是路由未正确加载');
}
});
}
}
}
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();
}
});
}

// 激活路由时的处理
onActivate(component: any) {
const currentRoute = this.router.url;
if (this.componentCache.has(currentRoute)) {
this.currentComponent = this.componentCache.get(currentRoute);
} else {
this.currentComponent = component;
this.componentCache.set(currentRoute, component);
}
}

// 路由离开时的处理
onDeactivate(component: any) {
// 可以在这里添加组件销毁前的清理逻辑
}
}

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

@@ -0,0 +1,102 @@
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,
RouteReuseStrategy
} 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 { MqttModule, IMqttServiceOptions, MqttService } from 'ngx-mqtt';

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

export const MQTT_SERVICE_OPTIONS: IMqttServiceOptions = {
// hostname:'112.33.111.160',
// port: 8096,
// protocol: 'wss',
// path: '/mqtt',
hostname: '192.168.35.11', //112.33.111.160
// hostname: '10.96.96.13',
port: 8083,
protocol: 'ws',
path: '/mqtt',
username: 'auseft',
password: '1q2w3E**'
};

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

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

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]
}),
{
provide: RouteReuseStrategy,
useClass: CustomRouteReuseStrategy
},
{
provide: MqttService,
useFactory: () => {
return new MqttService(MQTT_SERVICE_OPTIONS);
}
},
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
};

+ 18
- 0
src/app/app.module.ts 파일 보기

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgxEchartsModule } from 'ngx-echarts';

import { AppComponent } from './app.component';

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
NgxEchartsModule.forRoot({
echarts: () => import('echarts')
})
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}

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

@@ -0,0 +1,68 @@
// tslint:disable-next-line:ban-types
const Messages: any = {
T0001: '提示信息',
C0001: '确定要删除这条数据吗?',
C0002: '确定要把这条数据无效吗?',
C0003: '保存成功',
C0004: '确定要作废该药品及药品目录信息吗?',
C0005: '确定要启用该药品信息吗?',
C0006: '删除成功',
C0007: '确定要进行审核操作吗?',
E0001: '请输入{0}',
E0002: '{0}的最大长度是{1}',
E0003: '{0}的格式不正确',
E0004: '请输入数字',
E0005: '请输入{0}',
E0006: '{0}的最小长度是{1}',
E0007: '',
E0008: '请选择{0}',
E0009: '密码必须包含大小写字母、数字和特殊字符,且长度不少于8位',
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';

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

@@ -0,0 +1,82 @@
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 { NzMessageService } from 'ng-zorro-antd/message';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { Observable, of, throwError, mergeMap, catchError } from 'rxjs';

import { ReThrowHttpError, checkStatus, getAdditionalHeaders, toLogin } from './helper';
import { tryRefreshToken } from './refresh-token';

function handleData(injector: Injector, ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandlerFn): Observable<any> {
checkStatus(injector, ev);
let num = 0;
// 业务处理:一些通用操作
switch (ev.status) {
case 200:
const response = ev as any;
num++;
if (response.body && response.body.code === 401) {
if (environment.api.refreshTokenEnabled && environment.api.refreshTokenType === 're-request') {
return tryRefreshToken(injector, ev, req, next);
}
toLogin(injector);
}
if (response.body && response.body.code === 403) {
injector.get(NzMessageService).error('没有权限,请联系管理员授权');
}
//

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

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

@@ -0,0 +1,68 @@
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 {
return;
// if ((ev.status >= 200 && ev.status < 300) || ev.status === 401) {
// return;
// }
// if (ev.status == 0) {
// 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
}
];
}

+ 36
- 0
src/app/core/route-reuse-strategy.ts 파일 보기

@@ -0,0 +1,36 @@
import { RouteReuseStrategy, DetachedRouteHandle, ActivatedRouteSnapshot } from '@angular/router';

export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private storedRoutes: any = new Map<string, DetachedRouteHandle>();

// 判断是否要分离当前路由的组件
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.data['reuse'] === true;
}

// 存储分离的组件
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
if (handle) {
this.storedRoutes.set(this.getRouteKey(route), handle);
}
}

// 判断是否要重新挂载之前保存的组件
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return this.storedRoutes.has(this.getRouteKey(route));
}

// 获取之前保存的组件
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this.storedRoutes.get(this.getRouteKey(route)) || null;
}

// 决定是否要复用当前路由
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig && JSON.stringify(future.params) === JSON.stringify(curr.params);
}

private getRouteKey(route: ActivatedRouteSnapshot): string {
return route.pathFromRoot.map(r => r.url.map(u => u.path).join('/')).join('/');
}
}

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

@@ -0,0 +1,39 @@
/* eslint-disable prettier/prettier */
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { MenuService } from '@delon/theme';
import { Observable } from 'rxjs';

/**
* Dynamically load the start page
*
* 动态加载启动页
*/
export const startPageGuard: CanActivateFn = (a, b): boolean | Observable<boolean> => {
const menuService = inject(MenuService);
const router = inject(Router);
const url: string = a.url.map(seg => seg.path).join('/');
if (b.url == "/" || b.url == '/datav/dashboard-v') {
return true;
}
const menu = findNodeByPath(menuService['data'], b.url);


return true;
};

const findNodeByPath = (data: any, path: any) => {
for (let item of data) {
if (item.link === path) {
return item;
} else if (item.children && item.children.length > 0) {
let result: any = findNodeByPath(item.children, path);
if (result) {
return result;
}
}
}
return null;
}



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

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

/**
* 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
}
];
}
import { environment } from '@env/environment';
@Injectable()
export class StartupService {
private menuService = inject(MenuService);
private settingService = inject(SettingsService);
private httpClient = inject(HttpClient);
private router = inject(Router);

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

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

this.settingService.setApp(app);

if (res.length == 2) {
this.addConvertedMenu(res[1]);
}
}
private addConvertedMenu(res: any) {
if (res && res.code == 200) {
if (res.data && res.data.length && res.data[0]['children'][0].meta.title === '个人中心') {
res.data[0]['children'][0]['children'].push({
component: 'Layout',
hidden: true,
meta: { title: '待办详情', icon: '3column', noCache: false, link: null },
name: '/workflow/task-framework',
path: '/workflow/task-framework'
});
console.log(res.data[0]['children'][0]['children'], 'ssss');
}
this.processData(res.data, 0);
}
}
objModel: any = {
id: 0,
menuCodg: '/',
menuName: '智慧燃料',
text: '智慧燃料',
menuType: 1,
menuCatgeCodg: '',
menuCatgeName: '',
i18n: null,
group: false,
hideInBreadcrumbFlag: false,
visble: false,
shortcutFlag: false,
link: '//',
externalLink: null,
target: null,
icon: {
type: 'iconfont',
value: 'icon-yonghu'
},
openFlag: false,
valiFalg: false,
hideFlag: false,
reuseFlag: false,
srtno: 0,
parentId: 0,

acl: null,
applicationId: null,
creationTime: null,
stas: 0,
stasName: null
};
private processData(data: any[], level: number) {
data.forEach(
(item: { name: any; path: any; hidden: any; meta: any; component: any; redirect: any; alwaysShow: any; children: any[] }) => {
const newItem: any = {
...this.objModel,
menuCode: item?.path,
menuName: item?.meta?.title,
text: item?.meta?.title,
link: item?.path
// icon: item?.meta?.icon,
};
if (item.children) {
newItem.children = this.processChildren(item.children, level + 1);
} else {
newItem.isLeaf = true;
}
this.menuService.add(Array.of(newItem));
}
);
}

private processChildren(children: any[], level: number): any[] {
return children.map(
(child: { name: any; path: any; hidden: any; meta: any; component: any; redirect: any; alwaysShow: any; children: any[] }) => {
if (child.meta.icon.indexOf('anticon') != -1) {
this.objModel.icon = child.meta.icon;
} else {
this.objModel.icon = {
type: 'iconfont',
value: `icon-${child.meta.icon}`,
iconfont: `icon-${child.meta.icon}`,
color: 'red'
};
}

const newChild: any = {
...this.objModel,
menuCode: child?.path,
menuName: child?.meta?.title,
text: child?.meta?.title,
link: child?.path,
hide: child?.hidden
};

if (child.children) {
newChild.children = this.processChildren(child.children, level + 1);
} else {
newChild.isLeaf = true;
}

return newChild;
}
);
}

private viaHttp(): Observable<void> {
return this.initData$.pipe(
map((res: NzSafeAny) => {
window.sessionStorage.setItem('info', JSON.stringify(res[0]));
res.forEach((data: any) => {
if (data.code === 401) {
console.log('401');
this.router.navigateByUrl(`/passport/login`);
}
});

this.handleAppData(res);
})
);
}

load(): Observable<void> {
return this.viaHttp();
}
}

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

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

@@ -0,0 +1,140 @@
import { AbstractControl, ValidatorFn, Validators } 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.required(control) !== null) {
return { required: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}
if (_Validators.mobile(control) !== null) {
// return null;
return { mobile: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}
return null;
};
}

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 strongPassword(id: string, ...params: string[]): ValidatorFn {
const messageParams = params.length === 0 ? [] : params;
return (control: AbstractControl): AppValidatorsErrors | null => {
if (!control.value) {
return null;
}

const value = control.value;

// 检查密码长度
if (value.length < 8) {
return { strongPassword: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}

// 检查是否包含大写字母
if (!/[A-Z]/.test(value)) {
return { strongPassword: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}

// 检查是否包含小写字母
if (!/[a-z]/.test(value)) {
return { strongPassword: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}

// 检查是否包含数字
if (!/[0-9]/.test(value)) {
return { strongPassword: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}

// 检查是否包含特殊字符
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value)) {
return { strongPassword: { errorTip: MESSAGE_UTIL.getMessage(id, messageParams) } };
}

return null;
};
}

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

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

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

import { MESSAGE_UTIL } from '../../conf/message';

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: Array<FormGroup | FormProperty | null>): boolean {
const result = this.hasErrorBase(args);

return result;
}

hasErrorBase(args: Array<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; content?: string }) {
if (state.content) {
this.message.info(state.content);
} else {
this.message.info(this.getMessage(state.messageId));
}
}

public showConfirm(state: { title?: string; message?: string; okCallback?: () => void; cancelCallback?: () => void }) {
console.log(state.message, ' state.message');
this.modalService.create({
nzTitle: state.title,
nzContent: state.message,
nzClosable: false,
nzBodyStyle: { 'font-size': '1rem' },
nzFooter: [
{
label: '确定',
type: 'primary',
onClick: e => {
let del: any = state;
setTimeout(() => {
del.okCallback();
}, 100);
this.modalService.ngOnDestroy();
}
},
{
label: '取消',
onClick: () => {
this.modalService.ngOnDestroy();
console.log(2);
}
}
],

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,
nzMaskClosable: false
});
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;
}

+ 9
- 0
src/app/core/utils/nl2br.pipe.ts 파일 보기

@@ -0,0 +1,9 @@
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'nl2br' })
export class Nl2BrPipe implements PipeTransform {
transform(value: string): string {
if (!value) return value;
return value.replace(/\n/g, '<br/>');
}
}

+ 43
- 0
src/app/custom-reuse-strateg.ts 파일 보기

@@ -0,0 +1,43 @@
import { RouteReuseStrategy, DetachedRouteHandle, ActivatedRouteSnapshot } from '@angular/router';

export class KeepAliveReuseStrategy implements RouteReuseStrategy {
private cachedRoutes: { [key: string]: DetachedRouteHandle } = {};

// 是否允许复用路由(默认逻辑)
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}

// 是否允许分离路由(标记为keepAlive的路由)
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.data['keepAlive'] || false;
}

// 存储分离的路由实例
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
const path = this.getRoutePath(route);
this.cachedRoutes[path] = handle;
}

// 是否允许附加缓存的路由
shouldAttach(route: ActivatedRouteSnapshot): boolean {
const path = this.getRoutePath(route);
return !!this.cachedRoutes[path];
}

// 获取缓存的实例
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
const path = this.getRoutePath(route);
return this.cachedRoutes[path] || null;
}

// 清理指定缓存
clearCache(path: string): void {
delete this.cachedRoutes[path];
}

// 获取路由唯一标识(支持带参数的路由)
private getRoutePath(route: ActivatedRouteSnapshot): string {
return route.routeConfig?.path || '';
}
}

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

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

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

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

@@ -0,0 +1,40 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, HostListener, OnInit, inject } from '@angular/core';
import { I18nPipe } from '@delon/theme';
import { SHARED_IMPORTS } from '@shared';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';

@Component({
selector: 'header-date',
template: `
<div class="data-container">
{{ currentTime | date: 'yyyy-MM-dd HH:mm:ss' }}
</div>
`,
styles: `
.data-container {
align-items: center;
color: white;
padding-left: 1rem;
font-size: 15px;
}
`,
standalone: true,
imports: [CommonModule, ...SHARED_IMPORTS]
})
export class HeaderDateComponent implements OnInit {
currentTime: any;

ngOnInit(): void {
this.updateCurrentTime();
setInterval(() => {
this.updateCurrentTime();
}, 1000);
}

updateCurrentTime() {
this.currentTime = new Date();
}
}

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

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

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

@@ -0,0 +1,150 @@
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN } from '@delon/auth';
import { I18nPipe, SettingsService, User, _HttpClient } from '@delon/theme';
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 { NzMessageService } from 'ng-zorro-antd/message';
import { AppValidators } from 'src/app/core/utils/app-validators';
import { BaseService } from 'src/app/core/utils/base.service';

import { HeaderClearStorageComponent } from './clear-storage.component';
import { HeaderFullScreenComponent } from './fullscreen.component';
import { SharedModule } from '../../../shared/shared.module';

@Component({
selector: 'header-user',
template: `
<div class="alain-default__nav-item d-flex align-items-center px-sm" nz-dropdown nzPlacement="bottomRight" [nzDropdownMenu]="userMenu">
<i nz-icon nzType="user"></i><span style="margin-left:8px;">{{ user.name }}</span>
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<div nz-menu class="width-sm">
<div nz-menu-item (click)="center()">
<i nz-icon nzType="user" 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>
<nz-modal [nzWidth]="400" [nzStyle]="modalStyles" [nzVisible]="visible" [nzTitle]="detailTitle" (nzOnCancel)="close()">
<form *nzModalContent nz-form [formGroup]="form" se-container="1" labelWidth="120">
<se label="用户名" autoErrorTip>
{{ name }}
</se>
<se label="密码" autoErrorTip required>
<input nz-input formControlName="password" type="password" formControlName="password" placeholder="密码" />
</se>
<se label="是否是管理员" autoErrorTip>
{{ administrators ? '是' : '否' }}
</se>
</form>
<div *nzModalFooter style="margin-right: 0.6rem">
<button nz-button nzType="primary" (click)="save()" nzSize="large">确认</button>
<button nz-button (click)="close()" nzSize="large">取消</button>
</div>
</nz-modal>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
NzDropDownModule,
NzMenuModule,
NzIconModule,
I18nPipe,
NzAvatarModule,
HeaderFullScreenComponent,
HeaderClearStorageComponent,
SharedModule
]
})
export class HeaderUserComponent implements OnInit {
private readonly http = inject(_HttpClient);
private readonly settings = inject(SettingsService);
private readonly router = inject(Router);
private readonly tokenService = inject(DA_SERVICE_TOKEN);

private readonly baseService = inject(BaseService);
userId: any = '';
get user(): User {
return this.settings.user;
}
url = `${environment.api['fuelUrl']}system/user/resetPwd`;
form!: FormGroup;
private readonly fb = inject(FormBuilder);
detailTitle: string = '修改密码';
visible: boolean = false;
modalStyles = {
top: 200
};
name: string = '';
administrators: boolean = false;
constructor(private message: NzMessageService) {}

close() {
this.visible = false;
}

logout(): void {
if (window.localStorage.getItem('theme')) {
window.localStorage.removeItem('theme');
document.body.classList.remove('white');
}
this.tokenService.clear();
this.router.navigateByUrl(`/passport${this.tokenService.login_url!}`);
}

center(): void {
const info: any = window.sessionStorage.getItem('info');
const data: any = JSON.parse(info);
console.log(data.roles.indexOf('admin'), data, 'infoooo');
this.administrators = data.roles.indexOf('admin') != -1 ? true : false;
this.userId = data.user.userId;
this.form.reset({
password: ''
});

this.visible = true;
}
save() {
if (this.baseService.hasError(this.form)) {
return;
}

const req = Object.assign(this.form.getRawValue(), { userId: this.userId, iaAdmin: this.administrators });
this.http.put(`${this.url}`, req).subscribe(res => {
if (res.code === 403) {
this.message.warning(res.msg);
}
if (res.code === 500) {
this.message.warning(res.msg);
}
if (res.code === 200) {
this.message.success('修改成功');
}
});
}
ngOnInit(): void {
this.createForm();
this.name = this.settings.user.name;
}

/**
* 创建Form
*
*/
createForm() {
this.form = this.fb.group({
password: [, AppValidators.required('E0001', '请输入密码')]
});
}
}

+ 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]': 'true'
},
standalone: true,
imports: [RouterOutlet]
})
export class LayoutBlankComponent {}

+ 77
- 0
src/app/layout/datav/datav.component.html 파일 보기

@@ -0,0 +1,77 @@
<layout-default [options]="options" [nav]="null" [content]="contentTpl" [customError]="null" style="align-items: flex-start">
<layout-default-header-item direction="left">
<span class="sys_title">
<div class="head-left" style="position: absolute; top: 0px; left: 14px; z-index: 999; display: flex"> </div> </span
></layout-default-header-item>
<div class="top"
><img id="bg-image" src="../../../assets/datav/header.png" alt="" /><span>{{ title }}</span></div
>
<header-date class="date" />
<layout-default-header-item direction="middle" />
<layout-default-header-item direction="right" hidden="mobile">
<div
class="setting"
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 class="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>
<layout-default-header-item direction="right">
<div data-tauri-drag-region class="titlebar" id="titlebar" [hidden]="!isTauri">
<div class="titlebar-button" id="titlebar-minimize" (click)="minimize()">
<img src="../../../assets/window/minsize.svg" alt="minimize" />
</div>
<div class="titlebar-button" id="titlebar-maximize" (click)="toggleMaximize()" [hidden]="!isMax">
<img src="../../../assets/window/window-window_line.svg" alt="maximize" />
</div>
<div class="titlebar-button" id="titlebar-maximize" (click)="toggleMaximize()" [hidden]="isMax">
<img src="../../../assets/window/max-size.svg" alt="maximize" />
</div>
<div class="titlebar-button" id="titlebar-close" (click)="close()">
<img src="../../../assets/window/Close.svg" alt="close" />
</div>
</div>
</layout-default-header-item>
<layout-default-header-item direction="right">
<div data-tauri-drag-region class="titleDragger" id="titlebar" [hidden]="!isTauri"> </div>
</layout-default-header-item>
<ng-template #contentTpl>
<datav-menu />
<router-outlet />
<div id="container"></div>
</ng-template>
</layout-default>

+ 211
- 0
src/app/layout/datav/datav.component.less 파일 보기

@@ -0,0 +1,211 @@
::ng-deep .ul-v1 {
display: flex;
transition: transform 0.3s ease-in-out;
flex-wrap: nowrap;

li {
min-width: 120px !important;
}
}

::ng-deep .date {
position: absolute;
z-index: 9999;
top: 5px;
left: 9px;
}

::ng-deep .sys_title {
color: #fff;
font-size: 16px;
position: absolute;
top: 0px;
left: 0px;
}

nz-header {
position: fixed;
top: 0;
right: 0;
left: 0;
color: #fff;
background: rgb(4 5 26);
width: 104vw;
margin: 0 auto;
padding: 0;
z-index: 9;

img {
display: block;
margin-top: 10px;

}

span {
margin: 0 auto;
font-size: 30px;
line-height: 60px;
color: rgb(102, 255, 255);
}
}

::ng-deep .alain-default__content {
margin-top: 6.5rem !important;
}

::ng-deep .alain-default__header {
box-shadow: none !important;
}

::ng-deep datav-menu {
margin-bottom: 59px;
display: block;
width: 100%;
}

::ng-deep .alain-default__nav {
position: absolute;
right: 36px;
z-index: 999;
}

// 缩放样式
::ng-deep {

html,
body {
background-color: rgb(11, 36, 65) !important;
}

.top {
color: #fff;

span {
position: absolute;
color: #fff;
text-align: center;
width: 100%;
height: 50px;
font-weight: 600;
top: 14px;
font-size: 24px;
}
}

#bg-image {
position: absolute;
top: 32px;
left: 0;
width: 100% !important;
height: 49px !important;
}

.alain-default__nav-item {
padding: 5px 2px;
font-size: 18px;
}

.alain-default {
height: 100% !important;
}

.ant-row.container,
.contain-box {
height: calc(100% - 37px) !important;
max-width: none;
}

.left-item {
flex: 1;
height: auto;
}

.center-item {
height: 100% !important;
}

.right-item {
height: calc(100% - 273px) !important;

.box {
overflow: auto;
height: calc(100% - 3px);
}

}

.car-status-container {
height: calc(100% - 76px) !important;
overflow: auto !important;
}

.alain-default__content {
margin-top: 0 !important;
margin: 0 !important;
margin-left: 0 !important;
}

.alain-default__hide-aside {
.alain-default__content {
height: calc(100% - 118px);
margin-left: 0 !important;
}
}

.alain-default__fixed {
.alain-default__header {
position: static;
}
}

// 全水检测内容部分样式
.total-moisture-monitoring {
height: calc(100% - 39px) !important;
}
}


.titlebar {
height: 30px;
user-select: none;
display: flex;
justify-content: flex-end;
padding-left: 15px;

top: 0;
left: 0;
right: 0;
z-index: 1000;
}

.titleDragger {

height: 30px;
user-select: none;
display: flex;
justify-content: flex-end;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1001;
width: 200px;
left: 50%;
margin-left: -100px;
}

.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
user-select: none;
-webkit-user-select: none;
}

.titlebar-button img {
font-size: 14px;
width: 18px;
height: 18px;
}

+ 149
- 0
src/app/layout/datav/datav.component.ts 파일 보기

@@ -0,0 +1,149 @@
import { Location } from '@angular/common';
import { Component, inject, HostListener, OnInit } from '@angular/core';
import { Router, RouterLink, RouterOutlet } from '@angular/router';
import { QuickMenuModule } from '@delon/abc/quick-menu';
import { I18nPipe, SettingsService, User, _HttpClient } 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 { isTauri } from '@tauri-apps/api/core';
import { Window } from '@tauri-apps/api/window';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzListItemComponent, NzListModule } from 'ng-zorro-antd/list';
import { NzMenuModule } from 'ng-zorro-antd/menu';
import { NzSelectModule } from 'ng-zorro-antd/select';

import { LayoutDatavMenuComponent } from './menu/datav-menu.component';
import { HeaderClearStorageComponent } from '../basic/widgets/clear-storage.component';
import { HeaderDateComponent } from '../basic/widgets/date.component';
import { HeaderFullScreenComponent } from '../basic/widgets/fullscreen.component';
import { HeaderUserComponent } from '../basic/widgets/user.component';

@Component({
selector: 'layout-datav',
templateUrl: './datav.component.html',
styleUrls: ['./datav.component.less'],
standalone: true,
imports: [
RouterOutlet,
RouterLink,
I18nPipe,
LayoutDefaultModule,
SettingDrawerModule,
NzSelectModule,
ThemeBtnComponent,
NzIconModule,
NzMenuModule,
NzDropDownModule,
NzAvatarModule,
HeaderUserComponent,
HeaderClearStorageComponent,
HeaderFullScreenComponent,
NzLayoutModule,
LayoutDatavMenuComponent,
HeaderDateComponent,
QuickMenuModule,
NzListItemComponent,
NzListModule
]
})
export class LayoutDatavComponent implements OnInit {
private readonly settings = inject(SettingsService);
private readonly router = inject(Router);
private readonly http = inject(_HttpClient);
windowWidth: number | undefined;

@HostListener('window:resize', ['$event'])
options: LayoutDefaultOptions = {
logoExpanded: `./assets/datav/trn.png`,
logoCollapsed: `./assets/datav/trn.png`,
hideAside: true
};
scaleView(dom: any, width: number, height: number) {
dom.style.width = `${width}px`;
dom.style.height = `${height}px`;
}
title = '';
getSystemInfo() {
this.http.get(`${environment.api['fuelUrl']}system/config/configKey/pcSystemTitile`).subscribe((res: any) => {
if (res.code == 200) {
this.title = res.msg;
}
});
}
ngOnInit(): void {
document.body.className = 'dark';
// 根据设备分辨率动态设置容易宽度
const html: any = document.querySelector('html');
html.style.fontSize = '16px';
document.body.style.fontSize = '16px';
const width: number = window.screen.width;
this.getSystemInfo();
}
searchToggleStatus = false;
currentOrgan: any;
organs: any;
appWindow = new Window('main');
isTauri = false;
isMax = true;
showSettingDrawer = !environment.production;
get user(): User {
return this.settings.user;
}
@HostListener('window:resize', ['$event'])
onResize(event: any) {
const innerWidth: any = event.target.innerWidth;
const innerHeight: any = event.target.innerHeight;
const node: any = document.querySelector('html');
}

constructor(private location: Location) {
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;
}

this.initMicroApps();
this.obtainIsTauri();
}

obtainIsTauri() {
try {
this.isTauri = isTauri();
} catch {
this.isTauri = false;
}
}

minimize() {
this.appWindow.minimize();
}
toggleMaximize() {
this.appWindow.isMaximized().then(p => {
this.isMax = !p;
});
this.appWindow.toggleMaximize();
}
close() {
this.appWindow.close();
}

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

goBack(): void {
this.location.back();
}

initMicroApps() {}
}

+ 43
- 0
src/app/layout/datav/menu/datav-menu.component.html 파일 보기

@@ -0,0 +1,43 @@
<div class="container-box">
<div class="project">燃料</div>
<div class="menu">
<div *ngFor="let item of menuLv1List">
<button
class="menu-item"
nz-button
nzType="text"
nz-dropdown
[nzDropdownMenu]="menu"
nzPlacement="bottomCenter"
(click)="menuItem(item)"
[class.active]="item.isSelected"
>
<span
*ngIf="item.icon.type === 'iconfont' && item.icon.value != ''"
class="iconfont"
[innerHTML]="item.icon.value === 'icon-#' ? '' : item.icon.value"
style="margin-right: 4px"
></span>
<span
*ngIf="item.icon.type !== 'iconfont' && item.icon.value != ''"
nz-icon
[nzType]="item.icon.value"
style="vertical-align: middle; margin-right: -5px"
></span>
{{ item.menuName }}
</button>
<nz-dropdown-menu #menu="nzDropdownMenu">
<ul nz-menu *ngFor="let zitem of item.children">
<li *ngIf="zitem.children.length === 0 && !zitem.hide" class="menu-title" nz-menu-item (click)="nav(zitem)"
>{{ zitem.menuName }}
</li>
<li *ngIf="zitem.children.length" class="menu-title" nz-submenu [nzTitle]="zitem.menuName">
<ul>
<li nz-menu-item *ngFor="let mitem of zitem.children" class="menu-title" (click)="nav(mitem)"> {{ mitem.menuName }}</li>
</ul>
</li>
</ul>
</nz-dropdown-menu>
</div>
</div>
</div>

+ 302
- 0
src/app/layout/datav/menu/datav-menu.component.less 파일 보기

@@ -0,0 +1,302 @@
::ng-deep .cdk-overlay-pane {
margin-top: -4px !important;

.ant-dropdown-menu-sub {
li {
padding: 8px 16px;
}
}

.ant-dropdown-menu-item,
.ant-dropdown-menu-submenu-title {
text-align: center;
}
}

::ng-deep .ant-dropdown-menu-item:hover,
.ant-dropdown-menu-submenu-title:hover,
.ant-dropdown-menu-item.ant-dropdown-menu-item-active,
.ant-dropdown-menu-item.ant-dropdown-menu-submenu-title-active,
.ant-dropdown-menu-submenu-title.ant-dropdown-menu-item-active,
.ant-dropdown-menu-submenu-title.ant-dropdown-menu-submenu-title-active {
background-color: rgb(24, 144, 255) !important;
}

::ng-deep .ant-dropdown-menu {
background-color: rgb(11, 36, 66) !important;
box-shadow: none !important;
}

::ng-deep .ant-dropdown-menu-item,
.ant-dropdown-menu-submenu-title {
font-size: 14px !important;
}

::ng-deep .menu .ant-btn:hover {
border-bottom: 3px solid rgb(24, 144, 255);
}

::ng-deep .ant-btn>span {
font-weight: bold !important;
}

::ng-deep .ant-dropdown-menu-item,
.ant-dropdown-menu-submenu-title {
clear: both;
margin: 0;
padding: 5px 0;
color: rgba(255, 255, 255, 0.85);
font-weight: normal;
font-size: 14px;
line-height: 22px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
width: 100%;
display: block;
}

::ng-deep .ant-dropdown {
border: 1px solid rgb(166, 177, 187);
}



.container-box {
position: absolute;
width: 100%;
height: 48px;
margin: 5px auto;
top: 84px;
left: 0;
background: #172f4b;
display: flex;

.project {
width: 100px;
font-size: 20px !important;
line-height: 43px;
text-align: center;
margin-left: 20px;
cursor: pointer;
}

.menu {
display: flex;
margin: 2px 60px 0px 0px;
overflow-y: hidden;
overflow-x: auto;

.menu-item {
margin-right: 0px;
font-size: 18px;
color: rgb(174, 183, 192);
height: 38px;
padding: 4px 17px;
}

.active {
color: #fff;
border-bottom: 3px solid rgb(24, 144, 255);
}

}
}

.nav-block {
width: calc(100% - 4rem);
margin: 0 auto;
overflow: hidden;
padding-left: 2px;
}

/* 设置滚动条的宽度 */
::ng-deep .container-box .menu:-webkit-scrollbar {
width: 6px;
}

/* 设置滚动条滑块的样式 */
::ng-deep .container-box .menu::-webkit-scrollbar-thumb {
// margin-left: 0px;
background-color: rgb(24, 144, 255);
border: 2px solid rgb(24, 144, 255);
border-radius: 10px;
}

/* 设置滚动条滑块悬浮时的样式 */
::ng-deep .container-box .menu::-webkit-scrollbar-thumb:hover {
// margin-left: 0px;
background-color: rgb(24, 144, 255);
}

/* 设置滚动条滑块激活(即正在拖动滑块)时的样式 */
::ng-deep .container-box .menu::-webkit-scrollbar-thumb:active {
background-color: rgb(24, 144, 255);
}

//横向
::ng-deep .container-box {
scrollbar-color: auto;
}

/* 设置滚动条的宽度 */
::ng-deep .container-box .menu::-webkit-scrollbar {
height: 6px;
}

/* 设置横向滚动条轨道的样式 */
::ng-deep .container-box .menu::-webkit-scrollbar-track {
background-color: transparent;
border-color: rgb(24, 144, 255);
}

/* 设置横向滚动条滑块悬浮时的样式 */
::ng-deep .container-box .menu::-webkit-scrollbar-thumb:hover {
background-color: rgb(24, 144, 255);
}

/* 设置横向滚动条滑块激活时的样式 */
::ng-deep .container-box .menu::-webkit-scrollbar-thumb:active {
background-color: rgb(24, 144, 255);
}

/* 设置横向滚动条滑块的颜色 */
::ng-deep .container-box .menu::-webkit-scrollbar-thumb {
background-color: rgb(24, 144, 255);
}


.ul-v1 {
padding-top: 0.2rem;
padding-bottom: 0.6rem;
padding-right: 0.3rem;
border: none !important;
font-size: 16px;
background: none !important;

}

.arrow {
width: 1.5rem;
height: 46px;
position: absolute;
top: 82px;
background-color: rgba(5, 66, 84, 1);
border: none;
cursor: pointer;
color: rgb(100, 255, 255);
}

.show {
display: inline-flex !important;
background-color: #054254 !important;
min-width: 142px !important;
margin-right: 8px !important;
font-weight: 600 !important;
color: #74FAFB !important;
text-align: inherit !important;
white-space: nowrap !important;
}

.hide {
display: none !important;
}

.left {
left: 29px;
z-index: 99999;
}

.right {
right: 29px;
z-index: 99999;
}

.li-v1 {
display: inline-flex;
background-color: #054254;
min-width: 142px !important;
margin-right: 8px;
font-weight: 600;
color: #74FAFB;
text-align: inherit;
white-space: nowrap;

&:hover,
&:active,
&.active {}
}

.li-v_2_3 {
display: inline-flex;
background-color: #054254;
min-width: 4rem;
margin-right: 8px;
font-weight: 600;
color: #74FAFB;
text-align: inherit;
white-space: nowrap;
font-size: 15px;
}


.li-divider {
padding: -1rem;
margin-left: -1.2rem;
margin-right: -0.5rem;
align-items: start;
font-size: 1.5rem;
min-width: auto !important;
color: #13c2c2;
text-align: top;
}

.li-selected {
background: linear-gradient(to bottom, #054254 0%, #054254 94%, #13c2c2 94%, #13c2c2 100%);
}

.li-cur {
display: inline-flex;
background-color: #054254;
margin-right: 0.8rem;
padding-left: 1rem;
font-weight: 600;
padding: 0;

color: #13c2c2;
//color: #fff;
text-align: center;
white-space: nowrap;
}

.icon-border {
position: relative;
}

.icon-border-row11 {
position: absolute;
top: 0;
left: -0.8rem;
color: #74FAFB;
}

.icon-border-row12 {
position: absolute;
top: 0;
right: -0.8rem;
color: #74FAFB;
}

.icon-border-row21 {
position: absolute;
bottom: 0;
left: -0.8rem;
color: #74FAFB;
}

.icon-border-row22 {
position: absolute;
bottom: 0;
right: -0.8rem;
color: #74FAFB;
}

+ 286
- 0
src/app/layout/datav/menu/datav-menu.component.ts 파일 보기

@@ -0,0 +1,286 @@
import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterLink, RouterOutlet } from '@angular/router';
import { _HttpClient, I18nPipe, MenuService, ModalHelper } from '@delon/theme';
import { LayoutDefaultModule } from '@delon/theme/layout-default';
import { SettingDrawerModule } from '@delon/theme/setting-drawer';
import { ThemeBtnComponent } from '@delon/theme/theme-btn';
import { SharedModule } from '@shared';
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 zi from '../../../../assets/fonts/zi/iconfont';

@Component({
selector: 'datav-menu',
templateUrl: './datav-menu.component.html',
styleUrls: ['./datav-menu.component.less'],
standalone: true,
imports: [
RouterOutlet,
RouterLink,
I18nPipe,
LayoutDefaultModule,
SettingDrawerModule,
SharedModule,
NzSelectModule,
ThemeBtnComponent,
NzIconModule,
NzMenuModule,
NzDropDownModule,
NzAvatarModule
]
})
export class LayoutDatavMenuComponent implements OnInit {
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);
private readonly router = inject(Router);
private readonly cdr = inject(ChangeDetectorRef);
dropdownList = [
{
name: 'Dropdown 1',
isSelected: false,
menuItems: [{ label: 'Option 1-1' }, { label: 'Option 1-2' }, { label: 'Option 1-3' }]
},
{
name: 'Dropdown 2',
isSelected: false,
menuItems: [{ label: 'Option 2-1' }, { label: 'Option 2-2' }, { label: 'Option 2-3' }]
}
];
showHome: boolean = false;
menuList: any[] = [];
menuData: any[] = [];
menuLv1List: any[] = [];
menuLv1Cur: any[] = [];
menuLv2List: any[] = [];
menuLv2Cur: any[] = [];
menuLv3List: any[] = [];

selectedMenu: string = '';
menuLv = 1;
contentWidth = 0;
scrollX = 0;
ulWidth = 0;
showArrow = true;

constructor(
private menuService: MenuService,
private route: ActivatedRoute
) {
menuService.menus.forEach((menu: any) => {
menu.children.forEach((zitem: any) => {
// const sonMenu = zitem.children.filter((item:any)=>{
// return !item.hide
// })
// zitem.children = sonMenu
});
this.menuList = this.menuList.concat(menu.children);
this.menuData = this.menuList;
});
this.menuList.forEach((item: any) => {
if (item.icon.type === 'iconfont') {
const obj: any = zi.glyphs.find((li: any) => {
return li['font_class'] === item['icon']['value'].split('-')[1];
});
if (obj) {
item['icon']['value'] = `&#x${obj.unicode};`;
}
}

item['isSelected'] = false;
});
this.menuLv1List = this.menuList.filter(item => !item.hide);
}
fontObj: any = {
yonghu: `&#xe60f;`,
sousuowenjian: '&#xe60f;',
xinjian: '&#xe60f;',
tianjia: '&#xe60f;',
diannao: '&#xe60f;'
};

ngOnInit() {}
nav(item: any) {
console.log('Navigation called with item:', item);
console.log('Item link:', item.link);

if (!item.link) {
console.warn('No link found for menu item:', item);
return;
} else {
try {
// 检查是否是hy路由(化验相关),需要重定向到传统布局
if (item.link.startsWith('hy/') || item.link.startsWith('/hy/')) {
console.log('Hy route detected, redirecting to traditional layout');

// 清理URL,确保格式正确
let cleanLink = item.link;
if (cleanLink.startsWith('/')) {
cleanLink = cleanLink.substring(1); // 移除开头的斜杠
}
if (cleanLink.endsWith('/')) {
cleanLink = cleanLink.substring(0, cleanLink.length - 1); // 移除末尾的斜杠
}

// 构建完整的URL,重定向到传统布局(动态获取当前host和port)
const currentProtocol = window.location.protocol;
const currentHost = window.location.hostname;
const currentPort = window.location.port;
const baseUrl = `${currentProtocol}//${currentHost}${currentPort ? `:${currentPort}` : ''}`;
const fullUrl = `${baseUrl}/#/${cleanLink}`;
console.log('Redirecting to traditional layout URL:', fullUrl);

window.location.href = fullUrl;
return;
}

// 对于datav布局内的其他路由
if (item.link.includes('?')) {
// 如果链接已经包含参数,使用window.location进行导航
console.log('Navigating with parameters using window.location');
const fullUrl = `${window.location.origin + window.location.pathname}#${item.link}`;
console.log('Full URL:', fullUrl);
window.location.href = fullUrl;
} else {
// 如果链接没有参数,使用router导航
console.log('Navigating without parameters using router');
this.router.navigateByUrl(item.link).then(
success => console.log('Navigation success:', success),
error => console.error('Navigation error:', error)
);
}
} catch (error) {
console.error('Navigation failed:', error);
}
}
}
// 下拉切换
menuItem(item: any) {
console.log(item, 'item');
this.menuLv1List.forEach(item => (item.isSelected = false));
// 将当前点击对应的按钮选中状态设为 true
item.isSelected = true;
if (item.link) {
this.router.navigateByUrl(item.link);
}
}
// 递归找到路由对应的父
findAncestorByLink(link: string, menuItems: any[]) {
for (const item of menuItems) {
if (item.link === link) {
// 如果找到的是直接匹配的链接,向上追溯最顶层的祖先
return this.traceAncestor(item, []);
} else if (item.children) {
const found: any = this.findAncestorByLink(link, item.children);
if (found) {
return found;
}
}
}
return undefined;
}

traceAncestor(item: any, ancestors: any[]): any {
ancestors.push(item);
if (!item.children || !item.children.length) {
// 如果当前项没有孩子,返回祖先链中的最顶层
return ancestors[0];
} else {
// 否则,继续向上追溯
return this.traceAncestor(item.children[0], ancestors);
}
}

// 获取菜单容器宽度集合
getWidth() {
setTimeout(() => {
const node: any = document.querySelector('.ul-v1 li.show');
const ul: any = document.querySelector('.ul-v1');
this.ulWidth = ul.offsetWidth;
this.contentWidth = (node.offsetWidth + 8) * this.menuList.length;
}, 0);
}
scrollLeft() {
this.scrollX += 150; // Move left by the width of one item plus margin
this.updateScrollPosition();
}

scrollRight() {
this.scrollX -= 150; // Move right by the width of one item plus margin
this.updateScrollPosition();
}

canScrollLeft() {
return this.scrollX < 0;
}

canScrollRight() {
return this.scrollX > -(this.contentWidth - this.ulWidth);
}

updateScrollPosition() {
const scrollableContent: any = document.querySelector('.ul-v1');
if (scrollableContent) {
scrollableContent.style.transform = `translateX(${this.scrollX}px)`;
}
}

redirectToRoute(menu: any): void {
window.sessionStorage.setItem('link', menu.link);
if (menu.children && menu.children.length > 0) {
this.menuLv2Cur = [menu];
this.menuLv2List = [];
this.menuLv3List = [...menu.children];
}
if (menu.link) {
this.router.navigate([menu.link]);
this.selectedMenu = menu.link; // 以文本为键值来区分是否选中
}
}

routeLv1() {
this.router.navigateByUrl('/datav/dashboard-v');
this.showHome = false;
this.selectedMenu = '';
this.menuLv1List = this.menuList;
this.menuLv1Cur = [];
this.menuLv2List = [];
this.menuLv2Cur = [];
this.menuLv3List = [];
this.showArrow = true;
setTimeout(() => {
const node: any = document.querySelector('.nav-block');
node.style.margin = '0px auto';
node.style.paddingLeft = '2px';
}, 0);
this.getWidth();
}

routeLv2(menu: any) {
this.showHome = true;
this.menuLv1Cur = [];
this.menuLv1List = [];
this.menuLv1Cur = [menu];
this.menuLv2List = [...menu.children];
this.showArrow = false;
setTimeout(() => {
const node: any = document.querySelector('.nav-block');
node.style.margin = '0px';
node.style.paddingLeft = '0px';
}, 0);
const node: any = document.querySelector('.ul-v1');
node.style.transform = 'translateX(0)';
}

backLv2(menu: any) {
this.showHome = true;
this.selectedMenu = '';
this.menuLv2Cur = [];
this.menuLv3List = [];
this.menuLv2List = [];
this.menuLv2List = [...menu.children];
}
}

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

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

+ 135
- 0
src/app/layout/overflow/overflow.component.html 파일 보기

@@ -0,0 +1,135 @@
<div class="scroll-container" (wheel)="handleScroll($event)">
<a class="full-button" style="color: #fff">
<header-fullscreen />
</a>
<a class="back-home" style="color: #fff" (click)="backHome()">
<i class="material-icons first-name">home</i>
</a>
<img id="overflow-image" src="../../../assets/datav/overView.png" alt="Large-Image" />


<!-- 汽车计划 -->
<div class="count-view left1 vehicle-batch">{{vehicleOverView.planBatch||0}}</div>
<div class="count-view left1 vehicle-count">{{vehicleOverView.planVehicle||0}}</div>
<!-- 汽车入厂 -->
<div class="count-view left2 vehicle-center-completed">{{vehicleOverView.enterCompleted||0}}</div>
<div class="count-view left2 vehicle-center-pending">{{vehicleOverView.enterPending||0}}</div>
<div class="count-view left2 vehicle-center-now">{{vehicleOverView.enterNow||0}}</div>
<!-- 汽车采样机#1 -->
<div class="count-view left3 vehicle-top-second-completed">{{vehicleOverView.samplingCompleted1||0}}</div>
<div class="count-view left3 vehicle-top-second-pending">{{vehicleOverView.samplingPending1||0}}</div>
<div class="count-view left3 vehicle-top-second-now">{{vehicleOverView.samplingNow1||0}}</div>
<div class="count-view click-area vehicleSampling1" (click)="onClick('vehicleSampling')"></div>
<!-- 汽车采样机#2 -->
<div class="count-view left3 vehicle-bottom-first-completed">{{vehicleOverView.samplingCompleted2||0}}</div>
<div class="count-view left3 vehicle-bottom-first-pending">{{vehicleOverView.samplingPending2||0}}</div>
<div class="count-view left3 vehicle-bottom-first-now">{{vehicleOverView.samplingNow2||0}}</div>
<div class="count-view click-area vehicleSampling2" (click)="onClick('vehicleSampling')"></div>
<!-- 汽车重磅#1 -->
<div class="count-view left4 vehicle-top-first-completed">{{vehicleOverView.heavyCompleted1||0}}</div>
<div class="count-view left4 vehicle-top-first-pending">{{vehicleOverView.heavyPending1||0}}</div>
<div class="count-view left4 vehicle-top-first-now">{{vehicleOverView.heavyNow1||0}}</div>
<div class="count-view click-area vehicleHeavy1" (click)="onClick('vehicleHeavy')"></div>
<!-- 汽车重磅#2 -->
<div class="count-view left4 vehicle-center-completed">{{vehicleOverView.heavyCompleted2||0}}</div>
<div class="count-view left4 vehicle-center-pending">{{vehicleOverView.heavyPending2||0}}</div>
<div class="count-view left4 vehicle-center-now">{{vehicleOverView.heavyNow2||0}}</div>
<div class="count-view click-area vehicleHeavy2" (click)="onClick('vehicleHeavy')"></div>
<!-- 汽车重磅#3 -->
<div class="count-view left4 vehicle-bottom-second-completed">{{vehicleOverView.heavyCompleted3||0}}</div>
<div class="count-view left4 vehicle-bottom-second-pending">{{vehicleOverView.heavyPending3||0}}</div>
<div class="count-view left4 vehicle-bottom-second-now">{{vehicleOverView.heavyNow3||0}}</div>
<div class="count-view click-area vehicleHeavy3" (click)="onClick('vehicleHeavy')"></div>
<!-- 汽车卸煤 -->
<div class="count-view left5 vehicle-center-completed">{{vehicleOverView.coalCompleted||0}}</div>
<div class="count-view left5 vehicle-center-pending">{{vehicleOverView.coalPending||0}}</div>
<div class="count-view left5 vehicle-center-now">{{vehicleOverView.coalNow||0}}</div>
<!-- 汽车轻磅#1 -->
<div class="count-view left6 vehicle-top-second-completed">{{vehicleOverView.lightCompleted1||0}}</div>
<div class="count-view left6 vehicle-top-second-pending">{{vehicleOverView.lightPending1||0}}</div>
<div class="count-view left6 vehicle-top-second-now">{{vehicleOverView.lightNow1||0}}</div>
<div class="count-view click-area vehicleLight1" (click)="onClick('vehicleLight')"></div>
<!-- 汽车轻磅#4 -->
<div class="count-view left6 vehicle-bottom-first-completed">{{vehicleOverView.lightCompleted4||0}}</div>
<div class="count-view left6 vehicle-bottom-first-pending">{{vehicleOverView.lightPending4||0}}</div>
<div class="count-view left6 vehicle-bottom-first-now">{{vehicleOverView.lightNow4||0}}</div>
<div class="count-view click-area vehicleLight2" (click)="onClick('vehicleLight')"></div>
<!-- 汽车出厂 -->
<div class="count-view left7 vehicle-center-completed">{{vehicleOverView.outCompleted||0}}</div>
<div class="count-view left7 vehicle-center-pending">{{vehicleOverView.outPending||0}}</div>
<div class="count-view left7 vehicle-center-now">{{vehicleOverView.outNow||0}}</div>


<!-- 在线全水 -->
<div class="count-view left2 tasks-completed">{{water.completedTasks||0}}</div>
<div class="count-view left2 tasks-pending">{{water.pendingTasks||0}}</div>
<div class="count-view left2 tasks-now">{{water.nowTasks||0}}</div>
<!-- 自动制样 -->
<div class="count-view left3 tasks-completed">{{autoSampling.completedTasks||0}}</div>
<div class="count-view left3 tasks-pending">{{autoSampling.pendingTasks||0}}</div>
<div class="count-view left3 tasks-now">{{autoSampling.nowTasks||0}}</div>
<div class="count-view click-area autoSampling" (click)="onClick('autoSampling')"></div>
<!-- 自动存样 -->
<div class="count-view left4 tasks-completed">{{storage.completedTasks||0}}</div>
<div class="count-view left4 tasks-pending">{{storage.pendingTasks||0}}</div>
<div class="count-view left4 tasks-now">{{storage.nowTasks||0}}</div>
<div class="count-view click-area storage" (click)="onClick('storage')"></div>
<!-- 自动化验 -->
<div class="count-view left5 tasks-completed">{{assay.completedTasks||0}}</div>
<div class="count-view left5 tasks-pending">{{assay.pendingTasks||0}}</div>
<div class="count-view left5 tasks-now">{{assay.nowTasks||0}}</div>
<div class="count-view click-area assay" (click)="onClick('assay')"></div>


<!-- 火车计划 -->
<div class="count-view left1 train-batch">{{trainOverView.planBatchCount||0}}</div>
<div class="count-view left1 train-count">{{trainOverView.planVehicleCount ||0}}</div>
<!-- 火车重车过衡 -->
<div class="count-view left2 train-center-completed">{{trainOverView.heavyWeighCompleted||0}}</div>
<div class="count-view left2 train-center-pending">{{trainOverView.heavyWeighPending||0}}</div>
<div class="count-view left2 train-center-now">{{trainOverView.heavyWeighNow||0}}</div>
<!-- 火车采样机#1 -->
<div class="count-view left3 train-top-completed">{{sampling.completedCount1||0}}</div>
<div class="count-view left3 train-top-pending">{{sampling.pendingCount1||0}}</div>
<div class="count-view left3 train-top-now">{{sampling.nowCount1||0}}</div>
<!-- 火车采样机#2 -->
<div class="count-view left3 train-bottom-completed">{{sampling.completedCount2||0}}</div>
<div class="count-view left3 train-bottom-pending">{{sampling.pendingCount2||0}}</div>
<div class="count-view left3 train-bottom-now">{{sampling.nowCount2||0}}</div>
<!-- 火车翻车机#1 -->
<div class="count-view left4 train-top-completed">{{tipplers.completedCount1||0}}</div>
<div class="count-view left4 train-top-pending">{{tipplers.pendingCount1||0}}</div>
<div class="count-view left4 train-top-now">{{tipplers.nowCount1||0}}</div>
<!-- 火车翻车机#2 -->
<div class="count-view left4 train-bottom-completed">{{tipplers.completedCount2||0}}</div>
<div class="count-view left4 train-bottom-pending">{{tipplers.pendingCount2||0}}</div>
<div class="count-view left4 train-bottom-now">{{tipplers.nowCount2||0}}</div>
<!-- 火车皮带采样机#1 -->
<div class="count-view left5 train-top-completed">{{samplers.completedCount1||0}}</div>
<div class="count-view left5 train-top-pending">{{samplers.pendingCount1||0}}</div>
<div class="count-view left5 train-top-now">{{samplers.nowCount1||0}}</div>
<div class="count-view click-area samplers1" (click)="onClick('samplers')"></div>
<!-- 火车皮带采样机#2 -->
<div class="count-view left5 train-bottom-completed">{{samplers.completedCount2||0}}</div>
<div class="count-view left5 train-bottom-pending">{{samplers.pendingCount2||0}}</div>
<div class="count-view left5 train-bottom-now">{{samplers.nowCount2||0}}</div>
<div class="count-view click-area samplers2" (click)="onClick('samplers')"></div>
<!-- 火车出厂 -->
<div class="count-view left6 train-center-completed">{{trainOut.exitCount||0}}</div>
<div class="count-view left6 train-center-pending">{{(trainOut.arrivedCount||0) - (trainOut.exitCount||0)}}</div>
<div class="count-view left6 train-center-now">{{trainOut.nowCount||0}}</div>

<nz-modal
[nzWidth]="1200"
[nzCentered]="true"
[nzVisible]="editVisible"
(nzOnCancel)="close()"
[nzFooter]="null"
[nzMaskClosable]="false"
[nzMaskStyle]="{'background': '#000', 'opacity': '0', 'animation': 'none'}"
>
<div class="content" *nzModalContent>
<iframe class="iframe" [src]="src"></iframe>
</div>
</nz-modal>
</div>

+ 229
- 0
src/app/layout/overflow/overflow.component.less 파일 보기

@@ -0,0 +1,229 @@
.scroll-container{
overflow: scroll; /* 允许滚动 */
width: 100%;
height: 100vh;
position: absolute; /* 相对于定位 */
.full-button{
position: fixed;
top: 8px;
left: 32px;
z-index: 999;
}
.back-home{
position: fixed;
top: 6px;
left: 64px;
z-index: 999;
}
#overflow-image {
position: absolute;
}
.count-view{
position: absolute;
z-index: 999;
color: #fff;
}
.vehicle-batch{
margin-top: 354px;
}
.vehicle-count{
margin-top: 394px;
}
.vehicle-top-first-completed{
margin-top: 163px;
}
.vehicle-top-first-pending{
margin-top: 203px;
}
.vehicle-top-first-now{
margin-top: 243px;
}
.vehicle-top-second-completed{
margin-top: 238px;
}
.vehicle-top-second-pending{
margin-top: 278px;
}
.vehicle-top-second-now{
margin-top: 318px;
}
.vehicle-center-completed{
margin-top: 308px;
}
.vehicle-center-pending{
margin-top: 348px;
}
.vehicle-center-now{
margin-top: 388px;
}
.vehicle-bottom-first-completed{
margin-top: 378px;
}
.vehicle-bottom-first-pending{
margin-top: 418px;
}
.vehicle-bottom-first-now{
margin-top: 458px;
}
.vehicle-bottom-second-completed{
margin-top: 448px;
}
.vehicle-bottom-second-pending{
margin-top: 488px;
}
.vehicle-bottom-second-now{
margin-top: 528px;
}
.tasks-completed{
margin-top: 638px;
}
.tasks-pending{
margin-top: 678px;
}
.tasks-now{
margin-top: 718px;
}
.train-batch{
margin-top: 1013px;
}
.train-count{
margin-top: 1053px;
}
.train-top-completed{
margin-top: 898px;
}
.train-top-pending{
margin-top: 938px;
}
.train-top-now{
margin-top: 978px;
}
.train-center-completed{
margin-top: 970px;
}
.train-center-pending{
margin-top: 1010px;
}
.train-center-now{
margin-top: 1050px;
}
.train-bottom-completed{
margin-top: 1040px;
}
.train-bottom-pending{
margin-top: 1080px;
}
.train-bottom-now{
margin-top: 1120px;
}
.left1{
margin-left: 365px;
}
.left2{
margin-left: 1160px;
}
.left3{
margin-left: 2125px;
}
.left4{
margin-left: 3085px;
}
.left5{
margin-left: 4050px;
}
.left6{
margin-left: 5010px;
}
.left7{
margin-left: 5970px;
}
.click-area{
cursor: pointer;
width: 432px;
height: 123px;
}
.assay{
margin: 628px 0px 0px 3778px;
}
.storage{
margin: 628px 0px 0px 2817px;
}
.autoSampling{
margin: 628px 0px 0px 1855px;
}
.samplers1{
margin: 888px 0px 0px 3778px;
}
.samplers2{
margin: 1028px 0px 0px 3778px;
}
.vehicleSampling1{
margin: 227px 0px 0px 1855px;
}
.vehicleSampling2{
margin: 368px 0px 0px 1855px;
}
.vehicleHeavy1{
margin: 156px 0px 0px 2817px;
}
.vehicleHeavy2{
margin: 296px 0px 0px 2817px;
}
.vehicleHeavy3{
margin: 438px 0px 0px 2817px;
}
.vehicleLight1{
margin: 227px 0px 0px 4740px;
}
.vehicleLight2{
margin: 368px 0px 0px 4740px;
}
}

::ng-deep {
.ant-modal {
top: 50%;
padding-bottom: 0;

.ant-modal-content {
//border-radius: 12px !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
overflow: hidden !important;
background-color: #f9f9f9 !important;

.ant-modal-header {
background: #fff;
display: none;
.ant-modal-title {
font-weight: 500;
font-size: 16px;
line-height: 22px;
color: rgba(0, 0, 0, 0.85);
}
}
.ant-modal-close{
color: #fff !important;
}
.ant-modal-close-x{
width: auto;
line-height: 0px;
margin: 8px;
}

.ant-modal-body {
padding: 1px !important;
//background-color: #0b2441 !important;
}
}
}
}

.content{
width: 100%;
height: 750px;
.iframe{
width: 100%;
height: 100%;
border-width: 0px;
}
}

+ 340
- 0
src/app/layout/overflow/overflow.component.ts 파일 보기

@@ -0,0 +1,340 @@
import { Component, HostListener, inject } from '@angular/core';
import { Router } from '@angular/router';
import { environment } from '@env/environment';
import { HeaderFullScreenComponent } from '../basic/widgets/fullscreen.component';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { NzMessageService } from 'ng-zorro-antd/message';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { NzModalService } from 'ng-zorro-antd/modal';
import { CommonModule } from '@angular/common';
import { SHARED_IMPORTS } from '@shared';
import { NzDividerModule } from 'ng-zorro-antd/divider';
import { STModule } from '@delon/abc/st';
import { XlsxModule } from '@delon/abc/xlsx';
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzTreeSelectModule } from 'ng-zorro-antd/tree-select';
import { DelonFormModule } from '@delon/form';

@Component({
selector: 'layout-overflow',
templateUrl: './overflow.component.html',
styleUrls: ['./overflow.component.less'],
standalone: true,
imports: [
HeaderFullScreenComponent,
CommonModule,
...SHARED_IMPORTS,
STModule,
XlsxModule,
NzPaginationModule,
NzTreeSelectModule,
NzDividerModule,
NzTagModule,
DelonFormModule
],
})
export class LayoutOverflowComponent {
private readonly router = inject(Router);
private readonly http = inject(_HttpClient);
private readonly modal = inject(ModalHelper);
private readonly modalService = inject(NzModalService);
constructor(private msg: NzMessageService,private sanitizer: DomSanitizer) {}

vehicleOverViewUrl = `${environment.api['fuelUrl']}hy/vehicle/statistics/overview`;
//vehiclePrimaryUrl = `${environment.api['fuelUrl']}hy/vehicle/weigh/scales/primary`;
//vehicleSecondaryUrl = `${environment.api['fuelUrl']}hy/vehicle/weigh/scales/secondary`;
trainOverViewUrl = `${environment.api['fuelUrl']}hy/automation/train/overview`;
trainSamplingUrl = `${environment.api['fuelUrl']}hy/automation/train/sampling`;
tipplersUrl = `${environment.api['fuelUrl']}hy/automation/tipplers`;
samplersUrl = `${environment.api['fuelUrl']}hy/automation/samplers`;
trainOutUrl = `${environment.api['fuelUrl']}hy/vehicle/statistics/onceWeighingCount`;
autoSamplingUrl = `${environment.api['fuelUrl']}hy/automation/auto-sampling`;
storageUrl = `${environment.api['fuelUrl']}hy/automation/storage`;
assayUrl = `${environment.api['fuelUrl']}hy/automation/assay`;
waterUrl = `${environment.api['fuelUrl']}hy/automation/water`;

@HostListener('wheel', ['$event'])
handleScroll(event: WheelEvent) {
event.preventDefault();
}

ngOnInit(): void {
this.getVehicleOverView();
// this.getVehiclePrimary();
// this.getVehicleSecondary();
this.getTrainOverView();
this.getTrainSampling();
this.getTipplers();
this.getSamplers();
this.getTrainOut();
this.getAutoSampling();
this.getStorage();
this.getAssay();
this.getWater();
}

backHome() {
try {
console.log('Navigating without parameters using router');
this.router.navigateByUrl('/datav/dashboard-v').then(
success => console.log('Navigation success:', success),
error => console.error('Navigation error:', error)
);
} catch (error) {
console.error('Navigation failed:', error);
}
}

//汽车相关数据统计列表
vehicleOverView: any = {};
getVehicleOverView(){
this.http.get(this.vehicleOverViewUrl).subscribe((res: any) => {
console.log('res1',res);
if (res.code == 200) {
this.vehicleOverView = {
planBatch: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('计划批次') > -1)?.quantity,
planVehicle: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('计划车数') > -1)?.quantity,
enterCompleted: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('已入厂') > -1)?.quantity,
enterPending: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('未入厂') > -1)?.quantity,
enterNow: 0,
samplingCompleted1: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('采样完成') > -1)?.quantity,
samplingPending1: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('待采样') > -1)?.quantity,
samplingNow1: 0,
samplingCompleted2: 0,
samplingPending2: 0,
samplingNow2: 0,
heavyCompleted1: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('#1重磅') > -1)?.quantity,
heavyPending1: !!(res.data||[]).find((item: any) => (item.typeName||'').indexOf('#1重磅') > -1)?.typeName ? (res.data||[]).find((item: any) => (item.typeName||'').indexOf('等待重磅') > -1)?.quantity : 0,
heavyNow1: 0,
heavyCompleted2: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('#2重磅') > -1)?.quantity,
heavyPending2: !!(res.data||[]).find((item: any) => (item.typeName||'').indexOf('#2重磅') > -1)?.typeName ? (res.data||[]).find((item: any) => (item.typeName||'').indexOf('等待重磅') > -1)?.quantity : 0,
heavyNow2: 0,
heavyCompleted3: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('#3重磅') > -1)?.quantity,
heavyPending3: !!(res.data||[]).find((item: any) => (item.typeName||'').indexOf('#3重磅') > -1)?.typeName ? (res.data||[]).find((item: any) => (item.typeName||'').indexOf('等待重磅') > -1)?.quantity : 0,
heavyNow3: 0,
coalCompleted: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('卸煤完成') > -1)?.quantity,
coalPending: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('等待卸煤') > -1)?.quantity,
coalNow: 0,
lightCompleted1: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('#1轻磅') > -1)?.quantity,
lightPending1: !!(res.data||[]).find((item: any) => (item.typeName||'').indexOf('#1轻磅') > -1)?.typeName ? (res.data||[]).find((item: any) => (item.typeName||'').indexOf('等待轻磅') > -1)?.quantity : 0,
lightNow1: 0,
lightCompleted4: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('#4轻磅') > -1)?.quantity,
lightPending4: !!(res.data||[]).find((item: any) => (item.typeName||'').indexOf('#4轻磅') > -1)?.typeName ? (res.data||[]).find((item: any) => (item.typeName||'').indexOf('等待轻磅') > -1)?.quantity : 0,
lightNow4: 0,
outCompleted: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('出厂数') > -1)?.quantity,
outPending: (res.data||[]).find((item: any) => (item.typeName||'').indexOf('未出厂数') > -1)?.quantity,
outNow: 0,
}
} else {
//this.msg.error(res.msg);
}
});
}

// getVehiclePrimary(){
// this.http.get(this.vehiclePrimaryUrl).subscribe((res: any) => {
// console.log('res2',res);
// if (res.code == 200) {
// } else {
// this.msg.error(res.msg);
// }
// });
// }

// getVehicleSecondary(){
// this.http.get(this.vehicleSecondaryUrl).subscribe((res: any) => {
// console.log('res3',res);
// if (res.code == 200) {
// } else {
// this.msg.error(res.msg);
// }
// });
// }

//火车计划与过衡数据
trainOverView: any = {};
getTrainOverView(){
this.http.get(this.trainOverViewUrl).subscribe((res: any) => {
console.log('res4',res);
if (res.code == 200) {
this.trainOverView = res.data||{}
this.trainOut['arrivedCount'] = res.data?.arrivedCount||0
} else {
//this.msg.error(res.msg);
}
});
}

//火车采样情况
sampling: any = {};
getTrainSampling(){
this.http.get(this.trainSamplingUrl).subscribe((res: any) => {
console.log('res5',res);
if (res.code == 200) {
this.sampling = {
completedCount1: (res.data||[]).find((item: any) => (item.name||'').indexOf('完成') > -1)?.count,
pendingCount1: (res.data||[]).find((item: any) => (item.name||'').indexOf('等待') > -1)?.count,
nowCount1: 0,
completedCount2: 0,
pendingCount2: 0,
nowCount2: 0
}
} else {
//this.msg.error(res.msg);
}
});
}

//翻车机
tipplers: any = {};
getTipplers(){
this.http.get(this.tipplersUrl).subscribe((res: any) => {
console.log('res6',res);
if (res.code == 200) {
let tippler1 = (res.data||[]).find((item: any) => (item.tipplerIndex||0) == 0)||{}
let tippler2 = (res.data||[]).find((item: any) => (item.tipplerIndex||0) == 1)||{}
this.tipplers = {
completedCount1: tippler1.completedCount,
pendingCount1: tippler1.pendingCount,
nowCount1: 0,
completedCount2: tippler2.completedCount,
pendingCount2: tippler2.pendingCount,
nowCount2: 0,
}
} else {
//this.msg.error(res.msg);
}
});
}

//皮带采样机
samplers: any = {};
getSamplers(){
this.http.get(this.samplersUrl).subscribe((res: any) => {
console.log('res7',res);
if (res.code == 200) {
let sampler1 = (res.data||[]).find((item: any) => (item.samplerIndex||0) == 0)||{}
let sampler2 = (res.data||[]).find((item: any) => (item.samplerIndex||0) == 1)||{}
this.samplers = {
completedCount1: sampler1.completedCount,
pendingCount1: sampler1.pendingCount,
nowCount1: 0,
completedCount2: sampler2.completedCount,
pendingCount2: sampler2.pendingCount,
nowCount2: 0,
}
} else {
//this.msg.error(res.msg);
}
});
}

//火车出厂数据
trainOut: any = {
arrivedCount: 0,
exitCount: 0,
nowCount: 0
};
getTrainOut(){
this.http.get(this.trainOutUrl).subscribe((res: any) => {
console.log('res4',res);
if (res.code == 200) {
this.trainOut['exitCount'] = res.data?.exitCount||0
} else {
//this.msg.error(res.msg);
}
});
}

//自动制样
autoSampling: any = {};
getAutoSampling(){
this.http.get(this.autoSamplingUrl).subscribe((res: any) => {
console.log('res8',res);
if (res.code == 200) {
this.autoSampling = res.data||{}
} else {
//this.msg.error(res.msg);
}
});
}

//自动存样
storage: any = {};
getStorage(){
this.http.get(this.storageUrl).subscribe((res: any) => {
console.log('res9',res);
if (res.code == 200) {
this.storage = res.data||{}
} else {
//this.msg.error(res.msg);
}
});
}

//自动化验
assay: any = {};
getAssay(){
this.http.get(this.assayUrl).subscribe((res: any) => {
console.log('res10',res);
if (res.code == 200) {
this.assay = res.data||{}
} else {
//this.msg.error(res.msg);
}
});
}

modalStyles = {
top: 0
};
editVisible = false;
src: SafeResourceUrl = '';
onClick(flag: any){
this.editVisible = true;
let url = 'overflow'
if(flag == 'assay'){
url = 'datav/automatic-assay'
}
if(flag == 'storage'){
url = 'datav/cunyanggui'
}
if(flag == 'autoSampling'){
url = 'datav/auto-sample'
}
if(flag == 'samplers'){
url = 'datav/sampling-docking'
}
if(flag == 'vehicleSampling'){
url = 'datav/car-sampling'
}
if(flag == 'vehicleHeavy'){
url = 'datav/truck-scale'
}
if(flag == 'vehicleLight'){
url = 'datav/truck-scale2'
}
this.src = this.sanitizer.bypassSecurityTrustResourceUrl(
`${window.location.origin}/#/${url}`
);
}
close() {
this.editVisible = false;
}

//在线全水
water: any = {};
getWater(){
this.http.get(this.waterUrl).subscribe((res: any) => {
console.log('res11',res);
if (res.code == 200) {
this.water = res.data||{}
} else {
//this.msg.error(res.msg);
}
});
}
}

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

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

:host ::ng-deep {


.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;
}
}
.contain {
display: flex; // 启用 flex 布局
flex-direction: column;
// min-height: 100vh;
background: #f0f2f5;
overflow: auto;
// height: 100%;
// overflow: hidden;
width: 100%;
justify-content: center; // 先将内容垂直居中
position: relative;
}

// ... 已有代码 ...

.wrap {
// position: absolute;
padding: 32px 0;
z-index: 10;
position: absolute; // 确保 .wrap 是相对定位
right:160px;

}
.video {
// width: 100%;
// height: 100%;
// z-index: 1;
object-fit: cover;
// position: absolute;
}

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

.top {
text-align: center;
}

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

a {
text-decoration: none;
}
}

.logo {
height: 44px;
margin-right: 16px;
}

.title {
position: relative;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-size: 33px;
font-weight: 600;
color: @heading-color;
vertical-align: middle;
}

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

.container {
position: relative;
}



}

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

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

@@ -0,0 +1,75 @@
import { Component, OnInit, inject, AfterViewInit, ViewChild } from '@angular/core';
import { RouterOutlet, Router } 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="contain">
<video class="video" autoplay muted loop id="bgvid">
<source src="../../assets/login/login.mp4" type="video/mp4" />
您的浏览器不支持视频标签。
</video>
<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 />
<!-- <global-footer [links]="links">
Copyright
<i class="anticon anticon-copyright"></i> 2023 <a href="//github.com/cipchk" target="_blank">卡色</a>出品
</global-footer> -->
</div>
</div>
`,
styleUrls: ['./passport.component.less'],
standalone: true,
imports: [RouterOutlet, GlobalFooterModule, NzIconModule, ThemeBtnComponent]
})
export class LayoutPassportComponent implements OnInit {
private tokenService = inject(DA_SERVICE_TOKEN);
private readonly router = inject(Router);
@ViewChild('myVideo') myVideo: any;
links = [
{
title: '帮助',
href: ''
},
{
title: '隐私',
href: ''
},
{
title: '条款',
href: ''
}
];

ngOnInit(): void {
// **🔹 判断是否为移动端**
var ua = navigator.userAgent.toLowerCase();
var isMobile = /android|iphone|ipod|ipad|windows phone|blackberry|mobile/.test(ua);
// **🔹 进行路由跳转**
if (isMobile) {
this.router.navigateByUrl('/mobile');
}
setTimeout(() => {
const width = document.querySelector('body')?.style.width;
const height = document.querySelector('body')?.offsetHeight;
const video: any = document.querySelector('.video');
video.style.width = width;
video.style.height = `${height}px`;
console.log(width, height, 'width');
console.log(height, 'height');
}, 0);

this.tokenService.clear();
}
}

+ 11
- 0
src/app/layout/tabs/tabs.component.html 파일 보기

@@ -0,0 +1,11 @@
<nz-tabset
class="tabs tab-box"
(nzSelectedIndexChange)="selectTab($event)"
[(nzSelectedIndex)]="selectedIndex"
(nzClose)="closeTab($event)"
nzType="editable-card"
>
<nz-tab *ngFor="let tab of tabs; let i = index" nzClosable [nzTitle]="tab.title" />
</nz-tabset>
<!-- <img width="16" class="zuo" src="../../../assets/common/zuo.svg" alt="" (click)="nav('jian')">
<img width="16" class="you" src="../../../assets/common/you.svg" alt="" (click)="nav('add')"> -->

+ 70
- 0
src/app/layout/tabs/tabs.component.less 파일 보기

@@ -0,0 +1,70 @@
.tabs {
width: 100%;
right: 8px;
}

::ng-deep .ant-tabs-nav-add {
display: none;
}

::ng-deep #nz-tabs-0-tab-0 {
button {
display: none;
}
}

::ng-deep .ant-tabs-card>.ant-tabs-nav .ant-tabs-tab-active,
.ant-tabs-card>div>.ant-tabs-nav .ant-tabs-tab-active {
background-color: rgb(24, 144, 255) !important;
}

::ng-deep .ant-tabs-nav-list {

.ant-tabs-tab {
&:first-child {
svg {
display: none !important;
}
}

}

.ant-tabs-tab-remove {
right: 11px !important;
}
}

::ng-deep .ant-tabs-tab {
height: 32px;
margin-top: 3px !important;
margin-right: 6px !important;
border-radius: 3px !important;
}

::ng-deep .ant-tabs>.ant-tabs-nav .ant-tabs-nav-wrap,
.ant-tabs>div>.ant-tabs-nav .ant-tabs-nav-wrap {
height: 40px;
}


.zuo {
position: absolute;
top: 11px;
left: 12px;
z-index: 1111;
cursor: pointer;
}

.you {
position: absolute;
top: 11px;
right: 16px;
z-index: 1111;
cursor: pointer;
}

:host {
::ng-deep .ant-tabs-nav-wrap {
padding-left: 8px !important;
}
}

+ 325
- 0
src/app/layout/tabs/tabs.component.ts 파일 보기

@@ -0,0 +1,325 @@
import { Component, OnInit, inject, Input, ViewChild, AfterViewInit, ElementRef, HostListener } from '@angular/core';
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { ALLOW_ANONYMOUS, DA_SERVICE_TOKEN } from '@delon/auth';
import { I18nPipe, MenuService, SettingsService, User } from '@delon/theme';
import { SharedModule } from '@shared';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzTabSetComponent } from 'ng-zorro-antd/tabs';
import { forkJoin } from 'rxjs';

import { EventBusService } from '../../shared/event-bus.service';

const TABS_STORAGE_KEY = 'openedTabs';
const SELECTED_INDEX_STORAGE_KEY = 'selectedTabIndex';

@Component({
selector: 'tabs',
templateUrl: './tabs.component.html',
styleUrls: ['./tabs.component.less'],
standalone: true,
imports: [SharedModule, NzIconModule]
})
export class TabsComponent implements OnInit, AfterViewInit {
private menuService = inject(MenuService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private tokenService = inject(DA_SERVICE_TOKEN);

tabs: any[] = [];
selectedIndex: number = 0; // 默认选中第一个标签
@ViewChild('tabSet', { static: false }) tabSet!: NzTabSetComponent;
@ViewChild('tabSet', { read: ElementRef, static: false }) tabSetElement!: ElementRef;
ngAfterViewInit() {
this.checkTabWidth();
}
onResize() {
this.checkTabWidth();
}
private checkTabWidth() {
if (this.tabSetElement) {
const tabSetNativeElement = this.tabSetElement.nativeElement;
const tabsContainer = tabSetNativeElement.querySelector('.ant-tabs-nav-scroll');
if (tabsContainer) {
const scrollWidth = tabsContainer.scrollWidth;
const clientWidth = tabsContainer.clientWidth;
this.showArrows = scrollWidth > clientWidth;
}
}
}
showArrows = false;

private readonly eventBusService = inject(EventBusService);
ngOnInit() {
this.loadTabsFromStorage();
this.initTabs();
this.setupRouteListener();
this.eventBusService.closeTab$.subscribe(url => {
let newUrl = url.split('?')[0];
const index = this.tabs.findIndex(tab => {
console.log(tab.path, '=====', url, 'zz');
return tab.path.indexOf(newUrl) !== -1;
});
console.log(index, 'hanxiaosong');
if (index > -1) {
this.closeTab({ index });
}
});
}

/**
* 从 sessionStorage 加载页签信息
*/
private loadTabsFromStorage() {
const savedTabs = sessionStorage.getItem(TABS_STORAGE_KEY);
const savedIndex = sessionStorage.getItem(SELECTED_INDEX_STORAGE_KEY);

if (savedTabs) {
this.tabs = JSON.parse(savedTabs);
} else {
this.tabs = [{ title: '首页', path: '/' }];
}

if (savedIndex) {
this.selectedIndex = parseInt(savedIndex, 10);
} else {
this.selectedIndex = 0;
}
}
addTab(title: string, route: string, params?: any): void {
const [basePath, existingQueryString] = route.split('?');
const newQueryParams = params ? new URLSearchParams(params).toString() : '';
const fullPath = existingQueryString
? `${basePath}?${existingQueryString}${newQueryParams ? `&${newQueryParams}` : ''}`
: newQueryParams
? `${basePath}?${newQueryParams}`
: basePath;

const existingTab = this.tabs.find(tab => {
const [tabBasePath, tabQueryString] = tab.path.split('?');
return tabBasePath === basePath && tabQueryString === newQueryParams;
});

if (!existingTab) {
// 直接使用原始标题,移除详情和审批逻辑
this.tabs.push({ title, path: fullPath });
this.navigateToTab(this.tabs.length - 1);
} else {
this.navigateToTab(this.tabs.indexOf(existingTab));
}
this.saveTabsToStorage();
this.checkTabWidth(); // 检查页签宽度
}

closeTab(obj: any): void {
if (this.tabs.length > 1) {
const removedIndex = obj.index;
this.tabs.splice(removedIndex, 1);

if (removedIndex <= this.selectedIndex) {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
}

this.navigateToTab(this.selectedIndex);
} else {
this.router.navigate(['/home']);
}
this.saveTabsToStorage();
this.checkTabWidth(); // 检查页签宽度
}

/**
* 保存页签信息到 sessionStorage
*/
private saveTabsToStorage() {
let token: any = this.tokenService.get();
if (Object.keys(token).length) {
sessionStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(this.tabs));
sessionStorage.setItem(SELECTED_INDEX_STORAGE_KEY, this.selectedIndex.toString());
} else {
sessionStorage.removeItem(TABS_STORAGE_KEY);
sessionStorage.removeItem(SELECTED_INDEX_STORAGE_KEY);
}
}

/**
* 初始化页签
*/
private initTabs() {
this.handleCurrentRoute();
this.hideHomeTabCloseButton();
}

/**
* 处理当前路由
*/
private handleCurrentRoute() {
if (this.router.url !== '/') {
let token: any = this.tokenService.get();
if (Object.keys(token).length) {
const menu = this.findNodeByPath(this.menuService['data'], this.router.url);
if (menu) {
this.addTabIfNotExists(menu);
} else {
console.log('未找到匹配的菜单项,跳转到登录页');
this.router.navigateByUrl('/passport/login');
}
}
}
}

/**
* 隐藏首页页签的关闭按钮
*/
private hideHomeTabCloseButton() {
if (this.router.url === '/') {
const node: any = document.querySelector('.ant-tabs-tab-remove');
if (node) {
node.style.display = 'none';
}
}
}

/**
* 设置路由监听器
*/
private setupRouteListener() {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
const menu = this.findNodeByPath(this.menuService['data'], event.url);
if (menu) {
this.addTabIfNotExists(menu);
}
// this.saveTabsToStorage();
}
});
}

/**
* 通过路径查找菜单项节点
* @param data 菜单数据数组
* @param path 要查找的路径,可能包含路由参数
* @returns 匹配的菜单项节点,如果未找到则返回 null
*/
findNodeByPath(data: any[] = [], path: string) {
const [basePath, queryString] = path.split('?');
const queryParams = queryString ? new URLSearchParams(queryString) : null;

for (const item of data) {
const itemBasePath = item.link?.split('?')[0];
if (itemBasePath === basePath) {
const matchedItem = { ...item };
if (queryParams) {
matchedItem.queryParams = {};
for (const [key, value] of queryParams.entries()) {
matchedItem.queryParams[key] = value;
}
}
return matchedItem;
} else if (item.children && item.children.length > 0) {
const result: any = this.findNodeByPath(item.children, path);
if (result) {
return result;
}
}
}
return null;
}

/**
* 如果页签不存在则添加
* @param menu 菜单项
*/

private addTabIfNotExists(menu: any) {
const fullPath = this.buildFullPath(menu);
const [basePath, queryString] = fullPath.split('?');

const existingTab = this.tabs.find(tab => {
const [tabBasePath, tabQueryString] = tab.path.split('?');
return tabBasePath === basePath && tabQueryString === queryString;
});

if (!existingTab) {
// 直接使用菜单名称,移除详情和审批逻辑
this.tabs.push({ title: menu.menuName, path: fullPath });
this.selectedIndex = this.tabs.length - 1;
} else {
this.selectedIndex = this.tabs.indexOf(existingTab);
}
}

/**
* 构建完整路径,包含查询参数
* @param menu 菜单项
* @returns 完整路径
*/
private buildFullPath(menu: any) {
let fullPath = menu.link;
if (menu.queryParams) {
const queryString = Object.entries(menu.queryParams)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`)
.join('&');
fullPath += fullPath.includes('?') ? `&${queryString}` : `?${queryString}`;
}
return fullPath;
}

/**
* 添加页签
* @param title 页签标题
* @param route 路由路径
* @param params 路由参数
*/

/**
* 导航到指定页签
* @param index 页签索引
*/
navigateToTab(index: number): void {
const tab = this.tabs[index];
console.log(tab, 'tab');

let path = tab.path;
let queryParams = {};

if (tab.path.includes('?')) {
const [basePath, queryString] = tab.path.split('?');
path = basePath;
queryParams = Object.fromEntries(new URLSearchParams(queryString).entries());
}

this.router.navigate([path], { queryParams });
}

/**
* 选择页签
* @param index 页签索引
*/
selectTab(index: number): void {
this.selectedIndex = index;
this.navigateToTab(index);
// this.saveTabsToStorage();
}

/**
* 导航到相邻页签
* @param value 'add' 表示下一个页签,其他值表示上一个页签
*/
nav(value: string) {
if (value === 'add' && this.selectedIndex < this.tabs.length - 1) {
this.selectedIndex++;
} else if (value !== 'add' && this.selectedIndex > 0) {
this.selectedIndex--;
}
this.navigateToTab(this.selectedIndex);
// this.saveTabsToStorage();
}

// 新增方法:退出登录时清空 sessionStorage
clearTabsOnLogout() {
sessionStorage.removeItem(TABS_STORAGE_KEY);
sessionStorage.removeItem(SELECTED_INDEX_STORAGE_KEY);
this.tabs = [{ title: '首页', path: '/' }];
this.selectedIndex = 0;
}
}

+ 111
- 0
src/app/layout/trad/trad.component.html 파일 보기

@@ -0,0 +1,111 @@
<layout-default [options]="options" [nav]="null" [content]="contentTpl" [customError]="null">
<layout-default-header-item direction="left">
<span class="title">{{ title }}</span>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<header-fullscreen style="margin-right: 32px" />
</layout-default-header-item>
<layout-default-header-item direction="right" style="vertical-align: top">
<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>
<div class="tab-pane">
<tabs />
</div>
<!-- 应用菜单 -->
<div class="system">
<div class="logo">
<img style="width: 50px; height: 50px" src="../../../assets/logo.png" alt="" />
<!-- <img width="60" src="../../../assets/common/auseft.png" alt=""> -->
</div>
<div class="system-item">
<i class="oprateLeft material-icons menuCollapsed" (click)="collapsed()">menu</i>
</div>
<div class="primary-menu">
<div class="system-li" title="化验数据录核" [class.activeItem]="activeValue == 1" (click)="navigateToLaboratoryWithParams()">
<i class="material-icons">home</i>
</div>
<!-- <div class="system-li" title="燃料" (click)="toggleActive(2)" [class.activeItem]="activeValue==2">
<i class="material-icons">palette</i>
</div>
<div class="system-li" title="燃料" (click)="toggleActive(3)" [class.activeItem]="activeValue==3">
<i class="material-icons">account_balance_wallet</i>
</div>
<div class="system-li" title="燃料" (click)="toggleActive(4)" [class.activeItem]="activeValue==4">
<i class="material-icons">image</i>
</div> -->
<div class="set">
<div layout-default-header-item-trigger nz-dropdown [nzDropdownMenu]="settingsMenu" nzPlacement="bottomRight">
<i class="material-icons" style="font-size: 24px !important; color: rgb(166, 178, 178)">settings</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 class="theme" nz-menu-item [class.active]="theme === 'dark'" (click)="switchTheme('dark')">黑色主题</div>
<div class="theme" nz-menu-item [class.active]="theme === 'white'" (click)="switchTheme('white')">白色主题</div>
</div>
</nz-dropdown-menu>
</div>
</div>
</div>
<layout-default-header-item class="head-right" direction="right">
<header-date />
</layout-default-header-item>
<layout-default-header-item direction="right">
<div data-tauri-drag-region class="titlebar" id="titlebar" [hidden]="!isTauri">
<div class="titlebar-button" id="titlebar-minimize" (click)="minimize()">
<img src="../../../assets/window/minsize.svg" alt="minimize" />
</div>
<div class="titlebar-button" id="titlebar-maximize" (click)="toggleMaximize()" [hidden]="!isMax">
<img src="../../../assets/window/window-window_line.svg" alt="maximize" />
</div>
<div class="titlebar-button" id="titlebar-maximize" (click)="toggleMaximize()" [hidden]="isMax">
<img src="../../../assets/window/max-size.svg" alt="maximize" />
</div>
<div class="titlebar-button" id="titlebar-close" (click)="close()">
<img src="../../../assets/window/Close.svg" alt="close" />
</div>
</div>
</layout-default-header-item>
<layout-default-header-item direction="right">
<div data-tauri-drag-region class="titleDragger" id="titlebar" [hidden]="!isTauri"> </div>
</layout-default-header-item>
<ng-template #contentTpl>
<div class="content-container">
<router-outlet />
</div>
</ng-template>
<!-- 固定面包屑导航 -->
<nz-card class="breadcrumb fixed-breadcrumb">
<nz-breadcrumb [nzSeparator]="null">
<nz-breadcrumb-item *ngFor="let menu of breadcrumb">
<span *ngIf="menu.menuName === '智慧燃料'">
<i class="material-icons first-name">home</i><span>{{ menu.menuName }}</span></span
>
<span *ngIf="menu.menuName !== '智慧燃料'"> {{ menu.menuName }}</span>
<nz-breadcrumb-separator>&gt;</nz-breadcrumb-separator>
</nz-breadcrumb-item>

<nz-breadcrumb-item *ngIf="activeRoute" class="activeMenu">{{ activeRoute.menuName }}</nz-breadcrumb-item>
</nz-breadcrumb>
</nz-card>
</layout-default>

+ 803
- 0
src/app/layout/trad/trad.component.less 파일 보기

@@ -0,0 +1,803 @@
// 系统名字样式
::ng-deep .sys_title {
color: #fff;
font-size: 18px;
line-height: 52px;
height: 52px;
position: absolute;
top: 0px;
left: 58px;
padding-left: 16px;
font-weight: bold;
width: 220px;
background-color: rgb(0, 33, 64) !important;
visibility: hidden;
}

::ng-deep .add {
margin-top: -38px;
}

::ng-deep .card-search {
margin-bottom: 6px !important;
}

::ng-deep .ant-modal-body {
// padding: 24px 8px !important;
padding: 0 8px!important;
}

::ng-deep .ant-form-item-label>label.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
position: absolute;
right: -7px;
top: 11px;
}



::ng-deep .anticon-fullscreen {
cursor: pointer;

svg {
fill: #fff !important;
}
}

::ng-deep .anticon-fullscreen-exit {
cursor: pointer;

svg {
fill: #fff !important;
}
}

::ng-deep a.sidebar-nav__item-link {
color: #a5acb3;
}

:ng-deep .container-block .card-block {
margin-bottom: 8px;
}

::ng-deep .ant-breadcrumb .ant-breadcrumb-link {
color: #666;
}



.oprateLeft {
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.title {
position: absolute;
top: 10px;
font-size: 20px;
font-weight: bold;
margin: auto;
display: block;
height: 50px;
width: 30%;
text-align: center;
margin: auto;
left: 0;
right: 0;
}

// 设置
.set {
position: absolute;
bottom: 10px;
left: 5px;
}

.active {
color: rgb(19, 194, 194);
}

.theme {
padding-left: 30px;
}

::ng-deep .alain-default__header {
box-shadow: none !important;
height: 52px;
}

::ng-deep .ant-card-bordered {
border: 0px solid rgba(18, 189, 189, 1);

}

::ng-deep .sidebar-nav__group-title {
margin-left: -8px;
background-color: rgba(100, 255, 255, 0.3);
color: #fff;
font-size: 16px;
}

::ng-deep .alain-default__fixed .alain-default__aside {
background-color: rgb(0, 33, 64) !important;
transform: translate3d(0%, 0, 0) !important;
border: 0px solid rgba(18, 189, 189, 1);
height: calc(100% - 52px);
margin-top: 52px !important;
width: 218px;
margin-left: 60px;

.arrow {
position: absolute;
right: 0;
/* 使它紧贴父级右边缘 */
top: 0;
/* 从顶部开始 */
bottom: 0;
/* 高度等于父级 */
width: 3px;
/* 宽度设为1px */
// background-color: #215C6F;
background-color: transparent !important;
/* 将背景色保留为深蓝色 */
z-index: 111111;
/* 保持较高的层级 */
cursor: ew-resize;
/* 保持鼠标样式为左右拖动 */
height: 100%;
}
}

// ::ng-deep .sidebar-nav__selected
::ng-deep .sidebar-nav__selected {
border: none;
background: none !important;

}

::ng-deep .sidebar-nav__selected>.sidebar-nav__item-link {
color: #fff !important;
}

::ng-deep .dis-flex {
height: calc(100% - 48px);
overflow: auto;
}

::ng-deep .def-cont-height {
height: 100%;
overflow: auto;
}

::ng-deep .alain-default {
margin-top: 0px;


.page-header {
margin-left: -16px;
margin-right: 0;
}

}

::ng-deep .breadcrumb {

.ant-card-body {
padding: 6px 16px 6px 16px;
}

}

::ng-deep .ant-table-tbody>tr>td {
background-color: rgb(0, 33, 64);
padding: 4px 16px;
}

::ng-deep .ant-table-thead {
tr {
th {
padding: 4px 16px;
}
}
}

::ng-deep .ant-card {
background-color: rgb(0, 33, 64) !important;
border-radius: 4px !important;
}


::ng-deep .ant-btn {
height: 28px !important;
line-height: 19px !important;
box-shadow: none !important;
text-shadow: none !important;
border-radius: 4px !important;
}

::ng-deep {
.card-search {

.ant-select-single.ant-select-show-arrow .ant-select-selection-item,
.ant-select-single.ant-select-show-arrow .ant-select-selection-placeholder {
line-height: 27px;
}

.ant-select:not(.ant-select-customize-input) .ant-select-selector {

height: 28px;
}

// 输入框
.ant-input {
height: 28px;
}


}
}



// 状态样式
::ng-deep {
.ant-tag-green {
color: #fff;
border: none;
background: none;
font-size: 14px;

&::before {
vertical-align: middle;
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: rgb(82, 196, 26);
border-radius: 6px;

}

span {
margin-left: 4px;
}
}

.ant-tag-red {
color: #fff;
border: none;
background: none;
font-size: 14px;

&::before {
vertical-align: middle;
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: red;
border-radius: 6px;

}

span {
margin-left: 4px;
}
}
}


// 状态样式
::ng-deep {
.textarea-input {
.ant-form-item-control-input {
height: auto !important;
}
}

.ant-dropdown-trigger {
border: none !important;
}

}


.fixed-breadcrumb {
background-color: rgb(0, 33, 64);
position: absolute;
left: 279px;
// bottom: -5px;
width: 100%;
z-index: 9;
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
}

.content-container {
z-index: 1999;
height: 100%;
}

::ng-deep .ant-tabs-nav {
margin-bottom: 0px !important;
}

::ng-deep .ant-form {
padding-top: 6px;
// width: 100% !important;
}

//穿透表单
::ng-deep .ant-card-body {
padding: 0px 0px;
}

//穿透搜索栏)
::ng-deep .sf__inline .ant-form-inline>.sf__item {
margin: 0 10px;
margin-bottom: 0;
}

/* 设置滚动条的宽度 */
::ng-deep .ant-table-content::-webkit-scrollbar {
width: 1.2em;
}

/* 设置滚动条滑块的样式 */
::ng-deep .ant-table-content::-webkit-scrollbar-thumb {
// margin-left: 0px;
background-color: #1f7995;
border: 2px solid #1f7995;
border-radius: 10px;
}

/* 设置滚动条滑块悬浮时的样式 */
::ng-deep .ant-table-content::-webkit-scrollbar-thumb:hover {
// margin-left: 0px;
background-color: #1f7995;
}

/* 设置滚动条滑块激活(即正在拖动滑块)时的样式 */
::ng-deep .ant-table-content::-webkit-scrollbar-thumb:active {
background-color: #1f7995;
}

//横向
::ng-deep .ant-table-content {
scrollbar-color: auto;
}

/* 设置滚动条的宽度 */
::ng-deep .ant-table-content::-webkit-scrollbar {
height: 1.2em;
}

/* 设置横向滚动条轨道的样式 */
::ng-deep .ant-table-content::-webkit-scrollbar-track {
background-color: transparent;
border-color: #00eff8;
}

/* 设置横向滚动条滑块悬浮时的样式 */
::ng-deep .ant-table-content::-webkit-scrollbar-thumb:hover {
background-color: #1f7995;
}

/* 设置横向滚动条滑块激活时的样式 */
::ng-deep .ant-table-content::-webkit-scrollbar-thumb:active {
background-color: #1f7995;
}

/* 设置横向滚动条滑块的颜色 */
::ng-deep .ant-table-content::-webkit-scrollbar-thumb {
background-color: #1f7995;
}
::ng-deep .ant-table-fixed-header{
.ant-table-body{
overflow-y:auto !important;
}
}
::ng-deep .alain-default__nav {
position: static !important;

li {
&:first-child {
margin-left: 140px;
}
}
}



//系统菜单
.system {
width: 60px;
height: 100%;
position: absolute;
background-color: rgb(0, 33, 64);
z-index: 9;
top: 0;
left: 0;
border-right: 1px solid rgba(255, 255, 255, 0.1);

.logo {
height: 52px;
color: #fff;
line-height: 50px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);

}

.activeItem {
background: linear-gradient(to right, #002140, #003a70);
color: #1890ff !important;
margin: 4px;
width: 40px;
height: 40px;
border-radius: 2px;
margin: auto;
}

.menuCollapsed:hover {
background: linear-gradient(to right, #002140, #003a70);
color: #1890ff !important;
margin: 4px;
width: 40px;
height: 40px;
border-radius: 2px;
margin: auto;
}

.system-item {
height: 38px;
text-align: center;
color: rgba(255, 255, 255, 0.65);
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.activeItem::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(24, 144, 255, 0.1);
border-radius: 2px;

}

.material-icons {
line-height: inherit;
}

.system-li {
position: relative;
height: 38px;
text-align: center;
color: rgba(255, 255, 255, 0.65);
margin-top: 10px;
cursor: pointer;
}
}

.first-name {
vertical-align: middle;
line-height: inherit;
}

// 菜单页签选项卡下划线
::ng-deep .ant-tabs-top>.ant-tabs-nav::before,
.ant-tabs-bottom>.ant-tabs-nav::before,
.ant-tabs-top>div>.ant-tabs-nav::before,
.ant-tabs-bottom>div>.ant-tabs-nav::before {
border-bottom: none !important;
}



/* 设置滚动条的宽度 */
.tab-pane::-webkit-scrollbar {
height: 6px;
}

/* 设置横向滚动条轨道的样式 */
.tab-pane::-webkit-scrollbar-track {
background-color: transparent;
border-color: #00eff8;
}

/* 设置横向滚动条滑块悬浮时的样式 */
.tab-pane::-webkit-scrollbar-thumb:hover {
background-color: #1f7995;
}

/* 设置横向滚动条滑块激活时的样式 */
.tab-pane::-webkit-scrollbar-thumb:active {
background-color: #1f7995;
}

/* 设置横向滚动条滑块的颜色 */
.tab-pane::-webkit-scrollbar-thumb {
background-color: #1f7995;
}

::ng-deep .alain-default__nav-wrap {
height: 54px !important;
}



.branch {

img {
width: 30px;
}
}

nz-header {
position: absolute;
}

::ng-deep .alain-default {
margin: 0 auto;
position: relative;
}

::ng-deep .sidebar-nav__item {
border: none !important;
}

// 顶部区域
::ng-deep .alain-default__fixed .alain-default__header {
position: static;
width: calc(100vw);
border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
background-color: rgb(0, 33, 64) !important;
}

// 侧边栏菜单
::ng-deep .alain-default__fixed .alain-default__aside {
position: absolute !important;
}

::ng-deep .alain-default__aside::after {
display: none;
}

// 菜单箭头
::ng-deep .sidebar-nav__item-icon {
vertical-align: middle;
}

::ng-deep .sidebar-nav__sub-arrow::before {
content: "";
position: absolute;
display: none;
}

::ng-deep .sidebar-nav__open>.sidebar-nav__item-link .sidebar-nav__sub-arrow::before {
transform: rotate(180deg);
}

::ng-deep .sidebar-nav__sub-arrow::after {
content: "";
position: absolute;
display: none;

}

::ng-deep .sidebar-nav__sub-arrow::before {
transform: rotate(90deg);
margin-top: -5px;
}

::ng-deep .sidebar-nav__sub-arrow::before {
display: block;
// transform: none;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #a5acb3;
background: none;
transform-origin: center center;

}

::ng-deep .alain-default__content {
position: static !important;
margin-left: 284px !important;
margin-top: 46px !important;
margin-right: 6px !important;
height: calc(100% - 148px);
overflow-y: auto;
overflow-x: hidden;
border-radius: 5px;
// width: calc(100vw - 287px) !important;
}

::ng-deep .alain-default__content::-webkit-scrollbar {
width: 8px;
/* 滚动条宽度 */
height: 8px;
/* 如果是水平滚动条可设置宽度,垂直滚动条可设置高度,根据实际需求调整 */
}

::ng-deep .alain-default__content::-webkit-scrollbar-track {
background-color: transparent;
/* 滚动条轨道背景色 */
border-radius: 4px;
/* 轨道边角圆滑处理 */
}

::ng-deep .alain-default__content::-webkit-scrollbar-thumb {
background-color: #24c8db;
/* 滚动条滑块的背景色 */
border-radius: 4px;
/* 滑块边角圆滑 */
transition: background-color 0.3s ease;
/* 添加过渡效果,鼠标悬停时变色更平滑 */
}

::ng-deep .alain-default__content::-webkit-scrollbar-thumb:hover {
background-color: #24c8db;
/* 鼠标悬停在滑块上时改变的背景色 */
}

::ng-deep .alain-default {
height: 100% !important;
}

::ng-deep .ant-table-tbody>tr.ant-table-row:hover>td,
.ant-table-tbody>tr>td.ant-table-cell-row-hover {
background-color: rgba(0, 0, 0, 0.8) !important;
}

// 页签
.tab-pane {
z-index: 9;
top: 52px;
left: 278px;
right: 0px;
// width: calc(100% - 279px);
// display: grid;
align-content: space-between;
// overflow-x: auto;
// overflow-y: hidden;
// overflow:hidden;
}

.tab-pane {
position: absolute;
// padding-left: 32px;
// padding: 0 30px;
background-color: rgb(0, 33, 64);

::ng-deep .ant-tabs-nav {
width: 100%;

.ant-tabs-nav-wrap {
width: calc(100% - 565px);
}
}
}



::ng-deep .ant-tabs-tab:hover {
color: #fff !important;
background-color: rgb(24, 144, 255) !important;
}

::ng-deep .ant-tabs-card.ant-tabs-top>.ant-tabs-nav .ant-tabs-tab-active,
.ant-tabs-card.ant-tabs-top>div>.ant-tabs-nav .ant-tabs-tab-active {
border-bottom-color: #303030 !important;
}

::ng-deep .ant-tabs-nav .ant-tabs-tab-active {
color: #fff !important;
padding-right: 12px !important;
}

::ng-deep .tab-pane .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: #fff;
}

::ng-deep .sidebar-nav__item-link:hover {
color: #fff;
}

::ng-deep .sidebar-nav__open {

>a.sidebar-nav__item-link {
color: #fff;
}

}

::ng-deep li.sidebar-nav__open {
background: #003a70;
}

::ng-deep .sidebar-nav__open>.sidebar-nav__sub {
background-color: #000;
}

::ng-deep .sidebar-nav__open>a.sidebar-nav__item-link {
color: #fff;
}

::ng-deep .sidebar-nav__sub-arrow {
transform: translateY(0px) !important;
}

@media (min-width: 768px) {
::ng-deep .alain-default__collapsed .sidebar-nav:not(.sidebar-nav__sub) .sidebar-nav__item-link {
display: block !important;
padding: 8px 32px 8px 16px !important;
}

::ng-deep .alain-default__collapsed .sidebar-nav:not(.sidebar-nav__sub) .sidebar-nav__item-link .sidebar-nav__item-text {
opacity: 1 !important;
display: inline !important;
color: rgb(165, 172, 179) !important;
}

::ng-deep .alain-default__collapsed .sidebar-nav:not(.sidebar-nav__sub) .sidebar-nav__item-link .sidebar-nav__item-icon {
margin-right: 8px !important;
font-size: 14px !important;
}

::ng-deep .alain-default__collapsed .alain-default__content {
margin-left: 125px;
}
}


.titlebar {
height: 30px;
// user-select: none;
display: flex;
justify-content: flex-end;
padding-left: 15px;

top: 0;
left: 0;
right: 0;
z-index: 1000;
}

.titleDragger {

height: 30px;
// user-select: none;
display: flex;
justify-content: flex-end;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1001;
width: 200px;
left: 50%;
margin-left: -100px;
}

.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
user-select: none;
-webkit-user-select: none;
}

.titlebar-button img {
font-size: 14px;
width: 18px;
height: 18px;
}


+ 475
- 0
src/app/layout/trad/trad.component.ts 파일 보기

@@ -0,0 +1,475 @@
import { Location } from '@angular/common';
import { Component, inject, HostListener, ChangeDetectorRef, OnInit } from '@angular/core';
import { Router, RouterLink, RouterOutlet, ActivatedRoute, NavigationEnd } from '@angular/router';
import { QuickMenuModule } from '@delon/abc/quick-menu';
import { I18nPipe, MenuService, SettingsService, User, _HttpClient } 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 { SharedModule } from '@shared';
import { isTauri } from '@tauri-apps/api/core';
import { Window } from '@tauri-apps/api/window';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzBreadCrumbComponent, NzBreadCrumbItemComponent, NzBreadCrumbSeparatorComponent } from 'ng-zorro-antd/breadcrumb';
import { NzCardComponent } from 'ng-zorro-antd/card';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzMenuModule } from 'ng-zorro-antd/menu';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzTableComponent } from 'ng-zorro-antd/table';

import { HeaderClearStorageComponent } from '../basic/widgets/clear-storage.component';
import { HeaderDateComponent } from '../basic/widgets/date.component';
import { HeaderFullScreenComponent } from '../basic/widgets/fullscreen.component';
import { HeaderUserComponent } from '../basic/widgets/user.component';
import { TabsComponent } from '../tabs/tabs.component';

@Component({
selector: 'layout-trad',
templateUrl: './trad.component.html',
styleUrls: ['./trad.component.less'],
standalone: true,
imports: [
RouterOutlet,
RouterLink,
I18nPipe,
LayoutDefaultModule,
SettingDrawerModule,
NzSelectModule,
ThemeBtnComponent,
NzIconModule,
NzMenuModule,
NzDropDownModule,
NzAvatarModule,
HeaderUserComponent,
HeaderClearStorageComponent,
HeaderFullScreenComponent,
NzLayoutModule,
HeaderDateComponent,
QuickMenuModule,
NzBreadCrumbComponent,
NzBreadCrumbItemComponent,
NzBreadCrumbSeparatorComponent,
NzCardComponent,
TabsComponent,
SharedModule
]
})
export class LayoutTradComponent implements OnInit {
private readonly cdr = inject(ChangeDetectorRef);
private readonly settings = inject(SettingsService);
private menuService = inject(MenuService);
private readonly http = inject(_HttpClient);
url = `${environment.api['fuelUrl']}system/config/configKey/pcSystemTitile`;
options: LayoutDefaultOptions = {
logoExpanded: `./assets/datav/trn.png`,
logoCollapsed: `./assets/datav/trn.png`,
showHeaderCollapse: false
};
searchToggleStatus = false;
currentOrgan: any;
organs: any;
showSettingDrawer = !environment.production;
tabs: any[] = []; // 页签数据
theme: string = 'white';
breadcrumb: any[] = [];
activeRoute: any;
sidebarWidth: number = 200;

appWindow = new Window('main');
isTauri = false;
isMax = true;
title = '';
private mouseDown = false;
private mouseX = 0;
private sidebarInitialWidth = 0;

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

switchTheme(theme: string) {
this.theme = theme;
document.body.className = theme;
window.localStorage.setItem('theme', theme);
}
getMenu() {
return this.menuService.menus;
}
add() {
console.log(11);
}
activeValue = 1;
toggleActive(value: number) {
console.log('Toggle active called with:', value);
this.activeValue = value;
this.router.navigate(['/hy/laboratory-data-entry-1']);
}

onNavigateClick() {
console.log('Navigation link clicked!');
this.activeValue = 1;
}

navigateToLaboratoryWithParams() {
console.log('Navigating to laboratory with inputConfig parameters...');
this.activeValue = 1;

// 直接拼接URL字符串,绕过Angular路由的查询参数处理
const url = '/hy/laboratory-data-entry-1?inputConfig=100:true,300:false,400:false,500:false,600:false,700:true';
window.location.hash = `#${url}`;
}
constructor(
private route: ActivatedRoute,
private router: Router,
private location: Location,
private menuS: MenuService,
private settingsService: SettingsService
) {
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;
}

this.obtainIsTauri();
}
// 获取侧边栏折叠状态
getSidebarCollapsed() {
return this.settingsService.layout.collapsed;
}

obtainIsTauri() {
try {
this.isTauri = isTauri();
} catch {
this.isTauri = false;
}
}

findMenuDetails(menus: any, path: any) {
let result = null;

for (let menu of menus) {
if (menu.link === path) {
// 当前菜单就是我们要找的,返回结果
return {
menu: menu,
parent: null,
ancestors: []
};
}
if (menu.children && menu.children.length > 0) {
// 递归查找子菜单
result = this.findMenuInChildren(menu, path);
if (result) {
// 子菜单中找到了匹配的路径,返回结果
result.parent = menu;
result.ancestors.unshift(menu);
return result;
}
}
}
// 没有找到匹配的菜单
return null;
}
findMenuInChildren(parentMenu: any, path: any) {
for (let child of parentMenu.children) {
if (child.link === path) {
// 当前子菜单就是我们要找的,返回结果
return {
menu: child,
parent: null,
ancestors: []
};
}
if (child.children && child.children.length > 0) {
// 递归查找孙子菜单
const result: any = this.findMenuInChildren(child, path);
if (result) {
// 子孙菜单中找到了匹配的路径,返回结果
result.parent = child;
result.ancestors.unshift(child);
return result;
}
}
}
// 没有找到匹配的菜单
return null;
}

minimize() {
this.appWindow.minimize();
}
toggleMaximize() {
this.appWindow.isMaximized().then(p => {
this.isMax = !p;
});
this.appWindow.toggleMaximize();
}
close() {
this.appWindow.close();
}
findNodeByPath(data: any, path: any) {
for (let item of data) {
if (item.link === path) {
return item;
} else if (item.children && item.children.length > 0) {
let result: any = this.findNodeByPath(item.children, path);
if (result) {
return result;
}
}
}
return null; // 如果没有找到匹配的路径,返回null
}

addNode() {
let parent: any = document.querySelector('.alain-default__aside');

// 创建一个新的子元素
const childElement = document.createElement('div');

// 添加class属性
childElement.className = 'arrow';
console.log(this.theme, 'this.theme');

// 添加文本内容
// childElement.textContent = this.theme === 'white' ? '' : '>';
childElement.textContent = '';

const section: any = document.querySelector('section');
// 将子元素添加到父元素中
parent.appendChild(childElement);
let _this = this;
if (!this.getSidebarCollapsed()) {
const content: any = document.querySelector('.alain-default__content');
content.style = `width:calc(100vw - 287px) !important`;
// calc(-287px + 100vw) !important
// 定义鼠标移动事件处理函数
const mouseMoveHandler = (event: any) => {
if (_this.mouseDown) {
const delta = event.clientX - _this.mouseX;
_this.sidebarWidth = _this.sidebarInitialWidth + delta;

_this.sidebarWidth = Math.max(_this.sidebarWidth, 1); // 设置最小宽度
if (_this.sidebarWidth - 218 >= 1) {
const node: any = document.querySelector('.alain-default__aside');
node.style.width = `${_this.sidebarWidth}px`;
let tab: any = document.querySelector('.tab-pane');
setTimeout(() => {
const node1: any = document.querySelector('.content-container');
node1.style.marginLeft = `${_this.sidebarWidth - 218}px`;
tab.style.marginLeft = `${_this.sidebarWidth - 218}px`;

const node2: any = document.querySelector('.breadcrumb');
node2.style.marginLeft = `${_this.sidebarWidth - 218}px`;
}, 0);
} else {
console.log(this.sidebarWidth, 'this.sidebarWidth');
const node: any = document.querySelector('.alain-default__aside');

let tab: any = document.querySelector('.tab-pane');
const node2: any = document.querySelector('.breadcrumb');
node.style.width = `${_this.sidebarWidth}px`;
if (_this.sidebarWidth < 70) {
node.style.display = 'none';
content.style = `margin-left:65px !important`;
tab.style = `margin-left:-220px!important`;
node2.style = `margin-left:-219px!important`;
_this.sidebarWidth = 218;
_this.isCollapsed = true;
// 移除鼠标移动事件监听
document.removeEventListener('mousemove', mouseMoveHandler);
return;
}

setTimeout(() => {
const left = `margin-left:-${218 - _this.sidebarWidth}px !important;`;
// const container: any = document.querySelector('.content-container');
// container.style = left;

content.style = `margin-left:${284 - (218 - _this.sidebarWidth)}px !important`;

tab.style = left;

node2.style = left;
}, 0);
}
}
};

childElement.addEventListener(
'mousedown',
(event: any) => {
console.log(_this.sidebarInitialWidth);
const node: any = document.querySelector('.alain-default__aside');

_this.mouseDown = true;
_this.mouseX = event.clientX;
_this.sidebarInitialWidth = _this.sidebarWidth;
document.body.style.userSelect = 'none';

// 添加鼠标移动事件监听
document.addEventListener('mousemove', mouseMoveHandler);
},
false
);
}
}

@HostListener('window:mouseup', ['$event'])
onMouseUp(event: MouseEvent): void {
this.mouseDown = false;
document.body.style.userSelect = '';
}
isCollapsed = false;
private initialColWidths: number[] = []; // 用于存储初始列宽

// 记录初始列宽
private recordInitialColWidths() {
const table: any = document.querySelector('table[nz-table-content][tablelayout="fixed"]');
const content: any = document.querySelector('.alain-default__content');
if (table && content) {
const colElements = document.querySelectorAll('.ant-table-body table col');
colElements.forEach((col: any) => {
const width = col.style.width ? parseInt(col.style.width) : col.clientWidth;
this.initialColWidths.push(width);
});
}
}

// 切换侧边栏折叠状态
collapsed() {
this.isCollapsed = !this.isCollapsed;
let node: any = document.querySelector('.alain-default__fixed .alain-default__aside');
let breadcrumb: any = document.querySelector('.fixed-breadcrumb');
let tabPane: any = document.querySelector('.tab-pane');
let content: any = document.querySelector('.alain-default__content');
let table: any = document.querySelector('table[nz-table-content][tablelayout="fixed"]');

if (!this.isCollapsed) {
node.style = 'width:218px !important; opacity: 1;';
tabPane.style = 'left:280px !important;width:calc(100% - 279px) !important';
content.style = 'margin-left:284px !important;width:calc(100vw - 287px) !important';
breadcrumb.style = 'left:279px !important;';
} else {
node.style = 'width:0 !important;pointer-events: none;';
tabPane.style = 'left:62px !important;width:calc(100% - 61px)';
breadcrumb.style = 'left:62px !important;';
content.style = 'margin-left:65px !important;width:calc(100vw - 65px) !important';
}

// 侧边栏过渡动画结束事件处理
const handleTransitionEnd = () => {
if (table && content) {
setTimeout(() => {
const colElements = document.querySelectorAll('.ant-table-body table col');
const colCount = colElements.length;

if (colCount > 0) {
if (this.isCollapsed) {
// 折叠时使用初始列宽
colElements.forEach((col: any, index: number) => {
const width = this.initialColWidths[index];
col.style.width = `${width}px`;
col.style.minWidth = `${width}px`;
});
} else {
// 展开时重新计算列宽
const parentWidth = content.getBoundingClientRect().width;
const availableWidth = parentWidth - (this.isCollapsed ? 65 : 284);
const newColWidth = Math.floor(availableWidth / colCount);
colElements.forEach((col: any) => {
col.style.width = `${newColWidth}px`;
col.style.minWidth = `${newColWidth}px`;
});
}
}
this.cdr.detectChanges();
}, 100);
}
// node.removeEventListener('transitionend', handleTransitionEnd);
};

// node.addEventListener('transitionend', handleTransitionEnd);
}

getSystemInfo() {
this.http.get(this.url).subscribe((res: any) => {
console.log(res, '222');
if (res.code == 200) {
this.title = res.msg;
}
});
}

ngOnInit() {
// 初始化时记录表格列的初始宽度
this.recordInitialColWidths();

this.settingsService.setLayout('collapsed', false);
if (this.getSidebarCollapsed()) {
let content: any = document.querySelector('.alain-default__content');
content.style.marginLeft = '526px !important';
setTimeout(() => {
let node: any = document.querySelector('.alain-default__fixed .alain-default__aside');
node.style.width = '47px';
let tabPane: any = document.querySelector('.tab-pane');
tabPane.style.left = '108px';
tabPane.style.width = 'calc(100% - 64px)';
}, 0);
const arrow: any = document.querySelector('.arrow');
arrow.removeEventListener('mousedown', () => {});
document.addEventListener('mousemove', () => {});
}
if (window.localStorage.getItem('theme')) {
let theme: any = window.localStorage.getItem('theme');
document.body.className = theme;
this.theme = theme;
} else {
document.body.className = this.theme;
window.localStorage.setItem('theme', this.theme);
}
// 给侧边栏添加子元素
setTimeout(() => {
this.addNode();
}, 0);
// 根据设备分辨率动态设置容易宽度
const html: any = document.querySelector('html');
html.style.fontSize = '16px';
document.body.style.fontSize = '16px';
const width: number = window.screen.width;

const data = this.findMenuDetails(this.menuService['data'], this.router.url);
if (data && data.ancestors) {
this.breadcrumb = data.ancestors;
}
this.activeRoute = this.findNodeByPath(this.menuService['data'], this.router.url);
// 监听URL变化
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
const data = this.findMenuDetails(this.menuService['data'], this.router.url);
if (data && data.ancestors) {
this.breadcrumb = data.ancestors;
}
this.activeRoute = this.findNodeByPath(this.menuService['data'], this.router.url);
}
});
this.getSystemInfo();
}

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

goBack(): void {
this.location.back();
}
}

+ 90
- 0
src/app/routes/approve/leave-apply/leave-apply.component.html 파일 보기

@@ -0,0 +1,90 @@
<div class="leave-apply">
<form>
<div class="form-item">
<label>申请人姓名</label>
<nz-select
[disabled]="true"
[(ngModel)]="leaveForm.applicantId"
name="applicantId"
(ngModelChange)="onUserChange($event)"
nzShowSearch
[nzDropdownMatchSelectWidth]="false"
nzPlaceHolder="请选择人员"
style="width: 100%"
>
<nz-option *ngFor="let user of users" [nzLabel]="user.nickName" [nzValue]="user.userId" />
</nz-select>
</div>
<div class="form-item">
<label>所属部门</label>
<input nz-input [(ngModel)]="leaveForm.department" name="department" placeholder="自动带出" readonly />
</div>

<div class="form-item">
<label>请假类型</label>
<nz-select [(ngModel)]="leaveForm.leaveType" name="leaveType" [nzDropdownMatchSelectWidth]="false">
<nz-option *ngFor="let type of leaveTypes" [nzLabel]="type.label" [nzValue]="type.value" />
</nz-select>
</div>
<div class="form-item">
<label>开始日期</label>
<nz-date-picker
nzShowTime
nzFormat="yyyy-MM-dd HH:mm"
[(ngModel)]="leaveForm.startTime"
name="startTime"
style="width: 100%"
(ngModelChange)="autoCalcDays()"
/>
</div>
<div class="form-item">
<label>结束日期</label>
<nz-date-picker
nzShowTime
nzFormat="yyyy-MM-dd HH:mm"
[nzDisabledDate]="disableBeforeStart"
[nzDisabledTime]="getDisabledEndTime"
[(ngModel)]="leaveForm.endTime"
name="endTime"
style="width: 100%"
(ngModelChange)="autoCalcDays()"
/>
</div>

<div class="form-item">
<label>请假天数</label>
<input nz-input [value]="leaveForm.days" name="days" placeholder="请先选择开始和结束日期" readonly style="background: #f5f5f5" />
</div>

<div class="form-item">
<label>请假原因</label>
<textarea nz-input [(ngModel)]="leaveForm.reason" name="reason" placeholder="请输入请假原因"></textarea>
</div>
<!-- <div class="form-item">
<label>当前状态</label>
<nz-select [(ngModel)]="leaveForm.status" name="status">
<nz-option *ngFor="let s of statusList" [nzLabel]="s" [nzValue]="s"></nz-option>
</nz-select>
</div> -->
<!-- <div class="form-item">
<label>当前审批部门</label>
<input nz-input [(ngModel)]="leaveForm.currentDepartment" name="currentDepartment" placeholder="请输入当前审批部门" />
</div>
<div class="form-item">
<label>当前审批人姓名</label>
<input nz-input [(ngModel)]="leaveForm.currentApproverName" name="currentApproverName" placeholder="请输入当前审批人姓名" />
</div>
<div class="form-item">
<label>当前审批人ID</label>
<input nz-input [(ngModel)]="leaveForm.currentApproverId" name="currentApproverId" placeholder="请输入当前审批人ID" />
</div> -->
<!-- 关联流程实例ID,通常只读 -->
<!-- <div class="form-item">
<label>流程实例ID</label>
<input nz-input [(ngModel)]="leaveForm.processInstanceId" name="processInstanceId" placeholder="自动生成/只读" [readonly]="true" />
</div> -->
<div class="form-actions">
<button nz-button nzType="primary" (click)="submitForm()">提交</button>
</div>
</form>
</div>

+ 51
- 0
src/app/routes/approve/leave-apply/leave-apply.component.less 파일 보기

@@ -0,0 +1,51 @@
.leave-apply {
padding: 24px;
max-width: 800px;
margin: 0 auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

h2 {
text-align: center;
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: #333;
}

.form-item {
margin-bottom: 20px;

label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #666;
}

nz-select,
nz-date-picker,
textarea {
width: 100%;
border-radius: 4px;
}

textarea {
min-height: 100px;
resize: vertical;
}
}

.form-actions {
text-align: right;
margin-top: 32px;

button {
padding: 0 24px;
height: 40px;
font-size: 14px;
border-radius: 4px;
}
}
}

+ 385
- 0
src/app/routes/approve/leave-apply/leave-apply.component.ts 파일 보기

@@ -0,0 +1,385 @@
import { CommonModule, DatePipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { environment } from '@env/environment';
import dayjs from 'dayjs';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { Subscription } from 'rxjs/internal/Subscription';
import { EventBusService } from 'src/app/shared/event-bus.service';

import { WorkflowService } from '../../wf/service/workflow.service';
import { TaskEventsService, TaskExecutedPayload } from '../../wf/task-framework/task-events.service';

@Component({
selector: 'app-leave-apply',
standalone: true,
imports: [CommonModule, FormsModule, NzInputModule, NzDatePickerModule, NzSelectModule, NzButtonModule],
templateUrl: './leave-apply.component.html',
styleUrls: ['./leave-apply.component.less']
})
export class LeaveApplyComponent implements OnInit, OnDestroy {
private taskExecuted?: Subscription;
private readonly workflowService = inject(WorkflowService);
prodefId = '请假审批流程:4:6b853493-45be-11f0-a1fd-fe22573d199d';
userUrl = `${environment.api['fuelUrl']}system/user/list`;
users: any[] = []; // 新增:人员下拉数据
// 新增注入
private readonly router = inject(Router);
// 添加工作流相关属性
workFlowParams: any = {};
leaveForm: any = {
id: null,
applicantId: '',
applicantName: '',
department: '',
leaveType: '病假',
startTime: null,
endTime: null,
days: '',
reason: '',
status: '业务部门经理审批',
currentDepartment: '业务部门',
currentApproverId: '',
currentApproverName: '',
processInstanceId: ''
};

leaveTypes = [
{ label: '病假', value: '病假' },
{ label: '事假', value: '事假' },
{ label: '年假', value: '年假' }
];
statusList = ['申请中', '部门审批', '人事审批', '已通过', '已驳回'];

private readonly route = inject(ActivatedRoute);
private readonly http = inject(HttpClient);

constructor(
private message: NzMessageService,
private taskEventsService: TaskEventsService,
private eventBusService: EventBusService
) {}
// 禁止选择今天之前的日期(精确到分钟)
disableBeforeNow = (current: Date): boolean => {
// 当前时间往后推30分钟
const limit = dayjs().add(30, 'minute').startOf('minute');
return current && dayjs(current).isBefore(limit);
};
getUser() {
// 获取用户列表
this.http.get<any>(this.userUrl, { params: { pageNum: 1, pageSize: 999 } }).subscribe(res => {
// 你接口一般是 res.data.list,如果不是请调整
this.users = res?.rows || [];
const userInfo = JSON.parse(window.sessionStorage.getItem('info') || '{}');
const userId = userInfo.user?.userId || '';
this.onUserChange(userId);
});
}
// 新增:选中人员后,自动填充部门等
onUserChange(userId: string | number) {
const user = this.users.find(u => u.userId === userId);
if (user) {
this.leaveForm.applicantName = user.nickName;
this.leaveForm.department = user.dept?.deptName || '';
this.leaveForm.applicantId = user.userId;
} else {
this.leaveForm.applicantName = '';
this.leaveForm.department = '';
}
}
// 自动计算请假天数
autoCalcDays() {
const WORK_START = '08:30';
const WORK_END = '17:30';
const WORK_MINUTES = 9 * 60;

if (!this.leaveForm.startTime || !this.leaveForm.endTime) {
this.leaveForm.days = '';
return;
}

let start = dayjs(this.leaveForm.startTime);
let end = dayjs(this.leaveForm.endTime);

if (end.isBefore(start)) {
this.leaveForm.days = '';
return;
}

let totalMinutes = 0;
let cursor = start.startOf('day');

while (cursor.isBefore(end, 'day') || cursor.isSame(end, 'day')) {
// 当天的工作时间
let dayStart = cursor.hour(8).minute(30).second(0);
let dayEnd = cursor.hour(17).minute(30).second(0);

// 请假区间与工作时间的交集
let segStart = dayStart.isAfter(start) ? dayStart : start;
let segEnd = dayEnd.isBefore(end) ? dayEnd : end;

// 计算在这一天的工作区间重叠部分
if (segEnd.isAfter(segStart)) {
totalMinutes += segEnd.diff(segStart, 'minute');
}

// 下一天
cursor = cursor.add(1, 'day');
// 把start和end移到下一个有效区间
if (cursor.isAfter(end, 'day')) break;
}

// 计算请假天数
this.leaveForm.days = (totalMinutes / WORK_MINUTES).toFixed(2);
}
getDisabledEndTime = (current: Date | Date[], partial?: 'start' | 'end') => {
if (!this.leaveForm.startTime) {
// 必须完整返回
return {
nzDisabledHours: () => [],
nzDisabledMinutes: () => [],
nzDisabledSeconds: () => []
};
}

const start = dayjs(this.leaveForm.startTime);
const curr = Array.isArray(current) ? current[0] : current;

// 仅处理选同一天
if (curr && dayjs(curr).isSame(start, 'day')) {
const startHour = start.hour();
const startMinute = start.minute();
return {
nzDisabledHours: () => Array.from({ length: 24 }, (_, i) => i).filter(h => h < 8 || h > 17),
nzDisabledMinutes: (h: number) => {
// 8点只能选30及以后
if (h === 8) return Array.from({ length: 60 }, (_, i) => (i < 30 ? i : -1)).filter(i => i !== -1);
// 17点只能选30及以前
if (h === 17) return Array.from({ length: 60 }, (_, i) => (i > 30 ? i : -1)).filter(i => i !== -1);
// 等于开始小时,只能选大于开始分钟
if (h === startHour) return Array.from({ length: 60 }, (_, i) => (i <= startMinute ? i : -1)).filter(i => i !== -1);
return [];
},
nzDisabledSeconds: () => Array.from({ length: 60 }, () => 0) // 全允许,返回[]
};
} else {
// 不同天,只允许8:30-17:30
return {
nzDisabledHours: () => Array.from({ length: 24 }, (_, i) => i).filter(h => h < 8 || h > 17),
nzDisabledMinutes: (h: number) => {
if (h === 8) return Array.from({ length: 60 }, (_, i) => (i < 30 ? i : -1)).filter(i => i !== -1);
if (h === 17) return Array.from({ length: 60 }, (_, i) => (i > 30 ? i : -1)).filter(i => i !== -1);
return [];
},
nzDisabledSeconds: () => []
};
}
};

// 结束日期不能早于开始日期
// 禁止选择开始时间之前的日期
disableBeforeStart = (current: Date): boolean => {
if (!this.leaveForm.startTime) {
const limit = dayjs().add(30, 'minute').startOf('minute');
return current && dayjs(current).isBefore(limit);
}
// 不允许小于开始日期的
return current && dayjs(current).isBefore(dayjs(this.leaveForm.startTime).startOf('day'));
};

changeTitle() {
this.taskEventsService.emitButtonTitleChanged({
excuteTitle: '提交',
excuteEnable: true,
rejectTitle: '驳回',
rejectEnable: false,
rejectToStartTitle: '驳回到起点',
rejectToStartEnable: false,
deleteTitle: '删除',
deleteEnable: false
});
}
ngOnInit(): void {
// this.route.queryParams.subscribe(params => {
// this.workFlowParams = params;
// });
// 获取id,若有,查询详情
this.changeTitle();
this.getUser();
this.route.queryParamMap.subscribe(params => {
if (params && params.get('businessId')) {
this.leaveForm.id = params.get('businessId');
this.getLeaveDetail(Number(params.get('businessId') ?? 0));
} else {
// 自动填充申请人信息
const userInfo = JSON.parse(window.sessionStorage.getItem('info') || '{}');
this.leaveForm.applicantId = userInfo.user?.userId || '';
this.leaveForm.applicantName = userInfo.user?.nickName || '';
this.leaveForm.department = userInfo.user?.deptName || '';
}
});
this.taskExecuted = this.taskEventsService.taskExecuted$.subscribe((payload: TaskExecutedPayload) => {
// payload 里有 taskId、formKey、prodefId 等
console.log('监听到待办执行:', payload);

if (payload.action === 'approve') {
this.submitForm(false);
} else {
}
});
}
ngOnDestroy() {
if (this.taskExecuted) {
this.taskExecuted.unsubscribe();
}
}
// 查询详情
getLeaveDetail(id: number) {
this.http.get<any>(`/erp/request/${id}`).subscribe(res => {
if (res.code === 200 && res.data) {
this.leaveForm = { ...res.data };
}
});
}
submitForm(complete: boolean = true): void {
// if (
// !this.leaveForm.applicantName ||
// !this.leaveForm.department ||
// !this.leaveForm.leaveType ||
// !this.leaveForm.startTime ||
// !this.leaveForm.endTime ||
// !this.leaveForm.reason
// ) {
// this.message.error('请填写完整信息');
// return;
// }

const info: any = window.sessionStorage.getItem('info');
const data: any = JSON.parse(info);

let userId = data.user.userId;

const pipe = new DatePipe('zh-CN');
// 提交前格式化
this.leaveForm.startTime = pipe.transform(this.leaveForm.startTime, 'yyyy-MM-dd HH:mm:ss');
this.leaveForm.endTime = pipe.transform(this.leaveForm.endTime, 'yyyy-MM-dd HH:mm:ss');
this.leaveForm.currentDepartment = '业务部门';

const now = pipe.transform(new Date(), 'yyyy-MM-dd HH:mm:ss'); // 当前时间格式化
// 新增 or 修改
if (!this.leaveForm.id) {
this.http.post(`${environment.api['fuelUrl']}erp/request`, this.leaveForm).subscribe((res: any) => {
if (!res || res.code === 500) {
this.message.error('提交失败');
return;
}
this.leaveForm.id = res['data'];
// 获取部门经理角色id
let department = '10105';

// 1. 找到当前申请人(userId对应)
const applicant = this.users.find(u => u.userId == this.leaveForm.applicantId);

// 2. 判断deptId是否为15
if (applicant && applicant.deptId == 18) {
department = '10104';
}
if (applicant && applicant.deptId == 30) {
department = '10105';
}
if (applicant && applicant.deptId == 29) {
department = '10105';
}
if (applicant && applicant.deptId == 3) {
department = '10105';
}
if (applicant && applicant.deptId == 24) {
department = '10105';
}

const req = {
processDefinitionId: this.prodefId,

businessVariables: {
applyUserId: userId,
businessId: this.leaveForm.id,
department: department,
personnelManager: '10105',
time: now,
type: '请假',
content: this.leaveForm.reason
},

groupCode: {
approverGroup: ['user2', 'user3'], // 示例候选人组
reviewerGroup: ['user4'] // 示例候选人组
}
};
this.workflowService.startWorkflow(this.prodefId, req).subscribe(res => {
if (res && res.code !== 500) {
// 将流程实例id放入业务中
let instanceId = res.data;
this.leaveForm.processInstanceId = instanceId;
this.submitForm(true);

//执行完成步骤
if (complete) {
this.completeWorkflow();
}
} else {
}
});
});
} else {
this.http.put(`${environment.api['fuelUrl']}erp/request`, this.leaveForm).subscribe((res: any) => {
if (!res || res.code === 500) {
this.message.error('修改失败');
return;
}

if (complete) {
this.taskEventsService.emitTaskFinished({
taskId: this.workFlowParams.taskId,
action: 'approve', // 或 'reject'
prodefId: this.workFlowParams.prodefId,
params: {} // 可选,传递附加信息
});
}
});
}
}
completeWorkflow() {
const userInfo = JSON.parse(window.sessionStorage.getItem('info') || '{}');
const userId = userInfo.user?.userId || '';
// 为每个hyCode构建工作流参数
const workflowParams = {
identifierValue: this.leaveForm.id, //申请id
identifierKey: 'businessId',
// formKey: (window.location.hash).split('?')[0].replace(/^#/, '')
formKey: this.router.url
};
this.workflowService.completeWorkflowV2(this.workFlowParams.prodefId, this.workFlowParams.taskId, workflowParams).subscribe({
next: () => {
// this.message.success('流程完成成功');
this.message.success('提交成功');
this.goBack();
},
error: () => {
this.message.error('提交失败');
}
});
}
goBack(): void {
setTimeout(() => {
const currentUrl = this.router.url;
this.eventBusService.emitCloseTab(currentUrl);
this.router.navigate(['/approve/own-leave-request-list'], { replaceUrl: true });
}, 1000); // 2000毫秒=2秒
}
}

+ 64
- 0
src/app/routes/approve/leave-request-detail/leave-request-detail.component.html 파일 보기

@@ -0,0 +1,64 @@
<div class="manager-approve-detail">
<div class="detail-container">
<!-- <div class="detail-item">
<label>申请人ID:</label>
<span>{{ leaveRequest.applicantId }}</span>
</div> -->
<div class="detail-item">
<label>申请人姓名:</label>
<span>{{ leaveRequest.applicantName }}</span>
</div>
<div class="detail-item">
<label>所属部门:</label>
<span>{{ leaveRequest.department }}</span>
</div>
<div class="detail-item">
<label>请假类型:</label>
<span>{{ leaveRequest.leaveType }}</span>
</div>
<div class="detail-item">
<label>开始时间:</label>
<span>{{ leaveRequest.startTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>结束时间:</label>
<span>{{ leaveRequest.endTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>请假天数:</label>
<span>{{ leaveRequest.days }}</span>
</div>
<div class="detail-item">
<label>请假原因:</label>
<span>{{ leaveRequest.reason }}</span>
</div>
<div class="detail-item">
<label>当前状态:</label>
<span>{{ leaveRequest.status }}</span>
</div>
<div class="detail-item">
<label>当前审批部门:</label>
<span>{{ leaveRequest.currentDepartment }}</span>
</div>

<!-- <div class="detail-item">
<label>当前审批人姓名:</label>
<span>{{ leaveRequest.currentApproverName }}</span>
</div> -->

<div class="detail-item">
<label>创建时间:</label>
<span>{{ leaveRequest.createTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>更新时间:</label>
<span>{{ leaveRequest.updateTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
</div>

<div class="actions">
<!-- <button nz-button nzType="primary" (click)="rejectToStart(leaveRequest)">撤销</button>
<button nz-button nzType="default" (click)="delete(leaveRequest)">删除</button> -->
<!-- <button nz-button nzType="default" (click)="goBack()">返回</button> -->
</div>
</div>

+ 69
- 0
src/app/routes/approve/leave-request-detail/leave-request-detail.component.less 파일 보기

@@ -0,0 +1,69 @@
.manager-approve {
// padding: 16px;


h2 {
text-align: center;
margin-bottom: 24px;
}

nz-table {
margin-top: 16px;
}

button {
margin-right: 8px;
}
}

.manager-approve-detail {
padding: 24px;
max-width: 800px;
// margin-top: 20px;
padding-top: 20px;
margin: 20px auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

h2 {
text-align: center;
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: #333;
}

.detail-container {
display: flex;
flex-direction: column;
gap: 16px;
}

.detail-item {
display: flex;
align-items: center;
padding: 8px 16px;
background: #f9f9f9;
border-radius: 4px;

label {
font-weight: 500;
color: #333;
min-width: 120px;
}

span {
color: #666;
}
}

.actions {
margin-top: 24px;
text-align: right;

button {
margin-left: 8px;
}
}
}

+ 190
- 0
src/app/routes/approve/leave-request-detail/leave-request-detail.component.ts 파일 보기

@@ -0,0 +1,190 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { DownFileModule } from '@delon/abc/down-file';
import { SGModule } from '@delon/abc/sg';
import { STColumn, STComponent, STChange } from '@delon/abc/st';
import { DelonFormModule, SFComponent, SFSchema, SFDateWidgetSchema } from '@delon/form';
import { _HttpClient, SettingsService } from '@delon/theme';
import { environment } from '@env/environment';
import { SHARED_IMPORTS, SharedModule } from '@shared';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzQRCodeModule } from 'ng-zorro-antd/qr-code';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzSwitchModule } from 'ng-zorro-antd/switch';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTreeSelectModule } from 'ng-zorro-antd/tree-select';
import { Subscription } from 'rxjs/internal/Subscription';
import { BaseService } from 'src/app/core/utils/base.service';

import { TaskEventsService, TaskExecutedPayload } from '../../wf/task-framework/task-events.service';
@Component({
selector: 'app-leave-request-detail',
standalone: true,
imports: [
CommonModule,
NzTableModule,
NzButtonModule,
FormsModule,
NzDatePickerModule,
NzTreeSelectModule,
SharedModule,
SGModule,
DownFileModule,
...SHARED_IMPORTS,
NzRadioModule,
NzSwitchModule,
NzSelectModule,
NzDescriptionsModule
],
templateUrl: './leave-request-detail.component.html',
styleUrls: ['./leave-request-detail.component.less']
})
export class LeaveRequestDetailComponent implements OnInit, OnDestroy {
leaveRequest: any = {};
loading = false;
businessId: string | null = null;
routeParams: any;

private readonly route = inject(ActivatedRoute);
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
private taskExecuted?: Subscription;
constructor(
private message: NzMessageService,
private taskEventsService: TaskEventsService
) {}

ngOnInit() {
this.route.queryParamMap.subscribe(params => {
this.routeParams = params;
this.businessId = params.get('businessId');
if (this.businessId) {
// 详情模式
this.getDetail(this.businessId);
} else {
// 列表模式
this.getList();
}
});
this.taskExecuted = this.taskEventsService.taskExecuted$.subscribe((payload: TaskExecutedPayload) => {
// payload 里有 taskId、formKey、prodefId 等
console.log('监听到待办执行:', payload);
if (payload.action === 'rejectToStart') {
this.rejectToStart(Number(payload.businessId ?? 0), false);
} else if (payload.action === 'delete') {
this.delete(Number(payload.businessId ?? 0), false);
}
});
}

ngOnDestroy() {
if (this.taskExecuted) {
this.taskExecuted.unsubscribe();
}
}
// 获取详情
getDetail(id: string) {
this.loading = true;
this.http.get<any>(`${environment.api['fuelUrl']}erp/request/${id}`).subscribe({
next: res => {
this.leaveRequest = res.data;
this.loading = false;
// 获取businessId参数
this.changeTitle();
},
error: () => {
this.message.error('加载数据失败');
this.loading = false;
}
});
}

// 获取列表
getList() {
this.http
.get<any>(`/erp/request/list`, {
params: {
currentDepartment: '业务部门'
}
})
.subscribe(res => {
if (res.code === 200 && res.data) {
this.leaveRequest = res.data;
} else {
this.leaveRequest = [];
this.message.error('无待审批数据');
}
});
}

delete(id: number, complete: boolean = true): void {
this.http.delete(`${environment.api['fuelUrl']}erp/request/${id}`).subscribe({
next: () => {
this.message.success('已批准');
if (complete) {
// 可根据实际业务,调用后端API变更审批状态
this.taskEventsService.emitTaskFinished({
taskId: this.routeParams['taskId'],
action: 'delete', // 或 'reject'
prodefId: this.routeParams['prodefId'],
params: this.routeParams // 可选,传递附加信息
});
}
this.goBack();
},
error: () => {
this.message.error('删除失败');
}
});
}

rejectToStart(id: number, complete: boolean = true): void {
this.http.put(`${environment.api['fuelUrl']}erp/request`, { id, status: '填写申请单' }).subscribe({
next: () => {
this.message.success('已驳回');
if (complete) {
// 可根据实际业务,调用后端API变更审批状态
this.taskEventsService.emitTaskFinished({
taskId: this.routeParams['taskId'],
action: 'rejectToStart', // 或 'reject'
prodefId: this.routeParams['prodefId'],
params: this.routeParams // 可选,传递附加信息
});
}
this.goBack();
},
error: () => {
this.message.error('驳回失败');
}
});
}
changeTitle() {
let deleteEnable = true;
if (this.leaveRequest.status === '已批准') {
deleteEnable = false;
}
this.taskEventsService.emitButtonTitleChanged({
excuteTitle: '批准',
excuteEnable: false,
rejectTitle: '驳回',
rejectEnable: false,
rejectToStartTitle: '撤销申请',
rejectToStartEnable: true,
deleteTitle: '删除',
deleteEnable: deleteEnable
});
}
goBack(): void {
this.router.navigate(['/approve/leave-request-list']);
}
}

+ 51
- 0
src/app/routes/approve/leave-request-list/leave-request-list.component.html 파일 보기

@@ -0,0 +1,51 @@
<div class="container-block">
<nz-card class="card-block card-search">
<div>
<sf
#sf
mode="search"
size="large"
(change)="onSearchChange($event)"
[schema]="searchSchema"
(formReset)="resetSearch()"
(formSubmit)="search()"
[button]="null"
>
<button nz-button nzType="primary" style="margin-right: 0.5rem" type="submit" class="searchBtn">搜索</button>
<button class="resetBtn" (click)="sf.reset(); search()" type="button" nz-button>重置</button>
</sf>
</div>
</nz-card>

<nz-card class="table-card">
<st
#st
[data]="leaveRequests"
[columns]="columns"
[pi]="pageIndex"
[ps]="pageSize"
[page]="{ front: false }"
[total]="total"
(change)="stPageChange($event)"
size="small"
[rowClassName]="rowClassName"
[scroll]="{ y: 'calc(100vh - 283px)' }"
[page]="{ show: false }"
/>
<div class="common-pagination">
<nz-pagination
[(nzPageIndex)]="pageIndex"
[nzTotal]="total"
nzShowSizeChanger
[nzPageSize]="pageSize"
[nzShowTotal]="totalTemplate"
[nzShowQuickJumper]="true"
(nzPageSizeChange)="pageSizeChang($event)"
(nzPageIndexChange)="pageIndexChange($event)"
/>
<ng-template #totalTemplate let-total>共 {{ total }} 条</ng-template>
</div>
</nz-card>
</div>

<history-nz-modal *ngIf="hVisible" [id]="instantId" [visible]="hVisible" (modalCallBack)="modalCallBack($event)" />

+ 313
- 0
src/app/routes/approve/leave-request-list/leave-request-list.component.less 파일 보기

@@ -0,0 +1,313 @@
.card-block {
position: relative;
}

.add {
float: right;
margin-top: -38px;
}

::ng-deep .row-color {
background-color: rgb(214, 230, 245) !important;
}

::ng-deep {
.menu-tree {
.ant-tree {
width: 258px;
height: 296px;
overflow-x: hidden;
overflow-y: auto;
}

.ant-form-item-label {
width: 72px !important;
display: none !important;
}
}
}

::ng-deep {

.anticon-down {
display: none !important;
}
// .ant-table-body {
// overflow-x: auto;
// overflow-y: auto;
// scrollbar-color: #fff #949494;
// height: calc(100vh - 283px) !important;
// }
}


.el-button-add {
color: #1890ff !important;
background: #e8f4ff !important;
border-color: #a3d3ff !important
}

.el-button.is-round {
border-radius: 20px !important;
padding: 12px 23px !important;
}

.el-button.is-circle {
border-radius: 50% !important;
padding: 12px !important;
}

.el-button--primary {
color: #fff !important;
background-color: #1890ff !important;
border-color: #1890ff !important;
}

.el-button--primary:focus,
.el-button--primary:hover {
background: #46a6ff !important;
border-color: #46a6ff !important;
color: #fff !important;
}

.el-button--primary:active {
outline: none !important;
}

.el-button--primary.is-active,
.el-button--primary:active {
background: #1682e6 !important;
border-color: #1682e6 !important;
color: #fff !important;
}

.el-button--primary.is-disabled,
.el-button--primary.is-disabled:active,
.el-button--primary.is-disabled:focus,
.el-button--primary.is-disabled:hover {
color: #fff !important;
background-color: #8cc8ff !important;
border-color: #8cc8ff !important;
}

.el-button--primary.is-plain {
color: #1890ff !important;
background: #e8f4ff !important;
border-color: #a3d3ff !important;
}

.el-button--primary.is-plain:focus,
.el-button--primary.is-plain:hover {
background: #1890ff !important;
border-color: #1890ff !important;
color: #fff !important;
}

.el-button--primary.is-plain:active {
background: #1682e6 !important;
border-color: #1682e6 !important;
color: #fff !important;
outline: none !important;
}

.el-button--primary.is-plain.is-disabled,
.el-button--primary.is-plain.is-disabled:active,
.el-button--primary.is-plain.is-disabled:focus,
.el-button--primary.is-plain.is-disabled:hover {
color: #74bcff !important;
background-color: #e8f4ff !important;
border-color: #d1e9ff !important;
}

.el-button--success {
color: #fff !important;
background-color: #13ce66 !important;
border-color: #13ce66 !important;
}

.el-button--success:focus,
.el-button--success:hover {
background: #42d885 !important;
border-color: #42d885 !important;
color: #fff !important;
}

.el-button--success:active {
outline: none !important;
}

.el-button--success.is-active,
.el-button--success:active {
background: #11b95c !important;
border-color: #11b95c !important;
color: #fff !important;
}

.el-button--success.is-disabled,
.el-button--success.is-disabled:active,
.el-button--success.is-disabled:focus,
.el-button--success.is-disabled:hover {
color: #fff !important;
background-color: #89e7b3 !important;
border-color: #89e7b3 !important;
}

.el-button--success.is-plain {
color: #13ce66 !important;
background: #e7faf0 !important;
border-color: #a1ebc2 !important;
}

.el-button--success.is-plain:focus,
.el-button--success.is-plain:hover {
background: #13ce66 !important;
border-color: #13ce66 !important;
color: #fff !important;
}

.el-button--success.is-plain:active {
background: #11b95c !important;
border-color: #11b95c !important;
color: #fff !important;
outline: none !important;
}

.el-button--success.is-plain.is-disabled,
.el-button--success.is-plain.is-disabled:active,
.el-button--success.is-plain.is-disabled:focus,
.el-button--success.is-plain.is-disabled:hover {
color: #71e2a3 !important;
background-color: #e7faf0 !important;
border-color: #d0f5e0 !important;
}

.el-button--warning {
color: #fff !important;
background-color: #ffba00 !important;
border-color: #ffba00 !important;
}

.el-button--warning:focus,
.el-button--warning:hover {
background: #ffc833 !important;
border-color: #ffc833 !important;
color: #fff !important;
}

.el-button--warning:active {
outline: none !important;
}

.el-button--warning.is-active,
.el-button--warning:active {
background: #e6a700 !important;
border-color: #e6a700 !important;
color: #fff !important;
}

.el-button--warning.is-disabled,
.el-button--warning.is-disabled:active,
.el-button--warning.is-disabled:focus,
.el-button--warning.is-disabled:hover {
color: #fff !important;
background-color: #ffdd80 !important;
border-color: #ffdd80 !important;
}

.el-button--warning.is-plain {
color: #ffba00 !important;
background: #fff8e6 !important;
border-color: #ffe399 !important;
}

.el-button--warning.is-plain:focus,
.el-button--warning.is-plain:hover {
background: #ffba00 !important;
border-color: #ffba00 !important;
color: #fff !important;
}

.el-button--warning.is-plain:active {
background: #e6a700 !important;
border-color: #e6a700 !important;
color: #fff !important;
outline: none !important;
}

.el-button--warning.is-plain.is-disabled,
.el-button--warning.is-plain.is-disabled:active,
.el-button--warning.is-plain.is-disabled:focus,
.el-button--warning.is-plain.is-disabled:hover {
color: #ffd666 !important;
background-color: #fff8e6 !important;
border-color: #fff1cc !important;
}

.el-button--danger {
color: #fff !important;
background-color: #ff4949 !important;
border-color: #ff4949 !important;
}

.el-button--danger:focus,
.el-button--danger:hover {
background: #ff6d6d !important;
border-color: #ff6d6d !important;
color: #fff !important;
}

.el-button--danger:active {
outline: none !important;
}

.el-button--danger.is-active,
.el-button--danger:active {
background: #e64242 !important;
border-color: #e64242 !important;
color: #fff !important;
}

.el-button--danger.is-disabled,
.el-button--danger.is-disabled:active,
.el-button--danger.is-disabled:focus,
.el-button--danger.is-disabled:hover {
color: #fff !important;
background-color: #ffa4a4 !important;
border-color: #ffa4a4 !important;
}

.el-button--danger.is-plain {
color: #ff4949 !important;
background: #ffeded !important;
border-color: #ffb6b6 !important;
}

.el-button--danger.is-plain:focus,
.el-button--danger.is-plain:hover {
background: #ff4949 !important;
border-color: #ff4949 !important;
color: #fff !important;
}

.el-button--danger.is-plain:active {
background: #e64242 !important;
border-color: #e64242 !important;
color: #fff !important;
outline: none !important;
}

.el-button--danger.is-plain.is-disabled,
.el-button--danger.is-plain.is-disabled:active,
.el-button--danger.is-plain.is-disabled:focus,
.el-button--danger.is-plain.is-disabled:hover {
color: #ff9292 !important;
background-color: #ffeded !important;
border-color: #ffdbdb !important;
}
// ::ng-deep {
// .ant-tag{
// padding-left:20px !important;

// }
// }


+ 180
- 0
src/app/routes/approve/leave-request-list/leave-request-list.component.ts 파일 보기

@@ -0,0 +1,180 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { STChange, STColumn, STComponent, STModule, STColumnTag, STClickRowClassNameType } from '@delon/abc/st';
import { SFComponent, SFSchema, SFSelectWidgetSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { SHARED_IMPORTS, SharedModule } from '@shared';
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzFormatEmitEvent } from 'ng-zorro-antd/core/tree';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageService, NzMessageModule } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTreeComponent } from 'ng-zorro-antd/tree';

import { HistoryNzModalComponent } from '../../wf/common/history-nz-modal/history-nz-modal.component';
@Component({
selector: 'app-leave-request-list',
standalone: true,
imports: [
CommonModule,
STModule,
NzTableModule,
NzButtonModule,
HistoryNzModalComponent,
NzTableModule,
NzPaginationModule,
NzButtonModule,
NzPaginationModule,
NzInputModule,
SharedModule,
NzButtonModule,

NzPopconfirmModule,
NzMessageModule,
NzAlertModule,
...SHARED_IMPORTS,

NzRadioModule,

NzIconModule
],
templateUrl: './leave-request-list.component.html',
styleUrls: ['./leave-request-list.component.less']
})
export class LeaveRequestListComponent implements OnInit, AfterViewInit {
@ViewChild('sf', { static: false }) sf!: SFComponent;
leaveRequests: any[] = [];
loading = false;
instantId = '';
total = 0;
pageIndex = 1;
pageSize = 10;
constructor(
private http: HttpClient,
private message: NzMessageService,
private router: Router
) {}
searchParams: any = {};
searchSchema: any = {
properties: {
applicantName: { type: 'string', title: '申请人姓名' },
department: { type: 'string', title: '所属部门' },
leaveType: {
type: 'string',
title: '请假类型',
enum: [
{ label: '病假', value: '病假' },
{ label: '事假', value: '事假' },
{ label: '年假', value: '年假' }
]
}
}
};
hVisible = false;
columns: STColumn[] = [
{ title: '序号', type: 'no', className: 'text-center', width: 80 },
{ title: '申请人姓名', index: 'applicantName', width: 120, className: 'text-center' },
{ title: '所属部门', index: 'department', width: 120, className: 'text-center' },
{ title: '请假类型', index: 'leaveType', width: 100, className: 'text-center' },
{ title: '开始时间', index: 'startTime', width: 150, type: 'date', dateFormat: 'yyyy-MM-dd HH:mm:ss', className: 'text-center' },
{ title: '结束时间', index: 'endTime', width: 150, type: 'date', dateFormat: 'yyyy-MM-dd HH:mm:ss', className: 'text-center' },
{ title: '请假天数', index: 'days', width: 80, className: 'text-center' },
{ title: '请假原因', index: 'reason', width: 180, className: 'text-center' },
{ title: '当前状态', index: 'status', width: 100, className: 'text-center' },
{
title: '操作',
width: 140,
fixed: 'right',
buttons: [
{
text: '查看历史流程',
click: (item: any) => this.history(item)
}
]
}
];
rowClassName = (record: any, index: any) => {
if (index % 2 === 0) {
return 'even-tr';
} else {
return 'odd-tr';
}
};
ngOnInit(): void {}
ngAfterViewInit(): void {
this.loadData();
}

loadData(): void {
this.searchParams = this.sf.value || {};
// 如果 this.sf 没有,默认用空对象
const searchValues = this.sf ? this.sf.value : {};
const params = {
...searchValues,
pageNum: this.pageIndex,
pageSize: this.pageSize
};
console.log(params, 'params');
// this.loading = true;
this.http.get<any[]>(`${environment.api['fuelUrl']}erp/request/list`, { params }).subscribe({
next: (res: any) => {
this.leaveRequests = res.data;
this.total = res.data?.total || this.leaveRequests.length;
// this.loading = false;
},
error: () => {
this.message.error('加载数据失败');
// this.loading = false;
}
});
}

viewHistory(processInstanceId: string): void {
this.router.navigate(['/workflow/task-history'], {
queryParams: { processInstanceId }
});
}
hClose() {
this.hVisible = false;
}
history(item: any) {
this.instantId = item.processInstanceId || '';
// this.id = item.id;
this.hVisible = true;
}
modalCallBack(obj: any) {
this.hVisible = obj.visible;
}
onSearchChange(e: any) {
this.searchParams = e.value || {};
}
search() {
this.pageIndex = 1;
this.loadData();
}
resetSearch() {
this.searchParams = {};
this.pageIndex = 1;
this.loadData();
}
stPageChange(e: any) {
if (e.type === 'pi') this.pageIndex = e.pi;
if (e.type === 'ps') this.pageSize = e.ps;
}
pageSizeChang(ps: number) {
this.pageSize = ps;
this.pageIndex = 1;
}
pageIndexChange(pi: number) {
this.pageIndex = pi;
}
}

+ 64
- 0
src/app/routes/approve/manager-approve/manager-approve.component.html 파일 보기

@@ -0,0 +1,64 @@
<div class="manager-approve-detail">
<div class="detail-container">
<!-- <div class="detail-item">
<label>申请人ID:</label>
<span>{{ leaveRequest.applicantId }}</span>
</div> -->
<div class="detail-item">
<label>申请人姓名:</label>
<span>{{ leaveRequest.applicantName }}</span>
</div>
<div class="detail-item">
<label>所属部门:</label>
<span>{{ leaveRequest.department }}</span>
</div>
<div class="detail-item">
<label>请假类型:</label>
<span>{{ leaveRequest.leaveType }}</span>
</div>
<div class="detail-item">
<label>开始时间:</label>
<span>{{ leaveRequest.startTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>结束时间:</label>
<span>{{ leaveRequest.endTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>请假天数:</label>
<span>{{ leaveRequest.days }}</span>
</div>
<div class="detail-item">
<label>请假原因:</label>
<span>{{ leaveRequest.reason }}</span>
</div>
<div class="detail-item">
<label>当前状态:</label>
<span>{{ leaveRequest.status }}</span>
</div>
<div class="detail-item">
<label>当前审批部门:</label>
<span>{{ leaveRequest.currentDepartment }}</span>
</div>

<!-- <div class="detail-item">
<label>当前审批人姓名:</label>
<span>{{ leaveRequest.currentApproverName }}</span>
</div> -->

<div class="detail-item">
<label>创建时间:</label>
<span>{{ leaveRequest.createTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>更新时间:</label>
<span>{{ leaveRequest.updateTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
</div>

<!-- <div class="actions">
<button nz-button nzType="primary" (click)="approve(leaveRequest.id)">批准</button>
<button nz-button nzType="default" (click)="reject(leaveRequest.id)">驳回</button>
<button nz-button nzType="default" (click)="goBack()">返回</button>
</div> -->
</div>

+ 69
- 0
src/app/routes/approve/manager-approve/manager-approve.component.less 파일 보기

@@ -0,0 +1,69 @@
.manager-approve {
// padding: 16px;


h2 {
text-align: center;
margin-bottom: 24px;
}

nz-table {
margin-top: 16px;
}

button {
margin-right: 8px;
}
}

.manager-approve-detail {
padding: 24px;
max-width: 800px;
// margin-top: 20px;
padding-top: 20px;
margin: 20px auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

h2 {
text-align: center;
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: #333;
}

.detail-container {
display: flex;
flex-direction: column;
gap: 16px;
}

.detail-item {
display: flex;
align-items: center;
padding: 8px 16px;
background: #f9f9f9;
border-radius: 4px;

label {
font-weight: 500;
color: #333;
min-width: 120px;
}

span {
color: #666;
}
}

.actions {
margin-top: 24px;
text-align: right;

button {
margin-left: 8px;
}
}
}

+ 188
- 0
src/app/routes/approve/manager-approve/manager-approve.component.ts 파일 보기

@@ -0,0 +1,188 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { DownFileModule } from '@delon/abc/down-file';
import { SGModule } from '@delon/abc/sg';
import { STColumn, STComponent, STChange } from '@delon/abc/st';
import { DelonFormModule, SFComponent, SFSchema, SFDateWidgetSchema } from '@delon/form';
import { _HttpClient, SettingsService } from '@delon/theme';
import { environment } from '@env/environment';
import { SHARED_IMPORTS, SharedModule } from '@shared';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzQRCodeModule } from 'ng-zorro-antd/qr-code';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzSwitchModule } from 'ng-zorro-antd/switch';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTreeSelectModule } from 'ng-zorro-antd/tree-select';
import { Subscription } from 'rxjs/internal/Subscription';
import { BaseService } from 'src/app/core/utils/base.service';

import { TaskEventsService, TaskExecutedPayload } from '../../wf/task-framework/task-events.service';

@Component({
selector: 'app-manager-approve',
standalone: true,
imports: [
CommonModule,
NzTableModule,
NzButtonModule,
FormsModule,
NzDatePickerModule,
NzTreeSelectModule,
SharedModule,
SGModule,
DownFileModule,
...SHARED_IMPORTS,
NzRadioModule,
NzSwitchModule,
NzSelectModule,
NzDescriptionsModule
],
templateUrl: './manager-approve.component.html',
styleUrls: ['./manager-approve.component.less']
})
export class ManagerApproveComponent implements OnInit, OnDestroy {
leaveRequest: any = {};
loading = false;
businessId: string | null = null;
routeParams: any;

private readonly route = inject(ActivatedRoute);
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
private taskExecuted?: Subscription;
constructor(
private message: NzMessageService,
private taskEventsService: TaskEventsService
) {}

ngOnInit() {
// 获取businessId参数
this.changeTitle();
this.route.queryParamMap.subscribe(params => {
this.routeParams = params;
this.businessId = params.get('businessId');
if (this.businessId) {
// 详情模式
this.getDetail(this.businessId);
} else {
// 列表模式
this.getList();
}
});
this.taskExecuted = this.taskEventsService.taskExecuted$.subscribe((payload: TaskExecutedPayload) => {
// payload 里有 taskId、formKey、prodefId 等
console.log('监听到待办执行:', payload);

if (payload.action === 'approve') {
this.approve(Number(payload.businessId ?? 0), false);
} else {
this.reject(Number(payload.businessId ?? 0), false);
}
});
}

ngOnDestroy() {
if (this.taskExecuted) {
this.taskExecuted.unsubscribe();
}
}
// 获取详情
getDetail(id: string) {
this.loading = true;
this.http.get<any>(`${environment.api['fuelUrl']}erp/request/${id}`).subscribe({
next: res => {
this.leaveRequest = res.data;
this.loading = false;
},
error: () => {
this.message.error('加载数据失败');
this.loading = false;
}
});
}

// 获取列表
getList() {
this.http
.get<any>(`/erp/request/list`, {
params: {
currentDepartment: '业务部门'
}
})
.subscribe(res => {
if (res.code === 200 && res.data) {
this.leaveRequest = res.data;
} else {
this.leaveRequest = [];
this.message.error('无待审批数据');
}
});
}

approve(id: number, complete: boolean = true): void {
this.http.put(`${environment.api['fuelUrl']}erp/request`, { id, status: '业务部门领导已批准' }).subscribe({
next: () => {
this.message.success('已批准');
if (complete) {
// 可根据实际业务,调用后端API变更审批状态
this.taskEventsService.emitTaskFinished({
taskId: this.routeParams['taskId'],
action: 'approve', // 或 'reject'
prodefId: this.routeParams['prodefId'],
params: this.routeParams // 可选,传递附加信息
});
}
this.goBack();
},
error: () => {
this.message.error('批准失败');
}
});
}

reject(id: number, complete: boolean = true): void {
this.http.put(`${environment.api['fuelUrl']}erp/request`, { id, status: '业务部门经理已驳回' }).subscribe({
next: () => {
this.message.success('已驳回');
if (complete) {
// 可根据实际业务,调用后端API变更审批状态
this.taskEventsService.emitTaskFinished({
taskId: this.routeParams['taskId'],
action: 'reject', // 或 'reject'
prodefId: this.routeParams['prodefId'],
params: this.routeParams // 可选,传递附加信息
});
}
this.goBack();
},
error: () => {
this.message.error('驳回失败');
}
});
}
changeTitle() {
this.taskEventsService.emitButtonTitleChanged({
excuteTitle: '批准',
excuteEnable: true,
rejectTitle: '驳回',
rejectEnable: true,
rejectToStartTitle: '驳回到起点',
rejectToStartEnable: false,
deleteTitle: '删除',
deleteEnable: false
});
}
goBack(): void {
this.router.navigate(['/approve/leave-request-list']);
}
}

+ 51
- 0
src/app/routes/approve/own-leave-request-list/own-leave-request-list.component.html 파일 보기

@@ -0,0 +1,51 @@
<div class="container-block">
<nz-card class="card-block card-search">
<div>
<sf
#sf
mode="search"
size="large"
(change)="onSearchChange($event)"
[schema]="searchSchema"
(formReset)="resetSearch()"
(formSubmit)="search()"
[button]="null"
>
<button nz-button nzType="primary" style="margin-right: 0.5rem" type="submit" class="searchBtn">搜索</button>
<button class="resetBtn" (click)="sf.reset(); search()" type="button" nz-button>重置</button>
</sf>
</div>
</nz-card>

<nz-card class="table-card">
<st
#st
[data]="leaveRequests"
[columns]="columns"
[pi]="pageIndex"
[ps]="pageSize"
[page]="{ front: false }"
[total]="total"
(change)="stPageChange($event)"
size="small"
[rowClassName]="rowClassName"
[scroll]="{ y: 'calc(100vh - 283px)' }"
[page]="{ show: false }"
/>
<div class="common-pagination">
<nz-pagination
[(nzPageIndex)]="pageIndex"
[nzTotal]="total"
nzShowSizeChanger
[nzPageSize]="pageSize"
[nzShowTotal]="totalTemplate"
[nzShowQuickJumper]="true"
(nzPageSizeChange)="pageSizeChang($event)"
(nzPageIndexChange)="pageIndexChange($event)"
/>
<ng-template #totalTemplate let-total>共 {{ total }} 条</ng-template>
</div>
</nz-card>
</div>

<history-nz-modal *ngIf="hVisible" [id]="instantId" [visible]="hVisible" (modalCallBack)="modalCallBack($event)" />

+ 305
- 0
src/app/routes/approve/own-leave-request-list/own-leave-request-list.component.less 파일 보기

@@ -0,0 +1,305 @@
.card-block {
position: relative;
}

.add {
float: right;
margin-top: -38px;
}

::ng-deep .row-color {
background-color: rgb(214, 230, 245) !important;
}

::ng-deep {
.menu-tree {
.ant-tree {
width: 258px;
height: 296px;
overflow-x: hidden;
overflow-y: auto;
}

.ant-form-item-label {
width: 72px !important;
display: none !important;
}
}
}

::ng-deep {
.anticon-down {
display: none !important;
}
}

.el-button-add {
color: #1890ff !important;
background: #e8f4ff !important;
border-color: #a3d3ff !important
}

.el-button.is-round {
border-radius: 20px !important;
padding: 12px 23px !important;
}

.el-button.is-circle {
border-radius: 50% !important;
padding: 12px !important;
}

.el-button--primary {
color: #fff !important;
background-color: #1890ff !important;
border-color: #1890ff !important;
}

.el-button--primary:focus,
.el-button--primary:hover {
background: #46a6ff !important;
border-color: #46a6ff !important;
color: #fff !important;
}

.el-button--primary:active {
outline: none !important;
}

.el-button--primary.is-active,
.el-button--primary:active {
background: #1682e6 !important;
border-color: #1682e6 !important;
color: #fff !important;
}

.el-button--primary.is-disabled,
.el-button--primary.is-disabled:active,
.el-button--primary.is-disabled:focus,
.el-button--primary.is-disabled:hover {
color: #fff !important;
background-color: #8cc8ff !important;
border-color: #8cc8ff !important;
}

.el-button--primary.is-plain {
color: #1890ff !important;
background: #e8f4ff !important;
border-color: #a3d3ff !important;
}

.el-button--primary.is-plain:focus,
.el-button--primary.is-plain:hover {
background: #1890ff !important;
border-color: #1890ff !important;
color: #fff !important;
}

.el-button--primary.is-plain:active {
background: #1682e6 !important;
border-color: #1682e6 !important;
color: #fff !important;
outline: none !important;
}

.el-button--primary.is-plain.is-disabled,
.el-button--primary.is-plain.is-disabled:active,
.el-button--primary.is-plain.is-disabled:focus,
.el-button--primary.is-plain.is-disabled:hover {
color: #74bcff !important;
background-color: #e8f4ff !important;
border-color: #d1e9ff !important;
}

.el-button--success {
color: #fff !important;
background-color: #13ce66 !important;
border-color: #13ce66 !important;
}

.el-button--success:focus,
.el-button--success:hover {
background: #42d885 !important;
border-color: #42d885 !important;
color: #fff !important;
}

.el-button--success:active {
outline: none !important;
}

.el-button--success.is-active,
.el-button--success:active {
background: #11b95c !important;
border-color: #11b95c !important;
color: #fff !important;
}

.el-button--success.is-disabled,
.el-button--success.is-disabled:active,
.el-button--success.is-disabled:focus,
.el-button--success.is-disabled:hover {
color: #fff !important;
background-color: #89e7b3 !important;
border-color: #89e7b3 !important;
}

.el-button--success.is-plain {
color: #13ce66 !important;
background: #e7faf0 !important;
border-color: #a1ebc2 !important;
}

.el-button--success.is-plain:focus,
.el-button--success.is-plain:hover {
background: #13ce66 !important;
border-color: #13ce66 !important;
color: #fff !important;
}

.el-button--success.is-plain:active {
background: #11b95c !important;
border-color: #11b95c !important;
color: #fff !important;
outline: none !important;
}

.el-button--success.is-plain.is-disabled,
.el-button--success.is-plain.is-disabled:active,
.el-button--success.is-plain.is-disabled:focus,
.el-button--success.is-plain.is-disabled:hover {
color: #71e2a3 !important;
background-color: #e7faf0 !important;
border-color: #d0f5e0 !important;
}

.el-button--warning {
color: #fff !important;
background-color: #ffba00 !important;
border-color: #ffba00 !important;
}

.el-button--warning:focus,
.el-button--warning:hover {
background: #ffc833 !important;
border-color: #ffc833 !important;
color: #fff !important;
}

.el-button--warning:active {
outline: none !important;
}

.el-button--warning.is-active,
.el-button--warning:active {
background: #e6a700 !important;
border-color: #e6a700 !important;
color: #fff !important;
}

.el-button--warning.is-disabled,
.el-button--warning.is-disabled:active,
.el-button--warning.is-disabled:focus,
.el-button--warning.is-disabled:hover {
color: #fff !important;
background-color: #ffdd80 !important;
border-color: #ffdd80 !important;
}

.el-button--warning.is-plain {
color: #ffba00 !important;
background: #fff8e6 !important;
border-color: #ffe399 !important;
}

.el-button--warning.is-plain:focus,
.el-button--warning.is-plain:hover {
background: #ffba00 !important;
border-color: #ffba00 !important;
color: #fff !important;
}

.el-button--warning.is-plain:active {
background: #e6a700 !important;
border-color: #e6a700 !important;
color: #fff !important;
outline: none !important;
}

.el-button--warning.is-plain.is-disabled,
.el-button--warning.is-plain.is-disabled:active,
.el-button--warning.is-plain.is-disabled:focus,
.el-button--warning.is-plain.is-disabled:hover {
color: #ffd666 !important;
background-color: #fff8e6 !important;
border-color: #fff1cc !important;
}

.el-button--danger {
color: #fff !important;
background-color: #ff4949 !important;
border-color: #ff4949 !important;
}

.el-button--danger:focus,
.el-button--danger:hover {
background: #ff6d6d !important;
border-color: #ff6d6d !important;
color: #fff !important;
}

.el-button--danger:active {
outline: none !important;
}

.el-button--danger.is-active,
.el-button--danger:active {
background: #e64242 !important;
border-color: #e64242 !important;
color: #fff !important;
}

.el-button--danger.is-disabled,
.el-button--danger.is-disabled:active,
.el-button--danger.is-disabled:focus,
.el-button--danger.is-disabled:hover {
color: #fff !important;
background-color: #ffa4a4 !important;
border-color: #ffa4a4 !important;
}

.el-button--danger.is-plain {
color: #ff4949 !important;
background: #ffeded !important;
border-color: #ffb6b6 !important;
}

.el-button--danger.is-plain:focus,
.el-button--danger.is-plain:hover {
background: #ff4949 !important;
border-color: #ff4949 !important;
color: #fff !important;
}

.el-button--danger.is-plain:active {
background: #e64242 !important;
border-color: #e64242 !important;
color: #fff !important;
outline: none !important;
}

.el-button--danger.is-plain.is-disabled,
.el-button--danger.is-plain.is-disabled:active,
.el-button--danger.is-plain.is-disabled:focus,
.el-button--danger.is-plain.is-disabled:hover {
color: #ff9292 !important;
background-color: #ffeded !important;
border-color: #ffdbdb !important;
}
// ::ng-deep {
// .ant-tag{
// padding-left:20px !important;

// }
// }


+ 213
- 0
src/app/routes/approve/own-leave-request-list/own-leave-request-list.component.ts 파일 보기

@@ -0,0 +1,213 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { STChange, STColumn, STComponent, STModule, STColumnTag, STClickRowClassNameType } from '@delon/abc/st';
import { SFComponent, SFSchema, SFSelectWidgetSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { SHARED_IMPORTS, SharedModule } from '@shared';
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzFormatEmitEvent } from 'ng-zorro-antd/core/tree';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageService, NzMessageModule } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTreeComponent } from 'ng-zorro-antd/tree';

import { HistoryNzModalComponent } from '../../wf/common/history-nz-modal/history-nz-modal.component';
@Component({
selector: 'own-app-leave-request-list',
standalone: true,
imports: [
CommonModule,
STModule,
NzTableModule,
NzButtonModule,
HistoryNzModalComponent,
NzTableModule,
NzPaginationModule,
NzButtonModule,
NzPaginationModule,
NzInputModule,
SharedModule,
NzButtonModule,

NzPopconfirmModule,
NzMessageModule,
NzAlertModule,
...SHARED_IMPORTS,

NzRadioModule,

NzIconModule
],
templateUrl: './own-leave-request-list.component.html',
styleUrls: ['./own-leave-request-list.component.less']
})
export class OwnLeaveRequestListComponent implements OnInit, AfterViewInit {
@ViewChild('sf', { static: false }) sf!: SFComponent;
leaveRequests: any[] = [];
loading = false;
instantId = '';
total = 0;
pageIndex = 1;
pageSize = 10;
constructor(
private http: HttpClient,
private message: NzMessageService,
private router: Router
) {}
searchParams: any = {};
searchSchema: any = {
properties: {
// applicantName: { type: 'string', title: '申请人姓名' },
// department: { type: 'string', title: '所属部门' },
leaveType: {
type: 'string',
title: '请假类型',
enum: [
{ label: '病假', value: '病假' },
{ label: '事假', value: '事假' },
{ label: '调休', value: '调休' },
{ label: '年假', value: '年假' }
]
}
}
};
hVisible = false;
columns: STColumn[] = [
{ title: '序号', type: 'no', className: 'text-center', width: 80 },
{ title: '申请人姓名', index: 'applicantName', width: 120, className: 'text-center' },
{ title: '所属部门', index: 'department', width: 120, className: 'text-center' },
{ title: '请假类型', index: 'leaveType', width: 100, className: 'text-center' },
{ title: '开始时间', index: 'startTime', width: 150, type: 'date', dateFormat: 'yyyy-MM-dd HH:mm:ss', className: 'text-center' },
{ title: '结束时间', index: 'endTime', width: 150, type: 'date', dateFormat: 'yyyy-MM-dd HH:mm:ss', className: 'text-center' },
{ title: '请假天数', index: 'days', width: 80, className: 'text-center' },
{ title: '请假原因', index: 'reason', width: 180, className: 'text-center' },
{ title: '当前状态', index: 'status', width: 100, className: 'text-center' },
{
title: '操作',
width: 140,
fixed: 'right',
buttons: [
{
text: '详情',
click: (item: any) => this.withdrawRequest(item)
},
{
text: '查看历史流程',
click: (item: any) => this.history(item)
}
]
}
];
rowClassName = (record: any, index: any) => {
if (index % 2 === 0) {
return 'even-tr';
} else {
return 'odd-tr';
}
};
cancel(item: any): void {}
ngOnInit(): void {
this.loadData();
}
ngAfterViewInit(): void {
this.loadData();
}

loadData(): void {
this.searchParams = this.sf ? this.sf.value : {};
// 如果 this.sf 没有,默认用空对象
const searchValues = this.sf ? this.sf.value : {};
const userInfo = JSON.parse(window.sessionStorage.getItem('info') || '{}');
const userId = userInfo.user?.userId || '';
const params = {
...searchValues,
pageNum: this.pageIndex,
pageSize: this.pageSize,
applicantName: userInfo.user?.nickName || '无'
};
console.log(params, 'params');
this.loading = true;
this.http.get<any[]>(`${environment.api['fuelUrl']}erp/request/list`, { params }).subscribe({
next: (res: any) => {
this.leaveRequests = res.data;
this.total = res.data?.total || this.leaveRequests.length;
this.loading = false;
},
error: () => {
this.message.error('加载数据失败');
this.loading = false;
}
});
}

withdrawRequest(item: any): void {
// this.router.navigate(['/approve/leave-request-detail/'+id], {

// queryParams: {

// businessId: id,

// }
// });
this.router.navigate(['/workflow/task-framework'], {
queryParams: {
// taskId: task.id,
prodefId: item.processDefinitionId,
instantId: item.processInstanceId,
formKey: '/approve/leave-request-detail',
// hyCode: response?.variables.hyCode,
businessId: item.id,
title: '详情'
}
});
}

viewHistory(processInstanceId: string): void {
this.router.navigate(['/workflow/task-history'], {
queryParams: { processInstanceId }
});
}
hClose() {
this.hVisible = false;
}
history(item: any) {
this.instantId = item.processInstanceId || '';
// this.id = item.id;
this.hVisible = true;
}
modalCallBack(obj: any) {
this.hVisible = obj.visible;
}
onSearchChange(e: any) {
this.searchParams = e.value || {};
}
search() {
this.pageIndex = 1;
this.loadData();
}
resetSearch() {
this.searchParams = {};
this.pageIndex = 1;
this.loadData();
}
stPageChange(e: any) {
if (e.type === 'pi') this.pageIndex = e.pi;
if (e.type === 'ps') this.pageSize = e.ps;
}
pageSizeChang(ps: number) {
this.pageSize = ps;
this.pageIndex = 1;
}
pageIndexChange(pi: number) {
this.pageIndex = pi;
}
}

+ 64
- 0
src/app/routes/approve/people-manager-approve/people-manager-approve.component.html 파일 보기

@@ -0,0 +1,64 @@
<div class="manager-approve-detail">
<div class="detail-container">
<!-- <div class="detail-item">
<label>申请人ID:</label>
<span>{{ leaveRequest.applicantId }}</span>
</div> -->
<div class="detail-item">
<label>申请人姓名:</label>
<span>{{ leaveRequest.applicantName }}</span>
</div>
<div class="detail-item">
<label>所属部门:</label>
<span>{{ leaveRequest.department }}</span>
</div>
<div class="detail-item">
<label>请假类型:</label>
<span>{{ leaveRequest.leaveType }}</span>
</div>
<div class="detail-item">
<label>开始时间:</label>
<span>{{ leaveRequest.startTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>结束时间:</label>
<span>{{ leaveRequest.endTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>请假天数:</label>
<span>{{ leaveRequest.days }}</span>
</div>
<div class="detail-item">
<label>请假原因:</label>
<span>{{ leaveRequest.reason }}</span>
</div>
<div class="detail-item">
<label>当前状态:</label>
<span>{{ leaveRequest.status }}</span>
</div>
<div class="detail-item">
<label>当前审批部门:</label>
<span>{{ leaveRequest.currentDepartment }}</span>
</div>

<!-- <div class="detail-item">
<label>当前审批人姓名:</label>
<span>{{ leaveRequest.currentApproverName }}</span>
</div> -->

<div class="detail-item">
<label>创建时间:</label>
<span>{{ leaveRequest.createTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div class="detail-item">
<label>更新时间:</label>
<span>{{ leaveRequest.updateTime | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
</div>

<!-- <div class="actions">
<button nz-button nzType="primary" (click)="approve(leaveRequest.id)">批准</button>
<button nz-button nzType="default" (click)="reject(leaveRequest.id)">驳回</button>
<button nz-button nzType="default" (click)="goBack()">返回</button>
</div> -->
</div>

+ 69
- 0
src/app/routes/approve/people-manager-approve/people-manager-approve.component.less 파일 보기

@@ -0,0 +1,69 @@
.manager-approve {
// padding: 16px;


h2 {
text-align: center;
margin-bottom: 24px;
}

nz-table {
margin-top: 16px;
}

button {
margin-right: 8px;
}
}

.manager-approve-detail {
padding: 24px;
max-width: 800px;
// margin-top: 20px;
padding-top: 20px;
margin: 20px auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

h2 {
text-align: center;
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: #333;
}

.detail-container {
display: flex;
flex-direction: column;
gap: 16px;
}

.detail-item {
display: flex;
align-items: center;
padding: 8px 16px;
background: #f9f9f9;
border-radius: 4px;

label {
font-weight: 500;
color: #333;
min-width: 120px;
}

span {
color: #666;
}
}

.actions {
margin-top: 24px;
text-align: right;

button {
margin-left: 8px;
}
}
}

+ 189
- 0
src/app/routes/approve/people-manager-approve/people-manager-approve.component.ts 파일 보기

@@ -0,0 +1,189 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { DownFileModule } from '@delon/abc/down-file';
import { SGModule } from '@delon/abc/sg';
import { STColumn, STComponent, STChange } from '@delon/abc/st';
import { DelonFormModule, SFComponent, SFSchema, SFDateWidgetSchema } from '@delon/form';
import { _HttpClient, SettingsService } from '@delon/theme';
import { environment } from '@env/environment';
import { SHARED_IMPORTS, SharedModule } from '@shared';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzQRCodeModule } from 'ng-zorro-antd/qr-code';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzSwitchModule } from 'ng-zorro-antd/switch';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTreeSelectModule } from 'ng-zorro-antd/tree-select';
import { Subscription } from 'rxjs/internal/Subscription';
import { BaseService } from 'src/app/core/utils/base.service';

import { TaskEventsService, TaskExecutedPayload } from '../../wf/task-framework/task-events.service';

@Component({
selector: 'app-manager-approve',
standalone: true,
imports: [
CommonModule,
NzTableModule,
NzButtonModule,
FormsModule,
NzDatePickerModule,
NzTreeSelectModule,
SharedModule,
SGModule,
DownFileModule,
...SHARED_IMPORTS,
NzRadioModule,
NzSwitchModule,
NzSelectModule,
NzDescriptionsModule
],
templateUrl: './people-manager-approve.component.html',
styleUrls: ['./people-manager-approve.component.less']
})
export class PeopleManagerApproveComponent implements OnInit, OnDestroy {
leaveRequest: any = {};
loading = false;
businessId: string | null = null;
routeParams: any;

private readonly route = inject(ActivatedRoute);
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
private taskExecuted?: Subscription;
constructor(
private message: NzMessageService,
private taskEventsService: TaskEventsService
) {}

ngOnInit() {
this.changeTitle();
// 获取businessId参数
this.route.queryParamMap.subscribe(params => {
this.routeParams = params;
this.businessId = params.get('businessId');
if (this.businessId) {
// 详情模式
this.getDetail(this.businessId);
} else {
// 列表模式
this.getList();
}
});
this.taskExecuted = this.taskEventsService.taskExecuted$.subscribe((payload: TaskExecutedPayload) => {
// payload 里有 taskId、formKey、prodefId 等
console.log('监听到待办执行:', payload);

if (payload.action === 'approve') {
this.approve(Number(payload.businessId ?? 0), false);
} else {
this.reject(Number(payload.businessId ?? 0), false);
}
});
}

ngOnDestroy() {
if (this.taskExecuted) {
this.taskExecuted.unsubscribe();
}
}
// 获取详情
getDetail(id: string) {
this.loading = true;
this.http.get<any>(`${environment.api['fuelUrl']}erp/request/${id}`).subscribe({
next: res => {
this.leaveRequest = res.data;
this.loading = false;
},
error: () => {
this.message.error('加载数据失败');
this.loading = false;
}
});
}

// 获取列表
getList() {
this.http
.get<any>(`/erp/request/list`, {
params: {
currentDepartment: '人事部门'
}
})
.subscribe(res => {
if (res.code === 200 && res.data) {
this.leaveRequest = res.data;
} else {
this.leaveRequest = [];
this.message.error('无待审批数据');
}
});
}

approve(id: number, complete: boolean = true): void {
this.http.put(`${environment.api['fuelUrl']}erp/request`, { id, status: '已批准' }).subscribe({
next: () => {
this.message.success('已批准');
if (complete) {
// 可根据实际业务,调用后端API变更审批状态
this.taskEventsService.emitTaskFinished({
taskId: this.routeParams['taskId'],
action: 'approve', // 或 'reject'
prodefId: this.routeParams['prodefId'],
params: this.routeParams // 可选,传递附加信息
});
}
this.goBack();
},
error: () => {
this.message.error('批准失败');
}
});
}

reject(id: number, complete: boolean = true): void {
this.http.put(`${environment.api['fuelUrl']}erp/request`, { id, status: '任务部门经理已驳回' }).subscribe({
next: () => {
this.message.success('已驳回');
if (complete) {
// 可根据实际业务,调用后端API变更审批状态
this.taskEventsService.emitTaskFinished({
taskId: this.routeParams['taskId'],
action: 'reject', // 或 'reject'
prodefId: this.routeParams['prodefId'],
params: this.routeParams // 可选,传递附加信息
});
}
this.goBack();
},
error: () => {
this.message.error('驳回失败');
}
});
}
changeTitle() {
this.taskEventsService.emitButtonTitleChanged({
excuteTitle: '批准',
excuteEnable: true,
rejectTitle: '驳回',
rejectEnable: true,
rejectToStartTitle: '驳回到起点',
rejectToStartEnable: false,
deleteTitle: '删除',
deleteEnable: false
});
}

goBack(): void {
this.router.navigate(['/approve/leave-request-list']);
}
}

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

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

import { LeaveApplyComponent } from './leave-apply/leave-apply.component';
import { LeaveRequestDetailComponent } from './leave-request-detail/leave-request-detail.component';
import { LeaveRequestListComponent } from './leave-request-list/leave-request-list.component';
import { ManagerApproveComponent } from './manager-approve/manager-approve.component';
import { OwnLeaveRequestListComponent } from './own-leave-request-list/own-leave-request-list.component';
import { PeopleManagerApproveComponent } from './people-manager-approve/people-manager-approve.component';
export const routes: Routes = [
{ path: 'leave-apply', component: LeaveApplyComponent },
{ path: 'manager-approve', component: ManagerApproveComponent },
{ path: 'people-manager-approve', component: PeopleManagerApproveComponent },
{ path: 'leave-request-list', component: LeaveRequestListComponent },
{ path: 'own-leave-request-list', component: OwnLeaveRequestListComponent },
{ path: 'leave-request-detail', component: LeaveRequestDetailComponent }
];

+ 271
- 0
src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.html 파일 보기

@@ -0,0 +1,271 @@
<div>
<!-- *ngIf="dataLoaded" -->
<div class="container page-cont-height">
<div
nz-col
nzSpan="6"
nzGutter="10"
style="overflow: hidden; display: flex; height: 100%; flex-direction: column; justify-content: space-between"
>
<datav-border-box2 style="flex: 1">
<div class="card-default-title"> 系统状态 </div>
<app-data-v-sys-stas
[leftContentTitle]="'系统健康度'"
[leftContentValue]="systemStatusInfo.systemHealthDegree"
[rightContentTitle]="'设备投运率'"
[rightContentValue]="systemStatusInfo.equipmentCommissioningRate"
/>
</datav-border-box2>
<datav-border-box2 style="flex: 1">
<div class="card-default">
<div class="card-default-content">
<div style="margin-bottom: 1rem">
<datav-label-box4 [title]="'系 统 操 作 记 录'" [unit]="''" />
</div>
<!-- <app-fuel-eqp-oper-info [topicFullPath]="'/api/main/event-msg-recv-rec?TopicFullPath=CS/PC/SysOperRec'" /> -->
<!-- <sample-eqp-oper-info [topicFullPath]="'autosample/ReceptionRecords/sampleCabNum?sampleCabNum=1&TopicFullPath=CS/PC/scOperationRecordTopicTwo'" /> -->
<sample-eqp-oper-info [topicFullPath]="'bm/sample/configInfo/scNum?scNum=2&TopicFullPath=CS/PC/scOperationRecordTopicTwo'" />
</div>
</div>
</datav-border-box2>

<datav-border-box2 style="flex: 1">
<div class="card-default">
<div class="card-default-content">
<div style="margin-bottom: 1rem">
<datav-label-box4 [title]="'系 统 报 警 信 息'" [unit]="''" />
</div>
<app-fuel-sys-alarm-info [topicFullPath]="'api/taskScheduling/eventMsgRec/list?TopicFullPath=ASS2/PLC2'" />
</div>
</div>
</datav-border-box2>
<!-- <datav-border-box2> -->
<!--仪表盘-->
<!-- <div style="height: 10px;"></div> -->
<!-- <app-echarts-gauge></app-echarts-gauge> -->
<!-- </datav-border-box2> -->
</div>

<div
nz-col
nzSpan="14"
nzGutter="10"
style="overflow: hidden; display: flex; height: 100%; flex-direction: column; justify-content: space-between"
>
<div class="section section2" style="height: 84vh">
<datav-border-box2 style="height: 84vh">
<div class="inner-section top">
<!--存样瓶有无图片-->

<div class="vertical-column vert1">
<div class="imageSource">
<img
[src]="imageSource"
alt="Dynamic Image"
style="
width: calc(130px * var(--scaleX));
height: calc(400px * var(--scaleY));
margin-top: calc(160px * var(--scaleY));
margin-right: calc(20px * var(--scaleX));
"
/>
</div>
<div class="circle-widget" style="margin-left: calc(10px * var(--scaleX)); margin-top: calc(-80px * var(--scaleY))">
<div class="item">
<div class="dot gray"></div>
<p>无样空瓶</p>
</div>
<div class="item">
<div class="dot yellow"></div>
<p>全水样</p>
</div>
<div class="item">
<div class="dot green"></div>
<p>3mm样</p>
</div>
<div class="item">
<div class="dot blue"></div>
<p>0.2mm样</p>
</div>
<div class="item">
<div class="dot red"></div>
<p>停用</p>
</div>
</div>
</div>
<!--<div class="vertical-column border">-->
<div class="vertical-column vert2">
<!-- <div class="vert2top" style="height: 480px;"> -->
<div class="vert2top" style="height: calc(480px * var(--scaleY))">
<!-- 上部内容 -->
<!-- <canvas class="canvas" id="myChart" #chartCanvas></canvas> -->
<!-- 给 ECharts 图表预留的 DOM 容器,大小可以自定义 -->
<!-- <div #echartCanvas style="width: 1200px;height:450px;margin-top: 10px;" id="echartCanvas" class="echartCanvas"></div> -->
<div
#echartCanvas
style="
width: calc(765px * var(--scaleX));
height: calc(380px * var(--scaleY));
margin-top: calc(0px * var(--scaleY));
margin-left: calc(38px * var(--scaleX));
"
id="echartCanvas"
class="echartCanvas"
></div>
<div
#trackChartCanvas
style="
width: calc(900px * var(--scaleX));
height: calc(100px * var(--scaleY));
margin-top: calc(0px * var(--scaleY));
background-color: #034d6a;
"
id="trackChart"
class="trackChart"
></div>
</div>
<div class="vert2bottom" style="height: calc(400px * var(--scaleY))">
<!-- 下部内容 -->
<div
#bottomCanvas
style="
width: calc(765px * var(--scaleX));
height: calc(380px * var(--scaleY));
margin-top: calc(0px * var(--scaleY));
margin-left: calc(38px * var(--scaleX));
"
id="bottomCanvas"
class="bottomCanvas"
></div>
</div>
</div>
<!--</div>-->
<div class="vertical-column vert3">
<div
#barCanvas
style="width: calc(150px * var(--scaleX)); height: calc(400px * var(--scaleY))"
id="barCanvas"
class="barCanvas"
></div>
<div style="height: calc(80px * var(--scaleY))"></div>
<!-- <div #barbottomCanvas style="width:130px;height:450px;margin-top: 38px;" id="barbottomCanvas" class="barbottomCanvas"></div> -->
<div
#barbottomCanvas
style="width: calc(150px * var(--scaleX)); height: calc(400px * var(--scaleY))"
id="barbottomCanvas"
class="barbottomCanvas"
></div>
</div>
</div>
<!-- <div class="inner-section bottom">

</div> -->
</datav-border-box2>
</div>
</div>
<div
nz-col
nzSpan="4"
nzGutter="10"
style="overflow: hidden; display: flex; height: 100%; flex-direction: column; justify-content: space-between"
>
<datav-border-box2>
<!--仪表盘-->
<app-echarts-gauge />
</datav-border-box2>

<div *ngIf="dataLoaded">
<datav-border-box2>
<app-circle-icon
[line1]="'总样本量'"
[line2]="totalBottles.toString()"
[iconPath]="'/assets/fuel/automatic-sample-storage/circleicon01.svg'"
/>
<!--分割线-->
<div class="gradient-line"></div>

<div class="icon-group">
<div class="left-icon">
<!-- 空瓶-->
<app-circle-icon
[line1]="'无样空瓶'"
[line2]="totalNullBottles.toString()"
[iconPath]="'/assets/fuel/automatic-sample-storage/circleicon09.svg'"
/>
</div>
<div class="right-icon">
<!-- 全水样瓶-->
<app-circle-icon
[line1]="'全水样瓶'"
[line2]="totalWaterBottles.toString()"
[iconPath]="'/assets/fuel/automatic-sample-storage/circleicon06.svg'"
/>
</div>
</div>
<!--分割线-->
<div class="gradient-line"></div>
<div class="icon-group">
<div class="left-icon">
<!-- 3mm样瓶-->
<app-circle-icon
[line1]="'3mm样瓶'"
[line2]="totalThreeBottles.toString()"
[iconPath]="'/assets/fuel/automatic-sample-storage/circleicon07.svg'"
/>
</div>
<div class="right-icon">
<!-- 0.2mm样瓶-->
<app-circle-icon
[line1]="'0.2mm样瓶'"
[line2]="totalZeroTwoBottles.toString()"
[iconPath]="'/assets/fuel/automatic-sample-storage/circleicon08.svg'"
/>
</div>
</div>
<!--分割线-->
<div class="gradient-line"></div>
</datav-border-box2>
</div>

<!-- <datav-border-box2>
<div id="inputContainer"> -->
<input
type="text"
id="pointIdInput"
placeholder="请输入存样位置编号"
class="id-input"
(input)="onInput($event)"
[(ngModel)]="pointId"
[style.display]="'none'"
/>
<!-- <div class="right-container">
<app-device-control-button buttonText="存样" [pointId]="pointId"></app-device-control-button>
<app-device-control-button buttonText="取样" [pointId]="pointId"></app-device-control-button>
</div> -->
<!-- </div>
</datav-border-box2> -->
<datav-border-box2>
<app-switct-control-button (change)="onToggleChange($event)" />
</datav-border-box2>
<datav-border-box2>
<div class="section section3">
<div class="control-panel">
<!--方向盘组件-->
<app-joystick />
<div class="button-group">
<!--急停-->
<app-device-control-button buttonText="急停" [disabled]="toggleState" />
<!--取消急停-->
<app-device-control-button buttonText="取消急停" [disabled]="toggleState" />
<!--反转-->
<app-device-control-button buttonText="反转左" [disabled]="toggleState" />
<app-device-control-button buttonText="反转中" [disabled]="toggleState" />
<app-device-control-button buttonText="反转右" [disabled]="toggleState" />
<!--归位-->
<app-device-control-button buttonText="复位" [disabled]="toggleState" />
</div>
</div>
</div>
</datav-border-box2>
</div> </div
></div>

+ 265
- 0
src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.less 파일 보기

@@ -0,0 +1,265 @@

.container {
display: flex; /* 使用Flexbox */
height: 84vh; /* 全屏高度 */
}

.section {
flex-grow: 1; /* 使所有部分平均分配容器空间 */
// height: 80vh;
}
.section1 {
//background-color: lightblue; /* 第一块和第三块的背景色 */
//width: 14%; /* 占页面宽度的15% */
//display: flex;
//background-image: url('/assets/fuel/automatic-sample-storage/bothsidesbackgrounds.svg');
opacity: 0.8; /* 50% 透明度 */
}
.section3 {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
//height: 84vh;
//background-color: lightblue; /* 第一块和第三块的背景色 */
//width: 14%; /* 占页面宽度的15% */
//display: flex;
//background-image: url('/assets/fuel/automatic-sample-storage/bothsidesbackgrounds.svg');
opacity: 0.8; /* 50% 透明度 */
}

.section2 {
//background-color: lightgreen; /* 第二块的背景色 */
//width: 72%; /* 占页面宽度的70% */
display: flex;
flex-direction: column; /* 在第二块内部使用垂直布局 */
}
.section {
&.section2 {
// height: 78vh;
.inner-section {
&.top {
display: flex;
.vertical-column {
display: inline-block;
text-align: start;
//padding: 5px;
width: 100%; /* 设置竖直列占据整个宽度 */
// .top{

// height: 600px;
// }
// .bottom{
// height: 450px;
// }
&.vert1 {
width: 11%;
//height: 100%;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
flex-direction: column;
height: 100%; /* 容器的总高度,调整为需要的值 */
//overflow: hidden; /* 防止内容溢出 */
// img {
// width: 100%;
// height: 100%;
// //max-width: 100%;
// //height: auto; // 保持图片比例
// }
}
&.vert2 {
width: 78.5%;
height: 100%;
border: 2px solid transparent; /* 设置为透明,因为我们将使用 border-image */
border-image: radial-gradient(circle, #42B2FE, transparent) 1 / 1 / 0 stretch;
box-shadow: inset 0 0 20px #42B2FE; /* 添加发光效果 */
z-index: 9999; /* 确保边框效果显示在最上层 */
.vert2bottom {
flex: 1; /* 下部占据剩余空间 */
}
}
&.vert3 {
width: 10.5%;
height: 100%;
//background-color: #0000ff; /* 给竖3设置蓝色背景 */
}
}
}
}
}
}
.canvas {
width: 100%;
height: 100%;
// max-width: 1200px;
// max-height: 400px;
max-width: calc(1200px * var(--scaleX));
max-height: calc(400px * var(--scaleY));
display: block; // 确保 canvas 不受额外的空白符影响
}
.top {
//background-color: lightcoral; /* 第二块-上 的背景色 */
// background-image: url('/assets/fuel/automatic-sample-storage/maininterfacebackground.svg');
// opacity: 0.8; /* 50% 透明度 */
// background-repeat: no-repeat; /* 设置背景图片不重复 */
// background-size: cover; /* 让背景图片自适应并填满整个元素 */
flex-grow: 4; /* 因为我们想要占80%,所以这里用flex-grow的比例是4:1 */
}
.bottom {
background-image: url('/assets/fuel/automatic-sample-storage/bottombackground.svg');
opacity: 0.8; /* 50% 透明度 */
background-repeat: no-repeat; /* 设置背景图片不重复 */
background-size: cover; /* 让背景图片自适应并填满整个元素 */
//background-color: lightgrey; /* 第二块-下 的背景色 */
flex-grow: 1; /* 与上部分相比,这里使用1,总比例为4:1,即80%:20% */
}
// .inner-section.bottom {
// display: flex;
// flex-direction: row; /* 水平排列子元素 */
// }

.sub-section {
//position: relative;
flex-grow: 1; // 让每个子元素平均占据剩余空间
text-align: center; // 让子元素居中显示
// &.chart-section {
// position: absolute;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
// width: 60%; // 调整图表容器的宽度
// height: 60%; // 调整图表容器的高度
// }
.centered {
display: flex;
justify-content: center;
align-items: center;
//height: 84vh; /* 设置高度占据整个视口 */
}

}
.imageSource {
flex: 0 0 70%; /* 不允许imageSource部分伸缩,保持原有大小 */
width: calc(320px * var(--scaleX));
height: calc(300px * var(--scaleY));
display: flex;
justify-content: center; /* 图片水平居中 */
align-items: center; /* 图片垂直居中 */
}

.circle-widget {
flex: 1; /* 剩余空间全部由circle-widget占据 */
display: flex;
flex-direction: column;
//justify-content: flex-end; /* 内容靠近底部 */
//align-items: flex-end; /* 内容靠近右侧 */
padding: 10px; /* 添加内边距 */
overflow: auto; /* 如果内容过多,允许滚动 */
}

.dot {
height: calc(18px * var(--scaleX)); /* 圆点的直径减小到14px */
width: calc(18px * var(--scaleX)); /* 圆点的直径减小到14px */
border-radius: 50%; /* 圆形 */
margin: 2px 0; /* 圆点上下的间距改为2px */
}

.gray { background-color: #ccc; }
.yellow { background-color: #ffcc00; }
.green { background-color: #90ee90; }
.blue { background-color: #6495ed; }
.red{background-color: #ff0000;}
.item {
display: flex;
flex-direction: column;
align-items: center; /* 文字与圆点中心对齐 */
justify-content: center; /* 圆点垂直对齐 */
}
#inputContainer {
display: flex;
flex-direction: column; /* 设置父容器为列方向排列 */
align-items: center; /* 水平居中对齐 */
width: 100%; /* 父容器的宽度 */
}

.left-container {
width: 80%; /* 让子容器占据父容器的80%宽度 */
margin: 10px 0; /* 上下间距 */
}

.right-container {
width: 80%; /* 让子容器占据父容器的80%宽度 */
display: flex; /* 使按钮容器中的按钮并排 */
justify-content: space-between; /* 两个按钮左右分布 */
//margin: 0px 0; /* 上下间距 */
}
@darkBlueStart: #004e92; // 深蓝色开始
@darkBlueEnd: #000059; // 深蓝色结束
.id-input {
color: #fff; /* 设置字体颜色为白色 */
background: linear-gradient(45deg, transparent 5%, #004e92 5%, #000059); /* 使用直接颜色值 */
border: 0;
padding: calc(5px * var(--scaleY)); /* 调整内边距 */
font-size: calc(16px * var(--scaleX));
border-radius: calc(3px * var(--scaleX)); /* 与按钮的圆角保持一致 */
width: calc(220px * var(--scaleX)); /* 宽度与按钮一致 */
height: calc(50px * var(--scaleY)); /* 高度与按钮一致 */
box-shadow: 6px 0px 0px #00e6f6; /* 添加同样的阴影效果 */
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 3px; /* 字母间距 */
box-sizing: border-box; /* 包括内边距和边框 */
}
.id-input:focus {
border-color: #007bff; /* 设置聚焦时的边框颜色 */
outline: none; /* 去除默认的聚焦效果 */
}

app-device-control-button {
flex: 1;
margin: 0 5px; /* 水平间距,确保按钮之间有一些空隙 */
text-align: center; /* 让按钮文本居中对齐 */
}
.gradient-line {
//position:absolute;
//bottom: 20px;
//margin-top: 10px;
margin-top: calc(5px * var(--scaleY));
left: 0;
width: 100%;
height: calc(1.5px * var(--scaleY));
background: linear-gradient(to left, transparent, white 50%, white 50%, transparent); /* 两边向中间淡出的效果 */
}
.icon-group {
display: flex; /* 使用Flexbox布局 */
}

.left-icon,
.right-icon {
flex: 1; /* 使两个子元素平分父容器的宽度 */
text-align: center; /* 可选:使内容居中对齐 */
}
.control-panel {
display: flex; /* 使用Flex布局 */
align-items: center; /* 垂直居中对齐 */
justify-content: space-between; /* 水平空间在元素之间平均分配 */
gap: 10px; /* 控制摇杆和按钮组之间的间隙 */
padding: 10px; /* 可以根据需求调整内边距 */
}

.button-group {
display: flex; /* 使用Flex布局 */
flex-direction: column; /* 垂直排列按钮 */
gap: 10px; /* 按钮之间的间隙 */
}

/* 如果在不同组件中有不同指定的class名称,可以使用全局样式或特定的类名称来覆盖 */
app-joystick {
flex: 1; /* 让摇杆占据尽可能多的空间 */
max-width: 400px; /* 根据需求设置最大宽度 */
}


+ 24
- 0
src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.spec.ts 파일 보기

@@ -0,0 +1,24 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';

import { FuelAutomaticSampleStorageComponent2 } from './automatic-sample-storage2.component';

describe('FuelAutomaticSampleStorageComponent', () => {
let component: FuelAutomaticSampleStorageComponent2;
let fixture: ComponentFixture<FuelAutomaticSampleStorageComponent2>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FuelAutomaticSampleStorageComponent2]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(FuelAutomaticSampleStorageComponent2);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 1544
- 0
src/app/routes/apps/automatic-sample-storage-second/automatic-sample-storage2.component.ts
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 1
- 0
src/app/routes/apps/automatic-sample-storage-second/bar-chart.component/bar-chart.component.html 파일 보기

@@ -0,0 +1 @@
<div #chart id="chart" class="chart"></div>

+ 5
- 0
src/app/routes/apps/automatic-sample-storage-second/bar-chart.component/bar-chart.component.less 파일 보기

@@ -0,0 +1,5 @@
#chart {
display: flex;
width: 300px; /* 减少宽度 */
height: 150px; /* 减少高度 */
}

+ 67
- 0
src/app/routes/apps/automatic-sample-storage-second/bar-chart.component/bar-chart.component.ts 파일 보기

@@ -0,0 +1,67 @@
import { Component, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import * as echarts from 'echarts';
@Component({
selector: 'app-bar-chart',
templateUrl: './bar-chart.component.html',
styleUrls: ['./bar-chart.component.less'],
standalone: true
})
export class BarChartComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('chart') gaugeDiv!: ElementRef<HTMLDivElement>;
myChart!: echarts.ECharts;

ngOnInit(): void {
// this.initChart();
}
ngAfterViewInit(): void {
this.initChart();
}
ngOnDestroy(): void {
if (this.myChart) {
this.myChart.dispose();
}
}
private initChart(): void {
const myChart = echarts.init(document.getElementById('chart'));
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value', // 数值轴
min: 0,
max: 100,
interval: 20, // 设置间隔使刻度显示为20%的增量
axisLabel: {
formatter: '{value}%' // 显示格式为带百分号
}
},
yAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisTick: {
alignWithLabel: true
}
},
series: [
{
name: 'Direct',
type: 'bar',
barWidth: '60%',
data: [10, 52, 52, 52, 52, 52, 52] // 修改对应的数据
}
]
};

myChart.setOption(option);
}
}

+ 10
- 0
src/app/routes/apps/automatic-sample-storage-second/circle-icon.component/circle-icon.component.html 파일 보기

@@ -0,0 +1,10 @@
<!-- circle-icon.component.html -->
<div class="container" [style.color]="getTextColor()">
<div class="circle-icon">
<img class="icon" [src]="iconPath" alt="Icon" />
</div>
<div class="texts">
<div class="text">{{ line1 }}</div>
<div class="text">{{ line2 }}</div>
</div>
</div>

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

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