Browse Source

食谱详情

main
kkerwin 2 years ago
parent
commit
6bbf4f9585
  1. 35
      README.md
  2. 19
      projects/admin/src/app/components/ingredient-status-list/ingredient-status-list.component.ts
  3. 53
      projects/admin/src/app/pages/dish/dish.component.html
  4. 18
      projects/admin/src/app/pages/dish/dish.component.ts
  5. 2
      projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.html
  6. 88
      projects/admin/src/app/pages/ingredients/ingredient-form/ingredient-form.component.ts
  7. 15
      projects/admin/src/app/pages/ingredients/ingredient-list/ingredient-list.component.ts
  8. 12
      projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.html
  9. 30
      projects/admin/src/app/pages/ingredients/ingredient-release/ingredient-release.component.ts
  10. 2
      projects/admin/src/app/services/http.interceptor.ts
  11. 58
      projects/admin/src/styles.less
  12. 43
      projects/cdk/src/ingredient/ingredient-dish/ingredient-dish.component.ts
  13. 2
      projects/cdk/src/ingredient/ingredient-form-basic/ingredient-form-basic.component.html
  14. 23
      projects/cdk/src/ingredient/ingredient-form-basic/ingredient-form-basic.component.ts
  15. 2
      projects/cdk/src/ingredient/ingredient-meals/ingredient-meals.component.html
  16. 14
      projects/cdk/src/ingredient/ingredient-meals/ingredient-meals.component.ts
  17. 180
      projects/cdk/src/ingredient/ingredient-preview/ingredient-preview.component.html
  18. 38
      projects/cdk/src/ingredient/ingredient-preview/ingredient-preview.component.less
  19. 125
      projects/cdk/src/ingredient/ingredient-preview/ingredient-preview.component.ts
  20. 30
      projects/cdk/src/services/api.service.ts
  21. 1
      projects/cdk/src/shared/components/index.ts
  22. 3
      projects/cdk/src/shared/components/print/print.component.html
  23. 0
      projects/cdk/src/shared/components/print/print.component.less
  24. 92
      projects/cdk/src/shared/components/print/print.component.ts
  25. 10
      projects/cdk/src/shared/shared.module.ts
  26. 45
      projects/client/src/app/app-routing.module.ts
  27. 19
      projects/client/src/app/app.module.ts
  28. 20
      projects/client/src/app/components/app-layout/app-layout.component.html
  29. 8
      projects/client/src/app/components/app-layout/app-layout.component.ts
  30. 136
      projects/client/src/app/components/dish-form/dish-form.component.html
  31. 17
      projects/client/src/app/components/dish-form/dish-form.component.less
  32. 230
      projects/client/src/app/components/dish-form/dish-form.component.ts
  33. 1
      projects/client/src/app/components/index.ts
  34. 41
      projects/client/src/app/pages/data-vis/data-vis.component.html
  35. 240
      projects/client/src/app/pages/data-vis/data-vis.component.less
  36. 31
      projects/client/src/app/pages/data-vis/data-vis.component.ts
  37. 99
      projects/client/src/app/pages/dish/dish.component.html
  38. 177
      projects/client/src/app/pages/dish/dish.component.ts
  39. 5
      projects/client/src/app/pages/index.ts
  40. 51
      projects/client/src/app/pages/ingredients/ingredient-form/ingredient-form.component.html
  41. 7
      projects/client/src/app/pages/ingredients/ingredient-form/ingredient-form.component.less
  42. 162
      projects/client/src/app/pages/ingredients/ingredient-form/ingredient-form.component.ts
  43. 94
      projects/client/src/app/pages/ingredients/ingredient-list/ingredient-list.component.html
  44. 0
      projects/client/src/app/pages/ingredients/ingredient-list/ingredient-list.component.less
  45. 220
      projects/client/src/app/pages/ingredients/ingredient-list/ingredient-list.component.ts
  46. 1
      projects/client/src/app/pages/ingredients/ingredient-preview-page/ingredient-preview-page.component.html
  47. 0
      projects/client/src/app/pages/ingredients/ingredient-preview-page/ingredient-preview-page.component.less
  48. 13
      projects/client/src/app/pages/ingredients/ingredient-preview-page/ingredient-preview-page.component.ts
  49. 64
      projects/client/src/app/pages/ingredients/ingredient-release/ingredient-release.component.html
  50. 0
      projects/client/src/app/pages/ingredients/ingredient-release/ingredient-release.component.less
  51. 129
      projects/client/src/app/pages/ingredients/ingredient-release/ingredient-release.component.ts
  52. BIN
      projects/client/src/assets/diz/1/bg3.png
  53. BIN
      projects/client/src/assets/diz/1/m1-bg.png
  54. BIN
      projects/client/src/assets/diz/1/m2-bg.png
  55. BIN
      projects/client/src/assets/diz/1/m3-bg.png
  56. 57
      projects/client/src/styles.less
  57. 3
      tsconfig.json

35
README.md

@ -1,30 +1,5 @@
# CateringWebApp # CateringWebApp
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.4.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
------ ------
@ -66,3 +41,13 @@ To get more help on the Angular CLI use `ng help` or go check out the [Angular C
1. 食谱审核 列表需要 提交审核时间、提交人 字段 1. 食谱审核 列表需要 提交审核时间、提交人 字段
2. 食谱列表 需要 返回一个字段 标识当前食谱 是否是 管理系统添加的还是业务系统添加的 2. 食谱列表 需要 返回一个字段 标识当前食谱 是否是 管理系统添加的还是业务系统添加的
3. 食谱保存 菜品 报500,同样的数据 昨天都可以 3. 食谱保存 菜品 报500,同样的数据 昨天都可以
-------
# 09/29
1. 业务端大屏
2. 权限
3. 详情&编辑食谱 -> 显示类型优化 & 分析
4. 菜品分类
5. 食材批量删除

19
projects/admin/src/app/components/ingredient-status-list/ingredient-status-list.component.ts

@ -68,19 +68,19 @@ export class IngredientStatusListComponent {
{ key: "vender", title: "单位" }, { key: "vender", title: "单位" },
{ key: "modify", title: "提交审核时间" }, { key: "modify", title: "提交审核时间" },
{ key: "modify", title: "提交人" }, { key: "operate", title: "提交人" },
]); ]);
this.tableList = this.tableList.setOptions([ this.tableList = this.tableList.setOptions([
{ {
title: "详情", title: "详情",
premissions: [], premissions: [],
onClick: this.showFoodForm.bind(this), onClick: this.preview.bind(this),
}, },
{ {
title: "导出", title: "导出",
premissions: [], premissions: [],
onClick: this.showFoodForm.bind(this), onClick: this.export.bind(this),
}, },
{ {
title: "通过", title: "通过",
@ -101,6 +101,19 @@ export class IngredientStatusListComponent {
]); ]);
} }
preview({ id }: any) {
window.open(`/ingredient/preview?id=${id}`);
}
export({ id }: any) {
this.msg.loading("导出中...");
this.api.exportMenu(id).subscribe(() => {
setTimeout(() => {
this.msg.remove();
}, 1500);
});
}
fetchData(query: AnyObject, pager: AnyObject) { fetchData(query: AnyObject, pager: AnyObject) {
return this.api.getMenuStatusPage(pager, { ...query, status: this.status }).pipe( return this.api.getMenuStatusPage(pager, { ...query, status: this.status }).pipe(
tap((res) => { tap((res) => {

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

@ -88,3 +88,56 @@
</button> </button>
</nz-space> </nz-space>
</ng-template> </ng-template>
<app-print #print
[content]="printContent">
</app-print>
<ng-template #printContent>
<div class="printContent"
*ngIf="printData">
<table class="print-table">
<tbody>
<tr>
<th colspan="3">
{{printData.name}}
</th>
</tr>
<tr>
<th colspan="3">
营养成分表
</th>
</tr>
<tr>
<th>
名称
</th>
<th class="text-center">
每100克(g)
</th>
<th class="text-center">
营养参考值%(NVR%)
</th>
</tr>
</tbody>
<tbody>
<tr *ngFor="let th of printData.component">
<td [width]="'38.2%'">{{ th.name }}</td>
<td class="text-center">{{ th.nutrition }}</td>
<td class="text-center">{{ th.nvr }}</td>
</tr>
</tbody>
</table>
<div>
主要原料:{{printData.ingredients.join(',')}}
</div>
<div>
1毫克(mg)钠相当于2.5毫克食盐
</div>
</div>
</ng-template>

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

@ -18,6 +18,7 @@ import {
import { NzModalService } from "ng-zorro-antd/modal"; import { NzModalService } from "ng-zorro-antd/modal";
import { NzMessageService } from "ng-zorro-antd/message"; import { NzMessageService } from "ng-zorro-antd/message";
import { ResponseType } from "@cdk/types"; import { ResponseType } from "@cdk/types";
import { PrintComponent } from "@cdk/shared/components";
@Component({ @Component({
selector: "app-dish", selector: "app-dish",
@ -34,6 +35,8 @@ export class DishComponent {
@ViewChild("formFooterTpl") formFooterTpl!: TemplateRef<{}>; @ViewChild("formFooterTpl") formFooterTpl!: TemplateRef<{}>;
@ViewChild("print") printRef!: PrintComponent;
private drawerRef?: NzDrawerRef; private drawerRef?: NzDrawerRef;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -42,6 +45,8 @@ export class DishComponent {
public globalEnum = this.api.globalEnum; public globalEnum = this.api.globalEnum;
public printData: any | null;
public tableList = new TableListOption(this.fetchData.bind(this), { public tableList = new TableListOption(this.fetchData.bind(this), {
selectable: true, selectable: true,
frontPagination: false, frontPagination: false,
@ -114,7 +119,7 @@ export class DishComponent {
{ {
title: "打印营养标签", title: "打印营养标签",
premissions: [], premissions: [],
onClick: this.showFoodForm.bind(this), onClick: this.print.bind(this),
}, },
{ {
title: "编辑", title: "编辑",
@ -129,6 +134,17 @@ export class DishComponent {
]); ]);
} }
print(v: any) {
this.msg.loading("数据请求中,请不要刷新页面", {
nzDuration: 0,
});
this.api.getDishLabel(v.id).subscribe((res) => {
this.printData = res.body[0];
this.printRef.print();
this.msg.remove();
});
}
fetchData(query: AnyObject, pager: AnyObject) { fetchData(query: AnyObject, pager: AnyObject) {
return this.api.getDishPage(pager, query).pipe( return this.api.getDishPage(pager, query).pipe(
tap((res) => { tap((res) => {

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

@ -26,7 +26,7 @@
<button *nzSpaceItem nz-button> <button *nzSpaceItem nz-button>
导入食谱 导入食谱
</button> </button>
<button *nzSpaceItem nz-button> <button *nzSpaceItem nz-button (click)="previewMenu()">
食谱预览 食谱预览
</button> </button>
<button *nzSpaceItem nz-button nzType="primary" (click)="confirmSave()"> <button *nzSpaceItem nz-button nzType="primary" (click)="confirmSave()">

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

@ -1,12 +1,13 @@
import { Component, OnInit, ViewChild } from "@angular/core"; import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { ConfirmIngredientComponent } from "@cdk/ingredient/confirm-ingredient/confirm-ingredient.component"; import { ConfirmIngredientComponent } from "@cdk/ingredient/confirm-ingredient/confirm-ingredient.component";
import { IngredientDishComponent } from "@cdk/ingredient/ingredient-dish/ingredient-dish.component"; import { DishInterface, IngredientDishComponent } from "@cdk/ingredient/ingredient-dish/ingredient-dish.component";
import { MealDishInterface } from "@cdk/ingredient/ingredient-meals/ingredient-meals.component"; import { MealDishInterface } from "@cdk/ingredient/ingredient-meals/ingredient-meals.component";
import { ApiService } from "@cdk/services"; import { ApiService } from "@cdk/services";
import { OptionItemInterface } from "@cdk/types"; import { OptionItemInterface } from "@cdk/types";
import { NzMessageService } from "ng-zorro-antd/message"; import { NzMessageService } from "ng-zorro-antd/message";
import { NzModalService } from "ng-zorro-antd/modal"; import { NzModalService } from "ng-zorro-antd/modal";
import { forkJoin } from "rxjs";
@Component({ @Component({
selector: "app-ingredient-form", selector: "app-ingredient-form",
@ -58,12 +59,35 @@ export class IngredientFormComponent implements OnInit {
}); });
}); });
}); });
this.menuDishFormServer = res.body; this.initMenuDish(res.body);
} }
}); });
} }
} }
initMenuDish(menuDishFormServer: any[]) {
const foodIds = new Set<number>();
menuDishFormServer.forEach((i: any) => {
i.ingredient.map((food: any) => {
foodIds.add(food.key);
});
});
forkJoin([this.api.getFoodList({ keys: Array.from(foodIds) })]).subscribe(([res]) => {
this.menuDishFormServer = menuDishFormServer.map((i: any) => {
return {
...i,
dishName: i.name,
mealIndex: this.menuItem.meals.findIndex((m: string) => m === i.meal),
items: i.ingredient.map((food: any) => {
const fd = res.body.find((f) => f.key === food.key);
food.foodName = fd.name;
return food;
}),
};
});
});
}
onStepChange(basicInfo: any) { onStepChange(basicInfo: any) {
this.step = 1; this.step = 1;
this.menuItem = { this.menuItem = {
@ -90,6 +114,19 @@ export class IngredientFormComponent implements OnInit {
}); });
} }
previewMenu() {
const { mealDishList } = this.menuDish;
const snapshot = Date.now().toString();
sessionStorage.setItem(
snapshot,
JSON.stringify({
basic: this.menuItem,
dishs: this.formatData(mealDishList),
})
);
window.open(`/ingredient/preview?snapshot=${snapshot}`);
}
confirmSave() { confirmSave() {
this.modal.create({ this.modal.create({
nzTitle: "确认食谱信息", nzTitle: "确认食谱信息",
@ -99,25 +136,10 @@ export class IngredientFormComponent implements OnInit {
nzOnOk: () => { nzOnOk: () => {
const { mealDishList } = this.menuDish; const { mealDishList } = this.menuDish;
mealDishList.forEach((dish) => {
dish["dishId"] = dish.dish;
dish.items = dish.items.map((i) => {
return {
...i,
value: (i["groupValues"] as any[]).reduce((a, c) => {
return {
...a,
[c.peopleName]: c.value,
};
}, {} as Record<string, number>),
};
});
});
console.log("mealDishList", mealDishList);
this.api this.api
.saveMenuDist({ .saveMenuDist({
menuIds: this.menuItem.menuIds ?? [this.menuItem.id], menuIds: this.menuItem.menuIds ?? [this.menuItem.id],
dishes: mealDishList, dishes: this.formatData(mealDishList),
}) })
.subscribe((res) => { .subscribe((res) => {
this.msg.success(res.desc); this.msg.success(res.desc);
@ -127,36 +149,22 @@ export class IngredientFormComponent implements OnInit {
}); });
} }
formatData(menuObject: any) { formatData(d: DishInterface[]) {
let dishes: any[] = []; const data: DishInterface[] = JSON.parse(JSON.stringify(d));
Object.entries(menuObject).forEach(([day, v]) => { data.forEach((dish) => {
Object.entries(v as Record<string, MealDishInterface[]>).forEach(([mealIndex, dishList]) => { dish["dishId"] = dish.dish;
dishList.forEach((dish) => { dish.items = dish.items.map((i) => {
dishes.push({
...dish,
day: Number(day),
meal: this.menuItem.meals[mealIndex],
items: dish.foods.map((food) => {
return { return {
...food, ...i,
value: food.groupValues.reduce((a, c) => { value: (i["groupValues"] as any[]).reduce((a, c) => {
return { return {
...a, ...a,
[c.peopleName]: c.value, [c.peopleName]: c.value,
}; };
}, {} as Record<string, number>), }, {} as Record<string, number>),
}; };
}),
});
});
}); });
}); });
return data;
const toServer = {
menuIds: this.menuItem.menuIds ?? [this.menuItem.id],
dishes,
};
return toServer;
} }
} }

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

@ -2,7 +2,6 @@ import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms"; import { FormControl, FormGroup } from "@angular/forms";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer"; import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { AnyObject, OrgDTO, TableListOption } from "@cdk/public-api"; import { AnyObject, OrgDTO, TableListOption } from "@cdk/public-api";
import { DishFormComponent } from "@admin/app/components";
import { ApiService } from "@cdk/services"; import { ApiService } from "@cdk/services";
import { NzModalService } from "ng-zorro-antd/modal"; import { NzModalService } from "ng-zorro-antd/modal";
import { lastValueFrom, tap } from "rxjs"; import { lastValueFrom, tap } from "rxjs";
@ -94,7 +93,7 @@ export class IngredientListComponent {
title: "导出", title: "导出",
premissions: [], premissions: [],
onClick: this.showFoodForm.bind(this), onClick: this.export.bind(this),
}, },
{ {
title: "审核", title: "审核",
@ -174,12 +173,12 @@ export class IngredientListComponent {
window.open(`/ingredient/preview?id=${id}`); window.open(`/ingredient/preview?id=${id}`);
} }
showFoodForm(food?: any) { export({ id }: any) {
this.drawerRef = this.drawer.create({ this.msg.loading("导出中...");
nzTitle: food ? "编辑菜品" : "新增菜品", this.api.exportMenu(id).subscribe(() => {
nzWidth: 700, setTimeout(() => {
nzContent: DishFormComponent, this.msg.remove();
// nzFooter: this.foofFormFooterTpl, }, 1500);
}); });
} }

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

@ -66,15 +66,3 @@
</nz-card> </nz-card>
</div> </div>
</app-page> </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>

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

@ -22,10 +22,6 @@ export class IngredientReleaseComponent {
private msg: NzMessageService private msg: NzMessageService
) {} ) {}
@ViewChild("foofFormFooterTpl") foofFormFooterTpl!: TemplateRef<{}>;
private drawerRef?: NzDrawerRef;
public tableList = new TableListOption(this.fetchData.bind(this)); public tableList = new TableListOption(this.fetchData.bind(this));
public queryForm = new FormGroup({ public queryForm = new FormGroup({
@ -58,12 +54,12 @@ export class IngredientReleaseComponent {
{ {
title: "详情", title: "详情",
premissions: [], premissions: [],
onClick: this.showFoodForm.bind(this), onClick: this.preview.bind(this),
}, },
{ {
title: "导出食谱", title: "导出",
premissions: [], premissions: [],
onClick: this.showFoodForm.bind(this), onClick: this.export.bind(this),
}, },
{ {
@ -74,6 +70,10 @@ export class IngredientReleaseComponent {
]); ]);
} }
preview({ id }: any) {
window.open(`/ingredient/preview?id=${id}`);
}
fetchData(pager: AnyObject, query: AnyObject) { fetchData(pager: AnyObject, query: AnyObject) {
if (this.dateRange) { if (this.dateRange) {
if (this.dateRange[0]) { if (this.dateRange[0]) {
@ -129,19 +129,15 @@ export class IngredientReleaseComponent {
return [startDate, endDate]; return [startDate, endDate];
} }
showFoodForm(food?: any) { export({ id }: any) {
this.drawerRef = this.drawer.create({ this.msg.loading("导出中...");
nzTitle: food ? "编辑菜品" : "新增菜品", this.api.exportMenu(id).subscribe(() => {
nzWidth: 700, setTimeout(() => {
nzContent: DishFormComponent, this.msg.remove();
nzFooter: this.foofFormFooterTpl, }, 1500);
}); });
} }
cancelFoodForm() {
this.drawerRef?.close();
}
cancelRelease({ id }: any) { cancelRelease({ id }: any) {
this.modal.confirm({ this.modal.confirm({
nzTitle: "警告", nzTitle: "警告",

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

@ -18,7 +18,7 @@ export class HTTPInterceptor implements HttpInterceptor {
private msgFlag = false; private msgFlag = false;
private localStroageKey = "catering"; private localStroageKey = "catering_admin";
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem(this.localStroageKey); const token = localStorage.getItem(this.localStroageKey);

58
projects/admin/src/styles.less

@ -93,3 +93,61 @@ li {
} }
} }
::-webkit-scrollbar {
width: 10px;
height: 10px;
background-color: rgba(240, 240, 240, 1);
}
/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track {
box-shadow: inset 0 0 0px rgba(240, 240, 240, 0.5);
border-radius: 10px;
background-color: rgba(240, 240, 240, 0.5);
}
/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 0px rgba(240, 240, 240, 0.5);
background-color: rgba(0, 0, 0, 0.171);
}
@media print {
#root {
display: none;
}
@page {
size: auto;
width: 100vw;
margin: 5mm auto !important;
}
body {
background-color: transparent !important;
}
* {
color: #000 !important;
}
}
.print-table {
width: 100%;
margin-bottom: 12px;
border-collapse: collapse;
border-spacing: 0;
border: 1px solid #e8e8e8;
td,
th {
padding: 6px 12px;
border: 1px solid #000;
font-weight: normal;
}
}

43
projects/cdk/src/ingredient/ingredient-dish/ingredient-dish.component.ts

@ -36,6 +36,8 @@ export class IngredientDishComponent implements OnChanges {
@Input() menuBaisc: any | null; @Input() menuBaisc: any | null;
@Input() client = false;
@Input() menuDishFormServer: any | null; @Input() menuDishFormServer: any | null;
expanded = new Set<number>(); expanded = new Set<number>();
@ -67,26 +69,27 @@ export class IngredientDishComponent implements OnChanges {
} }
initMenuDish() { initMenuDish() {
const foodIds = new Set<number>(); this.mealDishList = this.menuDishFormServer;
this.menuDishFormServer.forEach((i: any) => { // const foodIds = new Set<number>();
i.ingredient.map((food: any) => { // this.menuDishFormServer.forEach((i: any) => {
foodIds.add(food.key); // i.ingredient.map((food: any) => {
}); // foodIds.add(food.key);
}); // });
forkJoin([this.api.getFoodList({ keys: Array.from(foodIds) })]).subscribe(([res]) => { // });
this.mealDishList = this.menuDishFormServer.map((i: any) => { // forkJoin([this.api.getFoodList({ keys: Array.from(foodIds) })]).subscribe(([res]) => {
return { // this.mealDishList = this.menuDishFormServer.map((i: any) => {
...i, // return {
dishName: i.name, // ...i,
mealIndex: this.menuBaisc.meals.findIndex((m: string) => m === i.meal), // dishName: i.name,
items: i.ingredient.map((food: any) => { // mealIndex: this.menuBaisc.meals.findIndex((m: string) => m === i.meal),
const fd = res.body.find((f) => f.key === food.key); // items: i.ingredient.map((food: any) => {
food.foodName = fd.name; // const fd = res.body.find((f) => f.key === food.key);
return food; // food.foodName = fd.name;
}), // return food;
}; // }),
}); // };
}); // });
// });
} }
initMenuBasic() { initMenuBasic() {

2
projects/cdk/src/ingredient/ingredient-form-basic/ingredient-form-basic.component.html

@ -29,7 +29,7 @@
</nz-select> </nz-select>
</nz-form-control> </nz-form-control>
</nz-form-item> </nz-form-item>
<nz-form-item> <nz-form-item *ngIf="!client">
<nz-form-label nzRequired nzSpan="6"> <nz-form-label nzRequired nzSpan="6">
适用单位 适用单位
</nz-form-label> </nz-form-label>

23
projects/cdk/src/ingredient/ingredient-form-basic/ingredient-form-basic.component.ts

@ -18,6 +18,8 @@ export class IngredientFormBasicComponent implements OnChanges {
@Input() menu: any | null; @Input() menu: any | null;
@Input() client: boolean = false;
@Output() onSave = new EventEmitter(); @Output() onSave = new EventEmitter();
private standardSearch$ = new Subject<{ id?: string; name?: string }>(); private standardSearch$ = new Subject<{ id?: string; name?: string }>();
@ -46,7 +48,7 @@ export class IngredientFormBasicComponent implements OnChanges {
name: this.fb.control("", [FormValidators.required()]), name: this.fb.control("", [FormValidators.required()]),
nutrient: this.fb.control("", [FormValidators.required()]), nutrient: this.fb.control("", [FormValidators.required()]),
day: this.fb.control(1, [FormValidators.required()]), day: this.fb.control(1, [FormValidators.required()]),
vendors: this.fb.control([], [FormValidators.required()]), vendors: this.fb.control([], this.client ? [] : [FormValidators.required()]),
month: this.fb.control([], [FormValidators.required()]), month: this.fb.control([], [FormValidators.required()]),
}); });
this.standardSearch$ this.standardSearch$
@ -64,7 +66,7 @@ export class IngredientFormBasicComponent implements OnChanges {
if (getById) { if (getById) {
data.body = [data.body]; data.body = [data.body];
} }
data.body.forEach((item) => { data.body.forEach((item: any) => {
listOfOption.push({ listOfOption.push({
label: item.name, label: item.name,
value: item.id, value: item.id,
@ -113,13 +115,6 @@ export class IngredientFormBasicComponent implements OnChanges {
onStandardChange(v: any) { onStandardChange(v: any) {
const currentStandard = this.searchedStandard.find((f) => f.value === v); const currentStandard = this.searchedStandard.find((f) => f.value === v);
if (currentStandard) { if (currentStandard) {
this.api.getOrgList({ vendors: currentStandard["vendors"] }).subscribe((res) => {
this.currentOrgs = res.body.map((i) => ({
...i,
value: String(i.id),
label: i.name,
}));
});
this.currentPeoples = Object.entries(currentStandard["ingredient"] ?? {}).map(([k, v]) => { this.currentPeoples = Object.entries(currentStandard["ingredient"] ?? {}).map(([k, v]) => {
return { return {
label: k, label: k,
@ -128,6 +123,16 @@ export class IngredientFormBasicComponent implements OnChanges {
...(v as any), ...(v as any),
}; };
}); });
if (this.client) {
return;
}
this.api.getOrgList({ vendors: currentStandard["vendors"] }).subscribe((res) => {
this.currentOrgs = res.body.map((i) => ({
...i,
value: String(i.id),
label: i.name,
}));
});
} }
} }

2
projects/cdk/src/ingredient/ingredient-meals/ingredient-meals.component.html

@ -103,7 +103,7 @@
本餐生重总量 本餐生重总量
</td> </td>
<td *ngFor="let p of peopleGroups"> <td *ngFor="let p of peopleGroups">
{{totalObj[p]}} {{totalObj[p]?.toFixed(2)}}
</td> </td>
</tr> </tr>

14
projects/cdk/src/ingredient/ingredient-meals/ingredient-meals.component.ts

@ -54,7 +54,7 @@ export class IngredientMealsComponent implements OnChanges, OnInit {
// currentDishs: DishInterface[] = []; // currentDishs: DishInterface[] = [];
totalObj: Record<string, string> = {}; totalObj: Record<string, number> = {};
ngOnInit(): void { ngOnInit(): void {
this.init(); this.init();
@ -101,16 +101,6 @@ export class IngredientMealsComponent implements OnChanges, OnInit {
}); });
} }
getTotal(dishId: number, people: string) {
const r = this.mealDishs.filter(
(f) => f.day === this.day && f["mealIndex"] === this.mealIndex && f.dish === dishId
);
r.forEach((dish) => {
dish.items;
});
return "ad";
}
clearThisMeal() { clearThisMeal() {
this.modal.confirm({ this.modal.confirm({
nzTitle: "警告", nzTitle: "警告",
@ -119,6 +109,8 @@ export class IngredientMealsComponent implements OnChanges, OnInit {
nzOnOk: () => { nzOnOk: () => {
this.mealDishs = this.mealDishs.filter((f) => !(f.day === this.day && f["mealIndex"] === this.mealIndex)); this.mealDishs = this.mealDishs.filter((f) => !(f.day === this.day && f["mealIndex"] === this.mealIndex));
this.onSaveDish.emit(this.mealDishs); this.onSaveDish.emit(this.mealDishs);
this.calcTotal();
return true;
}, },
}); });
} }

180
projects/cdk/src/ingredient/ingredient-preview/ingredient-preview.component.html

@ -1,49 +1,61 @@
<header class="header text-center text-base"> <nz-spin [nzSpinning]="!basic">
食谱名称 <div *ngIf="!basic" class="h-60"></div>
</header> <ng-container *ngIf="basic">
<header class="header text-center text-base relative">
<button nz-button routerLink="/ingredient/item/list" class=" absolute left-4 top-[6px]">
<span nz-icon nzType="ordered-list" nzTheme="outline"></span>
</button>
<span>
{{basic.name}}
</span>
</header>
<div class="p-4"> <div class="p-4">
<nz-descriptions [nzColumn]="4"> <nz-descriptions>
<nz-descriptions-item nzTitle="采用标准">Zhou Maomao</nz-descriptions-item> <nz-descriptions-item nzTitle="采用标准">{{basic.standardName ?? '-'}}</nz-descriptions-item>
<nz-descriptions-item nzTitle="食谱周期">18100000000</nz-descriptions-item> <nz-descriptions-item nzTitle="食谱周期">{{basic.day}}天</nz-descriptions-item>
<nz-descriptions-item nzTitle="适用月份">Hangzhou, Zhejiang</nz-descriptions-item> <nz-descriptions-item nzTitle="适用月份">
<nz-descriptions-item nzTitle="人群显示">Empty</nz-descriptions-item> <div class="flex flex-wrap" *ngIf="basic.month as data">
<ng-container *ngIf="data.length === 12">
<nz-tag>
全年
</nz-tag>
</ng-container>
<ng-container *ngIf="data.length !== 12">
<nz-tag *ngFor="let item of data" class="mb-1">
{{monthText[item]}}
</nz-tag>
</ng-container>
</div>
</nz-descriptions-item>
<nz-descriptions-item nzTitle="人群显示">
<nz-tag *ngFor="let p of basic.crows" class="mb-1">
{{p}}
</nz-tag>
</nz-descriptions-item>
<nz-descriptions-item nzTitle="餐次">
<nz-tag *ngFor="let c of basic.meals" class="mb-1">
{{c}}
</nz-tag>
</nz-descriptions-item>
</nz-descriptions> </nz-descriptions>
</div> </div>
</ng-container>
<div class="p-4"> </nz-spin>
<nz-spin [nzSpinning]="!dishs.length && !basic">
<div class="p-4 w-full overflow-auto" *ngIf="dishs.length && basic">
<table class="menu-table"> <table class="menu-table">
<thead> <thead>
<tr> <tr>
<th width="100px">
</th>
<th> <th>
<div> <div class="w-24">
<div class="table-day-row">
<div class="td"> 第1天 </div>
<div class="td"> 重量/克 </div>
</div>
<div class="table-menu-row flex ">
<div class="td dish-name text-left">
菜品
</div>
<div class="td food-name text-left">
食材
</div>
<div class="table-herder-ages">
<div class="td age-range"> 18-49岁男 </div>
<div class="td age-range"> 18-49岁男 </div>
<div class="td age-range"> 18-49岁男 </div>
</div>
</div>
</div> </div>
</th> </th>
<th> <th *ngFor="let day of days">
<div> <div>
<div class="table-day-row"> <div class="table-day-row">
<div class="td">2</div> <div class="td"> 第{{day}}天 </div>
<div class="td"> 重量/克 </div> <div class="td"> 重量/克 </div>
</div> </div>
<div class="table-menu-row flex "> <div class="table-menu-row flex ">
@ -54,80 +66,94 @@
食材 食材
</div> </div>
<div class="table-herder-ages"> <div class="table-herder-ages">
<div class="td age-range"> 18-49岁男 </div> <div class="td age-range" *ngFor="let people of basic.crows">{{people}}</div>
<div class="td age-range"> 18-49岁男 </div>
<div class="td age-range"> 18-49岁男 </div>
</div> </div>
</div> </div>
</div> </div>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<tr *ngFor="let meal of basic.meals">
<td class="text-center"> <td class="text-center">
<b> <b>
早餐 {{meal}}
</b> </b>
</td> </td>
<td> <td *ngFor="let day of days">
<div class="flex"> <div class="meal">
<ng-container *ngFor="let d of dishs">
<ng-container *ngIf="d.day === day ">
<div class="flex" *ngIf="d.meal === meal;">
<div class="menu-dish dish-name"> <div class="menu-dish dish-name">
<div class="text-left td">烂肉泡菜L</div> <div class="text-left td">
<b>
{{d['dishName']}}
</b>
</div>
</div> </div>
<div class="menu-food"> <div class="menu-food">
<ul> <ul>
<li class="flex"> <ng-container *ngFor="let f of d.items;let last = last">
<li class="flex food-item">
<div class="td food-name text-left"> <div class="td food-name text-left">
酸白菜[酸菜] {{f['foodName']}}
</div>
<div class="td age-range text-right">
1
</div>
<div class="td age-range text-right">
2
</div> </div>
<div class="td age-range text-right"> <div
3 class="td age-range text-right"
*ngFor="let p of basic.crows">
{{f.value[p].toFixed(2)}}
</div> </div>
</li> </li>
<li class="flex"> <!-- <li class="flex food-item placeholder" *ngIf="last">
<div class="td food-name text-left"> <div class="td food-name text-left">
酸白菜[酸菜] &nbsp;
</div> </div>
<div class="td age-range text-right"> <div
1 class="td age-range text-right"
*ngFor="let p of basic.crows">
&nbsp;
</div> </div>
<div class="td age-range text-right"> </li> -->
2 </ng-container>
</div>
<div class="td age-range text-right">
3 </ul>
</div>
</li>
<li class="flex">
<div class="td food-name text-left">
酸白菜[酸菜]
</div> </div>
<div class="td age-range text-right">
1
</div> </div>
<div class="td age-range text-right"> </ng-container>
2 </ng-container>
<div class="flex total">
<div class="menu-dish dish-name">
<div class="text-left td">本餐生重总量</div>
</div> </div>
<div class="td age-range text-right"> <div class="menu-food">
3 <ul>
<li class="flex ">
<div class="td food-name text-left">&nbsp;</div>
<div class="td age-range text-right" *ngFor="let p of basic.crows">
{{calcTotal(day,meal,p)}}
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</td>
<td>
</div>
</td> </td>
</tr> </tr>
<!-- <ng-container *ngFor="let dish of dishs">
<ng-container *ngIf="dish.meal === '早餐'">
</ng-container>
</ng-container> -->
</tbody> </tbody>
</table> </table>
</div> </div>
</nz-spin>

38
projects/cdk/src/ingredient/ingredient-preview/ingredient-preview.component.less

@ -12,6 +12,21 @@
border-spacing: 0; border-spacing: 0;
border: 1px solid #e8e8e8; border: 1px solid #e8e8e8;
.meal {
position: relative;
min-height: 200px;
padding-bottom: 42px;
.total {
position: absolute;
bottom: 0;
.td {
background-color: #fffbe6 !important;
}
}
}
thead { thead {
.table-day-row { .table-day-row {
display: flex; display: flex;
@ -50,6 +65,29 @@
} }
} }
tbody {
td {
vertical-align: top;
border: 1px solid #e8e8e8;
}
}
.food-item {
&:not(.placeholder) {
&:hover .td {
background-color: #a8b0c238;
}
}
}
// .menu-food {
// ul li {
// &:last-child {
// margin-bottom: 42px;
// }
// }
// }
.table-herder-ages { .table-herder-ages {
display: flex; display: flex;

125
projects/cdk/src/ingredient/ingredient-preview/ingredient-preview.component.ts

@ -1,8 +1,131 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from "@cdk/services";
import { NzMessageService } from "ng-zorro-antd/message";
import { DishInterface } from "../ingredient-dish/ingredient-dish.component";
import { forkJoin, map } from "rxjs";
@Component({ @Component({
selector: "app-ingredient-preview", selector: "app-ingredient-preview",
templateUrl: "./ingredient-preview.component.html", templateUrl: "./ingredient-preview.component.html",
styleUrls: ["./ingredient-preview.component.less"], styleUrls: ["./ingredient-preview.component.less"],
}) })
export class IngredientPreviewComponent {} export class IngredientPreviewComponent {
constructor(
private route: ActivatedRoute,
private router: Router,
private api: ApiService,
private msg: NzMessageService
) {}
basic: any | null;
dishs: DishInterface[] = [];
totalObj: Record<string, string> | null = null;
monthText = {
1: "一月",
2: "二月",
3: "三月",
4: "四月",
5: "五月",
6: "六月",
7: "七月",
8: "八月",
9: "九月",
10: "十月",
11: "十一月",
12: "十二月",
} as any;
days: number[] = [];
ngOnInit(): void {
const id = this.route.snapshot.queryParamMap.get("id");
const snapshot = this.route.snapshot.queryParamMap.get("snapshot");
const storage = snapshot ? sessionStorage.getItem(snapshot) : null;
if (id) {
forkJoin([this.api.getMenuDist(id), this.api.getMenuItem(id)])
.pipe(
map(([d, b]) => {
return [d.body, b.body];
})
)
.subscribe(([dishs, basic]) => {
this.days = Array.from({ length: basic.day }, (_, i) => i + 1);
this.getStandardName(basic.nutrient);
this.basic = basic;
this.initMenuDish(dishs);
});
return;
}
if (storage) {
try {
const { basic, dishs } = JSON.parse(storage);
this.days = Array.from({ length: basic.day }, (_, i) => i + 1);
this.getStandardName(basic.nutrient);
this.basic = basic;
this.dishs = dishs;
} catch (error) {
this.msg.error("解析食谱数据出错了,请重试!");
this.router.navigate(["/ingredient/item/list"]);
}
return;
}
this.msg.error("没有找到食谱数据");
this.router.navigate(["/ingredient/item/list"]);
}
initMenuDish(menuDishFormServer: any[]) {
const foodIds = new Set<number>();
menuDishFormServer.forEach((i: any) => {
i.ingredient.map((food: any) => {
// 收集 食材 key 获取 食材名称 & 把 value 对象转换成 groupValues 数组
foodIds.add(food.key);
food["groupValues"] = Object.entries(food.value).map(([peopleName, value]) => {
return {
peopleName,
value,
};
});
});
});
forkJoin([this.api.getFoodList({ keys: Array.from(foodIds) })]).subscribe(([res]) => {
this.dishs = menuDishFormServer.map((i: any) => {
return {
...i,
dishName: i.name,
mealIndex: this.basic.meals.findIndex((m: string) => m === i.meal),
items: i.ingredient.map((food: any) => {
const fd = res.body.find((f) => f.key === food.key);
food.foodName = fd.name;
return food;
}),
};
});
});
}
getStandardName(id: string) {
this.api.getStandard({ id }).subscribe((res) => {
this.basic["standardName"] = res.body.name;
});
}
calcTotal(day: number, meal: string, people: string) {
let total = 0;
this.dishs.forEach((d) => {
if (d.day === day && d.meal === meal) {
d.items.forEach((food) => {
total += food.value[people];
});
}
});
return total.toFixed(2);
}
}

30
projects/cdk/src/services/api.service.ts

@ -230,10 +230,10 @@ export class ApiService {
); );
} }
downLoadFile(response: HttpResponse<Object>) { downLoadFile(response: HttpResponse<Object>, defaultName = `${Date.now()}.xlsx`) {
const fileNameFromHeader = response.headers.get("Content-Disposition"); const fileNameFromHeader = response.headers.get("Content-Disposition");
if (fileNameFromHeader) { if (fileNameFromHeader) {
const fileName = fileNameFromHeader.trim()?.split("''")?.[1]?.replace(/"/g, "") ?? `模板_${Date.now()}.xlsx`; const fileName = fileNameFromHeader.trim()?.split("''")?.[1]?.replace(/"/g, "") ?? defaultName;
const blob = new Blob([response.body as any]); const blob = new Blob([response.body as any]);
const downloadLink = document.createElement("a"); const downloadLink = document.createElement("a");
downloadLink.href = URL.createObjectURL(blob); downloadLink.href = URL.createObjectURL(blob);
@ -303,7 +303,7 @@ export class ApiService {
getStandard(q: { id?: string; name?: string }) { getStandard(q: { id?: string; name?: string }) {
const query = Utils.objectStringify(q); const query = Utils.objectStringify(q);
return this.http.get<ResponseType<any[]>>(`/api/nutrition/select?${query}`); return this.http.get<ResponseType<any>>(`/api/nutrition/select?${query}`);
} }
saveStandard(v: AnyObject, isEdit?: boolean) { saveStandard(v: AnyObject, isEdit?: boolean) {
@ -340,10 +340,11 @@ export class ApiService {
); );
} }
getDishLabel(id: (string | number)[]) { getDishLabel(ids: (string | number)[]) {
const query = Utils.objectStringify({ id }); const query = Utils.objectStringify({ ids });
return this.http.get<ResponseType<any[]>>(`/api/dish?${query}`); return this.http.get<ResponseType<any[]>>(`/api/dish/label?${query}`);
} }
saveDish(v: AnyObject) { saveDish(v: AnyObject) {
const body = Utils.objectToFormData(v); const body = Utils.objectToFormData(v);
const method = v["id"] ? "post" : "put"; const method = v["id"] ? "post" : "put";
@ -420,6 +421,10 @@ export class ApiService {
return this.http.get<ResponseType>(`/api/menu/dish?menuId=${menuId}`); return this.http.get<ResponseType>(`/api/menu/dish?menuId=${menuId}`);
} }
getMenuDataVis() {
return this.http.get<ResponseType>(`/api/menu/dish`);
}
saveMenuDist(d: {}) { saveMenuDist(d: {}) {
return this.http.put<ResponseType>(`/api/menu/dish/batch`, d); return this.http.put<ResponseType>(`/api/menu/dish/batch`, d);
} }
@ -438,4 +443,17 @@ export class ApiService {
}) })
); );
} }
exportMenu(id: number | string) {
return this.http
.get<ResponseType>(`/api/menu/dish/export?id=${id}`, {
observe: "response",
responseType: "blob" as "json",
})
.pipe(
tap((res) => {
this.downLoadFile(res);
})
);
}
} }

1
projects/cdk/src/shared/components/index.ts

@ -2,3 +2,4 @@ export * from "./search-and-select/search-and-select.component";
export * from "./month-select/month-select.component"; export * from "./month-select/month-select.component";
export * from "./org-select/org-select.component"; export * from "./org-select/org-select.component";
export * from "./dish-select/dish-select.component"; export * from "./dish-select/dish-select.component";
export * from "./print/print.component";

3
projects/cdk/src/shared/components/print/print.component.html

@ -0,0 +1,3 @@
<iframe #iframe
style="display: none;">
</iframe>

0
projects/cdk/src/shared/components/print/print.component.less

92
projects/cdk/src/shared/components/print/print.component.ts

@ -0,0 +1,92 @@
import { DomPortalOutlet, PortalOutlet, TemplatePortal } from "@angular/cdk/portal";
import {
ApplicationRef,
Component,
ComponentFactoryResolver,
ElementRef,
Injector,
Input,
OnInit,
TemplateRef,
ViewChild,
ViewContainerRef,
} from "@angular/core";
@Component({
selector: "app-print",
templateUrl: "./print.component.html",
styleUrls: ["./print.component.less"],
})
export class PrintComponent implements OnInit {
@Input() content!: TemplateRef<any>;
@ViewChild("iframe") iframe!: ElementRef<HTMLIFrameElement>;
private portalHost?: PortalOutlet;
public printed: boolean = false;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
private appRef: ApplicationRef,
private viewContainerRef: ViewContainerRef
) {}
ngOnInit(): void {}
testCall() {
alert("testCall");
}
private _attachStyles(targetWindow: Window): void {
// Copy styles from parent window
document.querySelectorAll("style").forEach((htmlElement) => {
targetWindow.document.head.appendChild(htmlElement.cloneNode(true));
});
// Copy stylesheet link from parent window
const styleSheetElement = this._getStyleSheetElement();
targetWindow.document.head.appendChild(styleSheetElement);
}
private _getStyleSheetElement() {
const styleSheetElement = document.createElement("link");
document.querySelectorAll("link").forEach((htmlElement) => {
if (htmlElement.rel === "stylesheet") {
const absoluteUrl = new URL(htmlElement.href).href;
styleSheetElement.rel = "stylesheet";
styleSheetElement.type = "text/css";
styleSheetElement.href = absoluteUrl;
}
});
return styleSheetElement;
}
print(): void {
const iframe = this.iframe.nativeElement;
const { contentDocument, contentWindow } = iframe;
if (contentDocument && contentWindow) {
this.portalHost = new DomPortalOutlet(
contentDocument.body,
this.componentFactoryResolver,
this.appRef,
this.injector
);
const portal = new TemplatePortal(this.content, this.viewContainerRef);
if (!this.printed) {
this._attachStyles(contentWindow);
}
this.portalHost.attach(portal);
window.onafterprint = () => {
// Chrome 不会触发
contentDocument.body.innerHTML = "";
};
setTimeout(() => {
contentWindow.print();
contentDocument.body.innerHTML = "";
}, 500);
}
}
}

10
projects/cdk/src/shared/shared.module.ts

@ -20,7 +20,13 @@ import {
// import { environment } from "@manage/environments/environment"; // import { environment } from "@manage/environments/environment";
import { NgxPermissionsModule } from "ngx-permissions"; import { NgxPermissionsModule } from "ngx-permissions";
import { AppPageComponent } from "@cdk/app-page/app-page.component"; import { AppPageComponent } from "@cdk/app-page/app-page.component";
import { SearchAndSelectComponent, MonthSelectComponent, OrgSelectComponent, DishSelectComponent } from "./components"; import {
SearchAndSelectComponent,
MonthSelectComponent,
OrgSelectComponent,
DishSelectComponent,
PrintComponent,
} from "./components";
const ngModules = [CommonModule, HttpClientModule, FormsModule, RouterModule, ReactiveFormsModule]; const ngModules = [CommonModule, HttpClientModule, FormsModule, RouterModule, ReactiveFormsModule];
const components: any = []; const components: any = [];
@ -43,6 +49,7 @@ const cdks = [
MonthSelectComponent, MonthSelectComponent,
DishSelectComponent, DishSelectComponent,
OrgSelectComponent, OrgSelectComponent,
PrintComponent,
], ],
imports: [...ngZorroModules, ...ngModules, ...cdks, AppPageComponent], imports: [...ngZorroModules, ...ngModules, ...cdks, AppPageComponent],
exports: [ exports: [
@ -56,6 +63,7 @@ const cdks = [
MonthSelectComponent, MonthSelectComponent,
OrgSelectComponent, OrgSelectComponent,
DishSelectComponent, DishSelectComponent,
PrintComponent,
], ],
}) })
export class SharedModule {} export class SharedModule {}

45
projects/client/src/app/app-routing.module.ts

@ -9,6 +9,10 @@ import {
MealSettingComponent, MealSettingComponent,
OrgInfoComponent, OrgInfoComponent,
ClientUserManageComponent, ClientUserManageComponent,
IngredientFormComponent,
IngredientListComponent,
IngredientPreviewPageComponent,
IngredientReleaseComponent,
} from "./pages"; } from "./pages";
import { AppLayoutComponent } from "./components"; import { AppLayoutComponent } from "./components";
import { authGuard } from "./services/auth.guard"; import { authGuard } from "./services/auth.guard";
@ -45,6 +49,47 @@ const routes: Routes = [
path: "dish", path: "dish",
component: DishComponent, component: DishComponent,
}, },
{
path: "ingredient",
title: "食谱管理",
children: [
{
path: "",
pathMatch: "full",
redirectTo: "item",
},
{
path: "item",
title: "食谱库",
children: [
{
path: "",
pathMatch: "full",
redirectTo: "list",
},
{
path: "list",
component: IngredientListComponent,
},
{
path: "form/:id",
component: IngredientFormComponent,
},
],
},
{
path: "preview",
title: "食谱预览",
component: IngredientPreviewPageComponent,
},
{
path: "release",
title: "食谱发布计划",
component: IngredientReleaseComponent,
},
],
},
{ {
path: "system", path: "system",
children: [ children: [

19
projects/client/src/app/app.module.ts

@ -13,7 +13,13 @@ import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component"; import { AppComponent } from "./app.component";
import { IconsProviderModule, PROJECT_NAME, TableListModule } from "@cdk/public-api"; import { IconsProviderModule, PROJECT_NAME, TableListModule } from "@cdk/public-api";
import { SharedModule } from "@cdk/shared/shared.module"; import { SharedModule } from "@cdk/shared/shared.module";
import { AppLayoutComponent, OrgFormComponent, UserListComponent, RolePermissionComponent } from "./components"; import {
AppLayoutComponent,
OrgFormComponent,
UserListComponent,
RolePermissionComponent,
DishFormComponent,
} from "./components";
import { import {
DashboardComponent, DashboardComponent,
LoginComponent, LoginComponent,
@ -23,8 +29,13 @@ import {
DishComponent, DishComponent,
OrgInfoComponent, OrgInfoComponent,
ClientUserManageComponent, ClientUserManageComponent,
IngredientFormComponent,
IngredientListComponent,
IngredientPreviewPageComponent,
IngredientReleaseComponent,
} from "./pages"; } from "./pages";
import { HTTPInterceptor } from "./services/http.interceptor"; import { HTTPInterceptor } from "./services/http.interceptor";
import { IngredientModule } from "@cdk/ingredient/ingredient.module";
registerLocaleData(zh); registerLocaleData(zh);
@ -35,6 +46,7 @@ registerLocaleData(zh);
OrgFormComponent, OrgFormComponent,
UserListComponent, UserListComponent,
RolePermissionComponent, RolePermissionComponent,
DishFormComponent,
DashboardComponent, DashboardComponent,
LoginComponent, LoginComponent,
@ -44,6 +56,10 @@ registerLocaleData(zh);
DishComponent, DishComponent,
OrgInfoComponent, OrgInfoComponent,
ClientUserManageComponent, ClientUserManageComponent,
IngredientFormComponent,
IngredientListComponent,
IngredientPreviewPageComponent,
IngredientReleaseComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -53,6 +69,7 @@ registerLocaleData(zh);
BrowserAnimationsModule, BrowserAnimationsModule,
IconsProviderModule, IconsProviderModule,
SharedModule, SharedModule,
IngredientModule,
TableListModule, TableListModule,
], ],
providers: [ providers: [

20
projects/client/src/app/components/app-layout/app-layout.component.html

@ -1,4 +1,9 @@
<nz-layout class="app-layout"> <ng-container *ngIf="fullPage else layoutTpl">
<router-outlet></router-outlet>
</ng-container>
<ng-template #layoutTpl>
<nz-layout class="app-layout">
<nz-header class="app-header"> <nz-header class="app-header">
<div class="flex items-center justify-between h-full"> <div class="flex items-center justify-between h-full">
<div class="logo flex items-center h-full"> <div class="logo flex items-center h-full">
@ -34,16 +39,16 @@
<span nz-icon nzType="question-circle" nzTheme="outline"></span> <span nz-icon nzType="question-circle" nzTheme="outline"></span>
<span>使用流程</span> <span>使用流程</span>
</li> </li>
<li nz-menu-item class="k-icon" [routerLink]="['/','data-vis']" nzMatchRouter> <li nz-menu-item class="k-icon" (click)="openDataVis()" nzMatchRouter>
<span nz-icon nzType="fund" nzTheme="outline"></span> <span nz-icon nzType="fund" nzTheme="outline"></span>
<span>大屏显示</span> <span>大屏显示</span>
</li> </li>
<li nz-menu-item class="k-icon" [routerLink]="['/','meal-setting']" nzMatchRouter> <li nz-menu-item [routerLink]="['/','meal-setting']" nzMatchRouter>
<span nz-icon nzType="k-icon:food" nzTheme="outline"></span> <span nz-icon nzType="setting" nzTheme="outline"></span>
<span>配餐设置</span> <span>配餐设置</span>
</li> </li>
<li nz-menu-item class="k-icon" [routerLink]="['/','food']" nzMatchRouter> <li nz-menu-item class="k-icon" [routerLink]="['/','food']" nzMatchRouter>
<span nz-icon nzType="k-icon:food" nzTheme="outline"></span> <span nz-icon nzType="k-icon:carrot" nzTheme="outline"></span>
<span>食材管理</span> <span>食材管理</span>
</li> </li>
<li nz-menu-item class="k-icon" [routerLink]="['/','dish']" nzMatchRouter> <li nz-menu-item class="k-icon" [routerLink]="['/','dish']" nzMatchRouter>
@ -59,7 +64,7 @@
</ul> </ul>
</li> </li>
<li nz-submenu nzTitle="基础信息设置" nzIcon="setting" [nzOpen]="currentUrl.includes('/system/')"> <li nz-submenu nzTitle="基础信息设置" nzIcon="user" [nzOpen]="currentUrl.includes('/system/')">
<ul> <ul>
<li nz-menu-item nzMatchRouter [routerLink]="['/','system','org']">单位信息设置</li> <li nz-menu-item nzMatchRouter [routerLink]="['/','system','org']">单位信息设置</li>
</ul> </ul>
@ -73,4 +78,5 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
</nz-layout> </nz-layout>
</nz-layout> </nz-layout>
</nz-layout> </nz-layout>
</ng-template>

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

@ -25,17 +25,21 @@ export class AppLayoutComponent implements OnInit {
) )
.subscribe((e) => { .subscribe((e) => {
this.currentUrl = e.url; this.currentUrl = e.url;
this.fullPage = ["/ingredient/preview", "/data-vis"].some((s) => e.url.startsWith(s));
}); });
} }
fullPage = false;
account = this.api.account; account = this.api.account;
unSubscribe$ = new Subject<void>(); unSubscribe$ = new Subject<void>();
currentUrl: string = ""; currentUrl: string = "";
ngOnInit(): void { ngOnInit(): void {}
console.log("this.account", this.account);
openDataVis() {
window.open("/data-vis");
} }
logout() { logout() {

136
projects/client/src/app/components/dish-form/dish-form.component.html

@ -0,0 +1,136 @@
<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="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="mark"
nzPlaceHolder="请选择菜品标签">
<nz-option *ngFor="let item of globalEnum.mark" [nzValue]="item.value" [nzLabel]="item.key"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired>
适用月份
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<app-month-select formControlName="month"></app-month-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label>
菜品图片
</nz-form-label>
<nz-form-control [nzErrorTip]="formControlErrorTpl">
<input type="hidden" formControlName="icon" />
<div class="mb-2" *ngIf="icon">
<img [src]="icon" class="h-20 w-20" />
</div>
<button class="upload-btn " nz-button [nzLoading]="uploadLoading">
<i nz-icon nzType="upload"></i>
上传图片
<input type="file" (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">
<nz-select
[nzMode]="'multiple'"
nzShowSearch
nzServerSearch
nzPlaceHolder="请输入食材名称检索"
[nzShowArrow]="false"
[nzFilterOption]="nzFilterOption"
[(ngModel)]="foodSelected"
(ngModelChange)="onFoodSelected($event)"
(nzOnSearch)="searchFood($event)"
[ngModelOptions]="{standalone: true}">
<nz-option *ngFor="let o of foodListOfOption" [nzLabel]="o.text" [nzValue]="o.value"></nz-option>
</nz-select>
<!-- <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 class="mt-4">
<li class="mb-2" *ngFor="let f of foodItemSelected;let i = index">
<div class="flex items-center">
<!-- <div class="w-1/2 pr-2">
<button nz-button nzBlock>
{{f.value}}-{{f.text}}:
</button>
</div> -->
<div class="flex-1 pr-2">
<nz-input-group nzAddOnBefore="{{f.value}}-{{f.text}}:" [nzAddOnAfter]="'g'" class="w-full">
<input
nz-input
type="number"
[(ngModel)]="f.num"
[ngModelOptions]="{standalone: true}"
placeholder="请输入{{f.value}}-{{f.text}}重量" />
</nz-input-group>
</div>
<div class="pl-2">
<nz-switch [ngModel]="f.isMain" (ngModelChange)="onMainChange($event,f.value)"
[ngModelOptions]="{standalone: true}">
</nz-switch>
是否主料
</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/client/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%;
}
}
}

230
projects/client/src/app/components/dish-form/dish-form.component.ts

@ -0,0 +1,230 @@
import { Component, Input, OnInit } from "@angular/core";
import { FormArray, FormBuilder, FormGroup } from "@angular/forms";
import { ApiService } from "@cdk/services";
import { Utils } from "@cdk/utils";
import { FormValidators } from "@cdk/validators";
import { NzMessageService } from "ng-zorro-antd/message";
import { Subject, debounceTime, distinctUntilChanged, filter, finalize, switchMap, throttleTime } 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, private api: ApiService) {}
@Input() data: any;
@Input() orgs: any[] = [];
@Input() foods: any[] = [];
private orgSearch$ = new Subject<Record<string, string>>();
private foodSearch$ = new Subject<Record<string, string>>();
formGroup!: FormGroup;
selectedValue = null;
orgListOfOption: Array<{ value: string; text: string }> = [];
foodListOfOption: Array<{ value: string; text: string }> = [];
searchedFood: Array<{ value: string; text: string }> = [];
foodSelected: string[] = [];
foodItemSelected: any[] = [];
nzFilterOption = (): boolean => true;
globalEnum = this.api.globalEnum;
uploadLoading = false;
addFoodVisible = false;
get food(): FormArray {
return this.formGroup.get("ingredient") as FormArray;
}
get icon() {
return this.formGroup.get("icon")?.value;
}
ngOnInit(): void {
this.formGroup = this.fb.group({
id: this.fb.control("", []),
name: this.fb.control("", [FormValidators.required()]),
icon: this.fb.control("", []),
mark: this.fb.control("", [FormValidators.required()]),
month: this.fb.control([], []),
});
this.orgSearch$
.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap((q) => this.api.getOrgList(q))
)
.subscribe((data) => {
const listOfOption: Array<{ value: string; text: string }> = [];
data.body.forEach((item) => {
listOfOption.push({
value: item.id.toString(),
text: item.name,
});
});
this.orgListOfOption = listOfOption;
});
this.foodSearch$
.pipe(
filter((f) => !!f),
debounceTime(500),
distinctUntilChanged(),
switchMap((q) => this.api.getFoodList(q))
)
.subscribe((data) => {
const listOfOption: Array<{ value: string; text: string }> = [];
data.body.forEach((item) => {
listOfOption.push({
value: item.key,
text: item.name,
});
});
this.searchedFood = this.searchedFood.concat(listOfOption);
this.foodListOfOption = listOfOption;
});
this.setValues();
}
setValues() {
this.orgListOfOption = this.orgs.map((i) => ({ text: i.name, value: i.id }));
if (this.data) {
// this.allMonth = this.allMonth.map((i) =>
// (this.data.month ?? []).includes(i.value) ? { ...i, checked: true } : i
// );
this.foods.forEach((f) => {
const item = { text: f.name, value: f.key };
this.foodListOfOption.push(item);
this.searchedFood.push(item);
const num = this.data.ingredient.find((i: any) => i.key === f.key);
if (num) {
this.foodItemSelected.push({ num: num.value, ...item, isMain: num.isMain });
this.foodSelected.push(f.key);
}
});
this.formGroup.patchValue({
...this.data,
mark: this.data.marks,
});
}
}
public getValues() {
let values = null;
console.log("this.formGroup.getRawValue()", this.formGroup.getRawValue(), this.foodItemSelected);
if (Utils.validateFormGroup(this.formGroup)) {
const value = this.formGroup.getRawValue();
// const { _nutrition, key, name, type } = this.formGroup.getRawValue();
let ingredient: any[] = [];
for (const f of this.foodItemSelected) {
let num = Number(f.num);
if (!num) {
this.msg.error(`请输入${f.value}-${f.text}的重量`);
return;
}
ingredient.push({
isMain: f.isMain,
key: f.value,
value: num,
});
}
const month = value.month.join(",");
values = {
...value,
month,
ingredient,
};
}
return values;
}
onMainChange(e: boolean, key: string) {
this.foodItemSelected.forEach((i) => {
if (e) {
i.isMain = false;
if (i.value === key) {
i.isMain = true;
}
} else {
if (i.value === key) {
i.isMain = false;
}
}
});
}
searchOrg(value: string): void {
if (value) {
this.orgSearch$.next({ keyword: value });
}
}
searchFood(value: string): void {
if (value) {
this.foodSearch$.next({ keyword: value });
}
}
onFoodSelected(v: string[]) {
this.foodItemSelected = [];
this.searchedFood.forEach((item) => {
if (this.foodItemSelected.some((s) => s.value === item.value)) {
return;
}
if (v.includes(item.value)) {
this.foodItemSelected.push(item);
}
});
// this.foodItemSelected = this.searchedFood.filter((f) => {
// return v.includes(f.value)
// });
}
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);
}
onFileChange(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files![0];
target.value = "";
if (file.size / 1024 / 1024 >= 5) {
this.msg.error("图片大小不能超过5M");
return;
}
const fileReader = new FileReader();
fileReader.onload = () => {
const base64 = fileReader.result as string;
this.formGroup.get("icon")?.setValue(base64);
};
fileReader.readAsDataURL(file);
}
}

1
projects/client/src/app/components/index.ts

@ -2,3 +2,4 @@ export * from "./app-layout/app-layout.component";
export * from "./org-form/org-form.component"; export * from "./org-form/org-form.component";
export * from "./user-list/user-list.component"; export * from "./user-list/user-list.component";
export * from "./role-permission/role-permission.component"; export * from "./role-permission/role-permission.component";
export * from "./dish-form/dish-form.component";

41
projects/client/src/app/pages/data-vis/data-vis.component.html

@ -1 +1,40 @@
<p>data-vis works!</p> <div class="body">
<div class="head clearfix">
<div class="logo">
<img src="/assets/images/jl-logo.png" />
</div>
<h1 class="title">成都市实验小学西区分校食谱营养报告</h1>
<div class="time">{{showTime}}</div>
</div>
<div class="mainbox flex">
<!-- <div class="boxnav mapc">
</div> -->
<div class="flex-1 ">
<div class="box">
<div class="tit">今日带量食谱</div>
<div class="boxnav">
</div>
</div>
<div class="box">
<div class="tit">今日食材种类</div>
<div class="boxnav">
</div>
</div>
</div>
<div class="w-1/3 pl-4">
<div class="box">
<div class="tit">今日营养分析</div>
<div class="boxnav">
</div>
</div>
</div>
</div>
</div>

240
projects/client/src/app/pages/data-vis/data-vis.component.less

@ -0,0 +1,240 @@
@charset "utf-8";
/* CSS Document */
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.body {
width: 100vw;
height: 100vh;
position: relative;
color: #fff;
font-size: 16px;
background-color: #012059;
/* background: radial-gradient(50% 35%, #034f8e, #034987, #02366d, #002353); */
}
li {
list-style-type: none;
}
i {
margin: 0px;
padding: 0px;
text-indent: 0px;
}
img {
border: none;
max-width: 100%;
}
a {
text-decoration: none;
color: #fff;
}
a.active,
a:focus {
outline: none !important;
text-decoration: none;
}
ol,
ul,
p,
h1,
h2,
h3,
h4,
h5,
h6 {
padding: 0;
margin: 0;
}
a:hover {
color: #06c;
text-decoration: none !important;
}
.clearfix:after,
.clearfix:before {
display: table;
content: " ";
}
.clearfix:after {
clear: both;
}
.pulll_left {
float: left;
}
.pulll_right {
float: right;
}
i {
font-style: normal;
}
.text-w {
color: #ffe400;
}
.text-d {
color: #ff6316;
}
.text-s {
color: #14e144;
}
.text-b {
color: #07e5ff;
}
.head {
position: relative;
height: 90px;
margin: 0 15px;
padding-right: 60px;
display: flex;
align-items: center;
}
.head h1 {
font-size: 30px;
letter-spacing: -2px;
text-align: center;
line-height: 90px;
color: #daf9ff;
position: absolute;
top: 0;
left: 0;
right: 0;
}
.head .menu ul {
font-size: 0;
}
.head .menu li {
display: inline-block;
position: relative;
margin: 25px 15px;
}
.head .menu li a {
display: block;
font-size: 18px;
color: #fff;
line-height: 40px;
padding: 0 15px;
}
.head .time {
position: absolute;
top: 0;
right: 0;
line-height: 90px;
font-family: electronicFont;
font-size: 28px;
}
.head .menu li a:hover {
color: #f4e925;
}
.logo {
height: 60px;
img {
display: block;
height: 100%;
}
}
.mainbox {
padding: 0px 10px 10px;
}
.nav1 {
margin-left: -6px;
margin-right: -6px;
}
.nav1>li {
padding: 0 6px;
float: left;
}
.box {
border: 1px solid rgba(7, 118, 181, 0.5);
box-shadow: inset 0 0 10px rgba(7, 118, 181, 0.4);
margin-bottom: 12px;
position: relative;
}
.tit {
padding: 10px 10px 10px 25px;
border-bottom: 1px solid rgba(7, 118, 181, 0.7);
font-size: 16px;
font-weight: 500;
position: relative;
}
.tit:before,
.tit01:before {
position: absolute;
content: "";
width: 6px;
height: 6px;
background: rgba(22, 214, 255, 0.9);
box-shadow: 0 0 5px rgba(22, 214, 255, 0.9);
border-radius: 10px;
left: 10px;
top: 18px;
}
.tit:after,
.box:before {
width: 100%;
height: 1px;
content: "";
position: absolute;
left: 0;
bottom: -1px;
background: linear-gradient(to right, #076ead, #4ba6e0, #076ead);
box-shadow: 0 0 5px rgba(131, 189, 227, 1);
opacity: 0.6;
}
.box:before {
top: -1px;
}
.boxnav {
padding: 10px;
}
// .mapc {
// position: absolute;
// top: 10%;
// left: 50%;
// width: 500px;
// height: 500px;
// transform: translateX(-50%);
// background: url(/assets/diz/1/bg3.png) no-repeat center center;
// background-size: cover;
// }

31
projects/client/src/app/pages/data-vis/data-vis.component.ts

@ -1,10 +1,33 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ApiService } from "@cdk/services";
import { format } from "date-fns";
import { Subject, interval, takeUntil } from "rxjs";
@Component({ @Component({
selector: 'app-data-vis', selector: "app-data-vis",
templateUrl: './data-vis.component.html', templateUrl: "./data-vis.component.html",
styleUrls: ['./data-vis.component.less'] styleUrls: ["./data-vis.component.less"],
}) })
export class DataVisComponent { export class DataVisComponent {
constructor(private api: ApiService) {}
destroy$ = new Subject();
showTime: string = "";
ngOnInit(): void {
interval(1000)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.showTime = format(new Date(), "yyyy-MM-dd HH:mm:ss");
});
this.api.getMenuDataVis().subscribe((res) => {
console.log("res", res);
});
}
ngOnDestroy(): void {
this.destroy$.next(null);
this.destroy$.complete();
}
} }

99
projects/client/src/app/pages/dish/dish.component.html

@ -24,7 +24,8 @@
<div class="m-4"> <div class="m-4">
<ng-template #pageExtraTpl> <ng-template #pageExtraTpl>
<nz-space> <nz-space>
<button *nzSpaceItem nz-button>批量删除</button> <button *nzSpaceItem nz-button [disabled]="!selectedIds.length"
(click)="deleteItem()">批量删除</button>
<button *nzSpaceItem nz-button>批量打印营养标签</button> <button *nzSpaceItem nz-button>批量打印营养标签</button>
<button *nzSpaceItem nz-button nzType="primary" (click)="showFoodForm()"> <button *nzSpaceItem nz-button nzType="primary" (click)="showFoodForm()">
<i nz-icon nzType="plus"></i> <i nz-icon nzType="plus"></i>
@ -35,33 +36,46 @@
<table-list [props]="tableList" [search]="searchTpl" [action]="pageExtraTpl" [formGroup]="queryForm" <table-list [props]="tableList" [search]="searchTpl" [action]="pageExtraTpl" [formGroup]="queryForm"
[renderColumns]="renderColumnsTpl"> [renderColumns]="renderColumnsTpl">
<ng-template #actionTpl>
<button nz-button>批量删除</button>
</ng-template>
<ng-template #searchTpl> <ng-template #searchTpl>
<nz-form-item class="w-50"> <nz-form-item class="w-40">
<nz-form-control> <nz-form-control>
<nz-select nzPlaceHolder="菜品标签" [nzOptions]="[]"></nz-select> <nz-select nzPlaceHolder="菜品标签" formControlName="mark" nzAllowClear>
<nz-option *ngFor="let item of globalEnum.mark" [nzLabel]="item.key"
[nzValue]="item.key">
</nz-option>
</nz-select>
</nz-form-control> </nz-form-control>
</nz-form-item> </nz-form-item>
<nz-form-item> <nz-form-item>
<nz-form-control> <nz-form-control>
<input nz-input placeholder="请输入菜品名称" formControlName="name" /> <input nz-input placeholder="请输入菜品名称" formControlName="keyword" />
</nz-form-control> </nz-form-control>
</nz-form-item> </nz-form-item>
</ng-template> </ng-template>
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row"> <ng-template #renderColumnsTpl let-data let-key="key" let-row="row">
<ng-container [ngSwitch]="key"> <ng-container [ngSwitch]="key">
<ng-container *ngSwitchCase="'img'"> <ng-container *ngSwitchCase="'icon'">
<div class="dish-img overflow-auto" <div class="dish-img overflow-auto"
[ngStyle]="{'background-image':'url(' + tempImg + ')'}"> *ngIf="data"
[ngStyle]="{'background-image':'url(' + data + ')'}">
</div>
</ng-container>
<ng-container *ngSwitchCase="'vender'">
{{ tableOrg[data] ? tableOrg[data].name : '-'}}
</ng-container>
<ng-container *ngSwitchCase="'ingredient'">
<div class=" flex flex-wrap">
<ng-container *ngFor="let item of data">
<nz-tag *ngIf="tableFoods[item.key]" class="m-1">
{{tableFoods[item.key]['name']}}:{{item.value}} g
</nz-tag>
</ng-container>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngSwitchDefault> <ng-container *ngSwitchDefault>
{{data}} {{data}}
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-template> </ng-template>
@ -72,3 +86,66 @@
</div> </div>
</div> </div>
</app-page> </app-page>
<ng-template #formFooterTpl>
<nz-space>
<button *nzSpaceItem nz-button (click)="cancelForm()" type="button">
取消
</button>
<button *nzSpaceItem nz-button nzType="primary" [nzLoading]="submitLoading" (click)="onSubmit()">
保存
</button>
</nz-space>
</ng-template>
<app-print #print
[content]="printContent">
</app-print>
<ng-template #printContent>
<div class="printContent"
*ngIf="printData">
<table class="print-table">
<tbody>
<tr>
<th colspan="3">
{{printData.name}}
</th>
</tr>
<tr>
<th colspan="3">
营养成分表
</th>
</tr>
<tr>
<th>
名称
</th>
<th class="text-center">
每100克(g)
</th>
<th class="text-center">
营养参考值%(NVR%)
</th>
</tr>
</tbody>
<tbody>
<tr *ngFor="let th of printData.component">
<td [width]="'38.2%'">{{ th.name }}</td>
<td class="text-center">{{ th.nutrition }}</td>
<td class="text-center">{{ th.nvr }}</td>
</tr>
</tbody>
</table>
<div>
主要原料:{{printData.ingredients.join(',')}}
</div>
<div>
1毫克(mg)钠相当于2.5毫克食盐
</div>
</div>
</ng-template>

177
projects/client/src/app/pages/dish/dish.component.ts

@ -1,45 +1,87 @@
import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from "@angular/core"; import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms"; import { FormControl, FormGroup } from "@angular/forms";
import { AnyObject, TableListOption } from "@cdk/public-api";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer"; import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { NzModalService } from "ng-zorro-antd/modal"; import { AnyObject, OrgDTO, TableListOption } from "@cdk/public-api";
import { Subject, takeUntil } from "rxjs"; import { DishFormComponent } from "@client/app/components";
import { ApiService } from "@cdk/services"; import { ApiService } from "@cdk/services";
import {
Subject,
debounceTime,
distinctUntilChanged,
filter,
finalize,
lastValueFrom,
switchMap,
takeUntil,
tap,
} from "rxjs";
import { NzModalService } from "ng-zorro-antd/modal";
import { NzMessageService } from "ng-zorro-antd/message";
import { ResponseType } from "@cdk/types";
import { PrintComponent } from "@cdk/shared/components";
@Component({ @Component({
selector: "app-dish", selector: "app-dish",
templateUrl: "./dish.component.html", templateUrl: "./dish.component.html",
styleUrls: ["./dish.component.less"], styleUrls: ["./dish.component.less"],
}) })
export class DishComponent implements OnInit, OnDestroy { export class DishComponent {
constructor(private drawer: NzDrawerService, private api: ApiService, private modal: NzModalService) {} constructor(
private drawer: NzDrawerService,
private api: ApiService,
private modal: NzModalService,
private msg: NzMessageService
) {}
@ViewChild("formFooterTpl") formFooterTpl!: TemplateRef<{}>;
@ViewChild("foofFormFooterTpl") foofFormFooterTpl!: TemplateRef<{}>; @ViewChild("print") printRef!: PrintComponent;
private drawerRef?: NzDrawerRef;
private destroy$ = new Subject<void>();
tempImg = "https://cdn.pixabay.com/photo/2023/08/08/18/01/butterfly-8177925_1280.jpg"; private orgSearch$ = new Subject<string>();
public globalEnum = this.api.globalEnum;
public printData: any | null;
public tableList = new TableListOption(this.fetchData.bind(this), { public tableList = new TableListOption(this.fetchData.bind(this), {
selectable: true, selectable: true,
frontPagination: false,
}); });
public queryForm = new FormGroup({ public queryForm = new FormGroup({
type: new FormControl({ value: "A", disabled: false }), keyword: new FormControl(""),
name: new FormControl("addd"), mark: new FormControl(""),
vendors: new FormControl(""),
}); });
private destroy$ = new Subject<void>();
public selectedIds: string[] = []; public selectedIds: string[] = [];
temp = Array.from({ length: 50 }, (_, i) => i); tableOrg: { [k: number]: OrgDTO } = {};
tableFoods: { [k: string]: any } = {};
listOfOption: Array<{ value: number; text: string }> = [];
nzFilterOption = (): boolean => true;
submitLoading = false;
ngOnInit(): void { ngOnInit(): void {
this.initTableList(); this.initTableList();
this.tableList.getState$.pipe(takeUntil(this.destroy$)).subscribe((res) => { this.tableList.getState$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
this.selectedIds = res.selectedKeys as Array<string>; this.selectedIds = res.selectedKeys as Array<string>;
}); });
} }
searchOrg = (k: string) => {
this.orgSearch$.next(k);
};
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
@ -48,18 +90,17 @@ export class DishComponent implements OnInit, OnDestroy {
initTableList() { initTableList() {
this.tableList.scroll = { x: null }; this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([ this.tableList = this.tableList.setColumns([
{ key: "img", title: "菜品图片", width: "66px" }, { key: "icon", title: "菜品图片", width: "66px" },
{ key: "name", title: "菜品名称" }, { key: "name", title: "菜品名称" },
{ key: "name", title: "菜品标签" }, { key: "marks", title: "菜品标签" },
{ key: "name", title: "食材及含量", width: "30%" }, { key: "ingredient", title: "食材及含量", width: "30%" },
{ key: "name", title: "单位" },
]); ]);
this.tableList = this.tableList.setOptions([ this.tableList = this.tableList.setOptions([
{ {
title: "打印营养标签", title: "打印营养标签",
premissions: [], premissions: [],
onClick: this.showFoodForm.bind(this), onClick: this.print.bind(this),
}, },
{ {
title: "编辑", title: "编辑",
@ -74,26 +115,100 @@ export class DishComponent implements OnInit, OnDestroy {
]); ]);
} }
fetchData(pager: AnyObject, query: AnyObject) { fetchData(query: AnyObject, pager: AnyObject) {
return this.api.page(pager, query); return this.api.getDishPage(pager, query).pipe(
tap((res) => {
this.getTableColumData(res);
})
);
}
getTableColumData(res: ResponseType) {
if (Array.isArray(res.body.content)) {
const vendors = res.body.content.map((i: any) => i.vender);
const foodKeys = new Set(
res.body.content.reduce((a: string[], c: any) => {
return a.concat(c.ingredient.map((i: any) => i.key));
}, [] as string[])
);
if (foodKeys.size > 0) {
this.api.getFoodList({ keys: Array.from(foodKeys) }).subscribe((foods) => {
if (Array.isArray(foods.body)) {
this.tableFoods = foods.body.reduce((a, c) => {
return {
...a,
[c.key]: c,
};
}, {} as AnyObject);
}
});
}
}
}
print(v: any) {
this.msg.loading("数据请求中,请不要刷新页面", {
nzDuration: 0,
});
this.api.getDishLabel(v.id).subscribe((res) => {
this.printData = res.body[0];
this.printRef.print();
this.msg.remove();
});
}
showFoodForm(data?: any) {
this.drawerRef = this.drawer.create({
nzTitle: data ? "编辑菜品" : "新增菜品",
nzWidth: 700,
nzContent: DishFormComponent,
nzContentParams: {
data,
orgs: Object.values(this.tableOrg),
foods: Object.values(this.tableFoods),
},
nzFooter: this.formFooterTpl,
});
}
cancelForm() {
this.drawerRef?.close();
}
onSubmit() {
if (this.drawerRef) {
const com = this.drawerRef.getContentComponent() as DishFormComponent;
const val = com.getValues();
if (val) {
this.submitLoading = true;
this.api
.saveDish(val)
.pipe(
finalize(() => {
this.submitLoading = false;
})
)
.subscribe((res) => {
this.msg.success(res.desc);
this.tableList.run();
this.cancelForm();
});
}
}
} }
deleteItem(v?: any) { deleteItem(v?: any) {
const ids = v ? [v.id] : this.selectedIds; const ids = v ? [v.id] : this.selectedIds;
this.modal.confirm({ this.modal.confirm({
nzTitle: "警告", nzTitle: "警告",
nzContent: "是否要删除该食材?", nzContent: `是否要删除${ids.length}个菜品?`,
nzOkDanger: true, nzOkDanger: true,
nzOnOk: () => {}, nzOnOk: async () => {
const res = await lastValueFrom(this.api.deleteDish(ids));
this.msg.success(res.desc);
this.tableList.run();
},
}); });
} }
showFoodForm(food?: any) {
// this.drawerRef = this.drawer.create({
// nzTitle: food ? "编辑菜品" : "新增菜品",
// nzWidth: 700,
// nzContent: DishFormComponent,
// nzFooter: this.formFooterTpl,
// });
}
} }

5
projects/client/src/app/pages/index.ts

@ -7,3 +7,8 @@ export * from "./dish/dish.component";
export * from "./system/org-info/org-info.component"; export * from "./system/org-info/org-info.component";
export * from "./system/user-manage/user-manage.component"; export * from "./system/user-manage/user-manage.component";
export * from "./ingredients/ingredient-list/ingredient-list.component";
export * from "./ingredients/ingredient-form/ingredient-form.component";
export * from "./ingredients/ingredient-preview-page/ingredient-preview-page.component";
export * from "./ingredients/ingredient-release/ingredient-release.component";

51
projects/client/src/app/pages/ingredients/ingredient-form/ingredient-form.component.html

@ -0,0 +1,51 @@
<app-page [full]="true">
<div class="p-4" *ngIf="step === 0">
<nz-card nzTitle="录入食谱基础信息">
<app-ingredient-form-basic [client]="true" [menu]="menuItem"
(onSave)="onStepChange($event)">
</app-ingredient-form-basic>
</nz-card>
</div>
<ng-container *ngIf="step === 1">
<nz-card nzSize="small" [nzBordered]="false">
<div class="flex justify-between">
<div class="flex-1 ">
<button nz-button (click)="step = 0">
配置
</button>
</div>
<nz-space>
<button *nzSpaceItem nz-button>
食谱营养分析
</button>
<button *nzSpaceItem nz-button (click)="createNewMenu()">
新建食谱
</button>
<button *nzSpaceItem nz-button>
导入食谱
</button>
<button *nzSpaceItem nz-button>
食谱预览
</button>
<button *nzSpaceItem nz-button nzType="primary" (click)="confirmSave()">
保存
</button>
<!-- <button *nzSpaceItem nz-button>
另存为
</button> -->
</nz-space>
</div>
</nz-card>
<div class="p-4">
<app-ingredient-dish #menuDish [menuBaisc]="menuItem"
[client]="true"
[menuDishFormServer]="menuDishFormServer">
</app-ingredient-dish>
</div>
</ng-container>
</app-page>

7
projects/client/src/app/pages/ingredients/ingredient-form/ingredient-form.component.less

@ -0,0 +1,7 @@
.day-item {
::ng-deep {
.ant-card-head-title {
overflow: visible;
}
}
}

162
projects/client/src/app/pages/ingredients/ingredient-form/ingredient-form.component.ts

@ -0,0 +1,162 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ConfirmIngredientComponent } from "@cdk/ingredient/confirm-ingredient/confirm-ingredient.component";
import { IngredientDishComponent } from "@cdk/ingredient/ingredient-dish/ingredient-dish.component";
import { MealDishInterface } from "@cdk/ingredient/ingredient-meals/ingredient-meals.component";
import { ApiService } from "@cdk/services";
import { OptionItemInterface } from "@cdk/types";
import { NzMessageService } from "ng-zorro-antd/message";
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,
private msg: NzMessageService,
private router: Router,
private route: ActivatedRoute,
private api: ApiService
) {
this.id = this.route.snapshot.paramMap.get("id");
}
@ViewChild("menuDish") menuDish!: IngredientDishComponent;
step = 0;
id: string | null = "";
menuItem: any = null;
menuDishFormServer: any = null;
ngOnInit(): void {
this.step = this.id && this.id !== "create" ? 1 : 0;
this.getDetail();
}
getDetail() {
if (this.id && this.id !== "create") {
this.api.getMenuItem(this.id).subscribe((res) => {
if (res.body) {
this.menuItem = res.body;
}
});
this.api.getMenuDist(this.id).subscribe((res) => {
if (Array.isArray(res.body)) {
res.body.forEach((d) => {
d.ingredient.forEach((f: any) => {
f["groupValues"] = Object.entries(f.value).map(([peopleName, value]) => {
return {
peopleName,
value,
};
});
});
});
this.menuDishFormServer = res.body;
}
});
}
}
onStepChange(basicInfo: any) {
this.step = 1;
this.menuItem = {
...basicInfo,
menuIds: basicInfo.menuId,
meals: basicInfo.meals.reduce(
(a: string[], c: OptionItemInterface) => (c["checked"] ? a.concat(c.value) : a),
[] as string[]
),
crows: basicInfo.peoples.reduce(
(a: string[], c: OptionItemInterface) => (c["checked"] ? a.concat(c.value) : a),
[] as string[]
),
};
}
createNewMenu() {
this.modal.confirm({
nzTitle: "警告",
nzContent: "新建食谱将清空本次所有的配餐数据,确认要清空吗?",
nzOnOk: () => {
this.menuDish.mealDishList = [];
},
});
}
confirmSave() {
this.modal.create({
nzTitle: "确认食谱信息",
nzContent: ConfirmIngredientComponent,
nzData: this.menuItem,
nzWidth: 650,
nzOnOk: () => {
const { mealDishList } = this.menuDish;
mealDishList.forEach((dish) => {
dish["dishId"] = dish.dish;
dish.items = dish.items.map((i) => {
return {
...i,
value: (i["groupValues"] as any[]).reduce((a, c) => {
return {
...a,
[c.peopleName]: c.value,
};
}, {} as Record<string, number>),
};
});
});
console.log("mealDishList", mealDishList);
this.api
.saveMenuDist({
menuIds: this.menuItem.menuIds ?? [this.menuItem.id],
dishes: mealDishList,
})
.subscribe((res) => {
this.msg.success(res.desc);
this.router.navigate(["/ingredient/item/list"]);
});
},
});
}
formatData(menuObject: any) {
let dishes: any[] = [];
Object.entries(menuObject).forEach(([day, v]) => {
Object.entries(v as Record<string, MealDishInterface[]>).forEach(([mealIndex, dishList]) => {
dishList.forEach((dish) => {
dishes.push({
...dish,
day: Number(day),
meal: this.menuItem.meals[mealIndex],
items: dish.foods.map((food) => {
return {
...food,
value: food.groupValues.reduce((a, c) => {
return {
...a,
[c.peopleName]: c.value,
};
}, {} as Record<string, number>),
};
}),
});
});
});
});
const toServer = {
menuIds: this.menuItem.menuIds ?? [this.menuItem.id],
dishes,
};
return toServer;
}
}

94
projects/client/src/app/pages/ingredients/ingredient-list/ingredient-list.component.html

@ -0,0 +1,94 @@
<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"
optionWidth="300px">
<ng-template #actionTpl>
<button nz-button>批量删除</button>
</ng-template>
<ng-template #searchTpl>
<nz-form-item class="w-40">
<nz-form-control>
<nz-select nzPlaceHolder="状态" formControlName="status" [nzAllowClear]="true">
<nz-option *ngFor="let item of globalEnum.menuStatus" [nzValue]="item.label"
[nzLabel]="item.value">
</nz-option>
</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="'modify'">
{{data | date:'yyyy-MM-dd HH:mm:ss'}}
</ng-container>
<ng-container *ngSwitchCase="'vender'">
{{ tableOrg[data] ? tableOrg[data].name : '-'}}
</ng-container>
<ng-container *ngSwitchCase="'meals'">
<nz-tag *ngFor="let item of data">{{item}}</nz-tag>
</ng-container>
<ng-container *ngSwitchCase="'day'">
{{data}} 天
</ng-container>
<ng-container *ngSwitchCase="'status'">
{{statusTextMap[data]}}
</ng-container>
<ng-container *ngSwitchCase="'month'">
<div class="flex flex-wrap">
<ng-container *ngIf="data.length === 12">
<nz-tag>
全年
</nz-tag>
</ng-container>
<ng-container *ngIf="data.length !== 12">
<nz-tag *ngFor="let item of data" class="mb-1">
{{monthText[item]}}
</nz-tag>
</ng-container>
</div>
</ng-container>
<ng-container *ngSwitchDefault>
{{data}}
</ng-container>
</ng-container>
</ng-template>
</table-list>
</nz-card>
</div>
</app-page>
<ng-template #releaseStartTimeTpl>
<div nz-form>
<nz-form-item>
<nz-form-label nzSpan="6" [nzRequired]="true">
发布日期
</nz-form-label>
<nz-form-control nzSpan="12">
<nz-date-picker class="w-full" nzPlaceHolder="请选择发布日期" [(ngModel)]="startTime"></nz-date-picker>
</nz-form-control>
</nz-form-item>
</div>
</ng-template>

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

220
projects/client/src/app/pages/ingredients/ingredient-list/ingredient-list.component.ts

@ -0,0 +1,220 @@
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { AnyObject, OrgDTO, TableListOption } from "@cdk/public-api";
import { ApiService } from "@cdk/services";
import { NzModalService } from "ng-zorro-antd/modal";
import { lastValueFrom, tap } from "rxjs";
import { NzMessageService } from "ng-zorro-antd/message";
import { MyResponse } from "@cdk/types";
import { Router } from "@angular/router";
@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,
private modal: NzModalService,
private msg: NzMessageService,
private router: Router
) {}
globalEnum = this.api.globalEnum;
statusTextMap: Record<string, string> = {};
@ViewChild("releaseStartTimeTpl") releaseStartTimeTpl!: TemplateRef<{}>;
private drawerRef?: NzDrawerRef;
public tableList = new TableListOption(this.fetchData.bind(this), {
frontPagination: false,
});
public queryForm = new FormGroup({
name: new FormControl(""),
vender: new FormControl(""),
status: new FormControl(""),
});
startTime: Date | null = null;
tableOrg: { [k: number]: OrgDTO } = {};
monthText = {
1: "一月",
2: "二月",
3: "三月",
4: "四月",
5: "五月",
6: "六月",
7: "七月",
8: "八月",
9: "九月",
10: "十月",
11: "十一月",
12: "十二月",
} as any;
ngOnInit(): void {
this.statusTextMap = this.globalEnum.menuStatus.reduce((a, c) => {
return {
...a,
[String(c.label)]: c.value,
};
}, {} as Record<string, string>);
this.initTableList();
}
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "name", title: "食谱名称" },
{ key: "meals", title: "包含餐次" },
{ key: "month", title: "适用月份", width: "300px" },
{ key: "day", title: "周期" },
{ key: "status", title: "状态" },
{ key: "modify", title: "更新时间" },
{ key: "operate", title: "创建人" },
]);
this.tableList = this.tableList.setOptions([
{
title: "详情",
premissions: [],
onClick: this.preview.bind(this),
},
{
title: "导出",
premissions: [],
onClick: this.export.bind(this),
},
{
title: "审核",
premissions: [],
onClick: this.shenhe.bind(this),
visible(v) {
// 0 提交审核 编辑 删除
// 1 审核中 删除
// 2 发布 编辑 删除
// 3 提交审核 编辑 删除
// 4 提交审核 编辑 删除
return [0, 3, 4].includes(v.status);
},
},
{
title: "发布",
premissions: [],
onClick: this.release.bind(this),
visible(v) {
return [2].includes(v.status);
},
},
{
title: "禁用",
premissions: [],
onClick: this.disableMenu.bind(this),
visible(v) {
return [2].includes(v.status);
},
},
{
title: "编辑",
premissions: [],
onClick: (v) => {
this.router.navigate([`/ingredient/item/form/${v["id"]}`]);
},
visible(v) {
return [0, 3, 4].includes(v.status);
},
},
{
title: "删除",
premissions: [],
onClick: this.deleteItem.bind(this),
},
]);
}
fetchData(query: AnyObject, pager: AnyObject) {
return this.api.getMenuPage(pager, query).pipe();
}
preview({ id }: any) {
window.open(`/ingredient/preview?id=${id}`);
}
export({ id }: any) {
this.msg.loading("导出中...");
this.api.exportMenu(id).subscribe(() => {
setTimeout(() => {
this.msg.remove();
}, 1500);
});
}
cancelFoodForm() {
this.drawerRef?.close();
}
shenhe({ id }: any) {
this.modal.confirm({
nzTitle: "警告",
nzContent: `是否要将该食谱提交审核?`,
nzOnOk: async () => {
const res = await lastValueFrom(this.api.submitMenuForReview(id));
this.msg.success(res.desc);
this.tableList.run();
},
});
}
release({ id, day }: any) {
this.modal.create({
nzTitle: "发布食谱",
nzContent: this.releaseStartTimeTpl,
nzOnOk: async () => {
if (!this.startTime) {
this.msg.error("请选择发布日期");
return false;
}
const res = await lastValueFrom(this.api.release(id, this.startTime, day));
this.msg.success(res.desc);
this.tableList.run();
return true;
},
});
}
deleteItem({ id }: any) {
this.modal.confirm({
nzTitle: "警告",
nzContent: `是否要删除该食谱?`,
nzOkDanger: true,
nzOnOk: async () => {
const res = await lastValueFrom(this.api.deleteMenu(id));
this.msg.success(res.desc);
this.tableList.run();
},
});
}
disableMenu({ id }: any) {
this.modal.confirm({
nzTitle: "警告",
nzContent: `是否要禁用该食谱?`,
nzOkDanger: true,
nzOnOk: async () => {
const res = await lastValueFrom(this.api.disableMenu(id));
this.msg.success(res.desc);
this.tableList.run();
},
});
}
}

1
projects/client/src/app/pages/ingredients/ingredient-preview-page/ingredient-preview-page.component.html

@ -0,0 +1 @@
<app-ingredient-preview></app-ingredient-preview>

0
projects/client/src/app/pages/ingredients/ingredient-preview-page/ingredient-preview-page.component.less

13
projects/client/src/app/pages/ingredients/ingredient-preview-page/ingredient-preview-page.component.ts

@ -0,0 +1,13 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
@Component({
selector: "app-ingredient-preview-page",
templateUrl: "./ingredient-preview-page.component.html",
styleUrls: ["./ingredient-preview-page.component.less"],
})
export class IngredientPreviewPageComponent implements OnInit {
constructor(private route: ActivatedRoute, private router: Router) {}
ngOnInit(): void {}
}

64
projects/client/src/app/pages/ingredients/ingredient-release/ingredient-release.component.html

@ -0,0 +1,64 @@
<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>
<nz-form-control>
<nz-space>
<nz-radio-group *nzSpaceItem nzButtonStyle="solid"
[(ngModel)]="week"
(ngModelChange)="onWeekChange($event)"
[ngModelOptions]="{standalone: true}">
<label nz-radio-button [nzValue]="0">全部</label>
<label nz-radio-button [nzValue]="1">本周</label>
<label nz-radio-button [nzValue]="-1">上周</label>
</nz-radio-group>
<ng-container *nzSpaceItem>
<nz-range-picker
[(ngModel)]="dateRange"
[ngModelOptions]="{standalone: true}">
</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="'created'">
{{data | date:'yyyy-MM-dd HH:mm:ss'}}
</ng-container>
<ng-container *ngSwitchCase="'vender'">
{{ tableOrg[data] ? tableOrg[data].name : '-'}}
</ng-container>
<ng-container *ngSwitchCase="'meals'">
<nz-tag *ngFor="let item of data">{{item}}</nz-tag>
</ng-container>
<ng-container *ngSwitchCase="'day'">
{{data}} 天
</ng-container>
<ng-container *ngSwitchDefault>
{{data}}
</ng-container>
</ng-container>
</ng-template>
</table-list>
</nz-card>
</div>
</app-page>

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

129
projects/client/src/app/pages/ingredients/ingredient-release/ingredient-release.component.ts

@ -0,0 +1,129 @@
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { AnyObject, MyResponse, OrgDTO, TableListOption } from "@cdk/public-api";
import { ApiService } from "@cdk/services";
import { lastValueFrom, tap } from "rxjs";
import { endOfWeek, format, startOfWeek, subWeeks } from "date-fns";
import { NzModalService } from "ng-zorro-antd/modal";
import { NzMessageService } from "ng-zorro-antd/message";
@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,
private modal: NzModalService,
private msg: NzMessageService
) {}
public tableList = new TableListOption(this.fetchData.bind(this));
public queryForm = new FormGroup({
name: new FormControl(""),
vender: new FormControl(""),
});
tableOrg: { [k: number]: OrgDTO } = {};
week: number = 0;
dateRange: Date[] | null = null;
ngOnInit(): void {
this.initTableList();
}
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "name", title: "食谱名称" },
{ key: "meals", title: "包含餐次" },
{ key: "day", title: "周期" },
{ key: "created", title: "创建时间" },
{ key: "dateRange", title: "应用时间" },
]);
this.tableList = this.tableList.setOptions([
{
title: "详情",
premissions: [],
onClick: this.preview.bind(this),
},
{
title: "导出",
premissions: [],
onClick: this.export.bind(this),
},
{
title: "取消发布",
premissions: [],
onClick: this.cancelRelease.bind(this),
},
]);
}
fetchData(pager: AnyObject, query: AnyObject) {
if (this.dateRange) {
if (this.dateRange[0]) {
query["startTime"] = format(this.dateRange[0], "yyyy-MM-dd");
}
if (this.dateRange[1]) {
query["endTime"] = format(this.dateRange[1], "yyyy-MM-dd");
}
}
return this.api.getMenuReleasePage(pager, query).pipe();
}
preview({ id }: any) {
window.open(`/ingredient/preview?id=${id}`);
}
onWeekChange(e: -1 | 0 | 1) {
if (e !== 0) {
const range = this.getWeekDates(e);
this.dateRange = range;
} else {
this.dateRange = null;
}
}
getWeekDates(offset: -1 | 1) {
const now = new Date();
const startDate =
offset === -1 ? startOfWeek(subWeeks(now, 1), { weekStartsOn: 1 }) : startOfWeek(now, { weekStartsOn: 1 });
const endDate =
offset === -1 ? endOfWeek(subWeeks(now, 1), { weekStartsOn: 1 }) : endOfWeek(now, { weekStartsOn: 1 });
return [startDate, endDate];
}
export({ id }: any) {
this.msg.loading("导出中...");
this.api.exportMenu(id).subscribe(() => {
setTimeout(() => {
this.msg.remove();
}, 1500);
});
}
cancelRelease({ id }: any) {
this.modal.confirm({
nzTitle: "警告",
nzContent: "是否要取消发布该食谱?",
nzOkDanger: true,
nzOnOk: async () => {
const res = await lastValueFrom(this.api.cancelRelease(id));
this.msg.success(res.desc);
this.tableList.run();
},
});
}
}

BIN
projects/client/src/assets/diz/1/bg3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

BIN
projects/client/src/assets/diz/1/m1-bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
projects/client/src/assets/diz/1/m2-bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
projects/client/src/assets/diz/1/m3-bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

57
projects/client/src/styles.less

@ -57,3 +57,60 @@ li {
} }
} }
::-webkit-scrollbar {
width: 10px;
height: 10px;
background-color: rgba(240, 240, 240, 1);
}
/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track {
box-shadow: inset 0 0 0px rgba(240, 240, 240, 0.5);
border-radius: 10px;
background-color: rgba(240, 240, 240, 0.5);
}
/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 0px rgba(240, 240, 240, 0.5);
background-color: rgba(0, 0, 0, 0.171);
}
@media print {
#root {
display: none;
}
@page {
size: auto;
width: 100vw;
margin: 5mm auto !important;
}
body {
background-color: transparent !important;
}
* {
color: #000 !important;
}
}
.print-table {
width: 100%;
margin-bottom: 12px;
border-collapse: collapse;
border-spacing: 0;
border: 1px solid #e8e8e8;
td,
th {
padding: 6px 12px;
border: 1px solid #000;
font-weight: normal;
}
}

3
tsconfig.json

@ -11,6 +11,9 @@
"@admin/*": [ "@admin/*": [
"./projects/admin/src/*", "./projects/admin/src/*",
], ],
"@client/*": [
"./projects/client/src/*",
],
}, },
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,

Loading…
Cancel
Save