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