commit
a3d0b27f55
220 changed files with 23306 additions and 0 deletions
@ -0,0 +1,106 @@ |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
lerna-debug.log* |
|||
|
|||
# Diagnostic reports (https://nodejs.org/api/report.html) |
|||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json |
|||
|
|||
# Runtime data |
|||
pids |
|||
*.pid |
|||
*.seed |
|||
*.pid.lock |
|||
.angular |
|||
|
|||
# Directory for instrumented libs generated by jscoverage/JSCover |
|||
lib-cov |
|||
|
|||
# Coverage directory used by tools like istanbul |
|||
coverage |
|||
*.lcov |
|||
|
|||
# nyc test coverage |
|||
.nyc_output |
|||
|
|||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
|||
.grunt |
|||
|
|||
# Bower dependency directory (https://bower.io/) |
|||
bower_components |
|||
|
|||
# node-waf configuration |
|||
.lock-wscript |
|||
|
|||
# Compiled binary addons (https://nodejs.org/api/addons.html) |
|||
build/Release |
|||
|
|||
# Dependency directories |
|||
node_modules/ |
|||
jspm_packages/ |
|||
|
|||
# TypeScript v1 declaration files |
|||
typings/ |
|||
src/*.js |
|||
|
|||
# TypeScript cache |
|||
*.tsbuildinfo |
|||
|
|||
# Optional npm cache directory |
|||
.npm |
|||
|
|||
# Optional eslint cache |
|||
.eslintcache |
|||
|
|||
# Microbundle cache |
|||
.rpt2_cache/ |
|||
.rts2_cache_cjs/ |
|||
.rts2_cache_es/ |
|||
.rts2_cache_umd/ |
|||
|
|||
# Optional REPL history |
|||
.node_repl_history |
|||
|
|||
# Output of 'npm pack' |
|||
*.tgz |
|||
|
|||
# Yarn Integrity file |
|||
.yarn-integrity |
|||
|
|||
# dotenv environment variables file |
|||
.env |
|||
.env.test |
|||
|
|||
# parcel-bundler cache (https://parceljs.org/) |
|||
.cache |
|||
.angular |
|||
# Next.js build output |
|||
.next |
|||
|
|||
# Nuxt.js build / generate output |
|||
.nuxt |
|||
/dist |
|||
|
|||
# Gatsby files |
|||
.cache/ |
|||
# Comment in the public line in if your project uses Gatsby and *not* Next.js |
|||
# https://nextjs.org/blog/next-9-1#public-directory-support |
|||
# public |
|||
|
|||
# vuepress build output |
|||
.vuepress/dist |
|||
|
|||
# Serverless directories |
|||
.serverless/ |
|||
|
|||
# FuseBox cache |
|||
.fusebox/ |
|||
|
|||
# DynamoDB Local files |
|||
.dynamodb/ |
|||
|
|||
# TernJS port file |
|||
.tern-port |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"singleQuote": true, |
|||
"trailingComma": "all", |
|||
"useTabs": true, |
|||
"tabWidth": 4, |
|||
"semi": false, |
|||
"printWidth": 120 |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
# LicenseWebApp |
|||
|
|||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.3. |
|||
|
|||
## Development server |
|||
|
|||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. |
|||
|
|||
## Code scaffolding |
|||
|
|||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. |
|||
|
|||
## Build |
|||
|
|||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. |
|||
|
|||
## Running unit tests |
|||
|
|||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). |
|||
|
|||
## Running end-to-end tests |
|||
|
|||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. |
|||
|
|||
## Further help |
|||
|
|||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. |
|||
@ -0,0 +1,120 @@ |
|||
{ |
|||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", |
|||
"version": 1, |
|||
"newProjectRoot": "projects", |
|||
"projects": { |
|||
"license-web-app": { |
|||
"projectType": "application", |
|||
"schematics": { |
|||
"@schematics/angular:component": { |
|||
"style": "less" |
|||
} |
|||
}, |
|||
"root": "", |
|||
"sourceRoot": "src", |
|||
"prefix": "app", |
|||
"architect": { |
|||
"build": { |
|||
"builder": "@angular-devkit/build-angular:application", |
|||
"options": { |
|||
"outputPath": "dist/license-web-app", |
|||
"index": "src/index.html", |
|||
"browser": "src/main.ts", |
|||
"polyfills": [ |
|||
"zone.js" |
|||
], |
|||
"tsConfig": "tsconfig.app.json", |
|||
"inlineStyleLanguage": "less", |
|||
"assets": [ |
|||
"src/favicon.ico", |
|||
"src/assets", |
|||
{ |
|||
"glob": "**/*", |
|||
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", |
|||
"output": "/assets/" |
|||
}, |
|||
{ |
|||
"glob": "**/*", |
|||
"input": "node_modules/tinymce", |
|||
"output": "/assets/tinymce/" |
|||
} |
|||
], |
|||
"styles": [ |
|||
"src/styles.less" |
|||
], |
|||
"scripts": [ |
|||
"node_modules/tinymce/tinymce.min.js" |
|||
] |
|||
}, |
|||
"configurations": { |
|||
"production": { |
|||
"budgets": [ |
|||
{ |
|||
"type": "initial", |
|||
"maximumWarning": "10mb", |
|||
"maximumError": "10mb" |
|||
}, |
|||
{ |
|||
"type": "anyComponentStyle", |
|||
"maximumWarning": "10mb", |
|||
"maximumError": "10mb" |
|||
} |
|||
], |
|||
"outputHashing": "all" |
|||
}, |
|||
"development": { |
|||
"optimization": false, |
|||
"extractLicenses": false, |
|||
"sourceMap": true |
|||
} |
|||
}, |
|||
"defaultConfiguration": "production" |
|||
}, |
|||
"serve": { |
|||
"builder": "@angular-devkit/build-angular:dev-server", |
|||
"configurations": { |
|||
"production": { |
|||
"buildTarget": "license-web-app:build:production" |
|||
}, |
|||
"development": { |
|||
"buildTarget": "license-web-app:build:development" |
|||
} |
|||
}, |
|||
"defaultConfiguration": "development", |
|||
"options": { |
|||
"port": 6001, |
|||
"host": "0.0.0.0" |
|||
} |
|||
}, |
|||
"extract-i18n": { |
|||
"builder": "@angular-devkit/build-angular:extract-i18n", |
|||
"options": { |
|||
"buildTarget": "license-web-app: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": [] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"cli": { |
|||
"analytics": false |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,47 @@ |
|||
{ |
|||
"name": "admin-web-app", |
|||
"version": "0.0.0", |
|||
"scripts": { |
|||
"ng": "ng", |
|||
"start": "ng serve --proxy-config proxy/proxy.conf.js", |
|||
"start:remote": "ng serve --proxy-config proxy/proxy.conf.remote.js", |
|||
"build": "ng build", |
|||
"watch": "ng build --watch --configuration development", |
|||
"test": "ng test" |
|||
}, |
|||
"private": true, |
|||
"dependencies": { |
|||
"@angular/animations": "^17.0.0", |
|||
"@angular/common": "^17.0.0", |
|||
"@angular/compiler": "^17.0.0", |
|||
"@angular/core": "^17.0.0", |
|||
"@angular/forms": "^17.0.0", |
|||
"@angular/platform-browser": "^17.0.0", |
|||
"@angular/platform-browser-dynamic": "^17.0.0", |
|||
"@angular/router": "^17.0.0", |
|||
"@tinymce/tinymce-angular": "^7.0.0", |
|||
"echarts": "^5.4.3", |
|||
"ng-zorro-antd": "^17.0.1", |
|||
"rxjs": "~7.8.0", |
|||
"tinymce": "^7.0.0", |
|||
"tslib": "^2.3.0", |
|||
"zone.js": "~0.14.2" |
|||
}, |
|||
"devDependencies": { |
|||
"@angular-devkit/build-angular": "^17.0.3", |
|||
"@angular/cli": "^17.0.3", |
|||
"@angular/compiler-cli": "^17.0.0", |
|||
"@types/jasmine": "~5.1.0", |
|||
"autoprefixer": "^10.4.16", |
|||
"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", |
|||
"postcss": "^8.4.31", |
|||
"prettier": "3.1", |
|||
"tailwindcss": "^3.3.5", |
|||
"typescript": "~5.2.2" |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// const remoteTarget = "http://10.168.1.60:8300";
|
|||
const remoteTarget = 'http://47.109.27.8:8280/' |
|||
const localTarget = 'http://localhost:8300' |
|||
|
|||
const devProxy = (remote) => { |
|||
let config = {} |
|||
const target = remote ? remoteTarget : localTarget |
|||
|
|||
return { |
|||
'/api': { |
|||
target, |
|||
secure: false, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
module.exports = devProxy |
|||
@ -0,0 +1,2 @@ |
|||
const devProxy = require('./devserver') |
|||
module.exports = devProxy() |
|||
@ -0,0 +1,2 @@ |
|||
const devProxy = require("./devserver"); |
|||
module.exports = devProxy(true); |
|||
@ -0,0 +1 @@ |
|||
<router-outlet></router-outlet> |
|||
@ -0,0 +1,4 @@ |
|||
:host { |
|||
display: block; |
|||
height: 100%; |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import { Component } from '@angular/core' |
|||
import { SharedModule } from './shared/shared.module' |
|||
|
|||
@Component({ |
|||
selector: 'app-root', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './app.component.html', |
|||
styleUrl: './app.component.less', |
|||
}) |
|||
export class AppComponent {} |
|||
@ -0,0 +1,68 @@ |
|||
import { APP_INITIALIZER, ApplicationConfig, importProvidersFrom } from '@angular/core' |
|||
import { provideRouter } from '@angular/router' |
|||
|
|||
import { registerLocaleData } from '@angular/common' |
|||
import zh from '@angular/common/locales/zh' |
|||
import { FormsModule } from '@angular/forms' |
|||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http' |
|||
import { provideAnimations } from '@angular/platform-browser/animations' |
|||
import { zh_CN, provideNzI18n } from 'ng-zorro-antd/i18n' |
|||
import { routes } from './app.routes' |
|||
import { ServerPaginatedTableService } from './components/server-paginated-table' |
|||
import { LocalHttpInterceptorService } from './services' |
|||
import { PermissionModule } from './shared/permission/permission.module' |
|||
import { TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular' |
|||
|
|||
registerLocaleData(zh) |
|||
|
|||
export const APPOINTMENT_FROM = [ |
|||
{ value: 1, label: '美团' }, |
|||
{ value: 2, label: '大众点评' }, |
|||
{ value: 3, label: '小红书' }, |
|||
{ value: 4, label: '抖音' }, |
|||
] |
|||
|
|||
export function initializeApp(configService: ServerPaginatedTableService) { |
|||
return () => { |
|||
configService.initial() |
|||
configService.setConfig({ |
|||
formatPaginationData(v) { |
|||
return { |
|||
current: v.pageIndex, |
|||
pageSize: v.pageSize, |
|||
} |
|||
}, |
|||
formatServiceData(v) { |
|||
console.log('v', v) |
|||
return { |
|||
total: v.data.total, |
|||
totalPages: Math.ceil(v.data.records / 5), |
|||
data: v.data.records, |
|||
} |
|||
}, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
export const appConfig: ApplicationConfig = { |
|||
providers: [ |
|||
provideRouter(routes), |
|||
provideNzI18n(zh_CN), |
|||
importProvidersFrom(FormsModule), |
|||
importProvidersFrom(HttpClientModule), |
|||
importProvidersFrom(PermissionModule.forRoot()), |
|||
provideAnimations(), |
|||
{ |
|||
provide: HTTP_INTERCEPTORS, |
|||
useClass: LocalHttpInterceptorService, |
|||
multi: true, |
|||
}, |
|||
{ |
|||
provide: APP_INITIALIZER, |
|||
useFactory: initializeApp, |
|||
deps: [ServerPaginatedTableService], |
|||
multi: true, |
|||
}, |
|||
{ provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' }, |
|||
], |
|||
} |
|||
@ -0,0 +1,245 @@ |
|||
import { Routes } from '@angular/router' |
|||
import { DashboardComponent } from './pages/dashboard/dashboard.component' |
|||
import { LayoutComponent } from './components/layout/layout.component' |
|||
import { ProfileComponent } from './pages/profile/profile.component' |
|||
import { ProfileBasicComponent } from './pages/profile-basic/profile-basic.component' |
|||
import { ProfileAccountComponent } from './pages/profile-account/profile-account.component' |
|||
import { LoginComponent } from './pages/login/login.component' |
|||
import { authGuard } from './guards' |
|||
import { permissionGuard } from './shared/permission/permission.guard' |
|||
import { ForbiddenComponent } from './pages/forbidden/forbidden.component' |
|||
import { NotfoundComponent } from './pages/notfound/notfound.component' |
|||
import { SystemComponent } from './pages/system/system.component' |
|||
import { OrgComponent } from './pages/org/org.component' |
|||
import { OrgEmployeeComponent } from './pages/org-employee/org-employee.component' |
|||
import { OrgSettingComponent } from './pages/org-setting/org-setting.component' |
|||
import { FixedAssetComponent } from './pages/fixed-asset/fixed-asset.component' |
|||
import { FixedAssetBelongComponent } from './pages/fixed-asset/ledger/fixed-asset-belong/fixed-asset-belong.component' |
|||
import { FixedAssetEmployeeComponent } from './pages/fixed-asset/ledger/fixed-asset-employee/fixed-asset-employee.component' |
|||
import { FixedAssetOrgComponent } from './pages/fixed-asset/ledger/fixed-asset-org/fixed-asset-org.component' |
|||
import { FixedAssetCategoryComponent } from './pages/fixed-asset/ledger/fixed-asset-category/fixed-asset-category.component' |
|||
import { FixedAssetPositionComponent } from './pages/fixed-asset/ledger/fixed-asset-position/fixed-asset-position.component' |
|||
import { FixedAssetMyownComponent } from './pages/fixed-asset/ledger/fixed-asset-myown/fixed-asset-myown.component' |
|||
import { FixedAssetLedgerComponent } from './pages/fixed-asset/ledger/fixed-asset-ledger/fixed-asset-ledger.component' |
|||
import { FixedAssetSearchComponent } from './pages/fixed-asset/ledger/fixed-asset-search/fixed-asset-search.component' |
|||
import { FixedAssetManageComponent } from './pages/fixed-asset/manage/fixed-asset-manage/fixed-asset-manage.component' |
|||
import { FixedAssetManageEntryComponent } from './pages/fixed-asset/manage/fixed-asset-manage-entry/fixed-asset-manage-entry.component' |
|||
import { FixedAssetManageDistributionComponent } from './pages/fixed-asset/manage/fixed-asset-manage-distribution/fixed-asset-manage-distribution.component' |
|||
import { FixedAssetManageReturnComponent } from './pages/fixed-asset/manage/fixed-asset-manage-return/fixed-asset-manage-return.component' |
|||
import { FixedAssetManageBorrowComponent } from './pages/fixed-asset/manage/fixed-asset-manage-borrow/fixed-asset-manage-borrow.component' |
|||
import { FixedAssetManageRevertComponent } from './pages/fixed-asset/manage/fixed-asset-manage-revert/fixed-asset-manage-revert.component' |
|||
import { FixedAssetManageAllotComponent } from './pages/fixed-asset/manage/fixed-asset-manage-allot/fixed-asset-manage-allot.component' |
|||
import { FixedAssetManageTransferComponent } from './pages/fixed-asset/manage/fixed-asset-manage-transfer/fixed-asset-manage-transfer.component' |
|||
import { FixedAssetManageScrapComponent } from './pages/fixed-asset/manage/fixed-asset-manage-scrap/fixed-asset-manage-scrap.component' |
|||
|
|||
export const routes: Routes = [ |
|||
{ |
|||
path: 'login', |
|||
component: LoginComponent, |
|||
}, |
|||
{ |
|||
path: 'forbidden', |
|||
component: ForbiddenComponent, |
|||
}, |
|||
{ |
|||
path: '', |
|||
component: LayoutComponent, |
|||
canActivate: [authGuard], |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
redirectTo: 'dashboard', |
|||
}, |
|||
{ |
|||
path: 'dashboard', |
|||
component: DashboardComponent, |
|||
}, |
|||
|
|||
{ |
|||
path: 'profile', |
|||
component: ProfileComponent, |
|||
|
|||
children: [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
redirectTo: 'basic', |
|||
}, |
|||
{ |
|||
path: 'basic', |
|||
component: ProfileBasicComponent, |
|||
}, |
|||
{ |
|||
path: 'account', |
|||
component: ProfileAccountComponent, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'system', |
|||
component: SystemComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
redirectTo: 'user', |
|||
}, |
|||
{ |
|||
path: 'user', |
|||
component: SystemComponent, |
|||
canActivate: [permissionGuard], |
|||
data: { |
|||
permission: 'user', |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'org', |
|||
component: OrgComponent, |
|||
title: '人事管理', |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
redirectTo: 'setting', |
|||
}, |
|||
{ |
|||
path: 'setting', |
|||
component: OrgSettingComponent, |
|||
title: '组织架构', |
|||
}, |
|||
{ |
|||
path: 'employee', |
|||
component: OrgEmployeeComponent, |
|||
title: '员工管理', |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'fixed-asset', |
|||
title: '固资管理', |
|||
component: FixedAssetComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
redirectTo: 'ledger', |
|||
}, |
|||
{ |
|||
path: 'ledger', |
|||
title: '资产台账', |
|||
component: FixedAssetLedgerComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
pathMatch: 'full', |
|||
redirectTo: 'asset-search', |
|||
}, |
|||
{ |
|||
path: 'asset-search', |
|||
title: '资产公共台账', |
|||
component: FixedAssetSearchComponent, |
|||
}, |
|||
{ |
|||
path: 'asset-belong', |
|||
title: '归属组织台账', |
|||
component: FixedAssetBelongComponent, |
|||
}, |
|||
{ |
|||
path: 'asset-org', |
|||
title: '使用组织台账', |
|||
component: FixedAssetOrgComponent, |
|||
}, |
|||
{ |
|||
path: 'asset-position', |
|||
title: '存放位置台账', |
|||
component: FixedAssetPositionComponent, |
|||
}, |
|||
{ |
|||
path: 'asset-employee', |
|||
title: '员工查询台账', |
|||
component: FixedAssetEmployeeComponent, |
|||
}, |
|||
{ |
|||
path: 'asset-category', |
|||
title: '分类查询台账', |
|||
component: FixedAssetCategoryComponent, |
|||
}, |
|||
{ |
|||
path: 'asset-my-own', |
|||
title: '所管资产台账', |
|||
component: FixedAssetMyownComponent, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'manage', |
|||
// title: '固资管理',
|
|||
children: [ |
|||
{ |
|||
path: '', |
|||
redirectTo: 'list', |
|||
pathMatch: 'full', |
|||
}, |
|||
{ |
|||
path: 'list', |
|||
component: FixedAssetManageComponent, |
|||
title: '资产管理', |
|||
}, |
|||
{ |
|||
path: 'entry', |
|||
component: FixedAssetManageEntryComponent, |
|||
title: '资产入库', |
|||
}, |
|||
{ |
|||
path: 'distribution', |
|||
component: FixedAssetManageDistributionComponent, |
|||
title: '资产派发', |
|||
}, |
|||
{ |
|||
path: 'return', |
|||
component: FixedAssetManageReturnComponent, |
|||
title: '资产退还', |
|||
}, |
|||
{ |
|||
path: 'borrow', |
|||
component: FixedAssetManageBorrowComponent, |
|||
title: '资产借用', |
|||
}, |
|||
{ |
|||
path: 'revert', |
|||
component: FixedAssetManageRevertComponent, |
|||
title: '资产归还', |
|||
}, |
|||
{ |
|||
path: 'allot', |
|||
component: FixedAssetManageAllotComponent, |
|||
title: '资产调拨', |
|||
}, |
|||
{ |
|||
path: 'transfer', |
|||
component: FixedAssetManageTransferComponent, |
|||
title: '资产转换', |
|||
}, |
|||
{ |
|||
path: 'scrap', |
|||
component: FixedAssetManageScrapComponent, |
|||
title: '资产报废', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'not-found', |
|||
component: NotfoundComponent, |
|||
}, |
|||
{ |
|||
path: '**', |
|||
redirectTo: 'not-found', |
|||
}, |
|||
] |
|||
@ -0,0 +1,32 @@ |
|||
<div class="page-header"> |
|||
<div class="flex items-center mb-3 px-4 py-2 shadow bg-white rounded-sm"> |
|||
<div class="flex-1"> |
|||
<nz-breadcrumb> |
|||
<nz-breadcrumb-item> |
|||
<a href="/">首页</a> |
|||
</nz-breadcrumb-item> |
|||
@for (b of breadcrumbs; track $index; let last = $last) { |
|||
@if (last) { |
|||
<nz-breadcrumb-item>{{ b.label }}</nz-breadcrumb-item> |
|||
} @else { |
|||
<nz-breadcrumb-item> |
|||
<a [routerLink]="b.url">{{ b.label }}</a> |
|||
</nz-breadcrumb-item> |
|||
} |
|||
} |
|||
</nz-breadcrumb> |
|||
</div> |
|||
@if (actions) { |
|||
<div class="flex-1 flex justify-end items-center"> |
|||
<ng-container |
|||
[ngTemplateOutlet]="actions" |
|||
[ngTemplateOutletContext]="{ |
|||
$implicit: 12 |
|||
}" |
|||
/> |
|||
</div> |
|||
} |
|||
</div> |
|||
<div></div> |
|||
</div> |
|||
<ng-content></ng-content> |
|||
@ -0,0 +1,6 @@ |
|||
:host { |
|||
@apply p-3; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
import { Component, Input, OnDestroy, TemplateRef } from '@angular/core' |
|||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router' |
|||
import { BreadcrumbInterface, BreadcrumbService } from 'app/services/breadcrumb.service' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { Subscription, filter } from 'rxjs' |
|||
|
|||
@Component({ |
|||
selector: 'app-page', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './app-page.component.html', |
|||
styleUrl: './app-page.component.less', |
|||
}) |
|||
export class AppPageComponent implements OnDestroy { |
|||
constructor( |
|||
private route: ActivatedRoute, |
|||
private router: Router, |
|||
) { |
|||
this.subscriptions.add( |
|||
this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => { |
|||
this.getBreadcrumb() |
|||
}), |
|||
) |
|||
} |
|||
|
|||
private readonly subscriptions = new Subscription() |
|||
|
|||
@Input() actions?: TemplateRef<{}> |
|||
|
|||
breadcrumbs: BreadcrumbInterface[] = [] |
|||
|
|||
ngOnInit(): void { |
|||
this.getBreadcrumb() |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.subscriptions.unsubscribe() |
|||
} |
|||
|
|||
getBreadcrumb() { |
|||
this.breadcrumbs = this.buildBreadcrumb(this.route.root) |
|||
} |
|||
|
|||
private buildBreadcrumb( |
|||
route: ActivatedRoute, |
|||
url: string = '', |
|||
breadcrumbs: BreadcrumbInterface[] = [], |
|||
): BreadcrumbInterface[] { |
|||
const routeConfig = route.routeConfig |
|||
const label = routeConfig ? routeConfig.title : '' |
|||
const path = routeConfig ? routeConfig.path : '' |
|||
|
|||
const nextUrl = url !== '/' ? `${url}/${path}` : '' |
|||
|
|||
const breadcrumb = { |
|||
label: label as string, |
|||
url: nextUrl, |
|||
} |
|||
|
|||
const newBreadcrumbs = breadcrumb.label ? [...breadcrumbs, breadcrumb] : [...breadcrumbs] |
|||
|
|||
if (route.firstChild) { |
|||
return this.buildBreadcrumb(route.firstChild, nextUrl, newBreadcrumbs) |
|||
} |
|||
|
|||
return newBreadcrumbs |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<ng-container *ngFor="let item of control.errors | keyvalue"> |
|||
<ng-container *ngIf="item.value?.message;else defaultTipsTpl"> |
|||
{{item.value.message}} |
|||
</ng-container> |
|||
<ng-template #defaultTipsTpl> |
|||
<ng-container [ngSwitch]="item.key"> |
|||
<div *ngSwitchCase="'required'"> |
|||
不能为空 |
|||
</div> |
|||
<div *ngSwitchCase="'inputTrim'"> |
|||
首末字符不能为空格 |
|||
</div> |
|||
<div *ngSwitchCase="'email'"> |
|||
请输入正确的邮箱地址 |
|||
</div> |
|||
<div *ngSwitchCase="'maxlength'"> |
|||
最多输入{{item.value.requiredLength}}位字符 |
|||
</div> |
|||
<div *ngSwitchCase="'minlength'"> |
|||
最少输入{{item.value.requiredLength}}位字符 |
|||
</div> |
|||
<div *ngSwitchCase="'min'"> |
|||
不能小于{{item.value.min}} |
|||
</div> |
|||
<div *ngSwitchCase="'max'"> |
|||
不能大于{{item.value.max}} |
|||
</div> |
|||
<div *ngSwitchCase="'pattern'"> |
|||
请输入正确的内容 |
|||
</div> |
|||
<div *ngSwitchDefault> |
|||
字段验证失败 |
|||
</div> |
|||
</ng-container> |
|||
</ng-template> |
|||
</ng-container> |
|||
@ -0,0 +1,28 @@ |
|||
import { CommonModule } from "@angular/common"; |
|||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; |
|||
import { FormControl, FormGroup } from "@angular/forms"; |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: "form-error-tips", |
|||
templateUrl: "./form-error-tips.component.html", |
|||
styleUrls: ["./form-error-tips.component.less"], |
|||
imports: [CommonModule], |
|||
}) |
|||
export class FormErrorTipsComponent implements OnInit, OnChanges { |
|||
constructor() {} |
|||
|
|||
@Input() control!: FormControl; |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
// console.log("FormErrorTipsComponent changes", changes["control"]?.currentValue);
|
|||
// const formControl: FormControl = changes["control"].currentValue;
|
|||
// const root = formControl.root as FormGroup;
|
|||
// console.log("formControl.root", formControl);
|
|||
// if (formControl && !this.label) {
|
|||
// if(initLabelFormControlNameMaps.has(formControl))
|
|||
// }
|
|||
} |
|||
|
|||
ngOnInit(): void {} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
<header class="fixed z-20 left-0 top-0 right-0 shadow-md header"> |
|||
<div class="flex h-full px-4 app-width mx-auto"> |
|||
<a class="logo flex items-center"> |
|||
<!-- <img src="/assets/images/logo.png" class="w-8" /> --> |
|||
<span class="ml-2 text-2xl font-bold text-white">固资系统</span> |
|||
</a> |
|||
|
|||
<ul class="ml-8 nav flex"> |
|||
<li class="nav-item"> |
|||
<a routerLink="/dashboard" routerLinkActive="active">首页</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a routerLink="/fixed-asset" routerLinkActive="active">固资管理</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a routerLink="/org" routerLinkActive="active">人事管理</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a routerLink="/system" routerLinkActive="active">系统管理</a> |
|||
</li> |
|||
|
|||
<!-- <li |
|||
class="nav-item" |
|||
*appPermission="['jwClient:model', 'jwClient:list', 'jwClient:add', 'jwClient:edit', 'jwClient:delete']" |
|||
> |
|||
<a routerLink="/entity" routerLinkActive="active">授权实体</a> |
|||
</li> |
|||
<li |
|||
class="nav-item" |
|||
*appPermission="[ |
|||
'jwProduct:model', |
|||
'jwProduct:list', |
|||
'jwProduct:add', |
|||
'jwProduct:edit', |
|||
'jwProduct:delete' |
|||
]" |
|||
> |
|||
<a routerLink="/product" routerLinkActive="active">产品管理</a> |
|||
</li> |
|||
|
|||
<li |
|||
class="nav-item" |
|||
*appPermission="[ |
|||
'userGroup:model', |
|||
'sysUser:model', |
|||
'sysUser:list', |
|||
'sysUser:add', |
|||
'sysUser:edit', |
|||
'sysUser:delete' |
|||
]" |
|||
> |
|||
<a routerLink="/contacts" routerLinkActive="active">用户管理</a> |
|||
</li> |
|||
<li |
|||
class="nav-item" |
|||
*appPermission="['sysRole:model', 'sysRole:list', 'sysRole:add', 'sysRole:edit', 'sysRole:delete']" |
|||
> |
|||
<a routerLink="/role" routerLinkActive="active">角色管理</a> |
|||
</li> |
|||
<li class="nav-item" *appPermission="['sysLog:model']"> |
|||
<a routerLink="/log" routerLinkActive="active">操作记录</a> |
|||
</li> --> |
|||
</ul> |
|||
|
|||
<div class="ml-auto flex items-center"> |
|||
<a nz-button nzType="text" nz-dropdown [nzDropdownMenu]="menu"> |
|||
<nz-avatar nzSrc="/assets/images/avatar4.png" /> |
|||
<span class="ml-2"> |
|||
{{ api.authInfo.name || 'admin' }} |
|||
</span> |
|||
<span nz-icon nzType="down"></span> |
|||
</a> |
|||
<nz-dropdown-menu #menu="nzDropdownMenu"> |
|||
<ul nz-menu nzSelectable> |
|||
<li nz-menu-item routerLink="/profile">个人中心</li> |
|||
<li nz-menu-item (click)="logout()">退出登录</li> |
|||
</ul> |
|||
</nz-dropdown-menu> |
|||
</div> |
|||
</div> |
|||
</header> |
|||
<div class="h-12"></div> |
|||
@ -0,0 +1,32 @@ |
|||
.header { |
|||
background-color: var(--primary); |
|||
color: #fff; |
|||
|
|||
a { |
|||
color: rgba(255, 255, 255, .8); |
|||
} |
|||
} |
|||
|
|||
.nav { |
|||
&-item { |
|||
|
|||
|
|||
a { |
|||
@apply h-12 px-5 overflow-hidden text-center flex items-center; |
|||
font-size: 15px; |
|||
|
|||
&.active { |
|||
background-color: rgba(255, 255, 255, .4); |
|||
color: #fff; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
|
|||
&:hover { |
|||
a { |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
import { Component, OnInit, inject } from '@angular/core' |
|||
import { SharedModule } from '../../shared/shared.module' |
|||
import { ApiService, LocalHttpInterceptorService } from 'app/services' |
|||
import { Router } from '@angular/router' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { PermissionService } from 'app/shared/permission/permission.service' |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: 'app-header', |
|||
templateUrl: './header.component.html', |
|||
styleUrls: ['./header.component.less'], |
|||
imports: [SharedModule], |
|||
}) |
|||
export class HeaderComponent implements OnInit { |
|||
constructor( |
|||
private local: LocalHttpInterceptorService, |
|||
private router: Router, |
|||
private msg: NzMessageService, |
|||
private modal: NzModalService, |
|||
public api: ApiService, |
|||
) {} |
|||
|
|||
ngOnInit(): void {} |
|||
|
|||
permission = inject(PermissionService) |
|||
|
|||
logout() { |
|||
this.modal.confirm({ |
|||
nzTitle: '退出登录', |
|||
nzContent: '确认要退出当前账户吗?', |
|||
nzOnOk: () => { |
|||
this.permission.reset() |
|||
this.local.removeAccess() |
|||
this.router.navigate(['/login']) |
|||
this.msg.success('退出成功') |
|||
}, |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
// export * from "./form-error-tips/form-error-tips.component";
|
|||
// export * from "./server-paginated-table";
|
|||
|
|||
export * from './header/header.component' |
|||
export * from './layout/layout.component' |
|||
@ -0,0 +1,4 @@ |
|||
<app-header></app-header> |
|||
<main class="app-width mx-auto"> |
|||
<router-outlet></router-outlet> |
|||
</main> |
|||
@ -0,0 +1,15 @@ |
|||
:host { |
|||
display: block; |
|||
height: 100%; |
|||
} |
|||
|
|||
.app-width { |
|||
::ng-deep { |
|||
router-outlet+* { |
|||
display: block; |
|||
width: 100%; |
|||
height: 100%; |
|||
// overflow: hidden; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import { Component } from '@angular/core'; |
|||
import { HeaderComponent } from "../header/header.component"; |
|||
import { SharedModule } from "../../shared/shared.module"; |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: 'app-layout', |
|||
templateUrl: './layout.component.html', |
|||
styleUrls: ['./layout.component.less'], |
|||
imports: [SharedModule, HeaderComponent] |
|||
}) |
|||
export class LayoutComponent { |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<div class="flex items-center"> |
|||
<div class="flex-1"> |
|||
<nz-date-picker |
|||
class="w-full" |
|||
[nzShowTime]="showTime" |
|||
nzShowTime |
|||
[nzFormat]="_format" |
|||
[(ngModel)]="startValue" |
|||
(ngModelChange)="onStartValueChange($event)" |
|||
nzPlaceHolder="开始时间" |
|||
(nzOnOpenChange)="handleStartOpenChange($event)" |
|||
> |
|||
</nz-date-picker> |
|||
</div> |
|||
<div class="px-1">~</div> |
|||
<div class="flex-1"> |
|||
<nz-date-picker |
|||
class="w-full" |
|||
#endDatePicker |
|||
[nzShowTime]="showTime" |
|||
[(ngModel)]="endValue" |
|||
(ngModelChange)="onEndValueChange($event)" |
|||
nzPlaceHolder="结束时间" |
|||
[nzFormat]="_format" |
|||
(nzOnOpenChange)="handleEndOpenChange($event)" |
|||
> |
|||
</nz-date-picker> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,96 @@ |
|||
import { CommonModule } from '@angular/common' |
|||
import { Component, Input, ViewChild, forwardRef } from '@angular/core' |
|||
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { NzDatePickerComponent, NzDatePickerModule } from 'ng-zorro-antd/date-picker' |
|||
import { NzSpaceModule } from 'ng-zorro-antd/space' |
|||
|
|||
@Component({ |
|||
selector: 'app-range-picker', |
|||
standalone: true, |
|||
imports: [CommonModule, FormsModule, ReactiveFormsModule, NzDatePickerModule, NzSpaceModule], |
|||
templateUrl: './range-picker.component.html', |
|||
styleUrl: './range-picker.component.less', |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
multi: true, |
|||
useExisting: forwardRef(() => RangePickerComponent), |
|||
}, |
|||
], |
|||
}) |
|||
export class RangePickerComponent implements ControlValueAccessor { |
|||
constructor() {} |
|||
|
|||
@Input() showTime = false |
|||
|
|||
@Input() format?: string |
|||
|
|||
get _format() { |
|||
if (this.format) { |
|||
return this.format |
|||
} |
|||
return this.showTime ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd' |
|||
} |
|||
|
|||
startValue: Date | null = null |
|||
|
|||
endValue: Date | null = null |
|||
|
|||
@ViewChild('endDatePicker') endDatePicker!: NzDatePickerComponent |
|||
|
|||
disabledStartDate = (startValue: Date): boolean => { |
|||
if (!startValue || !this.endValue) { |
|||
return false |
|||
} |
|||
return startValue.getTime() > this.endValue.getTime() |
|||
} |
|||
|
|||
disabledEndDate = (endValue: Date): boolean => { |
|||
if (!endValue || !this.startValue) { |
|||
return false |
|||
} |
|||
return endValue.getTime() <= this.startValue.getTime() |
|||
} |
|||
|
|||
handleStartOpenChange(open: boolean): void { |
|||
if (!open) { |
|||
this.endDatePicker.open() |
|||
} |
|||
} |
|||
|
|||
handleEndOpenChange(open: boolean): void {} |
|||
|
|||
onTouched = () => {} |
|||
|
|||
onChange(v: (Date | null)[]) { |
|||
console.log('v', v) |
|||
} |
|||
|
|||
onStartValueChange(value: Date | null): void { |
|||
this.startValue = value |
|||
this.onChange([this.startValue, this.endValue]) |
|||
this.onTouched() |
|||
} |
|||
|
|||
onEndValueChange(value: Date | null): void { |
|||
this.endValue = value |
|||
this.onChange([this.startValue, this.endValue]) |
|||
this.onTouched() |
|||
} |
|||
|
|||
writeValue(v: any): void { |
|||
this.startValue = v?.[0] ? new Date(v[0]) : null |
|||
this.endValue = v?.[1] ? new Date(v[1]) : null |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.onChange = fn |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
this.onTouched = fn |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void {} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
<button |
|||
nz-button |
|||
nzType="text" |
|||
nz-popover |
|||
nzPopoverTrigger="click" |
|||
[nzPopoverContent]="innerDatePickerTpl" |
|||
nzPopoverOverlayClassName="inner-date-picker" |
|||
> |
|||
<span> |
|||
{{ date | date: 'yyyy-MM-dd' }} |
|||
</span> |
|||
<span nz-icon nzType="calendar" nzTheme="outline"></span> |
|||
</button> |
|||
|
|||
<ng-template #innerDatePickerTpl> |
|||
<nz-date-picker nzInline [(ngModel)]="date" /> |
|||
</ng-template> |
|||
@ -0,0 +1,12 @@ |
|||
::ng-deep { |
|||
.inner-date-picker { |
|||
|
|||
.ant-popover-inner-content { |
|||
padding: 0; |
|||
} |
|||
|
|||
.ant-picker-dropdown { |
|||
padding: 0; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import { Component } from '@angular/core' |
|||
|
|||
@Component({ |
|||
selector: 'app-date-query', |
|||
|
|||
templateUrl: './date-query.component.html', |
|||
styleUrl: './date-query.component.less', |
|||
}) |
|||
export class DateQueryComponent { |
|||
constructor() {} |
|||
|
|||
date?: Date |
|||
range: Array<Date | null> = [] |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
export * from './server-paginated-table.component' |
|||
export * from './server-paginated-table.module' |
|||
export * from './server-paginated-table.service' |
|||
export * from './table-action.directive' |
|||
export * from './table-form.directive' |
|||
export * from './query-item/query-item.component' |
|||
@ -0,0 +1,8 @@ |
|||
<nz-form-item> |
|||
@if (label) { |
|||
<nz-form-label>{{ label }}</nz-form-label> |
|||
} |
|||
<nz-form-control class="query-control"> |
|||
<ng-content></ng-content> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
@ -0,0 +1,7 @@ |
|||
.query-control { |
|||
::ng-deep { |
|||
input { |
|||
width: 100px; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
import { Component, Input } from '@angular/core' |
|||
|
|||
@Component({ |
|||
selector: 'app-query-item', |
|||
templateUrl: './query-item.component.html', |
|||
styleUrl: './query-item.component.less', |
|||
}) |
|||
export class QueryItemComponent { |
|||
@Input() label?: string |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
<div> |
|||
@if (formGroup) { |
|||
<form [formGroup]="formGroup" (ngSubmit)="onSubmit($event)"> |
|||
<ng-template [ngTemplateOutlet]="topTpl"></ng-template> |
|||
</form> |
|||
} @else { |
|||
<form (ngSubmit)="onSubmit($event)"> |
|||
<ng-template [ngTemplateOutlet]="topTpl"></ng-template> |
|||
</form> |
|||
} |
|||
</div> |
|||
<ng-template #topTpl> |
|||
@if (formContentChild) { |
|||
<div class="formgroup-container"> |
|||
<div class="form-items"> |
|||
<div class="inline-flex flex-wrap query-wrapper"> |
|||
<ng-template [ngTemplateOutlet]="formContentChild"></ng-template> |
|||
<nz-space class="inline-flex"> |
|||
<button *nzSpaceItem nz-button nzType="primary">查询</button> |
|||
<button *nzSpaceItem nz-button type="button" (click)="reset()">重置</button> |
|||
</nz-space> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
<!-- @if (actionContentChild) { |
|||
<div class="action-container pb-3"> |
|||
<ng-template [ngTemplateOutlet]="actionContentChild"> </ng-template> |
|||
</div> |
|||
} --> |
|||
</ng-template> |
|||
|
|||
<ng-template #columnsSettingTpl> |
|||
<ul> |
|||
@for (c of options.columns; track $index) { |
|||
<li> |
|||
<label nz-checkbox [(nzChecked)]="c.visible" (nzCheckedChange)="onColumnsChange()"> |
|||
{{ c.title }} |
|||
</label> |
|||
</li> |
|||
} |
|||
</ul> |
|||
</ng-template> |
|||
|
|||
<nz-card [nzBordered]="false" [nzSize]="'small'" class="shadow"> |
|||
<nz-table |
|||
#tableRef |
|||
class="server-paginated-table" |
|||
nzShowSizeChanger |
|||
[nzShowTotal]="totalTpl" |
|||
[nzData]="pagination.data" |
|||
[nzFrontPagination]="options.config.frontPagination" |
|||
[nzLoading]="pagination.loading" |
|||
[nzTotal]="pagination.total" |
|||
[(nzPageSize)]="pagination.pageSize" |
|||
[(nzPageIndex)]="pagination.pageIndex" |
|||
(nzPageIndexChange)="onPageChange()" |
|||
(nzPageSizeChange)="onPageChange()" |
|||
> |
|||
@if (actionContentChild && selected.size > 0) { |
|||
<div class="table-selected-action flex items-center"> |
|||
<span>已选择 ({{ selected.size }}) 项目</span> |
|||
<ng-template [ngTemplateOutlet]="actionContentChild"></ng-template> |
|||
</div> |
|||
} |
|||
|
|||
<thead> |
|||
<tr> |
|||
@if (options.config.selectable) { |
|||
<th |
|||
nzWidth="60px" |
|||
nzLabel="Select all" |
|||
[nzChecked]="selected.size > 0 && tableRef.data.length > 0" |
|||
[nzIndeterminate]="indeterminate" |
|||
(nzCheckedChange)="onAllChecked($event)" |
|||
></th> |
|||
} |
|||
@for (th of options.columns; track th.key) { |
|||
@if (th.visible) { |
|||
<th [nzColumnKey]="th.key" [nzWidth]="th.width ?? null">{{ th.title }}</th> |
|||
} |
|||
} |
|||
@if (options.operate.length > 0) { |
|||
<th nzWidth="100px">操作</th> |
|||
} |
|||
<th nzWidth="60px"> |
|||
<div |
|||
class="columns-setting w-full cursor-pointer" |
|||
(click)="handleColumnsSetting(columnsSettingTpl)" |
|||
> |
|||
<i nz-icon nzType="setting" class=""></i> |
|||
</div> |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@for (data of tableRef.data; track $index) { |
|||
<tr> |
|||
@if (options.config.selectable) { |
|||
<td |
|||
[nzChecked]="selected.has(String(data[options.config.rowKey!]))" |
|||
(nzCheckedChange)="onRowSelected($event, data)" |
|||
></td> |
|||
} |
|||
@for (td of options.columns; track td.key) { |
|||
<ng-container> |
|||
@if (td.visible) { |
|||
<td nzEllipsis [attr.title]="renderTitle(data[td.key])"> |
|||
<ng-container [ngSwitch]="td.key"> |
|||
<ng-container *ngSwitchDefault> |
|||
@if (renderColumn) { |
|||
<ng-container |
|||
[ngTemplateOutlet]="renderColumn" |
|||
[ngTemplateOutletContext]="{ |
|||
$implicit: data[td.key], |
|||
key: td.key, |
|||
row: data, |
|||
column: td |
|||
}" |
|||
> |
|||
</ng-container> |
|||
} @else { |
|||
{{ data[td.key] }} |
|||
} |
|||
</ng-container> |
|||
</ng-container> |
|||
</td> |
|||
} @else {} |
|||
</ng-container> |
|||
} |
|||
|
|||
@if (options.operate.length > 0) { |
|||
<td> |
|||
<button nz-button nzType="text" nz-dropdown [nzDropdownMenu]="menu"> |
|||
<span nz-icon nzType="more" nzTheme="outline" class="more-icon"></span> |
|||
</button> |
|||
<nz-dropdown-menu #menu="nzDropdownMenu"> |
|||
<ul nz-menu class="operate-list"> |
|||
@for (operate of options.operate; track operate.title) { |
|||
@if (operate?.visible(data)) { |
|||
<li |
|||
nz-menu-item |
|||
(click)="onOperateClick(operate, data)" |
|||
[routerLink]="operate.link" |
|||
[nzDanger]="operate.danger" |
|||
[nzDisabled]="operate.disabled" |
|||
> |
|||
{{ operate.title }} |
|||
</li> |
|||
} |
|||
} |
|||
</ul> |
|||
</nz-dropdown-menu> |
|||
</td> |
|||
} |
|||
<td></td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</nz-table> |
|||
</nz-card> |
|||
|
|||
<ng-template #totalTpl let-total> 共{{ total }}条 </ng-template> |
|||
@ -0,0 +1,90 @@ |
|||
.form-items { |
|||
|
|||
|
|||
::ng-deep { |
|||
.ant-form-item { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.ant-form-item-label { |
|||
text-align: left; |
|||
} |
|||
|
|||
.ant-divider-horizontal { |
|||
margin: 12px 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.columns-setting { |
|||
background-color: #f2f3f5; |
|||
} |
|||
|
|||
.query-wrapper { |
|||
::ng-deep { |
|||
&>* { |
|||
@apply mr-3 mb-3; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.formgroup-container { |
|||
::ng-deep { |
|||
nz-form-item { |
|||
padding-left: 10px; |
|||
background-color: #fff; |
|||
border-radius: 4px; |
|||
|
|||
input { |
|||
border-color: transparent !important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.server-paginated-table { |
|||
|
|||
.table-selected-action { |
|||
height: 41px; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 60px; |
|||
right: 0; |
|||
background-color: #f2f3f5; |
|||
} |
|||
|
|||
::ng-deep { |
|||
|
|||
table { |
|||
table-layout: fixed !important; |
|||
} |
|||
|
|||
.ant-table-thead>tr>th { |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.ant-table-thead>tr>th, |
|||
.ant-table-tbody>tr>td, |
|||
.ant-table tfoot>tr>th, |
|||
.ant-table tfoot>tr>td { |
|||
padding: 9px 16px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
.operate-list { |
|||
::ng-deep { |
|||
&>li { |
|||
min-width: 150px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.more-icon { |
|||
::ng-deep { |
|||
svg { |
|||
transform: scale(1.3); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,359 @@ |
|||
import { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ContentChild, |
|||
Input, |
|||
OnChanges, |
|||
OnInit, |
|||
SimpleChanges, |
|||
TemplateRef, |
|||
} from '@angular/core' |
|||
import { |
|||
AnyObject, |
|||
CacheStateInterface, |
|||
FetchDataInterface, |
|||
PaginationInterface, |
|||
ServerPaginatedTableService, |
|||
TableColumnsInterface, |
|||
persistKey, |
|||
} from './server-paginated-table.service' |
|||
import { finalize, map } from 'rxjs' |
|||
import { FormGroup } from '@angular/forms' |
|||
import { TableFormDirective } from './table-form.directive' |
|||
import { Router } from '@angular/router' |
|||
import { TableActionDirective } from './table-action.directive' |
|||
import { NzDrawerService } from 'ng-zorro-antd/drawer' |
|||
|
|||
export interface TableOperationInterface { |
|||
title: string |
|||
link?: string[] |
|||
premissions?: string[] |
|||
danger?: boolean |
|||
disabled?: boolean |
|||
onClick?: (v: any) => void |
|||
visible?: (v: any) => boolean |
|||
} |
|||
|
|||
export interface TableOptionConfigInterface { |
|||
rowKey?: string |
|||
cacheUrls: string[] |
|||
cacheKey?: string |
|||
manual?: boolean |
|||
frontPagination?: boolean |
|||
selectable?: boolean |
|||
} |
|||
|
|||
export class TableOption { |
|||
constructor(fetchData: FetchDataInterface) |
|||
constructor( |
|||
fetchData: FetchDataInterface, |
|||
columns?: TableColumnsInterface[], |
|||
operate?: TableOperationInterface[], |
|||
config?: Partial<TableOptionConfigInterface>, |
|||
) |
|||
constructor(fetchData: FetchDataInterface, config: Partial<TableOptionConfigInterface>) |
|||
constructor( |
|||
fetchData: FetchDataInterface, |
|||
configOrColumns?: Partial<TableOptionConfigInterface> | TableColumnsInterface[], |
|||
operate?: TableOperationInterface[], |
|||
config?: Partial<TableOptionConfigInterface>, |
|||
) { |
|||
this.fetchData = fetchData |
|||
let _config = config |
|||
if (Array.isArray(configOrColumns)) { |
|||
this.setColumn(configOrColumns) |
|||
} else if (configOrColumns) { |
|||
_config = configOrColumns |
|||
} |
|||
if (operate) { |
|||
this.setRowOperate(operate) |
|||
} |
|||
if (_config) { |
|||
this.setConfig(_config) |
|||
} |
|||
} |
|||
|
|||
config: TableOptionConfigInterface = { |
|||
cacheKey: persistKey + location.pathname, |
|||
cacheUrls: [], |
|||
} |
|||
|
|||
fetchData: FetchDataInterface |
|||
|
|||
columns: TableColumnsInterface[] = [] |
|||
|
|||
operate: TableOperationInterface[] = [] |
|||
|
|||
ref!: ServerPaginatedTableComponent |
|||
|
|||
columnsCacheKey = 'SERVER_PAGINATED_V1' |
|||
|
|||
setCommponentRef(ref: ServerPaginatedTableComponent) { |
|||
this.ref = ref |
|||
} |
|||
|
|||
setConfig(config: Partial<TableOptionConfigInterface>): TableOption { |
|||
this.config = { |
|||
...this.config, |
|||
rowKey: this.config.rowKey ?? config.rowKey ?? 'id', |
|||
...(config ?? {}), |
|||
} |
|||
return this |
|||
} |
|||
|
|||
setColumn(columns: TableColumnsInterface[]): TableOption { |
|||
const cachedColumns = this.getCachedColumns() |
|||
const cacheKey = this.config.cacheKey! |
|||
const currentTableColumns = cachedColumns[cacheKey] ?? [] |
|||
console.log('currentTableColumns', cachedColumns, cacheKey, currentTableColumns) |
|||
this.columns = columns.map((i) => { |
|||
const cachedItem = currentTableColumns.find((c) => c.key === i.key) |
|||
const visible = cachedItem?.visible ?? i.visible |
|||
return { |
|||
...i, |
|||
...cachedItem, |
|||
visible: typeof visible !== 'boolean' ? true : visible, |
|||
} |
|||
}) |
|||
return this |
|||
} |
|||
|
|||
setRowOperate(operate: TableOperationInterface[]): TableOption { |
|||
this.operate = operate.map((i) => { |
|||
return { |
|||
...i, |
|||
visible: i.visible ?? (() => true), |
|||
} |
|||
}) |
|||
return this |
|||
} |
|||
|
|||
getCachedColumns() { |
|||
let cachedColumns: null | Record<string, TableColumnsInterface[]> = null |
|||
try { |
|||
const str = localStorage.getItem(this.columnsCacheKey) |
|||
if (str) { |
|||
cachedColumns = JSON.parse(str) as Record<string, TableColumnsInterface[]> |
|||
} |
|||
} catch (error) {} |
|||
return cachedColumns ?? {} |
|||
} |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'app-server-paginated-table', |
|||
templateUrl: './server-paginated-table.component.html', |
|||
styleUrls: ['./server-paginated-table.component.less'], |
|||
}) |
|||
export class ServerPaginatedTableComponent implements OnInit, OnChanges { |
|||
constructor( |
|||
private cdr: ChangeDetectorRef, |
|||
private pageTable: ServerPaginatedTableService, |
|||
private router: Router, |
|||
private drawer: NzDrawerService, |
|||
) {} |
|||
|
|||
@Input() options!: TableOption |
|||
|
|||
/** |
|||
* 查询表单 |
|||
* https://github.com/angular/angular/issues/13761
|
|||
*/ |
|||
@Input() formGroup?: FormGroup |
|||
|
|||
@Input() simpleSearch = false |
|||
|
|||
@Input() initValue: AnyObject = {} |
|||
|
|||
/** |
|||
* 在表格模式下渲染具体每一个列的模板。 |
|||
* $implicit:dataItem[col.key] `具体的值`、 |
|||
* key:col.key `字段名`、 |
|||
* row:dataItem `当前行数据`、 |
|||
* column:col `当前列`、 |
|||
*/ |
|||
@Input() renderColumn?: TemplateRef<{}> |
|||
|
|||
/** |
|||
* 不使用表格模式,自定义渲染 |
|||
*/ |
|||
@Input() renderItem?: TemplateRef<{}> |
|||
|
|||
@ContentChild(TableFormDirective, { static: true, read: TemplateRef }) |
|||
formContentChild!: TemplateRef<any> |
|||
|
|||
@ContentChild(TableActionDirective, { static: true, read: TemplateRef }) |
|||
actionContentChild!: TemplateRef<{ $implicit: number }> |
|||
|
|||
pagination: PaginationInterface = { |
|||
loading: false, |
|||
data: [], |
|||
total: 0, |
|||
totalPages: 0, |
|||
pageSize: 10, |
|||
pageIndex: 1, |
|||
} |
|||
|
|||
String = String |
|||
|
|||
selected = new Set<string>() |
|||
|
|||
indeterminate = false |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void {} |
|||
|
|||
ngOnInit(): void { |
|||
if (!this.options) { |
|||
throw new Error('请设置正确的options') |
|||
} |
|||
this.options.setCommponentRef(this) |
|||
// this.options.config.cacheKey = this.cacheKeyCreator()
|
|||
this.setCachedDataToState() |
|||
if (!this.options.config.manual) { |
|||
this.run() |
|||
} |
|||
} |
|||
|
|||
private setCachedDataToState() { |
|||
const cachedState = this.pageTable.getCachedState() |
|||
if (cachedState) { |
|||
const cacheKey = this.options.config.cacheKey! |
|||
const currentPath = cachedState.paths[cacheKey] |
|||
const currentState = cachedState.state[cacheKey] |
|||
if (currentState && Array.isArray(currentPath) && currentPath.includes(this.router.url)) { |
|||
const { page, query } = currentState |
|||
this.pagination.pageIndex = page.pageIndex |
|||
this.pagination.pageSize = page.pageSize |
|||
this.formGroup?.patchValue(query) |
|||
} |
|||
} |
|||
} |
|||
|
|||
renderTitle(d: any) { |
|||
return typeof d === 'number' || typeof d === 'string' ? d : '' |
|||
} |
|||
|
|||
private saveCacheState() { |
|||
const cachedState = this.pageTable.getCachedState() |
|||
|
|||
const { pageIndex, pageSize } = this.pagination |
|||
const query = this.formGroup?.value ?? {} |
|||
const cacheKey = this.options.config.cacheKey! |
|||
|
|||
const state: CacheStateInterface = { |
|||
paths: { |
|||
...cachedState?.paths, |
|||
[cacheKey]: this.options.config.cacheUrls.concat(this.router.url), |
|||
}, |
|||
state: { |
|||
...cachedState?.state, |
|||
[cacheKey]: { |
|||
page: { pageIndex, pageSize }, |
|||
query, |
|||
}, |
|||
}, |
|||
} |
|||
sessionStorage.setItem(persistKey, JSON.stringify(state)) |
|||
} |
|||
|
|||
private cacheKeyCreator() { |
|||
if (this.options.config.cacheKey) { |
|||
return this.options.config.cacheKey |
|||
} |
|||
|
|||
return persistKey + this.router.url.replaceAll('/', '_') |
|||
} |
|||
|
|||
private run() { |
|||
const { pageIndex, pageSize } = this.pagination |
|||
const page = this.pageTable.globalConfig.formatPaginationData({ pageIndex, pageSize }) |
|||
const query = this.formGroup?.value ?? {} |
|||
|
|||
this.pagination.loading = true |
|||
this.options |
|||
.fetchData(page, query) |
|||
.pipe( |
|||
map(this.pageTable.globalConfig.formatServiceData), |
|||
finalize(() => { |
|||
this.saveCacheState() |
|||
this.pagination.loading = false |
|||
}), |
|||
) |
|||
.subscribe((res) => { |
|||
this.pagination = { |
|||
...this.pagination, |
|||
...res, |
|||
} |
|||
}) |
|||
} |
|||
|
|||
reload() { |
|||
this.run() |
|||
} |
|||
|
|||
search() { |
|||
this.pagination.pageIndex = 1 |
|||
this.reload() |
|||
} |
|||
|
|||
reset() { |
|||
this.formGroup?.reset(this.initValue) |
|||
this.search() |
|||
} |
|||
|
|||
onPageChange() { |
|||
if (!this.options.config.frontPagination) { |
|||
this.reload() |
|||
} |
|||
} |
|||
|
|||
onOperateClick(item: TableOperationInterface, data: any) { |
|||
if (item.onClick) { |
|||
item?.onClick(data) |
|||
} |
|||
} |
|||
|
|||
onSubmit(e: Event) { |
|||
this.search() |
|||
} |
|||
|
|||
handleColumnsSetting(nzContent: TemplateRef<any>) { |
|||
this.drawer.create({ |
|||
nzTitle: '表头设置', |
|||
nzContent, |
|||
}) |
|||
} |
|||
|
|||
onColumnsChange() { |
|||
const cacheKey = this.options.config.cacheKey! |
|||
const cachedColumns = this.options.getCachedColumns() |
|||
const newCachedColumns = { |
|||
...cachedColumns, |
|||
[cacheKey]: this.options.columns, |
|||
} |
|||
localStorage.setItem(this.options.columnsCacheKey, JSON.stringify(newCachedColumns)) |
|||
} |
|||
|
|||
onAllChecked(checked: boolean) { |
|||
if (checked) { |
|||
const rowKey = this.options.config.rowKey! |
|||
this.pagination.data.forEach((item: any) => { |
|||
this.selected.add(String(item[rowKey])) |
|||
}) |
|||
} else { |
|||
this.selected.clear() |
|||
} |
|||
} |
|||
|
|||
onRowSelected(selected: boolean, row: any) { |
|||
const id = String(row[this.options.config.rowKey!]) |
|||
console.log('selected', selected, id) |
|||
if (selected) { |
|||
this.selected.add(id) |
|||
} else { |
|||
this.selected.delete(id) |
|||
} |
|||
console.log('this.selected', this.selected) |
|||
this.indeterminate = this.selected.size > 0 && this.selected.size < this.pagination.data.length |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
import { NgModule } from '@angular/core' |
|||
import { CommonModule } from '@angular/common' |
|||
import { NzTableModule } from 'ng-zorro-antd/table' |
|||
import { NzSpaceModule } from 'ng-zorro-antd/space' |
|||
import { NzButtonModule } from 'ng-zorro-antd/button' |
|||
import { NzDividerModule } from 'ng-zorro-antd/divider' |
|||
import { NzIconModule } from 'ng-zorro-antd/icon' |
|||
import { NzCardModule } from 'ng-zorro-antd/card' |
|||
import { NzFormModule } from 'ng-zorro-antd/form' |
|||
import { NzPopoverModule } from 'ng-zorro-antd/popover' |
|||
import { NzDropDownModule } from 'ng-zorro-antd/dropdown' |
|||
import { NzDrawerModule } from 'ng-zorro-antd/drawer' |
|||
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox' |
|||
import { NzDatePickerModule } from 'ng-zorro-antd/date-picker' |
|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
|||
import { ServerPaginatedTableComponent } from './server-paginated-table.component' |
|||
import { QueryItemComponent } from './query-item/query-item.component' |
|||
import { DateQueryComponent } from './date-query/date-query.component' |
|||
import { TableFormDirective } from './table-form.directive' |
|||
import { TableActionDirective } from './table-action.directive' |
|||
import { RouterModule } from '@angular/router' |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
ServerPaginatedTableComponent, |
|||
QueryItemComponent, |
|||
DateQueryComponent, |
|||
TableFormDirective, |
|||
TableActionDirective, |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
RouterModule, |
|||
FormsModule, |
|||
ReactiveFormsModule, |
|||
NzTableModule, |
|||
NzSpaceModule, |
|||
NzButtonModule, |
|||
NzDividerModule, |
|||
NzIconModule, |
|||
NzDropDownModule, |
|||
NzCardModule, |
|||
NzFormModule, |
|||
NzPopoverModule, |
|||
NzDatePickerModule, |
|||
NzDrawerModule, |
|||
NzCheckboxModule, |
|||
], |
|||
exports: [ |
|||
ServerPaginatedTableComponent, |
|||
QueryItemComponent, |
|||
DateQueryComponent, |
|||
TableFormDirective, |
|||
TableActionDirective, |
|||
], |
|||
}) |
|||
export class ServerPaginatedTableModule {} |
|||
@ -0,0 +1,95 @@ |
|||
import { Injectable } from '@angular/core' |
|||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router' |
|||
import { Observable, filter } from 'rxjs' |
|||
|
|||
export type AnyObject = Record<string, any> |
|||
|
|||
export interface ServerPaginatedTableConfigInterface { |
|||
formatServiceData: (v: any) => TableRequiredDataInterface |
|||
formatPaginationData: (v: PageSizeAndIndexInterface) => any |
|||
} |
|||
|
|||
export interface TableRequiredDataInterface<T = any> { |
|||
total: number |
|||
totalPages: number |
|||
data: T |
|||
} |
|||
|
|||
export type FetchDataInterface = (pagination: PageSizeAndIndexInterface, query: Record<string, any>) => Observable<any> |
|||
|
|||
export type PageSizeAndIndexInterface = { |
|||
pageSize: number |
|||
pageIndex: number |
|||
} |
|||
|
|||
export interface PaginationInterface extends TableRequiredDataInterface, PageSizeAndIndexInterface { |
|||
loading: boolean |
|||
} |
|||
|
|||
export interface TableColumnsInterface { |
|||
key: string |
|||
title: string |
|||
visible?: boolean |
|||
width?: string |
|||
sort?: boolean |
|||
order?: number |
|||
disabled?: boolean |
|||
coverStorage?: boolean |
|||
} |
|||
|
|||
export interface CacheStateInterface { |
|||
paths: Record<string, string[]> |
|||
state: Record< |
|||
string, |
|||
{ |
|||
query: AnyObject |
|||
page: PageSizeAndIndexInterface |
|||
} |
|||
> |
|||
} |
|||
|
|||
export const persistKey = 'SERVER_PAGINATED_TABLE_V1' |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class ServerPaginatedTableService { |
|||
constructor(private router: Router) {} |
|||
|
|||
globalConfig: ServerPaginatedTableConfigInterface = { |
|||
formatServiceData: (v: any) => v, |
|||
formatPaginationData: (v: any) => v, |
|||
} |
|||
|
|||
initial() { |
|||
this.router.events.pipe(filter((f): f is NavigationEnd => f instanceof NavigationEnd)).subscribe((e) => { |
|||
const cachedState = this.getCachedState() |
|||
if (cachedState) { |
|||
Object.entries(cachedState.paths).forEach(([cacheKey, persistPaths]) => { |
|||
if (!persistPaths.includes(e.url)) { |
|||
Reflect.deleteProperty(cachedState.state, cacheKey) |
|||
sessionStorage.setItem(persistKey, JSON.stringify(cachedState)) |
|||
} |
|||
}) |
|||
} |
|||
}) |
|||
return this |
|||
} |
|||
|
|||
getCachedState(): CacheStateInterface | null { |
|||
let cachedState: null | CacheStateInterface = null |
|||
try { |
|||
const str = sessionStorage.getItem(persistKey) |
|||
if (str) { |
|||
cachedState = JSON.parse(str) as CacheStateInterface |
|||
} |
|||
} catch (error) {} |
|||
|
|||
return cachedState |
|||
} |
|||
|
|||
setConfig(config: Partial<ServerPaginatedTableConfigInterface>) { |
|||
this.globalConfig = { ...this.globalConfig, ...config } |
|||
return this |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import { Directive, TemplateRef } from "@angular/core"; |
|||
|
|||
@Directive({ |
|||
selector: "[appTableAction]", |
|||
exportAs: "appTableAction", |
|||
}) |
|||
export class TableActionDirective { |
|||
constructor(public templateRef: TemplateRef<{}>) {} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import { Directive, TemplateRef } from "@angular/core"; |
|||
|
|||
@Directive({ |
|||
selector: "[appTableForm]", |
|||
exportAs: "appTableForm", |
|||
}) |
|||
export class TableFormDirective { |
|||
constructor(public templateRef: TemplateRef<{}>) {} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
<form nz-form [formGroup]="formGroup"> |
|||
@if (!data.value) { |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired>用户名</nz-form-label> |
|||
<nz-form-control nzSpan="16" [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入用户名" formControlName="username" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired>密码</nz-form-label> |
|||
<nz-form-control nzSpan="16" [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input type="password" placeholder="请输入密码" formControlName="password" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
} |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired>姓名</nz-form-label> |
|||
<nz-form-control nzSpan="16" [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入姓名" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired>角色</nz-form-label> |
|||
<nz-form-control nzSpan="16" [nzErrorTip]="formErrorTipsTpl"> |
|||
<nz-select nzPlaceHolder="请选择角色" formControlName="roleId"> |
|||
@for (item of data.roleList; track item.id) { |
|||
<nz-option [nzValue]="item.id" [nzLabel]="item.roleName"></nz-option> |
|||
} |
|||
</nz-select> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4">备注</nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<textarea nz-input placeholder="请输入备注" formControlName="remark"></textarea> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<!-- <nz-form-item> |
|||
<nz-form-label nzSpan="4">登录限制</nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<nz-switch></nz-switch> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4">限制规则</nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<div> |
|||
<nz-input-group nzAddOnBefore="登录失败达到" nzAddOnAfter="次"> |
|||
<input type="number" nz-input /> |
|||
</nz-input-group> |
|||
</div> |
|||
<div class="mt-2"> |
|||
<ng-template #nzAddOnAfterTpl> |
|||
<nz-select class="!w-20"> |
|||
<nz-option nzValue="m" nzLabel="分钟"></nz-option> |
|||
<nz-option nzValue="h" nzLabel="小时"></nz-option> |
|||
<nz-option nzValue="d" nzLabel="天"></nz-option> |
|||
</nz-select> |
|||
</ng-template> |
|||
<nz-input-group nzAddOnBefore="锁定账号" [nzAddOnAfter]="nzAddOnAfterTpl"> |
|||
<input type="number" nz-input /> |
|||
</nz-input-group> |
|||
</div> |
|||
</nz-form-control> |
|||
</nz-form-item> --> |
|||
<!-- <nz-form-item> |
|||
<nz-form-control nzOffset="4"> |
|||
<button nz-button nzType="primary">保存</button> |
|||
</nz-form-control> |
|||
</nz-form-item> --> |
|||
</form> |
|||
|
|||
<ng-template #formErrorTipsTpl let-control> |
|||
<form-error-tips [control]="control"></form-error-tips> |
|||
</ng-template> |
|||
@ -0,0 +1,46 @@ |
|||
import { Component, OnInit, inject } from '@angular/core' |
|||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms' |
|||
import { RoleListDTO } from 'app/services/api.dto' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { FormValidators } from 'app/utils' |
|||
import { NZ_MODAL_DATA } from 'ng-zorro-antd/modal' |
|||
|
|||
@Component({ |
|||
selector: 'app-user-form', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './user-form.component.html', |
|||
styleUrl: './user-form.component.less', |
|||
}) |
|||
export class UserFormComponent implements OnInit { |
|||
constructor(private fb: FormBuilder) {} |
|||
|
|||
data = inject<{ roleList: RoleListDTO[]; value: {} }>(NZ_MODAL_DATA) |
|||
|
|||
formGroup!: FormGroup |
|||
|
|||
ngOnInit(): void { |
|||
this.formGroup = this.fb.group({ |
|||
name: new FormControl('', [FormValidators.required('请输入姓名'), FormValidators.maxLength(30)]), |
|||
roleId: new FormControl('', [FormValidators.required('请选择角色')]), |
|||
remark: new FormControl(''), |
|||
}) |
|||
if (this.data.value) { |
|||
this.formGroup.patchValue(this.data.value) |
|||
} else { |
|||
this.formGroup.addControl( |
|||
'username', |
|||
new FormControl('', [FormValidators.required('请输入用户名'), FormValidators.maxLength(30)]), |
|||
) |
|||
this.formGroup.addControl('password', new FormControl('', [FormValidators.required('请输入密码')])) |
|||
} |
|||
} |
|||
|
|||
public getValues() { |
|||
let values = null |
|||
if (FormValidators.validateFormGroup(this.formGroup)) { |
|||
values = this.formGroup.value |
|||
} |
|||
return values |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
import { inject } from '@angular/core' |
|||
import { CanActivateFn, Router, UrlTree } from '@angular/router' |
|||
import { ApiService, LocalHttpInterceptorService } from 'app/services' |
|||
import { map } from 'rxjs' |
|||
|
|||
export const authGuard: CanActivateFn = (route, state) => { |
|||
const local = inject(LocalHttpInterceptorService) |
|||
const accessData = local.getAccess() |
|||
let token = accessData?.access_token |
|||
if (token) { |
|||
return true |
|||
} |
|||
const router = inject(Router) |
|||
return router.createUrlTree(['/login']) |
|||
// const api = inject(ApiService)
|
|||
// if (!localStorage.getItem('sc')) {
|
|||
// return router.createUrlTree(['/login'])
|
|||
// }
|
|||
// return api.getAuthInfo(true).pipe(map((r) => !!r))
|
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from "./auth.guard"; |
|||
@ -0,0 +1,12 @@ |
|||
<form nz-form [formGroup]="formGroup"> |
|||
<nz-form-item> |
|||
<nz-form-label> 部门名称 </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入部门名称" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
|
|||
<ng-template #formErrorTipsTpl let-control> |
|||
<form-error-tips [control]="control"></form-error-tips> |
|||
</ng-template> |
|||
@ -0,0 +1,38 @@ |
|||
import { Component, OnInit, inject } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { FormValidators } from 'app/utils' |
|||
import { NZ_MODAL_DATA } from 'ng-zorro-antd/modal' |
|||
|
|||
@Component({ |
|||
selector: 'app-contact-group-form', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './contact-group-form.component.html', |
|||
styleUrl: './contact-group-form.component.less', |
|||
}) |
|||
export class ContactGroupFormComponent implements OnInit { |
|||
constructor() {} |
|||
|
|||
data = inject(NZ_MODAL_DATA) |
|||
|
|||
formGroup = new FormGroup({ |
|||
id: new FormControl(), |
|||
name: new FormControl('', [FormValidators.required('请输入部门名称'), FormValidators.maxLength(30)]), |
|||
parentId: new FormControl(), |
|||
}) |
|||
|
|||
ngOnInit(): void { |
|||
if (this.data) { |
|||
this.formGroup.patchValue(this.data) |
|||
} |
|||
} |
|||
|
|||
public getValues() { |
|||
let values = null |
|||
if (FormValidators.validateFormGroup(this.formGroup)) { |
|||
values = this.formGroup.value |
|||
} |
|||
return values |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<div class="data-tree"> |
|||
<nz-tree |
|||
nzBlockNode |
|||
[nzSelectedKeys]="contact.selecedKeys" |
|||
[nzExpandedKeys]="contact.expandedKeys" |
|||
[nzData]="nodes" |
|||
(nzClick)="activeNode($event)" |
|||
(nzExpandChange)="handleExpandedKeysChange($event)" |
|||
[nzTreeTemplate]="nzTreeTemplate" |
|||
> |
|||
</nz-tree> |
|||
</div> |
|||
|
|||
<ng-template #nzTreeTemplate let-node let-origin="origin"> |
|||
<div class="flex items-center justify-between overflow-hidden"> |
|||
<span class="flex-1 overflow-hidden text-ellipsis">{{ node.title }}</span> |
|||
|
|||
@if (origin.id !== '1') { |
|||
<button nz-button nzType="text" nz-dropdown [nzDropdownMenu]="menu" (click)="$event.stopPropagation()"> |
|||
<i nz-icon nzType="more"></i> |
|||
</button> |
|||
} |
|||
<nz-dropdown-menu #menu="nzDropdownMenu"> |
|||
<ul nz-menu> |
|||
<!-- <li nz-menu-item (click)="createGroup(node)">新增下级部门</li> --> |
|||
<li nz-menu-item (click)="editGroup(node)">编辑</li> |
|||
<li nz-menu-item (click)="deleteGroup(node)">删除</li> |
|||
</ul> |
|||
</nz-dropdown-menu> |
|||
</div> |
|||
</ng-template> |
|||
@ -0,0 +1,83 @@ |
|||
import { Component, EventEmitter, Input, Output, OnChanges, OnInit, SimpleChanges } from '@angular/core' |
|||
|
|||
import { NzContextMenuService, NzDropdownMenuComponent } from 'ng-zorro-antd/dropdown' |
|||
import { NzFormatEmitEvent, NzTreeNode, NzTreeNodeOptions } from 'ng-zorro-antd/tree' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { Persist } from 'app/utils' |
|||
import { ContactService } from '../contact.service' |
|||
|
|||
@Component({ |
|||
selector: 'app-contact-group', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
providers: [ContactService], |
|||
templateUrl: './contact-group.component.html', |
|||
styleUrl: './contact-group.component.less', |
|||
}) |
|||
export class ContactGroupComponent implements OnInit, OnChanges { |
|||
constructor( |
|||
private nzContextMenuService: NzContextMenuService, |
|||
public contact: ContactService, |
|||
) {} |
|||
|
|||
@Input() nodes: NzTreeNodeOptions[] = [] |
|||
|
|||
@Input() active!: Persist |
|||
|
|||
@Output() onSelect = new EventEmitter() |
|||
|
|||
@Output() onCreate = new EventEmitter() |
|||
|
|||
@Output() onEdit = new EventEmitter() |
|||
|
|||
@Output() onDelete = new EventEmitter() |
|||
|
|||
ngOnInit(): void {} |
|||
|
|||
ngOnChanges(changes: SimpleChanges) {} |
|||
|
|||
openFolder(data: NzTreeNode | NzFormatEmitEvent): void { |
|||
if (data instanceof NzTreeNode) { |
|||
data.isExpanded = !data.isExpanded |
|||
} else { |
|||
const node = data.node |
|||
if (node) { |
|||
node.isExpanded = !node.isExpanded |
|||
} |
|||
} |
|||
} |
|||
|
|||
activeNode(data: NzFormatEmitEvent): void { |
|||
this.contact.contactGroup.update((state) => { |
|||
return { |
|||
...state, |
|||
selecedKeys: [data.node!.key], |
|||
} |
|||
}) |
|||
this.onSelect.emit() |
|||
} |
|||
|
|||
handleExpandedKeysChange(expanded: NzFormatEmitEvent) { |
|||
this.contact.contactGroup.update((state) => { |
|||
return { |
|||
...state, |
|||
expandedKeys: expanded?.keys ?? [], |
|||
} |
|||
}) |
|||
} |
|||
|
|||
contextMenu($event: MouseEvent, menu: NzDropdownMenuComponent): void { |
|||
this.nzContextMenuService.create($event, menu) |
|||
} |
|||
|
|||
createGroup(node: NzTreeNode): void { |
|||
this.onCreate.emit(node.origin) |
|||
} |
|||
|
|||
editGroup(node: NzTreeNode): void { |
|||
this.onEdit.emit(node.origin) |
|||
} |
|||
deleteGroup(node: NzTreeNode): void { |
|||
this.onDelete.emit(node.origin.key) |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
<nz-card [nzBordered]="false" [nzBodyStyle]="{ padding: 0, height }"> |
|||
<div class="flex contacts h-full"> |
|||
<div class="group w-56 h-full flex flex-col"> |
|||
<div> |
|||
<h3 class="px-4 pt-4 flex items-center justify-between"> |
|||
<span>部门</span> |
|||
<button nz-button nzType="text" (click)="showGroupForm()"> |
|||
<i nz-icon nzType="plus"></i> |
|||
</button> |
|||
</h3> |
|||
</div> |
|||
|
|||
<div class="flex-1 overflow-y-auto"> |
|||
<app-contact-group |
|||
[nodes]="groups" |
|||
(onSelect)="handleGroupSelect()" |
|||
(onCreate)="showGroupForm($event)" |
|||
(onEdit)="showGroupForm($event, true)" |
|||
(onDelete)="deleteUserGroup($event)" |
|||
> |
|||
</app-contact-group> |
|||
</div> |
|||
</div> |
|||
<div class="flex-1 p-4 overflow-auto"> |
|||
<app-server-paginated-table |
|||
[formGroup]="queryForm" |
|||
[renderColumn]="renderColumnTpl" |
|||
[options]="tableOption" |
|||
> |
|||
<div *appTableForm nz-row [nzGutter]="[24, 24]"> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px">用户名</nz-form-label> |
|||
<nz-form-control nzFlex="1"> |
|||
<input nz-input placeholder="请输入用户名" formControlName="userName" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px">姓名</nz-form-label> |
|||
<nz-form-control nzFlex="1"> |
|||
<input nz-input placeholder="请输入姓名" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px">状态</nz-form-label> |
|||
<nz-form-control nzFlex="1"> |
|||
<nz-select nzPlaceHolder="请选择状态" formControlName="enable" nzAllowClear> |
|||
<nz-option [nzValue]="true" nzLabel="启用"></nz-option> |
|||
<nz-option [nzValue]="false" nzLabel="禁用"></nz-option> |
|||
</nz-select> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px">创建时间</nz-form-label> |
|||
<nz-form-control nzFlex="1"> |
|||
<!-- <nz-range-picker formControlName="createTime"></nz-range-picker> --> |
|||
<app-range-picker [showTime]="true" formControlName="createTime"></app-range-picker> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
</div> |
|||
<div *appTableAction> |
|||
<nz-space> |
|||
<button nz-button nzType="primary" (click)="handleCreateUser()">添加用户</button> |
|||
</nz-space> |
|||
</div> |
|||
<ng-template #renderColumnTpl let-data let-key="key"> |
|||
@switch (key) { |
|||
@case ('enable') { |
|||
@if (data) { |
|||
<nz-badge nzStatus="success" nzText="启用"></nz-badge> |
|||
} @else { |
|||
<nz-badge nzStatus="error" nzText="禁用"></nz-badge> |
|||
} |
|||
} |
|||
@case ('roleName') { |
|||
@if (data) { |
|||
<nz-tag>{{ data }}</nz-tag> |
|||
} @else { |
|||
- |
|||
} |
|||
} |
|||
@default { |
|||
{{ data }} |
|||
} |
|||
} |
|||
</ng-template> |
|||
</app-server-paginated-table> |
|||
</div> |
|||
</div> |
|||
</nz-card> |
|||
@ -0,0 +1,3 @@ |
|||
.group { |
|||
border-right: 1px solid var(--border-color); |
|||
} |
|||
@ -0,0 +1,223 @@ |
|||
import { Component, HostListener, OnInit, OnDestroy } from '@angular/core' |
|||
import { lastValueFrom } from 'rxjs' |
|||
import { NzTreeNodeOptions } from 'ng-zorro-antd/tree' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { format } from 'date-fns' |
|||
import { ApiService, UserGroupTreeItem } from 'app/services' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { ContactGroupComponent } from '../contact-group/contact-group.component' |
|||
import { ContactGroupFormComponent } from '../contact-group-form/contact-group-form.component' |
|||
import { AnyObject, TableOption } from 'app/components/server-paginated-table' |
|||
import { UserFormComponent } from 'app/components/user-form/user-form.component' |
|||
import { RoleListDTO, UserDTO } from 'app/services/api.dto' |
|||
import { ContactService } from '../contact.service' |
|||
|
|||
@Component({ |
|||
selector: 'app-contact-list', |
|||
standalone: true, |
|||
imports: [SharedModule, ContactGroupComponent, ContactGroupFormComponent], |
|||
templateUrl: './contact-list.component.html', |
|||
styleUrl: './contact-list.component.less', |
|||
providers: [ContactService], |
|||
}) |
|||
export class ContactListComponent implements OnInit, OnDestroy { |
|||
constructor( |
|||
private api: ApiService, |
|||
private modal: NzModalService, |
|||
private msg: NzMessageService, |
|||
private contact: ContactService, |
|||
) {} |
|||
|
|||
height = 'auto' |
|||
|
|||
groups: NzTreeNodeOptions[] = [] |
|||
|
|||
tableOption = new TableOption(this.fetchData.bind(this), { |
|||
manual: true, |
|||
}) |
|||
|
|||
queryForm = new FormGroup({ |
|||
userName: new FormControl(), |
|||
enable: new FormControl(), |
|||
name: new FormControl(), |
|||
createTime: new FormControl(), |
|||
}) |
|||
|
|||
roleList: RoleListDTO[] = [] |
|||
|
|||
ngOnInit(): void { |
|||
this.initTableOption() |
|||
this.onResize() |
|||
this.getUserGroupTree() |
|||
this.getRoleList() |
|||
} |
|||
|
|||
ngOnDestroy(): void {} |
|||
|
|||
initTableOption() { |
|||
this.tableOption |
|||
.setColumn([ |
|||
{ key: 'username', title: '用户名' }, |
|||
{ key: 'name', title: '姓名' }, |
|||
{ key: 'enable', title: '状态' }, |
|||
{ key: 'roleName', title: '角色' }, |
|||
{ key: 'remark', title: '说明' }, |
|||
{ key: 'createTime', title: '创建时间' }, |
|||
]) |
|||
.setRowOperate([ |
|||
{ title: '启用', onClick: this.updateStatus.bind(this), visible: (v) => v.id !== '1' && !v.enable }, |
|||
{ title: '禁用', onClick: this.updateStatus.bind(this), visible: (v) => v.id !== '1' && v.enable }, |
|||
{ |
|||
title: '重置密码', |
|||
onClick: this.resetPassword.bind(this), |
|||
premissions: [], |
|||
visible: (v) => v.id !== '1', |
|||
}, |
|||
{ |
|||
title: '编辑', |
|||
onClick: this.handleCreateUser.bind(this), |
|||
premissions: [], |
|||
visible: (v) => v.id !== '1', |
|||
}, |
|||
{ |
|||
title: '删除', |
|||
onClick: this.deleteUser.bind(this), |
|||
premissions: [], |
|||
visible: (v) => v.id !== '1', |
|||
}, |
|||
]) |
|||
} |
|||
|
|||
@HostListener('window:resize') |
|||
onResize() { |
|||
this.height = innerHeight - 100 + 'px' |
|||
} |
|||
|
|||
fetchData(p: {}, q: AnyObject) { |
|||
let query = Object.create(null) |
|||
const activeGroup = this.contact.contactGroup.value |
|||
if (Array.isArray(q['createTime'])) { |
|||
const createTimeStart = q['createTime']?.[0] |
|||
const createTimeEnd = q['createTime']?.[1] |
|||
|
|||
q['createTimeStart'] = createTimeStart ? format(new Date(createTimeStart), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
q['createTimeEnd'] = createTimeEnd ? format(new Date(createTimeEnd), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
} |
|||
query = { ...q, groupId: activeGroup?.selecedKeys[0] } |
|||
return this.api.getUserPage(p, query) |
|||
} |
|||
|
|||
handleGroupSelect() { |
|||
this.tableOption.ref?.search() |
|||
} |
|||
|
|||
updateStatus(v: UserDTO) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: `是否要${v.enable ? '禁用' : '启用'}该用户?`, |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.updateUserStatus(v.id)) |
|||
this.msg.success('操作成功') |
|||
this.tableOption.ref.reload() |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
getUserGroupTree() { |
|||
this.api.getUserGroupTree().subscribe((res) => { |
|||
this.groups = res |
|||
this.contact.initState(res) |
|||
if (res.length > 0) { |
|||
this.tableOption.ref?.reload() |
|||
} |
|||
}) |
|||
} |
|||
|
|||
getRoleList() { |
|||
this.api.getAllRole().subscribe((res) => { |
|||
this.roleList = res.data |
|||
}) |
|||
} |
|||
|
|||
showGroupForm(group?: UserGroupTreeItem, edit = false) { |
|||
this.modal.create({ |
|||
nzTitle: edit ? '编辑部门' : '新增部门', |
|||
nzContent: ContactGroupFormComponent, |
|||
nzData: edit ? group : { parentId: group?.id }, |
|||
nzOnOk: async (e) => { |
|||
const vals = e.getValues() |
|||
if (vals) { |
|||
await lastValueFrom(this.api.saveUserGroup(vals as UserGroupTreeItem)) |
|||
this.msg.success('保存成功') |
|||
this.getUserGroupTree() |
|||
return true |
|||
} |
|||
return false |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
deleteUserGroup(id: string) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: '是否要删除该分组?', |
|||
nzOkDanger: true, |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.deleteUserGroup(id)) |
|||
this.msg.success('删除成功') |
|||
this.getUserGroupTree() |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
deleteUser(v: UserDTO) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: '是否要删除该用户?', |
|||
nzOkDanger: true, |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.deleteUser(v.id)) |
|||
this.msg.success('删除成功') |
|||
this.getUserGroupTree() |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
resetPassword(v: UserDTO) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: '是否要将该用户的密码重置为jwkj?', |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.changeUserPassword(v.id, 'jwkj')) |
|||
this.msg.success('重置成功') |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
handleCreateUser(user?: UserDTO) { |
|||
this.modal.create({ |
|||
nzTitle: user ? '编辑用户' : '添加用户', |
|||
nzContent: UserFormComponent, |
|||
nzWidth: 720, |
|||
nzData: { |
|||
roleList: this.roleList, |
|||
value: user, |
|||
}, |
|||
nzOnOk: async (e) => { |
|||
const vals = e.getValues() |
|||
if (vals) { |
|||
const activeGroup = this.contact.contactGroup.value |
|||
await lastValueFrom( |
|||
this.api.saveUser({ ...(user ?? {}), ...vals, groupId: activeGroup.selecedKeys[0] }), |
|||
) |
|||
this.tableOption.ref?.reload() |
|||
this.msg.success('保存成功') |
|||
return true |
|||
} |
|||
return false |
|||
}, |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
import { Injectable, OnDestroy } from '@angular/core' |
|||
import { Persist, Utils } from 'app/utils' |
|||
import { NzTreeNodeOptions } from 'ng-zorro-antd/tree' |
|||
|
|||
@Injectable() |
|||
export class ContactService implements OnDestroy { |
|||
constructor() {} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.contactGroup.clear() |
|||
} |
|||
|
|||
contactGroup = new Persist<{ selecedKeys: string[]; expandedKeys: string[] }>(sessionStorage, 'contactGroup', { |
|||
selecedKeys: [], |
|||
expandedKeys: [], |
|||
}) |
|||
|
|||
public get selecedKeys(): string[] { |
|||
return this.contactGroup.value.selecedKeys |
|||
} |
|||
|
|||
public get expandedKeys(): string[] { |
|||
return this.contactGroup.value.expandedKeys |
|||
} |
|||
|
|||
initState(treeData: NzTreeNodeOptions[]) { |
|||
if (treeData.length === 0) { |
|||
return |
|||
} |
|||
let selecedKeys = this.selecedKeys |
|||
let expandedKeys = this.expandedKeys |
|||
const flatData: NzTreeNodeOptions[] = Utils.treeToFlatList(treeData) |
|||
if (!flatData.some((s) => selecedKeys.includes(s.key))) { |
|||
selecedKeys = [] |
|||
} |
|||
if (selecedKeys.length === 0) { |
|||
selecedKeys = [treeData[0].key] as string[] |
|||
} else { |
|||
expandedKeys = this.setExpandedKeysBySelecedKeys(treeData, selecedKeys) |
|||
} |
|||
this.contactGroup.update(() => { |
|||
return { |
|||
expandedKeys: [...new Set(expandedKeys)], |
|||
selecedKeys, |
|||
} |
|||
}) |
|||
} |
|||
|
|||
setExpandedKeysBySelecedKeys(flatData: NzTreeNodeOptions[], selecedKeys: string[]) { |
|||
const res: string[] = [] |
|||
|
|||
let parentId = flatData.find((f) => selecedKeys.includes(f.key))?.['parentId'] |
|||
while (parentId) { |
|||
res.push(parentId) |
|||
parentId = flatData.find((f) => f.key === parentId)?.['parentId'] |
|||
} |
|||
return this.expandedKeys.concat(res) |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
<nz-card [nzBordered]="false"> |
|||
<form nz-form class="w-[800px]" [formGroup]="formGroup" (ngSubmit)="onSubmit()"> |
|||
<nz-form-item> |
|||
<nz-form-label nzRequired nzSpan="4"> 角色名称 </nz-form-label> |
|||
<nz-form-control> |
|||
<input nz-input placeholder="请输入角色名称" formControlName="roleName" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label nzRequired nzSpan="4"> 权限 </nz-form-label> |
|||
<nz-form-control> |
|||
<nz-spin [nzSpinning]="permissionsLoading"> |
|||
<div class="bordered min-h-[20px]"> |
|||
<ul class="permissions"> |
|||
@for (p of permissions; track $index) { |
|||
<li> |
|||
<div |
|||
nz-checkbox |
|||
[nzChecked]="checked.has(p.id)" |
|||
(nzCheckedChange)="onPermissionChange($event, p.id)" |
|||
[nzValue]="p.id" |
|||
> |
|||
{{ p.name }} |
|||
</div> |
|||
</li> |
|||
} |
|||
</ul> |
|||
<!-- <nz-collapse nzGhost> |
|||
@for (item of permissions; track $index) { |
|||
<nz-collapse-panel [nzHeader]="item.name" [nzExtra]="nzExtraTpl"> |
|||
<ng-template #nzExtraTpl> |
|||
<label |
|||
nz-checkbox |
|||
[nzIndeterminate]="checkedAll.get(item.id)?.nzIndeterminate" |
|||
[nzChecked]="checkedAll.get(item.id)?.nzChecked" |
|||
(click)="$event.stopPropagation()" |
|||
(nzCheckedChange)="checkAllChange($event, item)" |
|||
> |
|||
全选 |
|||
</label> |
|||
</ng-template> |
|||
<div nz-row [nzGutter]="[12, 12]"> |
|||
@for (p of item.children; track $index) { |
|||
<div nz-col nzSpan="8"> |
|||
<label |
|||
nz-checkbox |
|||
[nzChecked]="checked.has(p.id)" |
|||
[nzValue]="p.id" |
|||
(nzCheckedChange)="onPermissionChange($event, p.id, item)" |
|||
> |
|||
{{ p.name }} |
|||
</label> |
|||
</div> |
|||
} @empty { |
|||
<div nz-col nzSpan="24"> |
|||
<nz-empty></nz-empty> |
|||
</div> |
|||
} |
|||
</div> |
|||
</nz-collapse-panel> |
|||
} |
|||
</nz-collapse> --> |
|||
</div> |
|||
</nz-spin> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4"> 说明 </nz-form-label> |
|||
<nz-form-control> |
|||
<textarea nz-input formControlName="remark" placeholder="请输入说明"></textarea> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-control nzOffset="4"> |
|||
<nz-space> |
|||
<button nz-button nzType="primary" *nzSpaceItem>保存</button> |
|||
<button nz-button *nzSpaceItem [routerLink]="['/', 'role']">取消</button> |
|||
</nz-space> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
</nz-card> |
|||
@ -0,0 +1,11 @@ |
|||
.permissions { |
|||
li { |
|||
display: flex; |
|||
padding: 8px 24px; |
|||
|
|||
&:not(:last-child) { |
|||
|
|||
border-bottom: 1px solid var(--border-color); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
import { finalize } from 'rxjs' |
|||
import { Component, OnInit } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { ActivatedRoute, Router } from '@angular/router' |
|||
import { ApiService } from 'app/services' |
|||
import { PermissionDTO } from 'app/services/api.dto' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { FormValidators } from 'app/utils' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
|
|||
@Component({ |
|||
selector: 'app-role-form', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './role-form.component.html', |
|||
styleUrl: './role-form.component.less', |
|||
}) |
|||
export class RoleFormComponent implements OnInit { |
|||
constructor( |
|||
private api: ApiService, |
|||
private msg: NzMessageService, |
|||
private route: ActivatedRoute, |
|||
private router: Router, |
|||
// private modal: NzModalService,
|
|||
) {} |
|||
|
|||
id: string | null = null |
|||
|
|||
permissions: PermissionDTO[] = [] |
|||
|
|||
formGroup = new FormGroup({ |
|||
roleName: new FormControl('', [FormValidators.required('请输入角色名称'), FormValidators.maxLength(30)]), |
|||
remark: new FormControl('', [FormValidators.maxLength(30)]), |
|||
}) |
|||
|
|||
checkedAll = new Map<string, { nzIndeterminate: boolean; nzChecked: boolean }>() |
|||
|
|||
checked = new Set<string>() |
|||
|
|||
permissionsLoading = false |
|||
|
|||
ngOnInit(): void { |
|||
this.getPermissions() |
|||
const id = this.route.snapshot.paramMap.get('id') |
|||
if (id && id !== 'create') { |
|||
this.id = id |
|||
this.getRoleInfo() |
|||
} |
|||
} |
|||
|
|||
getPermissions() { |
|||
this.permissionsLoading = true |
|||
this.api |
|||
.getAllPermission() |
|||
.pipe( |
|||
finalize(() => { |
|||
this.permissionsLoading = false |
|||
}), |
|||
) |
|||
.subscribe((res) => { |
|||
this.permissions = res.data |
|||
this.updateCheckedAll() |
|||
}) |
|||
} |
|||
|
|||
getRoleInfo() { |
|||
this.api.getRoleById(this.id!).subscribe((res) => { |
|||
this.formGroup.patchValue(res.data) |
|||
this.checked = new Set(res.data.authorityId) |
|||
this.updateCheckedAll() |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 权限之间的联动 |
|||
*/ |
|||
roleLinkage = new Map<string, string[]>([ |
|||
// ['2', ['2', '9', '15']],
|
|||
// ['9', ['2', '9', '15']],
|
|||
// ['15', ['2', '9', '15']],
|
|||
// ['20', ['20', '25']],
|
|||
// ['25', ['20', '25']],
|
|||
]) |
|||
|
|||
onPermissionChange(checked: boolean, id: string) { |
|||
const checkedIds = this.roleLinkage.get(id) ?? [id] |
|||
checkedIds.forEach((i) => { |
|||
if (checked) { |
|||
this.checked.add(i) |
|||
} else { |
|||
this.checked.delete(i) |
|||
} |
|||
}) |
|||
this.updateCheckedAll() |
|||
} |
|||
|
|||
checkAllChange(checked: boolean, p: PermissionDTO) { |
|||
p.children?.forEach((permission) => { |
|||
if (checked) { |
|||
this.checked.add(permission.id) |
|||
} else { |
|||
this.checked.delete(permission.id) |
|||
} |
|||
}) |
|||
this.updateCheckedAll() |
|||
} |
|||
|
|||
updateCheckedAll() { |
|||
this.permissions.forEach((g) => { |
|||
let nzChecked = true |
|||
let nzIndeterminate = false |
|||
g.children?.forEach((permission) => { |
|||
if (this.checked.has(permission.id)) { |
|||
nzIndeterminate = true |
|||
} else { |
|||
nzChecked = false |
|||
} |
|||
}) |
|||
this.checkedAll.set(g.id, { |
|||
nzChecked, |
|||
nzIndeterminate: nzChecked ? false : nzIndeterminate, |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
onSubmit() { |
|||
if (FormValidators.validateFormGroup(this.formGroup)) { |
|||
if (this.checked.size === 0) { |
|||
this.msg.error('请选择权限') |
|||
return |
|||
} |
|||
const { value } = this.formGroup |
|||
this.api |
|||
.saveRole({ |
|||
id: this.id, |
|||
roleName: value.roleName!, |
|||
remark: value.remark as string, |
|||
authorityId: Array.from(this.checked), |
|||
}) |
|||
.subscribe(() => { |
|||
this.msg.success('保存成功') |
|||
this.router.navigate(['/role/list']) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<nz-card [nzBordered]="false"> |
|||
<app-server-paginated-table [options]="tableOption" [formGroup]="queryForm"> |
|||
<div *appTableAction class="flex items-center justify-between"> |
|||
<div class="flex-1"> |
|||
<nz-space> |
|||
<button |
|||
nz-button |
|||
nzType="primary" |
|||
type="button" |
|||
[routerLink]="['/', 'role', 'form', 'create']" |
|||
*nzSpaceItem |
|||
> |
|||
添加角色 |
|||
</button> |
|||
</nz-space> |
|||
</div> |
|||
<!-- <div> |
|||
<nz-input-group nzSearch [nzSuffix]="suffixIconSearch"> |
|||
<input type="text" nz-input placeholder="请输入角色名称" formControlName="roleName" /> |
|||
</nz-input-group> |
|||
<ng-template #suffixIconSearch> |
|||
<span nz-icon nzType="search"></span> |
|||
</ng-template> |
|||
</div> --> |
|||
</div> |
|||
</app-server-paginated-table> |
|||
</nz-card> |
|||
@ -0,0 +1,72 @@ |
|||
import { lastValueFrom } from 'rxjs' |
|||
import { Component, OnInit } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { Router } from '@angular/router' |
|||
import { TableOption } from 'app/components/server-paginated-table' |
|||
import { ApiService } from 'app/services' |
|||
import { RoleListDTO } from 'app/services/api.dto' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
|
|||
@Component({ |
|||
selector: 'app-role-list', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './role-list.component.html', |
|||
styleUrl: './role-list.component.less', |
|||
}) |
|||
export class RoleListComponent implements OnInit { |
|||
constructor( |
|||
private api: ApiService, |
|||
private modal: NzModalService, |
|||
private router: Router, |
|||
private msg: NzMessageService, |
|||
) {} |
|||
|
|||
tableOption = new TableOption(this.fetchData.bind(this)) |
|||
|
|||
queryForm = new FormGroup({ |
|||
roleName: new FormControl(), |
|||
}) |
|||
|
|||
permissions = [] |
|||
|
|||
ngOnInit(): void { |
|||
this.initTable() |
|||
} |
|||
|
|||
initTable() { |
|||
this.tableOption |
|||
.setColumn([ |
|||
{ key: 'roleName', title: '角色名称' }, |
|||
{ key: 'remark', title: '说明' }, |
|||
{ key: 'createTime', title: '创建时间' }, |
|||
]) |
|||
.setRowOperate([ |
|||
{ title: '编辑', onClick: this.edit.bind(this), visible: (v) => v.id !== '1' }, |
|||
{ title: '删除', onClick: this.deleteItem.bind(this), visible: (v) => v.id !== '1' }, |
|||
]) |
|||
} |
|||
|
|||
fetchData(p: {}, q: {}) { |
|||
return this.api.getRolePage(p, q) |
|||
} |
|||
|
|||
edit(role: RoleListDTO) { |
|||
this.router.navigate(['/', 'role', 'form', role.id]) |
|||
} |
|||
|
|||
deleteItem(role: RoleListDTO) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: '是否要删除该角色?', |
|||
nzOkDanger: true, |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.deleteRole(role.id)) |
|||
this.msg.success('删除成功') |
|||
this.tableOption.ref?.reload() |
|||
}, |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
<div nz-row nzGutter="12"> |
|||
<div nz-col nzSpan="6"> |
|||
<nz-card [nzBordered]="false"> |
|||
<div> |
|||
<nz-statistic [nzValue]="counter.totalClient" [nzTitle]="'今日预约'"></nz-statistic> |
|||
</div> |
|||
</nz-card> |
|||
</div> |
|||
<div nz-col nzSpan="18"> |
|||
<nz-card [nzBordered]="false"> |
|||
<div nz-row nzGutter="24"> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-statistic [nzValue]="counter.totalClient" [nzTitle]="'会员'"></nz-statistic> |
|||
</div> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-statistic [nzValue]="counter.totalProduct" [nzTitle]="'会员卡'"></nz-statistic> |
|||
</div> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-statistic [nzValue]="counter.totalLicense" [nzTitle]="'教练'"></nz-statistic> |
|||
</div> |
|||
</div> |
|||
</nz-card> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="mt-4"> |
|||
<nz-card [nzBordered]="false" nzTitle="近30天办卡"> |
|||
<div class="h-80" #chart1Tpl></div> |
|||
</nz-card> |
|||
</div> |
|||
|
|||
<div class="mt-4 mb-4"> |
|||
<nz-card [nzBordered]="false" nzTitle="近30天到期会员"> |
|||
<nz-table nzTemplateMode nzSize="small" [nzBordered]="true"> |
|||
<thead> |
|||
<tr> |
|||
<th>实体</th> |
|||
<th>产品</th> |
|||
<!-- <th>版本</th> --> |
|||
<th>到期时间</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@for (p of expiringSoon; track $index) { |
|||
<tr> |
|||
<td>{{ p.clientName }}</td> |
|||
<td>{{ p.productName }}</td> |
|||
<!-- <td></td> --> |
|||
<td> |
|||
{{ p.endTime * 1000 | date: 'yyyy-MM-dd HH:mm:ss' }} |
|||
</td> |
|||
</tr> |
|||
} @empty { |
|||
<tr> |
|||
<td [colSpan]="4"> |
|||
<nz-empty nzNotFoundContent="没有近30天到期的授权"></nz-empty> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</nz-table> |
|||
</nz-card> |
|||
</div> |
|||
@ -0,0 +1,195 @@ |
|||
import { AfterViewInit, Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' |
|||
import { init, EChartsType } from 'echarts' |
|||
import { SharedModule } from '../../shared/shared.module' |
|||
import { ApiService } from 'app/services' |
|||
import { ConsoleClientTopDTO, ConsoleCountDTO, ConsoleExpireDTO } from 'app/services/api.dto' |
|||
|
|||
const antvColor = [ |
|||
'#5B8FF9', |
|||
'#61DDAA', |
|||
'#65789B', |
|||
'#F6BD16', |
|||
'#7262FD', |
|||
'#78D3F8', |
|||
'#9661BC', |
|||
'#F6903D', |
|||
'#008685', |
|||
'#F08BB4', |
|||
] |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
selector: 'app-dashboard', |
|||
templateUrl: './dashboard.component.html', |
|||
styleUrls: ['./dashboard.component.less'], |
|||
}) |
|||
export class DashboardComponent implements OnInit, AfterViewInit { |
|||
constructor(private api: ApiService) {} |
|||
|
|||
@ViewChild('chart1Tpl') chart1!: ElementRef<HTMLDivElement> |
|||
|
|||
@ViewChild('chart2Tpl') chart2!: ElementRef<HTMLDivElement> |
|||
|
|||
echartRef1?: EChartsType |
|||
|
|||
echartRef2?: EChartsType |
|||
|
|||
counter: ConsoleCountDTO = { totalClient: 0, totalLicense: 0, totalProduct: 0 } |
|||
|
|||
expiringSoon: ConsoleExpireDTO[] = [] |
|||
|
|||
clientTopTen: ConsoleClientTopDTO[] = [] |
|||
|
|||
@HostListener('window:resize') |
|||
onResize() { |
|||
// this.echartRef1?.resize();
|
|||
this.echartRef2?.resize() |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
// this.api.getDashboardCounter().subscribe((res) => {
|
|||
// this.counter = res.data
|
|||
// })
|
|||
// this.api.getDashboardExpire().subscribe((res) => {
|
|||
// this.expiringSoon = res.data
|
|||
// })
|
|||
// this.api.getDashboardClientTop().subscribe((res) => {
|
|||
// this.clientTopTen = res.data
|
|||
// this.initChart2()
|
|||
// })
|
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
// this.initChart1();
|
|||
} |
|||
|
|||
initChart1() { |
|||
const el = this.chart1.nativeElement |
|||
if (el) { |
|||
this.echartRef1 = init(el) |
|||
this.echartRef1.setOption({ |
|||
color: antvColor, |
|||
tooltip: { |
|||
trigger: 'axis', |
|||
axisPointer: { |
|||
type: 'cross', |
|||
label: { |
|||
backgroundColor: '#6a7985', |
|||
}, |
|||
}, |
|||
}, |
|||
legend: { |
|||
data: ['晶未智慧安全平台', 'NG-WAF', 'DLP', 'API'], |
|||
}, |
|||
|
|||
grid: { |
|||
left: '10px', |
|||
right: '10px', |
|||
bottom: '3%', |
|||
containLabel: true, |
|||
}, |
|||
xAxis: [ |
|||
{ |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], |
|||
}, |
|||
], |
|||
yAxis: [ |
|||
{ |
|||
type: 'value', |
|||
}, |
|||
], |
|||
series: [ |
|||
{ |
|||
name: '晶未智慧安全平台', |
|||
type: 'line', |
|||
smooth: true, |
|||
areaStyle: { |
|||
opacity: 0.2, |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [620, 332, 401, 534, 190, 330, 410], |
|||
}, |
|||
{ |
|||
name: 'NG-WAF', |
|||
type: 'line', |
|||
smooth: true, |
|||
areaStyle: { |
|||
opacity: 0.2, |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [220, 182, 191, 234, 290, 330, 310], |
|||
}, |
|||
{ |
|||
name: 'API', |
|||
type: 'line', |
|||
smooth: true, |
|||
areaStyle: { |
|||
opacity: 0.2, |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [55, 612, 491, 34, 90, 200, 411], |
|||
}, |
|||
{ |
|||
name: 'DLP', |
|||
type: 'line', |
|||
smooth: true, |
|||
areaStyle: { |
|||
opacity: 0.2, |
|||
}, |
|||
emphasis: { |
|||
focus: 'series', |
|||
}, |
|||
data: [20, 162, 291, 134, 390, 300, 10], |
|||
}, |
|||
], |
|||
}) |
|||
} |
|||
} |
|||
initChart2() { |
|||
const el = this.chart2.nativeElement |
|||
if (el) { |
|||
this.echartRef2 = init(el) |
|||
this.echartRef2.setOption({ |
|||
color: antvColor, |
|||
tooltip: { |
|||
trigger: 'axis', |
|||
axisPointer: { |
|||
type: 'cross', |
|||
label: { |
|||
backgroundColor: '#6a7985', |
|||
}, |
|||
}, |
|||
}, |
|||
|
|||
grid: { |
|||
left: '10px', |
|||
right: '10px', |
|||
bottom: '3%', |
|||
containLabel: true, |
|||
}, |
|||
xAxis: { |
|||
type: 'category', |
|||
data: this.clientTopTen.map((i) => i.clientName), |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
}, |
|||
series: [ |
|||
{ |
|||
data: this.clientTopTen.map((i) => i.count), |
|||
type: 'bar', |
|||
}, |
|||
], |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
@if (entity) { |
|||
<div class="mb-4"> |
|||
<nz-card [nzBordered]="false" nzTitle="实体详情" [nzExtra]="nzExtraTpl"> |
|||
<ng-template #nzExtraTpl> |
|||
<nz-space> |
|||
<button *nzSpaceItem nz-button [routerLink]="['/', 'entity']">返回</button> |
|||
</nz-space> |
|||
</ng-template> |
|||
|
|||
<nz-descriptions [nzColumn]="3" class="ml-16"> |
|||
<nz-descriptions-item nzTitle="实体名称"> |
|||
{{ entity.name }} |
|||
</nz-descriptions-item> |
|||
<nz-descriptions-item nzTitle="联系方式"> |
|||
{{ entity.contact ?? '-' }} |
|||
</nz-descriptions-item> |
|||
<!-- <nz-descriptions-item nzTitle="地址"> |
|||
{{ entity.address ?? '-' }} |
|||
</nz-descriptions-item> --> |
|||
<nz-descriptions-item nzTitle="说明">{{ entity.remark || '-' }}</nz-descriptions-item> |
|||
</nz-descriptions> |
|||
</nz-card> |
|||
</div> |
|||
} |
|||
|
|||
<div class="mb-4"> |
|||
<nz-card [nzBordered]="false" nzTitle="授权记录" [nzExtra]="licenseExtraTpl"> |
|||
<ng-template #licenseExtraTpl> |
|||
<nz-space> |
|||
<button *nzSpaceItem nz-button nzType="primary" (click)="authorize()">新增授权</button> |
|||
</nz-space> |
|||
</ng-template> |
|||
<div> |
|||
<div class="mb-3"> |
|||
<nz-space nzAlign="center"> |
|||
<div *nzSpaceItem>状态:</div> |
|||
<div *nzSpaceItem> |
|||
<nz-select |
|||
nzPlaceHolder="请选择状态" |
|||
class="w-64" |
|||
[(ngModel)]="status" |
|||
(ngModelChange)="onStatusChange()" |
|||
> |
|||
<nz-option [nzValue]="-1" nzLabel="全部"></nz-option> |
|||
<nz-option [nzValue]="0" nzLabel="无效"></nz-option> |
|||
<nz-option [nzValue]="1" nzLabel="生效中"></nz-option> |
|||
</nz-select> |
|||
</div> |
|||
</nz-space> |
|||
</div> |
|||
<nz-table #table [nzData]="visibleList" [nzShowPagination]="false"> |
|||
<thead> |
|||
<tr> |
|||
<th>授权产品</th> |
|||
<th>操作人</th> |
|||
<th>生效时间</th> |
|||
<th>失效时间</th> |
|||
<th>状态</th> |
|||
<th>创建时间</th> |
|||
<th>操作</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@for (item of table.data; track $index) { |
|||
<tr> |
|||
<td> |
|||
{{ item.productName }} |
|||
</td> |
|||
<td> |
|||
{{ item.username }} |
|||
</td> |
|||
<td> |
|||
{{ item.startTime * 1000 | date: 'yyyy-MM-dd HH:mm:ss' }} |
|||
</td> |
|||
<td> |
|||
{{ item.endTime ? (item.endTime * 1000 | date: 'yyyy-MM-dd HH:mm:ss') : '永久' }} |
|||
</td> |
|||
<td> |
|||
@if (item.status) { |
|||
<nz-badge nzStatus="processing" nzText="生效中"> </nz-badge> |
|||
} @else { |
|||
<nz-badge nzStatus="default" nzText="无效"></nz-badge> |
|||
} |
|||
</td> |
|||
<td> |
|||
{{ item.createTime }} |
|||
</td> |
|||
<td> |
|||
<nz-space> |
|||
<button *nzSpaceItem nz-button nzType="link" (click)="copy(item.license)"> |
|||
复制授权码 |
|||
</button> |
|||
</nz-space> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</nz-table> |
|||
</div> |
|||
</nz-card> |
|||
</div> |
|||
@ -0,0 +1,105 @@ |
|||
import { Component, OnInit } from '@angular/core' |
|||
import { ActivatedRoute } from '@angular/router' |
|||
import { ApiService } from 'app/services' |
|||
import { AuthorizeItemDTO, EntityDTO, EntityDetailDTO } from 'app/services/api.dto' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { Clipboard } from '@angular/cdk/clipboard' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { AuthorizeComponent } from 'app/components/authorize/authorize.component' |
|||
|
|||
@Component({ |
|||
selector: 'app-entity-detail', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './entity-detail.component.html', |
|||
styleUrl: './entity-detail.component.less', |
|||
}) |
|||
export class EntityDetailComponent implements OnInit { |
|||
constructor( |
|||
private api: ApiService, |
|||
private route: ActivatedRoute, |
|||
private clipboard: Clipboard, |
|||
private msg: NzMessageService, |
|||
private modal: NzModalService, |
|||
) {} |
|||
|
|||
entity?: EntityDetailDTO |
|||
|
|||
authorizeData: AuthorizeItemDTO[] = [] |
|||
|
|||
visibleList: AuthorizeItemDTO[] = [] |
|||
|
|||
product: { id: string; name: string }[] = [] |
|||
|
|||
id: string = '' |
|||
|
|||
status = -1 |
|||
|
|||
ngOnInit(): void { |
|||
const id = this.route.snapshot.paramMap.get('id') |
|||
if (id) { |
|||
this.id = id |
|||
this.api.getEntityDetail(id).subscribe((res) => { |
|||
this.entity = res.data |
|||
}) |
|||
this.getEntityAuthorizeList() |
|||
} |
|||
this.api.getAllProduct().subscribe((res) => { |
|||
this.product = res.data |
|||
}) |
|||
} |
|||
|
|||
getEntityAuthorizeList() { |
|||
this.api.getEntityAuthorizeList(this.id).subscribe((res) => { |
|||
this.authorizeData = res.data.map((i) => ({ |
|||
...i, |
|||
status: this.between(i.startTime, i.endTime) ? 1 : 0, |
|||
})) |
|||
this.onStatusChange() |
|||
}) |
|||
} |
|||
|
|||
onStatusChange() { |
|||
this.visibleList = this.authorizeData.filter((f) => (this.status === -1 ? true : f.status === this.status)) |
|||
} |
|||
|
|||
copy(license: string) { |
|||
const pending = this.clipboard.beginCopy(license) |
|||
let remainingAttempts = 3 |
|||
const attempt = () => { |
|||
const result = pending.copy() |
|||
if (!result && --remainingAttempts) { |
|||
setTimeout(attempt) |
|||
} else { |
|||
this.msg.success('复制成功') |
|||
pending.destroy() |
|||
} |
|||
} |
|||
attempt() |
|||
} |
|||
|
|||
between(start: number, end: number): boolean { |
|||
if (end === 0) { |
|||
return true |
|||
} |
|||
const now = Date.now() |
|||
return start * 1000 <= now && now <= end * 1000 |
|||
} |
|||
|
|||
authorize() { |
|||
const ref = this.modal.create({ |
|||
nzTitle: '添加授权', |
|||
nzContent: AuthorizeComponent, |
|||
nzWidth: 800, |
|||
nzData: { |
|||
product: this.product, |
|||
entity: this.entity, |
|||
}, |
|||
nzFooter: null, |
|||
}) |
|||
ref.afterClose.subscribe(() => { |
|||
this.getEntityAuthorizeList() |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
<nz-card [nzBordered]="false"> |
|||
<app-server-paginated-table |
|||
#tableTpl |
|||
[renderColumn]="renderColumnsTpl" |
|||
[formGroup]="queryForm" |
|||
[simpleSearch]="true" |
|||
[options]="table" |
|||
> |
|||
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row"> |
|||
@switch (key) { |
|||
@case ('name') { |
|||
<a [routerLink]="['/', 'entity', row.id, 'detail']">{{ data }}</a> |
|||
} |
|||
@default { |
|||
{{ data }} |
|||
} |
|||
} |
|||
</ng-template> |
|||
|
|||
<div *appTableForm nz-row [nzGutter]="[24, 24]"> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px"> 实体名称 </nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<input nz-input placeholder="请输入实体名称" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
<div nz-col nzSpan="12"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px"> 创建时间 </nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<!-- <nz-range-picker nzShowTime formControlName="createTime"></nz-range-picker> --> |
|||
<app-range-picker [showTime]="true" formControlName="createTime"></app-range-picker> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
</div> |
|||
<div *appTableAction class="flex"> |
|||
<div> |
|||
<nz-space> |
|||
<button *nzSpaceItem nzType="primary" nz-button (click)="create()">添加实体</button> |
|||
</nz-space> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- <div action>Custom Modal action</div> --> |
|||
</app-server-paginated-table> |
|||
</nz-card> |
|||
|
|||
<ng-template #entityFormTpl> |
|||
<form [formGroup]="createForm" nz-form [nzLayout]="'vertical'"> |
|||
<nz-form-item> |
|||
<nz-form-label nzRequired> 实体名称 </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入实体名称" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label> 联系方式 </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入联系方式" formControlName="contact" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<!-- <nz-form-item> |
|||
<nz-form-label> 地址 </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入地址" formControlName="address" /> |
|||
</nz-form-control> |
|||
</nz-form-item> --> |
|||
<nz-form-item> |
|||
<nz-form-label> 说明 </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<textarea nz-input placeholder="请输入说明" formControlName="remark"></textarea> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
</ng-template> |
|||
|
|||
<ng-template #formErrorTipsTpl let-control> |
|||
<form-error-tips [control]="control"></form-error-tips> |
|||
</ng-template> |
|||
@ -0,0 +1,132 @@ |
|||
import { Component, ElementRef, OnInit, TemplateRef, ViewChild } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { ApiService } from 'app/services' |
|||
import { AnyObject, ServerPaginatedTableComponent, TableOption } from 'app/components/server-paginated-table' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { FormValidators } from 'app/utils' |
|||
import { lastValueFrom } from 'rxjs' |
|||
import { EntityDTO, ProductDTO } from 'app/services/api.dto' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { format } from 'date-fns' |
|||
import { AuthorizeComponent } from 'app/components/authorize/authorize.component' |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
selector: 'app-entity-list', |
|||
templateUrl: './entity-list.component.html', |
|||
styleUrls: ['./entity-list.component.less'], |
|||
}) |
|||
export class EntityListComponent implements OnInit { |
|||
constructor( |
|||
private api: ApiService, |
|||
private modal: NzModalService, |
|||
private msg: NzMessageService, |
|||
) {} |
|||
|
|||
@ViewChild('entityFormTpl') entityFormTpl!: TemplateRef<{}> |
|||
|
|||
table = new TableOption(this.fetchData.bind(this), [{ key: 'name', title: '实体名称' }], [], { |
|||
cacheUrls: ['/dashboard'], |
|||
}) |
|||
|
|||
queryForm = new FormGroup({ |
|||
name: new FormControl(''), |
|||
createTime: new FormControl(), |
|||
}) |
|||
|
|||
createForm = new FormGroup({ |
|||
name: new FormControl('', [FormValidators.required('请输入实体名称'), FormValidators.maxLength(30)]), |
|||
// secretKey: new FormControl('', [FormValidators.required('请输入序列号')]),
|
|||
contact: new FormControl('', FormValidators.maxLength(30)), |
|||
// address: new FormControl('', FormValidators.maxLength(200)),
|
|||
remark: new FormControl('', FormValidators.maxLength(200)), |
|||
}) |
|||
|
|||
product: { id: string; name: string }[] = [] |
|||
|
|||
ngOnInit(): void { |
|||
this.table |
|||
.setColumn([ |
|||
{ key: 'name', title: '实体名称' }, |
|||
{ key: 'contact', title: '联系方式' }, |
|||
// { key: 'address', title: '地址' },
|
|||
{ key: 'remark', title: '说明' }, |
|||
{ key: 'createTime', title: '创建时间' }, |
|||
]) |
|||
.setRowOperate([ |
|||
{ title: '添加授权', premissions: [], onClick: this.authorize.bind(this) }, |
|||
{ title: '编辑', onClick: this.create.bind(this) }, |
|||
{ title: '删除', onClick: this.deleteItem.bind(this) }, |
|||
]) |
|||
this.api.getAllProduct().subscribe((res) => { |
|||
this.product = res.data |
|||
}) |
|||
} |
|||
|
|||
fetchData(p: {}, q: AnyObject) { |
|||
if (Array.isArray(q['createTime'])) { |
|||
const createTimeStart = q['createTime']?.[0] |
|||
const createTimeEnd = q['createTime']?.[1] |
|||
|
|||
q['createTimeStart'] = createTimeStart ? format(new Date(createTimeStart), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
q['createTimeEnd'] = createTimeEnd ? format(new Date(createTimeEnd), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
} |
|||
return this.api.getEntityPage(p, q) |
|||
} |
|||
|
|||
onSearch() {} |
|||
|
|||
authorize(v: EntityDTO) { |
|||
this.modal.create({ |
|||
nzTitle: '添加授权', |
|||
nzContent: AuthorizeComponent, |
|||
nzWidth: 800, |
|||
nzData: { |
|||
product: this.product, |
|||
entity: v, |
|||
}, |
|||
nzFooter: null, |
|||
}) |
|||
} |
|||
|
|||
create(entity?: EntityDTO) { |
|||
if (entity) { |
|||
this.createForm.patchValue(entity) |
|||
this.createForm.get('secretKey')?.disable() |
|||
} else { |
|||
this.createForm.get('secretKey')?.enable() |
|||
} |
|||
this.modal.create({ |
|||
nzTitle: entity ? '编辑实体' : '添加实体', |
|||
nzContent: this.entityFormTpl, |
|||
nzOnOk: async () => { |
|||
if (FormValidators.validateFormGroup(this.createForm)) { |
|||
await lastValueFrom(this.api.saveEntity({ ...(entity ?? {}), ...this.createForm.value })) |
|||
this.msg.success('保存成功') |
|||
this.table.ref?.reload() |
|||
this.createForm.reset() |
|||
return true |
|||
} |
|||
return false |
|||
}, |
|||
nzOnCancel: () => { |
|||
this.createForm.reset() |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
deleteItem(entity: EntityDTO) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: '是否要删除该实体?', |
|||
nzOkDanger: true, |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.deleteEntity(entity.id)) |
|||
this.msg.success('删除成功') |
|||
this.table.ref?.reload() |
|||
}, |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<nz-card [nzBordered]="false" class="h-screen"> |
|||
<div class="flex items-center justify-center"> |
|||
<nz-result nzStatus="403"></nz-result> |
|||
</div> |
|||
</nz-card> |
|||
@ -0,0 +1,11 @@ |
|||
import { Component } from '@angular/core' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
|
|||
@Component({ |
|||
selector: 'app-forbidden', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './forbidden.component.html', |
|||
styleUrl: './forbidden.component.less', |
|||
}) |
|||
export class ForbiddenComponent {} |
|||
@ -0,0 +1,82 @@ |
|||
<nz-card [nzBordered]="false"> |
|||
<app-server-paginated-table |
|||
#tableTpl |
|||
[options]="tableOption" |
|||
[formGroup]="queryForm" |
|||
[simpleSearch]="false" |
|||
[renderColumn]="renderColumnsTpl" |
|||
> |
|||
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row"> |
|||
@switch (key) { |
|||
@case ('content') { |
|||
<span> |
|||
@switch (row.operation) { |
|||
@case ('insert') { |
|||
<nz-tag nzColor="success">新增</nz-tag> |
|||
} |
|||
@case ('update') { |
|||
<nz-tag nzColor="blue">编辑</nz-tag> |
|||
} |
|||
@case ('delete') { |
|||
<nz-tag nzColor="error">删除</nz-tag> |
|||
} |
|||
@case ('login') { |
|||
<nz-tag nzColor="gold">登录</nz-tag> |
|||
} |
|||
} |
|||
</span> |
|||
<span> {{ data }} </span> |
|||
} |
|||
|
|||
@default { |
|||
{{ data }} |
|||
} |
|||
} |
|||
</ng-template> |
|||
<div *appTableForm nz-row [nzGutter]="[24, 24]"> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="6"> 操作人 </nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<input nz-input placeholder="请输入操作人" formControlName="username" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
|
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="6"> 操作类型 </nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<nz-select nzPlaceHolder="请选择操作类型" formControlName="operation" nzAllowClear> |
|||
<nz-option [nzValue]="'insert'" [nzLabel]="'新增'"></nz-option> |
|||
<nz-option [nzValue]="'update'" [nzLabel]="'编辑'"></nz-option> |
|||
<nz-option [nzValue]="'delete'" [nzLabel]="'删除'"></nz-option> |
|||
<nz-option [nzValue]="'login'" [nzLabel]="'登录'"></nz-option> |
|||
</nz-select> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
|
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="6"> 操作内容 </nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<input nz-input placeholder="请输入操作内容" formControlName="content" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
|
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="6"> 创建时间 </nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<!-- <nz-range-picker class="w-full" formControlName="createTime"></nz-range-picker> --> |
|||
<app-range-picker [showTime]="true" formControlName="createTime"></app-range-picker> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
</div> |
|||
</app-server-paginated-table> |
|||
</nz-card> |
|||
|
|||
<ng-template #createTpl> </ng-template> |
|||
@ -0,0 +1,87 @@ |
|||
import { AfterViewInit, Component, ElementRef, OnInit, TemplateRef, ViewChild } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { ApiService } from 'app/services/api.service' |
|||
import { |
|||
AnyObject, |
|||
ServerPaginatedTableComponent, |
|||
ServerPaginatedTableService, |
|||
TableOption, |
|||
} from 'app/components/server-paginated-table' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { format } from 'date-fns' |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
selector: 'app-log', |
|||
templateUrl: './log.component.html', |
|||
styleUrls: ['./log.component.less'], |
|||
}) |
|||
export class LogComponent implements OnInit, AfterViewInit { |
|||
constructor( |
|||
private api: ApiService, |
|||
private modal: NzModalService, |
|||
private table: ServerPaginatedTableService, |
|||
) {} |
|||
|
|||
@ViewChild('shadowLineTpl') shadowLineRef!: ElementRef<HTMLCanvasElement> |
|||
|
|||
tableOption = new TableOption(this.fetchData.bind(this)) |
|||
|
|||
queryForm = new FormGroup({ |
|||
operation: new FormControl(), |
|||
username: new FormControl(), |
|||
content: new FormControl(), |
|||
|
|||
createTime: new FormControl(), |
|||
}) |
|||
|
|||
ngOnInit(): void { |
|||
// this.table.run();
|
|||
this.tableOption.setConfig({ |
|||
cacheUrls: ['/dashboard'], |
|||
}) |
|||
this.tableOption.setColumn([ |
|||
{ |
|||
key: 'username', |
|||
title: '操作人', |
|||
}, |
|||
{ |
|||
key: 'content', |
|||
title: '操作内容', |
|||
}, |
|||
|
|||
{ |
|||
key: 'createTime', |
|||
title: '创建时间', |
|||
}, |
|||
]) |
|||
} |
|||
|
|||
ngAfterViewInit(): void {} |
|||
|
|||
fetchData(p: {}, q: AnyObject) { |
|||
if (Array.isArray(q['createTime'])) { |
|||
const createTimeStart = q['createTime']?.[0] |
|||
const createTimeEnd = q['createTime']?.[1] |
|||
|
|||
q['createTimeStart'] = createTimeStart ? format(new Date(createTimeStart), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
q['createTimeEnd'] = createTimeEnd ? format(new Date(createTimeEnd), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
} |
|||
return this.api.getSysLog(p, q) |
|||
} |
|||
|
|||
create(nzContent: TemplateRef<{}>) { |
|||
this.modal.create({ |
|||
nzTitle: '添加授权', |
|||
nzContent, |
|||
nzWidth: 720, |
|||
nzOnOk: async () => { |
|||
this.tableOption.ref?.reload() |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
onSearch() {} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
<div class="flex items-center justify-center fixed inset-0 z-10 login-page"> |
|||
<div class="bg fixed inset-0"></div> |
|||
<div class="w-[450px] relative z-10 login-form p-8 bg-white rounded-md shadow-2xl"> |
|||
<div class="px-4"> |
|||
<div class="text-center"> |
|||
<h1>Back to sea</h1> |
|||
<p class="text-lg mb-6">登录</p> |
|||
</div> |
|||
<form nz-form [formGroup]="loginForm" class="mt-12"> |
|||
<nz-form-item> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<nz-input-group [nzPrefix]="prefixTemplateUser" nzSize="large"> |
|||
<input nz-input nzSize="large" placeholder="用户名" formControlName="username" /> |
|||
</nz-input-group> |
|||
<ng-template #prefixTemplateUser> |
|||
<span nz-icon nzType="user"></span> |
|||
<nz-divider nzType="vertical"></nz-divider> |
|||
</ng-template> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<nz-input-group [nzPrefix]="prefixTemplatePassword" nzSize="large"> |
|||
<input nz-input type="password" placeholder="密码" formControlName="password" /> |
|||
</nz-input-group> |
|||
<ng-template #prefixTemplatePassword> |
|||
<span nz-icon nzType="lock"></span> |
|||
<nz-divider nzType="vertical"></nz-divider> |
|||
</ng-template> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-control> |
|||
<button |
|||
nz-button |
|||
nzType="primary" |
|||
nzBlock |
|||
class="btn" |
|||
nzSize="large" |
|||
(click)="onLogin()" |
|||
[nzLoading]="loading" |
|||
> |
|||
登录 |
|||
</button> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<ng-template #formErrorTipsTpl let-control> |
|||
<form-error-tips [control]="control"></form-error-tips> |
|||
</ng-template> |
|||
@ -0,0 +1,20 @@ |
|||
.login-page { |
|||
|
|||
|
|||
.bg { |
|||
background-image: url(/assets/images/login-bg.jpg); |
|||
background-repeat: no-repeat; |
|||
background-size: cover; |
|||
filter: blur(1px); |
|||
} |
|||
|
|||
|
|||
|
|||
.login-form { |
|||
|
|||
background-color: #fff; |
|||
background-color: rgba(255, 255, 255, .7); |
|||
-webkit-backdrop-filter: blur(10px); |
|||
backdrop-filter: blur(10px); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
import { Component } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { SharedModule } from '../../shared/shared.module' |
|||
import { FormValidators } from 'app/utils' |
|||
import { ApiService, LocalHttpInterceptorService } from 'app/services' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { Router } from '@angular/router' |
|||
import { finalize } from 'rxjs' |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
selector: 'app-login', |
|||
templateUrl: './login.component.html', |
|||
styleUrls: ['./login.component.less'], |
|||
}) |
|||
export class LoginComponent { |
|||
constructor( |
|||
private api: ApiService, |
|||
private msg: NzMessageService, |
|||
private local: LocalHttpInterceptorService, |
|||
private router: Router, |
|||
) {} |
|||
|
|||
loading = false |
|||
|
|||
loginForm = new FormGroup({ |
|||
username: new FormControl('', [FormValidators.required('请输入用户名')]), |
|||
password: new FormControl('', [FormValidators.required('请输入密码')]), |
|||
}) |
|||
|
|||
onLogin() { |
|||
if (FormValidators.validateFormGroup(this.loginForm)) { |
|||
this.loading = true |
|||
this.router.navigate(['/']) |
|||
return |
|||
this.api |
|||
.login(this.loginForm.value) |
|||
.pipe( |
|||
finalize(() => { |
|||
this.loading = false |
|||
}), |
|||
) |
|||
.subscribe((res) => { |
|||
this.msg.success('登录成功') |
|||
this.local.setAccess({ |
|||
access_token: res.data, |
|||
}) |
|||
localStorage.setItem('add', res.data) |
|||
this.router.navigate(['/']) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<nz-card [nzBordered]="false" class="h-screen"> |
|||
<div class="flex items-center justify-center"> |
|||
<nz-result nzStatus="404" nzTitle="此页面未找到。"></nz-result> |
|||
</div> |
|||
</nz-card> |
|||
@ -0,0 +1,11 @@ |
|||
import { Component } from '@angular/core' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
|
|||
@Component({ |
|||
selector: 'app-notfound', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './notfound.component.html', |
|||
styleUrl: './notfound.component.less', |
|||
}) |
|||
export class NotfoundComponent {} |
|||
@ -0,0 +1,43 @@ |
|||
<div> |
|||
<nz-card [nzBordered]="false" nzTitle="产品详情" [nzExtra]="nzExtraTpl"> |
|||
<ng-template #nzExtraTpl> |
|||
<nz-space> |
|||
<button *nzSpaceItem nz-button [routerLink]="['/', 'product']">返回</button> |
|||
</nz-space> |
|||
</ng-template> |
|||
|
|||
@if (detail(); as info) { |
|||
<nz-descriptions [nzColumn]="2" class="ml-16"> |
|||
<nz-descriptions-item nzTitle="产品ID"> |
|||
{{ info.productId }} |
|||
</nz-descriptions-item> |
|||
<nz-descriptions-item nzTitle="产品名称"> |
|||
{{ info.name }} |
|||
</nz-descriptions-item> |
|||
|
|||
<nz-descriptions-item nzTitle="说明">{{ info.remark || '-' }}</nz-descriptions-item> |
|||
</nz-descriptions> |
|||
} |
|||
</nz-card> |
|||
</div> |
|||
<!-- <div class="mt-4"> |
|||
<nz-card [nzBordered]="false" nzTitle="版本管理" [nzExtra]="versionExtraTpl"> |
|||
<ng-template #versionExtraTpl> |
|||
<nz-space> |
|||
<button *nzSpaceItem nz-button nzType="primary" (click)="createVersion()">添加</button> |
|||
</nz-space> |
|||
</ng-template> |
|||
<app-server-paginated-table [options]="tableOption"></app-server-paginated-table> |
|||
</nz-card> |
|||
</div> --> |
|||
|
|||
<ng-template #versionFormTpl> |
|||
<form nz-form [formGroup]="formGroup"> |
|||
<nz-form-item> |
|||
<nz-form-label nzRequired>版本号</nz-form-label> |
|||
<nz-form-control> |
|||
<input nz-input formControlName="name" placeholder="请输入版本号" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
</ng-template> |
|||
@ -0,0 +1,84 @@ |
|||
import { TemplateRef } from '@angular/core' |
|||
import { ViewChild } from '@angular/core' |
|||
import { Component, OnInit, signal, computed, effect } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { ActivatedRoute, Router } from '@angular/router' |
|||
import { TableOption } from 'app/components/server-paginated-table' |
|||
import { ApiService } from 'app/services' |
|||
import { VersionDTO } from 'app/services/api.dto' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { FormValidators } from 'app/utils' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { lastValueFrom } from 'rxjs' |
|||
|
|||
@Component({ |
|||
selector: 'app-product-detail', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './product-detail.component.html', |
|||
styleUrl: './product-detail.component.less', |
|||
}) |
|||
export class ProductDetailComponent implements OnInit { |
|||
constructor( |
|||
private api: ApiService, |
|||
private router: Router, |
|||
private route: ActivatedRoute, |
|||
private modal: NzModalService, |
|||
private msg: NzMessageService, |
|||
) {} |
|||
|
|||
@ViewChild('versionFormTpl') versionFormTpl!: TemplateRef<{}> |
|||
|
|||
tableOption = new TableOption(this.fetchData.bind(this)) |
|||
|
|||
formGroup = new FormGroup({ |
|||
name: new FormControl('', [FormValidators.required('请输入产品版本号')]), |
|||
}) |
|||
|
|||
detail = signal({ |
|||
productId: '', |
|||
name: '', |
|||
remark: '', |
|||
}) |
|||
|
|||
id!: string |
|||
|
|||
ngOnInit(): void { |
|||
this.id = this.route.snapshot.paramMap.get('id')! |
|||
this.api.getProductDetail(this.id).subscribe((res) => { |
|||
this.detail.set(res.data) |
|||
}) |
|||
} |
|||
|
|||
fetchData(p: {}, q: {}) { |
|||
return this.api.getProductVersionPage(p, { ...q, productId: this.id }) |
|||
} |
|||
|
|||
createVersion(version?: VersionDTO) { |
|||
this.modal.create({ |
|||
nzTitle: '添加版本', |
|||
nzContent: this.versionFormTpl, |
|||
nzOnOk: async () => { |
|||
if (FormValidators.validateFormGroup(this.formGroup)) { |
|||
await lastValueFrom(this.api.saveVersion({ ...(version ?? {}), ...this.formGroup.value })) |
|||
this.msg.success('保存成功') |
|||
this.tableOption.ref?.reload() |
|||
} |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
deleteItem(v: VersionDTO) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: '是否要删除该版本?', |
|||
nzOkDanger: true, |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.deleteVersion(v.id, this.id)) |
|||
this.msg.success('删除成功') |
|||
this.tableOption.ref?.reload() |
|||
}, |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
<nz-card [nzBordered]="false"> |
|||
<app-server-paginated-table |
|||
#tableTpl |
|||
[renderColumn]="renderColumnsTpl" |
|||
[formGroup]="queryForm" |
|||
[simpleSearch]="true" |
|||
[options]="table" |
|||
> |
|||
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row"> |
|||
@switch (key) { |
|||
@case ('name') { |
|||
<a [routerLink]="['/', 'product', row.id, 'detail']"> |
|||
{{ data }} |
|||
</a> |
|||
} |
|||
@default { |
|||
{{ data }} |
|||
} |
|||
} |
|||
</ng-template> |
|||
|
|||
<div *appTableForm nz-row [nzGutter]="[24, 24]"> |
|||
<div nz-col nzSpan="8"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px">产品名称</nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<input nz-input placeholder="请输入产品名称" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
<div nz-col nzSpan="12"> |
|||
<nz-form-item> |
|||
<nz-form-label nzFlex="80px">创建时间</nz-form-label> |
|||
<nz-form-control nzSpan="18"> |
|||
<!-- <nz-range-picker nzShowTime formControlName="createTime"></nz-range-picker> --> |
|||
<app-range-picker [showTime]="true" formControlName="createTime"></app-range-picker> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</div> |
|||
</div> |
|||
<div *appTableAction class="flex"> |
|||
<div> |
|||
<nz-space> |
|||
<button *nzSpaceItem nzType="primary" nz-button (click)="create()">添加产品</button> |
|||
</nz-space> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- <div action>Custom Modal action</div> --> |
|||
</app-server-paginated-table> |
|||
</nz-card> |
|||
|
|||
<ng-template #productFormTpl> |
|||
<form [formGroup]="createForm" nz-form [nzLayout]="'vertical'"> |
|||
<nz-form-item> |
|||
<nz-form-label nzRequired> 产品名称 </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入产品名称" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label nzRequired> 产品ID </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入产品ID" formControlName="productId" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label> 产品说明 </nz-form-label> |
|||
<nz-form-control [nzErrorTip]="formErrorTipsTpl"> |
|||
<textarea nz-input placeholder="请输入产品说明" formControlName="remark"></textarea> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
</ng-template> |
|||
|
|||
<ng-template #formErrorTipsTpl let-control> |
|||
<form-error-tips [control]="control"></form-error-tips> |
|||
</ng-template> |
|||
@ -0,0 +1,104 @@ |
|||
import { Component, ElementRef, OnInit, TemplateRef, ViewChild } from '@angular/core' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { NzModalService } from 'ng-zorro-antd/modal' |
|||
import { ApiService } from 'app/services' |
|||
import { AnyObject, ServerPaginatedTableComponent, TableOption } from 'app/components/server-paginated-table' |
|||
import { SharedModule } from 'app/shared/shared.module' |
|||
import { FormValidators } from 'app/utils' |
|||
import { lastValueFrom } from 'rxjs' |
|||
import { ProductDTO } from 'app/services/api.dto' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { format } from 'date-fns' |
|||
|
|||
@Component({ |
|||
selector: 'app-product-list', |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
templateUrl: './product-list.component.html', |
|||
styleUrl: './product-list.component.less', |
|||
}) |
|||
export class ProductListComponent { |
|||
constructor( |
|||
private api: ApiService, |
|||
private modal: NzModalService, |
|||
private msg: NzMessageService, |
|||
) {} |
|||
|
|||
@ViewChild('productFormTpl') productFormTpl!: TemplateRef<{}> |
|||
|
|||
table = new TableOption( |
|||
this.fetchData.bind(this), |
|||
[ |
|||
{ key: 'name', title: '产品名称' }, |
|||
{ key: 'productId', title: '产品ID' }, |
|||
{ key: 'remark', title: '产品说明', width: '500px' }, |
|||
{ key: 'createTime', title: '创建时间' }, |
|||
], |
|||
[], |
|||
) |
|||
|
|||
queryForm = new FormGroup({ |
|||
name: new FormControl(''), |
|||
createTime: new FormControl(), |
|||
}) |
|||
|
|||
createForm = new FormGroup({ |
|||
name: new FormControl('', [FormValidators.required('请输入产品名称'), FormValidators.maxLength(30)]), |
|||
productId: new FormControl('', [FormValidators.required('请输入产品ID'), FormValidators.maxLength(30)]), |
|||
remark: new FormControl('', [FormValidators.maxLength(200)]), |
|||
}) |
|||
|
|||
ngOnInit(): void { |
|||
this.table.setRowOperate([ |
|||
{ title: '编辑', onClick: this.create.bind(this) }, |
|||
{ title: '删除', onClick: this.deleteItem.bind(this) }, |
|||
]) |
|||
} |
|||
|
|||
fetchData(p: {}, q: AnyObject) { |
|||
if (Array.isArray(q['createTime'])) { |
|||
const createTimeStart = q['createTime']?.[0] |
|||
const createTimeEnd = q['createTime']?.[1] |
|||
|
|||
q['createTimeStart'] = createTimeStart ? format(new Date(createTimeStart), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
q['createTimeEnd'] = createTimeEnd ? format(new Date(createTimeEnd), 'yyyy-MM-dd HH:mm:ss') : '' |
|||
} |
|||
return this.api.getProductPage(p, q) |
|||
} |
|||
|
|||
onSearch() {} |
|||
|
|||
create(product?: ProductDTO) { |
|||
if (product) { |
|||
this.createForm.patchValue(product) |
|||
} |
|||
this.modal.create({ |
|||
nzTitle: product ? '编辑产品' : '添加产品', |
|||
nzContent: this.productFormTpl, |
|||
nzOnOk: async () => { |
|||
if (FormValidators.validateFormGroup(this.createForm)) { |
|||
await lastValueFrom(this.api.saveProduct({ ...(product ?? {}), ...this.createForm.value })) |
|||
this.msg.success('保存成功') |
|||
this.table.ref?.reload() |
|||
this.createForm.reset() |
|||
} |
|||
}, |
|||
nzOnCancel: () => { |
|||
this.createForm.reset() |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
deleteItem(product: ProductDTO) { |
|||
this.modal.confirm({ |
|||
nzTitle: '警告', |
|||
nzContent: '是否要删除该产品?', |
|||
nzOkDanger: true, |
|||
nzOnOk: async () => { |
|||
await lastValueFrom(this.api.deleteProduct(product.id)) |
|||
this.msg.success('删除成功') |
|||
this.table.ref?.reload() |
|||
}, |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<div class="w-[600px] pt-6"> |
|||
<form nz-form [formGroup]="formGroup" (ngSubmit)="onSubmit()"> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired> 用户名 </nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<input nz-input placeholder="请输入用户名" formControlName="username" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired> 新密码 </nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<input nz-input type="password" placeholder="请输入新密码" formControlName="password" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired> 确认密码 </nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<input nz-input type="password" placeholder="请再次输入密码" formControlName="confirmPassword" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-control nzOffset="4"> |
|||
<button nz-button nzType="primary">保存</button> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
</div> |
|||
@ -0,0 +1,46 @@ |
|||
import { Component } from '@angular/core' |
|||
import { SharedModule } from '../../shared/shared.module' |
|||
import { FormControl, FormGroup } from '@angular/forms' |
|||
import { FormValidators } from 'app/utils' |
|||
import { ApiService, LocalHttpInterceptorService } from 'app/services' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
imports: [SharedModule], |
|||
selector: 'app-profile-account', |
|||
templateUrl: './profile-account.component.html', |
|||
styleUrls: ['./profile-account.component.less'], |
|||
}) |
|||
export class ProfileAccountComponent { |
|||
constructor( |
|||
private local: LocalHttpInterceptorService, |
|||
private api: ApiService, |
|||
private msg: NzMessageService, |
|||
) {} |
|||
|
|||
formGroup = new FormGroup({ |
|||
id: new FormControl(this.api.authInfo.id), |
|||
username: new FormControl({ value: this.api.authInfo.username, disabled: true }, [ |
|||
FormValidators.required('请输入用户名'), |
|||
]), |
|||
password: new FormControl('', [FormValidators.required('请输入密码')]), |
|||
confirmPassword: new FormControl('', [FormValidators.required('请再次输入密码')]), |
|||
}) |
|||
|
|||
onSubmit() { |
|||
if (FormValidators.validateFormGroup(this.formGroup)) { |
|||
if (this.formGroup.value.confirmPassword !== this.formGroup.value.password) { |
|||
this.msg.error('两次输入密码不一致') |
|||
return |
|||
} |
|||
const v = this.formGroup.getRawValue() |
|||
this.api.changePassword(v).subscribe(() => { |
|||
this.msg.success('修改成功,请重新登录').onClose.subscribe(() => { |
|||
this.local.removeAccess() |
|||
window.location.href = '/login' |
|||
}) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
<div class="w-[600px] pt-6"> |
|||
<form nz-form [formGroup]="formGroup" (ngSubmit)="onSubmit()"> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4" nzRequired>姓名</nz-form-label> |
|||
<nz-form-control nzSpan="16" [nzErrorTip]="formErrorTipsTpl"> |
|||
<input nz-input placeholder="请输入姓名" formControlName="name" /> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
|
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4">备注</nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<textarea nz-input placeholder="请输入备注" formControlName="remark"></textarea> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<!-- <nz-form-item> |
|||
<nz-form-label nzSpan="4">登录限制</nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<nz-switch></nz-switch> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
<nz-form-item> |
|||
<nz-form-label nzSpan="4">限制规则</nz-form-label> |
|||
<nz-form-control nzSpan="16"> |
|||
<div> |
|||
<nz-input-group nzAddOnBefore="登录失败达到" nzAddOnAfter="次"> |
|||
<input type="number" nz-input /> |
|||
</nz-input-group> |
|||
</div> |
|||
<div class="mt-2"> |
|||
<ng-template #nzAddOnAfterTpl> |
|||
<nz-select class="!w-20"> |
|||
<nz-option nzValue="m" nzLabel="分钟"></nz-option> |
|||
<nz-option nzValue="h" nzLabel="小时"></nz-option> |
|||
<nz-option nzValue="d" nzLabel="天"></nz-option> |
|||
</nz-select> |
|||
</ng-template> |
|||
<nz-input-group nzAddOnBefore="锁定账号" [nzAddOnAfter]="nzAddOnAfterTpl"> |
|||
<input type="number" nz-input /> |
|||
</nz-input-group> |
|||
</div> |
|||
</nz-form-control> |
|||
</nz-form-item> --> |
|||
<nz-form-item> |
|||
<nz-form-control nzOffset="4"> |
|||
<button nz-button nzType="primary">保存</button> |
|||
</nz-form-control> |
|||
</nz-form-item> |
|||
</form> |
|||
|
|||
<ng-template #formErrorTipsTpl let-control> |
|||
<form-error-tips [control]="control"></form-error-tips> |
|||
</ng-template> |
|||
</div> |
|||
@ -0,0 +1,52 @@ |
|||
import { Component, OnDestroy, OnInit } from '@angular/core' |
|||
import { SharedModule } from '../../shared/shared.module' |
|||
import { ApiService } from 'app/services' |
|||
import { UserFormComponent } from 'app/components/user-form/user-form.component' |
|||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms' |
|||
import { FormValidators } from 'app/utils' |
|||
import { NzMessageService } from 'ng-zorro-antd/message' |
|||
import { Subscription } from 'rxjs' |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
imports: [SharedModule, UserFormComponent], |
|||
selector: 'app-profile-basic', |
|||
templateUrl: './profile-basic.component.html', |
|||
styleUrls: ['./profile-basic.component.less'], |
|||
}) |
|||
export class ProfileBasicComponent implements OnInit, OnDestroy { |
|||
constructor( |
|||
public api: ApiService, |
|||
private fb: FormBuilder, |
|||
private msg: NzMessageService, |
|||
) {} |
|||
|
|||
formGroup!: FormGroup |
|||
|
|||
ngOnInit(): void { |
|||
this.formGroup = this.fb.group({ |
|||
name: new FormControl(this.api.authInfo.name, [FormValidators.required('请输入姓名')]), |
|||
remark: new FormControl(this.api.authInfo.remark), |
|||
}) |
|||
} |
|||
|
|||
ngOnDestroy(): void {} |
|||
|
|||
public getValues() { |
|||
let values = null |
|||
if (FormValidators.validateFormGroup(this.formGroup)) { |
|||
values = this.formGroup.value |
|||
} |
|||
return values |
|||
} |
|||
|
|||
onSubmit() { |
|||
const vals = this.getValues() |
|||
if (vals) { |
|||
this.api.saveUser({ ...this.api.authInfo, ...vals }).subscribe(() => { |
|||
this.msg.success('保存成功') |
|||
this.api.getAuthInfo(true).subscribe(() => {}) |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<div> |
|||
<nz-card [nzBordered]="false"> |
|||
<nz-descriptions [nzColumn]="2" class="ml-16"> |
|||
<nz-descriptions-item nzTitle="用户名"> |
|||
{{ api.authInfo.username }} |
|||
</nz-descriptions-item> |
|||
<nz-descriptions-item nzTitle="姓名"> |
|||
{{ api.authInfo.name }} |
|||
</nz-descriptions-item> |
|||
<nz-descriptions-item nzTitle="角色"> |
|||
{{ api.authInfo.roleName }} |
|||
</nz-descriptions-item> |
|||
<nz-descriptions-item nzTitle="状态"> |
|||
@if (api.authInfo.enable) { |
|||
<nz-badge nzStatus="success" nzText="启用"></nz-badge> |
|||
} @else { |
|||
<nz-badge nzStatus="errot" nzText="禁用"></nz-badge> |
|||
} |
|||
</nz-descriptions-item> |
|||
<nz-descriptions-item nzTitle="备注">{{ api.authInfo.remark || '-' }}</nz-descriptions-item> |
|||
</nz-descriptions> |
|||
</nz-card> |
|||
</div> |
|||
<div class="mt-4"> |
|||
<nz-card [nzBordered]="false"> |
|||
<div class="nav-bar"> |
|||
<ul> |
|||
<li class="cursor-pointer" [routerLink]="['/profile/basic']" routerLinkActive="active">基础信息</li> |
|||
<li class="cursor-pointer" [routerLink]="['/profile/account']" routerLinkActive="active">账号密码</li> |
|||
</ul> |
|||
</div> |
|||
<div class="py-4"> |
|||
<router-outlet></router-outlet> |
|||
</div> |
|||
</nz-card> |
|||
</div> |
|||
@ -0,0 +1,18 @@ |
|||
.nav-bar { |
|||
ul { |
|||
display: flex; |
|||
|
|||
li { |
|||
padding: 6px 24px; |
|||
margin-right: 12px; |
|||
font-size: 14px; |
|||
border-radius: 20px; |
|||
font-weight: bold; |
|||
|
|||
&.active { |
|||
background-color: #f2f3f5; |
|||
color: var(--primary); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue