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: [],