diff --git a/apps/api/prisma/migrations/20260611062554_add_health_grade_sort_order/migration.sql b/apps/api/prisma/migrations/20260611062554_add_health_grade_sort_order/migration.sql new file mode 100644 index 0000000..f0e6f20 --- /dev/null +++ b/apps/api/prisma/migrations/20260611062554_add_health_grade_sort_order/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "materials" ADD COLUMN "healthGrade" TEXT, +ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0; + +-- CreateIndex +CREATE INDEX "materials_healthGrade_idx" ON "materials"("healthGrade"); + +-- CreateIndex +CREATE INDEX "materials_sortOrder_idx" ON "materials"("sortOrder"); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 4b694c3..c317b08 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -33,8 +33,10 @@ model Material { brand String? // 材料品牌 manufacturer String? // 材料厂家 spec String? // 材料规格,如 2.7SE / 18mm - envGrade String? // 环保等级 E0/E1/E2 + envGrade String? // 环保等级 E0/E1/E2(甲醛释放量分级) + healthGrade String? // 健康等级 A/B/C(综合健康评级,独立于环保等级) usageUnit String @default("m²") // 用量单位 + sortOrder Int @default(0) // 手动排序权重(越小越靠前,为厂商竞价排名预留) /// 污染物散发参数 Record,对应 shared 的 EmissionParams emissionParams Json @@ -51,7 +53,9 @@ model Material { @@index([category]) @@index([brand]) @@index([envGrade]) + @@index([healthGrade]) @@index([isPublic]) + @@index([sortOrder]) @@map("materials") } diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts index e1683cb..6c085aa 100644 --- a/apps/api/prisma/seed.ts +++ b/apps/api/prisma/seed.ts @@ -37,10 +37,13 @@ async function main() { }); console.log('组织已就绪:', org.username, org.name); - for (const m of MATERIALS) { + for (let i = 0; i < MATERIALS.length; i++) { + const m = MATERIALS[i]; + const healthGrade = ['A', 'B', 'C'][i % 3]; // 示例健康等级 + const sortOrder = (i + 1) * 10; // 预留竞价排名(越小越靠前) await prisma.material.upsert({ where: { id: m.id }, - update: {}, + update: { healthGrade, sortOrder }, // 回填已存在的材料 create: { id: m.id, name: m.name, @@ -49,6 +52,8 @@ async function main() { manufacturer: m.manufacturer, spec: m.spec ?? undefined, envGrade: m.envGrade ?? undefined, + healthGrade, + sortOrder, usageUnit: 'm²', emissionParams: ep(m.y0), isPublic: true, diff --git a/apps/api/src/materials/dto/create-material.dto.ts b/apps/api/src/materials/dto/create-material.dto.ts index 722574c..3e1aab3 100644 --- a/apps/api/src/materials/dto/create-material.dto.ts +++ b/apps/api/src/materials/dto/create-material.dto.ts @@ -29,7 +29,9 @@ export class CreateMaterialDto { @IsOptional() @IsString() manufacturer?: string; @IsOptional() @IsString() spec?: string; @IsOptional() @IsString() envGrade?: string; + @IsOptional() @IsString() healthGrade?: string; @IsOptional() @IsString() usageUnit?: string; + @IsOptional() @IsNumber() sortOrder?: number; @IsObject() @ValidateNested() diff --git a/apps/api/src/materials/dto/query-materials.dto.ts b/apps/api/src/materials/dto/query-materials.dto.ts index ce3245b..66dc0b4 100644 --- a/apps/api/src/materials/dto/query-materials.dto.ts +++ b/apps/api/src/materials/dto/query-materials.dto.ts @@ -9,6 +9,7 @@ export class QueryMaterialsDto { @IsOptional() @IsString() manufacturer?: string; @IsOptional() @IsString() spec?: string; @IsOptional() @IsString() envGrade?: string; + @IsOptional() @IsString() healthGrade?: string; /** 公共库 public | 自建库 self */ @IsOptional() @IsIn(['public', 'self']) scope?: 'public' | 'self'; diff --git a/apps/api/src/materials/dto/update-material.dto.ts b/apps/api/src/materials/dto/update-material.dto.ts index cb720b3..2acb7f2 100644 --- a/apps/api/src/materials/dto/update-material.dto.ts +++ b/apps/api/src/materials/dto/update-material.dto.ts @@ -1,5 +1,5 @@ import { Type } from 'class-transformer'; -import { IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsNumber, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; import { EmissionParamsDto } from './create-material.dto'; export class UpdateMaterialDto { @@ -9,7 +9,9 @@ export class UpdateMaterialDto { @IsOptional() @IsString() manufacturer?: string; @IsOptional() @IsString() spec?: string; @IsOptional() @IsString() envGrade?: string; + @IsOptional() @IsString() healthGrade?: string; @IsOptional() @IsString() usageUnit?: string; + @IsOptional() @IsNumber() sortOrder?: number; @IsOptional() @IsObject() diff --git a/apps/api/src/materials/materials.service.ts b/apps/api/src/materials/materials.service.ts index b545a23..d171a02 100644 --- a/apps/api/src/materials/materials.service.ts +++ b/apps/api/src/materials/materials.service.ts @@ -26,6 +26,7 @@ export class MaterialsService { if (q.manufacturer) where.manufacturer = { contains: q.manufacturer }; if (q.spec) where.spec = { contains: q.spec }; if (q.envGrade) where.envGrade = q.envGrade; + if (q.healthGrade) where.healthGrade = q.healthGrade; if (q.favorited === 'true') { const favIds = await this.favorites.idsOf(orgId, 'material'); @@ -71,7 +72,9 @@ export class MaterialsService { manufacturer: dto.manufacturer, spec: dto.spec, envGrade: dto.envGrade, + healthGrade: dto.healthGrade, usageUnit: dto.usageUnit ?? 'm²', + sortOrder: dto.sortOrder ?? 0, emissionParams: dto.emissionParams as unknown as Prisma.InputJsonValue, isPublic: false, ownerOrgId: orgId, @@ -110,11 +113,14 @@ export class MaterialsService { return 'PM' + n; } - private parseSort(sort?: string): Prisma.MaterialOrderByWithRelationInput { - if (!sort) return { updatedAt: 'desc' }; + private parseSort( + sort?: string, + ): Prisma.MaterialOrderByWithRelationInput | Prisma.MaterialOrderByWithRelationInput[] { + // 默认按手动排序权重(竞价排名)升序,其次最近更新 + if (!sort) return [{ sortOrder: 'asc' }, { updatedAt: 'desc' }]; const [field, dir] = sort.split(':'); - const allowed = ['updatedAt', 'name', 'id', 'envGrade']; - if (!allowed.includes(field)) return { updatedAt: 'desc' }; + const allowed = ['updatedAt', 'name', 'id', 'envGrade', 'healthGrade', 'sortOrder']; + if (!allowed.includes(field)) return [{ sortOrder: 'asc' }, { updatedAt: 'desc' }]; return { [field]: dir === 'asc' ? 'asc' : 'desc' }; } } diff --git a/apps/web/src/api/materials.ts b/apps/web/src/api/materials.ts index 587406e..69b2499 100644 --- a/apps/web/src/api/materials.ts +++ b/apps/web/src/api/materials.ts @@ -9,7 +9,9 @@ export interface Material { manufacturer?: string; spec?: string; envGrade?: string; + healthGrade?: string; usageUnit: string; + sortOrder: number; emissionParams: Record; isPublic: boolean; ownerOrgId?: string; @@ -25,6 +27,7 @@ export interface MaterialQuery { manufacturer?: string; spec?: string; envGrade?: string; + healthGrade?: string; scope?: 'public' | 'self'; favorited?: string; page?: number; @@ -46,7 +49,9 @@ export interface MaterialInput { manufacturer?: string; spec?: string; envGrade?: string; + healthGrade?: string; usageUnit?: string; + sortOrder?: number; emissionParams: Record; } diff --git a/apps/web/src/components/MaterialFormModal.vue b/apps/web/src/components/MaterialFormModal.vue index 38b34dc..2ef2d54 100644 --- a/apps/web/src/components/MaterialFormModal.vue +++ b/apps/web/src/components/MaterialFormModal.vue @@ -28,17 +28,24 @@ - + - + {{ g }} - + + + + {{ g }} 级 + + + + {{ u }} @@ -78,6 +85,7 @@ import { POLLUTANT_LABELS, MATERIAL_CATEGORIES, ENV_GRADES, + HEALTH_GRADES, USAGE_UNITS, type Pollutant, type EmissionParams, @@ -91,6 +99,7 @@ const pollutants = POLLUTANTS; const labels = POLLUTANT_LABELS; const categories = MATERIAL_CATEGORIES; const envGrades = ENV_GRADES; +const healthGrades = HEALTH_GRADES; const units = USAGE_UNITS; const formRef = ref(); @@ -111,6 +120,7 @@ const form = reactive({ manufacturer: '', spec: '', envGrade: undefined, + healthGrade: undefined, usageUnit: 'm²', emissionParams: emptyParams(), }); @@ -128,6 +138,7 @@ watch( manufacturer: props.material.manufacturer, spec: props.material.spec, envGrade: props.material.envGrade, + healthGrade: props.material.healthGrade, usageUnit: props.material.usageUnit, emissionParams: JSON.parse(JSON.stringify(props.material.emissionParams)), }); @@ -135,7 +146,7 @@ watch( isEdit.value = false; Object.assign(form, { name: '', category: '', brand: '', manufacturer: '', spec: '', - envGrade: undefined, usageUnit: 'm²', emissionParams: emptyParams(), + envGrade: undefined, healthGrade: undefined, usageUnit: 'm²', emissionParams: emptyParams(), }); } }, diff --git a/apps/web/src/components/MaterialPickerModal.vue b/apps/web/src/components/MaterialPickerModal.vue index 1a002ef..deabeb0 100644 --- a/apps/web/src/components/MaterialPickerModal.vue +++ b/apps/web/src/components/MaterialPickerModal.vue @@ -5,102 +5,105 @@ 完 成(已加 {{ existingIds.length }}) - -
-
- 材料库: - - 公共库 - 自建库 - - 勾选大类或二级分类,下方生成对应窗口,点击窗口放大快速录入 -
- -
- {{ g.major }} - - {{ s }} -
-
+ +
+ 材料库: + + 公共库 + 自建库 + + 健康等级: + + {{ g }} 级 + + 环保等级: + + {{ g }} +
- + +
+ 大类 + {{ g.major }} +
+ +
+ 子类 + 全部 + {{ s }} +
- -
-
- ← 返回平铺 - {{ catLabel(enlarged) }}({{ (cache[enlarged] || []).length }} 种) - - 批量添加所选({{ checkedCount }}) - -
- - diff --git a/apps/web/src/pages/MaterialLibrary.vue b/apps/web/src/pages/MaterialLibrary.vue index b3cdd34..cc6cd77 100644 --- a/apps/web/src/pages/MaterialLibrary.vue +++ b/apps/web/src/pages/MaterialLibrary.vue @@ -18,6 +18,13 @@ E2 + + + A 级 + B 级 + C 级 + + 重 置 查询 @@ -40,6 +47,10 @@ {{ record.envGrade }} - +