feat: 新增品牌实体 + 材料关联品牌 + Factory.brand 改名 short_name
- 新增 brand app(Brand 模型/CRUD API,读认证用户、写管理员、PROTECT 删除) - Material 新增 brand 外键(PROTECT,数据库可空,前端必填) - Factory.brand 改名 short_name,并附带数据迁移从 factory.short_name 回填 Material.brand 实现历史数据一步到位 - 前端新增品牌库菜单/页面/API,材料管理加品牌列/筛选/表单下拉, 材料详情显示品牌,供应商页面文案同步改为"供应商简称" Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d96140795e
commit
75dde5243e
|
|
@ -0,0 +1,30 @@
|
|||
# Generated migration for brand app
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Brand',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True, verbose_name='品牌名称')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='品牌描述')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '品牌',
|
||||
'verbose_name_plural': '品牌',
|
||||
'db_table': 'brand',
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Brand(models.Model):
|
||||
"""
|
||||
品牌模型
|
||||
"""
|
||||
name = models.CharField(max_length=100, unique=True, verbose_name='品牌名称')
|
||||
description = models.TextField(blank=True, null=True, verbose_name='品牌描述')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '品牌'
|
||||
verbose_name_plural = '品牌'
|
||||
db_table = 'brand'
|
||||
ordering = ['id']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Brand
|
||||
|
||||
|
||||
class BrandSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
品牌序列化器
|
||||
"""
|
||||
material_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Brand
|
||||
fields = ['id', 'name', 'description', 'created_at', 'updated_at', 'material_count']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count']
|
||||
|
||||
def get_material_count(self, obj):
|
||||
return obj.materials.count()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import BrandViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', BrandViewSet, basename='brand')
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
from django.db.models import ProtectedError
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from .models import Brand
|
||||
from .serializers import BrandSerializer
|
||||
|
||||
|
||||
class BrandViewSet(ModelViewSet):
|
||||
"""
|
||||
品牌视图集:所有已认证用户可读,仅管理员可写
|
||||
"""
|
||||
serializer_class = BrandSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Brand.objects.all()
|
||||
search = self.request.query_params.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(name__icontains=search)
|
||||
return queryset
|
||||
|
||||
def _check_admin(self, action_verb):
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionDenied(f"只有管理员可以{action_verb}品牌")
|
||||
|
||||
def perform_create(self, serializer):
|
||||
self._check_admin("创建")
|
||||
serializer.save()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
self._check_admin("修改")
|
||||
serializer.save()
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
if request.user.role != 'admin':
|
||||
raise PermissionDenied("只有管理员可以删除品牌")
|
||||
instance = self.get_object()
|
||||
try:
|
||||
instance.delete()
|
||||
except ProtectedError:
|
||||
return Response(
|
||||
{"detail": "该品牌下存在材料,无法删除"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
@ -219,7 +219,7 @@ class Command(BaseCommand):
|
|||
website = _normalize_website(r.website_raw)
|
||||
|
||||
factory, f_created = Factory.objects.get_or_create(
|
||||
brand=r.brand,
|
||||
short_name=r.brand,
|
||||
defaults={
|
||||
"factory_name": r.factory_name,
|
||||
"province": province or "未知",
|
||||
|
|
@ -255,10 +255,10 @@ class Command(BaseCommand):
|
|||
)
|
||||
if existing_user:
|
||||
ensured_users += 1
|
||||
creds.append((factory.brand, existing_user.username, ""))
|
||||
creds.append((factory.short_name, existing_user.username, ""))
|
||||
continue
|
||||
|
||||
base = _make_username_base(factory.brand)
|
||||
base = _make_username_base(factory.short_name)
|
||||
username = _unique_username(UserModel, base)
|
||||
password = fixed_password or secrets.token_urlsafe(12)
|
||||
|
||||
|
|
@ -269,7 +269,7 @@ class Command(BaseCommand):
|
|||
factory=factory,
|
||||
)
|
||||
created_users += 1
|
||||
creds.append((factory.brand, user.username, password))
|
||||
creds.append((factory.short_name, user.username, password))
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated migration for factory app
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('factory', '0003_rename_factory_short_name_to_brand'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='factory',
|
||||
old_name='brand',
|
||||
new_name='short_name',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='factory',
|
||||
name='short_name',
|
||||
field=models.CharField(max_length=100, unique=True, verbose_name='供应商简称'),
|
||||
),
|
||||
]
|
||||
|
|
@ -7,7 +7,7 @@ class Factory(models.Model):
|
|||
dealer_name = models.CharField(max_length=255, blank=True, null=True, verbose_name='经销商名称')
|
||||
product_category = models.CharField(max_length=255, blank=True, null=True, verbose_name='产品分类')
|
||||
factory_name = models.CharField(max_length=255, verbose_name='生产工厂全称')
|
||||
brand = models.CharField(max_length=100, unique=True, verbose_name='品牌')
|
||||
short_name = models.CharField(max_length=100, unique=True, verbose_name='供应商简称')
|
||||
province = models.CharField(max_length=50, verbose_name='省')
|
||||
city = models.CharField(max_length=50, verbose_name='市')
|
||||
district = models.CharField(max_length=50, blank=True, null=True, verbose_name='区')
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ class FactorySerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'dealer_name', 'product_category', 'factory_name',
|
||||
'brand', 'province', 'city', 'district',
|
||||
fields = ['id', 'dealer_name', 'product_category', 'factory_name',
|
||||
'short_name', 'province', 'city', 'district',
|
||||
'address', 'website', 'created_at', 'updated_at',
|
||||
'material_count', 'usernames']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count', 'usernames']
|
||||
|
|
@ -32,7 +32,7 @@ class FactoryListSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'factory_name', 'brand', 'province', 'city', 'dealer_name', 'usernames']
|
||||
fields = ['id', 'factory_name', 'short_name', 'province', 'city', 'dealer_name', 'usernames']
|
||||
|
||||
def get_usernames(self, obj):
|
||||
return list(obj.users.values_list('username', flat=True))
|
||||
|
|
|
|||
|
|
@ -229,9 +229,9 @@ def _resolve_factory(
|
|||
break
|
||||
term_lower = term.lower()
|
||||
for factory in all_factories:
|
||||
brand = (factory.brand or "").lower()
|
||||
short_name = (factory.short_name or "").lower()
|
||||
full_name = (factory.factory_name or "").lower()
|
||||
if (brand and brand in term_lower) or (full_name and full_name in term_lower):
|
||||
if (short_name and short_name in term_lower) or (full_name and full_name in term_lower):
|
||||
matched_factory = factory
|
||||
break
|
||||
if matched_factory:
|
||||
|
|
|
|||
|
|
@ -195,15 +195,15 @@ class Command(BaseCommand):
|
|||
if f:
|
||||
factory_cache[brand_name] = f
|
||||
return f
|
||||
# 2) 模糊:工厂品牌包含 Excel 值(如 Excel「立邦」匹配「立邦中国」)
|
||||
f = Factory.objects.filter(brand__icontains=brand_name).first()
|
||||
# 2) 模糊:供应商简称包含 Excel 值(如 Excel「立邦」匹配「立邦中国」)
|
||||
f = Factory.objects.filter(short_name__icontains=brand_name).first()
|
||||
if f:
|
||||
factory_cache[brand_name] = f
|
||||
return f
|
||||
# 3) 模糊:Excel 值包含某工厂品牌(如 Excel「立邦中国\n广州立邦」取首行后仍可匹配)
|
||||
# 3) 模糊:Excel 值包含某供应商简称(如 Excel「立邦中国\n广州立邦」取首行后仍可匹配)
|
||||
brand_lower = brand_name.lower()
|
||||
for factory in Factory.objects.all():
|
||||
if factory.brand and factory.brand.lower() in brand_lower:
|
||||
if factory.short_name and factory.short_name.lower() in brand_lower:
|
||||
factory_cache[brand_name] = factory
|
||||
return factory
|
||||
factory_cache[brand_name] = None
|
||||
|
|
@ -212,7 +212,7 @@ class Command(BaseCommand):
|
|||
# 未识别品牌工厂:品牌列无法匹配到任一工厂时,统一关联到此工厂(不存在则创建)
|
||||
UNRECOGNIZED_BRAND = "未识别的品牌"
|
||||
unrecognized_factory, _ = Factory.objects.get_or_create(
|
||||
brand=UNRECOGNIZED_BRAND,
|
||||
short_name=UNRECOGNIZED_BRAND,
|
||||
defaults={
|
||||
"factory_name": "未识别的品牌工厂",
|
||||
"province": "-",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# Generated migration for material app
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('brand', '0001_initial'),
|
||||
('material', '0006_alter_material_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='material',
|
||||
name='brand',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.deletion.PROTECT,
|
||||
related_name='materials',
|
||||
to='brand.brand',
|
||||
verbose_name='品牌',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Data migration: 从 Factory.short_name 的唯一值创建 Brand,并回填 Material.brand
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
Factory = apps.get_model('factory', 'Factory')
|
||||
Brand = apps.get_model('brand', 'Brand')
|
||||
Material = apps.get_model('material', 'Material')
|
||||
|
||||
short_names = (
|
||||
Factory.objects
|
||||
.exclude(short_name__isnull=True)
|
||||
.exclude(short_name='')
|
||||
.values_list('short_name', flat=True)
|
||||
.distinct()
|
||||
)
|
||||
for sn in short_names:
|
||||
Brand.objects.get_or_create(name=sn)
|
||||
|
||||
for brand in Brand.objects.all():
|
||||
Material.objects.filter(factory__short_name=brand.name).update(brand=brand)
|
||||
|
||||
|
||||
def backwards(apps, schema_editor):
|
||||
# schema migration 回滚时会删掉 Material.brand 字段,这里无需操作
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('material', '0007_add_brand_fk'),
|
||||
('factory', '0004_rename_brand_to_short_name'),
|
||||
('brand', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards, backwards),
|
||||
]
|
||||
|
|
@ -87,6 +87,7 @@ class Material(models.Model):
|
|||
construction_method = models.CharField(max_length=255, blank=True, null=True, verbose_name='施工工艺')
|
||||
limit_condition = models.TextField(blank=True, null=True, verbose_name='限制条件')
|
||||
factory = models.ForeignKey('factory.Factory', on_delete=models.CASCADE, related_name='materials', verbose_name='材料单位名称')
|
||||
brand = models.ForeignKey('brand.Brand', on_delete=models.PROTECT, null=True, blank=True, related_name='materials', verbose_name='品牌')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name='状态')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from rest_framework import serializers
|
||||
from apps.brand.models import Brand
|
||||
from .models import Material, MaterialCategory, MaterialSubcategory
|
||||
|
||||
|
||||
|
|
@ -18,7 +19,11 @@ class MaterialSerializer(serializers.ModelSerializer):
|
|||
材料序列化器
|
||||
"""
|
||||
factory_name = serializers.CharField(source='factory.factory_name', read_only=True)
|
||||
brand = serializers.CharField(source='factory.brand', read_only=True)
|
||||
factory_short_name = serializers.CharField(source='factory.short_name', read_only=True)
|
||||
brand = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Brand.objects.all(), allow_null=True, required=False
|
||||
)
|
||||
brand_name = serializers.CharField(source='brand.name', 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)
|
||||
|
|
@ -51,7 +56,8 @@ class MaterialSerializer(serializers.ModelSerializer):
|
|||
'advantage_desc', 'cost_compare', 'cost_desc', 'cases', 'brochure',
|
||||
'brochure_url', 'quality_level', 'durability_level', 'eco_level',
|
||||
'carbon_level', 'score_level', 'connection_method', 'construction_method',
|
||||
'limit_condition', 'factory', 'factory_name', 'brand',
|
||||
'limit_condition', 'factory', 'factory_name', 'factory_short_name',
|
||||
'brand', 'brand_name',
|
||||
'status', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
|
@ -82,7 +88,8 @@ class MaterialListSerializer(serializers.ModelSerializer):
|
|||
材料列表序列化器(简化版)
|
||||
"""
|
||||
factory_name = serializers.CharField(source='factory.factory_name', read_only=True)
|
||||
brand = serializers.CharField(source='factory.brand', read_only=True)
|
||||
factory_short_name = serializers.CharField(source='factory.short_name', read_only=True)
|
||||
brand_name = serializers.CharField(source='brand.name', 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)
|
||||
|
|
@ -94,7 +101,8 @@ class MaterialListSerializer(serializers.ModelSerializer):
|
|||
'material_category', 'material_subcategory', 'stage', 'stage_display',
|
||||
'importance_level', 'importance_level_display', 'landing_project',
|
||||
'contact_person', 'contact_phone', 'handler', 'remark', 'factory',
|
||||
'factory_name', 'brand', 'status', 'status_display']
|
||||
'factory_name', 'factory_short_name', 'brand', 'brand_name',
|
||||
'status', 'status_display']
|
||||
|
||||
|
||||
class MaterialCategorySerializer(serializers.ModelSerializer):
|
||||
|
|
|
|||
|
|
@ -170,6 +170,11 @@ class MaterialViewSet(ModelViewSet):
|
|||
if factory_id:
|
||||
queryset = queryset.filter(factory_id=factory_id)
|
||||
|
||||
# 支持按品牌过滤
|
||||
brand_id = self.request.query_params.get('brand')
|
||||
if brand_id:
|
||||
queryset = queryset.filter(brand_id=brand_id)
|
||||
|
||||
# 支持按专业类别过滤
|
||||
major_category = self.request.query_params.get('major_category')
|
||||
if major_category:
|
||||
|
|
@ -373,7 +378,7 @@ class MaterialViewSet(ModelViewSet):
|
|||
material.contact_person or "",
|
||||
material.contact_phone or "",
|
||||
material.handler or "",
|
||||
material.factory.brand if material.factory else "",
|
||||
material.factory.short_name if material.factory else "",
|
||||
material.factory.factory_name if material.factory else "",
|
||||
material.spec or "",
|
||||
material.standard or "",
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ def factory_statistics(request):
|
|||
})
|
||||
|
||||
factories_list = list(Factory.objects.values(
|
||||
'id', 'factory_name', 'brand', 'province', 'city', 'website'
|
||||
'id', 'factory_name', 'short_name', 'province', 'city', 'website'
|
||||
))
|
||||
|
||||
return Response({
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ INSTALLED_APPS = [
|
|||
'corsheaders',
|
||||
'apps.authentication',
|
||||
'apps.factory',
|
||||
'apps.brand',
|
||||
'apps.material',
|
||||
'apps.dictionary',
|
||||
'apps.statistics',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ urlpatterns = [
|
|||
path('admin/', admin.site.urls),
|
||||
path('api/auth/', include('apps.authentication.urls')),
|
||||
path('api/factory/', include('apps.factory.urls')),
|
||||
path('api/brand/', include('apps.brand.urls')),
|
||||
path('api/material/', include('apps.material.urls')),
|
||||
path('api/dictionary/', include('apps.dictionary.urls')),
|
||||
path('api/statistics/', include('apps.statistics.urls')),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import api from './client'
|
||||
|
||||
export const fetchBrands = async (params) => {
|
||||
const { data } = await api.get('/brand/', { params })
|
||||
return data
|
||||
}
|
||||
|
||||
export const fetchBrandDetail = async (id) => {
|
||||
const { data } = await api.get(`/brand/${id}/`)
|
||||
return data
|
||||
}
|
||||
|
||||
export const createBrand = async (payload) => {
|
||||
const { data } = await api.post('/brand/', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export const updateBrand = async (id, payload) => {
|
||||
const { data } = await api.put(`/brand/${id}/`, payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export const deleteBrand = async (id) => {
|
||||
const { data } = await api.delete(`/brand/${id}/`)
|
||||
return data
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
>
|
||||
<el-menu-item v-if="isAdmin" index="/users">用户管理</el-menu-item>
|
||||
<el-menu-item index="/factories">供应商库</el-menu-item>
|
||||
<el-menu-item v-if="isAdmin" index="/brands">品牌库</el-menu-item>
|
||||
<el-menu-item index="/materials">材料管理</el-menu-item>
|
||||
<el-menu-item v-if="isAdmin" index="/screen/overview">数据大屏</el-menu-item>
|
||||
<el-sub-menu v-if="isAdmin" index="config">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const routes = [
|
|||
{ path: 'users', name: 'users', component: () => import('@/views/UserManage.vue'), meta: { admin: true } },
|
||||
{ path: 'factories', name: 'factories', component: () => import('@/views/FactoryManage.vue') },
|
||||
{ path: 'factories/:id', name: 'factory-detail', component: () => import('@/views/FactoryDetail.vue') },
|
||||
{ path: 'brands', name: 'brands', component: () => import('@/views/BrandManage.vue'), meta: { admin: true } },
|
||||
{ path: 'dictionary', name: 'dictionary', component: () => import('@/views/DictionaryManage.vue'), meta: { admin: true } },
|
||||
{ path: 'materials', name: 'materials', component: () => import('@/views/MaterialManage.vue') },
|
||||
{ path: 'materials/:id', name: 'material-detail', component: () => import('@/views/MaterialDetail.vue') }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
<template>
|
||||
<div class="page">
|
||||
<div class="page-title">品牌库</div>
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="filters.search"
|
||||
placeholder="按品牌名称搜索"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="onSearch"
|
||||
@clear="onSearch"
|
||||
/>
|
||||
<el-button type="primary" @click="onSearch">搜索</el-button>
|
||||
<el-button v-if="isAdmin" type="primary" @click="openCreate">新增品牌</el-button>
|
||||
</div>
|
||||
<el-table v-loading="tableLoading" :data="brands" border :max-height="560">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="品牌名称" min-width="180" />
|
||||
<el-table-column prop="description" label="品牌描述" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="material_count" label="关联材料数" width="120" />
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" v-if="isAdmin">
|
||||
<template #default="scope">
|
||||
<div class="table-actions">
|
||||
<el-button size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="onDelete(scope.row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@current-change="onPageChange"
|
||||
@size-change="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" class="dialog-scroll">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="品牌名称" required>
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAuth } from '@/store/auth'
|
||||
import { fetchBrands, createBrand, updateBrand, deleteBrand } from '@/api/brand'
|
||||
|
||||
const { isAdmin } = useAuth()
|
||||
const brands = ref([])
|
||||
const tableLoading = ref(false)
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const filters = reactive({ search: '' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentId = ref(null)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const formatDate = (val) => {
|
||||
if (!val) return ''
|
||||
const d = new Date(val)
|
||||
if (Number.isNaN(d.getTime())) return val
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
const loadBrands = async () => {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize
|
||||
}
|
||||
if (filters.search) params.search = filters.search
|
||||
const data = await fetchBrands(params)
|
||||
brands.value = data.results || data
|
||||
pagination.total = data.count || brands.value.length
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.name = ''
|
||||
form.description = ''
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
currentId.value = null
|
||||
dialogTitle.value = '新增品牌'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEdit = (row) => {
|
||||
resetForm()
|
||||
isEdit.value = true
|
||||
currentId.value = row.id
|
||||
form.name = row.name || ''
|
||||
form.description = row.description || ''
|
||||
dialogTitle.value = '编辑品牌'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!form.name.trim()) {
|
||||
ElMessage.warning('请填写品牌名称')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload = { name: form.name.trim(), description: form.description || '' }
|
||||
if (isEdit.value) {
|
||||
await updateBrand(currentId.value, payload)
|
||||
} else {
|
||||
await createBrand(payload)
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
loadBrands()
|
||||
} catch (error) {
|
||||
const detail = error.response?.data?.detail
|
||||
const nameErr = error.response?.data?.name?.[0]
|
||||
ElMessage.error(detail || nameErr || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const onDelete = (row) => {
|
||||
ElMessageBox.confirm(`确认删除品牌 ${row.name} 吗?`, '提示', { type: 'warning' })
|
||||
.then(async () => {
|
||||
try {
|
||||
await deleteBrand(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadBrands()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '删除失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
pagination.page = 1
|
||||
loadBrands()
|
||||
}
|
||||
|
||||
const onPageChange = (page) => {
|
||||
pagination.page = page
|
||||
loadBrands()
|
||||
}
|
||||
|
||||
const onPageSizeChange = (size) => {
|
||||
pagination.pageSize = size
|
||||
pagination.page = 1
|
||||
loadBrands()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBrands()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.dialog-scroll :deep(.el-dialog__body) {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<div class="card detail-card" v-if="factory">
|
||||
<el-descriptions :column="1" border class="detail-descriptions">
|
||||
<el-descriptions-item label="供应商全称">{{ displayText(factory.factory_name) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="品牌">{{ displayText(factory.brand) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商简称">{{ displayText(factory.short_name) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="经销商">{{ displayText(factory.dealer_name) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品分类">{{ displayText(factory.product_category) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地区">{{ displayRegion(factory) }}</el-descriptions-item>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
</div>
|
||||
<el-table v-loading="tableLoading" :data="factories" border :max-height="560">
|
||||
<el-table-column prop="factory_name" label="供应商全称" />
|
||||
<el-table-column prop="brand" label="品牌" />
|
||||
<el-table-column prop="short_name" label="供应商简称" />
|
||||
<el-table-column prop="dealer_name" label="经销商" />
|
||||
<el-table-column label="用户账号">
|
||||
<template #default="scope">
|
||||
|
|
@ -52,8 +52,8 @@
|
|||
<el-form-item label="供应商全称" required>
|
||||
<el-input v-model="form.factory_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌" required>
|
||||
<el-input v-model="form.brand" />
|
||||
<el-form-item label="供应商简称" required>
|
||||
<el-input v-model="form.short_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="省市区" required>
|
||||
<el-cascader
|
||||
|
|
@ -109,7 +109,7 @@ const form = reactive({
|
|||
dealer_name: '',
|
||||
product_category: '',
|
||||
factory_name: '',
|
||||
brand: '',
|
||||
short_name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
|
|
@ -132,7 +132,7 @@ const resetForm = () => {
|
|||
form.dealer_name = ''
|
||||
form.product_category = ''
|
||||
form.factory_name = ''
|
||||
form.brand = ''
|
||||
form.short_name = ''
|
||||
form.province = ''
|
||||
form.city = ''
|
||||
form.district = ''
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
<el-descriptions-item label="成本说明">{{ displayText(material.cost_desc) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="案例">{{ displayText(material.cases) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属供应商">{{ displayText(material.factory_name) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="品牌">{{ displayText(material.brand_name) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="质量等级">{{ formatStarLevel(material.quality_level) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="耐久等级">{{ formatStarLevel(material.durability_level) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="环保等级">{{ formatStarLevel(material.eco_level) }}</el-descriptions-item>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@
|
|||
<el-select v-model="filters.material_subcategory" placeholder="材料子类" clearable style="width: 180px">
|
||||
<el-option v-for="item in filterSubcategoryOptions" :key="item.value" :label="item.name" :value="item.value" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filters.brand"
|
||||
placeholder="品牌"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchBrands"
|
||||
:loading="brandSearchLoading"
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option v-for="item in brandFilterOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<el-button @click="loadMaterials">查询</el-button>
|
||||
<el-button v-if="isAdmin" :loading="importing" @click="importDialogVisible = true">
|
||||
{{ importing ? '导入中...' : '导入数据' }}
|
||||
|
|
@ -32,7 +44,8 @@
|
|||
<el-table-column prop="contact_phone" label="对接人联系方式" width="130px"/>
|
||||
<el-table-column prop="handler" label="经办人" />
|
||||
<el-table-column prop="remark" label="备注" />
|
||||
<el-table-column prop="brand" label="材料单位名称" />
|
||||
<el-table-column prop="factory_short_name" label="材料单位名称" />
|
||||
<el-table-column prop="brand_name" label="品牌" />
|
||||
<el-table-column prop="status_display" label="状态" width="120" />
|
||||
<el-table-column label="操作" width="320">
|
||||
<template #default="scope">
|
||||
|
|
@ -191,7 +204,19 @@
|
|||
</el-form-item>
|
||||
<el-form-item label="材料单位名称" v-if="isAdmin">
|
||||
<el-select v-model="form.factory">
|
||||
<el-option v-for="item in factories" :key="item.id" :label="item.brand" :value="item.id" />
|
||||
<el-option v-for="item in factories" :key="item.id" :label="item.short_name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌" required>
|
||||
<el-select
|
||||
v-model="form.brand"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchBrandsForForm"
|
||||
:loading="brandFormSearchLoading"
|
||||
placeholder="请选择品牌"
|
||||
>
|
||||
<el-option v-for="item in brandFormOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
|
@ -230,6 +255,7 @@ import { useAuth } from '@/store/auth'
|
|||
import { fetchMaterials, fetchMaterialDetail, createMaterial, updateMaterial, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial, fetchMaterialChoices, uploadImage, importMaterialsExcel, exportMaterialsExcel } from '@/api/material'
|
||||
import { fetchCategories, fetchSubcategories } from '@/api/category'
|
||||
import { fetchFactorySimple } from '@/api/factory'
|
||||
import { fetchBrands } from '@/api/brand'
|
||||
|
||||
const router = useRouter()
|
||||
const { isAdmin } = useAuth()
|
||||
|
|
@ -255,9 +281,15 @@ const templateDownloadUrl = `http://101.42.1.64:2260/media/material_import_templ
|
|||
const filters = reactive({
|
||||
name: '',
|
||||
status: '',
|
||||
material_subcategory: ''
|
||||
material_subcategory: '',
|
||||
brand: ''
|
||||
})
|
||||
|
||||
const brandFilterOptions = ref([])
|
||||
const brandFormOptions = ref([])
|
||||
const brandSearchLoading = ref(false)
|
||||
const brandFormSearchLoading = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
major_category: '',
|
||||
|
|
@ -290,7 +322,8 @@ const form = reactive({
|
|||
connection_method: '',
|
||||
construction_method: '',
|
||||
limit_condition: '',
|
||||
factory: null
|
||||
factory: null,
|
||||
brand: null
|
||||
})
|
||||
|
||||
const majorOptions = ref([])
|
||||
|
|
@ -392,10 +425,43 @@ const resetForm = () => {
|
|||
connection_method: '',
|
||||
construction_method: '',
|
||||
limit_condition: '',
|
||||
factory: null
|
||||
factory: null,
|
||||
brand: null
|
||||
})
|
||||
}
|
||||
|
||||
const loadBrandFilterOptions = async () => {
|
||||
const data = await fetchBrands({ page_size: 100 })
|
||||
brandFilterOptions.value = data.results || data
|
||||
}
|
||||
|
||||
const searchBrands = async (query) => {
|
||||
brandSearchLoading.value = true
|
||||
try {
|
||||
const data = await fetchBrands({ page_size: 50, search: query || '' })
|
||||
brandFilterOptions.value = data.results || data
|
||||
} finally {
|
||||
brandSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchBrandsForForm = async (query) => {
|
||||
brandFormSearchLoading.value = true
|
||||
try {
|
||||
const data = await fetchBrands({ page_size: 50, search: query || '' })
|
||||
brandFormOptions.value = data.results || data
|
||||
} finally {
|
||||
brandFormSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const ensureBrandInFormOptions = (brandId, brandName) => {
|
||||
if (!brandId) return
|
||||
if (!brandFormOptions.value.some((item) => item.id === brandId)) {
|
||||
brandFormOptions.value = [{ id: brandId, name: brandName || '' }, ...brandFormOptions.value]
|
||||
}
|
||||
}
|
||||
|
||||
const onCategoryChange = async (val, resetSub = true) => {
|
||||
if (!val) {
|
||||
await loadSubcategories()
|
||||
|
|
@ -410,10 +476,11 @@ const onCategoryChange = async (val, resetSub = true) => {
|
|||
}
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
const openCreate = async () => {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增材料'
|
||||
await searchBrandsForForm('')
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
|
|
@ -426,9 +493,12 @@ const openEdit = async (row) => {
|
|||
form.application_scene = item.application_scene || []
|
||||
form.advantage = item.advantage || []
|
||||
form.brochure_url = item.brochure_url || ''
|
||||
form.brand = item.brand || null
|
||||
if (form.material_category) {
|
||||
await onCategoryChange(form.material_category, false)
|
||||
}
|
||||
await searchBrandsForForm('')
|
||||
ensureBrandInFormOptions(item.brand, item.brand_name)
|
||||
dialogTitle.value = '编辑材料'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
|
@ -448,9 +518,14 @@ const handleUpload = async (options) => {
|
|||
}
|
||||
|
||||
const onSave = async () => {
|
||||
if (!form.brand) {
|
||||
ElMessage.warning('请选择品牌')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload = { ...form }
|
||||
delete payload.brochure_url
|
||||
delete payload.brand_name
|
||||
if (!isAdmin.value) {
|
||||
delete payload.factory
|
||||
}
|
||||
|
|
@ -580,6 +655,7 @@ onMounted(() => {
|
|||
loadCategories()
|
||||
loadSubcategories()
|
||||
loadFactories()
|
||||
loadBrandFilterOptions()
|
||||
loadMaterials()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue