feat: item image now is required
This commit is contained in:
@@ -1,20 +1,35 @@
|
|||||||
from django.contrib import admin
|
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)
|
@admin.register(Item)
|
||||||
class ItemAdmin(admin.ModelAdmin):
|
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']
|
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']
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
|
inlines = [ItemImageInline]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('基本信息', {
|
('基本信息', {
|
||||||
'fields': ('name', 'description', 'serial_number', 'category')
|
'fields': ('name', 'description', 'serial_number', 'category')
|
||||||
}),
|
}),
|
||||||
('状态和位置', {
|
('状态和位置', {
|
||||||
'fields': ('status', 'location')
|
'fields': ('status', 'location', 'owner')
|
||||||
}),
|
}),
|
||||||
('购买信息', {
|
('购买信息', {
|
||||||
'fields': ('purchase_date', 'value')
|
'fields': ('purchase_date', 'value')
|
||||||
@@ -28,16 +43,17 @@ class ItemAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(ItemUsage)
|
@admin.register(ItemUsage)
|
||||||
class ItemUsageAdmin(admin.ModelAdmin):
|
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']
|
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']
|
readonly_fields = ['created_at']
|
||||||
|
inlines = [UsageImageInline]
|
||||||
fieldsets = (
|
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')
|
'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)
|
@admin.register(Category)
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'description', 'created_at']
|
list_display = ['name', 'description', 'created_at']
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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} 个类别。'
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1,4 +1,27 @@
|
|||||||
from django.db import models
|
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):
|
class Item(models.Model):
|
||||||
@@ -34,6 +57,23 @@ class Item(models.Model):
|
|||||||
return f"{self.name} ({self.serial_number})"
|
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):
|
class ItemUsage(models.Model):
|
||||||
"""物品使用记录模型"""
|
"""物品使用记录模型"""
|
||||||
item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name='物品')
|
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')})"
|
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):
|
class Category(models.Model):
|
||||||
"""物品类别模型"""
|
"""物品类别模型"""
|
||||||
name = models.CharField(max_length=50, unique=True, verbose_name='类别名称')
|
name = models.CharField(max_length=50, unique=True, verbose_name='类别名称')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Item, ItemUsage, Category
|
from .models import Item, ItemUsage, Category, ItemImage, UsageImage
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
@@ -16,15 +16,47 @@ class CategorySerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'name', 'description', 'created_at']
|
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):
|
class ItemSerializer(serializers.ModelSerializer):
|
||||||
current_user = serializers.SerializerMethodField()
|
current_user = serializers.SerializerMethodField()
|
||||||
|
images = ItemImageSerializer(many=True, read_only=True)
|
||||||
|
primary_image = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'description', 'serial_number', 'category', 'status',
|
'id', 'name', 'description', 'serial_number', 'category', 'status',
|
||||||
'location', 'owner', 'purchase_date', 'value', 'created_at', 'updated_at',
|
'location', 'owner', 'purchase_date', 'value', 'created_at', 'updated_at',
|
||||||
'current_user'
|
'current_user', 'images', 'primary_image'
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_current_user(self, obj):
|
def get_current_user(self, obj):
|
||||||
@@ -39,20 +71,43 @@ class ItemSerializer(serializers.ModelSerializer):
|
|||||||
}
|
}
|
||||||
return None
|
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):
|
class ItemUsageSerializer(serializers.ModelSerializer):
|
||||||
item_name = serializers.CharField(source='item.name', read_only=True)
|
item_name = serializers.CharField(source='item.name', read_only=True)
|
||||||
item_serial = serializers.CharField(source='item.serial_number', 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:
|
class Meta:
|
||||||
model = ItemUsage
|
model = ItemUsage
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'item', 'item_name', 'item_serial', 'user', 'borrower_contact',
|
'id', 'item', 'item_name', 'item_serial', 'user', 'borrower_contact',
|
||||||
'start_time', 'end_time', 'purpose', 'notes', 'is_returned',
|
'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']
|
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):
|
class ItemDetailSerializer(ItemSerializer):
|
||||||
"""物品详情序列化器,包含使用历史"""
|
"""物品详情序列化器,包含使用历史"""
|
||||||
@@ -64,4 +119,4 @@ class ItemDetailSerializer(ItemSerializer):
|
|||||||
def get_usage_history(self, obj):
|
def get_usage_history(self, obj):
|
||||||
"""获取物品的使用历史"""
|
"""获取物品的使用历史"""
|
||||||
usages = ItemUsage.objects.filter(item=obj).order_by('-start_time')[:10]
|
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
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ router.register(r'items', views.ItemViewSet)
|
|||||||
router.register(r'usages', views.ItemUsageViewSet)
|
router.register(r'usages', views.ItemUsageViewSet)
|
||||||
router.register(r'item_categories', views.CategoryViewSet)
|
router.register(r'item_categories', views.CategoryViewSet)
|
||||||
router.register(r'users', views.UserViewSet)
|
router.register(r'users', views.UserViewSet)
|
||||||
|
router.register(r'item-images', views.ItemImageViewSet)
|
||||||
|
router.register(r'usage-images', views.UsageImageViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('api/', include(router.urls)),
|
path('api/', include(router.urls)),
|
||||||
|
|||||||
+145
-4
@@ -4,17 +4,19 @@ from rest_framework import viewsets, status
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
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 (
|
from .serializers import (
|
||||||
ItemSerializer, ItemDetailSerializer, ItemUsageSerializer,
|
ItemSerializer, ItemDetailSerializer, ItemUsageSerializer,
|
||||||
CategorySerializer, UserSerializer
|
CategorySerializer, UserSerializer, ItemImageSerializer, UsageImageSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ItemViewSet(viewsets.ModelViewSet):
|
class ItemViewSet(viewsets.ModelViewSet):
|
||||||
"""物品管理API"""
|
"""物品管理API"""
|
||||||
authentication_classes = [JWTAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
queryset = Item.objects.all()
|
queryset = Item.objects.all()
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
|
|
||||||
@@ -23,6 +25,80 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
return ItemDetailSerializer
|
return ItemDetailSerializer
|
||||||
return ItemSerializer
|
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'])
|
@action(detail=True, methods=['post'])
|
||||||
def borrow(self, request, pk=None):
|
def borrow(self, request, pk=None):
|
||||||
"""借用物品"""
|
"""借用物品"""
|
||||||
@@ -57,11 +133,22 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
condition_before=condition_before
|
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.status = 'in_use'
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
return Response(ItemUsageSerializer(usage).data)
|
response_serializer = ItemUsageSerializer(usage, context={'request': request})
|
||||||
|
return Response(response_serializer.data)
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def return_item(self, request, pk=None):
|
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.notes = request.data.get('return_notes', current_usage.notes)
|
||||||
current_usage.save()
|
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.status = 'available'
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
return Response(ItemUsageSerializer(current_usage).data)
|
response_serializer = ItemUsageSerializer(current_usage, context={'request': request})
|
||||||
|
return Response(response_serializer.data)
|
||||||
|
|
||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def available(self, request):
|
def available(self, request):
|
||||||
@@ -110,9 +208,36 @@ class ItemViewSet(viewsets.ModelViewSet):
|
|||||||
class ItemUsageViewSet(viewsets.ModelViewSet):
|
class ItemUsageViewSet(viewsets.ModelViewSet):
|
||||||
"""使用记录管理API"""
|
"""使用记录管理API"""
|
||||||
authentication_classes = [JWTAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
queryset = ItemUsage.objects.all()
|
queryset = ItemUsage.objects.all()
|
||||||
serializer_class = ItemUsageSerializer
|
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)
|
@action(detail=False)
|
||||||
def current(self, request):
|
def current(self, request):
|
||||||
"""获取当前使用中的记录"""
|
"""获取当前使用中的记录"""
|
||||||
@@ -143,3 +268,19 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
authentication_classes = [JWTAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserSerializer
|
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
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {createRouter, createWebHistory} from 'vue-router'
|
|||||||
import Login from '../views/Login.vue'
|
import Login from '../views/Login.vue'
|
||||||
import ItemList from '../views/ItemList.vue'
|
import ItemList from '../views/ItemList.vue'
|
||||||
import ItemDetail from '../views/ItemDetail.vue'
|
import ItemDetail from '../views/ItemDetail.vue'
|
||||||
|
import ItemCreate from '../views/ItemCreate.vue'
|
||||||
import ItemUsage from '../views/ItemUsage.vue'
|
import ItemUsage from '../views/ItemUsage.vue'
|
||||||
import Dashboard from '../views/Dashboard.vue'
|
import Dashboard from '../views/Dashboard.vue'
|
||||||
import FinanceDashboard from '../views/FinanceDashboard.vue'
|
import FinanceDashboard from '../views/FinanceDashboard.vue'
|
||||||
@@ -28,6 +29,11 @@ const routes = [
|
|||||||
name: 'ItemList',
|
name: 'ItemList',
|
||||||
component: ItemList
|
component: ItemList
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/items/create',
|
||||||
|
name: 'ItemCreate',
|
||||||
|
component: ItemCreate
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/items/:id',
|
path: '/items/:id',
|
||||||
name: 'ItemDetail',
|
name: 'ItemDetail',
|
||||||
|
|||||||
@@ -113,9 +113,13 @@ export const itemService = {
|
|||||||
return apiClient.get(`/items/${id}/`)
|
return apiClient.get(`/items/${id}/`)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 创建新物品
|
// 创建新物品(支持图片上传)
|
||||||
createItem(item) {
|
createItem(formData) {
|
||||||
return apiClient.post('/items/', item)
|
return apiClient.post('/items/', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新物品
|
// 更新物品
|
||||||
@@ -128,6 +132,31 @@ export const itemService = {
|
|||||||
return apiClient.delete(`/items/${id}/`)
|
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() {
|
getAvailableItems() {
|
||||||
return apiClient.get('/items/available/')
|
return apiClient.get('/items/available/')
|
||||||
@@ -138,14 +167,22 @@ export const itemService = {
|
|||||||
return apiClient.get('/items/in_use/')
|
return apiClient.get('/items/in_use/')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 借用物品
|
// 借用物品(支持图片上传)
|
||||||
borrowItem(itemId, borrowData) {
|
borrowItem(itemId, formData) {
|
||||||
return apiClient.post(`/items/${itemId}/borrow/`, borrowData)
|
return apiClient.post(`/items/${itemId}/borrow/`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 归还物品
|
// 归还物品(支持图片上传)
|
||||||
returnItem(itemId, returnData) {
|
returnItem(itemId, formData) {
|
||||||
return apiClient.post(`/items/${itemId}/return_item/`, returnData)
|
return apiClient.post(`/items/${itemId}/return_item/`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +210,15 @@ export const usageService = {
|
|||||||
// 更新使用记录
|
// 更新使用记录
|
||||||
updateUsage(id, usage) {
|
updateUsage(id, usage) {
|
||||||
return apiClient.put(`/usages/${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'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
<template>
|
||||||
|
<AppHeader />
|
||||||
|
<div class="item-create">
|
||||||
|
<div class="create-header">
|
||||||
|
<el-button @click="$router.go(-1)" style="margin-bottom: 20px;">
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
返回
|
||||||
|
</el-button>
|
||||||
|
<h2>创建新物品</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card>
|
||||||
|
<el-form :model="itemForm" :rules="rules" ref="itemFormRef" label-width="120px">
|
||||||
|
<!-- 物品图片上传 - 必填项 -->
|
||||||
|
<el-form-item label="物品图片" required>
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:multiple="true"
|
||||||
|
:limit="10"
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
:on-change="handleImageChange"
|
||||||
|
:on-remove="handleImageRemove"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<el-text type="info" size="small">
|
||||||
|
* 必须上传至少一张图片,支持 JPG、PNG 格式,最多上传 10 张图片
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="物品名称" prop="name">
|
||||||
|
<el-input v-model="itemForm.name" placeholder="请输入物品名称" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="序列号" prop="serial_number">
|
||||||
|
<el-input v-model="itemForm.serial_number" placeholder="请输入序列号" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="类别" prop="category">
|
||||||
|
<el-select v-model="itemForm.category" placeholder="请选择类别" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
:label="category.name"
|
||||||
|
:value="category.name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="位置" prop="location">
|
||||||
|
<el-input v-model="itemForm.location" placeholder="请输入存放位置" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="所有者" prop="owner">
|
||||||
|
<el-input v-model="itemForm.owner" placeholder="请输入所有者姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="购买日期" prop="purchase_date">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="itemForm.purchase_date"
|
||||||
|
type="date"
|
||||||
|
placeholder="请选择购买日期"
|
||||||
|
style="width: 100%"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="价值" prop="value">
|
||||||
|
<el-input-number
|
||||||
|
v-model="itemForm.value"
|
||||||
|
:precision="2"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入物品价值"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="itemForm.status" placeholder="请选择状态" style="width: 100%">
|
||||||
|
<el-option label="可用" value="available" />
|
||||||
|
<el-option label="维护中" value="maintenance" />
|
||||||
|
<el-option label="损坏" value="damaged" />
|
||||||
|
<el-option label="已弃用" value="abandoned" />
|
||||||
|
<el-option label="禁止借用" value="prohibited" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
v-model="itemForm.description"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入物品描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="resetForm">重置</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm" :loading="submitting">
|
||||||
|
创建物品
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { itemService, categoryService } from '@/services/api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import AppHeader from '../components/AppHeader.vue'
|
||||||
|
import { Plus, ArrowLeft } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ItemCreate',
|
||||||
|
components: {
|
||||||
|
AppHeader,
|
||||||
|
Plus,
|
||||||
|
ArrowLeft
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
itemForm: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
serial_number: '',
|
||||||
|
category: '',
|
||||||
|
status: 'available',
|
||||||
|
location: '',
|
||||||
|
owner: '',
|
||||||
|
purchase_date: null,
|
||||||
|
value: null
|
||||||
|
},
|
||||||
|
uploadFiles: [],
|
||||||
|
categories: [],
|
||||||
|
submitting: false,
|
||||||
|
rules: {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入物品名称', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
serial_number: [
|
||||||
|
{ required: true, message: '请输入序列号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
category: [
|
||||||
|
{ required: true, message: '请选择类别', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.loadCategories()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadCategories() {
|
||||||
|
try {
|
||||||
|
const response = await categoryService.getAllCategories()
|
||||||
|
this.categories = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载类别失败:', error)
|
||||||
|
ElMessage.error('加载类别失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageChange(file, fileList) {
|
||||||
|
this.uploadFiles = fileList
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageRemove(file, fileList) {
|
||||||
|
this.uploadFiles = fileList
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitForm() {
|
||||||
|
// 首先验证图片是否上传
|
||||||
|
if (this.uploadFiles.length === 0) {
|
||||||
|
ElMessage.error('请至少上传一张物品图片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证表单
|
||||||
|
await this.$refs.itemFormRef.validate()
|
||||||
|
|
||||||
|
this.submitting = true
|
||||||
|
|
||||||
|
// 创建FormData对象
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
// 添加物品基本信息
|
||||||
|
Object.keys(this.itemForm).forEach(key => {
|
||||||
|
const value = this.itemForm[key]
|
||||||
|
if (value !== null && value !== '' && value !== undefined) {
|
||||||
|
formData.append(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加图片文件
|
||||||
|
this.uploadFiles.forEach((fileItem, index) => {
|
||||||
|
// 确保获取正确的文件对象
|
||||||
|
const file = fileItem.raw || fileItem
|
||||||
|
if (file instanceof File) {
|
||||||
|
formData.append('images', file)
|
||||||
|
formData.append(`image_descriptions[${index}]`, '')
|
||||||
|
} else {
|
||||||
|
console.error('Invalid file object:', fileItem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调试信息
|
||||||
|
console.log('上传文件数量:', this.uploadFiles.length)
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
console.log('FormData:', key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await itemService.createItem(formData)
|
||||||
|
ElMessage.success('物品创建成功')
|
||||||
|
|
||||||
|
// 跳转到物品详情页
|
||||||
|
this.$router.push(`/items/${response.data.id}`)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建物品失败:', error)
|
||||||
|
|
||||||
|
// 更详细的错误处理
|
||||||
|
let errorMessage = '创建物品失败'
|
||||||
|
if (error.response && error.response.data) {
|
||||||
|
if (error.response.data.error) {
|
||||||
|
errorMessage = error.response.data.error
|
||||||
|
} else if (error.response.data.message) {
|
||||||
|
errorMessage = error.response.data.message
|
||||||
|
} else if (typeof error.response.data === 'string') {
|
||||||
|
errorMessage = error.response.data
|
||||||
|
}
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.error(errorMessage)
|
||||||
|
} finally {
|
||||||
|
this.submitting = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.$refs.itemFormRef.resetFields()
|
||||||
|
this.uploadFiles = []
|
||||||
|
this.$refs.uploadRef.clearFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item-create {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-header h2 {
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload--picture-card) {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,13 +12,63 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>物品详情</span>
|
<span>物品详情</span>
|
||||||
<el-button type="primary" @click="editMode = !editMode">
|
<div>
|
||||||
{{ editMode ? '取消编辑' : '编辑' }}
|
<el-button @click="showImageUpload = true" style="margin-right: 10px;">
|
||||||
</el-button>
|
<el-icon><Picture /></el-icon>
|
||||||
|
上传图片
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="editMode = !editMode">
|
||||||
|
{{ editMode ? '取消编辑' : '编辑' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="item">
|
<div v-if="item">
|
||||||
|
<!-- 物品图片展示 -->
|
||||||
|
<el-card class="image-section" style="margin-bottom: 20px;">
|
||||||
|
<template #header>
|
||||||
|
<span>物品图片</span>
|
||||||
|
</template>
|
||||||
|
<div v-if="item.images && item.images.length > 0" class="image-gallery">
|
||||||
|
<div v-for="image in item.images" :key="image.id" class="image-item">
|
||||||
|
<div class="image-container">
|
||||||
|
<el-image
|
||||||
|
:src="image.image_url"
|
||||||
|
:preview-src-list="imagePreviewList"
|
||||||
|
fit="cover"
|
||||||
|
class="item-image"
|
||||||
|
/>
|
||||||
|
<!-- 图片操作按钮 -->
|
||||||
|
<div class="image-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="!image.is_primary"
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="setPrimaryImage(image.id)"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
设为主图
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="deleteImage(image.id)"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-info">
|
||||||
|
<el-tag v-if="image.is_primary" type="success" size="small">主图</el-tag>
|
||||||
|
<p v-if="image.description" class="image-desc">{{ image.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无图片" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-form :model="editableItem" label-width="120px" :disabled="!editMode">
|
<el-form :model="editableItem" label-width="120px" :disabled="!editMode">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -103,6 +153,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="purpose" label="使用目的" />
|
<el-table-column prop="purpose" label="使用目的" />
|
||||||
|
<el-table-column label="图片" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.borrow_images?.length > 0 || scope.row.return_images?.length > 0"
|
||||||
|
size="small"
|
||||||
|
@click="showUsageImages(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="is_returned" label="状态">
|
<el-table-column prop="is_returned" label="状态">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.is_returned ? 'success' : 'warning'">
|
<el-tag :type="scope.row.is_returned ? 'success' : 'warning'">
|
||||||
@@ -120,8 +182,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 物品图片上传对话框 -->
|
||||||
|
<el-dialog v-model="showImageUpload" title="上传物品图片" width="500px">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:multiple="true"
|
||||||
|
:limit="10"
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
:on-change="handleImageChange"
|
||||||
|
:on-remove="handleImageRemove"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<el-text type="info" size="small">
|
||||||
|
支持 JPG、PNG 格式,最多上传 10 张图片
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="showImageUpload = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="uploadImages" :loading="uploading">
|
||||||
|
上传
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 使用记录详情对话框 -->
|
<!-- 使用记录详情对话框 -->
|
||||||
<el-dialog v-model="showUsageDialog" title="使用记录详情" width="600px">
|
<el-dialog v-model="showUsageDialog" title="使用记录详情" width="800px">
|
||||||
<div v-if="selectedUsage">
|
<div v-if="selectedUsage">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="使用者">{{ selectedUsage.user }}</el-descriptions-item>
|
<el-descriptions-item label="使用者">{{ selectedUsage.user }}</el-descriptions-item>
|
||||||
@@ -132,11 +223,49 @@
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="使用前状况">{{ selectedUsage.condition_before || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="使用前状况">{{ selectedUsage.condition_before || '无' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="使用后状况">{{ selectedUsage.condition_after || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="使用后状况">{{ selectedUsage.condition_after || '无' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="使用目的">{{ selectedUsage.notes || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="使用目的">{{ selectedUsage.purpose || '无' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">{{ selectedUsage.notes || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="备注" :span="2">{{ selectedUsage.notes || '无' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 使用记录图片对话框 -->
|
||||||
|
<el-dialog v-model="showUsageImagesDialog" title="使用记录图片" width="800px">
|
||||||
|
<div v-if="selectedUsageImages">
|
||||||
|
<div v-if="selectedUsageImages.borrow_images?.length > 0" class="usage-images-section">
|
||||||
|
<h4>借用时图片</h4>
|
||||||
|
<div class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
v-for="image in selectedUsageImages.borrow_images"
|
||||||
|
:key="image.id"
|
||||||
|
:src="image.image_url"
|
||||||
|
:preview-src-list="borrowImagePreviewList"
|
||||||
|
fit="cover"
|
||||||
|
class="usage-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedUsageImages.return_images?.length > 0" class="usage-images-section">
|
||||||
|
<h4>归还时图片</h4>
|
||||||
|
<div class="image-gallery">
|
||||||
|
<el-image
|
||||||
|
v-for="image in selectedUsageImages.return_images"
|
||||||
|
:key="image.id"
|
||||||
|
:src="image.image_url"
|
||||||
|
:preview-src-list="returnImagePreviewList"
|
||||||
|
fit="cover"
|
||||||
|
class="usage-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="(!selectedUsageImages.borrow_images || selectedUsageImages.borrow_images.length === 0) &&
|
||||||
|
(!selectedUsageImages.return_images || selectedUsageImages.return_images.length === 0)">
|
||||||
|
<el-empty description="暂无图片" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -144,12 +273,17 @@
|
|||||||
import {itemService} from '@/services/api'
|
import {itemService} from '@/services/api'
|
||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
import AppHeader from '../components/AppHeader.vue'
|
import AppHeader from '../components/AppHeader.vue'
|
||||||
|
import { Picture, Plus, ArrowLeft, Delete } from '@element-plus/icons-vue'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ItemDetail',
|
name: 'ItemDetail',
|
||||||
components: {
|
components: {
|
||||||
AppHeader
|
AppHeader,
|
||||||
|
Picture,
|
||||||
|
Plus,
|
||||||
|
ArrowLeft,
|
||||||
|
Delete
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
@@ -164,7 +298,23 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
editMode: false,
|
editMode: false,
|
||||||
showUsageDialog: 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() {
|
async mounted() {
|
||||||
@@ -199,6 +349,73 @@ export default {
|
|||||||
this.selectedUsage = usage
|
this.selectedUsage = usage
|
||||||
this.showUsageDialog = true
|
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) {
|
formatDate(dateString) {
|
||||||
return moment(dateString).format('YYYY-MM-DD HH:mm:ss')
|
return moment(dateString).format('YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
@@ -216,4 +433,70 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+212
-233
@@ -2,7 +2,7 @@
|
|||||||
<AppHeader />
|
<AppHeader />
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<el-button type="primary" @click="showAddDialog = true">
|
<el-button type="primary" @click="$router.push('/items/create')">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
添加物品
|
添加物品
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -31,6 +31,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="filteredItems" style="width: 100%" v-loading="loading">
|
<el-table :data="filteredItems" style="width: 100%" v-loading="loading">
|
||||||
|
<el-table-column label="图片" width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-image
|
||||||
|
v-if="scope.row.primary_image"
|
||||||
|
:src="scope.row.primary_image"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 50px; height: 50px; border-radius: 4px; cursor: pointer;"
|
||||||
|
:preview-src-list="[scope.row.primary_image]"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
<el-image
|
||||||
|
v-else-if="scope.row.images && scope.row.images.length > 0"
|
||||||
|
:src="scope.row.images[0].image_url"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 50px; height: 50px; border-radius: 4px; cursor: pointer;"
|
||||||
|
:preview-src-list="[scope.row.images[0].image_url]"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
<div v-else class="no-image-placeholder">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="name" label="物品名称" />
|
<el-table-column prop="name" label="物品名称" />
|
||||||
<el-table-column prop="serial_number" label="序列号" />
|
<el-table-column prop="serial_number" label="序列号" />
|
||||||
<el-table-column prop="category" label="类别" />
|
<el-table-column prop="category" label="类别" />
|
||||||
@@ -79,171 +102,146 @@
|
|||||||
>
|
>
|
||||||
归还
|
归还
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" @click="editItem(scope.row)">
|
|
||||||
编辑
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 添加物品对话框 -->
|
|
||||||
<el-dialog v-model="showAddDialog" title="添加物品" width="600px">
|
|
||||||
<el-form :model="newItem" label-width="100px" :rules="itemRules" ref="itemForm">
|
|
||||||
<el-form-item label="物品名称" prop="name">
|
|
||||||
<el-input v-model="newItem.name" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="序列号" prop="serial_number">
|
|
||||||
<el-input v-model="newItem.serial_number" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="类别" prop="category">
|
|
||||||
<el-input v-model="newItem.category" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="描述">
|
|
||||||
<el-input type="textarea" v-model="newItem.description" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="位置">
|
|
||||||
<el-input v-model="newItem.location" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="价值">
|
|
||||||
<el-input-number v-model="newItem.value" :precision="2" :min="0" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="购买日期">
|
|
||||||
<el-date-picker v-model="newItem.purchase_date" type="date" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="所有者">
|
|
||||||
<el-input v-model="newItem.owner" placeholder="请输入所有者姓名" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showAddDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="saveItem">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 借用物品对话框 -->
|
<!-- 借用物品对话框 -->
|
||||||
<el-dialog v-model="showBorrowDialog" title="借用物品" width="500px">
|
<el-dialog v-model="showBorrowDialog" title="借用物品" width="600px">
|
||||||
<el-form :model="borrowForm" :rules="borrowRules" ref="borrowForm" label-width="100px">
|
<div v-if="selectedItem">
|
||||||
<el-form-item label="使用者" prop="user_name">
|
<h4>{{ selectedItem.name }} ({{ selectedItem.serial_number }})</h4>
|
||||||
<el-input v-model="borrowForm.user_name" placeholder="请输入使用者姓名" />
|
<el-form :model="borrowForm" :rules="borrowRules" ref="borrowFormRef" label-width="120px">
|
||||||
</el-form-item>
|
<el-form-item label="借用人姓名" prop="user_name">
|
||||||
<el-form-item label="联系方式" prop="user_contact">
|
<el-input v-model="borrowForm.user_name" placeholder="请输入借用人姓名" />
|
||||||
<el-input v-model="borrowForm.user_contact" placeholder="请输入联系方式(手机号/QQ/微信/邮箱等)" />
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item label="联系方式" prop="user_contact">
|
||||||
<el-form-item label="使用目的">
|
<el-input v-model="borrowForm.user_contact" placeholder="请输入联系方式" />
|
||||||
<el-input v-model="borrowForm.purpose" />
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item label="使用目的" prop="purpose">
|
||||||
<el-form-item label="使用前状况">
|
<el-input v-model="borrowForm.purpose" placeholder="请输入使用目的" />
|
||||||
<el-input v-model="borrowForm.condition_before" />
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item label="预计归还时间">
|
||||||
<el-form-item label="备注">
|
<el-date-picker
|
||||||
<el-input type="textarea" v-model="borrowForm.notes" />
|
v-model="borrowForm.expected_return_time"
|
||||||
</el-form-item>
|
type="datetime"
|
||||||
</el-form>
|
placeholder="请选择预计归还时间"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="使用前状况">
|
||||||
|
<el-input v-model="borrowForm.condition_before" type="textarea" :rows="2" placeholder="请描述物品当前状况" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="借用时图片">
|
||||||
|
<el-upload
|
||||||
|
ref="borrowUploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:multiple="true"
|
||||||
|
:limit="5"
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
:on-change="handleBorrowImageChange"
|
||||||
|
:on-remove="handleBorrowImageRemove"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<div style="margin-top: 5px;">
|
||||||
|
<el-text type="info" size="small">
|
||||||
|
可上传借用时的物品状态图片(可选,最多5张)
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="borrowForm.notes" type="textarea" :rows="3" placeholder="其他备注信息" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showBorrowDialog = false">取消</el-button>
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" @click="confirmBorrow">确认借用</el-button>
|
<el-button @click="showBorrowDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmBorrow" :loading="borrowing">
|
||||||
|
确认借用
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 归还物品对话框 -->
|
<!-- 归还物品对话框 -->
|
||||||
<el-dialog v-model="showReturnDialog" title="归还物品" width="500px">
|
<el-dialog v-model="showReturnDialog" title="归还物品" width="600px">
|
||||||
<el-form :model="returnForm" :rules="returnRules" ref="returnForm" label-width="100px">
|
<div v-if="selectedItem">
|
||||||
<el-form-item label="使用后状况" prop="condition_after">
|
<h4>{{ selectedItem.name }} ({{ selectedItem.serial_number }})</h4>
|
||||||
<el-input v-model="returnForm.condition_after" placeholder="请描述物品使用后的状况" />
|
<el-form :model="returnForm" ref="returnFormRef" label-width="120px">
|
||||||
</el-form-item>
|
<el-form-item label="使用后状况">
|
||||||
<el-form-item label="归还备注">
|
<el-input v-model="returnForm.condition_after" type="textarea" :rows="2" placeholder="请描述物品归还时的状况" />
|
||||||
<el-input type="textarea" v-model="returnForm.return_notes" placeholder="可填写其他归还说明(可选)" />
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item label="归还时图片">
|
||||||
</el-form>
|
<el-upload
|
||||||
|
ref="returnUploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:multiple="true"
|
||||||
|
:limit="5"
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
:on-change="handleReturnImageChange"
|
||||||
|
:on-remove="handleReturnImageRemove"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<div style="margin-top: 5px;">
|
||||||
|
<el-text type="info" size="small">
|
||||||
|
可上传归还时的物品状态图片(可选,最多5张)
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="归还备注">
|
||||||
|
<el-input v-model="returnForm.return_notes" type="textarea" :rows="3" placeholder="归还时的备注信息" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showReturnDialog = false">取消</el-button>
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" @click="confirmReturn">确认归还</el-button>
|
<el-button @click="showReturnDialog = false">取消</el-button>
|
||||||
</template>
|
<el-button type="primary" @click="confirmReturn" :loading="returning">
|
||||||
</el-dialog>
|
确认归还
|
||||||
|
</el-button>
|
||||||
<!-- 编辑物品对话框 -->
|
</div>
|
||||||
<el-dialog v-model="showEditDialog" title="编辑物品" width="600px">
|
|
||||||
<el-form :model="editForm" label-width="100px" :rules="itemRules" ref="editForm">
|
|
||||||
<el-form-item label="物品名称" prop="name">
|
|
||||||
<el-input v-model="editForm.name" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="序列号" prop="serial_number">
|
|
||||||
<el-input v-model="editForm.serial_number" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="类别" prop="category">
|
|
||||||
<el-input v-model="editForm.category" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="状态">
|
|
||||||
<el-select v-model="editForm.status">
|
|
||||||
<el-option label="可用" value="available" />
|
|
||||||
<el-option label="使用中" value="in_use" />
|
|
||||||
<el-option label="维护中" value="maintenance" />
|
|
||||||
<el-option label="损坏" value="damaged" />
|
|
||||||
<el-option label="丢失" value="lost" />
|
|
||||||
<el-option label="已弃用" value="abandoned" />
|
|
||||||
<el-option label="禁止借用" value="prohibited" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="描述">
|
|
||||||
<el-input type="textarea" v-model="editForm.description" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="位置">
|
|
||||||
<el-input v-model="editForm.location" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="价值">
|
|
||||||
<el-input-number v-model="editForm.value" :precision="2" :min="0" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="购买日期">
|
|
||||||
<el-date-picker v-model="editForm.purchase_date" type="date" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="所有者" prop="owner_id">
|
|
||||||
<el-input v-model="editForm.owner" placeholder="请输入所有者姓名" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showEditDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="updateItem">保存修改</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {itemService, userService} from '../services/api'
|
import {itemService} from '@/services/api'
|
||||||
import {ElMessage} from 'element-plus'
|
import {ElMessage} from 'element-plus'
|
||||||
import AppHeader from '../components/AppHeader.vue'
|
import AppHeader from '../components/AppHeader.vue'
|
||||||
|
import { Plus, Search, Picture } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ItemList',
|
name: 'ItemList',
|
||||||
components: {
|
components: {
|
||||||
AppHeader
|
AppHeader,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Picture
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
users: [],
|
|
||||||
loading: false,
|
loading: false,
|
||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
statusFilter: '',
|
statusFilter: '',
|
||||||
showAddDialog: false,
|
|
||||||
showBorrowDialog: false,
|
showBorrowDialog: false,
|
||||||
showReturnDialog: false,
|
showReturnDialog: false,
|
||||||
showEditDialog: false,
|
selectedItem: null,
|
||||||
currentItem: null,
|
borrowing: false,
|
||||||
newItem: {
|
returning: false,
|
||||||
name: '',
|
borrowFiles: [],
|
||||||
serial_number: '',
|
returnFiles: [],
|
||||||
category: '',
|
|
||||||
description: '',
|
|
||||||
location: '',
|
|
||||||
value: null,
|
|
||||||
purchase_date: null,
|
|
||||||
owner: ''
|
|
||||||
},
|
|
||||||
borrowForm: {
|
borrowForm: {
|
||||||
user_name: '',
|
user_name: '',
|
||||||
user_contact: '',
|
user_contact: '',
|
||||||
purpose: '',
|
purpose: '',
|
||||||
|
expected_return_time: null,
|
||||||
condition_before: '',
|
condition_before: '',
|
||||||
notes: ''
|
notes: ''
|
||||||
},
|
},
|
||||||
@@ -251,29 +249,16 @@ export default {
|
|||||||
condition_after: '',
|
condition_after: '',
|
||||||
return_notes: ''
|
return_notes: ''
|
||||||
},
|
},
|
||||||
editForm: {
|
|
||||||
id: null,
|
|
||||||
name: '',
|
|
||||||
serial_number: '',
|
|
||||||
category: '',
|
|
||||||
status: '',
|
|
||||||
description: '',
|
|
||||||
location: '',
|
|
||||||
value: null,
|
|
||||||
purchase_date: null,
|
|
||||||
owner: ''
|
|
||||||
},
|
|
||||||
itemRules: {
|
|
||||||
name: [{ required: true, message: '请输入物品名称', trigger: 'blur' }],
|
|
||||||
serial_number: [{ required: true, message: '请输入序列号', trigger: 'blur' }],
|
|
||||||
category: [{ required: true, message: '请输入类别', trigger: 'blur' }]
|
|
||||||
},
|
|
||||||
returnRules: {
|
|
||||||
condition_after: [{ required: true, message: '请输入使用后状况', trigger: 'blur' }]
|
|
||||||
},
|
|
||||||
borrowRules: {
|
borrowRules: {
|
||||||
user_name: [{ required: true, message: '请输入使用者姓名', trigger: 'blur' }],
|
user_name: [
|
||||||
user_contact: [{ required: true, message: '请输入联系方式', trigger: 'blur' }]
|
{ required: true, message: '请输入借用人姓名', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
user_contact: [
|
||||||
|
{ required: true, message: '请输入联系方式', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
purpose: [
|
||||||
|
{ required: true, message: '请输入使用目的', trigger: 'blur' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -297,7 +282,6 @@ export default {
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadItems()
|
await this.loadItems()
|
||||||
await this.loadUsers()
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadItems() {
|
async loadItems() {
|
||||||
@@ -312,14 +296,6 @@ export default {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadUsers() {
|
|
||||||
try {
|
|
||||||
const response = await userService.getAllUsers()
|
|
||||||
this.users = response.data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载用户列表失败:', error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
// 搜索逻辑在computed中处理
|
// 搜索逻辑在computed中处理
|
||||||
},
|
},
|
||||||
@@ -329,116 +305,95 @@ export default {
|
|||||||
viewItem(id) {
|
viewItem(id) {
|
||||||
this.$router.push(`/items/${id}`)
|
this.$router.push(`/items/${id}`)
|
||||||
},
|
},
|
||||||
editItem(item) {
|
|
||||||
this.currentItem = item
|
|
||||||
this.editForm = { ...item }
|
|
||||||
this.showEditDialog = true
|
|
||||||
},
|
|
||||||
borrowItem(item) {
|
borrowItem(item) {
|
||||||
this.currentItem = item
|
this.selectedItem = item
|
||||||
this.borrowForm = {
|
this.borrowForm = {
|
||||||
user_name: '',
|
user_name: '',
|
||||||
user_contact: '',
|
user_contact: '',
|
||||||
purpose: '',
|
purpose: '',
|
||||||
|
expected_return_time: null,
|
||||||
condition_before: '',
|
condition_before: '',
|
||||||
notes: ''
|
notes: ''
|
||||||
}
|
}
|
||||||
this.showBorrowDialog = true
|
this.showBorrowDialog = true
|
||||||
},
|
},
|
||||||
returnItem(item) {
|
returnItem(item) {
|
||||||
this.currentItem = item
|
this.selectedItem = item
|
||||||
this.returnForm = {
|
this.returnForm = {
|
||||||
condition_after: '',
|
condition_after: '',
|
||||||
return_notes: ''
|
return_notes: ''
|
||||||
}
|
}
|
||||||
this.showReturnDialog = true
|
this.showReturnDialog = true
|
||||||
},
|
},
|
||||||
async saveItem() {
|
|
||||||
try {
|
|
||||||
await this.$refs.itemForm.validate()
|
|
||||||
|
|
||||||
const itemData = { ...this.newItem }
|
|
||||||
|
|
||||||
// 格式化购买日期
|
|
||||||
if (itemData.purchase_date) {
|
|
||||||
const date = new Date(itemData.purchase_date)
|
|
||||||
itemData.purchase_date = date.getFullYear() + '-' +
|
|
||||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(date.getDate()).padStart(2, '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
await itemService.createItem(itemData)
|
|
||||||
ElMessage.success('物品添加成功')
|
|
||||||
this.showAddDialog = false
|
|
||||||
this.newItem = {
|
|
||||||
name: '',
|
|
||||||
serial_number: '',
|
|
||||||
category: '',
|
|
||||||
description: '',
|
|
||||||
location: '',
|
|
||||||
value: null,
|
|
||||||
purchase_date: null,
|
|
||||||
owner: ''
|
|
||||||
}
|
|
||||||
await this.loadItems()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('添加物品失败:', error)
|
|
||||||
ElMessage.error('添加物品失败')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async confirmBorrow() {
|
async confirmBorrow() {
|
||||||
try {
|
try {
|
||||||
await this.$refs.borrowForm.validate()
|
await this.$refs.borrowFormRef.validate()
|
||||||
|
|
||||||
await itemService.borrowItem(this.currentItem.id, this.borrowForm)
|
this.borrowing = true
|
||||||
|
|
||||||
|
// 创建FormData对象
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
// 添加表单数据
|
||||||
|
Object.keys(this.borrowForm).forEach(key => {
|
||||||
|
if (this.borrowForm[key] !== null && this.borrowForm[key] !== '') {
|
||||||
|
formData.append(key, this.borrowForm[key])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加借用时图片
|
||||||
|
this.borrowFiles.forEach((file, index) => {
|
||||||
|
formData.append('borrow_images', file)
|
||||||
|
formData.append(`borrow_image_descriptions[${index}]`, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
await itemService.borrowItem(this.selectedItem.id, formData)
|
||||||
ElMessage.success('借用成功')
|
ElMessage.success('借用成功')
|
||||||
this.showBorrowDialog = false
|
this.showBorrowDialog = false
|
||||||
|
this.borrowFiles = []
|
||||||
|
this.$refs.borrowUploadRef?.clearFiles()
|
||||||
await this.loadItems()
|
await this.loadItems()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message && error.message.includes('validation')) {
|
if (error.message && error.message.includes('validation')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.error('借用失败:', error)
|
console.error('借用失败:', error)
|
||||||
ElMessage.error('借用失败')
|
ElMessage.error(error.response?.data?.error || '借用失败')
|
||||||
|
} finally {
|
||||||
|
this.borrowing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async confirmReturn() {
|
async confirmReturn() {
|
||||||
try {
|
try {
|
||||||
await this.$refs.returnForm.validate()
|
this.returning = true
|
||||||
|
|
||||||
await itemService.returnItem(this.currentItem.id, this.returnForm)
|
// 创建FormData对象
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
// 添加表单数据
|
||||||
|
Object.keys(this.returnForm).forEach(key => {
|
||||||
|
if (this.returnForm[key] !== null && this.returnForm[key] !== '') {
|
||||||
|
formData.append(key, this.returnForm[key])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加归还时图片
|
||||||
|
this.returnFiles.forEach((file, index) => {
|
||||||
|
formData.append('return_images', file)
|
||||||
|
formData.append(`return_image_descriptions[${index}]`, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
await itemService.returnItem(this.selectedItem.id, formData)
|
||||||
ElMessage.success('归还成功')
|
ElMessage.success('归还成功')
|
||||||
this.showReturnDialog = false
|
this.showReturnDialog = false
|
||||||
|
this.returnFiles = []
|
||||||
|
this.$refs.returnUploadRef?.clearFiles()
|
||||||
await this.loadItems()
|
await this.loadItems()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message && error.message.includes('validation')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error('归还失败:', error)
|
console.error('归还失败:', error)
|
||||||
ElMessage.error('归还失败')
|
ElMessage.error(error.response?.data?.error || '归还失败')
|
||||||
}
|
} finally {
|
||||||
},
|
this.returning = false
|
||||||
async updateItem() {
|
|
||||||
try {
|
|
||||||
await this.$refs.editForm.validate()
|
|
||||||
|
|
||||||
const itemData = { ...this.editForm }
|
|
||||||
|
|
||||||
// 格式化购买日期
|
|
||||||
if (itemData.purchase_date) {
|
|
||||||
const date = new Date(itemData.purchase_date)
|
|
||||||
itemData.purchase_date = date.getFullYear() + '-' +
|
|
||||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(date.getDate()).padStart(2, '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
await itemService.updateItem(itemData.id, itemData)
|
|
||||||
ElMessage.success('物品信息更新成功')
|
|
||||||
this.showEditDialog = false
|
|
||||||
await this.loadItems()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新物品失败:', error)
|
|
||||||
ElMessage.error('更新物品失败')
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getStatusType(status) {
|
getStatusType(status) {
|
||||||
@@ -464,6 +419,18 @@ export default {
|
|||||||
'prohibited': '禁止借用'
|
'prohibited': '禁止借用'
|
||||||
}
|
}
|
||||||
return textMap[status] || '未知'
|
return textMap[status] || '未知'
|
||||||
|
},
|
||||||
|
handleBorrowImageChange(file, fileList) {
|
||||||
|
this.borrowFiles = fileList.map(file => file.raw)
|
||||||
|
},
|
||||||
|
handleBorrowImageRemove(file, fileList) {
|
||||||
|
this.borrowFiles = fileList.map(file => file.raw)
|
||||||
|
},
|
||||||
|
handleReturnImageChange(file, fileList) {
|
||||||
|
this.returnFiles = fileList.map(file => file.raw)
|
||||||
|
},
|
||||||
|
handleReturnImageRemove(file, fileList) {
|
||||||
|
this.returnFiles = fileList.map(file => file.raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,4 +452,16 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-image-placeholder {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user