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. 172
      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. 16
      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
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. 食谱审核 列表需要 提交审核时间、提交人 字段
2. 食谱列表 需要 返回一个字段 标识当前食谱 是否是 管理系统添加的还是业务系统添加的
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: "modify", title: "提交审核时间" },
{ key: "modify", title: "提交人" },
{ key: "operate", title: "提交人" },
]);
this.tableList = this.tableList.setOptions([
{
title: "详情",
premissions: [],
onClick: this.showFoodForm.bind(this),
onClick: this.preview.bind(this),
},
{
title: "导出",
premissions: [],
onClick: this.showFoodForm.bind(this),
onClick: this.export.bind(this),
},
{
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) {
return this.api.getMenuStatusPage(pager, { ...query, status: this.status }).pipe(
tap((res) => {

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

@ -88,3 +88,56 @@
</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>

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

@ -18,6 +18,7 @@ import {
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({
selector: "app-dish",
@ -34,6 +35,8 @@ export class DishComponent {
@ViewChild("formFooterTpl") formFooterTpl!: TemplateRef<{}>;
@ViewChild("print") printRef!: PrintComponent;
private drawerRef?: NzDrawerRef;
private destroy$ = new Subject<void>();
@ -42,6 +45,8 @@ export class DishComponent {
public globalEnum = this.api.globalEnum;
public printData: any | null;
public tableList = new TableListOption(this.fetchData.bind(this), {
selectable: true,
frontPagination: false,
@ -114,7 +119,7 @@ export class DishComponent {
{
title: "打印营养标签",
premissions: [],
onClick: this.showFoodForm.bind(this),
onClick: this.print.bind(this),
},
{
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) {
return this.api.getDishPage(pager, query).pipe(
tap((res) => {

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

@ -26,7 +26,7 @@
<button *nzSpaceItem nz-button>
导入食谱
</button>
<button *nzSpaceItem nz-button>
<button *nzSpaceItem nz-button (click)="previewMenu()">
食谱预览
</button>
<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 { 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 { DishInterface, 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";
import { forkJoin } from "rxjs";
@Component({
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) {
this.step = 1;
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() {
this.modal.create({
nzTitle: "确认食谱信息",
@ -99,25 +136,10 @@ export class IngredientFormComponent implements OnInit {
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,
dishes: this.formatData(mealDishList),
})
.subscribe((res) => {
this.msg.success(res.desc);
@ -127,36 +149,22 @@ export class IngredientFormComponent implements OnInit {
});
}
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) => {
formatData(d: DishInterface[]) {
const data: DishInterface[] = JSON.parse(JSON.stringify(d));
data.forEach((dish) => {
dish["dishId"] = dish.dish;
dish.items = dish.items.map((i) => {
return {
...food,
value: food.groupValues.reduce((a, c) => {
...i,
value: (i["groupValues"] as any[]).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;
return data;
}
}

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

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

@ -66,15 +66,3 @@
</nz-card>
</div>
</app-page>
<ng-template #foofFormFooterTpl>
<nz-space>
<button *nzSpaceItem nz-button (click)="cancelFoodForm()">
取消
</button>
<button *nzSpaceItem nz-button nzType="primary">
保存
</button>
</nz-space>
</ng-template>

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

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

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

@ -18,7 +18,7 @@ export class HTTPInterceptor implements HttpInterceptor {
private msgFlag = false;
private localStroageKey = "catering";
private localStroageKey = "catering_admin";
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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() client = false;
@Input() menuDishFormServer: any | null;
expanded = new Set<number>();
@ -67,26 +69,27 @@ export class IngredientDishComponent implements OnChanges {
}
initMenuDish() {
const foodIds = new Set<number>();
this.menuDishFormServer.forEach((i: any) => {
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) => {
return {
...i,
dishName: i.name,
mealIndex: this.menuBaisc.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;
}),
};
});
});
this.mealDishList = this.menuDishFormServer;
// const foodIds = new Set<number>();
// this.menuDishFormServer.forEach((i: any) => {
// 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) => {
// return {
// ...i,
// dishName: i.name,
// mealIndex: this.menuBaisc.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;
// }),
// };
// });
// });
}
initMenuBasic() {

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

@ -29,7 +29,7 @@
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-item *ngIf="!client">
<nz-form-label nzRequired nzSpan="6">
适用单位
</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() client: boolean = false;
@Output() onSave = new EventEmitter();
private standardSearch$ = new Subject<{ id?: string; name?: string }>();
@ -46,7 +48,7 @@ export class IngredientFormBasicComponent implements OnChanges {
name: this.fb.control("", [FormValidators.required()]),
nutrient: this.fb.control("", [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()]),
});
this.standardSearch$
@ -64,7 +66,7 @@ export class IngredientFormBasicComponent implements OnChanges {
if (getById) {
data.body = [data.body];
}
data.body.forEach((item) => {
data.body.forEach((item: any) => {
listOfOption.push({
label: item.name,
value: item.id,
@ -113,13 +115,6 @@ export class IngredientFormBasicComponent implements OnChanges {
onStandardChange(v: any) {
const currentStandard = this.searchedStandard.find((f) => f.value === v);
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]) => {
return {
label: k,
@ -128,6 +123,16 @@ export class IngredientFormBasicComponent implements OnChanges {
...(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 *ngFor="let p of peopleGroups">
{{totalObj[p]}}
{{totalObj[p]?.toFixed(2)}}
</td>
</tr>

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

@ -54,7 +54,7 @@ export class IngredientMealsComponent implements OnChanges, OnInit {
// currentDishs: DishInterface[] = [];
totalObj: Record<string, string> = {};
totalObj: Record<string, number> = {};
ngOnInit(): void {
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() {
this.modal.confirm({
nzTitle: "警告",
@ -119,6 +109,8 @@ export class IngredientMealsComponent implements OnChanges, OnInit {
nzOnOk: () => {
this.mealDishs = this.mealDishs.filter((f) => !(f.day === this.day && f["mealIndex"] === this.mealIndex));
this.onSaveDish.emit(this.mealDishs);
this.calcTotal();
return true;
},
});
}

172
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>
<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">
<nz-descriptions [nzColumn]="4">
<nz-descriptions-item nzTitle="采用标准">Zhou Maomao</nz-descriptions-item>
<nz-descriptions-item nzTitle="食谱周期">18100000000</nz-descriptions-item>
<nz-descriptions-item nzTitle="适用月份">Hangzhou, Zhejiang</nz-descriptions-item>
<nz-descriptions-item nzTitle="人群显示">Empty</nz-descriptions-item>
<nz-descriptions>
<nz-descriptions-item nzTitle="采用标准">{{basic.standardName ?? '-'}}</nz-descriptions-item>
<nz-descriptions-item nzTitle="食谱周期">{{basic.day}}天</nz-descriptions-item>
<nz-descriptions-item nzTitle="适用月份">
<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>
</div>
<div class="p-4">
</ng-container>
</nz-spin>
<nz-spin [nzSpinning]="!dishs.length && !basic">
<div class="p-4 w-full overflow-auto" *ngIf="dishs.length && basic">
<table class="menu-table">
<thead>
<tr>
<th width="100px">
</th>
<th>
<div>
<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 class="w-24">
</div>
</th>
<th>
<th *ngFor="let day of days">
<div>
<div class="table-day-row">
<div class="td">2</div>
<div class="td"> 第{{day}}天 </div>
<div class="td"> 重量/克 </div>
</div>
<div class="table-menu-row flex ">
@ -54,80 +66,94 @@
食材
</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 class="td age-range" *ngFor="let people of basic.crows">{{people}}</div>
</div>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<tr *ngFor="let meal of basic.meals">
<td class="text-center">
<b>
早餐
{{meal}}
</b>
</td>
<td>
<div class="flex">
<td *ngFor="let day of days">
<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="text-left td">烂肉泡菜L</div>
<div class="text-left td">
<b>
{{d['dishName']}}
</b>
</div>
</div>
<div class="menu-food">
<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">
酸白菜[酸菜]
{{f['foodName']}}
</div>
<div class="td age-range text-right">
1
</div>
<div class="td age-range text-right">
2
</div>
<div class="td age-range text-right">
3
<div
class="td age-range text-right"
*ngFor="let p of basic.crows">
{{f.value[p].toFixed(2)}}
</div>
</li>
<li class="flex">
<!-- <li class="flex food-item placeholder" *ngIf="last">
<div class="td food-name text-left">
酸白菜[酸菜]
&nbsp;
</div>
<div class="td age-range text-right">
1
<div
class="td age-range text-right"
*ngFor="let p of basic.crows">
&nbsp;
</div>
<div class="td age-range text-right">
2
</div>
<div class="td age-range text-right">
3
</div>
</li>
<li class="flex">
<div class="td food-name text-left">
酸白菜[酸菜]
</li> -->
</ng-container>
</ul>
</div>
<div class="td age-range text-right">
1
</div>
<div class="td age-range text-right">
2
</ng-container>
</ng-container>
<div class="flex total">
<div class="menu-dish dish-name">
<div class="text-left td">本餐生重总量</div>
</div>
<div class="td age-range text-right">
3
<div class="menu-food">
<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>
</li>
</ul>
</div>
</div>
</td>
<td>
</div>
</td>
</tr>
<!-- <ng-container *ngFor="let dish of dishs">
<ng-container *ngIf="dish.meal === '早餐'">
</ng-container>
</ng-container> -->
</tbody>
</table>
</div>
</nz-spin>

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

@ -12,6 +12,21 @@
border-spacing: 0;
border: 1px solid #e8e8e8;
.meal {
position: relative;
min-height: 200px;
padding-bottom: 42px;
.total {
position: absolute;
bottom: 0;
.td {
background-color: #fffbe6 !important;
}
}
}
thead {
.table-day-row {
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 {
display: flex;

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

@ -1,8 +1,131 @@
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({
selector: "app-ingredient-preview",
templateUrl: "./ingredient-preview.component.html",
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");
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 downloadLink = document.createElement("a");
downloadLink.href = URL.createObjectURL(blob);
@ -303,7 +303,7 @@ export class ApiService {
getStandard(q: { id?: string; name?: string }) {
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) {
@ -340,10 +340,11 @@ export class ApiService {
);
}
getDishLabel(id: (string | number)[]) {
const query = Utils.objectStringify({ id });
return this.http.get<ResponseType<any[]>>(`/api/dish?${query}`);
getDishLabel(ids: (string | number)[]) {
const query = Utils.objectStringify({ ids });
return this.http.get<ResponseType<any[]>>(`/api/dish/label?${query}`);
}
saveDish(v: AnyObject) {
const body = Utils.objectToFormData(v);
const method = v["id"] ? "post" : "put";
@ -420,6 +421,10 @@ export class ApiService {
return this.http.get<ResponseType>(`/api/menu/dish?menuId=${menuId}`);
}
getMenuDataVis() {
return this.http.get<ResponseType>(`/api/menu/dish`);
}
saveMenuDist(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 "./org-select/org-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 { NgxPermissionsModule } from "ngx-permissions";
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 components: any = [];
@ -43,6 +49,7 @@ const cdks = [
MonthSelectComponent,
DishSelectComponent,
OrgSelectComponent,
PrintComponent,
],
imports: [...ngZorroModules, ...ngModules, ...cdks, AppPageComponent],
exports: [
@ -56,6 +63,7 @@ const cdks = [
MonthSelectComponent,
OrgSelectComponent,
DishSelectComponent,
PrintComponent,
],
})
export class SharedModule {}

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

@ -9,6 +9,10 @@ import {
MealSettingComponent,
OrgInfoComponent,
ClientUserManageComponent,
IngredientFormComponent,
IngredientListComponent,
IngredientPreviewPageComponent,
IngredientReleaseComponent,
} from "./pages";
import { AppLayoutComponent } from "./components";
import { authGuard } from "./services/auth.guard";
@ -45,6 +49,47 @@ const routes: Routes = [
path: "dish",
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",
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 { IconsProviderModule, PROJECT_NAME, TableListModule } from "@cdk/public-api";
import { SharedModule } from "@cdk/shared/shared.module";
import { AppLayoutComponent, OrgFormComponent, UserListComponent, RolePermissionComponent } from "./components";
import {
AppLayoutComponent,
OrgFormComponent,
UserListComponent,
RolePermissionComponent,
DishFormComponent,
} from "./components";
import {
DashboardComponent,
LoginComponent,
@ -23,8 +29,13 @@ import {
DishComponent,
OrgInfoComponent,
ClientUserManageComponent,
IngredientFormComponent,
IngredientListComponent,
IngredientPreviewPageComponent,
IngredientReleaseComponent,
} from "./pages";
import { HTTPInterceptor } from "./services/http.interceptor";
import { IngredientModule } from "@cdk/ingredient/ingredient.module";
registerLocaleData(zh);
@ -35,6 +46,7 @@ registerLocaleData(zh);
OrgFormComponent,
UserListComponent,
RolePermissionComponent,
DishFormComponent,
DashboardComponent,
LoginComponent,
@ -44,6 +56,10 @@ registerLocaleData(zh);
DishComponent,
OrgInfoComponent,
ClientUserManageComponent,
IngredientFormComponent,
IngredientListComponent,
IngredientPreviewPageComponent,
IngredientReleaseComponent,
],
imports: [
BrowserModule,
@ -53,6 +69,7 @@ registerLocaleData(zh);
BrowserAnimationsModule,
IconsProviderModule,
SharedModule,
IngredientModule,
TableListModule,
],
providers: [

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

@ -1,3 +1,8 @@
<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">
<div class="flex items-center justify-between h-full">
@ -34,16 +39,16 @@
<span nz-icon nzType="question-circle" nzTheme="outline"></span>
<span>使用流程</span>
</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>大屏显示</span>
</li>
<li nz-menu-item class="k-icon" [routerLink]="['/','meal-setting']" nzMatchRouter>
<span nz-icon nzType="k-icon:food" nzTheme="outline"></span>
<li nz-menu-item [routerLink]="['/','meal-setting']" nzMatchRouter>
<span nz-icon nzType="setting" nzTheme="outline"></span>
<span>配餐设置</span>
</li>
<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>
</li>
<li nz-menu-item class="k-icon" [routerLink]="['/','dish']" nzMatchRouter>
@ -59,7 +64,7 @@
</ul>
</li>
<li nz-submenu nzTitle="基础信息设置" nzIcon="setting" [nzOpen]="currentUrl.includes('/system/')">
<li nz-submenu nzTitle="基础信息设置" nzIcon="user" [nzOpen]="currentUrl.includes('/system/')">
<ul>
<li nz-menu-item nzMatchRouter [routerLink]="['/','system','org']">单位信息设置</li>
</ul>
@ -74,3 +79,4 @@
</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) => {
this.currentUrl = e.url;
this.fullPage = ["/ingredient/preview", "/data-vis"].some((s) => e.url.startsWith(s));
});
}
fullPage = false;
account = this.api.account;
unSubscribe$ = new Subject<void>();
currentUrl: string = "";
ngOnInit(): void {
console.log("this.account", this.account);
ngOnInit(): void {}
openDataVis() {
window.open("/data-vis");
}
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 "./user-list/user-list.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({
selector: 'app-data-vis',
templateUrl: './data-vis.component.html',
styleUrls: ['./data-vis.component.less']
selector: "app-data-vis",
templateUrl: "./data-vis.component.html",
styleUrls: ["./data-vis.component.less"],
})
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">
<ng-template #pageExtraTpl>
<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 nzType="primary" (click)="showFoodForm()">
<i nz-icon nzType="plus"></i>
@ -35,33 +36,46 @@
<table-list [props]="tableList" [search]="searchTpl" [action]="pageExtraTpl" [formGroup]="queryForm"
[renderColumns]="renderColumnsTpl">
<ng-template #actionTpl>
<button nz-button>批量删除</button>
</ng-template>
<ng-template #searchTpl>
<nz-form-item class="w-50">
<nz-form-item class="w-40">
<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-item>
<nz-form-item>
<nz-form-control>
<input nz-input placeholder="请输入菜品名称" formControlName="name" />
<input nz-input placeholder="请输入菜品名称" formControlName="keyword" />
</nz-form-control>
</nz-form-item>
</ng-template>
<ng-template #renderColumnsTpl let-data let-key="key" let-row="row">
<ng-container [ngSwitch]="key">
<ng-container *ngSwitchCase="'img'">
<ng-container *ngSwitchCase="'icon'">
<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>
</ng-container>
<ng-container *ngSwitchDefault>
{{data}}
</ng-container>
</ng-container>
</ng-template>
@ -72,3 +86,66 @@
</div>
</div>
</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 { AnyObject, TableListOption } from "@cdk/public-api";
import { NzDrawerRef, NzDrawerService } from "ng-zorro-antd/drawer";
import { NzModalService } from "ng-zorro-antd/modal";
import { Subject, takeUntil } from "rxjs";
import { AnyObject, OrgDTO, TableListOption } from "@cdk/public-api";
import { DishFormComponent } from "@client/app/components";
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({
selector: "app-dish",
templateUrl: "./dish.component.html",
styleUrls: ["./dish.component.less"],
})
export class DishComponent implements OnInit, OnDestroy {
constructor(private drawer: NzDrawerService, private api: ApiService, private modal: NzModalService) {}
export class DishComponent {
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), {
selectable: true,
frontPagination: false,
});
public queryForm = new FormGroup({
type: new FormControl({ value: "A", disabled: false }),
name: new FormControl("addd"),
keyword: new FormControl(""),
mark: new FormControl(""),
vendors: new FormControl(""),
});
private destroy$ = new Subject<void>();
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 {
this.initTableList();
this.tableList.getState$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
this.selectedIds = res.selectedKeys as Array<string>;
});
}
searchOrg = (k: string) => {
this.orgSearch$.next(k);
};
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
@ -48,18 +90,17 @@ export class DishComponent implements OnInit, OnDestroy {
initTableList() {
this.tableList.scroll = { x: null };
this.tableList = this.tableList.setColumns([
{ key: "img", title: "菜品图片", width: "66px" },
{ key: "icon", title: "菜品图片", width: "66px" },
{ key: "name", title: "菜品名称" },
{ key: "name", title: "菜品标签" },
{ key: "name", title: "食材及含量", width: "30%" },
{ key: "name", title: "单位" },
{ key: "marks", title: "菜品标签" },
{ key: "ingredient", title: "食材及含量", width: "30%" },
]);
this.tableList = this.tableList.setOptions([
{
title: "打印营养标签",
premissions: [],
onClick: this.showFoodForm.bind(this),
onClick: this.print.bind(this),
},
{
title: "编辑",
@ -74,26 +115,100 @@ export class DishComponent implements OnInit, OnDestroy {
]);
}
fetchData(pager: AnyObject, query: AnyObject) {
return this.api.page(pager, query);
fetchData(query: AnyObject, pager: AnyObject) {
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) {
const ids = v ? [v.id] : this.selectedIds;
this.modal.confirm({
nzTitle: "警告",
nzContent: "是否要删除该食材?",
nzContent: `是否要删除${ids.length}个菜品?`,
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/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/*": [
"./projects/admin/src/*",
],
"@client/*": [
"./projects/client/src/*",
],
},
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,

Loading…
Cancel
Save