From 80a8f69edfd0ec34691fab6adc4c03c314fd3f79 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 10 Mar 2026 16:40:21 +0800 Subject: [PATCH] feat: add material categories and polish UI --- ...lter_user_managers_user_groups_and_more.py | 43 + backend/apps/authentication/serializers.py | 6 + backend/apps/authentication/views.py | 23 +- backend/apps/dictionary/views.py | 13 +- backend/apps/factory/views.py | 9 +- .../0002_alter_material_advantage_and_more.py | 23 + ...03_materialcategory_materialsubcategory.py | 45 + backend/apps/material/models.py | 41 +- backend/apps/material/serializers.py | 68 +- backend/apps/material/urls.py | 6 +- backend/apps/material/views.py | 100 +- backend/apps/statistics/views.py | 133 +- frontend/.env.development | 1 + frontend/index.html | 12 + frontend/package-lock.json | 1736 +++++++++++++++++ frontend/package.json | 23 + frontend/src/App.vue | 3 + frontend/src/api/auth.js | 31 + frontend/src/api/category.js | 41 + frontend/src/api/client.js | 28 + frontend/src/api/dictionary.js | 26 + frontend/src/api/factory.js | 31 + frontend/src/api/material.js | 65 + frontend/src/api/statistics.js | 16 + frontend/src/layouts/MainLayout.vue | 148 ++ frontend/src/layouts/ScreenLayout.vue | 41 + frontend/src/main.js | 11 + frontend/src/router/index.js | 68 + frontend/src/store/auth.js | 30 + frontend/src/styles/base.css | 117 ++ frontend/src/styles/screen.css | 205 ++ frontend/src/utils/region.js | 28 + frontend/src/views/DictionaryManage.vue | 235 +++ frontend/src/views/FactoryDetail.vue | 50 + frontend/src/views/FactoryManage.vue | 171 ++ frontend/src/views/Login.vue | 92 + frontend/src/views/MaterialDetail.vue | 77 + frontend/src/views/MaterialManage.vue | 403 ++++ frontend/src/views/NotFound.vue | 6 + frontend/src/views/UserManage.vue | 166 ++ frontend/src/views/screen/ScreenFactories.vue | 109 ++ frontend/src/views/screen/ScreenMaterials.vue | 148 ++ frontend/src/views/screen/ScreenOverview.vue | 173 ++ frontend/vite.config.js | 15 + 44 files changed, 4740 insertions(+), 76 deletions(-) create mode 100644 backend/apps/authentication/migrations/0002_alter_user_managers_user_groups_and_more.py create mode 100644 backend/apps/material/migrations/0002_alter_material_advantage_and_more.py create mode 100644 backend/apps/material/migrations/0003_materialcategory_materialsubcategory.py create mode 100644 frontend/.env.development create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/auth.js create mode 100644 frontend/src/api/category.js create mode 100644 frontend/src/api/client.js create mode 100644 frontend/src/api/dictionary.js create mode 100644 frontend/src/api/factory.js create mode 100644 frontend/src/api/material.js create mode 100644 frontend/src/api/statistics.js create mode 100644 frontend/src/layouts/MainLayout.vue create mode 100644 frontend/src/layouts/ScreenLayout.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/store/auth.js create mode 100644 frontend/src/styles/base.css create mode 100644 frontend/src/styles/screen.css create mode 100644 frontend/src/utils/region.js create mode 100644 frontend/src/views/DictionaryManage.vue create mode 100644 frontend/src/views/FactoryDetail.vue create mode 100644 frontend/src/views/FactoryManage.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/MaterialDetail.vue create mode 100644 frontend/src/views/MaterialManage.vue create mode 100644 frontend/src/views/NotFound.vue create mode 100644 frontend/src/views/UserManage.vue create mode 100644 frontend/src/views/screen/ScreenFactories.vue create mode 100644 frontend/src/views/screen/ScreenMaterials.vue create mode 100644 frontend/src/views/screen/ScreenOverview.vue create mode 100644 frontend/vite.config.js diff --git a/backend/apps/authentication/migrations/0002_alter_user_managers_user_groups_and_more.py b/backend/apps/authentication/migrations/0002_alter_user_managers_user_groups_and_more.py new file mode 100644 index 0000000..2b58aa6 --- /dev/null +++ b/backend/apps/authentication/migrations/0002_alter_user_managers_user_groups_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.7 on 2026-03-10 07:49 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('authentication', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.AddField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'), + ), + migrations.AddField( + model_name='user', + name='user_permissions', + field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), + ), + migrations.AlterField( + model_name='user', + name='date_joined', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/backend/apps/authentication/serializers.py b/backend/apps/authentication/serializers.py index 3549419..c06b563 100644 --- a/backend/apps/authentication/serializers.py +++ b/backend/apps/authentication/serializers.py @@ -33,6 +33,12 @@ class UserCreateSerializer(serializers.ModelSerializer): def validate(self, attrs): if attrs['password'] != attrs['password_confirm']: raise serializers.ValidationError({"password": "密码字段不匹配。"}) + role = attrs.get('role', 'user') + factory = attrs.get('factory') + if role == 'user' and not factory: + raise serializers.ValidationError({"factory": "普通账号必须绑定所属工厂。"}) + if role == 'admin' and factory: + raise serializers.ValidationError({"factory": "管理员账号不应绑定工厂。"}) return attrs def create(self, validated_data): diff --git a/backend/apps/authentication/views.py b/backend/apps/authentication/views.py index cfd716f..795629d 100644 --- a/backend/apps/authentication/views.py +++ b/backend/apps/authentication/views.py @@ -1,8 +1,9 @@ -from rest_framework import generics, status +from rest_framework import generics from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework_simplejwt.views import TokenObtainPairView +from rest_framework.exceptions import PermissionDenied from .models import User from .serializers import UserSerializer, UserCreateSerializer, CustomTokenObtainPairSerializer @@ -12,16 +13,21 @@ class CustomTokenObtainPairView(TokenObtainPairView): 自定义JWT令牌获取视图 """ serializer_class = CustomTokenObtainPairSerializer + permission_classes = [AllowAny] class UserListView(generics.ListCreateAPIView): """ 用户列表和创建视图 """ - queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [IsAuthenticated] + def get_queryset(self): + if self.request.user.role == 'admin': + return User.objects.all() + return User.objects.filter(id=self.request.user.id) + def get_serializer_class(self): if self.request.method == 'POST': return UserCreateSerializer @@ -30,7 +36,7 @@ class UserListView(generics.ListCreateAPIView): def perform_create(self, serializer): # 只有管理员可以创建用户 if self.request.user.role != 'admin': - raise PermissionError("只有管理员可以创建用户") + raise PermissionDenied("只有管理员可以创建用户") serializer.save() @@ -45,13 +51,20 @@ class UserDetailView(generics.RetrieveUpdateDestroyAPIView): def perform_update(self, serializer): # 普通用户只能修改自己的信息 if self.request.user.role != 'admin' and self.request.user.id != self.get_object().id: - raise PermissionError("无权修改其他用户信息") + raise PermissionDenied("无权修改其他用户信息") + + if self.request.user.role != 'admin': + allowed_fields = {'first_name', 'last_name', 'email', 'phone'} + for field in list(serializer.validated_data.keys()): + if field not in allowed_fields: + serializer.validated_data.pop(field) + serializer.save() def perform_destroy(self, instance): # 只有管理员可以删除用户 if self.request.user.role != 'admin': - raise PermissionError("只有管理员可以删除用户") + raise PermissionDenied("只有管理员可以删除用户") instance.delete() diff --git a/backend/apps/dictionary/views.py b/backend/apps/dictionary/views.py index 6b4d171..0ae74dc 100644 --- a/backend/apps/dictionary/views.py +++ b/backend/apps/dictionary/views.py @@ -1,10 +1,10 @@ -from rest_framework import generics, status +from rest_framework import generics from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from django.db.models import Q +from rest_framework.exceptions import PermissionDenied from .models import Dictionary -from .serializers import DictionarySerializer, DictionaryGroupSerializer +from .serializers import DictionarySerializer class DictionaryListView(generics.ListCreateAPIView): @@ -35,7 +35,7 @@ class DictionaryListView(generics.ListCreateAPIView): def perform_create(self, serializer): # 只有管理员可以创建字典 if self.request.user.role != 'admin': - raise PermissionError("只有管理员可以创建字典") + raise PermissionDenied("只有管理员可以创建字典") serializer.save() @@ -50,13 +50,13 @@ class DictionaryDetailView(generics.RetrieveUpdateDestroyAPIView): def perform_update(self, serializer): # 只有管理员可以更新字典 if self.request.user.role != 'admin': - raise PermissionError("只有管理员可以更新字典") + raise PermissionDenied("只有管理员可以更新字典") serializer.save() def perform_destroy(self, instance): # 只有管理员可以删除字典 if self.request.user.role != 'admin': - raise PermissionError("只有管理员可以删除字典") + raise PermissionDenied("只有管理员可以删除字典") instance.delete() @@ -66,7 +66,6 @@ def dictionary_grouped(request): """ 获取分组的数据字典 """ - # 获取所有字典类型 dict_types = Dictionary.objects.values('type').distinct() result = [] diff --git a/backend/apps/factory/views.py b/backend/apps/factory/views.py index d54dbce..e4e7b32 100644 --- a/backend/apps/factory/views.py +++ b/backend/apps/factory/views.py @@ -1,7 +1,8 @@ -from rest_framework import generics, status +from rest_framework import generics from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.exceptions import PermissionDenied from .models import Factory from .serializers import FactorySerializer, FactoryListSerializer @@ -21,7 +22,7 @@ class FactoryListView(generics.ListCreateAPIView): def perform_create(self, serializer): # 只有管理员可以创建工厂 if self.request.user.role != 'admin': - raise PermissionError("只有管理员可以创建工厂") + raise PermissionDenied("只有管理员可以创建工厂") serializer.save() @@ -37,13 +38,13 @@ class FactoryDetailView(generics.RetrieveUpdateDestroyAPIView): # 普通用户只能修改自己所属工厂的信息 if (self.request.user.role != 'admin' and self.request.user.factory_id != self.get_object().id): - raise PermissionError("无权修改其他工厂信息") + raise PermissionDenied("无权修改其他工厂信息") serializer.save() def perform_destroy(self, instance): # 只有管理员可以删除工厂 if self.request.user.role != 'admin': - raise PermissionError("只有管理员可以删除工厂") + raise PermissionDenied("只有管理员可以删除工厂") instance.delete() diff --git a/backend/apps/material/migrations/0002_alter_material_advantage_and_more.py b/backend/apps/material/migrations/0002_alter_material_advantage_and_more.py new file mode 100644 index 0000000..606269f --- /dev/null +++ b/backend/apps/material/migrations/0002_alter_material_advantage_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2026-03-10 07:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('material', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='material', + name='advantage', + field=models.JSONField(blank=True, default=list, null=True, verbose_name='竞争优势'), + ), + migrations.AlterField( + model_name='material', + name='application_scene', + field=models.JSONField(blank=True, default=list, null=True, verbose_name='应用场景'), + ), + ] diff --git a/backend/apps/material/migrations/0003_materialcategory_materialsubcategory.py b/backend/apps/material/migrations/0003_materialcategory_materialsubcategory.py new file mode 100644 index 0000000..c79a6ca --- /dev/null +++ b/backend/apps/material/migrations/0003_materialcategory_materialsubcategory.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.7 on 2026-03-10 08:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('material', '0002_alter_material_advantage_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='MaterialCategory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='分类名称')), + ('value', models.CharField(max_length=255, unique=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': 'material_category', + }, + ), + migrations.CreateModel( + name='MaterialSubcategory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='子分类名称')), + ('value', models.CharField(max_length=255, unique=True, verbose_name='子分类值')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='material.materialcategory', verbose_name='所属分类')), + ], + options={ + 'verbose_name': '材料子分类', + 'verbose_name_plural': '材料子分类', + 'db_table': 'material_subcategory', + }, + ), + ] diff --git a/backend/apps/material/models.py b/backend/apps/material/models.py index 5d354b5..2deb703 100644 --- a/backend/apps/material/models.py +++ b/backend/apps/material/models.py @@ -47,10 +47,10 @@ class Material(models.Model): material_subcategory = models.CharField(max_length=255, 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.CharField(max_length=20, choices=APPLICATION_SCENE_CHOICES, blank=True, null=True, verbose_name='应用场景') + application_scene = models.JSONField(default=list, blank=True, null=True, verbose_name='应用场景') application_desc = models.TextField(blank=True, null=True, verbose_name='应用场景说明') replace_type = models.CharField(max_length=20, choices=REPLACE_TYPE_CHOICES, blank=True, null=True, verbose_name='替代材料类型') - advantage = models.CharField(max_length=20, choices=ADVANTAGE_CHOICES, blank=True, null=True, verbose_name='竞争优势') + advantage = models.JSONField(default=list, blank=True, null=True, verbose_name='竞争优势') advantage_desc = models.TextField(blank=True, null=True, verbose_name='优势说明') cost_compare = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True, verbose_name='成本对比百分数') cost_desc = models.TextField(blank=True, null=True, verbose_name='成本说明') @@ -76,3 +76,40 @@ class Material(models.Model): def __str__(self): return self.name + + +class MaterialCategory(models.Model): + """ + 材料分类 + """ + name = models.CharField(max_length=255, verbose_name='分类名称') + value = models.CharField(max_length=255, unique=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 = 'material_category' + + def __str__(self): + return self.name + + +class MaterialSubcategory(models.Model): + """ + 材料子分类 + """ + category = models.ForeignKey(MaterialCategory, on_delete=models.CASCADE, related_name='subcategories', verbose_name='所属分类') + name = models.CharField(max_length=255, verbose_name='子分类名称') + value = models.CharField(max_length=255, unique=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 = 'material_subcategory' + + def __str__(self): + return self.name diff --git a/backend/apps/material/serializers.py b/backend/apps/material/serializers.py index cda112c..3625a43 100644 --- a/backend/apps/material/serializers.py +++ b/backend/apps/material/serializers.py @@ -1,5 +1,16 @@ +import json from rest_framework import serializers -from .models import Material +from .models import Material, MaterialCategory, MaterialSubcategory + + +class JSONListField(serializers.ListField): + def to_internal_value(self, data): + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError: + data = [data] + return super().to_internal_value(data) class MaterialSerializer(serializers.ModelSerializer): @@ -10,8 +21,18 @@ class MaterialSerializer(serializers.ModelSerializer): factory_short_name = serializers.CharField(source='factory.factory_short_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) - advantage_display = serializers.CharField(source='get_advantage_display', read_only=True) - application_scene_display = serializers.CharField(source='get_application_scene_display', read_only=True) + application_scene = JSONListField( + child=serializers.ChoiceField(choices=Material.APPLICATION_SCENE_CHOICES), + required=False, + allow_empty=True + ) + advantage = JSONListField( + child=serializers.ChoiceField(choices=Material.ADVANTAGE_CHOICES), + required=False, + allow_empty=True + ) + advantage_display = serializers.SerializerMethodField() + application_scene_display = serializers.SerializerMethodField() brochure_url = serializers.SerializerMethodField() class Meta: @@ -37,6 +58,20 @@ class MaterialSerializer(serializers.ModelSerializer): return request.build_absolute_uri(obj.brochure.url) return None + def _display_list(self, codes, choices): + mapping = dict(choices) + if not codes: + return [] + if isinstance(codes, str): + codes = [codes] + return [mapping.get(code, code) for code in codes] + + def get_application_scene_display(self, obj): + return self._display_list(obj.application_scene, Material.APPLICATION_SCENE_CHOICES) + + def get_advantage_display(self, obj): + return self._display_list(obj.advantage, Material.ADVANTAGE_CHOICES) + class MaterialListSerializer(serializers.ModelSerializer): """ @@ -52,3 +87,30 @@ class MaterialListSerializer(serializers.ModelSerializer): fields = ['id', 'name', 'major_category', 'major_category_display', 'material_category', 'material_subcategory', 'factory', 'factory_name', 'factory_short_name', 'status', 'status_display'] + + +class MaterialCategorySerializer(serializers.ModelSerializer): + """ + 材料分类序列化器 + """ + subcategory_count = serializers.SerializerMethodField() + + class Meta: + model = MaterialCategory + fields = ['id', 'name', 'value', 'created_at', 'updated_at', 'subcategory_count'] + read_only_fields = ['id', 'created_at', 'updated_at', 'subcategory_count'] + + def get_subcategory_count(self, obj): + return obj.subcategories.count() + + +class MaterialSubcategorySerializer(serializers.ModelSerializer): + """ + 材料子分类序列化器 + """ + category_name = serializers.CharField(source='category.name', read_only=True) + + class Meta: + model = MaterialSubcategory + fields = ['id', 'category', 'category_name', 'name', 'value', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at', 'category_name'] diff --git a/backend/apps/material/urls.py b/backend/apps/material/urls.py index 8a1ba18..0eec20c 100644 --- a/backend/apps/material/urls.py +++ b/backend/apps/material/urls.py @@ -1,10 +1,14 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import MaterialViewSet +from .views import MaterialViewSet, MaterialCategoryViewSet, MaterialSubcategoryViewSet router = DefaultRouter() router.register(r'', MaterialViewSet, basename='material') urlpatterns = [ + path('categories/', MaterialCategoryViewSet.as_view({'get': 'list', 'post': 'create'}), name='material-category-list'), + path('categories//', MaterialCategoryViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'}), name='material-category-detail'), + path('subcategories/', MaterialSubcategoryViewSet.as_view({'get': 'list', 'post': 'create'}), name='material-subcategory-list'), + path('subcategories//', MaterialSubcategoryViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'}), name='material-subcategory-detail'), path('', include(router.urls)), ] diff --git a/backend/apps/material/views.py b/backend/apps/material/views.py index 6ef2cdb..c22df97 100644 --- a/backend/apps/material/views.py +++ b/backend/apps/material/views.py @@ -1,10 +1,11 @@ from rest_framework import generics, status -from rest_framework.decorators import api_view, permission_classes, action +from rest_framework.decorators import api_view, action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from .models import Material -from .serializers import MaterialSerializer, MaterialListSerializer +from rest_framework.exceptions import PermissionDenied +from .models import Material, MaterialCategory, MaterialSubcategory +from .serializers import MaterialSerializer, MaterialListSerializer, MaterialCategorySerializer, MaterialSubcategorySerializer class MaterialViewSet(ModelViewSet): @@ -28,6 +29,11 @@ class MaterialViewSet(ModelViewSet): if status_filter: queryset = queryset.filter(status=status_filter) + # 支持按名称搜索 + name = self.request.query_params.get('name') + if name: + queryset = queryset.filter(name__icontains=name) + # 支持按工厂过滤 factory_id = self.request.query_params.get('factory_id') if factory_id: @@ -38,11 +44,26 @@ class MaterialViewSet(ModelViewSet): if major_category: queryset = queryset.filter(major_category=major_category) + # 支持按材料分类过滤 + material_category = self.request.query_params.get('material_category') + if material_category: + queryset = queryset.filter(material_category=material_category) + # 支持按材料子类过滤 material_subcategory = self.request.query_params.get('material_subcategory') if material_subcategory: queryset = queryset.filter(material_subcategory=material_subcategory) + # 支持按应用场景过滤 (JSONField contains) + application_scene = self.request.query_params.get('application_scene') + if application_scene: + queryset = queryset.filter(application_scene__contains=[application_scene]) + + # 支持按竞争优势过滤 (JSONField contains) + advantage = self.request.query_params.get('advantage') + if advantage: + queryset = queryset.filter(advantage__contains=[advantage]) + return queryset def get_serializer_class(self): @@ -70,7 +91,7 @@ class MaterialViewSet(ModelViewSet): # 普通用户只能更新自己工厂的材料 if (self.request.user.role != 'admin' and self.request.user.factory_id != self.get_object().factory_id): - raise PermissionError("无权修改其他工厂的材料") + raise PermissionDenied("无权修改其他工厂的材料") serializer.save() def perform_destroy(self, instance): @@ -80,7 +101,7 @@ class MaterialViewSet(ModelViewSet): # 普通用户只能删除自己工厂的材料 if (self.request.user.role != 'admin' and self.request.user.factory_id != instance.factory_id): - raise PermissionError("无权删除其他工厂的材料") + raise PermissionDenied("无权删除其他工厂的材料") instance.delete() @action(detail=True, methods=['post']) @@ -153,3 +174,72 @@ class MaterialViewSet(ModelViewSet): material.status = 'draft' material.save() return Response({"status": "审核拒绝"}) + + @action(detail=False, methods=['get']) + def choices(self, request): + """ + 材料字段枚举 + """ + return Response({ + 'major_category': Material.MAJOR_CATEGORY_CHOICES, + 'replace_type': Material.REPLACE_TYPE_CHOICES, + 'advantage': Material.ADVANTAGE_CHOICES, + 'application_scene': Material.APPLICATION_SCENE_CHOICES, + 'star_level': Material.STAR_LEVEL_CHOICES, + 'status': Material.STATUS_CHOICES, + }) + + +class MaterialCategoryViewSet(ModelViewSet): + """ + 材料分类视图集 + """ + queryset = MaterialCategory.objects.all().order_by('id') + serializer_class = MaterialCategorySerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + if self.request.user.role != 'admin': + raise PermissionDenied("只有管理员可以创建材料分类") + serializer.save() + + def perform_update(self, serializer): + if self.request.user.role != 'admin': + raise PermissionDenied("只有管理员可以更新材料分类") + serializer.save() + + def perform_destroy(self, instance): + if self.request.user.role != 'admin': + raise PermissionDenied("只有管理员可以删除材料分类") + instance.delete() + + +class MaterialSubcategoryViewSet(ModelViewSet): + """ + 材料子分类视图集 + """ + queryset = MaterialSubcategory.objects.select_related('category').all().order_by('id') + serializer_class = MaterialSubcategorySerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + queryset = super().get_queryset() + category_id = self.request.query_params.get('category_id') + if category_id: + queryset = queryset.filter(category_id=category_id) + return queryset + + def perform_create(self, serializer): + if self.request.user.role != 'admin': + raise PermissionDenied("只有管理员可以创建材料子分类") + serializer.save() + + def perform_update(self, serializer): + if self.request.user.role != 'admin': + raise PermissionDenied("只有管理员可以更新材料子分类") + serializer.save() + + def perform_destroy(self, instance): + if self.request.user.role != 'admin': + raise PermissionDenied("只有管理员可以删除材料子分类") + instance.delete() diff --git a/backend/apps/statistics/views.py b/backend/apps/statistics/views.py index 7f2fa93..104ca89 100644 --- a/backend/apps/statistics/views.py +++ b/backend/apps/statistics/views.py @@ -1,42 +1,50 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from django.db.models import Count, Q +from django.db.models import Count from apps.material.models import Material from apps.factory.models import Factory +def _build_brochure_url(request, brochure_field): + if brochure_field and request: + return request.build_absolute_uri(brochure_field.url) + return None + + @api_view(['GET']) @permission_classes([IsAuthenticated]) def overview_statistics(request): """ 数据总览统计 """ - # 只有管理员可以访问 + # 只有管理员可访问 if request.user.role != 'admin': return Response({"detail": "无权访问"}, status=403) + approved_materials = Material.objects.filter(status='approved') + # 材料总数 - total_materials = Material.objects.count() + total_materials = approved_materials.count() # 材料种类(材料子类数量) - total_material_categories = Material.objects.values('material_subcategory').distinct().count() + total_material_categories = approved_materials.values('material_subcategory').distinct().count() # 品牌数(工厂数) total_brands = Factory.objects.count() # 按专业类别的材料数量分布 - major_category_stats = Material.objects.values('major_category').annotate( + major_category_stats = approved_materials.values('major_category').annotate( count=Count('id') ).order_by('-count') # 按材料子类的材料数量分布 - material_subcategory_stats = Material.objects.values('material_subcategory').annotate( + material_subcategory_stats = approved_materials.values('material_subcategory').annotate( count=Count('id') - ).order_by('-count')[:10] # 取前10个 + ).order_by('-count')[:10] # 按所属品牌的材料数量分布 - brand_stats = Material.objects.values('factory__factory_name').annotate( + brand_stats = approved_materials.values('factory__factory_name').annotate( count=Count('id') ).order_by('-count') @@ -45,12 +53,16 @@ def overview_statistics(request): count=Count('id') ).order_by('-count') - # 应用案例列表(有案例描述的材料) - cases_list = Material.objects.filter( - status='approved', - cases__isnull=False, - cases__gt='' - ).values('id', 'name', 'cases', 'factory__factory_name')[:10] + # 应用案例列表 + cases_list = [] + for item in approved_materials.filter(cases__isnull=False).exclude(cases='')[:10]: + cases_list.append({ + 'id': item.id, + 'name': item.name, + 'cases': item.cases, + 'factory_name': item.factory.factory_name if item.factory else None, + 'brochure_url': _build_brochure_url(request, item.brochure), + }) return Response({ 'total_materials': total_materials, @@ -60,7 +72,7 @@ def overview_statistics(request): 'material_subcategory_stats': list(material_subcategory_stats), 'brand_stats': list(brand_stats), 'region_stats': list(region_stats), - 'cases_list': list(cases_list), + 'cases_list': cases_list, }) @@ -70,53 +82,79 @@ def material_statistics(request): """ 材料库统计 """ - # 只有管理员可以访问 if request.user.role != 'admin': return Response({"detail": "无权访问"}, status=403) - # 获取筛选条件 material_subcategory = request.query_params.get('material_subcategory') - - # 基础查询 queryset = Material.objects.filter(status='approved') - # 按材料子类筛选 if material_subcategory: queryset = queryset.filter(material_subcategory=material_subcategory) - # 材料星级对比(按材料子类) - star_stats = {} - for subcategory in queryset.values_list('material_subcategory', flat=True).distinct(): - materials = queryset.filter(material_subcategory=subcategory) - star_stats[subcategory] = { - 'quality_level': list(materials.values('quality_level').annotate(count=Count('id'))), - 'durability_level': list(materials.values('durability_level').annotate(count=Count('id'))), - 'eco_level': list(materials.values('eco_level').annotate(count=Count('id'))), - 'carbon_level': list(materials.values('carbon_level').annotate(count=Count('id'))), - 'score_level': list(materials.values('score_level').annotate(count=Count('id'))), - } + subcategories = list(Material.objects.filter(status='approved').values_list( + 'material_subcategory', flat=True + ).distinct()) - # 竞争优势与替代材料对比 - advantage_replace_stats = list(queryset.values('advantage', 'replace_type').annotate( - count=Count('id') - )) + def _level_counts(qs, field): + raw = qs.values(field).annotate(count=Count('id')) + mapping = {item[field]: item['count'] for item in raw if item[field] is not None} + return [mapping.get(1, 0), mapping.get(2, 0), mapping.get(3, 0)] - # 应用场景对比 - application_scene_stats = list(queryset.values('application_scene').annotate( - count=Count('id') - )) + star_stats = { + 'quality_level': _level_counts(queryset, 'quality_level'), + 'durability_level': _level_counts(queryset, 'durability_level'), + 'eco_level': _level_counts(queryset, 'eco_level'), + 'carbon_level': _level_counts(queryset, 'carbon_level'), + 'score_level': _level_counts(queryset, 'score_level'), + } + + # 竞争优势与替代材料对比(优势为多选) + advantage_replace_counter = {} + for material in queryset: + advantages = material.advantage or [] + for adv in advantages: + key = (adv, material.replace_type) + advantage_replace_counter[key] = advantage_replace_counter.get(key, 0) + 1 + + advantage_replace_stats = [ + {'advantage': key[0], 'replace_type': key[1], 'count': count} + for key, count in advantage_replace_counter.items() + ] + + # 应用场景对比(多选) + application_scene_counter = {} + for material in queryset: + scenes = material.application_scene or [] + for scene in scenes: + application_scene_counter[scene] = application_scene_counter.get(scene, 0) + 1 + + application_scene_stats = [ + {'application_scene': key, 'count': count} + for key, count in application_scene_counter.items() + ] # 材料列表 - materials_list = list(queryset.values( - 'id', 'name', 'material_category', 'material_subcategory', - 'factory__factory_name', 'brochure' - )[:20]) + materials_list = [] + for item in queryset[:20]: + materials_list.append({ + 'id': item.id, + 'name': item.name, + 'material_category': item.material_category, + 'material_subcategory': item.material_subcategory, + 'factory_name': item.factory.factory_name if item.factory else None, + 'brochure_url': _build_brochure_url(request, item.brochure), + }) return Response({ + 'levels': [1, 2, 3], + 'subcategories': subcategories, 'star_stats': star_stats, 'advantage_replace_stats': advantage_replace_stats, 'application_scene_stats': application_scene_stats, 'materials_list': materials_list, + 'advantage_choices': Material.ADVANTAGE_CHOICES, + 'replace_type_choices': Material.REPLACE_TYPE_CHOICES, + 'application_scene_choices': Material.APPLICATION_SCENE_CHOICES, }) @@ -126,19 +164,17 @@ def factory_statistics(request): """ 工厂库统计 """ - # 只有管理员可以访问 if request.user.role != 'admin': return Response({"detail": "无权访问"}, status=403) - # 工厂地区分布 region_stats = list(Factory.objects.values('province', 'city').annotate( count=Count('id') ).order_by('-count')) - # 工厂材料分类分布 factory_category_stats = [] + approved_materials = Material.objects.filter(status='approved') for factory in Factory.objects.all(): - material_categories = Material.objects.filter(factory=factory).values( + material_categories = approved_materials.filter(factory=factory).values( 'material_category' ).annotate( count=Count('id') @@ -148,10 +184,9 @@ def factory_statistics(request): 'factory_id': factory.id, 'factory_name': factory.factory_name, 'categories': list(material_categories), - 'total_materials': factory.materials.count() + 'total_materials': approved_materials.filter(factory=factory).count() }) - # 工厂列表 factories_list = list(Factory.objects.values( 'id', 'factory_name', 'factory_short_name', 'province', 'city', 'website' )) diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..eeed0b8 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://127.0.0.1:8000/api diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0f8bd7d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 新材料数据库 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..cb63c1a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1736 @@ +{ + "name": "new-materials-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "new-materials-frontend", + "version": "0.1.0", + "dependencies": { + "axios": "^1.6.8", + "echarts": "^5.5.0", + "element-china-area-data": "^5.0.0", + "element-plus": "^2.7.3", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.2.10" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/china-area-data": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/china-area-data/-/china-area-data-5.0.1.tgz", + "integrity": "sha512-BQDPpiv5Nn+018ekcJK2oSD9PAD+E1bvXB0wgabc//dFVS/KvRqCgg0QOEUt3vBkx9XzB5a9BmkJCEZDBxVjVw==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/element-china-area-data": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/element-china-area-data/-/element-china-area-data-5.0.2.tgz", + "integrity": "sha512-vLQuvOKJy/uiX7MRHEk3x/j09hipuIl6DJ/C4XFUG7D7Pj3O47sy+Y6aAArM6k9v8cD9UX6e+yz2S4J+IPnZ8g==", + "license": "MIT", + "dependencies": { + "china-area-data": "^5.0.1", + "lodash-es": "^4.17.15" + } + }, + "node_modules/element-plus": { + "version": "2.13.5", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.5.tgz", + "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "12.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.30", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2a3a267 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "new-materials-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.6.8", + "echarts": "^5.5.0", + "element-china-area-data": "^5.0.0", + "element-plus": "^2.7.3", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.2.10" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..7dcf773 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..11d22bd --- /dev/null +++ b/frontend/src/api/auth.js @@ -0,0 +1,31 @@ +import api from './client' + +export const login = async (payload) => { + const { data } = await api.post('/auth/login/', payload) + return data +} + +export const fetchCurrentUser = async () => { + const { data } = await api.get('/auth/user/') + return data +} + +export const fetchUsers = async (params) => { + const { data } = await api.get('/auth/users/', { params }) + return data +} + +export const createUser = async (payload) => { + const { data } = await api.post('/auth/users/', payload) + return data +} + +export const updateUser = async (id, payload) => { + const { data } = await api.put(`/auth/users/${id}/`, payload) + return data +} + +export const deleteUser = async (id) => { + const { data } = await api.delete(`/auth/users/${id}/`) + return data +} diff --git a/frontend/src/api/category.js b/frontend/src/api/category.js new file mode 100644 index 0000000..abc5d5f --- /dev/null +++ b/frontend/src/api/category.js @@ -0,0 +1,41 @@ +import api from './client' + +export const fetchCategories = async () => { + const { data } = await api.get('/material/categories/') + return data +} + +export const createCategory = async (payload) => { + const { data } = await api.post('/material/categories/', payload) + return data +} + +export const updateCategory = async (id, payload) => { + const { data } = await api.put(`/material/categories/${id}/`, payload) + return data +} + +export const deleteCategory = async (id) => { + const { data } = await api.delete(`/material/categories/${id}/`) + return data +} + +export const fetchSubcategories = async (params) => { + const { data } = await api.get('/material/subcategories/', { params }) + return data +} + +export const createSubcategory = async (payload) => { + const { data } = await api.post('/material/subcategories/', payload) + return data +} + +export const updateSubcategory = async (id, payload) => { + const { data } = await api.put(`/material/subcategories/${id}/`, payload) + return data +} + +export const deleteSubcategory = async (id) => { + const { data } = await api.delete(`/material/subcategories/${id}/`) + return data +} diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..4ec2184 --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,28 @@ +import axios from 'axios' +import { useAuth } from '@/store/auth' + +const api = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', + timeout: 15000 +}) + +api.interceptors.request.use((config) => { + const { state } = useAuth() + if (state.token) { + config.headers.Authorization = `Bearer ${state.token}` + } + return config +}) + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + const { clearAuth } = useAuth() + clearAuth() + } + return Promise.reject(error) + } +) + +export default api diff --git a/frontend/src/api/dictionary.js b/frontend/src/api/dictionary.js new file mode 100644 index 0000000..7a5446a --- /dev/null +++ b/frontend/src/api/dictionary.js @@ -0,0 +1,26 @@ +import api from './client' + +export const fetchDictionary = async (params) => { + const { data } = await api.get('/dictionary/', { params }) + return data +} + +export const fetchDictionaryGrouped = async () => { + const { data } = await api.get('/dictionary/grouped/') + return data +} + +export const createDictionary = async (payload) => { + const { data } = await api.post('/dictionary/', payload) + return data +} + +export const updateDictionary = async (id, payload) => { + const { data } = await api.put(`/dictionary/${id}/`, payload) + return data +} + +export const deleteDictionary = async (id) => { + const { data } = await api.delete(`/dictionary/${id}/`) + return data +} diff --git a/frontend/src/api/factory.js b/frontend/src/api/factory.js new file mode 100644 index 0000000..7fd589f --- /dev/null +++ b/frontend/src/api/factory.js @@ -0,0 +1,31 @@ +import api from './client' + +export const fetchFactories = async (params) => { + const { data } = await api.get('/factory/', { params }) + return data +} + +export const fetchFactorySimple = async () => { + const { data } = await api.get('/factory/simple/') + return data +} + +export const fetchFactoryDetail = async (id) => { + const { data } = await api.get(`/factory/${id}/`) + return data +} + +export const createFactory = async (payload) => { + const { data } = await api.post('/factory/', payload) + return data +} + +export const updateFactory = async (id, payload) => { + const { data } = await api.put(`/factory/${id}/`, payload) + return data +} + +export const deleteFactory = async (id) => { + const { data } = await api.delete(`/factory/${id}/`) + return data +} diff --git a/frontend/src/api/material.js b/frontend/src/api/material.js new file mode 100644 index 0000000..bed4164 --- /dev/null +++ b/frontend/src/api/material.js @@ -0,0 +1,65 @@ +import api from './client' + +const toFormData = (payload) => { + const form = new FormData() + Object.entries(payload).forEach(([key, value]) => { + if (value === undefined || value === null) return + if (Array.isArray(value)) { + form.append(key, JSON.stringify(value)) + return + } + form.append(key, value) + }) + return form +} + +export const fetchMaterials = async (params) => { + const { data } = await api.get('/material/', { params }) + return data +} + +export const fetchMaterialDetail = async (id) => { + const { data } = await api.get(`/material/${id}/`) + return data +} + +export const createMaterial = async (payload, withFile = false) => { + const dataPayload = withFile ? toFormData(payload) : payload + const { data } = await api.post('/material/', dataPayload, { + headers: withFile ? { 'Content-Type': 'multipart/form-data' } : undefined + }) + return data +} + +export const updateMaterial = async (id, payload, withFile = false) => { + const dataPayload = withFile ? toFormData(payload) : payload + const { data } = await api.put(`/material/${id}/`, dataPayload, { + headers: withFile ? { 'Content-Type': 'multipart/form-data' } : undefined + }) + return data +} + +export const deleteMaterial = async (id) => { + const { data } = await api.delete(`/material/${id}/`) + return data +} + +export const submitMaterial = async (id) => { + const { data } = await api.post(`/material/${id}/submit/`) + return data +} + +export const approveMaterial = async (id) => { + const { data } = await api.post(`/material/${id}/approve/`) + return data +} + +export const rejectMaterial = async (id) => { + const { data } = await api.post(`/material/${id}/reject/`) + return data +} + +export const fetchMaterialChoices = async () => { + const { data } = await api.get('/material/choices/') + return data +} diff --git a/frontend/src/api/statistics.js b/frontend/src/api/statistics.js new file mode 100644 index 0000000..e66a77d --- /dev/null +++ b/frontend/src/api/statistics.js @@ -0,0 +1,16 @@ +import api from './client' + +export const fetchOverviewStats = async () => { + const { data } = await api.get('/statistics/overview/') + return data +} + +export const fetchMaterialStats = async (params) => { + const { data } = await api.get('/statistics/materials/', { params }) + return data +} + +export const fetchFactoryStats = async () => { + const { data } = await api.get('/statistics/factories/') + return data +} diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue new file mode 100644 index 0000000..16a1c1b --- /dev/null +++ b/frontend/src/layouts/MainLayout.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/frontend/src/layouts/ScreenLayout.vue b/frontend/src/layouts/ScreenLayout.vue new file mode 100644 index 0000000..63494a2 --- /dev/null +++ b/frontend/src/layouts/ScreenLayout.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..5224f16 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,11 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import './styles/base.css' + +const app = createApp(App) +app.use(router) +app.use(ElementPlus) +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..0673fdf --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,68 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuth } from '@/store/auth' +import MainLayout from '@/layouts/MainLayout.vue' +import ScreenLayout from '@/layouts/ScreenLayout.vue' + +const routes = [ + { + path: '/login', + name: 'login', + component: () => import('@/views/Login.vue'), + meta: { public: true } + }, + { + path: '/', + component: MainLayout, + redirect: '/materials', + children: [ + { 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: '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') } + ] + }, + { + path: '/screen', + component: ScreenLayout, + meta: { admin: true }, + redirect: '/screen/overview', + children: [ + { path: 'overview', name: 'screen-overview', component: () => import('@/views/screen/ScreenOverview.vue') }, + { path: 'materials', name: 'screen-materials', component: () => import('@/views/screen/ScreenMaterials.vue') }, + { path: 'factories', name: 'screen-factories', component: () => import('@/views/screen/ScreenFactories.vue') } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'notfound', + component: () => import('@/views/NotFound.vue'), + meta: { public: true } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +router.beforeEach((to, from, next) => { + const { isAuthed, isAdmin } = useAuth() + + if (to.meta.public) { + return next() + } + + if (!isAuthed.value) { + return next('/login') + } + + if (to.meta.admin && !isAdmin.value) { + return next('/materials') + } + + return next() +}) + +export default router diff --git a/frontend/src/store/auth.js b/frontend/src/store/auth.js new file mode 100644 index 0000000..92b5934 --- /dev/null +++ b/frontend/src/store/auth.js @@ -0,0 +1,30 @@ +import { reactive, computed } from 'vue' + +const tokenKey = 'nm_token' +const userKey = 'nm_user' + +const state = reactive({ + token: localStorage.getItem(tokenKey) || '', + user: JSON.parse(localStorage.getItem(userKey) || 'null') +}) + +export const useAuth = () => { + const isAuthed = computed(() => !!state.token) + const isAdmin = computed(() => state.user?.role === 'admin') + + const setAuth = (token, user) => { + state.token = token + state.user = user + localStorage.setItem(tokenKey, token) + localStorage.setItem(userKey, JSON.stringify(user)) + } + + const clearAuth = () => { + state.token = '' + state.user = null + localStorage.removeItem(tokenKey) + localStorage.removeItem(userKey) + } + + return { state, isAuthed, isAdmin, setAuth, clearAuth } +} diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css new file mode 100644 index 0000000..8418b18 --- /dev/null +++ b/frontend/src/styles/base.css @@ -0,0 +1,117 @@ +:root { + --brand-950: #0e1a2a; + --brand-900: #16263c; + --brand-800: #203754; + --brand-700: #2f4b6b; + --brand-500: #4e86b8; + --accent-500: #f2b24c; + --accent-400: #ffd18a; + --bg: #f3f5fb; + --bg-soft: #eef2f9; + --text-900: #0f1a2a; + --text-600: #5c6b7a; + --card: #ffffff; + --danger: #d64550; + --success: #24a26a; + --radius-lg: 14px; + --radius-md: 10px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Alibaba PuHuiTi", "Noto Sans SC", "Microsoft YaHei", sans-serif; + background: radial-gradient(circle at top left, #ffffff 0%, #f5f7fb 35%, #e8eef8 100%); + color: var(--text-900); +} + +a { + color: inherit; + text-decoration: none; +} + +.page { + padding: 20px 24px 28px; +} + +.page-title { + font-size: 20px; + font-weight: 600; + margin: 8px 0 16px; +} + +.card { + background: var(--card); + border-radius: var(--radius-lg); + padding: 18px; + box-shadow: 0 18px 40px rgba(15, 26, 42, 0.08); + border: 1px solid rgba(15, 26, 42, 0.05); +} + +.toolbar { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.table-actions { + display: flex; + gap: 8px; +} + +.status-tag { + text-transform: capitalize; +} + +.fade-in { + animation: fadeIn 0.6s ease-out both; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.el-table { + border-radius: var(--radius-md); + overflow: hidden; +} + +.el-table th.el-table__cell { + background: var(--bg-soft); + color: var(--text-900); + font-weight: 600; +} + +.el-table td.el-table__cell { + color: var(--text-600); +} + +.el-input__wrapper, +.el-textarea__inner, +.el-select__wrapper { + border-radius: var(--radius-md); +} + +.el-button--primary { + background: linear-gradient(135deg, var(--brand-500), #6ea7d8); + border: none; +} + +.el-button--primary:hover { + background: linear-gradient(135deg, #3f7bb1, #7db7e7); +} + +.el-dialog { + border-radius: var(--radius-lg); +} diff --git a/frontend/src/styles/screen.css b/frontend/src/styles/screen.css new file mode 100644 index 0000000..ea82301 --- /dev/null +++ b/frontend/src/styles/screen.css @@ -0,0 +1,205 @@ +.screen-body { + margin: 0; + background: radial-gradient(circle at top left, #203450 0%, #0c1422 45%, #070b12 100%); + color: #e6edf5; + font-family: "Rajdhani", "Alibaba PuHuiTi", "Noto Sans SC", sans-serif; +} + +.screen-layout { + min-height: 100vh; + display: flex; + flex-direction: column; + position: relative; +} + +.screen-layout::before { + content: ""; + position: absolute; + inset: 0; + background: radial-gradient(circle at 20% 10%, rgba(159, 226, 255, 0.15), transparent 40%), + radial-gradient(circle at 80% 20%, rgba(255, 209, 138, 0.12), transparent 35%); + pointer-events: none; +} + +.screen-header { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: 20px; + padding: 18px 28px; + background: rgba(6, 12, 20, 0.92); + border-bottom: 1px solid rgba(159, 226, 255, 0.18); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45); + z-index: 1; +} + +.screen-title { + font-size: 22px; + letter-spacing: 6px; + text-transform: uppercase; + font-weight: 600; + color: #e3f3ff; +} + +.screen-nav { + display: flex; + gap: 16px; +} + +.screen-nav a { + padding: 6px 14px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + color: #e6edf5; + font-size: 14px; + transition: all 0.2s ease; + border: 1px solid rgba(159, 226, 255, 0.15); + box-shadow: inset 0 0 8px rgba(159, 226, 255, 0.2); +} + +.screen-nav a:hover { + background: rgba(159, 226, 255, 0.2); + color: #e3f3ff; +} + +.screen-nav a.active { + background: linear-gradient(120deg, #5fb5ff, #9fe2ff); + color: #051321; + font-weight: 600; + box-shadow: 0 0 12px rgba(159, 226, 255, 0.6); +} + +.screen-back { + padding: 6px 16px; + border-radius: 999px; + border: 1px solid rgba(255, 209, 138, 0.4); + background: rgba(255, 209, 138, 0.1); + color: #ffe2a6; + cursor: pointer; + font-size: 13px; + letter-spacing: 2px; + text-transform: uppercase; + transition: all 0.2s ease; +} + +.screen-back:hover { + background: rgba(255, 209, 138, 0.2); + box-shadow: 0 0 12px rgba(255, 209, 138, 0.5); +} + +.screen-content { + flex: 1; + padding: 18px 24px 28px; + position: relative; + z-index: 1; +} + +.screen-grid { + display: grid; + gap: 18px; + grid-template-columns: repeat(12, 1fr); +} + +.screen-card { + background: rgba(7, 14, 24, 0.92); + border: 1px solid rgba(159, 226, 255, 0.18); + border-radius: 18px; + padding: 16px; + box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45); + position: relative; + overflow: hidden; +} + +.screen-card::after { + content: ""; + position: absolute; + top: -120px; + right: -120px; + width: 220px; + height: 220px; + background: radial-gradient(circle, rgba(159, 226, 255, 0.22), transparent 70%); + opacity: 0.8; +} + +.screen-card h3 { + margin: 0 0 12px; + font-size: 15px; + color: #e6edf5; + text-transform: uppercase; + letter-spacing: 2px; +} + +.screen-card h3::after { + content: ""; + display: block; + width: 46px; + height: 2px; + margin-top: 6px; + background: linear-gradient(90deg, #9fe2ff, transparent); +} + +.stat-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.stat-card { + padding: 16px; + border-radius: 14px; + background: linear-gradient(140deg, rgba(78, 134, 184, 0.35), rgba(8, 18, 28, 0.9)); + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: inset 0 0 20px rgba(159, 226, 255, 0.08); +} + +.stat-card .label { + font-size: 12px; + color: #c9d7e6; + letter-spacing: 2px; + text-transform: uppercase; +} + +.stat-card .value { + font-size: 28px; + font-weight: 700; + margin-top: 6px; +} + +.scroll-list { + max-height: 220px; + overflow: hidden; + position: relative; +} + +.scroll-item { + padding: 8px 0; + border-bottom: 1px dashed rgba(255, 255, 255, 0.1); + cursor: pointer; +} + +@keyframes scrollUp { + 0% { transform: translateY(0); } + 100% { transform: translateY(-50%); } +} + +.scroll-inner { + display: grid; + gap: 6px; + animation: scrollUp 18s linear infinite; +} + +@media (max-width: 1200px) { + .screen-grid { + grid-template-columns: repeat(6, 1fr); + } + .stat-cards { + grid-template-columns: 1fr; + } +} + +@media (max-width: 900px) { + .screen-grid { + grid-template-columns: repeat(2, 1fr); + } +} + diff --git a/frontend/src/utils/region.js b/frontend/src/utils/region.js new file mode 100644 index 0000000..1bd8799 --- /dev/null +++ b/frontend/src/utils/region.js @@ -0,0 +1,28 @@ +import { codeToText, regionData } from 'element-china-area-data' + +const buildMap = (nodes, map) => { + nodes.forEach((node) => { + if (node?.value && node?.label) { + map[node.value] = node.label + } + if (node?.children?.length) { + buildMap(node.children, map) + } + }) +} + +const fallbackMap = {} +if (!codeToText && Array.isArray(regionData)) { + buildMap(regionData, fallbackMap) +} + +export const regionLabel = (code) => { + if (!code) return '' + const mapping = codeToText || fallbackMap + return mapping?.[code] || code +} + +export const formatRegion = (province, city, district) => { + const parts = [province, city, district].filter(Boolean).map(regionLabel) + return parts.join(' ') +} diff --git a/frontend/src/views/DictionaryManage.vue b/frontend/src/views/DictionaryManage.vue new file mode 100644 index 0000000..bde76db --- /dev/null +++ b/frontend/src/views/DictionaryManage.vue @@ -0,0 +1,235 @@ + + + diff --git a/frontend/src/views/FactoryDetail.vue b/frontend/src/views/FactoryDetail.vue new file mode 100644 index 0000000..7b44db1 --- /dev/null +++ b/frontend/src/views/FactoryDetail.vue @@ -0,0 +1,50 @@ + + + + + + diff --git a/frontend/src/views/FactoryManage.vue b/frontend/src/views/FactoryManage.vue new file mode 100644 index 0000000..49331f9 --- /dev/null +++ b/frontend/src/views/FactoryManage.vue @@ -0,0 +1,171 @@ + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..edec466 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend/src/views/MaterialDetail.vue b/frontend/src/views/MaterialDetail.vue new file mode 100644 index 0000000..7a583c0 --- /dev/null +++ b/frontend/src/views/MaterialDetail.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/frontend/src/views/MaterialManage.vue b/frontend/src/views/MaterialManage.vue new file mode 100644 index 0000000..e62cf1f --- /dev/null +++ b/frontend/src/views/MaterialManage.vue @@ -0,0 +1,403 @@ + + + + + diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue new file mode 100644 index 0000000..a4ca336 --- /dev/null +++ b/frontend/src/views/NotFound.vue @@ -0,0 +1,6 @@ + diff --git a/frontend/src/views/UserManage.vue b/frontend/src/views/UserManage.vue new file mode 100644 index 0000000..c8b9c83 --- /dev/null +++ b/frontend/src/views/UserManage.vue @@ -0,0 +1,166 @@ + + + diff --git a/frontend/src/views/screen/ScreenFactories.vue b/frontend/src/views/screen/ScreenFactories.vue new file mode 100644 index 0000000..fdc38ff --- /dev/null +++ b/frontend/src/views/screen/ScreenFactories.vue @@ -0,0 +1,109 @@ + + + diff --git a/frontend/src/views/screen/ScreenMaterials.vue b/frontend/src/views/screen/ScreenMaterials.vue new file mode 100644 index 0000000..edfd4c7 --- /dev/null +++ b/frontend/src/views/screen/ScreenMaterials.vue @@ -0,0 +1,148 @@ + + + diff --git a/frontend/src/views/screen/ScreenOverview.vue b/frontend/src/views/screen/ScreenOverview.vue new file mode 100644 index 0000000..9d8ef86 --- /dev/null +++ b/frontend/src/views/screen/ScreenOverview.vue @@ -0,0 +1,173 @@ + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..f5ebf55 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') + } + }, + server: { + port: 5173 + } +})