feat: 项目初始化
This commit is contained in:
commit
adedaecf29
|
|
@ -0,0 +1,62 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
/media
|
||||
/staticfiles
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Node / Frontend
|
||||
node_modules/
|
||||
dist/
|
||||
dist-ssr/
|
||||
*.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
# 新材料数据库系统
|
||||
|
||||
## 项目简介
|
||||
新材料数据库管理系统是一个基于 Django + Vue3 的前后端分离系统,用于管理工厂、材料信息和数据字典,并提供数据大屏展示功能。
|
||||
|
||||
## 技术栈
|
||||
- 后端:Python 3.9+ + Django 4.2 + Django REST Framework + JWT
|
||||
- 数据库:PostgreSQL 14+
|
||||
- 前端:Vue 3 + Vite + Element Plus + ECharts 5
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
mat/
|
||||
├── backend/ # 后端项目
|
||||
│ ├── config/ # 项目配置
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── settings.py # 主配置文件
|
||||
│ │ ├── urls.py # 主路由
|
||||
│ │ ├── wsgi.py
|
||||
│ │ └── asgi.py
|
||||
│ ├── apps/ # 应用目录
|
||||
│ │ ├── authentication/ # 认证应用
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── models.py # 用户模型
|
||||
│ │ │ ├── serializers.py
|
||||
│ │ │ ├── views.py
|
||||
│ │ │ └── urls.py
|
||||
│ │ ├── factory/ # 工厂管理
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── models.py
|
||||
│ │ │ ├── serializers.py
|
||||
│ │ │ ├── views.py
|
||||
│ │ │ └── urls.py
|
||||
│ │ ├── material/ # 材料管理
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── models.py
|
||||
│ │ │ ├── serializers.py
|
||||
│ │ │ ├── views.py
|
||||
│ │ │ └── urls.py
|
||||
│ │ ├── dictionary/ # 数据字典
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── models.py
|
||||
│ │ │ ├── serializers.py
|
||||
│ │ │ ├── views.py
|
||||
│ │ │ └── urls.py
|
||||
│ │ └── statistics/ # 统计分析
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── views.py
|
||||
│ │ └── urls.py
|
||||
│ ├── media/ # 媒体文件
|
||||
│ ├── manage.py
|
||||
│ └── requirements.txt
|
||||
└── frontend/ # 前端项目
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── api/ # API接口
|
||||
│ │ ├── auth.js
|
||||
│ │ ├── factory.js
|
||||
│ │ ├── material.js
|
||||
│ │ ├── dictionary.js
|
||||
│ │ └── statistics.js
|
||||
│ ├── assets/ # 静态资源
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ ├── common/
|
||||
│ │ └── charts/ # 图表组件
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── stores/ # 状态管理
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── views/ # 页面视图
|
||||
│ │ ├── auth/ # 认证相关
|
||||
│ │ ├── user/ # 用户管理
|
||||
│ │ ├── factory/ # 工厂管理
|
||||
│ │ ├── material/ # 材料管理
|
||||
│ │ ├── dictionary/ # 字典管理
|
||||
│ │ └── dashboard/ # 数据大屏
|
||||
│ │ ├── Overview.vue
|
||||
│ │ ├── MaterialLibrary.vue
|
||||
│ │ └── FactoryLibrary.vue
|
||||
│ ├── App.vue
|
||||
│ └── main.js
|
||||
├── .env.development
|
||||
├── .env.production
|
||||
├── index.html
|
||||
├── package.json
|
||||
└── vite.config.js
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
### 1. 用户管理
|
||||
- 管理员可以创建普通用户并分配工厂
|
||||
- 普通用户可以登录系统
|
||||
|
||||
### 2. 工厂管理
|
||||
- 管理员可以创建和管理工厂信息
|
||||
- 普通用户可以完善所属工厂信息
|
||||
|
||||
### 3. 材料管理
|
||||
- 管理员可以管理所有材料
|
||||
- 普通用户可以录入材料并提交审核
|
||||
- 支持材料审核流程(创建中、待审核、已审核)
|
||||
|
||||
### 4. 数据字典管理
|
||||
- 管理员可以管理数据字典
|
||||
- 支持字典类型、名称和值的管理
|
||||
|
||||
### 5. 数据大屏(仅管理员可见)
|
||||
- 数据总览:展示材料总数、种类、品牌数等统计信息
|
||||
- 材料库:展示材料星级对比、竞争优势、应用场景等图表
|
||||
- 工厂库:展示工厂地区分布、材料分类分布等图表
|
||||
- 数据每10秒自动刷新
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 1. 工厂表 (factory)
|
||||
- id: 主键
|
||||
- dealer_name: 经销商名称
|
||||
- product_category: 产品分类
|
||||
- factory_name: 生产工厂全称
|
||||
- factory_short_name: 工厂简称
|
||||
- province: 省
|
||||
- city: 市
|
||||
- district: 区
|
||||
- address: 详细地址
|
||||
- website: 官网链接
|
||||
- created_at: 创建时间
|
||||
- updated_at: 更新时间
|
||||
|
||||
### 2. 材料表 (material)
|
||||
- id: 主键
|
||||
- name: 材料名称
|
||||
- major_category: 专业类别
|
||||
- material_category: 材料分类
|
||||
- material_subcategory: 材料子分类
|
||||
- spec: 规格型号
|
||||
- standard: 符合标准
|
||||
- application_scene: 应用场景
|
||||
- application_desc: 应用场景说明
|
||||
- replace_type: 替代材料类型
|
||||
- advantage: 竞争优势
|
||||
- advantage_desc: 优势说明
|
||||
- cost_compare: 成本对比百分数
|
||||
- cost_desc: 成本说明
|
||||
- cases: 案例
|
||||
- brochure: 宣传页图片
|
||||
- quality_level: 质量提升等级
|
||||
- durability_level: 耐久可靠等级
|
||||
- eco_level: 环保健康等级
|
||||
- carbon_level: 循环低碳等级
|
||||
- score_level: 总评分等级
|
||||
- connection_method: 连接方式
|
||||
- construction_method: 施工工艺
|
||||
- limit_condition: 限制条件
|
||||
- factory_id: 所属工厂
|
||||
- status: 状态(draft/pending/approved)
|
||||
|
||||
### 3. 数据字典 (dictionary)
|
||||
- id: 主键
|
||||
- type: 字典类型
|
||||
- name: 字典名称
|
||||
- value: 字典值
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 后端部署
|
||||
1. 安装依赖:
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. 配置数据库:
|
||||
修改 `config/settings.py` 中的数据库配置
|
||||
|
||||
3. 初始化数据库:
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
4. 创建超级用户:
|
||||
```bash
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
5. 启动服务:
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
### 前端部署
|
||||
1. 安装依赖:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
2. 配置环境变量:
|
||||
修改 `.env.development` 和 `.env.production` 文件
|
||||
|
||||
3. 启动开发服务器:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. 构建生产版本:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## API接口文档
|
||||
|
||||
### 认证接口
|
||||
- POST /api/auth/login/ - 用户登录
|
||||
- POST /api/auth/logout/ - 用户登出
|
||||
- GET /api/auth/user/ - 获取当前用户信息
|
||||
|
||||
### 工厂接口
|
||||
- GET /api/factory/ - 获取工厂列表
|
||||
- POST /api/factory/ - 创建工厂
|
||||
- GET /api/factory/{id}/ - 获取工厂详情
|
||||
- PUT /api/factory/{id}/ - 更新工厂信息
|
||||
- DELETE /api/factory/{id}/ - 删除工厂
|
||||
|
||||
### 材料接口
|
||||
- GET /api/material/ - 获取材料列表
|
||||
- POST /api/material/ - 创建材料
|
||||
- GET /api/material/{id}/ - 获取材料详情
|
||||
- PUT /api/material/{id}/ - 更新材料信息
|
||||
- DELETE /api/material/{id}/ - 删除材料
|
||||
- POST /api/material/{id}/submit/ - 提交审核
|
||||
- POST /api/material/{id}/approve/ - 审核通过
|
||||
- POST /api/material/{id}/reject/ - 审核拒绝
|
||||
|
||||
### 统计接口
|
||||
- GET /api/statistics/overview/ - 数据总览
|
||||
- GET /api/statistics/materials/ - 材料统计
|
||||
- GET /api/statistics/factories/ - 工厂统计
|
||||
|
||||
### 字典接口
|
||||
- GET /api/dictionary/ - 获取字典列表
|
||||
- POST /api/dictionary/ - 创建字典
|
||||
- GET /api/dictionary/{id}/ - 获取字典详情
|
||||
- PUT /api/dictionary/{id}/ - 更新字典
|
||||
- DELETE /api/dictionary/{id}/ - 删除字典
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Generated migration for authentication app
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('factory', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', 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, verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='date joined')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('role', models.CharField(choices=[('admin', '管理员'), ('user', '普通账号')], default='user', max_length=20, verbose_name='角色')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机号')),
|
||||
('factory', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='factory.factory', verbose_name='所属工厂')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户',
|
||||
'db_table': 'auth_user',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
# This file is required for Python to treat the directory as a package.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
自定义用户模型
|
||||
"""
|
||||
ROLE_CHOICES = (
|
||||
('admin', '管理员'),
|
||||
('user', '普通账号'),
|
||||
)
|
||||
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user', verbose_name='角色')
|
||||
factory = models.ForeignKey('factory.Factory', on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name='users', verbose_name='所属工厂')
|
||||
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name='手机号')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '用户'
|
||||
verbose_name_plural = '用户'
|
||||
db_table = 'auth_user'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.username} ({self.get_role_display()})"
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
用户序列化器
|
||||
"""
|
||||
factory_name = serializers.CharField(source='factory.factory_name', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'role',
|
||||
'factory', 'factory_name', 'phone', 'is_active', 'date_joined']
|
||||
read_only_fields = ['id', 'date_joined']
|
||||
|
||||
|
||||
class UserCreateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
用户创建序列化器
|
||||
"""
|
||||
password = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'})
|
||||
password_confirm = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'})
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'role',
|
||||
'factory', 'phone', 'password', 'password_confirm']
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['password'] != attrs['password_confirm']:
|
||||
raise serializers.ValidationError({"password": "密码字段不匹配。"})
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data.pop('password_confirm')
|
||||
password = validated_data.pop('password')
|
||||
user = User.objects.create(**validated_data)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||
"""
|
||||
自定义JWT令牌序列化器
|
||||
"""
|
||||
@classmethod
|
||||
def get_token(cls, user):
|
||||
token = super().get_token(user)
|
||||
|
||||
# 添加自定义声明
|
||||
token['role'] = user.role
|
||||
token['factory_id'] = user.factory_id if user.factory else None
|
||||
|
||||
return token
|
||||
|
||||
def validate(self, attrs):
|
||||
data = super().validate(attrs)
|
||||
|
||||
# 添加用户信息到响应
|
||||
data['user'] = UserSerializer(self.user).data
|
||||
|
||||
return data
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from django.urls import path
|
||||
from rest_framework_simplejwt.views import TokenRefreshView
|
||||
from .views import CustomTokenObtainPairView, UserListView, UserDetailView, current_user
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('users/', UserListView.as_view(), name='user-list'),
|
||||
path('users/<int:pk>/', UserDetailView.as_view(), name='user-detail'),
|
||||
path('user/', current_user, name='current-user'),
|
||||
]
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
from rest_framework import generics, status
|
||||
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 .models import User
|
||||
from .serializers import UserSerializer, UserCreateSerializer, CustomTokenObtainPairSerializer
|
||||
|
||||
|
||||
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||
"""
|
||||
自定义JWT令牌获取视图
|
||||
"""
|
||||
serializer_class = CustomTokenObtainPairSerializer
|
||||
|
||||
|
||||
class UserListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
用户列表和创建视图
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'POST':
|
||||
return UserCreateSerializer
|
||||
return UserSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# 只有管理员可以创建用户
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionError("只有管理员可以创建用户")
|
||||
serializer.save()
|
||||
|
||||
|
||||
class UserDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
用户详情视图
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def perform_update(self, serializer):
|
||||
# 普通用户只能修改自己的信息
|
||||
if self.request.user.role != 'admin' and self.request.user.id != self.get_object().id:
|
||||
raise PermissionError("无权修改其他用户信息")
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# 只有管理员可以删除用户
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionError("只有管理员可以删除用户")
|
||||
instance.delete()
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def current_user(request):
|
||||
"""
|
||||
获取当前用户信息
|
||||
"""
|
||||
serializer = UserSerializer(request.user)
|
||||
return Response(serializer.data)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Generated migration for dictionary app
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Dictionary',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.CharField(max_length=100, verbose_name='字典类型')),
|
||||
('name', models.CharField(max_length=255, verbose_name='字典名称')),
|
||||
('value', models.CharField(max_length=255, 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': 'dictionary',
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='dictionary',
|
||||
unique_together={('type', 'value')},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
# This file is required for Python to treat the directory as a package.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
from django.db import models
|
||||
|
||||
class Dictionary(models.Model):
|
||||
"""
|
||||
数据字典模型
|
||||
"""
|
||||
type = models.CharField(max_length=100, verbose_name='字典类型')
|
||||
name = models.CharField(max_length=255, verbose_name='字典名称')
|
||||
value = models.CharField(max_length=255, 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 = 'dictionary'
|
||||
unique_together = ('type', 'value')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.type} - {self.name}"
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Dictionary
|
||||
|
||||
|
||||
class DictionarySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
数据字典序列化器
|
||||
"""
|
||||
class Meta:
|
||||
model = Dictionary
|
||||
fields = ['id', 'type', 'name', 'value', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
class DictionaryGroupSerializer(serializers.Serializer):
|
||||
"""
|
||||
数据字典分组序列化器
|
||||
"""
|
||||
type = serializers.CharField()
|
||||
items = DictionarySerializer(many=True)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
from .views import DictionaryListView, DictionaryDetailView, dictionary_grouped
|
||||
|
||||
urlpatterns = [
|
||||
path('', DictionaryListView.as_view(), name='dictionary-list'),
|
||||
path('grouped/', dictionary_grouped, name='dictionary-grouped'),
|
||||
path('<int:pk>/', DictionaryDetailView.as_view(), name='dictionary-detail'),
|
||||
]
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
from rest_framework import generics, status
|
||||
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 .models import Dictionary
|
||||
from .serializers import DictionarySerializer, DictionaryGroupSerializer
|
||||
|
||||
|
||||
class DictionaryListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
数据字典列表和创建视图
|
||||
"""
|
||||
serializer_class = DictionarySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
支持按类型过滤
|
||||
"""
|
||||
queryset = Dictionary.objects.all()
|
||||
|
||||
# 支持按类型过滤
|
||||
dict_type = self.request.query_params.get('type')
|
||||
if dict_type:
|
||||
queryset = queryset.filter(type=dict_type)
|
||||
|
||||
# 支持按名称搜索
|
||||
name = self.request.query_params.get('name')
|
||||
if name:
|
||||
queryset = queryset.filter(name__icontains=name)
|
||||
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# 只有管理员可以创建字典
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionError("只有管理员可以创建字典")
|
||||
serializer.save()
|
||||
|
||||
|
||||
class DictionaryDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
数据字典详情视图
|
||||
"""
|
||||
queryset = Dictionary.objects.all()
|
||||
serializer_class = DictionarySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def perform_update(self, serializer):
|
||||
# 只有管理员可以更新字典
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionError("只有管理员可以更新字典")
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# 只有管理员可以删除字典
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionError("只有管理员可以删除字典")
|
||||
instance.delete()
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def dictionary_grouped(request):
|
||||
"""
|
||||
获取分组的数据字典
|
||||
"""
|
||||
# 获取所有字典类型
|
||||
dict_types = Dictionary.objects.values('type').distinct()
|
||||
|
||||
result = []
|
||||
for dict_type in dict_types:
|
||||
type_name = dict_type['type']
|
||||
items = Dictionary.objects.filter(type=type_name)
|
||||
serializer = DictionarySerializer(items, many=True)
|
||||
result.append({
|
||||
'type': type_name,
|
||||
'items': serializer.data
|
||||
})
|
||||
|
||||
return Response(result)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# Generated migration for factory app
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Factory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('dealer_name', models.CharField(max_length=255, verbose_name='经销商名称')),
|
||||
('product_category', models.CharField(blank=True, max_length=255, null=True, verbose_name='产品分类')),
|
||||
('factory_name', models.CharField(max_length=255, verbose_name='生产工厂全称')),
|
||||
('factory_short_name', models.CharField(max_length=100, verbose_name='工厂简称')),
|
||||
('province', models.CharField(max_length=50, verbose_name='省')),
|
||||
('city', models.CharField(max_length=50, verbose_name='市')),
|
||||
('district', models.CharField(blank=True, max_length=50, null=True, verbose_name='区')),
|
||||
('address', models.TextField(blank=True, null=True, verbose_name='详细地址')),
|
||||
('website', models.URLField(blank=True, null=True, verbose_name='官网链接')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '工厂',
|
||||
'verbose_name_plural': '工厂',
|
||||
'db_table': 'factory',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
# This file is required for Python to treat the directory as a package.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from django.db import models
|
||||
|
||||
class Factory(models.Model):
|
||||
"""
|
||||
工厂模型
|
||||
"""
|
||||
dealer_name = models.CharField(max_length=255, verbose_name='经销商名称')
|
||||
product_category = models.CharField(max_length=255, blank=True, null=True, verbose_name='产品分类')
|
||||
factory_name = models.CharField(max_length=255, verbose_name='生产工厂全称')
|
||||
factory_short_name = models.CharField(max_length=100, verbose_name='工厂简称')
|
||||
province = models.CharField(max_length=50, verbose_name='省')
|
||||
city = models.CharField(max_length=50, verbose_name='市')
|
||||
district = models.CharField(max_length=50, blank=True, null=True, verbose_name='区')
|
||||
address = models.TextField(blank=True, null=True, verbose_name='详细地址')
|
||||
website = models.URLField(blank=True, null=True, verbose_name='官网链接')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '工厂'
|
||||
verbose_name_plural = '工厂'
|
||||
db_table = 'factory'
|
||||
|
||||
def __str__(self):
|
||||
return self.factory_name
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Factory
|
||||
|
||||
|
||||
class FactorySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
工厂序列化器
|
||||
"""
|
||||
material_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'dealer_name', 'product_category', 'factory_name',
|
||||
'factory_short_name', 'province', 'city', 'district',
|
||||
'address', 'website', 'created_at', 'updated_at', 'material_count']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count']
|
||||
|
||||
def get_material_count(self, obj):
|
||||
"""
|
||||
获取工厂的材料数量
|
||||
"""
|
||||
return obj.materials.count()
|
||||
|
||||
|
||||
class FactoryListSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
工厂列表序列化器(简化版)
|
||||
"""
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'factory_name', 'factory_short_name', 'province', 'city']
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
from .views import FactoryListView, FactoryDetailView, factory_list_simple
|
||||
|
||||
urlpatterns = [
|
||||
path('', FactoryListView.as_view(), name='factory-list'),
|
||||
path('simple/', factory_list_simple, name='factory-list-simple'),
|
||||
path('<int:pk>/', FactoryDetailView.as_view(), name='factory-detail'),
|
||||
]
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
from rest_framework import generics, status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from .models import Factory
|
||||
from .serializers import FactorySerializer, FactoryListSerializer
|
||||
|
||||
|
||||
class FactoryListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
工厂列表和创建视图
|
||||
"""
|
||||
queryset = Factory.objects.all()
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return FactoryListSerializer
|
||||
return FactorySerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# 只有管理员可以创建工厂
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionError("只有管理员可以创建工厂")
|
||||
serializer.save()
|
||||
|
||||
|
||||
class FactoryDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
工厂详情视图
|
||||
"""
|
||||
queryset = Factory.objects.all()
|
||||
serializer_class = FactorySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def perform_update(self, serializer):
|
||||
# 普通用户只能修改自己所属工厂的信息
|
||||
if (self.request.user.role != 'admin' and
|
||||
self.request.user.factory_id != self.get_object().id):
|
||||
raise PermissionError("无权修改其他工厂信息")
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# 只有管理员可以删除工厂
|
||||
if self.request.user.role != 'admin':
|
||||
raise PermissionError("只有管理员可以删除工厂")
|
||||
instance.delete()
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def factory_list_simple(request):
|
||||
"""
|
||||
简化的工厂列表,用于下拉选择
|
||||
"""
|
||||
factories = Factory.objects.all()
|
||||
serializer = FactoryListSerializer(factories, many=True)
|
||||
return Response(serializer.data)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# Generated migration for material app
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('factory', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Material',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='材料名称')),
|
||||
('major_category', models.CharField(choices=[('architecture', '建筑'), ('landscape', '景观'), ('equipment', '设备'), ('decoration', '装修')], max_length=20, verbose_name='专业类别')),
|
||||
('material_category', models.CharField(max_length=255, verbose_name='材料分类')),
|
||||
('material_subcategory', models.CharField(max_length=255, verbose_name='材料子分类')),
|
||||
('spec', models.CharField(blank=True, max_length=255, null=True, verbose_name='规格型号')),
|
||||
('standard', models.CharField(blank=True, max_length=255, null=True, verbose_name='符合标准')),
|
||||
('application_scene', models.CharField(blank=True, choices=[('fu', '府系'), ('jing', '境系'), ('cheng', '城系'), ('zhu', '住系'), ('affordable', '保障房')], max_length=20, null=True, verbose_name='应用场景')),
|
||||
('application_desc', models.TextField(blank=True, null=True, verbose_name='应用场景说明')),
|
||||
('replace_type', models.CharField(blank=True, choices=[('alternative', '平替'), ('new_development', '新研发')], max_length=20, null=True, verbose_name='替代材料类型')),
|
||||
('advantage', models.CharField(blank=True, choices=[('quality', '品质'), ('cost', '成本')], max_length=20, null=True, verbose_name='竞争优势')),
|
||||
('advantage_desc', models.TextField(blank=True, null=True, verbose_name='优势说明')),
|
||||
('cost_compare', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='成本对比百分数')),
|
||||
('cost_desc', models.TextField(blank=True, null=True, verbose_name='成本说明')),
|
||||
('cases', models.TextField(blank=True, null=True, verbose_name='案例')),
|
||||
('brochure', models.ImageField(blank=True, null=True, upload_to='material_brochures/', verbose_name='宣传页图片')),
|
||||
('quality_level', models.IntegerField(blank=True, choices=[(1, '1星'), (2, '2星'), (3, '3星')], null=True, verbose_name='质量提升等级')),
|
||||
('durability_level', models.IntegerField(blank=True, choices=[(1, '1星'), (2, '2星'), (3, '3星')], null=True, verbose_name='耐久可靠等级')),
|
||||
('eco_level', models.IntegerField(blank=True, choices=[(1, '1星'), (2, '2星'), (3, '3星')], null=True, verbose_name='环保健康等级')),
|
||||
('carbon_level', models.IntegerField(blank=True, choices=[(1, '1星'), (2, '2星'), (3, '3星')], null=True, verbose_name='循环低碳等级')),
|
||||
('score_level', models.IntegerField(blank=True, choices=[(1, '1星'), (2, '2星'), (3, '3星')], null=True, verbose_name='总评分等级')),
|
||||
('connection_method', models.CharField(blank=True, max_length=255, null=True, verbose_name='连接方式')),
|
||||
('construction_method', models.CharField(blank=True, max_length=255, null=True, verbose_name='施工工艺')),
|
||||
('limit_condition', models.TextField(blank=True, null=True, verbose_name='限制条件')),
|
||||
('status', models.CharField(choices=[('draft', '创建中'), ('pending', '待审核'), ('approved', '已审核')], default='draft', max_length=20, verbose_name='状态')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('factory', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materials', to='factory.factory', verbose_name='所属工厂')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '材料',
|
||||
'verbose_name_plural': '材料',
|
||||
'db_table': 'material',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
# This file is required for Python to treat the directory as a package.
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
from django.db import models
|
||||
|
||||
class Material(models.Model):
|
||||
"""
|
||||
材料模型
|
||||
"""
|
||||
MAJOR_CATEGORY_CHOICES = (
|
||||
('architecture', '建筑'),
|
||||
('landscape', '景观'),
|
||||
('equipment', '设备'),
|
||||
('decoration', '装修'),
|
||||
)
|
||||
|
||||
REPLACE_TYPE_CHOICES = (
|
||||
('alternative', '平替'),
|
||||
('new_development', '新研发'),
|
||||
)
|
||||
|
||||
ADVANTAGE_CHOICES = (
|
||||
('quality', '品质'),
|
||||
('cost', '成本'),
|
||||
)
|
||||
|
||||
APPLICATION_SCENE_CHOICES = (
|
||||
('fu', '府系'),
|
||||
('jing', '境系'),
|
||||
('cheng', '城系'),
|
||||
('zhu', '住系'),
|
||||
('affordable', '保障房'),
|
||||
)
|
||||
|
||||
STAR_LEVEL_CHOICES = (
|
||||
(1, '1星'),
|
||||
(2, '2星'),
|
||||
(3, '3星'),
|
||||
)
|
||||
|
||||
STATUS_CHOICES = (
|
||||
('draft', '创建中'),
|
||||
('pending', '待审核'),
|
||||
('approved', '已审核'),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255, verbose_name='材料名称')
|
||||
major_category = models.CharField(max_length=20, choices=MAJOR_CATEGORY_CHOICES, verbose_name='专业类别')
|
||||
material_category = models.CharField(max_length=255, verbose_name='材料分类')
|
||||
material_subcategory = models.CharField(max_length=255, verbose_name='材料子分类')
|
||||
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_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_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='成本说明')
|
||||
cases = models.TextField(blank=True, null=True, verbose_name='案例')
|
||||
brochure = models.ImageField(upload_to='material_brochures/', blank=True, null=True, verbose_name='宣传页图片')
|
||||
quality_level = models.IntegerField(choices=STAR_LEVEL_CHOICES, blank=True, null=True, verbose_name='质量提升等级')
|
||||
durability_level = models.IntegerField(choices=STAR_LEVEL_CHOICES, blank=True, null=True, verbose_name='耐久可靠等级')
|
||||
eco_level = models.IntegerField(choices=STAR_LEVEL_CHOICES, blank=True, null=True, verbose_name='环保健康等级')
|
||||
carbon_level = models.IntegerField(choices=STAR_LEVEL_CHOICES, blank=True, null=True, verbose_name='循环低碳等级')
|
||||
score_level = models.IntegerField(choices=STAR_LEVEL_CHOICES, blank=True, null=True, verbose_name='总评分等级')
|
||||
connection_method = models.CharField(max_length=255, blank=True, null=True, verbose_name='连接方式')
|
||||
construction_method = models.CharField(max_length=255, blank=True, null=True, verbose_name='施工工艺')
|
||||
limit_condition = models.TextField(blank=True, null=True, verbose_name='限制条件')
|
||||
factory = models.ForeignKey('factory.Factory', on_delete=models.CASCADE, related_name='materials', verbose_name='所属工厂')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name='状态')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '材料'
|
||||
verbose_name_plural = '材料'
|
||||
db_table = 'material'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Material
|
||||
|
||||
|
||||
class MaterialSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
材料序列化器
|
||||
"""
|
||||
factory_name = serializers.CharField(source='factory.factory_name', read_only=True)
|
||||
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)
|
||||
brochure_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Material
|
||||
fields = ['id', 'name', 'major_category', 'major_category_display',
|
||||
'material_category', 'material_subcategory', 'spec', 'standard',
|
||||
'application_scene', 'application_scene_display', 'application_desc',
|
||||
'replace_type', 'replace_type_display', 'advantage', 'advantage_display',
|
||||
'advantage_desc', 'cost_compare', 'cost_desc', 'cases', 'brochure',
|
||||
'brochure_url', 'quality_level', 'durability_level', 'eco_level',
|
||||
'carbon_level', 'score_level', 'connection_method', 'construction_method',
|
||||
'limit_condition', 'factory', 'factory_name', 'factory_short_name',
|
||||
'status', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
def get_brochure_url(self, obj):
|
||||
"""
|
||||
获取宣传页图片URL
|
||||
"""
|
||||
if obj.brochure:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
return request.build_absolute_uri(obj.brochure.url)
|
||||
return None
|
||||
|
||||
|
||||
class MaterialListSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
材料列表序列化器(简化版)
|
||||
"""
|
||||
factory_name = serializers.CharField(source='factory.factory_name', read_only=True)
|
||||
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)
|
||||
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Material
|
||||
fields = ['id', 'name', 'major_category', 'major_category_display',
|
||||
'material_category', 'material_subcategory', 'factory',
|
||||
'factory_name', 'factory_short_name', 'status', 'status_display']
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import MaterialViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', MaterialViewSet, basename='material')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
from rest_framework import generics, status
|
||||
from rest_framework.decorators import api_view, permission_classes, 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
|
||||
|
||||
|
||||
class MaterialViewSet(ModelViewSet):
|
||||
"""
|
||||
材料视图集
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
根据用户角色过滤材料
|
||||
"""
|
||||
queryset = Material.objects.all()
|
||||
|
||||
# 普通用户只能看到自己工厂的材料
|
||||
if self.request.user.role != 'admin':
|
||||
queryset = queryset.filter(factory=self.request.user.factory)
|
||||
|
||||
# 支持按状态过滤
|
||||
status_filter = self.request.query_params.get('status')
|
||||
if status_filter:
|
||||
queryset = queryset.filter(status=status_filter)
|
||||
|
||||
# 支持按工厂过滤
|
||||
factory_id = self.request.query_params.get('factory_id')
|
||||
if factory_id:
|
||||
queryset = queryset.filter(factory_id=factory_id)
|
||||
|
||||
# 支持按专业类别过滤
|
||||
major_category = self.request.query_params.get('major_category')
|
||||
if major_category:
|
||||
queryset = queryset.filter(major_category=major_category)
|
||||
|
||||
# 支持按材料子类过滤
|
||||
material_subcategory = self.request.query_params.get('material_subcategory')
|
||||
if material_subcategory:
|
||||
queryset = queryset.filter(material_subcategory=material_subcategory)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
根据操作类型选择序列化器
|
||||
"""
|
||||
if self.action == 'list':
|
||||
return MaterialListSerializer
|
||||
return MaterialSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
创建材料时自动设置工厂
|
||||
"""
|
||||
# 普通用户只能为自己工厂创建材料
|
||||
if self.request.user.role != 'admin':
|
||||
serializer.save(factory=self.request.user.factory)
|
||||
else:
|
||||
serializer.save()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""
|
||||
更新材料时的权限控制
|
||||
"""
|
||||
# 普通用户只能更新自己工厂的材料
|
||||
if (self.request.user.role != 'admin' and
|
||||
self.request.user.factory_id != self.get_object().factory_id):
|
||||
raise PermissionError("无权修改其他工厂的材料")
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""
|
||||
删除材料时的权限控制
|
||||
"""
|
||||
# 普通用户只能删除自己工厂的材料
|
||||
if (self.request.user.role != 'admin' and
|
||||
self.request.user.factory_id != instance.factory_id):
|
||||
raise PermissionError("无权删除其他工厂的材料")
|
||||
instance.delete()
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def submit(self, request, pk=None):
|
||||
"""
|
||||
提交审核
|
||||
"""
|
||||
material = self.get_object()
|
||||
|
||||
# 普通用户只能提交自己工厂的材料
|
||||
if (request.user.role != 'admin' and
|
||||
request.user.factory_id != material.factory_id):
|
||||
return Response(
|
||||
{"detail": "无权提交其他工厂的材料"},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
if material.status != 'draft':
|
||||
return Response(
|
||||
{"detail": "只有创建中的材料才能提交审核"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
material.status = 'pending'
|
||||
material.save()
|
||||
return Response({"status": "已提交审核"})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def approve(self, request, pk=None):
|
||||
"""
|
||||
审核通过
|
||||
"""
|
||||
# 只有管理员可以审核
|
||||
if request.user.role != 'admin':
|
||||
return Response(
|
||||
{"detail": "只有管理员可以审核材料"},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
material = self.get_object()
|
||||
if material.status != 'pending':
|
||||
return Response(
|
||||
{"detail": "只有待审核的材料才能审核"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
material.status = 'approved'
|
||||
material.save()
|
||||
return Response({"status": "审核通过"})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def reject(self, request, pk=None):
|
||||
"""
|
||||
审核拒绝
|
||||
"""
|
||||
# 只有管理员可以审核
|
||||
if request.user.role != 'admin':
|
||||
return Response(
|
||||
{"detail": "只有管理员可以审核材料"},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
material = self.get_object()
|
||||
if material.status != 'pending':
|
||||
return Response(
|
||||
{"detail": "只有待审核的材料才能审核"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
material.status = 'draft'
|
||||
material.save()
|
||||
return Response({"status": "审核拒绝"})
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
from .views import overview_statistics, material_statistics, factory_statistics
|
||||
|
||||
urlpatterns = [
|
||||
path('overview/', overview_statistics, name='overview-statistics'),
|
||||
path('materials/', material_statistics, name='material-statistics'),
|
||||
path('factories/', factory_statistics, name='factory-statistics'),
|
||||
]
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
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 apps.material.models import Material
|
||||
from apps.factory.models import Factory
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def overview_statistics(request):
|
||||
"""
|
||||
数据总览统计
|
||||
"""
|
||||
# 只有管理员可以访问
|
||||
if request.user.role != 'admin':
|
||||
return Response({"detail": "无权访问"}, status=403)
|
||||
|
||||
# 材料总数
|
||||
total_materials = Material.objects.count()
|
||||
|
||||
# 材料种类(材料子类数量)
|
||||
total_material_categories = Material.objects.values('material_subcategory').distinct().count()
|
||||
|
||||
# 品牌数(工厂数)
|
||||
total_brands = Factory.objects.count()
|
||||
|
||||
# 按专业类别的材料数量分布
|
||||
major_category_stats = Material.objects.values('major_category').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')
|
||||
|
||||
# 按材料子类的材料数量分布
|
||||
material_subcategory_stats = Material.objects.values('material_subcategory').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')[:10] # 取前10个
|
||||
|
||||
# 按所属品牌的材料数量分布
|
||||
brand_stats = Material.objects.values('factory__factory_name').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')
|
||||
|
||||
# 按地区的工厂数量分布
|
||||
region_stats = Factory.objects.values('province', 'city').annotate(
|
||||
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]
|
||||
|
||||
return Response({
|
||||
'total_materials': total_materials,
|
||||
'total_material_categories': total_material_categories,
|
||||
'total_brands': total_brands,
|
||||
'major_category_stats': list(major_category_stats),
|
||||
'material_subcategory_stats': list(material_subcategory_stats),
|
||||
'brand_stats': list(brand_stats),
|
||||
'region_stats': list(region_stats),
|
||||
'cases_list': list(cases_list),
|
||||
})
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
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'))),
|
||||
}
|
||||
|
||||
# 竞争优势与替代材料对比
|
||||
advantage_replace_stats = list(queryset.values('advantage', 'replace_type').annotate(
|
||||
count=Count('id')
|
||||
))
|
||||
|
||||
# 应用场景对比
|
||||
application_scene_stats = list(queryset.values('application_scene').annotate(
|
||||
count=Count('id')
|
||||
))
|
||||
|
||||
# 材料列表
|
||||
materials_list = list(queryset.values(
|
||||
'id', 'name', 'material_category', 'material_subcategory',
|
||||
'factory__factory_name', 'brochure'
|
||||
)[:20])
|
||||
|
||||
return Response({
|
||||
'star_stats': star_stats,
|
||||
'advantage_replace_stats': advantage_replace_stats,
|
||||
'application_scene_stats': application_scene_stats,
|
||||
'materials_list': materials_list,
|
||||
})
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
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 = []
|
||||
for factory in Factory.objects.all():
|
||||
material_categories = Material.objects.filter(factory=factory).values(
|
||||
'material_category'
|
||||
).annotate(
|
||||
count=Count('id')
|
||||
)
|
||||
|
||||
factory_category_stats.append({
|
||||
'factory_id': factory.id,
|
||||
'factory_name': factory.factory_name,
|
||||
'categories': list(material_categories),
|
||||
'total_materials': factory.materials.count()
|
||||
})
|
||||
|
||||
# 工厂列表
|
||||
factories_list = list(Factory.objects.values(
|
||||
'id', 'factory_name', 'factory_short_name', 'province', 'city', 'website'
|
||||
))
|
||||
|
||||
return Response({
|
||||
'region_stats': region_stats,
|
||||
'factory_category_stats': factory_category_stats,
|
||||
'factories_list': factories_list,
|
||||
})
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
ASGI config for new_materials_db project.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
"""
|
||||
Django settings for new_materials_db project.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from decouple import config
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = config('SECRET_KEY', default='django-insecure-change-in-production')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = config('DEBUG', default=True, cast=bool)
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'corsheaders',
|
||||
'apps.authentication',
|
||||
'apps.factory',
|
||||
'apps.material',
|
||||
'apps.dictionary',
|
||||
'apps.statistics',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': config('DB_NAME', default='new_materials_db'),
|
||||
'USER': config('DB_USER', default='postgres'),
|
||||
'PASSWORD': config('DB_PASSWORD', default='postgres'),
|
||||
'HOST': config('DB_HOST', default='localhost'),
|
||||
'PORT': config('DB_PORT', default='5432'),
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
# Media files
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Custom user model
|
||||
AUTH_USER_MODEL = 'authentication.User'
|
||||
|
||||
# REST Framework settings
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
}
|
||||
|
||||
# JWT settings
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=2),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||
'ROTATE_REFRESH_TOKENS': False,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
'UPDATE_LAST_LOGIN': True,
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
}
|
||||
|
||||
# CORS settings
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
]
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
"""
|
||||
URL configuration for new_materials_db project.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/auth/', include('apps.authentication.urls')),
|
||||
path('api/factory/', include('apps.factory.urls')),
|
||||
path('api/material/', include('apps.material.urls')),
|
||||
path('api/dictionary/', include('apps.dictionary.urls')),
|
||||
path('api/statistics/', include('apps.statistics.urls')),
|
||||
]
|
||||
|
||||
# 开发环境下提供媒体文件服务
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
WSGI config for new_materials_db project.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Django==4.2.7
|
||||
djangorestframework==3.14.0
|
||||
djangorestframework-simplejwt==5.3.0
|
||||
psycopg2-binary==2.9.9
|
||||
django-cors-headers==4.3.0
|
||||
Pillow==10.1.0
|
||||
python-decouple==3.8
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 开发环境配置
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 生产环境配置
|
||||
VITE_API_BASE_URL=/api
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# 新材料数据库管理系统 - 前端
|
||||
|
||||
## 项目介绍
|
||||
|
||||
新材料数据库管理系统前端,基于Vue 3 + Element Plus构建。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3 - 渐进式JavaScript框架
|
||||
- Vite - 新一代前端构建工具
|
||||
- Element Plus - 基于Vue 3的组件库
|
||||
- Vue Router - Vue.js官方路由
|
||||
- Pinia - Vue状态管理库
|
||||
- Axios - HTTP客户端
|
||||
- ECharts - 数据可视化库
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## 开发模式
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 生产构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 预览生产构建
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── public/ # 静态资源
|
||||
├── src/
|
||||
│ ├── api/ # API接口
|
||||
│ ├── assets/ # 资源文件
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── layout/ # 布局组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── stores/ # 状态管理
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── views/ # 页面组件
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.js # 入口文件
|
||||
├── index.html # HTML模板
|
||||
├── package.json # 项目配置
|
||||
└── vite.config.js # Vite配置
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
- 开发环境: `.env.development`
|
||||
- 生产环境: `.env.production`
|
||||
|
||||
## 功能模块
|
||||
|
||||
- 用户认证与授权
|
||||
- 材料库管理
|
||||
- 工厂库管理
|
||||
- 数据统计与分析
|
||||
- 字典管理
|
||||
- 用户管理
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>新材料数据库管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "new-materials-frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"pinia": "^2.1.6",
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.14",
|
||||
"echarts": "^5.4.3",
|
||||
"@element-plus/icons-vue": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"vite": "^4.4.9",
|
||||
"sass": "^1.66.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时检查用户登录状态
|
||||
userStore.checkAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {Object} credentials - 登录凭证 { username, password }
|
||||
* @returns {Promise} - 返回包含token和用户信息的Promise
|
||||
*/
|
||||
export function login(credentials) {
|
||||
return request({
|
||||
url: '/auth/login/',
|
||||
method: 'post',
|
||||
data: credentials
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns {Promise} - 返回用户信息的Promise
|
||||
*/
|
||||
export function getCurrentUser() {
|
||||
return request({
|
||||
url: '/auth/user/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @param {String} refreshToken - 刷新令牌
|
||||
* @returns {Promise} - 返回新token的Promise
|
||||
*/
|
||||
export function refreshToken(refreshToken) {
|
||||
return request({
|
||||
url: '/auth/token/refresh/',
|
||||
method: 'post',
|
||||
data: { refresh: refreshToken }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回用户列表的Promise
|
||||
*/
|
||||
export function getUserList(params) {
|
||||
return request({
|
||||
url: '/auth/users/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
* @param {Object} data - 用户数据
|
||||
* @returns {Promise} - 返回创建的用户信息的Promise
|
||||
*/
|
||||
export function createUser(data) {
|
||||
return request({
|
||||
url: '/auth/users/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
* @param {Number} id - 用户ID
|
||||
* @returns {Promise} - 返回用户详情的Promise
|
||||
*/
|
||||
export function getUserDetail(id) {
|
||||
return request({
|
||||
url: `/auth/users/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {Number} id - 用户ID
|
||||
* @param {Object} data - 更新的用户数据
|
||||
* @returns {Promise} - 返回更新后的用户信息的Promise
|
||||
*/
|
||||
export function updateUser(id, data) {
|
||||
return request({
|
||||
url: `/auth/users/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param {Number} id - 用户ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteUser(id) {
|
||||
return request({
|
||||
url: `/auth/users/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取字典列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回字典列表的Promise
|
||||
*/
|
||||
export function getDictionaryList(params) {
|
||||
return request({
|
||||
url: '/dictionary/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组字典
|
||||
* @returns {Promise} - 返回分组字典的Promise
|
||||
*/
|
||||
export function getDictionaryGrouped() {
|
||||
return request({
|
||||
url: '/dictionary/grouped/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典详情
|
||||
* @param {Number} id - 字典ID
|
||||
* @returns {Promise} - 返回字典详情的Promise
|
||||
*/
|
||||
export function getDictionaryDetail(id) {
|
||||
return request({
|
||||
url: `/dictionary/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建字典
|
||||
* @param {Object} data - 字典数据
|
||||
* @returns {Promise} - 返回创建的字典信息的Promise
|
||||
*/
|
||||
export function createDictionary(data) {
|
||||
return request({
|
||||
url: '/dictionary/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典信息
|
||||
* @param {Number} id - 字典ID
|
||||
* @param {Object} data - 更新的字典数据
|
||||
* @returns {Promise} - 返回更新后的字典信息的Promise
|
||||
*/
|
||||
export function updateDictionary(id, data) {
|
||||
return request({
|
||||
url: `/dictionary/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
* @param {Number} id - 字典ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteDictionary(id) {
|
||||
return request({
|
||||
url: `/dictionary/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取工厂列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回工厂列表的Promise
|
||||
*/
|
||||
export function getFactoryList(params) {
|
||||
return request({
|
||||
url: '/factory/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简化工厂列表(用于下拉选择)
|
||||
* @returns {Promise} - 返回简化工厂列表的Promise
|
||||
*/
|
||||
export function getFactoryListSimple() {
|
||||
return request({
|
||||
url: '/factory/simple/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工厂详情
|
||||
* @param {Number} id - 工厂ID
|
||||
* @returns {Promise} - 返回工厂详情的Promise
|
||||
*/
|
||||
export function getFactoryDetail(id) {
|
||||
return request({
|
||||
url: `/factory/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工厂
|
||||
* @param {Object} data - 工厂数据
|
||||
* @returns {Promise} - 返回创建的工厂信息的Promise
|
||||
*/
|
||||
export function createFactory(data) {
|
||||
return request({
|
||||
url: '/factory/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工厂信息
|
||||
* @param {Number} id - 工厂ID
|
||||
* @param {Object} data - 更新的工厂数据
|
||||
* @returns {Promise} - 返回更新后的工厂信息的Promise
|
||||
*/
|
||||
export function updateFactory(id, data) {
|
||||
return request({
|
||||
url: `/factory/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除工厂
|
||||
* @param {Number} id - 工厂ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteFactory(id) {
|
||||
return request({
|
||||
url: `/factory/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取材料列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回材料列表的Promise
|
||||
*/
|
||||
export function getMaterialList(params) {
|
||||
return request({
|
||||
url: '/material/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材料详情
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回材料详情的Promise
|
||||
*/
|
||||
export function getMaterialDetail(id) {
|
||||
return request({
|
||||
url: `/material/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建材料
|
||||
* @param {Object} data - 材料数据
|
||||
* @returns {Promise} - 返回创建的材料信息的Promise
|
||||
*/
|
||||
export function createMaterial(data) {
|
||||
return request({
|
||||
url: '/material/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新材料信息
|
||||
* @param {Number} id - 材料ID
|
||||
* @param {Object} data - 更新的材料数据
|
||||
* @returns {Promise} - 返回更新后的材料信息的Promise
|
||||
*/
|
||||
export function updateMaterial(id, data) {
|
||||
return request({
|
||||
url: `/material/${id}/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除材料
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回删除结果的Promise
|
||||
*/
|
||||
export function deleteMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交材料审核
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回提交结果的Promise
|
||||
*/
|
||||
export function submitMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/submit/`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过材料
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回审核结果的Promise
|
||||
*/
|
||||
export function approveMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/approve/`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核拒绝材料
|
||||
* @param {Number} id - 材料ID
|
||||
* @returns {Promise} - 返回审核结果的Promise
|
||||
*/
|
||||
export function rejectMaterial(id) {
|
||||
return request({
|
||||
url: `/material/${id}/reject/`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取数据总览统计
|
||||
* @returns {Promise} - 返回数据总览统计的Promise
|
||||
*/
|
||||
export function getOverviewStatistics() {
|
||||
return request({
|
||||
url: '/statistics/overview/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材料库统计
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} - 返回材料库统计的Promise
|
||||
*/
|
||||
export function getMaterialStatistics(params) {
|
||||
return request({
|
||||
url: '/statistics/materials/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工厂库统计
|
||||
* @returns {Promise} - 返回工厂库统计的Promise
|
||||
*/
|
||||
export function getFactoryStatistics() {
|
||||
return request({
|
||||
url: '/statistics/factories/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/* 全局样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Element Plus 样式覆盖 */
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.el-card__header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
|
||||
th {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
.el-pagination__sizes {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 8px;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-divider__text {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑工厂' : '新增工厂'"
|
||||
:visible="visible"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="经销商名称" prop="dealer_name">
|
||||
<el-input v-model="form.dealer_name" placeholder="请输入经销商名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="产品分类" prop="product_category">
|
||||
<el-input v-model="form.product_category" placeholder="请输入产品分类" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="工厂全称" prop="factory_name">
|
||||
<el-input v-model="form.factory_name" placeholder="请输入工厂全称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="工厂简称" prop="factory_short_name">
|
||||
<el-input v-model="form.factory_short_name" placeholder="请输入工厂简称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="省份" prop="province">
|
||||
<el-input v-model="form.province" placeholder="请输入省份" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="城市" prop="city">
|
||||
<el-input v-model="form.city" placeholder="请输入城市" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="区" prop="district">
|
||||
<el-input v-model="form.district" placeholder="请输入区" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="详细地址" prop="address">
|
||||
<el-input
|
||||
v-model="form.address"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入详细地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="官网链接" prop="website">
|
||||
<el-input v-model="form.website" placeholder="请输入官网链接" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { createFactory, updateFactory } from '@/api/factory'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
factory: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const isEdit = computed(() => !!props.factory)
|
||||
|
||||
const form = reactive({
|
||||
dealer_name: '',
|
||||
product_category: '',
|
||||
factory_name: '',
|
||||
factory_short_name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: '',
|
||||
website: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
dealer_name: [
|
||||
{ required: true, message: '请输入经销商名称', trigger: 'blur' }
|
||||
],
|
||||
factory_name: [
|
||||
{ required: true, message: '请输入工厂全称', trigger: 'blur' }
|
||||
],
|
||||
factory_short_name: [
|
||||
{ required: true, message: '请输入工厂简称', trigger: 'blur' }
|
||||
],
|
||||
province: [
|
||||
{ required: true, message: '请输入省份', trigger: 'blur' }
|
||||
],
|
||||
city: [
|
||||
{ required: true, message: '请输入城市', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听factory变化,填充表单
|
||||
watch(() => props.factory, (newVal) => {
|
||||
if (newVal) {
|
||||
Object.assign(form, newVal)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
dealer_name: '',
|
||||
product_category: '',
|
||||
factory_name: '',
|
||||
factory_short_name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: '',
|
||||
website: ''
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateFactory(props.factory.id, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createFactory(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑材料' : '新增材料'"
|
||||
:visible="visible"
|
||||
width="80%"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="材料名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入材料名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="专业类别" prop="major_category">
|
||||
<el-select v-model="form.major_category" placeholder="请选择专业类别" style="width: 100%">
|
||||
<el-option label="建筑" value="architecture" />
|
||||
<el-option label="景观" value="landscape" />
|
||||
<el-option label="设备" value="equipment" />
|
||||
<el-option label="装修" value="decoration" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="材料分类" prop="material_category">
|
||||
<el-input v-model="form.material_category" placeholder="请输入材料分类" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="材料子类" prop="material_subcategory">
|
||||
<el-input v-model="form.material_subcategory" placeholder="请输入材料子类" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规格型号" prop="spec">
|
||||
<el-input v-model="form.spec" placeholder="请输入规格型号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="符合标准" prop="standard">
|
||||
<el-input v-model="form.standard" placeholder="请输入符合标准" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">应用场景</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="应用场景" prop="application_scene">
|
||||
<el-select v-model="form.application_scene" placeholder="请选择应用场景" clearable style="width: 100%">
|
||||
<el-option label="府系" value="fu" />
|
||||
<el-option label="境系" value="jing" />
|
||||
<el-option label="城系" value="cheng" />
|
||||
<el-option label="住系" value="zhu" />
|
||||
<el-option label="保障房" value="affordable" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="应用场景说明" prop="application_desc">
|
||||
<el-input
|
||||
v-model="form.application_desc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入应用场景说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">替代材料</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="替代材料类型" prop="replace_type">
|
||||
<el-select v-model="form.replace_type" placeholder="请选择替代材料类型" clearable style="width: 100%">
|
||||
<el-option label="平替" value="alternative" />
|
||||
<el-option label="新研发" value="new_development" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="竞争优势" prop="advantage">
|
||||
<el-select v-model="form.advantage" placeholder="请选择竞争优势" clearable style="width: 100%">
|
||||
<el-option label="品质" value="quality" />
|
||||
<el-option label="成本" value="cost" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="优势说明" prop="advantage_desc">
|
||||
<el-input
|
||||
v-model="form.advantage_desc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入优势说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="成本对比百分数" prop="cost_compare">
|
||||
<el-input-number
|
||||
v-model="form.cost_compare"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="成本说明" prop="cost_desc">
|
||||
<el-input
|
||||
v-model="form.cost_desc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入成本说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">案例与宣传</el-divider>
|
||||
|
||||
<el-form-item label="案例" prop="cases">
|
||||
<el-input
|
||||
v-model="form.cases"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入案例"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="宣传页图片" prop="brochure">
|
||||
<el-upload
|
||||
class="brochure-uploader"
|
||||
action="/api/upload/"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<img v-if="form.brochure_url" :src="form.brochure_url" class="brochure" />
|
||||
<el-icon v-else class="brochure-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">星级评价</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="质量提升等级" prop="quality_level">
|
||||
<el-rate v-model="form.quality_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="耐久可靠等级" prop="durability_level">
|
||||
<el-rate v-model="form.durability_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="环保健康等级" prop="eco_level">
|
||||
<el-rate v-model="form.eco_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="循环低碳等级" prop="carbon_level">
|
||||
<el-rate v-model="form.carbon_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="总评分等级" prop="score_level">
|
||||
<el-rate v-model="form.score_level" :max="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">施工与限制</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="连接方式" prop="connection_method">
|
||||
<el-input v-model="form.connection_method" placeholder="请输入连接方式" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="施工工艺" prop="construction_method">
|
||||
<el-input v-model="form.construction_method" placeholder="请输入施工工艺" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="限制条件" prop="limit_condition">
|
||||
<el-input
|
||||
v-model="form.limit_condition"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入限制条件"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { createMaterial, updateMaterial } from '@/api/material'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
material: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const userStore = useUserStore()
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const isEdit = computed(() => !!props.material)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
major_category: '',
|
||||
material_category: '',
|
||||
material_subcategory: '',
|
||||
spec: '',
|
||||
standard: '',
|
||||
application_scene: '',
|
||||
application_desc: '',
|
||||
replace_type: '',
|
||||
advantage: '',
|
||||
advantage_desc: '',
|
||||
cost_compare: null,
|
||||
cost_desc: '',
|
||||
cases: '',
|
||||
brochure: null,
|
||||
brochure_url: '',
|
||||
quality_level: null,
|
||||
durability_level: null,
|
||||
eco_level: null,
|
||||
carbon_level: null,
|
||||
score_level: null,
|
||||
connection_method: '',
|
||||
construction_method: '',
|
||||
limit_condition: '',
|
||||
factory: userStore.factoryId
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入材料名称', trigger: 'blur' }
|
||||
],
|
||||
major_category: [
|
||||
{ required: true, message: '请选择专业类别', trigger: 'change' }
|
||||
],
|
||||
material_category: [
|
||||
{ required: true, message: '请输入材料分类', trigger: 'blur' }
|
||||
],
|
||||
material_subcategory: [
|
||||
{ required: true, message: '请输入材料子类', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听material变化,填充表单
|
||||
watch(() => props.material, (newVal) => {
|
||||
if (newVal) {
|
||||
Object.assign(form, newVal)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
major_category: '',
|
||||
material_category: '',
|
||||
material_subcategory: '',
|
||||
spec: '',
|
||||
standard: '',
|
||||
application_scene: '',
|
||||
application_desc: '',
|
||||
replace_type: '',
|
||||
advantage: '',
|
||||
advantage_desc: '',
|
||||
cost_compare: null,
|
||||
cost_desc: '',
|
||||
cases: '',
|
||||
brochure: null,
|
||||
brochure_url: '',
|
||||
quality_level: null,
|
||||
durability_level: null,
|
||||
eco_level: null,
|
||||
carbon_level: null,
|
||||
score_level: null,
|
||||
connection_method: '',
|
||||
construction_method: '',
|
||||
limit_condition: '',
|
||||
factory: userStore.factoryId
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const formData = { ...form }
|
||||
|
||||
// 处理星级评价,如果没有选择则设为null
|
||||
['quality_level', 'durability_level', 'eco_level', 'carbon_level', 'score_level'].forEach(field => {
|
||||
if (formData[field] === 0) {
|
||||
formData[field] = null
|
||||
}
|
||||
})
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateMaterial(props.material.id, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createMaterial(formData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传前校验
|
||||
const beforeUpload = (file) => {
|
||||
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isJPG) {
|
||||
ElMessage.error('上传图片只能是 JPG/PNG 格式!')
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('上传图片大小不能超过 2MB!')
|
||||
}
|
||||
return isJPG && isLt2M
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
const handleUploadSuccess = (response) => {
|
||||
form.brochure = response.url
|
||||
form.brochure_url = response.url
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.brochure-uploader {
|
||||
.brochure {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.brochure-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
line-height: 178px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑用户' : '新增用户'"
|
||||
:visible="visible"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password" v-if="!isEdit">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="password_confirm" v-if="!isEdit">
|
||||
<el-input v-model="form.password_confirm" type="password" placeholder="请再次输入密码" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="姓" prop="first_name">
|
||||
<el-input v-model="form.first_name" placeholder="请输入姓" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="名" prop="last_name">
|
||||
<el-input v-model="form.last_name" placeholder="请输入名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="请选择角色" style="width: 100%">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通账号" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属工厂" prop="factory" v-if="form.role === 'user'">
|
||||
<el-select v-model="form.factory" placeholder="请选择所属工厂" style="width: 100%" filterable>
|
||||
<el-option
|
||||
v-for="factory in factoryList"
|
||||
:key="factory.id"
|
||||
:label="factory.factory_name"
|
||||
:value="factory.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { createUser, updateUser } from '@/api/auth'
|
||||
import { getFactoryListSimple } from '@/api/factory'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
const factoryList = ref([])
|
||||
|
||||
const isEdit = computed(() => !!props.user)
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role: 'user',
|
||||
factory: null,
|
||||
phone: '',
|
||||
is_active: true
|
||||
})
|
||||
|
||||
const validatePasswordConfirm = (rule, value, callback) => {
|
||||
if (value !== form.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password_confirm: [
|
||||
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||
{ validator: validatePasswordConfirm, trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
factory: [
|
||||
{ required: true, message: '请选择所属工厂', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听user变化,填充表单
|
||||
watch(() => props.user, (newVal) => {
|
||||
if (newVal) {
|
||||
Object.assign(form, newVal)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听角色变化,清空工厂选择
|
||||
watch(() => form.role, (newVal) => {
|
||||
if (newVal === 'admin') {
|
||||
form.factory = null
|
||||
}
|
||||
})
|
||||
|
||||
// 加载工厂列表
|
||||
const loadFactoryList = async () => {
|
||||
try {
|
||||
factoryList.value = await getFactoryListSimple()
|
||||
} catch (error) {
|
||||
console.error('加载工厂列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
username: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role: 'user',
|
||||
factory: null,
|
||||
phone: '',
|
||||
is_active: true
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const formData = { ...form }
|
||||
|
||||
// 移除不需要提交的字段
|
||||
if (isEdit.value) {
|
||||
delete formData.password
|
||||
delete formData.password_confirm
|
||||
} else {
|
||||
delete formData.password_confirm
|
||||
}
|
||||
|
||||
// 如果是管理员,清空工厂字段
|
||||
if (formData.role === 'admin') {
|
||||
formData.factory = null
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateUser(props.user.id, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createUser(formData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFactoryList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<el-container class="main-layout">
|
||||
<el-aside width="200px">
|
||||
<div class="logo">
|
||||
<h2>新材料数据库</h2>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:router="true"
|
||||
class="sidebar-menu"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<el-menu-item index="/dashboard" v-if="isAdmin">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>数据总览</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/materials">
|
||||
<el-icon><Box /></el-icon>
|
||||
<span>材料库</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/factories">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span>工厂库</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/users" v-if="isAdmin">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/dictionaries" v-if="isAdmin">
|
||||
<el-icon><Notebook /></el-icon>
|
||||
<span>字典管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<div class="breadcrumb">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-if="currentRoute.meta.title">
|
||||
{{ currentRoute.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userStore.userInfo?.username }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
const currentRoute = computed(() => route)
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
const handleCommand = (command) => {
|
||||
if (command === 'logout') {
|
||||
userStore.logout()
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-layout {
|
||||
height: 100vh;
|
||||
|
||||
.el-aside {
|
||||
background-color: #304156;
|
||||
color: #fff;
|
||||
transition: width 0.3s;
|
||||
|
||||
.logo {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #1f2d3d;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: calc(100vh - 50px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.user-info {
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/styles/main.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { locale: zhCn })
|
||||
|
||||
app.mount('#app')
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/Login.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layout/MainLayout.vue'),
|
||||
redirect: '/dashboard',
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { title: '数据总览', requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'materials',
|
||||
name: 'Materials',
|
||||
component: () => import('@/views/Materials.vue'),
|
||||
meta: { title: '材料库', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'materials/:id',
|
||||
name: 'MaterialDetail',
|
||||
component: () => import('@/views/MaterialDetail.vue'),
|
||||
meta: { title: '材料详情', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'factories',
|
||||
name: 'Factories',
|
||||
component: () => import('@/views/Factories.vue'),
|
||||
meta: { title: '工厂库', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'factories/:id',
|
||||
name: 'FactoryDetail',
|
||||
component: () => import('@/views/FactoryDetail.vue'),
|
||||
meta: { title: '工厂详情', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'Users',
|
||||
component: () => import('@/views/Users.vue'),
|
||||
meta: { title: '用户管理', requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'dictionaries',
|
||||
name: 'Dictionaries',
|
||||
component: () => import('@/views/Dictionaries.vue'),
|
||||
meta: { title: '字典管理', requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
|
||||
ElMessage.warning('请先登录')
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要管理员权限
|
||||
if (to.meta.requiresAdmin && !userStore.isAdmin) {
|
||||
ElMessage.warning('需要管理员权限')
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { login as loginApi, getCurrentUser as getCurrentUserApi } from '@/api/auth'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('token') || '',
|
||||
refreshToken: localStorage.getItem('refreshToken') || '',
|
||||
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'),
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token,
|
||||
isAdmin: (state) => state.userInfo && state.userInfo.role === 'admin',
|
||||
factoryId: (state) => state.userInfo ? state.userInfo.factory : null,
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
async login(credentials) {
|
||||
try {
|
||||
const response = await loginApi(credentials)
|
||||
const { access, refresh, user } = response
|
||||
|
||||
this.token = access
|
||||
this.refreshToken = refresh
|
||||
this.userInfo = user
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('token', access)
|
||||
localStorage.setItem('refreshToken', refresh)
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
return user
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '登录失败')
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
this.token = ''
|
||||
this.refreshToken = ''
|
||||
this.userInfo = null
|
||||
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('refreshToken')
|
||||
localStorage.removeItem('userInfo')
|
||||
},
|
||||
|
||||
// 检查认证状态
|
||||
async checkAuth() {
|
||||
if (this.token) {
|
||||
try {
|
||||
const user = await getCurrentUserApi()
|
||||
this.userInfo = user
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
} catch (error) {
|
||||
this.logout()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUserInfo(userInfo) {
|
||||
this.userInfo = { ...this.userInfo, ...userInfo }
|
||||
localStorage.setItem('userInfo', JSON.stringify(this.userInfo))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '@/router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
const userStore = useUserStore()
|
||||
if (userStore.token) {
|
||||
config.headers['Authorization'] = `Bearer ${userStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 如果响应的状态码不是200,则判断为错误
|
||||
if (response.status !== 200) {
|
||||
ElMessage({
|
||||
message: res.message || '请求失败',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('响应错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
if (status === 401) {
|
||||
// 未授权,跳转到登录页
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
} else if (status === 403) {
|
||||
ElMessage.error('没有权限访问该资源')
|
||||
} else if (status === 404) {
|
||||
ElMessage.error('请求的资源不存在')
|
||||
} else if (status === 500) {
|
||||
ElMessage.error('服务器错误')
|
||||
} else {
|
||||
ElMessage.error(data.detail || '请求失败')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="20">
|
||||
<!-- 统计卡片 -->
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #409EFF;">
|
||||
<el-icon :size="30"><Box /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ overviewData.total_materials || 0 }}</div>
|
||||
<div class="stat-label">材料总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #67C23A;">
|
||||
<el-icon :size="30"><Grid /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ overviewData.total_material_categories || 0 }}</div>
|
||||
<div class="stat-label">材料种类</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #E6A23C;">
|
||||
<el-icon :size="30"><OfficeBuilding /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ overviewData.total_brands || 0 }}</div>
|
||||
<div class="stat-label">品牌数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" style="background-color: #F56C6C;">
|
||||
<el-icon :size="30"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ overviewData.cases_list?.length || 0 }}</div>
|
||||
<div class="stat-label">应用案例</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 按专业类别的材料数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按专业类别的材料数量分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="majorCategoryChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 按材料子类的材料数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按材料子类的材料数量分布(TOP10)</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="materialSubcategoryChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 按所属品牌的材料数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按所属品牌的材料数量分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="brandChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 按地区的工厂数量分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按地区的工厂数量分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="regionChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 应用案例列表 -->
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>应用案例列表</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="overviewData.cases_list || []" stripe>
|
||||
<el-table-column prop="name" label="材料名称" />
|
||||
<el-table-column prop="factory__factory_name" label="所属品牌" />
|
||||
<el-table-column prop="cases" label="案例说明" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getOverviewStatistics } from '@/api/statistics'
|
||||
|
||||
// 图表实例
|
||||
let majorCategoryChartInstance = null
|
||||
let materialSubcategoryChartInstance = null
|
||||
let brandChartInstance = null
|
||||
let regionChartInstance = null
|
||||
|
||||
// 图表容器引用
|
||||
const majorCategoryChart = ref(null)
|
||||
const materialSubcategoryChart = ref(null)
|
||||
const brandChart = ref(null)
|
||||
const regionChart = ref(null)
|
||||
|
||||
// 数据总览数据
|
||||
const overviewData = ref({
|
||||
total_materials: 0,
|
||||
total_material_categories: 0,
|
||||
total_brands: 0,
|
||||
major_category_stats: [],
|
||||
material_subcategory_stats: [],
|
||||
brand_stats: [],
|
||||
region_stats: [],
|
||||
cases_list: []
|
||||
})
|
||||
|
||||
// 自动刷新定时器
|
||||
let refreshTimer = null
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const data = await getOverviewStatistics()
|
||||
overviewData.value = data
|
||||
|
||||
// 更新图表
|
||||
updateCharts(data)
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
majorCategoryChartInstance = echarts.init(majorCategoryChart.value)
|
||||
materialSubcategoryChartInstance = echarts.init(materialSubcategoryChart.value)
|
||||
brandChartInstance = echarts.init(brandChart.value)
|
||||
regionChartInstance = echarts.init(regionChart.value)
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = (data) => {
|
||||
// 按专业类别的材料数量分布 - 饼图
|
||||
majorCategoryChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '专业类别',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: data.major_category_stats.map(item => ({
|
||||
value: item.count,
|
||||
name: item.major_category
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 按材料子类的材料数量分布 - 柱状图
|
||||
materialSubcategoryChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: data.material_subcategory_stats.map(item => item.material_subcategory)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '材料数量',
|
||||
type: 'bar',
|
||||
data: data.material_subcategory_stats.map(item => item.count)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 按所属品牌的材料数量分布 - 柱状图
|
||||
brandChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.brand_stats.map(item => item.factory__factory_name),
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '材料数量',
|
||||
type: 'bar',
|
||||
data: data.brand_stats.map(item => item.count)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 按地区的工厂数量分布 - 柱状图
|
||||
regionChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.region_stats.map(item => `${item.province} ${item.city}`),
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '工厂数量',
|
||||
type: 'bar',
|
||||
data: data.region_stats.map(item => item.count)
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// 窗口大小变化时重新调整图表大小
|
||||
const handleResize = () => {
|
||||
majorCategoryChartInstance && majorCategoryChartInstance.resize()
|
||||
materialSubcategoryChartInstance && materialSubcategoryChartInstance.resize()
|
||||
brandChartInstance && brandChartInstance.resize()
|
||||
regionChartInstance && regionChartInstance.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化图表
|
||||
initCharts()
|
||||
|
||||
// 加载数据
|
||||
loadData()
|
||||
|
||||
// 设置自动刷新,每10秒刷新一次
|
||||
refreshTimer = setInterval(loadData, 10000)
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清除定时器
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
|
||||
// 销毁图表实例
|
||||
majorCategoryChartInstance && majorCategoryChartInstance.dispose()
|
||||
materialSubcategoryChartInstance && materialSubcategoryChartInstance.dispose()
|
||||
brandChartInstance && brandChartInstance.dispose()
|
||||
regionChartInstance && regionChartInstance.dispose()
|
||||
|
||||
// 移除窗口大小变化监听
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
.stat-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-top: 20px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<div class="dictionaries">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>字典管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增字典</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="字典类型">
|
||||
<el-input v-model="searchForm.type" placeholder="请输入字典类型" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字典名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入字典名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 字典表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="type" label="字典类型" />
|
||||
<el-table-column prop="name" label="字典名称" />
|
||||
<el-table-column prop="value" label="字典值" />
|
||||
<el-table-column prop="created_at" label="创建时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 字典表单对话框 -->
|
||||
<DictionaryForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:dictionary="currentDictionary"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getDictionaryList, deleteDictionary } from '@/api/dictionary'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import DictionaryForm from '@/components/DictionaryForm.vue'
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
type: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentDictionary = ref(null)
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getDictionaryList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
type: '',
|
||||
name: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentDictionary.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentDictionary.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该字典吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteDictionary(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dictionaries {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
<template>
|
||||
<div class="factories">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>工厂库</span>
|
||||
<el-button type="primary" @click="handleAdd" v-if="isAdmin">新增工厂</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="工厂名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入工厂名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="省份">
|
||||
<el-input v-model="searchForm.province" placeholder="请输入省份" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="城市">
|
||||
<el-input v-model="searchForm.city" placeholder="请输入城市" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 工厂表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="factory_name" label="工厂全称" />
|
||||
<el-table-column prop="factory_short_name" label="工厂简称" />
|
||||
<el-table-column prop="dealer_name" label="经销商名称" />
|
||||
<el-table-column prop="province" label="省份" />
|
||||
<el-table-column prop="city" label="城市" />
|
||||
<el-table-column prop="material_count" label="材料数量" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)" v-if="canEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)" v-if="canDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 工厂表单对话框 -->
|
||||
<FactoryForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:factory="currentFactory"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getFactoryList, deleteFactory } from '@/api/factory'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import FactoryForm from '@/components/FactoryForm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
province: '',
|
||||
city: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentFactory = ref(null)
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === row.id
|
||||
}
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === row.id
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getFactoryList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
province: '',
|
||||
city: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentFactory.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = (row) => {
|
||||
router.push(`/factories/${row.id}`)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentFactory.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该工厂吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteFactory(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.factories {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<div class="factory-detail">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-button link @click="handleBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button v-if="canEdit" type="primary" @click="handleEdit">编辑</el-button>
|
||||
<el-button v-if="canDelete" type="danger" @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="经销商名称">{{ factory.dealer_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品分类">{{ factory.product_category || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工厂全称">{{ factory.factory_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工厂简称">{{ factory.factory_short_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="省份">{{ factory.province }}</el-descriptions-item>
|
||||
<el-descriptions-item label="城市">{{ factory.city }}</el-descriptions-item>
|
||||
<el-descriptions-item label="区">{{ factory.district || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="官网链接">
|
||||
<a v-if="factory.website" :href="factory.website" target="_blank">{{ factory.website }}</a>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider>详细地址</el-divider>
|
||||
<div class="content-text">{{ factory.address || '暂无' }}</div>
|
||||
|
||||
<el-divider>其他信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="材料数量">{{ factory.material_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(factory.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(factory.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider>材料列表</el-divider>
|
||||
<el-table :data="factory.materials || []" stripe>
|
||||
<el-table-column prop="name" label="材料名称" />
|
||||
<el-table-column prop="major_category_display" label="专业类别" />
|
||||
<el-table-column prop="material_category" label="材料分类" />
|
||||
<el-table-column prop="material_subcategory" label="材料子类" />
|
||||
<el-table-column prop="status_display" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ row.status_display }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewMaterial(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getFactoryDetail, deleteFactory } from '@/api/factory'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const factory = ref({})
|
||||
const loading = ref(false)
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === factory.value.id
|
||||
})
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return userStore.factoryId === factory.value.id
|
||||
})
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
draft: 'info',
|
||||
pending: 'warning',
|
||||
approved: 'success'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
factory.value = await getFactoryDetail(id)
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = () => {
|
||||
router.push(`/factories/${factory.value.id}/edit`)
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该工厂吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteFactory(factory.value.id)
|
||||
ElMessage.success('删除成功')
|
||||
router.push('/factories')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查看材料
|
||||
const handleViewMaterial = (row) => {
|
||||
router.push(`/materials/${row.id}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.factory-detail {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
padding: 10px 0;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>新材料数据库管理系统</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
label-width="80px"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="User"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="Lock"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
style="width: 100%"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loginFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
await loginFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
await userStore.login(loginForm)
|
||||
|
||||
// 根据用户角色跳转到不同页面
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
router.push('/materials')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
<template>
|
||||
<div class="material-detail">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-button link @click="handleBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button v-if="canEdit" type="primary" @click="handleEdit">编辑</el-button>
|
||||
<el-button v-if="canSubmit" type="warning" @click="handleSubmit">提交审核</el-button>
|
||||
<el-button v-if="canApprove" type="success" @click="handleApprove">审核通过</el-button>
|
||||
<el-button v-if="canReject" type="danger" @click="handleReject">审核拒绝</el-button>
|
||||
<el-button v-if="canDelete" type="danger" @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="材料名称">{{ material.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="专业类别">{{ material.major_category_display }}</el-descriptions-item>
|
||||
<el-descriptions-item label="材料分类">{{ material.material_category }}</el-descriptions-item>
|
||||
<el-descriptions-item label="材料子类">{{ material.material_subcategory }}</el-descriptions-item>
|
||||
<el-descriptions-item label="规格型号">{{ material.spec || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="符合标准">{{ material.standard || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="应用场景">{{ material.application_scene_display || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属品牌">{{ material.factory_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusType(material.status)">
|
||||
{{ material.status_display }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider>应用场景说明</el-divider>
|
||||
<div class="content-text">{{ material.application_desc || '暂无' }}</div>
|
||||
|
||||
<el-divider>替代材料信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="替代材料类型">{{ material.replace_type_display || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="竞争优势">{{ material.advantage_display || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="content-text">{{ material.advantage_desc || '暂无' }}</div>
|
||||
|
||||
<el-divider>成本信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="成本对比百分数">
|
||||
{{ material.cost_compare ? `${material.cost_compare}%` : '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="content-text">{{ material.cost_desc || '暂无' }}</div>
|
||||
|
||||
<el-divider>案例</el-divider>
|
||||
<div class="content-text">{{ material.cases || '暂无' }}</div>
|
||||
|
||||
<el-divider>宣传页图片</el-divider>
|
||||
<div v-if="material.brochure_url" class="brochure-container">
|
||||
<el-image
|
||||
:src="material.brochure_url"
|
||||
fit="contain"
|
||||
:preview-src-list="[material.brochure_url]"
|
||||
style="max-width: 100%; max-height: 500px;"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="content-text">暂无</div>
|
||||
|
||||
<el-divider>星级评价</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">质量提升等级:</span>
|
||||
<el-rate v-model="material.quality_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">耐久可靠等级:</span>
|
||||
<el-rate v-model="material.durability_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">环保健康等级:</span>
|
||||
<el-rate v-model="material.eco_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">循环低碳等级:</span>
|
||||
<el-rate v-model="material.carbon_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="star-item">
|
||||
<span class="star-label">总评分等级:</span>
|
||||
<el-rate v-model="material.score_level" disabled :max="3" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider>施工与限制</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="连接方式">{{ material.connection_method || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="施工工艺">{{ material.construction_method || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="content-text">{{ material.limit_condition || '暂无' }}</div>
|
||||
|
||||
<el-divider>其他信息</el-divider>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(material.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(material.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getMaterialDetail, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial } from '@/api/material'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const material = ref({})
|
||||
const loading = ref(false)
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return material.value.status === 'draft' && userStore.factoryId === material.value.factory
|
||||
})
|
||||
|
||||
// 判断是否可以提交
|
||||
const canSubmit = computed(() => {
|
||||
return material.value.status === 'draft' && (isAdmin.value || userStore.factoryId === material.value.factory)
|
||||
})
|
||||
|
||||
// 判断是否可以审核
|
||||
const canApprove = computed(() => {
|
||||
return isAdmin.value && material.value.status === 'pending'
|
||||
})
|
||||
|
||||
// 判断是否可以拒绝
|
||||
const canReject = computed(() => {
|
||||
return isAdmin.value && material.value.status === 'pending'
|
||||
})
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = computed(() => {
|
||||
if (isAdmin.value) return true
|
||||
return material.value.status === 'draft' && userStore.factoryId === material.value.factory
|
||||
})
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
draft: 'info',
|
||||
pending: 'warning',
|
||||
approved: 'success'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
material.value = await getMaterialDetail(id)
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = () => {
|
||||
router.push(`/materials/${material.value.id}/edit`)
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该材料进行审核吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await submitMaterial(material.value.id)
|
||||
ElMessage.success('提交成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
const handleApprove = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认审核通过该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await approveMaterial(material.value.id)
|
||||
ElMessage.success('审核通过')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('审核失败:', error)
|
||||
ElMessage.error('审核失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核拒绝
|
||||
const handleReject = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认拒绝该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await rejectMaterial(material.value.id)
|
||||
ElMessage.success('已拒绝')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteMaterial(material.value.id)
|
||||
ElMessage.success('删除成功')
|
||||
router.push('/materials')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.material-detail {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
padding: 10px 0;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.brochure-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.star-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.star-label {
|
||||
margin-right: 10px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,338 @@
|
|||
<template>
|
||||
<div class="materials">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>材料库</span>
|
||||
<el-button type="primary" @click="handleAdd">新增材料</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="材料名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入材料名称" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="专业类别">
|
||||
<el-select v-model="searchForm.major_category" placeholder="请选择专业类别" clearable>
|
||||
<el-option label="建筑" value="architecture" />
|
||||
<el-option label="景观" value="landscape" />
|
||||
<el-option label="设备" value="equipment" />
|
||||
<el-option label="装修" value="decoration" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="创建中" value="draft" />
|
||||
<el-option label="待审核" value="pending" />
|
||||
<el-option label="已审核" value="approved" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 材料表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="name" label="材料名称" />
|
||||
<el-table-column prop="major_category_display" label="专业类别" />
|
||||
<el-table-column prop="material_category" label="材料分类" />
|
||||
<el-table-column prop="material_subcategory" label="材料子类" />
|
||||
<el-table-column prop="factory_name" label="所属品牌" />
|
||||
<el-table-column prop="status_display" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ row.status_display }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)" v-if="canEdit(row)">编辑</el-button>
|
||||
<el-button link type="primary" @click="handleSubmit(row)" v-if="canSubmit(row)">提交</el-button>
|
||||
<el-button link type="success" @click="handleApprove(row)" v-if="canApprove(row)">通过</el-button>
|
||||
<el-button link type="danger" @click="handleReject(row)" v-if="canReject(row)">拒绝</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)" v-if="canDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 材料表单对话框 -->
|
||||
<MaterialForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:material="currentMaterial"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getMaterialList, deleteMaterial, submitMaterial, approveMaterial, rejectMaterial } from '@/api/material'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MaterialForm from '@/components/MaterialForm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
major_category: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentMaterial = ref(null)
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
draft: 'info',
|
||||
pending: 'warning',
|
||||
approved: 'success'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 判断是否可以编辑
|
||||
const canEdit = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return row.status === 'draft' && userStore.factoryId === row.factory
|
||||
}
|
||||
|
||||
// 判断是否可以提交
|
||||
const canSubmit = (row) => {
|
||||
return row.status === 'draft' && (isAdmin.value || userStore.factoryId === row.factory)
|
||||
}
|
||||
|
||||
// 判断是否可以审核
|
||||
const canApprove = (row) => {
|
||||
return isAdmin.value && row.status === 'pending'
|
||||
}
|
||||
|
||||
// 判断是否可以拒绝
|
||||
const canReject = (row) => {
|
||||
return isAdmin.value && row.status === 'pending'
|
||||
}
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = (row) => {
|
||||
if (isAdmin.value) return true
|
||||
return row.status === 'draft' && userStore.factoryId === row.factory
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getMaterialList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
major_category: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentMaterial.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = (row) => {
|
||||
router.push(`/materials/${row.id}`)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentMaterial.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该材料进行审核吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await submitMaterial(row.id)
|
||||
ElMessage.success('提交成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
const handleApprove = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认审核通过该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await approveMaterial(row.id)
|
||||
ElMessage.success('审核通过')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('审核失败:', error)
|
||||
ElMessage.error('审核失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审核拒绝
|
||||
const handleReject = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认拒绝该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await rejectMaterial(row.id)
|
||||
ElMessage.success('已拒绝')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该材料吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteMaterial(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.materials {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
<template>
|
||||
<div class="users">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通账号" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="first_name" label="姓" />
|
||||
<el-table-column prop="last_name" label="名" />
|
||||
<el-table-column prop="role_display" label="角色">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.role === 'admin' ? 'danger' : 'primary'">
|
||||
{{ row.role === 'admin' ? '管理员' : '普通账号' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="factory_name" label="所属工厂" />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column prop="is_active" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_active ? 'success' : 'info'">
|
||||
{{ row.is_active ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)" v-if="canDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户表单对话框 -->
|
||||
<UserForm
|
||||
v-if="showForm"
|
||||
:visible="showForm"
|
||||
:user="currentUser"
|
||||
@close="showForm = false"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getUserList, deleteUser } from '@/api/auth'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import UserForm from '@/components/UserForm.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => userStore.userInfo?.id)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
role: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单对话框
|
||||
const showForm = ref(false)
|
||||
const currentUser = ref(null)
|
||||
|
||||
// 判断是否可以删除
|
||||
const canDelete = (row) => {
|
||||
// 不能删除自己
|
||||
return row.id !== currentUserId.value
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...searchForm
|
||||
}
|
||||
const data = await getUserList(params)
|
||||
tableData.value = data.results || data
|
||||
pagination.total = data.count || data.length
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
username: '',
|
||||
role: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentUser.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 查看
|
||||
const handleView = (row) => {
|
||||
router.push(`/users/${row.id}`)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentUser.value = { ...row }
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该用户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteUser(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.users {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue