Browse Source

开始 界面

main
kkerwin 2 years ago
parent
commit
a4d7678832
  1. 136
      angular.json
  2. 80
      package.json
  3. 4731
      pnpm-lock.yaml
  4. 70
      projects/admin/src/app/app-routing.module.ts
  5. 1
      projects/admin/src/app/app.component.html
  6. 4
      projects/admin/src/app/app.component.less
  7. 10
      projects/admin/src/app/app.component.ts
  8. 70
      projects/admin/src/app/app.module.ts
  9. 52
      projects/admin/src/app/components/app-layout/app-layout.component.html
  10. 59
      projects/admin/src/app/components/app-layout/app-layout.component.less
  11. 8
      projects/admin/src/app/components/app-layout/app-layout.component.ts
  12. 17
      projects/admin/src/app/components/app-page/app-page.component.html
  13. 9
      projects/admin/src/app/components/app-page/app-page.component.less
  14. 16
      projects/admin/src/app/components/app-page/app-page.component.ts
  15. 139
      projects/admin/src/app/components/dish-form/dish-form.component.html
  16. 17
      projects/admin/src/app/components/dish-form/dish-form.component.less
  17. 122
      projects/admin/src/app/components/dish-form/dish-form.component.ts
  18. 70
      projects/admin/src/app/components/food-form/food-form.component.html
  19. 8
      projects/admin/src/app/components/food-form/food-form.component.less
  20. 40
      projects/admin/src/app/components/food-form/food-form.component.ts
  21. 7
      projects/admin/src/app/components/index.ts
  22. 86
      projects/admin/src/app/components/ingredient-form-basic/ingredient-form-basic.component.html
  23. 7
      projects/admin/src/app/components/ingredient-form-basic/ingredient-form-basic.component.less
  24. 43
      projects/admin/src/app/components/ingredient-form-basic/ingredient-form-basic.component.ts
  25. 17
      projects/admin/src/app/icons-provider.module.ts
  26. 67
      projects/admin/src/app/pages/dish/dish.component.html
  27. 8
      projects/admin/src/app/pages/dish/dish.component.less
  28. 81
      projects/admin/src/app/pages/dish/dish.component.ts
  29. 71
      projects/admin/src/app/pages/food/food.component.html
  30. 9
      projects/admin/src/app/pages/food/food.component.less
  31. 72
      projects/admin/src/app/pages/food/food.component.ts
  32. 1
      projects/admin/src/app/pages/home/home.component.html
  33. 0
      projects/admin/src/app/pages/home/home.component.less
  34. 10
      projects/admin/src/app/pages/home/home.component.ts
  35. 11
      projects/admin/src/app/pages/index.ts
  36. 279
      projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.html
  37. 0
      projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.less
  38. 25
      projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.ts
  39. 65
      projects/admin/src/app/pages/ingredients/ingredient-list/ingredient-list.component.html
  40. 0
      projects/admin/src/app/pages/ingredients/ingredient-list/ingredient-list.component.less
  41. 98
      projects/admin/src/app/pages/ingredients/ingredient-list/ingredient-list.component.ts
  42. 70
      projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.html
  43. 0
      projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.less
  44. 81
      projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.ts
  45. 61
      projects/admin/src/app/pages/ingredients/ingredient-review/ingredient-review.component.html
  46. 0
      projects/admin/src/app/pages/ingredients/ingredient-review/ingredient-review.component.less
  47. 81
      projects/admin/src/app/pages/ingredients/ingredient-review/ingredient-review.component.ts
  48. 63
      projects/admin/src/app/pages/login/login.component.html
  49. 65
      projects/admin/src/app/pages/login/login.component.less
  50. 42
      projects/admin/src/app/pages/login/login.component.ts
  51. 25
      projects/admin/src/app/services/api.service.ts
  52. 84
      projects/admin/src/app/services/http.interceptor.ts
  53. 1
      projects/admin/src/app/services/index.ts
  54. 0
      projects/admin/src/app/shared/components/index.ts
  55. 102
      projects/admin/src/app/shared/ng-zorro.ts
  56. 41
      projects/admin/src/app/shared/shared.module.ts
  57. 0
      projects/admin/src/assets/.gitkeep
  58. BIN
      projects/admin/src/assets/images/jl-logo.png
  59. BIN
      projects/admin/src/assets/images/login.jpg
  60. 1
      projects/admin/src/assets/k-icon/carrot.svg
  61. 1
      projects/admin/src/assets/k-icon/food.svg
  62. BIN
      projects/admin/src/favicon.ico
  63. 20
      projects/admin/src/index.html
  64. 7
      projects/admin/src/main.ts
  65. 39
      projects/admin/src/styles.less
  66. 14
      projects/admin/tsconfig.app.json
  67. 14
      projects/admin/tsconfig.spec.json
  68. 24
      projects/cdk/README.md
  69. 7
      projects/cdk/ng-package.json
  70. 12
      projects/cdk/package.json
  71. 36
      projects/cdk/src/form-error-tips/form-error-tips.component.html
  72. 0
      projects/cdk/src/form-error-tips/form-error-tips.component.less
  73. 28
      projects/cdk/src/form-error-tips/form-error-tips.component.ts
  74. 24
      projects/cdk/src/input-space-error/input-space-error.directive.ts
  75. 14
      projects/cdk/src/public-api.ts
  76. 0
      projects/cdk/src/quick-date-range/quick-date-range.component.css
  77. 17
      projects/cdk/src/quick-date-range/quick-date-range.component.html
  78. 94
      projects/cdk/src/quick-date-range/quick-date-range.component.ts
  79. 71
      projects/cdk/src/storage/cache.service.ts
  80. 3
      projects/cdk/src/storage/index.ts
  81. 15
      projects/cdk/src/storage/storage.module.ts
  82. 79
      projects/cdk/src/storage/storage.service.ts
  83. 5
      projects/cdk/src/table-list/index.ts
  84. 143
      projects/cdk/src/table-list/table-list-options.ts
  85. 56
      projects/cdk/src/table-list/table-list.module.ts
  86. 225
      projects/cdk/src/table-list/table-list/table-list.component.html
  87. 119
      projects/cdk/src/table-list/table-list/table-list.component.less
  88. 386
      projects/cdk/src/table-list/table-list/table-list.component.ts
  89. 30
      projects/cdk/src/table-list/table-operation/table-operation.component.html
  90. 25
      projects/cdk/src/table-list/table-operation/table-operation.component.less
  91. 61
      projects/cdk/src/table-list/table-operation/table-operation.component.ts
  92. 30
      projects/cdk/src/table-list/td-overflow.directive.ts
  93. 58
      projects/cdk/src/types/index.ts
  94. 46
      projects/cdk/src/utils/index.ts
  95. 66
      projects/cdk/src/validators/index.ts
  96. 14
      projects/cdk/tsconfig.lib.json
  97. 10
      projects/cdk/tsconfig.lib.prod.json
  98. 14
      projects/cdk/tsconfig.spec.json
  99. 8
      tailwind.config.js
  100. 69
      tsconfig.json

136
angular.json

@ -1,10 +1,130 @@
{ {
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1, "version": 1,
"cli": { "cli": {
"packageManager": "pnpm" "packageManager": "pnpm"
}, },
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
} "admin": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "less"
}
},
"root": "projects/admin",
"sourceRoot": "projects/admin/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/admin",
"index": "projects/admin/src/index.html",
"main": "projects/admin/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "projects/admin/tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
"projects/admin/src/favicon.ico",
"projects/admin/src/assets",
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
}
],
"styles": ["projects/admin/src/styles.less"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "admin:build:production"
},
"development": {
"browserTarget": "admin:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "admin:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "projects/admin/tsconfig.spec.json",
"inlineStyleLanguage": "less",
"assets": ["projects/admin/src/favicon.ico", "projects/admin/src/assets"],
"styles": ["projects/admin/src/styles.less"],
"scripts": []
}
}
}
},
"cdk": {
"projectType": "library",
"root": "projects/cdk",
"sourceRoot": "projects/cdk/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/cdk/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/cdk/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/cdk/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "projects/cdk/tsconfig.spec.json",
"polyfills": ["zone.js", "zone.js/testing"]
}
}
}
}
}
} }

80
package.json

@ -1,37 +1,47 @@
{ {
"name": "catering-web-app", "name": "catering-web-app",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test" "test": "ng test"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^16.1.0", "@angular/animations": "^16.1.0",
"@angular/common": "^16.1.0", "@angular/cdk": "^16.2.0",
"@angular/compiler": "^16.1.0", "@angular/common": "^16.1.0",
"@angular/core": "^16.1.0", "@angular/compiler": "^16.1.0",
"@angular/forms": "^16.1.0", "@angular/core": "^16.1.0",
"@angular/platform-browser": "^16.1.0", "@angular/forms": "^16.1.0",
"@angular/platform-browser-dynamic": "^16.1.0", "@angular/platform-browser": "^16.1.0",
"@angular/router": "^16.1.0", "@angular/platform-browser-dynamic": "^16.1.0",
"rxjs": "~7.8.0", "@angular/router": "^16.1.0",
"tslib": "^2.3.0", "@ant-design/icons-angular": "^16.0.0",
"zone.js": "~0.13.0" "immer": "^10.0.2",
}, "ng-zorro-antd": "16.1.0",
"devDependencies": { "ngx-permissions": "^15.0.1",
"@angular/cli": "~16.1.4", "rxjs": "~7.8.0",
"@angular/compiler-cli": "^16.1.0", "tslib": "^2.3.0",
"@types/jasmine": "~4.3.0", "zone.js": "~0.13.0"
"jasmine-core": "~4.6.0", },
"karma": "~6.4.0", "devDependencies": {
"karma-chrome-launcher": "~3.2.0", "@angular-devkit/build-angular": "^16.1.4",
"karma-coverage": "~2.2.0", "@angular/cli": "~16.1.4",
"karma-jasmine": "~5.1.0", "@angular/compiler-cli": "^16.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "@types/jasmine": "~4.3.0",
"typescript": "~5.1.3" "autoprefixer": "^10.4.14",
} "jasmine-core": "~4.6.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",
"ng-packagr": "^16.0.0",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "~5.1.3"
}
} }

4731
pnpm-lock.yaml

File diff suppressed because it is too large

70
projects/admin/src/app/app-routing.module.ts

@ -0,0 +1,70 @@
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import {
DishComponent,
FoodComponent,
HomeComponent,
IngredientFormComponent,
IngredientListComponent,
IngredientReleaseComponent,
IngredientReviewComponent,
LoginComponent,
} from "./pages";
import { AppLayoutComponent } from "./components";
const routes: Routes = [
{ path: "login", component: LoginComponent },
{
path: "",
component: AppLayoutComponent,
children: [
{ path: "", pathMatch: "full", redirectTo: "home" },
{ path: "home", component: HomeComponent },
{ path: "food", component: FoodComponent },
{ path: "dish", component: DishComponent },
{
path: "ingredient",
children: [
{
path: "",
pathMatch: "full",
redirectTo: "item",
},
{
path: "item",
children: [
{
path: "",
pathMatch: "full",
redirectTo: "list",
},
{
path: "list",
component: IngredientListComponent,
},
{
path: "form/:id",
component: IngredientFormComponent,
},
],
},
{
path: "review",
component: IngredientReviewComponent,
},
{
path: "release",
component: IngredientReleaseComponent,
},
],
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

1
projects/admin/src/app/app.component.html

@ -0,0 +1 @@
<router-outlet></router-outlet>

4
projects/admin/src/app/app.component.less

@ -0,0 +1,4 @@
:host {
display: block;
height: 100%;
}

10
projects/admin/src/app/app.component.ts

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
})
export class AppComponent {
isCollapsed = false;
}

70
projects/admin/src/app/app.module.ts

@ -0,0 +1,70 @@
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { NZ_I18N } from "ng-zorro-antd/i18n";
import { zh_CN } from "ng-zorro-antd/i18n";
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 { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { IconsProviderModule } from "./icons-provider.module";
import { SharedModule } from "./shared/shared.module";
import {
AppLayoutComponent,
AppPageComponent,
FoodFormComponent,
DishFormComponent,
IngredientFormBasicComponent,
} from "./components";
import {
HomeComponent,
LoginComponent,
FoodComponent,
DishComponent,
IngredientListComponent,
IngredientReleaseComponent,
IngredientReviewComponent,
IngredientFormComponent,
} from "./pages";
import { HTTPInterceptor } from "./services/http.interceptor";
registerLocaleData(zh);
@NgModule({
declarations: [
AppComponent,
AppLayoutComponent,
AppPageComponent,
FoodFormComponent,
DishFormComponent,
IngredientFormBasicComponent,
HomeComponent,
LoginComponent,
FoodComponent,
DishComponent,
IngredientListComponent,
IngredientReleaseComponent,
IngredientReviewComponent,
IngredientFormComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
BrowserAnimationsModule,
IconsProviderModule,
SharedModule,
],
providers: [
{ provide: NZ_I18N, useValue: zh_CN },
{ provide: HTTP_INTERCEPTORS, useClass: HTTPInterceptor, multi: true },
],
bootstrap: [AppComponent],
})
export class AppModule {}

52
projects/admin/src/app/components/app-layout/app-layout.component.html

@ -0,0 +1,52 @@
<nz-layout class="app-layout">
<nz-header class="app-header">
<div class="flex items-center justify-between h-full">
<div class="logo flex items-center h-full">
<img class="block h-[40px] mr-2" src="../assets/images/jl-logo.png" />
<span class="text-lg text-white font-bold">
智慧配餐管理后台
</span>
</div>
</div>
</nz-header>
<nz-layout class="app-layout-main">
<nz-sider nzWidth="200px" nzTheme="light" class="sider-menu">
<ul nz-menu nzMode="inline">
<li nz-menu-item class="k-icon" [routerLink]="['/','food']" nzMatchRouter>
<span nz-icon nzType="k-icon:carrot" nzTheme="outline"></span>
<span>食材管理</span>
</li>
<li nz-menu-item class="k-icon" [routerLink]="['/','dish']" nzMatchRouter>
<span nz-icon nzType="k-icon:food" nzTheme="outline"></span>
<span>菜品管理</span>
</li>
<li nz-submenu nzTitle="食谱管理" nzIcon="book">
<ul>
<li nz-menu-item nzMatchRouter [routerLink]="['/','ingredient','item']">食谱库</li>
<li nz-menu-item nzMatchRouter [routerLink]="['/','ingredient','review']">食谱审核</li>
<li nz-menu-item nzMatchRouter [routerLink]="['/','ingredient','release']">食谱发布计划</li>
</ul>
</li>
<li nz-menu-item nz-icon="profile">
<span nz-icon nzType="profile" nzTheme="outline"></span>
<span>人群营养标准管理</span>
</li>
<li nz-menu-item>
<span nz-icon nzType="usergroup-add" nzTheme="outline"></span>
<span>单位管理</span>
</li>
<li nz-submenu nzTitle="系统设置" nzIcon="setting">
<ul>
<li nz-menu-item>用户管理</li>
</ul>
</li>
</ul>
</nz-sider>
<nz-layout class="inner-layout">
<router-outlet></router-outlet>
</nz-layout>
</nz-layout>
</nz-layout>

59
projects/admin/src/app/components/app-layout/app-layout.component.less

@ -0,0 +1,59 @@
:host {
display: flex;
flex-direction: column;
min-height: 100%;
}
@header-height: 48px;
.app-layout {
display: flex;
flex-direction: column;
flex: 1;
min-height: 100%;
}
.app-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: @header-height;
line-height: @header-height;
padding: 0 24px;
}
.k-icon {
::ng-deep {
.anticon {
font-size: 16px;
}
}
}
.sider-menu {
position: fixed;
top: @header-height;
left: 0;
bottom: 0;
z-index: 100;
}
.app-layout-main {
min-height: 100%;
}
.inner-layout {
padding-top: @header-height;
padding-left: 200px;
::ng-deep {
router-outlet+* {
display: flex;
flex-direction: column;
min-height: 100%;
}
}
}

8
projects/admin/src/app/components/app-layout/app-layout.component.ts

@ -0,0 +1,8 @@
import { Component } from "@angular/core";
@Component({
selector: "app-layout",
templateUrl: "./app-layout.component.html",
styleUrls: ["./app-layout.component.less"],
})
export class AppLayoutComponent {}

17
projects/admin/src/app/components/app-page/app-page.component.html

@ -0,0 +1,17 @@
<div class="app-page flex flex-col h-full " [ngClass]="{'m-4' : !full}">
<div class="app-header flex justify-between mb-2" *ngIf="pageTitle || pageExtra">
<div class="app-page-title" *ngIf="pageTitle">
<h2 *nzStringTemplateOutlet="pageTitle" class="mb-1">
{{pageTitle}}
</h2>
</div>
<div class="app-page-extra">
<ng-container *nzStringTemplateOutlet="pageExtra">
{{pageExtra}}
</ng-container>
</div>
</div>
<main class="app-page-body flex-1 ">
<ng-content></ng-content>
</main>
</div>

9
projects/admin/src/app/components/app-page/app-page.component.less

@ -0,0 +1,9 @@
:host {
display: flex;
flex-direction: column;
min-height: 100%;
}
.app-page {
border-radius: 10px;
}

16
projects/admin/src/app/components/app-page/app-page.component.ts

@ -0,0 +1,16 @@
import { Component, Input, TemplateRef } from "@angular/core";
@Component({
selector: "app-page",
templateUrl: "./app-page.component.html",
styleUrls: ["./app-page.component.less"],
})
export class AppPageComponent {
constructor() {}
@Input() pageTitle?: TemplateRef<{}> | string;
@Input() full: boolean = false;
@Input() pageExtra?: TemplateRef<{}> | string;
}

139
projects/admin/src/app/components/dish-form/dish-form.component.html

@ -0,0 +1,139 @@
<form nz-form [formGroup]="formGroup" nzLayout="vertical">
<nz-form-item>
<nz-form-label nzRequired>
单位
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<nz-select
formControlName="unit"
[nzOptions]="[]"
nzMode="multiple"
nzPlaceHolder="请选择单位,多选时在多个单位均添加此菜品">
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired>
菜品名称
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<input placeholder="请输入菜品名称" nz-input formControlName="name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired>
菜品标签
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<nz-select
formControlName="tag"
[nzOptions]="[]"
nzPlaceHolder="请选择菜品标签">
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label>
适用月份
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<div>
<label
nz-checkbox
[(ngModel)]="allMonthChecked"
[ngModelOptions]="{standalone: true}"
(ngModelChange)="updateAllMonthChecked()"
[nzIndeterminate]="indeterminate">
全年
</label>
</div>
<nz-divider nzDashed class="my-1"></nz-divider>
<nz-checkbox-group [ngModel]="allMonth"
class="flex flex-wrap month-wrap"
formControlName="month"
(ngModelChange)="monthChecked()">
</nz-checkbox-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label>
菜品图片
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<button class="upload-btn " nz-button [nzLoading]="uploadLoading">
<i nz-icon nzType="upload"></i>
上传图片
<input type="file" formControlName="img" (change)="onFileChange($event)" />
</button>
</nz-form-control>
</nz-form-item>
<!-- <nz-form-item>
<nz-form-label nzRequired>
食材名称
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<input placeholder="请输入食材名称/编号检索" nz-input formControlName="food" />
</nz-form-control>
</nz-form-item> -->
<nz-form-item>
<nz-form-label nzRequired class="block-label">
<div class="flex justify-between items-center flex-1">
<span class="flex-1">
食材名称
</span>
<a nz-button nzType="link" (click)="addFoodVisible = true">
<span nz-icon nzType="plus"></span>
<span>
添加食材
</span>
</a>
</div>
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<div *ngIf="addFoodVisible">
<nz-select nzSize="large" nzShowSearch nzPlaceHolder="请输入食材名称/编号检索">
</nz-select>
<div class="flex justify-end my-2">
<nz-space>
<button *nzSpaceItem nz-button nzType="text" (click)="addFoodVisible = false">取消</button>
<button *nzSpaceItem nz-button nzType="primary" (click)="addFood()">确定</button>
</nz-space>
</div>
</div>
<ul formArrayName="food">
<li class="mb-2" *ngFor="let n of food.controls;let i = index" [formGroupName]="i">
<div class="flex items-center">
<div class="pr-2">
食材名称:
</div>
<div class="pr-2">
<nz-select class="!w-[200px]" nzPlaceHolder="食材标签"
formControlName="tag">
</nz-select>
</div>
<div class="flex-1 pr-2">
<nz-input-group [nzAddOnAfter]="'g'" class="w-full">
<input nz-input formControlName="weight" [placeholder]="'请输入xxx重量'" />
</nz-input-group>
</div>
<div>
<button nz-button (click)="removeFood(i)">
<span nz-icon nzType="delete"></span>
</button>
</div>
</div>
</li>
</ul>
</nz-form-control>
</nz-form-item>
</form>
<ng-template #formControlErrorTpl let-control>
<form-error-tips [control]="control"></form-error-tips>
</ng-template>

17
projects/admin/src/app/components/dish-form/dish-form.component.less

@ -0,0 +1,17 @@
.month-wrap {
::ng-deep {
.ant-checkbox-wrapper {
margin: 6px 0;
flex-basis: calc(100% / 6);
}
}
}
.block-label {
::ng-deep {
label {
display: inline-flex;
width: 100%;
}
}
}

122
projects/admin/src/app/components/dish-form/dish-form.component.ts

@ -0,0 +1,122 @@
import { Component, OnInit } from "@angular/core";
import { FormArray, FormBuilder, FormGroup } from "@angular/forms";
import { FormValidators } from "@cdk/validators";
import { NzMessageService } from "ng-zorro-antd/message";
import { finalize } from "rxjs";
@Component({
selector: "app-dish-form",
templateUrl: "./dish-form.component.html",
styleUrls: ["./dish-form.component.less"],
})
export class DishFormComponent {
constructor(private fb: FormBuilder, private msg: NzMessageService) {}
formGroup!: FormGroup;
allMonth = [
{ value: "1", label: "一月", checked: false },
{ value: "2", label: "二月", checked: false },
{ value: "3", label: "三月", checked: false },
{ value: "4", label: "四月", checked: false },
{ value: "5", label: "五月", checked: false },
{ value: "6", label: "六月", checked: false },
{ value: "7", label: "七月", checked: false },
{ value: "8", label: "八月", checked: false },
{ value: "9", label: "九月", checked: false },
{ value: "10", label: "十月", checked: false },
{ value: "11", label: "十一月", checked: false },
{ value: "12", label: "十二月", checked: false },
];
allMonthChecked = false;
indeterminate = false;
uploadLoading = false;
addFoodVisible = false;
get food(): FormArray {
return this.formGroup.get("food") as FormArray;
}
ngOnInit(): void {
this.formGroup = this.fb.group({
id: this.fb.control("", [FormValidators.required()]),
unit: this.fb.control([], [FormValidators.required()]),
name: this.fb.control("", [FormValidators.required()]),
img: this.fb.control("", []),
tag: this.fb.control([], []),
food: this.fb.array([], [FormValidators.required()]),
month: this.fb.control([], []),
});
}
addFood() {
this.food.push(
this.fb.group({
name: this.fb.control("", [FormValidators.required()]),
tag: this.fb.control(0, [FormValidators.required()]),
weight: this.fb.control(0, [FormValidators.required()]),
})
);
}
removeFood(idx: number) {
this.food.removeAt(idx);
}
updateAllMonthChecked() {
this.indeterminate = false;
if (this.allMonthChecked) {
this.allMonth = this.allMonth.map((item) => ({
...item,
checked: true,
}));
} else {
this.allMonth = this.allMonth.map((item) => ({
...item,
checked: false,
}));
}
}
monthChecked() {
if (this.allMonth.every((item) => !item.checked)) {
this.allMonthChecked = false;
this.indeterminate = false;
} else if (this.allMonth.every((item) => item.checked)) {
this.allMonthChecked = true;
this.indeterminate = false;
} else {
this.indeterminate = true;
}
}
onFileChange(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files![0];
target.value = "";
const formData = new FormData();
const fileReader = new FileReader();
fileReader.onload = () => {
const base64 = fileReader.result as string;
const v = base64.split("base64,")[1];
};
formData.append("file", file);
this.uploadLoading = true;
// this.api
// .uploadLogo(formData)
// .pipe(
// finalize(() => {
// this.uploadLoading = false;
// })
// )
// .subscribe((r) => {
// this.msg.success(r.desc);
// fileReader.readAsDataURL(file);
// });
}
}

70
projects/admin/src/app/components/food-form/food-form.component.html

@ -0,0 +1,70 @@
<form nz-form [formGroup]="formGroup" nzLayout="vertical">
<nz-form-item>
<nz-form-label nzRequired>
食材编号
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<input placeholder="请输入食材编号" nz-input formControlName="id" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired>
食材名称
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<input placeholder="请输入食材名称" nz-input formControlName="name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired>
食材类型
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<input placeholder="请输入食材类型" nz-input formControlName="type" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired class="block-label">
<div class="flex justify-between items-center flex-1">
<span class="flex-1">
营养素(每100g可食部)
</span>
<a nz-button nzType="link" (click)="createNutrition()">
<span nz-icon nzType="plus"></span>
<span>
添加营养素
</span>
</a>
</div>
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<ul formArrayName="nutrition">
<li class="mb-2" *ngFor="let n of nutrition.controls;let i = index" [formGroupName]="i">
<div class="flex">
<div>
<nz-select class="!w-[160px]" nzPlaceHolder="营养素"
formControlName="nutritionName">
</nz-select>
</div>
<div class="flex-1 px-2">
<nz-input-group [nzAddOnAfter]="'μgRAE'" class="w-full">
<input nz-input formControlName="nutritionNum" />
</nz-input-group>
</div>
<div>
<button nz-button (click)="removeNutrition(i)">
<span nz-icon nzType="delete"></span>
</button>
</div>
</div>
</li>
</ul>
</nz-form-control>
</nz-form-item>
</form>
<ng-template #formControlErrorTpl let-control>
<form-error-tips [control]="control"></form-error-tips>
</ng-template>

8
projects/admin/src/app/components/food-form/food-form.component.less

@ -0,0 +1,8 @@
.block-label {
::ng-deep {
label {
display: inline-flex;
width: 100%;
}
}
}

40
projects/admin/src/app/components/food-form/food-form.component.ts

@ -0,0 +1,40 @@
import { Component, OnInit } from "@angular/core";
import { FormArray, FormBuilder, FormGroup } from "@angular/forms";
import { FormValidators } from "@cdk/validators";
@Component({
selector: "app-food-form",
templateUrl: "./food-form.component.html",
styleUrls: ["./food-form.component.less"],
})
export class FoodFormComponent implements OnInit {
constructor(private fb: FormBuilder) {}
formGroup!: FormGroup;
get nutrition(): FormArray {
return this.formGroup.get("nutrition") as FormArray;
}
ngOnInit(): void {
this.formGroup = this.fb.group({
id: this.fb.control("", [FormValidators.required()]),
name: this.fb.control("", [FormValidators.required()]),
type: this.fb.control("", [FormValidators.required()]),
nutrition: this.fb.array([], [FormValidators.required()]),
});
}
createNutrition() {
this.nutrition.push(
this.fb.group({
nutritionName: this.fb.control("", [FormValidators.required()]),
nutritionNum: this.fb.control(0, [FormValidators.required()]),
})
);
}
removeNutrition(idx: number) {
this.nutrition.removeAt(idx);
}
}

7
projects/admin/src/app/components/index.ts

@ -0,0 +1,7 @@
export * from "./app-layout/app-layout.component";
export * from "./app-page/app-page.component";
export * from "./food-form/food-form.component";
export * from "./dish-form/dish-form.component";
export * from "./ingredient-form-basic/ingredient-form-basic.component";

86
projects/admin/src/app/components/ingredient-form-basic/ingredient-form-basic.component.html

@ -0,0 +1,86 @@
<form nz-form [formGroup]="formGroup" nzLayout="vertical">
<nz-form-item>
<nz-form-label nzRequired nzSpan="6">
适用单位
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl" nzSpan="12">
<nz-select [nzOptions]="[]" nzPlaceHolder="请选择单位"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired nzSpan="6">
天数
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<nz-radio-group formControlName="day">
<label nz-radio nzValue="1">1天</label>
<label nz-radio nzValue="2">2天</label>
<label nz-radio nzValue="3">3天</label>
<label nz-radio nzValue="4">4天</label>
<label nz-radio nzValue="5">5天</label>
<label nz-radio nzValue="6">6天</label>
<label nz-radio nzValue="7">7天</label>
</nz-radio-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired nzSpan="6">
标题
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl" nzSpan="12">
<nz-checkbox-wrapper class="flex flex-wrap checkbox-wrap">
<label nz-checkbox nzValue="1">早餐</label>
<label nz-checkbox nzValue="2">午餐</label>
<label nz-checkbox nzValue="3">晚餐</label>
</nz-checkbox-wrapper>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired nzSpan="6">
标准
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl" nzSpan="12">
<nz-select [nzOptions]="[]" nzPlaceHolder="请选择标准"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired nzSpan="6">
人群显示
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<nz-checkbox-group [ngModel]="ages"
class="flex flex-wrap checkbox-wrap"
formControlName="month"
(ngModelChange)="ageChange()">
</nz-checkbox-group>
</nz-form-control>
</nz-form-item>
<nz-divider></nz-divider>
<nz-form-item>
<nz-form-label nzRequired nzSpan="6">
批量修改重量
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl" nzSpan="12">
<nz-switch></nz-switch>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<nz-space>
<button *nzSpaceItem nz-button nzType="primary" (click)="onSubmit()">
确定
</button>
<button *nzSpaceItem nz-button>
取消
</button>
</nz-space>
</nz-form-control>
</nz-form-item>
</form>
<ng-template #formControlErrorTpl let-control>
<form-error-tips [control]="control"></form-error-tips>
</ng-template>

7
projects/admin/src/app/components/ingredient-form-basic/ingredient-form-basic.component.less

@ -0,0 +1,7 @@
.checkbox-wrap {
::ng-deep {
&>label {
flex-basis: 130px;
}
}
}

43
projects/admin/src/app/components/ingredient-form-basic/ingredient-form-basic.component.ts

@ -0,0 +1,43 @@
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
import { FormArray, FormBuilder, FormGroup } from "@angular/forms";
import { FormValidators } from "@cdk/validators";
@Component({
selector: "app-ingredient-form-basic",
templateUrl: "./ingredient-form-basic.component.html",
styleUrls: ["./ingredient-form-basic.component.less"],
})
export class IngredientFormBasicComponent {
constructor(private fb: FormBuilder) {}
@Output() onSave = new EventEmitter();
formGroup!: FormGroup;
ages = [
{ value: "1", label: "6-8岁(男)", checked: false },
{ value: "2", label: "6-8岁(女)", checked: false },
{ value: "3", label: "9-11岁(男)", checked: false },
{ value: "4", label: "9-11岁(女)", checked: false },
{ value: "5", label: "12-14岁(男)", checked: false },
{ value: "6", label: "12-14岁(女)", checked: false },
];
ngOnInit(): void {
this.formGroup = this.fb.group({
id: this.fb.control("", [FormValidators.required()]),
unit: this.fb.control("", [FormValidators.required()]),
day: this.fb.control("1", [FormValidators.required()]),
name: this.fb.control("", [FormValidators.required()]),
food: this.fb.array([], [FormValidators.required()]),
tag: this.fb.control([], []),
month: this.fb.control([], []),
});
}
ageChange() {}
onSubmit() {
this.onSave.emit();
}
}

17
projects/admin/src/app/icons-provider.module.ts

@ -0,0 +1,17 @@
import { NgModule } from "@angular/core";
import { NZ_ICONS, NzIconModule, NzIconService } from "ng-zorro-antd/icon";
import { MenuFoldOutline, MenuUnfoldOutline, FormOutline, DashboardOutline } from "@ant-design/icons-angular/icons";
const icons = [MenuFoldOutline, MenuUnfoldOutline, DashboardOutline, FormOutline];
@NgModule({
imports: [NzIconModule],
exports: [NzIconModule],
providers: [{ provide: NZ_ICONS, useValue: icons }],
})
export class IconsProviderModule {
constructor(private icon: NzIconService) {
// this.icon.addIconLiteral('/ass','SVG')
}
}

67
projects/admin/src/app/pages/dish/dish.component.html

@ -0,0 +1,67 @@
<app-page>
<ng-template #pageExtraTpl>
<nz-space>
<button *nzSpaceItem nz-button>批量删除</button>
<button *nzSpaceItem nz-button>批量下载营养标签</button>
<button *nzSpaceItem nz-button nzType="primary" (click)="showFoodForm()">
<i nz-icon nzType="plus"></i>
新增菜品
</button>
</nz-space>
</ng-template>
<div class="h-full overflow-hidden bg-white rounded-lg">
<nz-card [nzBordered]="false" nzTitle="菜品管理">
<table-list [props]="tableList" [search]="searchTpl" [action]="pageExtraTpl" [formGroup]="queryForm"
[renderColumns]="renderColumnsTpl">
<ng-template #actionTpl>
<button nz-button>批量删除</button>
</ng-template>
<ng-template #searchTpl>
<nz-form-item class="w-40">
<nz-form-control>
<nz-select nzPlaceHolder="单位" [nzOptions]="[]"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item class="w-40">
<nz-form-control>
<nz-select nzPlaceHolder="菜品标签" [nzOptions]="[]"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<input nz-input placeholder="请输入菜品名称" formControlName="name" />
</nz-form-control>
</nz-form-item>
</ng-template>
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row">
<ng-container [ngSwitch]="key">
<ng-container *ngSwitchCase="'img'">
<div class="dish-img overflow-auto"
[ngStyle]="{'background-image':'url(' + tempImg + ')'}">
</div>
</ng-container>
<ng-container *ngSwitchDefault>
{{data}}
</ng-container>
</ng-container>
</ng-template>
</table-list>
</nz-card>
</div>
</app-page>
<ng-template #formFooterTpl>
<nz-space>
<button *nzSpaceItem nz-button (click)="cancelFoodForm()">
取消
</button>
<button *nzSpaceItem nz-button nzType="primary">
保存
</button>
</nz-space>
</ng-template>

8
projects/admin/src/app/pages/dish/dish.component.less

@ -0,0 +1,8 @@
.dish-img {
width: 64px;
height: 64px;
border-radius: 10px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}

81
projects/admin/src/app/pages/dish/dish.component.ts

@ -0,0 +1,81 @@
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { AnyObject, TableListOption } from "@cdk/public-api";
import { DishFormComponent } from "@admin/app/components";
import { ApiService } from "@admin/app/services";
@Component({
selector: "app-dish",
templateUrl: "./dish.component.html",
styleUrls: ["./dish.component.less"],
})
export class DishComponent {
constructor(private drawer: NzDrawerService, private api: ApiService) {}
@ViewChild("formFooterTpl") formFooterTpl!: TemplateRef<{}>;
private drawerRef?: NzDrawerRef;
tempImg = "https://cdn.pixabay.com/photo/2023/08/08/18/01/butterfly-8177925_1280.jpg";
public tableList = new TableListOption(this.fetchData.bind(this), {
selectable: true,
});
public queryForm = new FormGroup({
name: new FormControl(""),
});
ngOnInit(): void {
this.initTableList();
}
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "img", title: "菜品图片", width: "66px" },
{ key: "name", title: "菜品名称" },
{ key: "name", title: "菜品标签" },
{ key: "name", title: "食材及含量", width: "30%" },
{ key: "name", title: "单位" },
]);
this.tableList = this.tableList.setOptions([
{
title: "下载营养标签",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "编辑",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "删除",
premissions: [],
onClick: this.deleteItem.bind(this),
},
]);
}
fetchData(query: AnyObject, pager: AnyObject) {
return this.api.page(pager, query);
}
showFoodForm(food?: any) {
this.drawerRef = this.drawer.create({
nzTitle: food ? "编辑菜品" : "新增菜品",
nzWidth: 700,
nzContent: DishFormComponent,
nzFooter: this.formFooterTpl,
});
}
cancelFoodForm() {
this.drawerRef?.close();
}
deleteItem() {}
}

71
projects/admin/src/app/pages/food/food.component.html

@ -0,0 +1,71 @@
<app-page>
<ng-template #pageExtraTpl>
<nz-space>
<button *nzSpaceItem nz-button>批量删除</button>
<button *nzSpaceItem nz-button>导入食材清单</button>
<button *nzSpaceItem nz-button nzType="primary" (click)="showFoodForm()">
<i nz-icon nzType="plus"></i>
新增食材
</button>
</nz-space>
</ng-template>
<div nz-row class="h-full overflow-hidden bg-white rounded-lg">
<div nz-col nzFlex="220px" class="food-type">
<nz-card class="h-full" [nzBordered]="false" nzTitle="食材类型" [nzBodyStyle]="{padding:'1px 0 0 0'}">
<ul nz-menu nzMode="inline">
<li nz-menu-item>
<a>
全部
</a>
</li>
<li nz-menu-item>
<a>
谷薯类
</a>
</li>
<li nz-menu-item>
<a>
大豆类及其制品
</a>
</li>
<li nz-menu-item>
<a>
蔬菜类
</a>
</li>
</ul>
</nz-card>
</div>
<div nz-col nzFlex="1" class="flex-1 overflow-hidden bg-white ">
<nz-card [nzBordered]="false" nzTitle="食材管理">
<table-list [props]="tableList" [search]="searchTpl" [action]="pageExtraTpl" [formGroup]="queryForm">
<ng-template #actionTpl>
<button nz-button>批量删除</button>
</ng-template>
<ng-template #searchTpl>
<nz-form-item>
<nz-form-control>
<input nz-input placeholder="请输入食材名称/编号" formControlName="name" />
</nz-form-control>
</nz-form-item>
</ng-template>
</table-list>
</nz-card>
</div>
</div>
</app-page>
<ng-template #foofFormFooterTpl>
<nz-space>
<button *nzSpaceItem nz-button (click)="cancelFoodForm()">
取消
</button>
<button *nzSpaceItem nz-button nzType="primary">
保存
</button>
</nz-space>
</ng-template>

9
projects/admin/src/app/pages/food/food.component.less

@ -0,0 +1,9 @@
.food-type {
border-right: 1px solid #e8e8e8;
::ng-deep {
.ant-menu-inline {
border-right: none;
}
}
}

72
projects/admin/src/app/pages/food/food.component.ts

@ -0,0 +1,72 @@
import { FoodFormComponent } from "@admin/app/components";
import { ApiService } from "@admin/app/services";
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { AnyObject, TableListOption } from "@cdk/public-api";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
@Component({
selector: "app-food",
templateUrl: "./food.component.html",
styleUrls: ["./food.component.less"],
})
export class FoodComponent implements OnInit {
constructor(private drawer: NzDrawerService, private api: ApiService) {}
@ViewChild("foofFormFooterTpl") foofFormFooterTpl!: TemplateRef<{}>;
private drawerRef?: NzDrawerRef;
public tableList = new TableListOption(this.fetchData.bind(this));
public queryForm = new FormGroup({
name: new FormControl(""),
});
ngOnInit(): void {
this.initTableList();
}
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "name", title: "食材编号" },
{ key: "name", title: "食材名称" },
{ key: "name", title: "食材类型" },
{ key: "name", title: "营养素(每100g可食部)", width: "50%" },
{ key: "name", title: "更新日期" },
]);
this.tableList = this.tableList.setOptions([
{
title: "编辑",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "删除",
premissions: [],
onClick: this.deleteItem.bind(this),
},
]);
}
fetchData(query: AnyObject, pager: AnyObject) {
return this.api.page(pager, query);
}
showFoodForm(food?: any) {
this.drawerRef = this.drawer.create({
nzTitle: food ? "编辑食材" : "新增食材",
nzWidth: 520,
nzContent: FoodFormComponent,
nzFooter: this.foofFormFooterTpl,
});
}
cancelFoodForm() {
this.drawerRef?.close();
}
deleteItem() {}
}

1
projects/admin/src/app/pages/home/home.component.html

@ -0,0 +1 @@
<p>home works!</p>

0
projects/admin/src/app/pages/home/home.component.less

10
projects/admin/src/app/pages/home/home.component.ts

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.less']
})
export class HomeComponent {
}

11
projects/admin/src/app/pages/index.ts

@ -0,0 +1,11 @@
export * from "./home/home.component";
export * from "./login/login.component";
export * from "./food/food.component";
export * from "./dish/dish.component";
export * from "./ingredients/ingredient-list/ingredient-list.component";
export * from "./ingredients/ingredient-review/ingredient-review.component";
export * from "./ingredients/ingredient-release/ingredient-release.component";
export * from "./ingredients/ingredient-form/ingredient-form.component";

279
projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.html

@ -0,0 +1,279 @@
<app-page [full]="true">
<div class="p-4" *ngIf="step === 0">
<nz-card nzTitle="录入食谱基础信息">
<app-ingredient-form-basic (onSave)="onStepChange()"></app-ingredient-form-basic>
</nz-card>
</div>
<ng-container *ngIf="step === 1">
<nz-card nzSize="small">
<div class="flex justify-between">
<div class="flex-1 ">
<button nz-button (click)="step = 0">
配置
</button>
</div>
<nz-space>
<button *nzSpaceItem nz-button nzType="primary">
食谱营养分析
</button>
<button *nzSpaceItem nz-button nzType="primary">
新建食谱
</button>
<button *nzSpaceItem nz-button>
导入食谱
</button>
<button *nzSpaceItem nz-button>
食谱预览
</button>
<button *nzSpaceItem nz-button nzType="primary">
保存
</button>
<button *nzSpaceItem nz-button>
另存为
</button>
</nz-space>
</div>
</nz-card>
<div class="p-4">
<nz-card nzTitle="第一天" class="mb-4">
<nz-card-tab>
<nz-tabset nzSize="large">
<nz-tab nzTitle="早餐"></nz-tab>
<nz-tab nzTitle="午餐"></nz-tab>
<nz-tab nzTitle="晚餐"></nz-tab>
</nz-tabset>
</nz-card-tab>
<div>
<nz-table nzTemplateMode [nzBordered]="true" nzSize="small">
<thead>
<tr>
<th>
菜品
</th>
<th>
食材
</th>
<th>
轻体力(体重过低)
</th>
<th>
轻体力(正常体重)
</th>
<th>
休息(超重/肥胖)
</th>
<th>
轻体力(体重过低)
</th>
<th>
轻体力(正常体重)
</th>
</tr>
</thead>
<tbody>
<tr>
<td [rowSpan]="2">
<div class="flex justify-between">
<span>
番茄煎蛋面
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
<div class="flex justify-between">
<span>
番茄
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
</tr>
<tr>
<td>
<div class="flex justify-between">
<span>
面条
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
</tr>
<tr>
<td [rowSpan]="4">
<div class="flex justify-between">
<span>
鸡蛋青菜面
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
<div class="flex justify-between">
<span>
食用油
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
</tr>
<tr>
<td>
<div class="flex justify-between">
<span>
小白菜[青菜]
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
</tr>
<tr>
<td>
<div class="flex justify-between">
<span>
鸡蛋(均值)
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
</tr>
<tr>
<td>
<div class="flex justify-between">
<span>
面条(均值)
</span>
<button nz-button nzType="text">
<i nz-icon nzType="more"></i>
</button>
</div>
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
<td>
1
</td>
</tr>
</tbody>
</nz-table>
</div>
</nz-card>
<nz-card nzTitle="第二天" class="mb-4"></nz-card>
<nz-card nzTitle="第三天" class="mb-4"></nz-card>
<nz-card nzTitle="第四天" class="mb-4"></nz-card>
<nz-card nzTitle="第五天" class="mb-4"></nz-card>
<nz-card nzTitle="第六天" class="mb-4"></nz-card>
<nz-card nzTitle="第七天" class="mb-4"></nz-card>
</div>
</ng-container>
</app-page>

0
projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.less

25
projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.ts

@ -0,0 +1,25 @@
import { IngredientFormBasicComponent } from "@admin/app/components";
import { Component, OnInit } from "@angular/core";
import { NzModalService } from "ng-zorro-antd/modal";
@Component({
selector: "app-ingredient-form",
templateUrl: "./ingredient-form.component.html",
styleUrls: ["./ingredient-form.component.less"],
})
export class IngredientFormComponent implements OnInit {
constructor(private modal: NzModalService) {}
step = 1;
ngOnInit(): void {}
onStepChange() {
this.step = 1;
console.log(456);
}
showForm(food?: any) {}
cancelForm() {}
}

65
projects/admin/src/app/pages/ingredients/ingredient-list/ingredient-list.component.html

@ -0,0 +1,65 @@
<app-page>
<ng-template #pageExtraTpl>
<nz-space>
<button *nzSpaceItem nz-button nzType="primary" [routerLink]="['/','ingredient','item','form','create']">
<i nz-icon nzType="plus"></i>
创建食谱
</button>
</nz-space>
</ng-template>
<div class="h-full overflow-hidden bg-white rounded-lg">
<nz-card [nzBordered]="false" nzTitle="食谱库">
<table-list [props]="tableList" [search]="searchTpl" [action]="pageExtraTpl" [formGroup]="queryForm"
[renderColumns]="renderColumnsTpl">
<ng-template #actionTpl>
<button nz-button>批量删除</button>
</ng-template>
<ng-template #searchTpl>
<nz-form-item class="w-40">
<nz-form-control>
<nz-select nzPlaceHolder="单位" [nzOptions]="[]"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item class="w-40">
<nz-form-control>
<nz-select nzPlaceHolder="状态" [nzOptions]="[]"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<input nz-input placeholder="请输入食谱名称" formControlName="name" />
</nz-form-control>
</nz-form-item>
</ng-template>
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row">
<ng-container [ngSwitch]="key">
<!-- <ng-container *ngSwitchCase="'img'">
<div class="dish-img overflow-auto"
[ngStyle]="{'background-image':'url(' + tempImg + ')'}">
</div>
</ng-container> -->
<ng-container *ngSwitchDefault>
{{data}}
</ng-container>
</ng-container>
</ng-template>
</table-list>
</nz-card>
</div>
</app-page>
<ng-template #foofFormFooterTpl>
<nz-space>
<button *nzSpaceItem nz-button (click)="cancelFoodForm()">
取消
</button>
<button *nzSpaceItem nz-button nzType="primary">
保存
</button>
</nz-space>
</ng-template>

0
projects/admin/src/app/pages/ingredients/ingredient-list/ingredient-list.component.less

98
projects/admin/src/app/pages/ingredients/ingredient-list/ingredient-list.component.ts

@ -0,0 +1,98 @@
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { AnyObject, TableListOption } from "@cdk/public-api";
import { DishFormComponent } from "@admin/app/components";
import { ApiService } from "@admin/app/services";
@Component({
selector: "app-ingredient-list",
templateUrl: "./ingredient-list.component.html",
styleUrls: ["./ingredient-list.component.less"],
})
export class IngredientListComponent {
constructor(private drawer: NzDrawerService, private api: ApiService) {}
@ViewChild("foofFormFooterTpl") foofFormFooterTpl!: TemplateRef<{}>;
private drawerRef?: NzDrawerRef;
tempImg = "https://cdn.pixabay.com/photo/2023/08/08/18/01/butterfly-8177925_1280.jpg";
public tableList = new TableListOption(this.fetchData.bind(this));
public queryForm = new FormGroup({
name: new FormControl(""),
});
ngOnInit(): void {
this.initTableList();
}
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "name", title: "食谱名称" },
{ key: "name", title: "单位" },
{ key: "name", title: "包含餐次" },
{ key: "name", title: "单位" },
{ key: "name", title: "适用月份" },
{ key: "name", title: "周期" },
{ key: "name", title: "状态" },
{ key: "name", title: "更新时间" },
{ key: "name", title: "创建人" },
]);
this.tableList = this.tableList.setOptions([
{
title: "详情",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "导出",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "审核",
premissions: [],
onClick: this.deleteItem.bind(this),
},
{
title: "发布",
premissions: [],
onClick: this.deleteItem.bind(this),
},
{
title: "编辑",
premissions: [],
onClick: this.deleteItem.bind(this),
},
{
title: "删除",
premissions: [],
onClick: this.deleteItem.bind(this),
},
]);
}
fetchData(query: AnyObject, pager: AnyObject) {
return this.api.page(pager, query);
}
showFoodForm(food?: any) {
this.drawerRef = this.drawer.create({
nzTitle: food ? "编辑菜品" : "新增菜品",
nzWidth: 700,
nzContent: DishFormComponent,
nzFooter: this.foofFormFooterTpl,
});
}
cancelFoodForm() {
this.drawerRef?.close();
}
deleteItem() {}
}

70
projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.html

@ -0,0 +1,70 @@
<app-page>
<div class="h-full overflow-hidden bg-white rounded-lg">
<nz-card [nzBordered]="false" nzTitle="食谱发布计划">
<table-list [props]="tableList" [search]="searchTpl" [formGroup]="queryForm"
[renderColumns]="renderColumnsTpl">
<ng-template #searchTpl>
<nz-form-item>
<nz-form-control>
<input nz-input placeholder="请输入食谱名称" formControlName="name" />
</nz-form-control>
</nz-form-item>
<nz-form-item class="w-40">
<nz-form-control>
<nz-select nzPlaceHolder="单位" [nzOptions]="[]"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<nz-space>
<nz-radio-group *nzSpaceItem nzButtonStyle="solid">
<label nz-radio-button nzValue="A">全部</label>
<label nz-radio-button nzValue="B">本周</label>
<label nz-radio-button nzValue="C">上周</label>
</nz-radio-group>
<ng-container *nzSpaceItem>
<nz-range-picker
[nzShowTime]="{ nzFormat: 'HH:mm' }"
nzFormat="yyyy-MM-dd HH:mm"
ngModel>
</nz-range-picker>
</ng-container>
</nz-space>
</nz-form-control>
</nz-form-item>
</ng-template>
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row">
<ng-container [ngSwitch]="key">
<!-- <ng-container *ngSwitchCase="'img'">
<div class="dish-img overflow-auto"
[ngStyle]="{'background-image':'url(' + tempImg + ')'}">
</div>
</ng-container> -->
<ng-container *ngSwitchDefault>
{{data}}
</ng-container>
</ng-container>
</ng-template>
</table-list>
</nz-card>
</div>
</app-page>
<ng-template #foofFormFooterTpl>
<nz-space>
<button *nzSpaceItem nz-button (click)="cancelFoodForm()">
取消
</button>
<button *nzSpaceItem nz-button nzType="primary">
保存
</button>
</nz-space>
</ng-template>

0
projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.less

81
projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.ts

@ -0,0 +1,81 @@
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { AnyObject, TableListOption } from "@cdk/public-api";
import { DishFormComponent } from "@admin/app/components";
import { ApiService } from "@admin/app/services";
@Component({
selector: "app-ingredient-release",
templateUrl: "./ingredient-release.component.html",
styleUrls: ["./ingredient-release.component.less"],
})
export class IngredientReleaseComponent {
constructor(private drawer: NzDrawerService, private api: ApiService) {}
@ViewChild("foofFormFooterTpl") foofFormFooterTpl!: TemplateRef<{}>;
private drawerRef?: NzDrawerRef;
tempImg = "https://cdn.pixabay.com/photo/2023/08/08/18/01/butterfly-8177925_1280.jpg";
public tableList = new TableListOption(this.fetchData.bind(this));
public queryForm = new FormGroup({
name: new FormControl(""),
});
ngOnInit(): void {
this.initTableList();
}
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "name", title: "食谱名称" },
{ key: "name", title: "单位" },
{ key: "name", title: "包含餐次" },
{ key: "name", title: "周期" },
{ key: "name", title: "创建时间" },
{ key: "name", title: "应用时间" },
]);
this.tableList = this.tableList.setOptions([
{
title: "详情",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "导出食谱",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "取消发布",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
]);
}
fetchData(query: AnyObject, pager: AnyObject) {
return this.api.page(pager, query);
}
showFoodForm(food?: any) {
this.drawerRef = this.drawer.create({
nzTitle: food ? "编辑菜品" : "新增菜品",
nzWidth: 700,
nzContent: DishFormComponent,
nzFooter: this.foofFormFooterTpl,
});
}
cancelFoodForm() {
this.drawerRef?.close();
}
deleteItem() {}
}

61
projects/admin/src/app/pages/ingredients/ingredient-review/ingredient-review.component.html

@ -0,0 +1,61 @@
<app-page>
<ng-template #pageExtraTpl>
<nz-space>
<button *nzSpaceItem nz-button nzType="primary" (click)="showFoodForm()">
<i nz-icon nzType="plus"></i>
批量通过
</button>
</nz-space>
</ng-template>
<div class="h-full overflow-hidden bg-white rounded-lg">
<nz-card [nzBordered]="false" nzTitle="食谱审核">
<table-list [props]="tableList" [search]="searchTpl" [action]="pageExtraTpl" [formGroup]="queryForm"
[renderColumns]="renderColumnsTpl">
<ng-template #actionTpl>
<button nz-button>批量删除</button>
</ng-template>
<ng-template #searchTpl>
<nz-form-item class="w-40">
<nz-form-control>
<nz-select nzPlaceHolder="单位" [nzOptions]="[]"></nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<input nz-input placeholder="请输入食谱名称" formControlName="name" />
</nz-form-control>
</nz-form-item>
</ng-template>
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row">
<ng-container [ngSwitch]="key">
<!-- <ng-container *ngSwitchCase="'img'">
<div class="dish-img overflow-auto"
[ngStyle]="{'background-image':'url(' + tempImg + ')'}">
</div>
</ng-container> -->
<ng-container *ngSwitchDefault>
{{data}}
</ng-container>
</ng-container>
</ng-template>
</table-list>
</nz-card>
</div>
</app-page>
<ng-template #foofFormFooterTpl>
<nz-space>
<button *nzSpaceItem nz-button (click)="cancelFoodForm()">
取消
</button>
<button *nzSpaceItem nz-button nzType="primary">
保存
</button>
</nz-space>
</ng-template>

0
projects/admin/src/app/pages/ingredients/ingredient-review/ingredient-review.component.less

81
projects/admin/src/app/pages/ingredients/ingredient-review/ingredient-review.component.ts

@ -0,0 +1,81 @@
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { AnyObject, TableListOption } from "@cdk/public-api";
import { DishFormComponent } from "@admin/app/components";
import { ApiService } from "@admin/app/services";
@Component({
selector: "app-ingredient-review",
templateUrl: "./ingredient-review.component.html",
styleUrls: ["./ingredient-review.component.less"],
})
export class IngredientReviewComponent {
constructor(private drawer: NzDrawerService, private api: ApiService) {}
@ViewChild("foofFormFooterTpl") foofFormFooterTpl!: TemplateRef<{}>;
private drawerRef?: NzDrawerRef;
tempImg = "https://cdn.pixabay.com/photo/2023/08/08/18/01/butterfly-8177925_1280.jpg";
public tableList = new TableListOption(this.fetchData.bind(this), {
selectable: true,
});
public queryForm = new FormGroup({
name: new FormControl(""),
});
ngOnInit(): void {
this.initTableList();
}
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "name", title: "食谱名称" },
{ key: "name", title: "单位" },
{ key: "name", title: "提交审核时间" },
{ key: "name", title: "提交人" },
]);
this.tableList = this.tableList.setOptions([
{
title: "详情",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "导出食谱",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
{
title: "取消发布",
premissions: [],
onClick: this.showFoodForm.bind(this),
},
]);
}
fetchData(query: AnyObject, pager: AnyObject) {
return this.api.page(pager, query);
}
showFoodForm(food?: any) {
this.drawerRef = this.drawer.create({
nzTitle: food ? "编辑菜品" : "新增菜品",
nzWidth: 700,
nzContent: DishFormComponent,
nzFooter: this.foofFormFooterTpl,
});
}
cancelFoodForm() {
this.drawerRef?.close();
}
deleteItem() {}
}

63
projects/admin/src/app/pages/login/login.component.html

@ -0,0 +1,63 @@
<section class="h-full flex items-center justify-center">
<div class="login">
<!-- <div>
<h1 class="text-center my-[25px]">
<img class="logo" [src]="'/assets/images/jl-logo.png'" />
</h1>
</div> -->
<div class="card shadow-2xl">
<div class="img"></div>
<div class="form py-5 px-10 flex-1">
<div class="text-left ">
<h2 class="mt-4 text-3xl font-bold">智慧配餐管理后台</h2>
<h3 class="mt-10 text-xl">登录</h3>
<form nz-form [formGroup]="loginForm" class="mt-10">
<nz-form-item>
<nz-form-control [nzErrorTip]="formErrorTipsTpl">
<nz-input-group [nzPrefix]="prefixTemplateUser" nzSize="large">
<input nz-input nzSize="large" placeholder="账户" formControlName="uid" />
</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>
</div>
</section>
<ng-template #formErrorTipsTpl let-control>
<div class="text-left">
<!-- <dec-form-error-tips [control]="control"></dec-form-error-tips> -->
</div>
</ng-template>

65
projects/admin/src/app/pages/login/login.component.less

@ -0,0 +1,65 @@
//
:host {
display: block;
width: 100vw;
height: 100vh;
position: relative;
background-color: #edf0f5;
background-repeat: no-repeat;
background-size: cover;
}
.login {
position: relative;
z-index: 1;
h1 {
margin-bottom: 24px;
font-size: 24px;
font-weight: 400;
color: #fff;
}
.logo {
display: inline-block;
height: 36px;
}
.card {
width: 960px;
height: 60vh;
min-height: 410px;
display: flex;
align-items: center;
background-color: #fff;
.img {
display: block;
width: 560px;
height: 100%;
background-image: url('/assets/images/login.jpg');
background-size: cover;
background-position: center;
}
}
.form {
margin: 0 auto;
background-color: #fff;
border-radius: 6px;
}
p {
font-size: 16px;
line-height: 24px;
letter-spacing: 0.23em;
text-shadow: 0px 8px 20px rgba(0, 0, 0, 0.1);
}
.btn {
margin-top: 16px;
}
}

42
projects/admin/src/app/pages/login/login.component.ts

@ -0,0 +1,42 @@
import { Component } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { Router } from "@angular/router";
import { NzMessageService } from "ng-zorro-antd/message";
import { FormValidators } from "projects/cdk/src/public-api";
import { Utils } from "projects/cdk/src/utils";
import { finalize, lastValueFrom } from "rxjs";
import { ApiService } from "../../services";
@Component({
selector: "app-login",
templateUrl: "./login.component.html",
styleUrls: ["./login.component.less"],
})
export class LoginComponent {
constructor(private msg: NzMessageService, private api: ApiService, private router: Router) {}
public loginForm = new FormGroup({
uid: new FormControl("", [FormValidators.required("请输入账户")]),
password: new FormControl("", [FormValidators.required("请输入密码")]),
});
public loading: boolean = false;
ngOnInit(): void {}
async onLogin() {
if (Utils.validateFormGroup(this.loginForm)) {
const { value } = this.loginForm;
this.loading = true;
const res = await lastValueFrom(
this.api.login(value).pipe(
finalize(() => {
this.loading = false;
})
)
);
this.msg.success(res.desc);
this.router.navigate(["/"]);
}
}
}

25
projects/admin/src/app/services/api.service.ts

@ -0,0 +1,25 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { map } from "rxjs";
@Injectable({
providedIn: "root",
})
export class ApiService {
constructor(private http: HttpClient) {}
login(v: {}) {
return this.http.post<any>("/", v);
}
page(v: {}, q: {}) {
return this.http.get<any>("https://jsonplaceholder.typicode.com/users", v).pipe(
map((r) => {
return {
total: 10,
content: r,
};
})
);
}
}

84
projects/admin/src/app/services/http.interceptor.ts

@ -0,0 +1,84 @@
import { Inject, Injectable } from "@angular/core";
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
HttpResponse,
} from "@angular/common/http";
import { catchError, Observable, switchMap, tap, throwError, timer } from "rxjs";
import { Router } from "@angular/router";
import { NzMessageService } from "ng-zorro-antd/message";
import { ResponseType } from "@cdk/types";
@Injectable({ providedIn: "root" })
export class HTTPInterceptor implements HttpInterceptor {
constructor(private router: Router, private msg: NzMessageService) {}
private msgFlag = false;
private localStroageKey = "catering";
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem(this.localStroageKey);
if (token) {
req = req.clone({
// headers: req.headers.set('Authorization', `Bearer ${token}`),
headers: req.headers.set("Authorization", token),
});
}
return this.handleResult(next, req);
}
private handleResult(next: HttpHandler, authReq: HttpRequest<any>): Observable<HttpEvent<any>> {
return next.handle(authReq).pipe(
tap((res) => {
if (res instanceof HttpResponse) {
const Authorization = res.headers.get("Authorization");
if (Authorization) {
localStorage.setItem(this.localStroageKey, Authorization);
}
// if (this.decConfig.triggerError) {
// this.decConfig.triggerError(res);
// }
if (res.body?.success === false && res.body.desc) {
throw new HttpErrorResponse({ error: res.body });
}
}
}),
catchError((err: HttpErrorResponse) => {
const throwErr = throwError(() => err);
if (this.msgFlag) {
return throwErr;
}
setTimeout(() => {
this.msgFlag = false;
}, 1500);
const error: ResponseType = err.error;
this.msgFlag = true;
if (error.success === false) {
this.msg.error(error.desc);
switch (error.code) {
case 401:
this.router.navigate(["/", "login"]);
break;
default:
break;
}
} else {
this.msg.error("服务器出错了!");
}
return throwErr;
})
);
}
}

1
projects/admin/src/app/services/index.ts

@ -0,0 +1 @@
export * from "./api.service";

0
projects/admin/src/app/shared/components/index.ts

102
projects/admin/src/app/shared/ng-zorro.ts

@ -0,0 +1,102 @@
import { NzGridModule } from "ng-zorro-antd/grid";
import { NzCardModule } from "ng-zorro-antd/card";
import { NzStatisticModule } from "ng-zorro-antd/statistic";
import { NzIconModule } from "ng-zorro-antd/icon";
import { NzToolTipModule } from "ng-zorro-antd/tooltip";
import { NzTableModule } from "ng-zorro-antd/table";
import { NzSegmentedModule } from "ng-zorro-antd/segmented";
import { NzSpaceModule } from "ng-zorro-antd/space";
import { NzDatePickerModule } from "ng-zorro-antd/date-picker";
import { NzListModule } from "ng-zorro-antd/list";
import { NzInputModule } from "ng-zorro-antd/input";
import { NzButtonModule } from "ng-zorro-antd/button";
import { NzTagModule } from "ng-zorro-antd/tag";
import { NzBadgeModule } from "ng-zorro-antd/badge";
import { NzPaginationModule } from "ng-zorro-antd/pagination";
import { NzDividerModule } from "ng-zorro-antd/divider";
import { NzSelectModule } from "ng-zorro-antd/select";
import { NzModalModule } from "ng-zorro-antd/modal";
import { NzMessageModule } from "ng-zorro-antd/message";
import { NzDrawerModule } from "ng-zorro-antd/drawer";
import { NzFormModule } from "ng-zorro-antd/form";
import { NzDescriptionsModule } from "ng-zorro-antd/descriptions";
import { NzTabsModule } from "ng-zorro-antd/tabs";
import { NzProgressModule } from "ng-zorro-antd/progress";
import { NzAvatarModule } from "ng-zorro-antd/avatar";
import { NzMenuModule } from "ng-zorro-antd/menu";
import { NzDropDownModule } from "ng-zorro-antd/dropdown";
import { NzTreeSelectModule } from "ng-zorro-antd/tree-select";
import { NzRadioModule } from "ng-zorro-antd/radio";
import { NzCheckboxModule } from "ng-zorro-antd/checkbox";
import { NzCalendarModule } from "ng-zorro-antd/calendar";
import { NzSkeletonModule } from "ng-zorro-antd/skeleton";
import { NzTimelineModule } from "ng-zorro-antd/timeline";
import { NzEmptyModule } from "ng-zorro-antd/empty";
import { NzSpinModule } from "ng-zorro-antd/spin";
import { NzResultModule } from "ng-zorro-antd/result";
import { NzCascaderModule } from "ng-zorro-antd/cascader";
import { NzAutocompleteModule } from "ng-zorro-antd/auto-complete";
import { NzPopoverModule } from "ng-zorro-antd/popover";
import { NzPageHeaderModule } from "ng-zorro-antd/page-header";
import { NzTreeModule } from "ng-zorro-antd/tree";
import { NzSwitchModule } from "ng-zorro-antd/switch";
import { NzCarouselModule } from "ng-zorro-antd/carousel";
import { NzTimePickerModule } from "ng-zorro-antd/time-picker";
import { NzImageModule } from "ng-zorro-antd/image";
import { NzInputNumberModule } from "ng-zorro-antd/input-number";
import { NzLayoutModule } from "ng-zorro-antd/layout";
import { NzBreadCrumbModule } from "ng-zorro-antd/breadcrumb";
import { NzOutletModule } from "ng-zorro-antd/core/outlet";
export const ngZorroModules = [
NzOutletModule,
NzBreadCrumbModule,
NzLayoutModule,
NzInputNumberModule,
NzImageModule,
NzTimePickerModule,
NzCarouselModule,
NzSwitchModule,
NzTreeModule,
NzPageHeaderModule,
NzPopoverModule,
NzAutocompleteModule,
NzCascaderModule,
NzResultModule,
NzSpinModule,
NzGridModule,
NzCardModule,
NzStatisticModule,
NzIconModule,
NzToolTipModule,
NzTableModule,
NzSegmentedModule,
NzSpaceModule,
NzDatePickerModule,
NzListModule,
NzInputModule,
NzButtonModule,
NzTagModule,
NzBadgeModule,
NzPaginationModule,
NzDividerModule,
NzSelectModule,
NzSelectModule,
NzModalModule,
NzMessageModule,
NzDrawerModule,
NzFormModule,
NzDescriptionsModule,
NzTabsModule,
NzProgressModule,
NzAvatarModule,
NzMenuModule,
NzDropDownModule,
NzTreeSelectModule,
NzRadioModule,
NzCalendarModule,
NzCheckboxModule,
NzSkeletonModule,
NzTimelineModule,
NzEmptyModule,
];

41
projects/admin/src/app/shared/shared.module.ts

@ -0,0 +1,41 @@
import { RouterModule } from "@angular/router";
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http";
import { ngZorroModules } from "./ng-zorro";
// import { AuthorizationLayoutComponent, BgBorderComponent } from "./components";
// import { DecCornerDirective } from "./directives";
import {
// DecModule,
FormErrorTipsComponent,
StorageModule,
TableListModule,
// InputSpaceErrorDirective,
// PublicPathPipe,
// TableListModule,
// StorageModule,
// QuickDateRangeComponent,
} from "@cdk/public-api";
// import { environment } from "@manage/environments/environment";
import { NgxPermissionsModule } from "ngx-permissions";
const ngModules = [CommonModule, HttpClientModule, FormsModule, RouterModule, ReactiveFormsModule];
const components: any = [];
const directives: any[] = [];
const cdks = [
// DecModule
FormErrorTipsComponent,
// InputSpaceErrorDirective,
// PublicPathPipe,
TableListModule,
StorageModule,
// QuickDateRangeComponent,
] as any;
@NgModule({
declarations: [...components, ...directives],
imports: [...ngZorroModules, ...ngModules, ...cdks],
exports: [...ngZorroModules, ...ngModules, ...components, ...directives, ...cdks],
})
export class SharedModule {}

0
projects/admin/src/assets/.gitkeep

BIN
projects/admin/src/assets/images/jl-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
projects/admin/src/assets/images/login.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

1
projects/admin/src/assets/k-icon/carrot.svg

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1691900436240" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="882" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1013.78176 206.816c-56.256-21.568-124.96-15.264-180.48 14.432l-3.328-3.296c21.824-18.976 50.816-29.792 83.36-29.792a16 16 0 1 0 0-32c-19.392 0-37.696 3.552-54.816 9.376a169.6 169.6 0 0 0 9.376-54.816 16 16 0 1 0-32 0c0 32.48-10.784 61.44-29.792 83.36l-3.424-3.424c29.824-56.64 35.936-124.576 14.56-180.384a16 16 0 0 0-29.888 11.456c17.344 45.28 12.992 100.224-9.856 147.424-67.296-52.16-146.24-54.752-225.088-4.416-17.248 11.008-34.016 23.584-49.408 36.992-44.48 37.344-83.328 82.048-118.56 124.64A4205.664 4205.664 0 0 0 73.33376 750.4C19.89376 833.504-24.07424 917.76 14.77376 971.424a31.136 31.136 0 0 0 3.296 3.872l30.72 30.72a31.136 31.136 0 0 0 3.84 3.296c13.44 9.728 29.92 14.688 48.896 14.688 51.232 0 115.776-37.024 172.096-73.184 146.24-93.696 288.96-198.4 423.968-311.104 59.52-49.28 120.32-103.744 161.76-168.064 50.336-78.88 47.84-158.208-4.416-225.088 47.2-22.848 102.08-27.2 147.392-9.824a15.968 15.968 0 1 0 11.456-29.888z m-208.32 230.272c-38.464 59.712-96.928 110.432-148.8 153.408-133.088 111.04-273.568 214.144-417.6 306.432-26.848 17.216-98.144 63.04-137.536 63.04a24.896 24.896 0 0 1-9.984-1.728l-25.824-25.824c-5.024-9.76-6.112-42.24 61.44-147.456 11.424-17.824 23.488-35.392 35.232-53.12l19.296 19.328a15.936 15.936 0 0 0 22.624 0 16 16 0 0 0 0-22.624l-23.872-23.904a4104.96 4104.96 0 0 1 50.56-72.992l34.848 35.008a15.968 15.968 0 1 0 22.688-22.592l-38.72-38.88c17.184-23.904 34.624-47.584 52.352-71.168l48.16 48.16a15.936 15.936 0 0 0 22.624 0 16 16 0 0 0 0-22.624l-51.264-51.264c17.824-23.36 35.84-46.528 54.176-69.536l58.944 58.976a15.936 15.936 0 0 0 22.624 0 16 16 0 0 0 0-22.624l-61.344-61.376c12.544-15.456 24.832-31.168 37.568-46.464 5.856-7.072 11.936-14.24 18.016-21.376l67.648 67.648a15.936 15.936 0 0 0 22.624 0 16 16 0 0 0 0-22.624l-69.184-69.152a742.912 742.912 0 0 1 61.568-62.08l69.12 69.376a15.904 15.904 0 0 0 22.624 0.032 16 16 0 0 0 0.032-22.624L558.77376 238.912c9.184-7.232 18.496-14.112 28.096-20.256 60.928-38.88 114.176-35.872 166.08 12.96 1.6 1.376 2.944 2.752 4.32 4.16l31.04 31.008c1.376 1.376 2.752 2.72 5.12 5.408 47.04 49.92 50.944 103.904 12.032 164.896z" fill="#2c2c2c" p-id="883"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
projects/admin/src/assets/k-icon/food.svg

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1691900448064" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1032" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M225.745 428.82333334c4.655 4.655 9.31 9.31 16.291 9.30999999 4.655 0 6.982 0 11.637-2.32699999 9.309-4.655 11.636-18.618 6.982-27.92800001 0 0-60.51-95.418-9.31-160.581 27.928-37.237 32.582-81.455 13.964-130.32799999C251.345 82.06033333 230.4 56.46033334 230.4 54.13333333c-6.982-9.31-18.618-9.31-27.927-2.32699999-6.982 6.982-9.31 18.618-2.328 27.927 0 0 67.491 83.782 18.619 144.28999999-67.491 83.783 4.654 200.146 6.981 204.80000001z m558.546-6.98000001c4.654 4.654 9.309 9.308 16.29 9.30800001 4.655 0 6.983 0 11.637-2.32700001 9.31-4.655 11.637-18.618 6.982-27.927 0 0-60.51-95.419-9.31-160.58199999 27.928-34.91 32.583-81.455 13.965-130.327-13.964-34.91-34.91-60.51-34.91-62.837-6.981-9.309-18.618-9.309-27.927-2.32700001-9.309 6.982-9.309 18.618-2.327 27.927 0 0 67.49 83.782 18.618 144.291-69.818 86.11 2.327 200.146 6.982 204.80000001z m130.327 0c4.655 4.654 9.31 9.308 16.291 9.30800001 4.655 0 6.982 0 11.636-2.32700001 9.31-4.655 11.637-18.618 6.982-27.927 0 0-60.509-95.419-9.309-160.58199999 27.927-34.91 32.582-81.455 13.964-130.327-13.964-34.91-34.91-60.51-34.91-62.837-6.981-9.309-18.617-9.309-27.927-2.32700001-9.309 6.982-9.309 18.618-2.327 27.927 0 0 67.491 83.782 18.618 144.291-69.818 86.11 2.328 200.146 6.982 204.80000001z m-819.2 6.98000001c4.655 4.655 9.31 9.31 16.291 9.30999999 4.655 0 6.982 0 11.636-2.32699999 9.31-4.655 11.637-18.618 4.655-27.92800001 0 0-60.51-95.418-9.31-160.581 30.255-37.237 34.91-81.455 16.292-132.65499999-13.964-34.909-34.91-60.509-34.91-60.50900001-6.981-9.31-18.617-9.31-27.927-4.65499999-9.309 6.982-9.309 20.946-2.327 27.928 0 0 67.491 83.782 18.618 144.29-67.49 86.11 4.655 200.146 6.982 207.12799999z m870.4 409.59999999c-13.963-18.617-34.909-27.926-55.854-30.25399999-2.328-195.49-144.291-356.072-328.146-388.65400001 13.964-16.291 23.273-37.237 23.273-60.51 0-51.2-41.891-90.763-90.764-90.76299999-51.2 0-90.763 41.891-90.763 90.764 0 23.272 9.309 44.218 23.272 60.50899999-186.181 30.254-330.472 193.163-335.127 388.65400001-20.945 2.328-37.236 11.637-51.2 27.928-13.964 18.618-23.273 41.89-23.273 67.49 0 25.6 6.982 48.873 23.273 67.491C76.8 989.69733334 97.745 1001.33333334 121.02 1001.33333334h784.29c23.273 0 44.218-11.636 60.51-30.255 13.963-18.618 23.272-41.89 23.272-67.49 0-23.273-9.31-46.546-23.273-65.164z m-451.49-525.963c25.6 0 44.217 20.946 44.217 44.21800001 0 25.6-20.945 44.219-44.218 44.21899999-25.6 0-44.218-20.946-44.218-44.21899999 0-23.272 18.618-44.218 44.218-44.21800001z m414.254 628.36400001c-4.655 4.654-11.637 11.636-23.273 11.636L118.691 950.13333334c-11.636 0-20.946-6.982-23.273-13.96400001-6.982-9.309-11.636-23.272-11.636-37.236 0-13.964 4.654-27.927 11.636-37.236 4.655-4.655 11.637-11.637 23.273-11.637l514.327 2.328c13.964 0 23.273-9.31 23.273-23.27299999s-9.31-23.273-23.273-23.273l-474.763-2.32700001c2.327-190.837 160.581-346.764 351.418-346.764H512c193.164 0 349.09 155.927 349.09 349.09100001h-74.472c-13.963 0-23.273 9.31-23.273 23.273s9.31 23.273 23.273 23.27299999h116.364c11.636 0 20.945 6.981 23.273 13.96300001 6.981 9.31 11.636 23.273 11.636 37.237 2.327 13.963-2.327 27.927-9.31 37.236z" p-id="1033"></path></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
projects/admin/src/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

20
projects/admin/src/index.html

File diff suppressed because one or more lines are too long

7
projects/admin/src/main.ts

@ -0,0 +1,7 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

39
projects/admin/src/styles.less

@ -0,0 +1,39 @@
// Custom Theming for NG-ZORRO
// For more information: https://ng.ant.design/docs/customize-theme/en
@import "../../../node_modules/ng-zorro-antd/ng-zorro-antd.less";
// Override less variables to here
// View all variables: https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/components/style/themes/default.less
// @primary-color: #1890ff;
/* You can add global styles to this file, and also import other style files */
@tailwind utilities;
html,
body {
height: 100vh;
}
ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
.upload-btn {
position: relative;
input {
display: block;
height: 100%;
background-color: red;
position: absolute;
inset: 0;
opacity: 0;
font-size: 0;
cursor: pointer;
}
}

14
projects/admin/tsconfig.app.json

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

14
projects/admin/tsconfig.spec.json

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

24
projects/cdk/README.md

@ -0,0 +1,24 @@
# Cdk
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.0.
## Code scaffolding
Run `ng generate component component-name --project cdk` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project cdk`.
> Note: Don't forget to add `--project cdk` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build cdk` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build cdk`, go to the dist folder `cd dist/cdk` and run `npm publish`.
## Running unit tests
Run `ng test cdk` to execute the unit tests via [Karma](https://karma-runner.github.io).
## 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.

7
projects/cdk/ng-package.json

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/cdk",
"lib": {
"entryFile": "src/public-api.ts"
}
}

12
projects/cdk/package.json

@ -0,0 +1,12 @@
{
"name": "cdk",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^16.1.0",
"@angular/core": "^16.1.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

36
projects/cdk/src/form-error-tips/form-error-tips.component.html

@ -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
projects/cdk/src/form-error-tips/form-error-tips.component.less

28
projects/cdk/src/form-error-tips/form-error-tips.component.ts

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

24
projects/cdk/src/input-space-error/input-space-error.directive.ts

@ -0,0 +1,24 @@
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn } from "@angular/forms";
import { Directive } from "@angular/core";
const VALIDATE_WHITE_SPACE_REGEX = /^[\s]|[\s]$/;
export function stringWhiteSpaceForbiddenValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
const isInvalid = VALIDATE_WHITE_SPACE_REGEX.test(control.value);
return isInvalid ? { inputTrim: { value: control.value } } : null;
};
}
@Directive({
standalone: true,
selector: "[nz-input]",
providers: [{ provide: NG_VALIDATORS, useExisting: InputSpaceErrorDirective, multi: true }],
})
export class InputSpaceErrorDirective implements Validator {
constructor() {}
validate(control: AbstractControl): ValidationErrors | null {
return stringWhiteSpaceForbiddenValidator()(control);
}
}

14
projects/cdk/src/public-api.ts

@ -0,0 +1,14 @@
/*
* Public API Surface of cdk
*/
export * from "./types";
export * from "./utils";
export * from "./validators";
// export * from "./dec-module/dec.module";
export * from "./form-error-tips/form-error-tips.component";
export * from "./input-space-error/input-space-error.directive";
// export * from "./public-path/public-path.pipe";
// export * from "./quick-date-range/quick-date-range.component";
export * from "./table-list";
export * from "./storage";

0
projects/cdk/src/quick-date-range/quick-date-range.component.css

17
projects/cdk/src/quick-date-range/quick-date-range.component.html

@ -0,0 +1,17 @@
<nz-space>
<nz-radio-group
*nzSpaceItem
[ngModel]="quick"
(ngModelChange)="onQuickChange($event)">
<label *ngFor="let item of dateFilterOptions"
nz-radio-button
[nzValue]="item.value">
{{item.label}}
</label>
</nz-radio-group>
<nz-range-picker
*nzSpaceItem
[ngModel]="range"
(ngModelChange)="onRangeChange($event)">
</nz-range-picker>
</nz-space>

94
projects/cdk/src/quick-date-range/quick-date-range.component.ts

@ -0,0 +1,94 @@
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from "@angular/forms";
import { NzRadioModule } from "ng-zorro-antd/radio";
import { NzSpaceModule } from "ng-zorro-antd/space";
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { NzDatePickerModule } from "ng-zorro-antd/date-picker";
import { subDays, format } from "date-fns";
@Component({
standalone: true,
selector: "dec-quick-date-range",
templateUrl: "./quick-date-range.component.html",
styleUrls: ["./quick-date-range.component.css"],
imports: [CommonModule, NzDatePickerModule, NzSpaceModule, NzRadioModule, FormsModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: QuickDateRangeComponent,
},
],
})
export class QuickDateRangeComponent implements OnInit, ControlValueAccessor {
constructor() {}
dateFilterOptions = [
{
label: "今日",
value: 0,
},
{
label: "近7日",
value: 7,
},
{
label: "近30日",
value: 30,
},
{
label: "近90日",
value: 90,
},
];
quick: number | null = null;
range: Date[] = [];
onQuickChange(v: number) {
this.quick = v;
this.range = this.getDateRange(v).map((d) => new Date(d));
const val = this.range.map((date) => format(date, "yyyy-MM-dd"));
// console.log("val", val);
this.onChange(val);
}
getDateRange(n: number): [string, string] {
const today = new Date();
const previousDay = subDays(today, n);
const formattedToday = format(today, "yyyy-MM-dd");
const formattedPreviousDay = format(previousDay, "yyyy-MM-dd");
return [formattedPreviousDay, formattedToday];
}
ngOnInit(): void {}
onRangeChange(v: Date[]) {
this.quick = null;
this.range = v;
const val = v.map((date) => format(date, "yyyy-MM-dd"));
this.onChange(val);
}
onChange(v: any) {}
ontouch(v: any) {}
writeValue(v: string[]): void {
// console.log("v", v);
if (v && Array.isArray(v)) {
this.range = v?.map((d) => new Date(d));
} else {
this.onQuickChange(90);
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.ontouch = fn;
}
}

71
projects/cdk/src/storage/cache.service.ts

@ -0,0 +1,71 @@
import { Injectable } from "@angular/core";
import { NavigationEnd, Router } from "@angular/router";
import { filter } from "rxjs/operators";
import { DecSafeAny } from "../types";
import { StorageService } from "./storage.service";
type IcacheUrlMap = Record<string, Array<string>>;
export class CacheItem<T = any> {
constructor(private storage: StorageService, private cacheKey: string) {}
getItem(): T {
return this.storage.get(this.cacheKey, { stroage: "session" });
}
setItem(query: T) {
this.storage.set(this.cacheKey, query, { stroage: "session" });
}
remove() {
this.storage.set(this.cacheKey, null, { stroage: "session" });
}
}
@Injectable()
export class CacheService {
constructor(private router: Router, private storage: StorageService) {}
/**
* cacheKey url
*/
private get cacheUrlMap(): IcacheUrlMap {
return this.storage.get("cacheUrlMap") ?? {};
}
private set cacheUrlMap(o: IcacheUrlMap) {
this.storage.set("cacheUrlMap", o);
}
listen() {
this.router.events.pipe(filter((f): f is NavigationEnd => f instanceof NavigationEnd)).subscribe((r) => {
const currentUrl = r.url;
for (const [api, urls] of Object.entries(this.cacheUrlMap)) {
if (!urls.some((s) => currentUrl.startsWith(s))) {
this.storage.remove(api, { stroage: "session" });
}
}
});
}
/**
*
* @param cacheKey key
* @param cacheInUrl cacheKey url中缓存url中时删除
* @return getItem & setItem
*/
initCache(cacheKey: string, cacheInUrl?: string): CacheItem;
initCache(cacheKey: string, cacheInUrl?: Array<string>): CacheItem;
initCache(cacheKey: string, cacheInUrl?: string | Array<string>): CacheItem {
if (!cacheInUrl) {
cacheInUrl = [this.router.url];
}
if (!this.cacheUrlMap[cacheKey]) {
cacheInUrl = (Array.isArray(cacheInUrl) ? cacheInUrl : [cacheInUrl]) as Array<string>;
const storageData = this.cacheUrlMap;
storageData[cacheKey] = cacheInUrl;
this.cacheUrlMap = storageData;
}
return new CacheItem(this.storage, cacheKey);
}
}

3
projects/cdk/src/storage/index.ts

@ -0,0 +1,3 @@
export * from "./storage.module";
export * from "./storage.service";
export * from "./cache.service";

15
projects/cdk/src/storage/storage.module.ts

@ -0,0 +1,15 @@
import { NgModule } from "@angular/core";
import { CacheService } from "./cache.service";
import { StorageService } from "./storage.service";
@NgModule({
declarations: [],
imports: [],
providers: [StorageService, CacheService],
exports: [],
})
export class StorageModule {
constructor(private cache: CacheService) {
this.cache.listen();
}
}

79
projects/cdk/src/storage/storage.service.ts

@ -0,0 +1,79 @@
import { Injectable } from "@angular/core";
export interface IFunc<T> {
(prev?: T): T;
}
interface Option<T = any> {
stroage?: "local" | "session";
defaultValue?: T | IFunc<T>;
serializer?: (v: T) => string;
deserializer?: (v: string) => T;
}
export const isFunction = (value: unknown): value is Function => typeof value === "function";
@Injectable()
export class StorageService {
constructor() {}
private parseOption(option?: Option) {
const storage = option?.stroage === "session" ? sessionStorage : localStorage;
const serializer = option?.serializer ? option?.serializer : JSON.stringify;
const deserializer = option?.deserializer ? option?.deserializer : JSON.parse;
let defaultValue = option?.defaultValue;
if (isFunction(option?.defaultValue)) {
defaultValue = option?.defaultValue();
}
return {
storage,
serializer,
deserializer,
defaultValue,
};
}
get<T>(key: string, option?: Option<T>) {
const { storage, deserializer, defaultValue } = this.parseOption(option);
try {
const val = storage.getItem(key);
if (val) {
return deserializer(val);
}
} catch (error) {
console.error(error);
}
return defaultValue;
}
set<T>(key: string, value: T | IFunc<T>, option?: Option<T>) {
const { storage, serializer, defaultValue } = this.parseOption(option);
const val = (isFunction(value) ? value() : value) ?? defaultValue;
if (typeof val === "undefined") {
storage.removeItem(key);
} else {
try {
storage.setItem(key, serializer(val));
} catch (error) {
console.error(error);
}
}
}
remove(key: string, option?: Pick<Option, "stroage">) {
const { storage } = this.parseOption(option);
storage.removeItem(key);
}
clear(option?: Pick<Option, "stroage">) {
const { storage } = this.parseOption(option);
storage.clear();
}
keys(option?: Pick<Option, "stroage">) {
const { storage } = this.parseOption(option);
return Object.keys(storage);
}
}

5
projects/cdk/src/table-list/index.ts

@ -0,0 +1,5 @@
export * from "./table-list.module";
export * from "./table-list-options";
export * from "./table-list/table-list.component";
export * from "./table-operation/table-operation.component";

143
projects/cdk/src/table-list/table-list-options.ts

@ -0,0 +1,143 @@
import { Observable, Subject } from "rxjs";
import { produce, immerable, setAutoFreeze } from "immer";
import { DecSafeAny, TableListColumns, TableOperation } from "../types";
import { EventEmitter } from "@angular/core";
type IfetchData = (...args: DecSafeAny[]) => Observable<DecSafeAny>;
type ITableListConfig = {
manual?: boolean;
cacheKey?: string;
cacheTo?: string[];
rowKey?: string;
columnKey?: string;
selectable?: boolean;
withOutDefaultColumns?: boolean;
theadSettable?: boolean;
pageFromZero?: boolean;
frontPagination?: boolean;
};
type TableListState = {
selectedKeys: number[];
};
type TableListPager = {
page: number;
size: number;
total: number;
loading: boolean;
sort: { [K: string]: "ascend" | "descend" };
};
setAutoFreeze(false);
export class TableListOption {
[immerable] = true;
trigger$ = new Subject<DecSafeAny>();
getState$ = new EventEmitter<TableListState>();
columns: TableListColumns[] = [];
operations!: TableOperation[];
fetchData: IfetchData;
manual?: boolean = false;
cacheKey?: string;
cacheTo?: string[];
rowKey: string;
columnKey?: string;
selectable: boolean = false;
withOutDefaultColumns?: boolean;
pageFromZero?: boolean;
theadSettable: boolean = false;
frontPagination: boolean = true;
scroll: { x?: string | null; y?: string | null } = { x: "1000px", y: null };
pager: TableListPager = {
page: 1,
size: 5,
loading: false,
total: 0,
sort: {},
};
constructor(fetchData: IfetchData, config?: ITableListConfig) {
this.fetchData = fetchData;
this.manual = config?.manual;
this.cacheKey = config?.cacheKey;
this.cacheTo = config?.cacheTo;
this.rowKey = config?.rowKey ?? "id";
this.selectable = config?.selectable ?? false;
this.withOutDefaultColumns = config?.withOutDefaultColumns;
this.theadSettable = config?.theadSettable ?? false;
this.columnKey = config?.columnKey;
this.pageFromZero = config?.pageFromZero;
this.frontPagination = config?.frontPagination ?? true;
}
/**
*
* @param columns TableListColumns
* @returns
*/
setColumns(columns: TableListColumns[]) {
return produce(this, (draft) => {
// if (!this.withOutDefaultColumns) {
// columns = columns.concat(
// { key: "createTime", title: "创建时间", sort: true },
// { key: "updateTime", title: "更新时间", sort: true, visible: false }
// );
// draft.pager.sort = { createTime: "descend" };
// }
draft.columns = columns;
});
}
/**
*
* @param columns TableListColumns
* @returns
*/
setOptions(operations: TableOperation[]) {
return produce(this, (draft) => {
draft.operations = operations;
});
}
/**
* CacheKey
* @param columns TableListColumns
* @returns
*/
setCacheKey(cacheKey: string) {
return produce(this, (draft) => {
draft.cacheKey = cacheKey;
});
}
run(e?: DecSafeAny) {
setTimeout(() => {
// 防止 还没有 subscribe 就 next 了
this.trigger$.next(e);
}, 10);
}
reset() {
this.pager.page = 1;
this.run();
}
}

56
projects/cdk/src/table-list/table-list.module.ts

@ -0,0 +1,56 @@
import { NgxPermissionsModule } from "ngx-permissions";
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { DragDropModule } from "@angular/cdk/drag-drop";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { NzDrawerModule } from "ng-zorro-antd/drawer";
import { NzDropDownModule } from "ng-zorro-antd/dropdown";
import { NzCheckboxModule } from "ng-zorro-antd/checkbox";
import { NzEmptyModule } from "ng-zorro-antd/empty";
import { NzTableModule } from "ng-zorro-antd/table";
import { NzPaginationModule } from "ng-zorro-antd/pagination";
import { NzCardModule } from "ng-zorro-antd/card";
import { NzFormModule } from "ng-zorro-antd/form";
import { NzSpaceModule } from "ng-zorro-antd/space";
import { NzIconModule } from "ng-zorro-antd/icon";
import { NzSwitchModule } from "ng-zorro-antd/switch";
import { NzSkeletonModule } from "ng-zorro-antd/skeleton";
import { NzDividerModule } from "ng-zorro-antd/divider";
import { NzPopoverModule } from "ng-zorro-antd/popover";
import { NzButtonModule } from "ng-zorro-antd/button";
import { TableListComponent } from "./table-list/table-list.component";
import { TableOperationComponent } from "./table-operation/table-operation.component";
import { TdOverflowDirective } from "./td-overflow.directive";
@NgModule({
declarations: [TableListComponent, TableOperationComponent, TdOverflowDirective],
imports: [
CommonModule,
RouterModule,
FormsModule,
ReactiveFormsModule,
DragDropModule,
NzSwitchModule,
NzDividerModule,
NzCheckboxModule,
NzDrawerModule,
NzDropDownModule,
NzEmptyModule,
NzTableModule,
NzPaginationModule,
NzCardModule,
NzFormModule,
NzSpaceModule,
NzButtonModule,
NzPopoverModule,
NzIconModule,
NzSkeletonModule,
NgxPermissionsModule.forRoot(),
],
exports: [TableListComponent, TableOperationComponent],
})
export class TableListModule {
constructor() {}
}

225
projects/cdk/src/table-list/table-list/table-list.component.html

@ -0,0 +1,225 @@
<form nz-form class="query-form mb-2" [formGroup]="formGroup" [nzLayout]="searchLayout">
<div
*ngIf="search || action">
<div nz-row>
<div nz-col nzFlex="auto" nz-row [nzGutter]="[12,12]" class="search-row">
<ng-container *ngIf="search">
<ng-container [ngTemplateOutlet]="search"></ng-container>
<div nz-col nzFlex="120px">
<nz-form-item>
<nz-form-label nzNoColon></nz-form-label>
<nz-form-control>
<nz-space *ngIf="search">
<button nz-button nzGhost nzType="primary" *nzSpaceItem (click)="doQuery()">
查询
</button>
<button nz-button *nzSpaceItem (click)="reset()">
重置
</button>
</nz-space>
</nz-form-control>
</nz-form-item>
</div>
</ng-container>
</div>
<div nz-col class="flex items-center justify-end">
<ng-container *ngIf="action">
<ng-container [ngTemplateOutlet]="action"></ng-container>
</ng-container>
</div>
</div>
</div>
</form>
<ng-template #renderTableTpl>
<div class="table-card table-list shadow-sm ">
<div #tableEl class="overflow-auto">
<nz-table
#basicTable
[nzData]="dataSource"
[nzPageSizeOptions]="[5,10,20,]"
[nzLoading]="props.pager.loading"
[(nzPageSize)]="props.pager.size"
[(nzPageIndex)]="props.pager.page"
[nzTotal]="props.pager.total"
[nzFrontPagination]="props.frontPagination"
[nzShowPagination]="true"
[nzShowSizeChanger]="true"
[nzShowQuickJumper]="true"
(nzPageIndexChange)="onPageChange()"
(nzPageSizeChange)="onPageChange()"
[nzTableLayout]="props.scroll.x ? 'fixed' : 'auto' "
[nzScroll]="props.scroll">
<thead>
<tr>
<th
*ngIf="props.selectable"
[nzChecked]="dataSource && selection.selected.length === dataSource.length && dataSource.length !== 0"
(nzCheckedChange)="onChecked($event)"
nzWidth="40px">
</th>
<ng-container *ngFor="let col of columns">
<th *ngIf="col.visible"
[nzWidth]="col.width || null "
[nzShowSort]="col.sort"
[nzSortDirections]="['ascend', 'descend']"
[nzSortOrder]="props.pager.sort[col.key] || null"
(nzSortOrderChange)="onSort($event,col.key)">
{{col.title}}
</th>
</ng-container>
<th *ngIf="operation.length" [nzWidth]="optionWidth" [nzRight]="!!props.scroll.x">
操作
</th>
<th nzRight nzWidth="40px" *ngIf="props.theadSettable">
<span (click)="toggleColumnVisible(columnVisibleSettingTpl)"
class="cursor-pointer"
nz-icon
nzType="setting"
nzTheme="outline">
</span>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let dataItem of basicTable.data" (click)="onTrClick(dataItem)"
[ngClass]="rowClass">
<td *ngIf="props.selectable" [nzChecked]="selection.isSelected(dataItem[props.rowKey])"
(nzCheckedChange)="onChecked($event,dataItem[props.rowKey])">
</td>
<ng-container *ngFor="let col of columns">
<td *ngIf="col.visible" nzEllipsis>
<ng-container [ngSwitch]="col.key">
<ng-container *ngSwitchCase="'createTime'">
<ng-container
[ngTemplateOutlet]="dateTimeTpl"
[ngTemplateOutletContext]="{$implicit:dataItem[col.key]}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'updateTime'">
<ng-container
[ngTemplateOutlet]="dateTimeTpl"
[ngTemplateOutletContext]="{$implicit:dataItem[col.key]}">
</ng-container>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-container *ngIf="renderColumns else defaultTdTpl"
[ngTemplateOutlet]="renderColumns"
[ngTemplateOutletContext]="{
$implicit:dataItem[col.key],
key:col.key,
row:dataItem,
column:col
}">
</ng-container>
<ng-template #defaultTdTpl>
{{dataItem[col.key]}}
</ng-template>
</ng-container>
</ng-container>
</td>
</ng-container>
<td *ngIf="operation.length" [nzRight]="!!props.scroll.x" class="operation">
<dec-table-operation [rowData]="dataItem" [options]="operation">
</dec-table-operation>
</td>
<td nzRight *ngIf="props.theadSettable"></td>
</tr>
</tbody>
</nz-table>
</div>
</div>
</ng-template>
<ng-container *ngIf="renderItem else renderTableTpl">
<ng-container *ngIf="props.pager.loading">
<nz-card [nzBordered]="false">
<nz-skeleton [nzActive]="true"></nz-skeleton>
</nz-card>
</ng-container>
<div class="custom-render" *ngIf="!props.pager.loading">
<ng-container *ngIf="dataSource.length > 0 else emptyTpl">
<div>
<ng-container
[ngTemplateOutlet]="renderItem"
[ngTemplateOutletContext]="{$implicit:dataSource}">
</ng-container>
</div>
<div class="mt-4 flex justify-end">
<nz-pagination [nzPageSizeOptions]="[10,20,50,100]"
[(nzPageSize)]="props.pager.size"
[(nzPageIndex)]="props.pager.page"
[nzTotal]="props.pager.total"
[nzShowTotal]="totalTpl"
[nzShowSizeChanger]="true"
[nzShowQuickJumper]="true"
(nzPageIndexChange)="onPageChange()"
(nzPageSizeChange)="onPageChange()">
</nz-pagination>
</div>
</ng-container>
<ng-template #emptyTpl>
<nz-card [nzBordered]="false">
<nz-empty></nz-empty>
</nz-card>
</ng-template>
</div>
</ng-container>
<ng-template #columnVisibleSettingTpl>
<nz-checkbox-wrapper class="w-full">
<ul cdkDropList (cdkDropListDropped)="colunmsSort($event)" class="columns-list">
<li *ngFor="let item of columns"
class="table-setting-item cursor-pointer" cdkDrag>
<div class="columns-item-placeholder" *cdkDragPlaceholder></div>
<div class="flex items-center justify-between p-2">
<div class="flex-shrink-0">
<span nz-icon nzType="holder" nzTheme="outline"></span>
</div>
<div class="flex-1 pl-1">
{{item.title}}
</div>
<div class="flex-shrink-0">
<nz-switch
nzSize="small"
[disabled]="!!item.disabled"
[(ngModel)]="item.visible"
(ngModelChange)="onColumnsChange()">
</nz-switch>
</div>
</div>
</li>
</ul>
</nz-checkbox-wrapper>
</ng-template>
<ng-template #totalTpl let-total>
<ng-container *ngIf="showTotal">
<ng-template [ngTemplateOutlet]="showTotal" [ngTemplateOutletContext]="{$implicit:props.pager}"></ng-template>
</ng-container>
<ng-container *ngIf="!showTotal">
共{{total}}条
</ng-container>
</ng-template>
<ng-template #dateTimeTpl let-date>
{{date| date:'yyyy-MM-dd HH:mm:ss'}}
</ng-template>
<!-- 为了计算操作栏的宽度 ---- start -->
<dec-table-operation #operationEl [rowData]="{}" [options]="operation" class="hidden-option">
</dec-table-operation>
<!-- 为了计算操作栏的宽度 ---- end -->

119
projects/cdk/src/table-list/table-list/table-list.component.less

@ -0,0 +1,119 @@
.advance-search-btn {
position: relative;
.up-arrow {
position: absolute;
bottom: calc(-100% + 10px);
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 28px;
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.1));
}
.double-arrow {
transform: rotate(90deg);
transition: transform 0.3s;
&.up {
transform: rotate(-90deg);
}
}
}
.hidden-option {
display: inline-block;
height: 0;
opacity: 0;
overflow: hidden;
pointer-events: none;
}
.table-list {
td {
word-break: break-all;
}
th {
white-space: nowrap;
}
::ng-deep {
.ant-table-pagination.ant-pagination {
padding: 0 16px;
}
// tbody {
// tr td:last-child {
// width: 0;
// }
// }
}
}
.advance-search {
::ng-deep {
.ant-card-body {
padding: 16px 24px;
}
}
}
.search-row {
::ng-deep {
> * {
padding: 6px;
}
}
}
.query-form {
::ng-deep {
.hor {
nz-form-item {
align-items: center;
}
}
nz-form-item {
margin-bottom: 0;
// flex-direction: column;
// align-items: flex-start;
// justify-content: flex-start;
// margin-right: 12px;
}
}
}
::ng-deep {
.table-settings {
.ant-drawer-body {
padding: 12px 0;
}
}
.table-setting-item {
background-color: #fff;
&:hover {
background-color: var(--bg-light);
}
}
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-animating {
transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.table-setting-item:last-child {
border: none;
}
.columns-list.cdk-drop-list-dragging .table-setting-item:not(.cdk-drag-placeholder) {
transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.columns-item-placeholder {
background: #fff;
border: 3px dotted var(--p);
min-height: 38px;
transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);
}

386
projects/cdk/src/table-list/table-list/table-list.component.ts

@ -0,0 +1,386 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
TemplateRef,
ViewChild,
Pipe,
PipeTransform,
AfterViewInit,
ElementRef,
ViewChildren,
QueryList,
Renderer2,
ChangeDetectorRef,
OnDestroy,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { SelectionModel } from "@angular/cdk/collections";
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
import { Router } from "@angular/router";
import { debounceTime, finalize } from "rxjs/operators";
import { FormsModule, FormControl, FormGroup, AbstractControl } from "@angular/forms";
import { NzDrawerModule, NzDrawerService } from "ng-zorro-antd/drawer";
import { DecSafeAny, PageResult, TableListColumns, TableOperation } from "../../types";
import { TableListOption } from "../table-list-options";
import { CacheItem, CacheService, StorageService } from "../../storage";
import { TableOperationComponent } from "../table-operation/table-operation.component";
@Pipe({
name: "operationFilter",
})
export class OperationPipe implements PipeTransform {
transform(operations: TableOperation[], rowItem: any): TableOperation[] {
return operations?.filter((f) => (f.visible ? f.visible(rowItem) : true)) ?? [];
}
}
const DATE_RANGE_FIELDS = ["createTime", "updateTime"];
@Component({
selector: "table-list",
templateUrl: "./table-list.component.html",
styleUrls: ["./table-list.component.less"],
})
export class TableListComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
constructor(
// private modal: NzModalService,
private cdr: ChangeDetectorRef,
private el: ElementRef,
private router: Router,
private drawerService: NzDrawerService,
private storage: StorageService,
private cacheService: CacheService
) {}
@Input() props!: TableListOption;
// https://github.com/angular/angular/issues/13761
@Input() formGroup = new FormGroup<any>({});
/**
*
* $implicit:dataItem[col.key] `具体的值`
* key:col.key `字段名`
* row:dataItem `当前行数据`
* column:col `当前列`
*/
@Input() renderColumns?: TemplateRef<{}>;
/**
* 使
*/
@Input() renderItem?: TemplateRef<{}>;
@Input() action?: TemplateRef<{}>;
@Input() search?: TemplateRef<{}>;
@Input() searchLayout: "horizontal" | "vertical" = "horizontal";
/**
* @deprecated
*/
@Input() advanceSearch?: TemplateRef<{}>;
@Input() showTotal?: TemplateRef<{}>;
@Input() beforeReset?: Function;
@Input() rowClass?: string;
// @Input() resizeable?: boolean;
@Output() onRowClick = new EventEmitter();
dataSource: DecSafeAny[] = [];
totalPages = 0;
public selection = new SelectionModel<number>(true);
public advanceSearchVisible: boolean = false;
public optionWidth: string = "200px";
private cache?: CacheItem;
get columns(): TableListColumns[] {
return this.props.columns ?? [];
}
get operation(): TableOperation[] {
return this.props.operations ?? [];
}
get createTime() {
return this.formGroup.get("createTime");
}
ngOnInit(): void {
this.props.trigger$.pipe(debounceTime(100)).subscribe((e?: any) => {
if (this.props.fetchData) {
this.props.pager.loading = true;
this.selection.clear();
this.emitState();
const query = this.formGroup.getRawValue();
// this.saveQueryDataToCache(query);
const pager = this.parsePager();
this.props
.fetchData(pager, this.parseQueryValue(), e)
.pipe(
finalize(() => {
this.props.pager.loading = false;
})
)
.subscribe((f: PageResult) => {
this.dataSource = f.records ?? f.content;
console.log("this.dataSource", f);
this.props.pager.total = f.total;
this.totalPages = Math.ceil(f.total / this.props.pager.size);
this.checkPage();
});
}
});
setTimeout(() => {
this.emitState();
}, 10);
// 初始化的时候?
if (!this.props.manual) {
this.props.run();
}
this.getCacheData();
}
ngOnDestroy(): void {
// console.log("ngOnDestroy");
}
ngAfterViewInit(): void {
const opEl: HTMLDivElement = this.el.nativeElement.querySelector(".hidden-option");
if (opEl) {
setTimeout(() => {
const paddingX = 2 * 24;
this.optionWidth = Math.ceil(opEl.offsetWidth + paddingX) + "px";
this.cdr.detectChanges();
});
}
}
ngOnChanges(changes: SimpleChanges): void {
const props = changes["props"];
const currentProps = props?.currentValue;
const previousProps = props?.previousValue;
if (currentProps) {
if (previousProps?.["cacheKey"] !== currentProps?.["cacheKey"]) {
this.removeOldCache();
this.getCacheData();
}
this.parseColumus(currentProps?.["columns"]);
this.parseFormControls(currentProps?.["queryForm"]);
}
}
private saveQueryDataToCache(query: {}) {
if (this.cache) {
const { page, size, sort, total } = this.props.pager;
this.cache.setItem({ page, size, sort, total, ...query });
}
}
private removeOldCache() {
this.cache?.remove();
}
private getCacheData() {
let { cacheKey, cacheTo = [] } = this.props;
this.formGroup.reset();
if (!cacheKey) {
cacheKey = `DATA_CACHE_${this.formatePathname()}`;
}
if (!this.cache) {
this.cache = this.cacheService.initCache(cacheKey, [this.router.url, ...cacheTo]);
}
const cacheData = this.cache?.getItem();
if (cacheData) {
const { page, size, sort, total, ...query } = cacheData;
this.props.pager = {
...this.props.pager,
total,
page,
size,
sort,
};
this.formGroup.patchValue(query);
}
}
private parsePager() {
let { page, size } = this.props.pager;
if (this.props.pageFromZero) {
page = page - 1;
}
const pager = { page: page, current: page, size, sort: this.formatSort() };
return pager;
}
private parseQueryValue(): {} {
const o = Object.create(null);
Object.entries(this.formGroup.getRawValue()).forEach(([k, v]) => {
if (DATE_RANGE_FIELDS.includes(k) && Array.isArray(v)) {
const from = v?.[0] instanceof Date ? v?.[0]?.toISOString() : v[0];
const to = v?.[1] instanceof Date ? v?.[1]?.toISOString() : v[1];
o[k] = { from, to };
} else {
o[k] = v;
}
});
return o;
}
private checkPage() {
const maxPages = this.totalPages === 0 ? 1 : this.totalPages;
if (maxPages < this.props.pager.page) {
this.props.pager.page = maxPages;
this.reload();
}
}
private emitState() {
this.props.getState$.emit({ selectedKeys: this.selection.selected });
}
private parseColumus(currentCols: TableListColumns[]) {
if (this.props.columns.some((s) => s.coverStorage)) {
this.onColumnsChange();
}
const colsFormStorage: TableListColumns[] = this.storage.get(this.formatePathname()) ?? [];
this.props.columns = currentCols
.map((i: TableListColumns) => {
const storageCol = colsFormStorage.find((f) => f.key === i.key);
let visible = i.visible !== void 0 ? i.visible : true;
let width = i.width;
// if (["createTime", "updateTime"].includes(i.key)) {
// width = "180px";
// }
return {
...i,
visible,
width,
...(storageCol ?? {}),
};
})
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
private parseFormControls(formGroup?: FormGroup) {
// if (!this.props.withOutDefaultColumns) {
// this.formGroup.addControl("updateTime", new FormControl(null));
// this.formGroup.addControl("createTime", new FormControl(null));
// }
}
formatSort() {
const { sort } = this.props.pager;
if (!sort) {
return null;
}
const sortArr = [...Object.entries(sort)]?.[0];
if (!sortArr) {
return null;
}
return `${sortArr[0]},${sortArr[1] === "ascend" ? "asc" : "desc"}`;
}
colunmsSort(event: unknown) {
const e = event as CdkDragDrop<TableListColumns[]>;
moveItemInArray(this.props.columns, e.previousIndex, e.currentIndex);
this.onColumnsChange();
}
reload() {
this.props.run();
}
onPageChange() {
if (!this.props.frontPagination) {
this.props.run();
}
}
doQuery() {
this.props.pager.page = 1;
this.reload();
}
reset() {
this.beforeReset?.();
this.formGroup.reset();
this.doQuery();
}
onSort(v: string | null, fieldName: string) {
this.props.pager.sort = { [fieldName]: v as "ascend" | "descend" };
this.reload();
}
toggleAdvanceSearch(show?: boolean) {
if (typeof show === "boolean") {
this.advanceSearchVisible = show;
} else {
this.advanceSearchVisible = !this.advanceSearchVisible;
}
}
filteroperation(dataItem: any): TableOperation[] {
return this.operation?.filter((f) => (f.visible ? f.visible(dataItem) : true)) ?? [];
}
toggleColumnVisible(nzContent: TemplateRef<DecSafeAny>) {
this.drawerService.create({
nzTitle: "设置展示项",
nzWidth: 280,
nzContent,
nzWrapClassName: "table-settings",
});
}
private formatePathname() {
return this.props.columnKey ?? this.router.url.replace(/\//g, "_");
}
onColumnsChange() {
const columnStorageKey = `COLUMN_${this.formatePathname()}`;
this.storage.set(
columnStorageKey,
this.columns.map((i, idx) => {
const { key, width, visible } = i;
return { key, width, visible, order: idx };
})
);
}
onChecked(checked: boolean, rowKey?: DecSafeAny) {
const fn = checked ? "select" : "deselect";
const rowKeys = rowKey ? [rowKey] : this.dataSource.map((i) => i[this.props.rowKey]);
this.selection[fn](...rowKeys);
this.emitState();
}
onTrClick(dataItem: DecSafeAny) {
if (this.onRowClick) {
this.onRowClick.emit(dataItem);
}
}
}

30
projects/cdk/src/table-list/table-operation/table-operation.component.html

@ -0,0 +1,30 @@
<div class="operation">
<ng-container *ngFor="let item of options.slice(0,maximum); let i = index; let last = last">
<span [ngClass]="{disabled:item.disabled}">
<a [ngClass]="{danger:item.danger}" [href]="item.href" [routerLink]="item.link"
(click)="onClick(item)">
{{item.title}}
</a>
</span>
<nz-divider nzType="vertical" *ngIf="!last"></nz-divider>
</ng-container>
<ng-container *ngIf="options.length > maximum">
<nz-divider nzType="vertical"></nz-divider>
<a nz-dropdown [nzDropdownMenu]="menu">
更多操作
<i nz-icon nzType="down" nzTheme="outline"></i>
</a>
<nz-dropdown-menu #menu="nzDropdownMenu">
<ul nz-menu class="operation">
<li
*ngFor="let item of options.slice(maximum);"
nz-menu-item
(click)="onClick(item)"
[nzDanger]="item.danger"
[nzDisabled]="item.disabled">
{{item.title}}
</li>
</ul>
</nz-dropdown-menu>
</ng-container>
</div>

25
projects/cdk/src/table-list/table-operation/table-operation.component.less

@ -0,0 +1,25 @@
.operation {
display: inline-block;
min-width: 120px;
}
.danger {
color: var(--red);
}
.disabled {
cursor: not-allowed;
a {
pointer-events: none;
color: rgba(0, 0, 0, 0.65);
}
}
.operation {
a {
// color: rgba(0, 0, 0, 0.65);
&:hover {
color: var(--p);
text-decoration: underline;
}
}
}

61
projects/cdk/src/table-list/table-operation/table-operation.component.ts

@ -0,0 +1,61 @@
import { Component, Input, OnInit } from "@angular/core";
import { NgxPermissionsService } from "ngx-permissions";
import { DecSafeAny, TableOperation } from "../../types";
@Component({
selector: "dec-table-operation",
templateUrl: "./table-operation.component.html",
styleUrls: ["./table-operation.component.less"],
})
export class TableOperationComponent implements OnInit {
constructor(private premission: NgxPermissionsService) {}
@Input() options: TableOperation[] = [];
@Input() maximum: number = 3;
@Input() rowData: DecSafeAny;
ngOnInit(): void {
// this.options = this.options.filter(async (f) => {
// if (f.visible) {
// return f.visible(this.rowData);
// }
// if (f.premissions.length) {
// console.log("f.premissions", f.premissions, this.premission.hasPermission(f.premissions));
// return await this.premission.hasPermission(f.premissions);
// }
// return true;
// });
this.filterOption();
}
async filterOption() {
const o = [];
for (const f of this.options) {
let visible = true;
if (typeof f.visible === "function") {
visible = f.visible(this.rowData);
}
if (!visible) {
continue;
}
if (f.premissions.length) {
const r = await this.premission.hasPermission(f.premissions);
if (r) {
o.push(f);
}
continue;
}
o.push(f);
}
this.options = o;
}
onClick(item: TableOperation) {
if (item.onClick) {
item?.onClick(this.rowData);
}
}
}

30
projects/cdk/src/table-list/td-overflow.directive.ts

@ -0,0 +1,30 @@
import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Directive, ElementRef, Input } from "@angular/core";
import { NzPopoverDirective } from "ng-zorro-antd/popover";
@Directive({
selector: "[jwTdOverflow]",
})
export class TdOverflowDirective implements AfterViewInit {
@Input("jwTdOverflow") content!: string;
constructor(
private elementRef: ElementRef,
private popoverDirective: NzPopoverDirective,
private cdr: ChangeDetectorRef
) {}
ngOnInit() {}
ngAfterViewInit(): void {
const element = this.elementRef.nativeElement as HTMLElement;
console.log("element", element.offsetWidth, element.scrollWidth);
// 如果元素的实际宽度大于可见宽度,就使用 nz-popover 指令来显示完整的内容
if (element.offsetWidth < element.scrollWidth) {
this.popoverDirective.content = this.content;
} else {
this.popoverDirective.trigger = null;
this.popoverDirective.content = "da";
element.textContent = this.content;
}
}
}

58
projects/cdk/src/types/index.ts

@ -0,0 +1,58 @@
export type AnyObject = { [k: string]: any };
export type DecSafeAny = any;
export type DecText = number | string;
export type Augmented<O extends object> = O & AnyObject;
export interface ResponseType<T = any> {
body: T;
code: number;
desc: string;
success: boolean;
}
export interface TableListColumns {
key: string;
title: string;
visible?: boolean;
width?: string;
sort?: boolean;
order?: number;
disabled?: boolean;
coverStorage?: boolean;
}
export interface TableOperation {
title: string;
href?: string;
link?: string[];
target?: string;
premissions: string[];
danger?: boolean;
disabled?: boolean;
onClick?: (v: DecSafeAny) => void;
visible?: (v: DecSafeAny) => boolean;
}
export interface PageResult<T = DecSafeAny> {
total: number;
content: T[];
records: T[];
}
export interface AuthInterface {
role: string;
userId: string;
userName: string;
permissionList: AuthPermissionInterface[];
}
export interface AuthPermissionInterface {
name: string;
roleId: string;
scope: 1 | 0;
type: number;
value: "true" | "false";
}

46
projects/cdk/src/utils/index.ts

@ -0,0 +1,46 @@
import { AbstractControl, FormControl, FormGroup } from "@angular/forms";
export class Utils {
static validateFormGroup(formGroup: FormGroup) {
if (!formGroup.valid) {
Object.keys(formGroup.controls).forEach((field) => {
const control = formGroup.get(field);
if (control instanceof FormControl) {
control.markAsDirty();
control.markAsTouched({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true });
} else if (control instanceof FormGroup) {
Utils.validateFormGroup(control);
}
});
}
return formGroup.valid;
}
static validateFormControl(control: AbstractControl) {
control.markAsDirty();
control.markAsTouched({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true });
return control.valid;
}
static queryify(query: {}): string {
const o = Object.create(null);
Object.entries(query).forEach(([k, v]) => {
if (v !== void 0 && v !== null) {
o[k] = v;
}
});
return new URLSearchParams(o).toString();
}
static getHostByEnvironment(prod?: boolean) {
const protocol = window.location.protocol;
const host = prod ? window.location.host : `localhost:${window.location.port}`;
return `${protocol}//${host}`;
}
// static detectionImagePath(jobId: string, imgName: string) {
// return `/record/${jobId}/detect/${imgName}`;
// }
}

66
projects/cdk/src/validators/index.ts

@ -0,0 +1,66 @@
import { ValidatorFn, Validators } from "@angular/forms";
export class FormValidators {
static required(message?: string): ValidatorFn {
return (control) => {
const error = Validators.required(control);
return error ? { ...error, required: { message } } : null;
};
}
static maxLength(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.maxLength(maxLength)(control);
if (error) {
error["maxlength"]["message"] = message;
return error;
}
return null;
};
}
static minLength(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.minLength(maxLength)(control);
if (error) {
error["minlength"]["message"] = message;
return error;
}
return null;
};
}
static pattern(pattern: RegExp | string, message?: string): ValidatorFn {
return (control) => {
const error = Validators.pattern(pattern)(control);
if (error) {
error["pattern"]["message"] = message;
return error;
}
return null;
};
}
static min(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.min(maxLength)(control);
if (error) {
error["min"]["message"] = message;
return error;
}
return null;
};
}
static max(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.max(maxLength)(control);
if (error) {
error["max"]["message"] = message;
return error;
}
return null;
};
}
static email(message?: string): ValidatorFn {
return (control) => {
const error = Validators.email(control);
return error ? { ...error, email: { message } } : null;
};
}
}

14
projects/cdk/tsconfig.lib.json

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"**/*.spec.ts"
]
}

10
projects/cdk/tsconfig.lib.prod.json

@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

14
projects/cdk/tsconfig.spec.json

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

8
tailwind.config.js

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./projects/**/*.{html,ts}"],
theme: {
extend: {},
},
plugins: [],
};

69
tsconfig.json

@ -1,33 +1,42 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */ /* To learn more about this file see: https://angular.io/config/tsconfig. */
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"outDir": "./dist/out-tsc", "paths": {
"forceConsistentCasingInFileNames": true, "@cdk/*": [
"strict": true, "./projects/cdk/src/*",
"noImplicitOverride": true, "dist/cdk",
"noPropertyAccessFromIndexSignature": true, ],
"noImplicitReturns": true, "@admin/*": [
"noFallthroughCasesInSwitch": true, "./projects/admin/src/*",
"sourceMap": true, ],
"declaration": false, },
"downlevelIteration": true, "outDir": "./dist/out-tsc",
"experimentalDecorators": true, "forceConsistentCasingInFileNames": true,
"moduleResolution": "node", "strict": true,
"importHelpers": true, "noImplicitOverride": true,
"target": "ES2022", "noPropertyAccessFromIndexSignature": true,
"module": "ES2022", "noImplicitReturns": true,
"useDefineForClassFields": false, "noFallthroughCasesInSwitch": true,
"lib": [ "sourceMap": true,
"ES2022", "declaration": false,
"dom" "downlevelIteration": true,
] "experimentalDecorators": true,
}, "moduleResolution": "node",
"angularCompilerOptions": { "importHelpers": true,
"enableI18nLegacyMessageIdFormat": false, "target": "ES2022",
"strictInjectionParameters": true, "module": "ES2022",
"strictInputAccessModifiers": true, "useDefineForClassFields": false,
"strictTemplates": true "lib": [
} "ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
} }
Loading…
Cancel
Save