From fabc00c4f31830d03ad6a05acc3980a4c527f221 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Fri, 19 Sep 2025 23:10:54 +0800 Subject: [PATCH] feat: update finance manager and fix some issue --- src/backend/finance/__init__.py | 0 src/backend/finance/admin.py | 30 ++ src/backend/finance/apps.py | 6 + src/backend/finance/management/__init__.py | 1 + .../finance/management/commands/__init__.py | 0 .../management/commands/init_finance_data.py | 55 +++ src/backend/finance/models.py | 80 ++++ src/backend/finance/serializers.py | 48 ++ src/backend/finance/tests.py | 1 + src/backend/finance/urls.py | 13 + src/backend/finance/views.py | 98 ++++ src/backend/item_manager/settings.py | 4 + src/backend/item_manager/urls.py | 6 + src/backend/items/urls.py | 2 +- src/fronted/src/components/AppHeader.vue | 35 +- src/fronted/src/router/index.js | 19 + src/fronted/src/services/api.js | 76 ++- src/fronted/src/views/FinanceDashboard.vue | 441 ++++++++++++++++++ src/fronted/src/views/FinanceRecordDetail.vue | 305 ++++++++++++ src/fronted/src/views/FinanceRecordForm.vue | 432 +++++++++++++++++ src/fronted/src/views/FinanceRecordList.vue | 337 +++++++++++++ src/fronted/src/views/ItemUsage.vue | 5 +- src/fronted/vue.config.js | 10 +- 23 files changed, 1982 insertions(+), 22 deletions(-) create mode 100644 src/backend/finance/__init__.py create mode 100644 src/backend/finance/admin.py create mode 100644 src/backend/finance/apps.py create mode 100644 src/backend/finance/management/__init__.py create mode 100644 src/backend/finance/management/commands/__init__.py create mode 100644 src/backend/finance/management/commands/init_finance_data.py create mode 100644 src/backend/finance/models.py create mode 100644 src/backend/finance/serializers.py create mode 100644 src/backend/finance/tests.py create mode 100644 src/backend/finance/urls.py create mode 100644 src/backend/finance/views.py create mode 100644 src/fronted/src/views/FinanceDashboard.vue create mode 100644 src/fronted/src/views/FinanceRecordDetail.vue create mode 100644 src/fronted/src/views/FinanceRecordForm.vue create mode 100644 src/fronted/src/views/FinanceRecordList.vue diff --git a/src/backend/finance/__init__.py b/src/backend/finance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/finance/admin.py b/src/backend/finance/admin.py new file mode 100644 index 0000000..c84e14b --- /dev/null +++ b/src/backend/finance/admin.py @@ -0,0 +1,30 @@ +from django.contrib import admin +from .models import Department, Category, FinancialRecord, ProofImage + + +@admin.register(Department) +class DepartmentAdmin(admin.ModelAdmin): + list_display = ['name'] + search_fields = ['name'] + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ['name'] + search_fields = ['name'] + + +@admin.register(ProofImage) +class ProofImageAdmin(admin.ModelAdmin): + list_display = ['financial_record', 'image', 'description', 'uploaded_at'] + list_filter = ['uploaded_at', 'financial_record'] + search_fields = ['description', 'financial_record__title'] + readonly_fields = ['uploaded_at'] + + +@admin.register(FinancialRecord) +class FinancialRecordAdmin(admin.ModelAdmin): + list_display = ['title', 'amount', 'record_type', 'transaction_date', 'department', 'category'] + list_filter = ['record_type', 'department', 'category', 'transaction_date'] + search_fields = ['title', 'description'] + date_hierarchy = 'transaction_date' diff --git a/src/backend/finance/apps.py b/src/backend/finance/apps.py new file mode 100644 index 0000000..2fc5a78 --- /dev/null +++ b/src/backend/finance/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FinanceConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "finance" diff --git a/src/backend/finance/management/__init__.py b/src/backend/finance/management/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/backend/finance/management/__init__.py @@ -0,0 +1 @@ + diff --git a/src/backend/finance/management/commands/__init__.py b/src/backend/finance/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/finance/management/commands/init_finance_data.py b/src/backend/finance/management/commands/init_finance_data.py new file mode 100644 index 0000000..93186ab --- /dev/null +++ b/src/backend/finance/management/commands/init_finance_data.py @@ -0,0 +1,55 @@ +from django.core.management.base import BaseCommand +from finance.models import Department, Category + +# 创建:python manage.py init_finance_data + +class Command(BaseCommand): + help = '初始化财务系统的部门和类别数据' + + def handle(self, *args, **options): + # 创建部门 + departments = [ + '爱特工作室本部', + '程序部', + 'Web部', + '游戏部', + 'IOS部', + 'APP部', + 'UI部', + '智能应用部', + 'OpenHarmony部', + 'FOSS部' + ] + + for dept_name in departments: + dept, created = Department.objects.get_or_create(name=dept_name) + if created: + self.stdout.write(self.style.SUCCESS(f'创建部门: {dept_name}')) + else: + self.stdout.write(f'部门已存在: {dept_name}') + + # 创建类别 + categories = [ + '办公用品', + '设备采购', + '软件授权', + '差旅费', + '会议费', + '宣传费用', + '日用品费用', + '维护费用', + '其他支出', + '项目收入', + '服务收入', + '资金交接', + '其他收入' + ] + + for cat_name in categories: + cat, created = Category.objects.get_or_create(name=cat_name) + if created: + self.stdout.write(self.style.SUCCESS(f'创建类别: {cat_name}')) + else: + self.stdout.write(f'类别已存在: {cat_name}') + + self.stdout.write(self.style.SUCCESS('初始化完成!')) diff --git a/src/backend/finance/models.py b/src/backend/finance/models.py new file mode 100644 index 0000000..28c1c63 --- /dev/null +++ b/src/backend/finance/models.py @@ -0,0 +1,80 @@ +from django.db import models +from django.contrib.auth.models import User + + +def proof_image_upload_path(instance, filename): + """ + 自定义文件上传路径 + 格式: proofs/{记录ID}-{年月日}/{文件名} + """ + record = instance.financial_record + date_str = record.transaction_date.strftime('%Y%m%d') + folder_name = f"{record.id}-{date_str}" + return f'proofs/{folder_name}/{filename}' + + +class Department(models.Model): + """部门""" + name = models.CharField(max_length=100, unique=True, verbose_name="部门名称") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "部门" + verbose_name_plural = verbose_name + + +class Category(models.Model): + """财务记录类别""" + name = models.CharField(max_length=100, unique=True, verbose_name="类别名称") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "财务类别" + verbose_name_plural = verbose_name + + +class FinancialRecord(models.Model): + """财务记录""" + RECORD_TYPE_CHOICES = [ + ('expense', '支出'), + ('income', '收入'), + ] + + title = models.CharField(max_length=200, verbose_name="标题") + description = models.TextField(blank=True, null=True, verbose_name="描述") + amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="金额") + record_type = models.CharField(max_length=10, choices=RECORD_TYPE_CHOICES, verbose_name="记录类型") + transaction_date = models.DateField(verbose_name="交易日期") + department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属部门") + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="类别") + fund_manager = models.CharField(max_length=100, blank=True, null=True, verbose_name="批准人") + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="创建人") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + def __str__(self): + return f"{self.title} - {self.amount}" + + class Meta: + verbose_name = "财务记录" + verbose_name_plural = verbose_name + ordering = ['-transaction_date'] + + +class ProofImage(models.Model): + """凭证图片""" + financial_record = models.ForeignKey(FinancialRecord, on_delete=models.CASCADE, related_name='proof_images', verbose_name="财务记录") + image = models.ImageField(upload_to=proof_image_upload_path, verbose_name="凭证图片") + description = models.CharField(max_length=200, blank=True, null=True, verbose_name="图片描述") + uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") + + def __str__(self): + return f"{self.financial_record.title} - 凭证{self.id}" + + class Meta: + verbose_name = "凭证图片" + verbose_name_plural = verbose_name diff --git a/src/backend/finance/serializers.py b/src/backend/finance/serializers.py new file mode 100644 index 0000000..d91f4cb --- /dev/null +++ b/src/backend/finance/serializers.py @@ -0,0 +1,48 @@ +from rest_framework import serializers +from .models import FinancialRecord, Department, Category, ProofImage + + +class DepartmentSerializer(serializers.ModelSerializer): + class Meta: + model = Department + fields = '__all__' + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = '__all__' + + +class ProofImageSerializer(serializers.ModelSerializer): + """凭证图片序列化器""" + class Meta: + model = ProofImage + fields = ['id', 'image', 'description', 'uploaded_at'] + + +class FinancialRecordWriteSerializer(serializers.ModelSerializer): + """用于创建和更新财务记录的序列化器""" + class Meta: + model = FinancialRecord + fields = '__all__' + + +class FinancialRecordReadSerializer(serializers.ModelSerializer): + """用于读取财务记录的序列化器""" + department = DepartmentSerializer(read_only=True) + category = CategorySerializer(read_only=True) + proof_images = ProofImageSerializer(many=True, read_only=True) + + class Meta: + model = FinancialRecord + fields = '__all__' + + +# 保持向后兼容的默认序列化器 +class FinancialRecordSerializer(serializers.ModelSerializer): + proof_images = ProofImageSerializer(many=True, read_only=True) + + class Meta: + model = FinancialRecord + fields = '__all__' diff --git a/src/backend/finance/tests.py b/src/backend/finance/tests.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/backend/finance/tests.py @@ -0,0 +1 @@ + diff --git a/src/backend/finance/urls.py b/src/backend/finance/urls.py new file mode 100644 index 0000000..922cbb4 --- /dev/null +++ b/src/backend/finance/urls.py @@ -0,0 +1,13 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import FinancialRecordViewSet, DepartmentViewSet, CategoryViewSet, ProofImageViewSet + +router = DefaultRouter() +router.register(r'finance', FinancialRecordViewSet) +router.register(r'departments', DepartmentViewSet) +router.register(r'finance_categories', CategoryViewSet) +router.register(r'proof-images', ProofImageViewSet) + +urlpatterns = [ + path('api/', include(router.urls)), +] diff --git a/src/backend/finance/views.py b/src/backend/finance/views.py new file mode 100644 index 0000000..3383d65 --- /dev/null +++ b/src/backend/finance/views.py @@ -0,0 +1,98 @@ +import os +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from .models import FinancialRecord, Department, Category, ProofImage +from .serializers import ( + FinancialRecordWriteSerializer, + FinancialRecordReadSerializer, + DepartmentSerializer, + CategorySerializer, + ProofImageSerializer +) + +class FinancialRecordViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows financial records to be viewed or edited. + """ + queryset = FinancialRecord.objects.all() + + def get_serializer_class(self): + """根据操作类型返回不同的序列化器""" + if self.action in ['list', 'retrieve']: + return FinancialRecordReadSerializer + return FinancialRecordWriteSerializer + + @action(detail=True, methods=['post']) + def upload_images(self, request, pk=None): + """为财务记录上传多张凭证图片""" + record = self.get_object() + files = request.FILES.getlist('images') + + if not files: + return Response({'error': '没有接收到图片文件'}, status=status.HTTP_400_BAD_REQUEST) + + created_images = [] + for file in files: + proof_image = ProofImage.objects.create( + financial_record=record, + image=file, + description=request.data.get('description', '') + ) + created_images.append(ProofImageSerializer(proof_image).data) + + return Response({ + 'message': f'成功上传 {len(created_images)} 张图片', + 'images': created_images + }, status=status.HTTP_201_CREATED) + + +class ProofImageViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows proof images to be viewed or edited. + """ + queryset = ProofImage.objects.all() + serializer_class = ProofImageSerializer + + def destroy(self, request, *args, **kwargs): + """重写删除方法,确保删除图片记录时同时删除物理文件""" + instance = self.get_object() + + # 获取文件路径 + file_path = None + if instance.image: + try: + file_path = instance.image.path + except (ValueError, AttributeError): + # 如果文件路径无效或文件不存在,只删除数据库记录 + pass + + # 删除数据库记录 + super().destroy(request, *args, **kwargs) + + # 删除物理文件 + if file_path and os.path.isfile(file_path): + try: + os.remove(file_path) + print(f"成功删除文件: {file_path}") + except OSError as e: + print(f"删除文件失败: {file_path}, 错误: {e}") + # 即使文件删除失败,也不抛出异常,因为数据库记录已经删除 + + return Response({'message': '图片删除成功'}, status=status.HTTP_200_OK) + + +class DepartmentViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows departments to be viewed or edited. + """ + queryset = Department.objects.all() + serializer_class = DepartmentSerializer + + +class CategoryViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows categories to be viewed or edited. + """ + queryset = Category.objects.all() + serializer_class = CategorySerializer diff --git a/src/backend/item_manager/settings.py b/src/backend/item_manager/settings.py index b37b9dc..71b09b4 100644 --- a/src/backend/item_manager/settings.py +++ b/src/backend/item_manager/settings.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ "rest_framework", "corsheaders", "items", + "finance", ] MIDDLEWARE = [ @@ -141,6 +142,9 @@ USE_TZ = True STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" + # 安全设置(生产环境) SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_SSL_REDIRECT = False # 如果使用HTTPS,设置为True diff --git a/src/backend/item_manager/urls.py b/src/backend/item_manager/urls.py index 12c3fd9..949beb4 100644 --- a/src/backend/item_manager/urls.py +++ b/src/backend/item_manager/urls.py @@ -17,9 +17,15 @@ Including another URLconf from django.contrib import admin from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), path("", include("items.urls")), + path("", include("finance.urls")), path("api-auth/", include("rest_framework.urls")), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/src/backend/items/urls.py b/src/backend/items/urls.py index 53636bd..b2808f0 100644 --- a/src/backend/items/urls.py +++ b/src/backend/items/urls.py @@ -5,7 +5,7 @@ from . import views router = DefaultRouter() router.register(r'items', views.ItemViewSet) router.register(r'usages', views.ItemUsageViewSet) -router.register(r'categories', views.CategoryViewSet) +router.register(r'item_categories', views.CategoryViewSet) router.register(r'users', views.UserViewSet) urlpatterns = [ diff --git a/src/fronted/src/components/AppHeader.vue b/src/fronted/src/components/AppHeader.vue index ba4e312..3a91c05 100644 --- a/src/fronted/src/components/AppHeader.vue +++ b/src/fronted/src/components/AppHeader.vue @@ -1,7 +1,7 @@ @@ -47,8 +64,8 @@ export default { .header-content { display: flex; align-items: center; - justify-content: space-between; width: 100%; + position: relative; } .logo { @@ -56,12 +73,20 @@ export default { font-size: 20px; font-weight: bold; margin: 0; + position: absolute; + left: 0; + z-index: 1; } .nav-menu { border-bottom: none; - width: 60%; background-color: transparent; + width: 100%; + display: flex; + justify-content: center; + position: absolute; + left: 0; + right: 0; } .nav-menu .el-menu-item { diff --git a/src/fronted/src/router/index.js b/src/fronted/src/router/index.js index 845ca7f..1111588 100644 --- a/src/fronted/src/router/index.js +++ b/src/fronted/src/router/index.js @@ -4,6 +4,9 @@ import ItemList from '../views/ItemList.vue' import ItemDetail from '../views/ItemDetail.vue' import ItemUsage from '../views/ItemUsage.vue' import Dashboard from '../views/Dashboard.vue' +import FinanceDashboard from '../views/FinanceDashboard.vue' +import FinanceRecordList from '../views/FinanceRecordList.vue' +import FinanceRecordDetail from '../views/FinanceRecordDetail.vue' const routes = [ { @@ -31,6 +34,22 @@ const routes = [ path: '/usage', name: 'ItemUsage', component: ItemUsage + }, + { + path: '/finance', + name: 'FinanceDashboard', + component: FinanceDashboard + }, + { + path: '/finance/records', + name: 'FinanceRecordList', + component: FinanceRecordList + }, + { + path: '/finance/records/:id', + name: 'FinanceRecordDetail', + component: FinanceRecordDetail, + props: true } ] diff --git a/src/fronted/src/services/api.js b/src/fronted/src/services/api.js index 96eaf2d..9032d15 100644 --- a/src/fronted/src/services/api.js +++ b/src/fronted/src/services/api.js @@ -1,6 +1,8 @@ import axios from 'axios' -const API_BASE_URL = 'http://localhost:8000/api' +export const API_BASE_URL = 'http://localhost:8000/api' + +export const API_BASE_URL_WITHOUT_API = 'http://localhost:8000' const apiClient = axios.create({ baseURL: API_BASE_URL, @@ -109,22 +111,22 @@ export const usageService = { export const categoryService = { // 获取所有类别 getAllCategories() { - return apiClient.get('/categories/') + return apiClient.get('/item_categories/') }, // 创建新类别 createCategory(category) { - return apiClient.post('/categories/', category) + return apiClient.post('/item_categories/', category) }, // 更新类别 updateCategory(id, category) { - return apiClient.put(`/categories/${id}/`, category) + return apiClient.put(`/item_categories/${id}/`, category) }, // 删除类别 deleteCategory(id) { - return apiClient.delete(`/categories/${id}/`) + return apiClient.delete(`/item_categories/${id}/`) } } @@ -135,11 +137,61 @@ export const userService = { } } -// 健康检查API -export const healthService = { - checkBackendConnection() { - return apiClient.get('/items/') - } -} +export const financeService = { + // 获取所有财务记录 + getAllFinanceRecords() { + return apiClient.get('/finance/') + }, -export default apiClient + // 获取单个财务记录 + getFinanceRecord(id) { + return apiClient.get(`/finance/${id}/`) + }, + + // 创建财务记录 + createFinanceRecord(record) { + return apiClient.post('/finance/', record, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 更新财务记录 + updateFinanceRecord(id, record) { + return apiClient.put(`/finance/${id}/`, record, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 删除财务记录 + deleteFinanceRecord(id) { + return apiClient.delete(`/finance/${id}/`) + }, + + // 为财务记录上传多张凭证图片 + uploadImages(recordId, formData) { + return apiClient.post(`/finance/${recordId}/upload_images/`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 删除凭证图片 + deleteProofImage(imageId) { + return apiClient.delete(`/proof-images/${imageId}/`) + }, + + // 获取所有部门 + getAllDepartments() { + return apiClient.get('/departments/') + }, + + // 获取所有类别 + getAllCategories() { + return apiClient.get('/finance_categories/') + }, +} diff --git a/src/fronted/src/views/FinanceDashboard.vue b/src/fronted/src/views/FinanceDashboard.vue new file mode 100644 index 0000000..6c1b7c9 --- /dev/null +++ b/src/fronted/src/views/FinanceDashboard.vue @@ -0,0 +1,441 @@ + + + + + diff --git a/src/fronted/src/views/FinanceRecordDetail.vue b/src/fronted/src/views/FinanceRecordDetail.vue new file mode 100644 index 0000000..c2ad5e9 --- /dev/null +++ b/src/fronted/src/views/FinanceRecordDetail.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/fronted/src/views/FinanceRecordForm.vue b/src/fronted/src/views/FinanceRecordForm.vue new file mode 100644 index 0000000..c4caf98 --- /dev/null +++ b/src/fronted/src/views/FinanceRecordForm.vue @@ -0,0 +1,432 @@ + + + + + diff --git a/src/fronted/src/views/FinanceRecordList.vue b/src/fronted/src/views/FinanceRecordList.vue new file mode 100644 index 0000000..bdd06b9 --- /dev/null +++ b/src/fronted/src/views/FinanceRecordList.vue @@ -0,0 +1,337 @@ + + + + + diff --git a/src/fronted/src/views/ItemUsage.vue b/src/fronted/src/views/ItemUsage.vue index b1e7c21..ca01d2b 100644 --- a/src/fronted/src/views/ItemUsage.vue +++ b/src/fronted/src/views/ItemUsage.vue @@ -2,7 +2,6 @@
-

使用记录

@@ -150,7 +149,7 @@