From a6456966b6f431f9db0b23e5aabc92cc25dd5f2a Mon Sep 17 00:00:00 2001 From: shijing Date: Wed, 18 Mar 2026 16:39:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=9D=90=E6=96=99=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E2=80=9C=E9=98=B6=E6=AE=B5=E2=80=9D=E5=92=8C=E2=80=9C=E9=87=8D?= =?UTF-8?q?=E8=A6=81=E7=AD=89=E7=BA=A7=E2=80=9D=E4=B8=A4=E5=88=97=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E4=B8=94=E8=BF=94=E5=9B=9E=E6=8C=89createtime?= =?UTF-8?q?=E5=80=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/import_materials_from_excel.py | 11 +++++++++ .../0004_alter_material_options_and_more.py | 23 +++++++++++++++++++ backend/apps/material/models.py | 17 ++++++++++++++ backend/apps/material/serializers.py | 10 ++++++-- backend/apps/material/views.py | 4 +++- backend/config/settings.py | 23 +++++++++++++++++-- frontend/src/views/MaterialDetail.vue | 2 ++ frontend/src/views/MaterialManage.vue | 20 ++++++++++++++++ 8 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 backend/apps/material/migrations/0004_alter_material_options_and_more.py diff --git a/backend/apps/material/management/commands/import_materials_from_excel.py b/backend/apps/material/management/commands/import_materials_from_excel.py index 872647e..777f5e5 100644 --- a/backend/apps/material/management/commands/import_materials_from_excel.py +++ b/backend/apps/material/management/commands/import_materials_from_excel.py @@ -36,6 +36,8 @@ REPLACE_TYPE_MAP = {"平替": "alternative", "新研发": "new_development"} ADVANTAGE_MAP = {"品质": "quality", "成本": "cost"} # 星级:中文 -> 1/2/3 STAR_LEVEL_MAP = {"一": 1, "二": 2, "三": 3, "1": 1, "2": 2, "3": 3} +STAGE_VALUES = {choice[0] for choice in Material.STAGE_CHOICES} +IMPORTANCE_LEVEL_VALUES = {choice[0] for choice in Material.IMPORTANCE_LEVEL_CHOICES} def _cell(v: Any) -> str: @@ -88,6 +90,13 @@ def _parse_cost_compare(s: str) -> Optional[Decimal]: return None +def _parse_choice(s: str, allowed_values: set) -> Optional[str]: + value = _cell(s) + if not value: + return None + return value if value in allowed_values else None + + def _first_line(s: str) -> str: if not s: return "" @@ -248,6 +257,8 @@ class Command(BaseCommand): "major_category": major_category, "material_category": material_category, "material_subcategory": material_subcategory, + "stage": _parse_choice(get(row, "阶段"), STAGE_VALUES), + "importance_level": _parse_choice(get(row, "重要等级"), IMPORTANCE_LEVEL_VALUES), "spec": _single_line(get(row, "规格型号")) or None, "standard": _single_line(get(row, "符合标准")) or None, "application_scene": application_scene or None, diff --git a/backend/apps/material/migrations/0004_alter_material_options_and_more.py b/backend/apps/material/migrations/0004_alter_material_options_and_more.py new file mode 100644 index 0000000..b5cacb3 --- /dev/null +++ b/backend/apps/material/migrations/0004_alter_material_options_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2026-03-18 07:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('material', '0003_materialcategory_materialsubcategory'), + ] + + operations = [ + migrations.AddField( + model_name='material', + name='importance_level', + field=models.CharField(blank=True, choices=[('核心', '核心'), ('优先', '优先'), ('一般', '一般'), ('观察/受限', '观察/受限')], max_length=20, null=True, verbose_name='重要等级'), + ), + migrations.AddField( + model_name='material', + name='stage', + field=models.CharField(blank=True, choices=[('初步交流阶段', '初步交流阶段'), ('完成上会审议', '完成上会审议'), ('选品上样阶段', '选品上样阶段'), ('招标阶段', '招标阶段'), ('项目落地阶段', '项目落地阶段')], max_length=20, null=True, verbose_name='阶段'), + ), + ] diff --git a/backend/apps/material/models.py b/backend/apps/material/models.py index f76683b..b876f15 100644 --- a/backend/apps/material/models.py +++ b/backend/apps/material/models.py @@ -41,10 +41,27 @@ class Material(models.Model): ('approved', '已审核'), ) + STAGE_CHOICES = ( + ('初步交流阶段', '初步交流阶段'), + ('完成上会审议', '完成上会审议'), + ('选品上样阶段', '选品上样阶段'), + ('招标阶段', '招标阶段'), + ('项目落地阶段', '项目落地阶段'), + ) + + IMPORTANCE_LEVEL_CHOICES = ( + ('核心', '核心'), + ('优先', '优先'), + ('一般', '一般'), + ('观察/受限', '观察/受限'), + ) + name = models.CharField(max_length=255, verbose_name='材料名称') major_category = models.CharField(max_length=20, choices=MAJOR_CATEGORY_CHOICES, verbose_name='专业类别') material_category = models.CharField(max_length=255, verbose_name='材料分类') material_subcategory = models.CharField(max_length=255, verbose_name='材料子分类') + stage = models.CharField(max_length=20, choices=STAGE_CHOICES, blank=True, null=True, verbose_name='阶段') + importance_level = models.CharField(max_length=20, choices=IMPORTANCE_LEVEL_CHOICES, blank=True, null=True, verbose_name='重要等级') spec = models.CharField(max_length=255, blank=True, null=True, verbose_name='规格型号') standard = models.CharField(max_length=255, blank=True, null=True, verbose_name='符合标准') application_scene = models.JSONField(default=list, blank=True, null=True, verbose_name='应用场景') diff --git a/backend/apps/material/serializers.py b/backend/apps/material/serializers.py index b86e01b..e1f2f3c 100644 --- a/backend/apps/material/serializers.py +++ b/backend/apps/material/serializers.py @@ -21,6 +21,8 @@ class MaterialSerializer(serializers.ModelSerializer): brand = serializers.CharField(source='factory.brand', read_only=True) major_category_display = serializers.CharField(source='get_major_category_display', read_only=True) replace_type_display = serializers.CharField(source='get_replace_type_display', read_only=True) + stage_display = serializers.CharField(source='get_stage_display', read_only=True) + importance_level_display = serializers.CharField(source='get_importance_level_display', read_only=True) application_scene = JSONListField( child=serializers.ChoiceField(choices=Material.APPLICATION_SCENE_CHOICES), required=False, @@ -41,7 +43,8 @@ class MaterialSerializer(serializers.ModelSerializer): class Meta: model = Material fields = ['id', 'name', 'major_category', 'major_category_display', - 'material_category', 'material_subcategory', 'spec', 'standard', + 'material_category', 'material_subcategory', 'stage', 'stage_display', + 'importance_level', 'importance_level_display', 'spec', 'standard', 'application_scene', 'application_scene_display', 'application_desc', 'replace_type', 'replace_type_display', 'advantage', 'advantage_display', 'advantage_desc', 'cost_compare', 'cost_desc', 'cases', 'brochure', @@ -81,11 +84,14 @@ class MaterialListSerializer(serializers.ModelSerializer): brand = serializers.CharField(source='factory.brand', read_only=True) major_category_display = serializers.CharField(source='get_major_category_display', read_only=True) status_display = serializers.CharField(source='get_status_display', read_only=True) + stage_display = serializers.CharField(source='get_stage_display', read_only=True) + importance_level_display = serializers.CharField(source='get_importance_level_display', read_only=True) class Meta: model = Material fields = ['id', 'name', 'major_category', 'major_category_display', - 'material_category', 'material_subcategory', 'factory', + 'material_category', 'material_subcategory', 'stage', 'stage_display', + 'importance_level', 'importance_level_display', 'factory', 'factory_name', 'brand', 'status', 'status_display'] diff --git a/backend/apps/material/views.py b/backend/apps/material/views.py index 37f2d91..fc60004 100644 --- a/backend/apps/material/views.py +++ b/backend/apps/material/views.py @@ -18,7 +18,7 @@ class MaterialViewSet(ModelViewSet): """ 根据用户角色过滤材料 """ - queryset = Material.objects.all() + queryset = Material.objects.all().order_by('-created_at', '-id') # 普通用户只能看到自己工厂的材料 if self.request.user.role != 'admin': @@ -190,6 +190,8 @@ class MaterialViewSet(ModelViewSet): """ return Response({ 'major_category': Material.MAJOR_CATEGORY_CHOICES, + 'stage': Material.STAGE_CHOICES, + 'importance_level': Material.IMPORTANCE_LEVEL_CHOICES, 'replace_type': Material.REPLACE_TYPE_CHOICES, 'advantage': Material.ADVANTAGE_CHOICES, 'application_scene': Material.APPLICATION_SCENE_CHOICES, diff --git a/backend/config/settings.py b/backend/config/settings.py index a2887a3..bb46a51 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -6,6 +6,21 @@ from pathlib import Path from decouple import config import os + +def _cast_debug(value): + if isinstance(value, bool): + return value + + normalized = str(value).strip().lower() + truthy = {'1', 'true', 't', 'yes', 'y', 'on', 'debug', 'dev', 'development'} + falsy = {'0', 'false', 'f', 'no', 'n', 'off', 'release', 'prod', 'production'} + + if normalized in truthy: + return True + if normalized in falsy: + return False + raise ValueError(f"Invalid DEBUG value: {value}") + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -16,9 +31,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = config('SECRET_KEY', default='django-insecure-change-in-production') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = config('DEBUG', default=True, cast=bool) +DEBUG = config('DEBUG', default=True, cast=_cast_debug) -ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')]) +ALLOWED_HOSTS = config( + 'ALLOWED_HOSTS', + default='localhost,127.0.0.1,testserver', + cast=lambda v: [s.strip() for s in v.split(',')], +) # Application definition diff --git a/frontend/src/views/MaterialDetail.vue b/frontend/src/views/MaterialDetail.vue index a46f4e8..298e870 100644 --- a/frontend/src/views/MaterialDetail.vue +++ b/frontend/src/views/MaterialDetail.vue @@ -10,6 +10,8 @@ {{ displayText(material.major_category_display) }} {{ displayText(material.material_category) }} {{ displayText(material.material_subcategory) }} + {{ displayText(material.stage_display) }} + {{ displayText(material.importance_level_display) }} {{ displayText(material.spec) }} {{ displayText(material.standard) }} {{ displayList(material.application_scene_display) }} diff --git a/frontend/src/views/MaterialManage.vue b/frontend/src/views/MaterialManage.vue index 5603b21..f9f0567 100644 --- a/frontend/src/views/MaterialManage.vue +++ b/frontend/src/views/MaterialManage.vue @@ -18,6 +18,8 @@ + + @@ -66,6 +68,16 @@ + + + + + + + + + + @@ -200,6 +212,8 @@ const form = reactive({ major_category: '', material_category: '', material_subcategory: '', + stage: '', + importance_level: '', spec: '', standard: '', application_scene: [], @@ -224,6 +238,8 @@ const form = reactive({ }) const majorOptions = ref([]) +const stageOptions = ref([]) +const importanceLevelOptions = ref([]) const replaceOptions = ref([]) const advantageOptions = ref([]) const sceneOptions = ref([]) @@ -252,6 +268,8 @@ const loadMaterials = async () => { const loadChoices = async () => { const data = await fetchMaterialChoices() majorOptions.value = data.major_category + stageOptions.value = data.stage + importanceLevelOptions.value = data.importance_level replaceOptions.value = data.replace_type advantageOptions.value = data.advantage sceneOptions.value = data.application_scene @@ -291,6 +309,8 @@ const resetForm = () => { major_category: '', material_category: '', material_subcategory: '', + stage: '', + importance_level: '', spec: '', standard: '', application_scene: [],