From d2ab8d12f6a6db261a6c0d1b112b6801e0daf422 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Sun, 21 Sep 2025 13:47:19 +0800 Subject: [PATCH] feat: item image now is required --- src/backend/items/admin.py | 48 +- src/backend/items/management/__init__.py | 1 + .../items/management/commands/__init__.py | 0 .../commands/create_default_categories.py | 143 ++++++ src/backend/items/models.py | 87 ++++ src/backend/items/serializers.py | 63 ++- src/backend/items/urls.py | 2 + src/backend/items/views.py | 149 +++++- src/fronted/src/router/index.js | 6 + src/fronted/src/services/api.js | 64 ++- src/fronted/src/views/ItemCreate.vue | 290 ++++++++++++ src/fronted/src/views/ItemDetail.vue | 297 +++++++++++- src/fronted/src/views/ItemList.vue | 445 +++++++++--------- 13 files changed, 1330 insertions(+), 265 deletions(-) create mode 100644 src/backend/items/management/__init__.py create mode 100644 src/backend/items/management/commands/__init__.py create mode 100644 src/backend/items/management/commands/create_default_categories.py create mode 100644 src/fronted/src/views/ItemCreate.vue diff --git a/src/backend/items/admin.py b/src/backend/items/admin.py index b4a05c6..f6152ad 100644 --- a/src/backend/items/admin.py +++ b/src/backend/items/admin.py @@ -1,20 +1,35 @@ from django.contrib import admin -from .models import Item, ItemUsage, Category +from .models import Item, ItemUsage, Category, ItemImage, UsageImage + + +class ItemImageInline(admin.TabularInline): + """物品图片内联编辑""" + model = ItemImage + extra = 1 + fields = ['image', 'description', 'is_primary'] + + +class UsageImageInline(admin.TabularInline): + """使用记录图片内联编辑""" + model = UsageImage + extra = 1 + fields = ['image', 'image_type', 'description'] @admin.register(Item) class ItemAdmin(admin.ModelAdmin): - list_display = ['name', 'serial_number', 'category', 'status', 'location', 'created_at'] + list_display = ['name', 'serial_number', 'category', 'status', 'location', 'owner', 'created_at'] list_filter = ['status', 'category', 'created_at'] - search_fields = ['name', 'serial_number', 'description'] + search_fields = ['name', 'serial_number', 'description', 'owner'] readonly_fields = ['created_at', 'updated_at'] + inlines = [ItemImageInline] fieldsets = ( ('基本信息', { 'fields': ('name', 'description', 'serial_number', 'category') }), ('状态和位置', { - 'fields': ('status', 'location') + 'fields': ('status', 'location', 'owner') }), ('购买信息', { 'fields': ('purchase_date', 'value') @@ -28,16 +43,17 @@ class ItemAdmin(admin.ModelAdmin): @admin.register(ItemUsage) class ItemUsageAdmin(admin.ModelAdmin): - list_display = ['item', 'user', 'start_time', 'end_time', 'is_returned', 'purpose'] + list_display = ['item', 'user', 'borrower_contact', 'start_time', 'end_time', 'is_returned', 'purpose'] list_filter = ['is_returned', 'start_time', 'item__category'] - search_fields = ['item__name', 'user__username', 'purpose'] + search_fields = ['item__name', 'user', 'purpose', 'borrower_contact'] readonly_fields = ['created_at'] + inlines = [UsageImageInline] fieldsets = ( ('使用信息', { - 'fields': ('item', 'user', 'purpose', 'notes') + 'fields': ('item', 'user', 'borrower_contact', 'purpose', 'notes') }), ('时间信息', { - 'fields': ('start_time', 'end_time', 'is_returned') + 'fields': ('start_time', 'end_time', 'expected_return_time', 'is_returned') }), ('状况记录', { 'fields': ('condition_before', 'condition_after') @@ -49,6 +65,22 @@ class ItemUsageAdmin(admin.ModelAdmin): ) +@admin.register(ItemImage) +class ItemImageAdmin(admin.ModelAdmin): + list_display = ['item', 'description', 'is_primary', 'created_at'] + list_filter = ['is_primary', 'created_at'] + search_fields = ['item__name', 'description'] + readonly_fields = ['created_at'] + + +@admin.register(UsageImage) +class UsageImageAdmin(admin.ModelAdmin): + list_display = ['usage', 'image_type', 'description', 'created_at'] + list_filter = ['image_type', 'created_at'] + search_fields = ['usage__item__name', 'usage__user', 'description'] + readonly_fields = ['created_at'] + + @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ['name', 'description', 'created_at'] diff --git a/src/backend/items/management/__init__.py b/src/backend/items/management/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/backend/items/management/__init__.py @@ -0,0 +1 @@ + diff --git a/src/backend/items/management/commands/__init__.py b/src/backend/items/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/items/management/commands/create_default_categories.py b/src/backend/items/management/commands/create_default_categories.py new file mode 100644 index 0000000..0234194 --- /dev/null +++ b/src/backend/items/management/commands/create_default_categories.py @@ -0,0 +1,143 @@ +from django.core.management.base import BaseCommand +from items.models import Category + +# 添加物品类别:python manage.py create_default_categories + + +class Command(BaseCommand): + help = '创建默认的物品类别' + + def handle(self, *args, **options): + # 默认物品类别列表 + default_categories = [ + { + 'name': '电子设备', + 'description': '包括电脑、手机、平板等电子产品' + }, + { + 'name': '办公用品', + 'description': '办公桌椅、文具、打印机等办公设备' + }, + { + 'name': '实验器材', + 'description': '实验室设备、仪器、工具等' + }, + { + 'name': '家具', + 'description': '桌子、椅子、柜子等家具用品' + }, + { + 'name': '图书资料', + 'description': '书籍、文档、资料等' + }, + { + 'name': '工具设备', + 'description': '维修工具、测量仪器等' + }, + { + 'name': '音响设备', + 'description': '音响、麦克风、投影仪等音视频设备' + }, + { + 'name': '运动器材', + 'description': '体育用品、健身器材等' + }, + { + 'name': '清洁用品', + 'description': '清洁工具、清洁剂等' + }, + { + 'name': '网络设备', + 'description': '路由器、交换机、网线等网络相关设备' + }, + { + 'name': '安全设备', + 'description': '监控摄像头、门禁系统、报警器等安全设备' + }, + { + 'name': '医疗用品', + 'description': '急救包、体温计、血压计等医疗相关用品' + }, + { + 'name': '厨房用具', + 'description': '微波炉、咖啡机、餐具等厨房设备' + }, + { + 'name': '照明设备', + 'description': '台灯、吊灯、应急灯等照明用品' + }, + { + 'name': '存储设备', + 'description': '硬盘、U盘、移动硬盘等存储设备' + }, + { + 'name': '车辆工具', + 'description': '汽车配件、维修工具、车载设备等' + }, + { + 'name': '服装用品', + 'description': '工作服、防护服、鞋帽等服装类物品' + }, + { + 'name': '教学用品', + 'description': '黑板、白板、教学模型等教学设备' + }, + { + 'name': '通讯设备', + 'description': '对讲机、电话、传真机等通讯设备' + }, + { + 'name': '空调制冷', + 'description': '空调、风扇、加湿器等温度调节设备' + }, + { + 'name': '消防设备', + 'description': '灭火器、烟雾报警器、消防栓等消防用品' + }, + { + 'name': '软件许可', + 'description': '软件授权、许可证、数字资产等' + }, + { + 'name': '包装材料', + 'description': '纸箱、胶带、包装袋等包装用品' + }, + { + 'name': '园艺用品', + 'description': '花盆、园艺工具、肥料等园艺相关用品' + }, + { + 'name': '其他', + 'description': '其他未分类的物品' + } + ] + + created_count = 0 + updated_count = 0 + + for category_data in default_categories: + category, created = Category.objects.get_or_create( + name=category_data['name'], + defaults={'description': category_data['description']} + ) + + if created: + created_count += 1 + self.stdout.write( + self.style.SUCCESS(f'创建类别: {category.name}') + ) + else: + # 更新描述(如果不同) + if category.description != category_data['description']: + category.description = category_data['description'] + category.save() + updated_count += 1 + self.stdout.write( + self.style.WARNING(f'更新类别: {category.name}') + ) + + self.stdout.write( + self.style.SUCCESS( + f'\n任务完成!创建了 {created_count} 个新类别,更新了 {updated_count} 个类别。' + ) + ) diff --git a/src/backend/items/models.py b/src/backend/items/models.py index e2e832b..cc28303 100644 --- a/src/backend/items/models.py +++ b/src/backend/items/models.py @@ -1,4 +1,27 @@ from django.db import models +import os +import uuid + + +def get_item_image_path(instance, filename): + """生成物品图片的存储路径""" + # 如果物品还没有ID,先生成一个临时文件夹名 + item_id = instance.item.id if instance.item.id else str(uuid.uuid4()) + return f'items/{item_id}/initial/{filename}' + + +def get_usage_borrow_image_path(instance, filename): + """生成借用时图片的存储路径""" + item_id = instance.usage.item.id if instance.usage.item.id else str(uuid.uuid4()) + usage_id = instance.usage.id if instance.usage.id else str(uuid.uuid4()) + return f'items/{item_id}/usage/{usage_id}/borrow/{filename}' + + +def get_usage_return_image_path(instance, filename): + """生成归还时图片的存储路径""" + item_id = instance.usage.item.id if instance.usage.item.id else str(uuid.uuid4()) + usage_id = instance.usage.id if instance.usage.id else str(uuid.uuid4()) + return f'items/{item_id}/usage/{usage_id}/return/{filename}' class Item(models.Model): @@ -34,6 +57,23 @@ class Item(models.Model): return f"{self.name} ({self.serial_number})" +class ItemImage(models.Model): + """物品图片模型""" + item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='images', verbose_name='物品') + image = models.ImageField(upload_to=get_item_image_path, verbose_name='图片') + description = models.CharField(max_length=200, blank=True, verbose_name='图片描述') + is_primary = models.BooleanField(default=False, verbose_name='是否为主图') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='上传时间') + + class Meta: + verbose_name = '物品图片' + verbose_name_plural = '物品图片' + ordering = ['-is_primary', '-created_at'] + + def __str__(self): + return f"{self.item.name} - 图片" + + class ItemUsage(models.Model): """物品使用记录模型""" item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name='物品') @@ -58,6 +98,53 @@ class ItemUsage(models.Model): return f"{self.item.name} - {self.user} ({self.start_time.strftime('%Y-%m-%d %H:%M')})" +class UsageImage(models.Model): + """使用记录图片模型""" + IMAGE_TYPE_CHOICES = [ + ('borrow', '借用时'), + ('return', '归还时'), + ] + + usage = models.ForeignKey(ItemUsage, on_delete=models.CASCADE, related_name='images', verbose_name='使用记录') + image = models.ImageField(upload_to='usage_images/', verbose_name='图片') + image_type = models.CharField(max_length=10, choices=IMAGE_TYPE_CHOICES, verbose_name='图片类型') + description = models.CharField(max_length=200, blank=True, verbose_name='图片描述') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='上传时间') + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # 保存后重新组织文件路径 + if self.image and self.usage_id and self.usage.item_id: + import os + import shutil + from django.conf import settings + + old_path = self.image.path + if self.image_type == 'borrow': + new_relative_path = f'items/{self.usage.item_id}/usage/{self.usage_id}/borrow/{os.path.basename(old_path)}' + else: + new_relative_path = f'items/{self.usage.item_id}/usage/{self.usage_id}/return/{os.path.basename(old_path)}' + + new_path = os.path.join(settings.MEDIA_ROOT, new_relative_path) + + # 创建目录 + os.makedirs(os.path.dirname(new_path), exist_ok=True) + + # 移动文件 + if os.path.exists(old_path) and old_path != new_path: + shutil.move(old_path, new_path) + self.image.name = new_relative_path + super().save(update_fields=['image']) + + class Meta: + verbose_name = '使用记录图片' + verbose_name_plural = '使用记录图片' + ordering = ['-created_at'] + + def __str__(self): + return f"{self.usage.item.name} - {self.get_image_type_display()}" + + class Category(models.Model): """物品类别模型""" name = models.CharField(max_length=50, unique=True, verbose_name='类别名称') diff --git a/src/backend/items/serializers.py b/src/backend/items/serializers.py index e9fc02c..5b22206 100644 --- a/src/backend/items/serializers.py +++ b/src/backend/items/serializers.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import User from rest_framework import serializers -from .models import Item, ItemUsage, Category +from .models import Item, ItemUsage, Category, ItemImage, UsageImage class UserSerializer(serializers.ModelSerializer): @@ -16,15 +16,47 @@ class CategorySerializer(serializers.ModelSerializer): fields = ['id', 'name', 'description', 'created_at'] +class ItemImageSerializer(serializers.ModelSerializer): + """物品图片序列化器""" + image_url = serializers.SerializerMethodField() + + class Meta: + model = ItemImage + fields = ['id', 'image', 'image_url', 'description', 'is_primary', 'created_at'] + + def get_image_url(self, obj): + request = self.context.get('request') + if obj.image and request: + return request.build_absolute_uri(obj.image.url) + return None + + +class UsageImageSerializer(serializers.ModelSerializer): + """使用记录图片序列化器""" + image_url = serializers.SerializerMethodField() + + class Meta: + model = UsageImage + fields = ['id', 'image', 'image_url', 'image_type', 'description', 'created_at'] + + def get_image_url(self, obj): + request = self.context.get('request') + if obj.image and request: + return request.build_absolute_uri(obj.image.url) + return None + + class ItemSerializer(serializers.ModelSerializer): current_user = serializers.SerializerMethodField() + images = ItemImageSerializer(many=True, read_only=True) + primary_image = serializers.SerializerMethodField() class Meta: model = Item fields = [ 'id', 'name', 'description', 'serial_number', 'category', 'status', 'location', 'owner', 'purchase_date', 'value', 'created_at', 'updated_at', - 'current_user' + 'current_user', 'images', 'primary_image' ] def get_current_user(self, obj): @@ -39,20 +71,43 @@ class ItemSerializer(serializers.ModelSerializer): } return None + def get_primary_image(self, obj): + """获取主图片""" + primary_image = obj.images.filter(is_primary=True).first() + if primary_image: + request = self.context.get('request') + if request: + return request.build_absolute_uri(primary_image.image.url) + return None + class ItemUsageSerializer(serializers.ModelSerializer): item_name = serializers.CharField(source='item.name', read_only=True) item_serial = serializers.CharField(source='item.serial_number', read_only=True) + images = UsageImageSerializer(many=True, read_only=True) + borrow_images = serializers.SerializerMethodField() + return_images = serializers.SerializerMethodField() class Meta: model = ItemUsage fields = [ 'id', 'item', 'item_name', 'item_serial', 'user', 'borrower_contact', 'start_time', 'end_time', 'purpose', 'notes', 'is_returned', - 'condition_before', 'condition_after', 'expected_return_time', 'created_at' + 'condition_before', 'condition_after', 'expected_return_time', 'created_at', + 'images', 'borrow_images', 'return_images' ] read_only_fields = ['created_at'] + def get_borrow_images(self, obj): + """获取借用时图片""" + borrow_images = obj.images.filter(image_type='borrow') + return UsageImageSerializer(borrow_images, many=True, context=self.context).data + + def get_return_images(self, obj): + """获取归还时图片""" + return_images = obj.images.filter(image_type='return') + return UsageImageSerializer(return_images, many=True, context=self.context).data + class ItemDetailSerializer(ItemSerializer): """物品详情序列化器,包含使用历史""" @@ -64,4 +119,4 @@ class ItemDetailSerializer(ItemSerializer): def get_usage_history(self, obj): """获取物品的使用历史""" usages = ItemUsage.objects.filter(item=obj).order_by('-start_time')[:10] - return ItemUsageSerializer(usages, many=True).data + return ItemUsageSerializer(usages, many=True, context=self.context).data diff --git a/src/backend/items/urls.py b/src/backend/items/urls.py index 1061e8f..fae589d 100644 --- a/src/backend/items/urls.py +++ b/src/backend/items/urls.py @@ -8,6 +8,8 @@ router.register(r'items', views.ItemViewSet) router.register(r'usages', views.ItemUsageViewSet) router.register(r'item_categories', views.CategoryViewSet) router.register(r'users', views.UserViewSet) +router.register(r'item-images', views.ItemImageViewSet) +router.register(r'usage-images', views.UsageImageViewSet) urlpatterns = [ path('api/', include(router.urls)), diff --git a/src/backend/items/views.py b/src/backend/items/views.py index 08285a8..5dfafa6 100644 --- a/src/backend/items/views.py +++ b/src/backend/items/views.py @@ -4,17 +4,19 @@ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework.parsers import MultiPartParser, FormParser -from .models import Item, ItemUsage, Category +from .models import Item, ItemUsage, Category, ItemImage, UsageImage from .serializers import ( ItemSerializer, ItemDetailSerializer, ItemUsageSerializer, - CategorySerializer, UserSerializer + CategorySerializer, UserSerializer, ItemImageSerializer, UsageImageSerializer ) class ItemViewSet(viewsets.ModelViewSet): """物品管理API""" authentication_classes = [JWTAuthentication] + parser_classes = [MultiPartParser, FormParser] queryset = Item.objects.all() serializer_class = ItemSerializer @@ -23,6 +25,80 @@ class ItemViewSet(viewsets.ModelViewSet): return ItemDetailSerializer return ItemSerializer + def create(self, request, *args, **kwargs): + """创建物品,强制要求上传至少一张图片""" + # 检查是否上传了图片 + images = request.FILES.getlist('images') + if not images: + return Response( + {'error': '创建物品时必须上传至少一张图片'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 创建物品 + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + item = serializer.save() + + # 保存图片 + for i, image in enumerate(images): + ItemImage.objects.create( + item=item, + image=image, + description=request.data.get(f'image_descriptions[{i}]', ''), + is_primary=(i == 0) # 第一张图片设为主图 + ) + + # 返回包含图片的完整数据 + response_serializer = ItemDetailSerializer(item, context={'request': request}) + return Response(response_serializer.data, status=status.HTTP_201_CREATED) + + @action(detail=True, methods=['post']) + def upload_images(self, request, pk=None): + """上传物品图片""" + item = self.get_object() + images = request.FILES.getlist('images') + + if not images: + return Response( + {'error': '请选择要上传的图片'}, + status=status.HTTP_400_BAD_REQUEST + ) + + uploaded_images = [] + for i, image in enumerate(images): + item_image = ItemImage.objects.create( + item=item, + image=image, + description=request.data.get(f'image_descriptions[{i}]', ''), + is_primary=False + ) + uploaded_images.append(item_image) + + serializer = ItemImageSerializer(uploaded_images, many=True, context={'request': request}) + return Response(serializer.data) + + @action(detail=True, methods=['post']) + def set_primary_image(self, request, pk=None): + """设置主图片""" + item = self.get_object() + image_id = request.data.get('image_id') + + try: + # 取消当前主图 + item.images.filter(is_primary=True).update(is_primary=False) + # 设置新主图 + new_primary = item.images.get(id=image_id) + new_primary.is_primary = True + new_primary.save() + + return Response({'message': '主图设置成功'}) + except ItemImage.DoesNotExist: + return Response( + {'error': '图片不存在'}, + status=status.HTTP_400_BAD_REQUEST + ) + @action(detail=True, methods=['post']) def borrow(self, request, pk=None): """借用物品""" @@ -57,11 +133,22 @@ class ItemViewSet(viewsets.ModelViewSet): condition_before=condition_before ) + # 保存借用时的图片 + borrow_images = request.FILES.getlist('borrow_images') + for i, image in enumerate(borrow_images): + UsageImage.objects.create( + usage=usage, + image=image, + image_type='borrow', + description=request.data.get(f'borrow_image_descriptions[{i}]', '') + ) + # 更新物品状态 item.status = 'in_use' item.save() - return Response(ItemUsageSerializer(usage).data) + response_serializer = ItemUsageSerializer(usage, context={'request': request}) + return Response(response_serializer.data) @action(detail=True, methods=['post']) def return_item(self, request, pk=None): @@ -86,11 +173,22 @@ class ItemViewSet(viewsets.ModelViewSet): current_usage.notes = request.data.get('return_notes', current_usage.notes) current_usage.save() + # 保存归还时的图片 + return_images = request.FILES.getlist('return_images') + for i, image in enumerate(return_images): + UsageImage.objects.create( + usage=current_usage, + image=image, + image_type='return', + description=request.data.get(f'return_image_descriptions[{i}]', '') + ) + # 更新物品状态 item.status = 'available' item.save() - return Response(ItemUsageSerializer(current_usage).data) + response_serializer = ItemUsageSerializer(current_usage, context={'request': request}) + return Response(response_serializer.data) @action(detail=False) def available(self, request): @@ -110,9 +208,36 @@ class ItemViewSet(viewsets.ModelViewSet): class ItemUsageViewSet(viewsets.ModelViewSet): """使用记录管理API""" authentication_classes = [JWTAuthentication] + parser_classes = [MultiPartParser, FormParser] queryset = ItemUsage.objects.all() serializer_class = ItemUsageSerializer + @action(detail=True, methods=['post']) + def upload_images(self, request, pk=None): + """上传使用记录图片""" + usage = self.get_object() + images = request.FILES.getlist('images') + image_type = request.data.get('image_type', 'borrow') + + if not images: + return Response( + {'error': '请选择要上传的图片'}, + status=status.HTTP_400_BAD_REQUEST + ) + + uploaded_images = [] + for i, image in enumerate(images): + usage_image = UsageImage.objects.create( + usage=usage, + image=image, + image_type=image_type, + description=request.data.get(f'image_descriptions[{i}]', '') + ) + uploaded_images.append(usage_image) + + serializer = UsageImageSerializer(uploaded_images, many=True, context={'request': request}) + return Response(serializer.data) + @action(detail=False) def current(self, request): """获取当前使用中的记录""" @@ -143,3 +268,19 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): authentication_classes = [JWTAuthentication] queryset = User.objects.all() serializer_class = UserSerializer + + +class ItemImageViewSet(viewsets.ModelViewSet): + """物品图片管理API""" + authentication_classes = [JWTAuthentication] + parser_classes = [MultiPartParser, FormParser] + queryset = ItemImage.objects.all() + serializer_class = ItemImageSerializer + + +class UsageImageViewSet(viewsets.ModelViewSet): + """使用记录图片管理API""" + authentication_classes = [JWTAuthentication] + parser_classes = [MultiPartParser, FormParser] + queryset = UsageImage.objects.all() + serializer_class = UsageImageSerializer diff --git a/src/fronted/src/router/index.js b/src/fronted/src/router/index.js index 13d8a7f..2522121 100644 --- a/src/fronted/src/router/index.js +++ b/src/fronted/src/router/index.js @@ -2,6 +2,7 @@ import {createRouter, createWebHistory} from 'vue-router' import Login from '../views/Login.vue' import ItemList from '../views/ItemList.vue' import ItemDetail from '../views/ItemDetail.vue' +import ItemCreate from '../views/ItemCreate.vue' import ItemUsage from '../views/ItemUsage.vue' import Dashboard from '../views/Dashboard.vue' import FinanceDashboard from '../views/FinanceDashboard.vue' @@ -28,6 +29,11 @@ const routes = [ name: 'ItemList', component: ItemList }, + { + path: '/items/create', + name: 'ItemCreate', + component: ItemCreate + }, { path: '/items/:id', name: 'ItemDetail', diff --git a/src/fronted/src/services/api.js b/src/fronted/src/services/api.js index ecdf45f..788f5d0 100644 --- a/src/fronted/src/services/api.js +++ b/src/fronted/src/services/api.js @@ -113,9 +113,13 @@ export const itemService = { return apiClient.get(`/items/${id}/`) }, - // 创建新物品 - createItem(item) { - return apiClient.post('/items/', item) + // 创建新物品(支持图片上传) + createItem(formData) { + return apiClient.post('/items/', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) }, // 更新物品 @@ -128,6 +132,31 @@ export const itemService = { return apiClient.delete(`/items/${id}/`) }, + // 上传物品图片 + uploadItemImages(itemId, formData) { + return apiClient.post(`/items/${itemId}/upload_images/`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 设置主图片 + setPrimaryImage(itemId, imageId) { + const formData = new FormData() + formData.append('image_id', imageId) + return apiClient.post(`/items/${itemId}/set_primary_image/`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 删除物品图片 + deleteItemImage(itemId, imageId) { + return apiClient.delete(`/item-images/${imageId}/`) + }, + // 获取可用物品 getAvailableItems() { return apiClient.get('/items/available/') @@ -138,14 +167,22 @@ export const itemService = { return apiClient.get('/items/in_use/') }, - // 借用物品 - borrowItem(itemId, borrowData) { - return apiClient.post(`/items/${itemId}/borrow/`, borrowData) + // 借用物品(支持图片上传) + borrowItem(itemId, formData) { + return apiClient.post(`/items/${itemId}/borrow/`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) }, - // 归还物品 - returnItem(itemId, returnData) { - return apiClient.post(`/items/${itemId}/return_item/`, returnData) + // 归还物品(支持图片上传) + returnItem(itemId, formData) { + return apiClient.post(`/items/${itemId}/return_item/`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) } } @@ -173,6 +210,15 @@ export const usageService = { // 更新使用记录 updateUsage(id, usage) { return apiClient.put(`/usages/${id}/`, usage) + }, + + // 上传使用记录图片 + uploadUsageImages(usageId, formData) { + return apiClient.post(`/usages/${usageId}/upload_images/`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) } } diff --git a/src/fronted/src/views/ItemCreate.vue b/src/fronted/src/views/ItemCreate.vue new file mode 100644 index 0000000..da991fc --- /dev/null +++ b/src/fronted/src/views/ItemCreate.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/src/fronted/src/views/ItemDetail.vue b/src/fronted/src/views/ItemDetail.vue index d0221e0..31b3e28 100644 --- a/src/fronted/src/views/ItemDetail.vue +++ b/src/fronted/src/views/ItemDetail.vue @@ -12,13 +12,63 @@
+ + + + + + + @@ -103,6 +153,18 @@ + + + @@ -144,12 +273,17 @@ import {itemService} from '@/services/api' import {ElMessage} from 'element-plus' import AppHeader from '../components/AppHeader.vue' +import { Picture, Plus, ArrowLeft, Delete } from '@element-plus/icons-vue' import moment from 'moment' export default { name: 'ItemDetail', components: { - AppHeader + AppHeader, + Picture, + Plus, + ArrowLeft, + Delete }, props: { id: { @@ -164,7 +298,23 @@ export default { loading: false, editMode: false, showUsageDialog: false, - selectedUsage: null + selectedUsage: null, + showImageUpload: false, + uploading: false, + uploadFiles: [], + showUsageImagesDialog: false, + selectedUsageImages: null + } + }, + computed: { + imagePreviewList() { + return this.item?.images?.map(img => img.image_url) || [] + }, + borrowImagePreviewList() { + return this.selectedUsageImages?.borrow_images?.map(img => img.image_url) || [] + }, + returnImagePreviewList() { + return this.selectedUsageImages?.return_images?.map(img => img.image_url) || [] } }, async mounted() { @@ -199,6 +349,73 @@ export default { this.selectedUsage = usage this.showUsageDialog = true }, + showUsageImages(usage) { + this.selectedUsageImages = usage + this.showUsageImagesDialog = true + }, + handleImageChange(file, fileList) { + this.uploadFiles = fileList + }, + handleImageRemove(file, fileList) { + this.uploadFiles = fileList + }, + async uploadImages() { + if (this.uploadFiles.length === 0) { + ElMessage.warning('请选择要上传的图片') + return + } + + this.uploading = true + try { + const formData = new FormData() + this.uploadFiles.forEach((file, index) => { + formData.append('images', file.raw) + formData.append(`image_descriptions[${index}]`, '') + }) + + await itemService.uploadItemImages(this.id, formData) + ElMessage.success('图片上传成功') + this.showImageUpload = false + this.uploadFiles = [] + this.$refs.uploadRef.clearFiles() + await this.loadItem() + } catch (error) { + console.error('图片上传失败:', error) + ElMessage.error('图片上传失败') + } finally { + this.uploading = false + } + }, + async setPrimaryImage(imageId) { + try { + await itemService.setPrimaryImage(this.id, imageId) + ElMessage.success('主图设置成功') + await this.loadItem() + } catch (error) { + console.error('设置主图失败:', error) + ElMessage.error('设置主图失败') + } + }, + async deleteImage(imageId) { + this.$confirm('确定删除这张图片吗?', '确认删除', { + confirmButtonText: '删除', + cancelButtonText: '取消', + type: 'warning' + }) + .then(async () => { + try { + await itemService.deleteItemImage(this.id, imageId) + ElMessage.success('图片删除成功') + await this.loadItem() + } catch (error) { + console.error('删除图片失败:', error) + ElMessage.error('删除图片失败') + } + }) + .catch(() => { + // 取消删除 + }) + }, formatDate(dateString) { return moment(dateString).format('YYYY-MM-DD HH:mm:ss') } @@ -216,4 +433,70 @@ export default { justify-content: space-between; align-items: center; } + +.image-section { + margin-bottom: 20px; +} + +.image-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 15px; + padding: 10px 0; +} + +.image-item { + text-align: center; +} + +.item-image { + width: 200px; + height: 150px; + border-radius: 8px; + cursor: pointer; +} + +.usage-image { + width: 150px; + height: 120px; + border-radius: 8px; + margin: 5px; + cursor: pointer; +} + +.image-info { + margin-top: 8px; +} + +.image-desc { + font-size: 12px; + color: #666; + margin: 4px 0 0 0; +} + +.usage-images-section { + margin-bottom: 20px; +} + +.usage-images-section h4 { + margin-bottom: 10px; + color: #409eff; +} + +.image-container { + position: relative; +} + +.image-actions { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 5px; +} + +.action-btn { + padding: 0 5px; + font-size: 12px; +} diff --git a/src/fronted/src/views/ItemList.vue b/src/fronted/src/views/ItemList.vue index ac968b7..34b2607 100644 --- a/src/fronted/src/views/ItemList.vue +++ b/src/fronted/src/views/ItemList.vue @@ -2,7 +2,7 @@
- + 添加物品 @@ -31,6 +31,29 @@
+ + + @@ -79,171 +102,146 @@ > 归还 - - 编辑 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+

{{ selectedItem.name }} ({{ selectedItem.serial_number }})

+ + + + + + + + + + + + + + + + + + + + +
+ + 可上传借用时的物品状态图片(可选,最多5张) + +
+
+ + + +
+
- - - - - - - - - + +
+

{{ selectedItem.name }} ({{ selectedItem.serial_number }})

+ + + + + + + + +
+ + 可上传归还时的物品状态图片(可选,最多5张) + +
+
+ + + +
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -