diff --git a/backend/app/api/leave.py b/backend/app/api/leave.py index 2ff8f187d..3930f027c 100644 --- a/backend/app/api/leave.py +++ b/backend/app/api/leave.py @@ -2,7 +2,10 @@ from __future__ import annotations import logging -from typing import Optional +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session @@ -13,6 +16,13 @@ from app.core.response import success_response, error_response from app.crud import leave as leave_crud from app.crud.leave import total_pages from app.schemas.leave import LeaveCreate, LeaveUpdate +from app.utils.excel_export import ( + EXPORT_MAX_ROWS, + ExportColumn, + ExportValidationError, + export_rows_to_xlsx_response, + filter_export_columns, +) logger = logging.getLogger(__name__) @@ -58,6 +68,79 @@ def list_leaves( }) + +_LEAVE_STATUS_MAP = {"draft": "草稿", "pending": "审批中", "approved": "已通过", "rejected": "已驳回"} + +LEAVE_EXPORT_COLUMNS = [ + ExportColumn(title="请假类型", field="leaveTypeLabel"), + ExportColumn(title="开始日期", field="startDate"), + ExportColumn(title="结束日期", field="endDate"), + ExportColumn(title="天数", field="days"), + ExportColumn(title="请假原因", field="reason"), + ExportColumn(title="状态", field="statusLabel"), + ExportColumn(title="创建时间", field="createTime"), +] + + +class LeaveExportBody(BaseModel): + keyword: Optional[str] = None + status: Optional[str] = None + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + +@router.post("/export") +def export_leaves( + body: LeaveExportBody, + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + """导出请假列表(当前筛选全量,或 ids 已选)。""" + try: + tenant_no = _user.get("tenant_no", "") + user_id = _user.get("user_id") or _user.get("id", "") + items, _ = leave_crud.list_leaves( + db, + page=1, + page_size=EXPORT_MAX_ROWS, + keyword=body.keyword, + status=body.status, + tenant_no=tenant_no, + create_by=user_id, + ids=body.ids, + max_page_size=EXPORT_MAX_ROWS, + user_info=_user, + ) + for item in items: + item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", "")) + item["statusLabel"] = _LEAVE_STATUS_MAP.get(item.get("status", ""), item.get("status", "")) + # map frontend props status/leaveType to label fields when columns requested + requested = None + if body.columns: + requested = [] + for key in body.columns: + if key == "status": + requested.append("statusLabel") + elif key == "leaveType": + requested.append("leaveTypeLabel") + else: + requested.append(key) + cols = filter_export_columns(LEAVE_EXPORT_COLUMNS, requested) + date_str = datetime.now().strftime("%Y%m%d") + return export_rows_to_xlsx_response( + db, + tenant_no=tenant_no or None, + sheet_title="请假列表", + columns=cols, + rows=items, + filename_stem=f"请假列表_{date_str}", + ) + except ExportValidationError as e: + return error_response(400, str(e)) + except Exception as e: + return error_response(500, str(e)) + + @router.post("") def create_leave( body: LeaveCreate, diff --git a/backend/app/crud/leave.py b/backend/app/crud/leave.py index 6939306d2..0c7372c6d 100644 --- a/backend/app/crud/leave.py +++ b/backend/app/crud/leave.py @@ -45,6 +45,8 @@ def list_leaves( status: Optional[str] = None, tenant_no: Optional[str] = None, create_by: Optional[str] = None, + ids: Optional[List[str]] = None, + max_page_size: Optional[int] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: q = db.query(BizLeaveRequest).filter(BizLeaveRequest.del_flag == 0) @@ -54,6 +56,8 @@ def list_leaves( q = q.filter(BizLeaveRequest.status == status) if create_by: q = q.filter(BizLeaveRequest.create_by == create_by) + if ids: + q = q.filter(BizLeaveRequest.id.in_(ids)) kw = (keyword or "").strip() if kw: like = f"%{kw}%" @@ -62,6 +66,9 @@ def list_leaves( from app.core.data_scope import apply_data_scope q = apply_data_scope(q, BizLeaveRequest, user_info, db, tenant_no) total = q.count() + page = max(1, page) + cap = int(max_page_size) if max_page_size else 500 + page_size = min(max(1, page_size), cap) rows = ( q.order_by(BizLeaveRequest.create_time.desc()) .offset((page - 1) * page_size) diff --git a/web-admin/src/api/leave.ts b/web-admin/src/api/leave.ts index e673333d8..9735634bb 100644 --- a/web-admin/src/api/leave.ts +++ b/web-admin/src/api/leave.ts @@ -18,6 +18,14 @@ export function fetchLeaveList(params?: { }) } +export function fetchExportLeaves(data: Record) { + return request.post({ + url: '/web/admin/leave/export', + data, + responseType: 'blob' + }) +} + export function fetchCreateLeave(data: { leaveType: string startDate: string diff --git a/web-admin/src/views/system/leave/index.vue b/web-admin/src/views/system/leave/index.vue index 2fdba5f74..5921f16ab 100644 --- a/web-admin/src/views/system/leave/index.vue +++ b/web-admin/src/views/system/leave/index.vue @@ -75,6 +75,15 @@ 删除 + + import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue' import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' import { Close, Check, RefreshLeft, Search, Plus, Edit, Delete, Promotion, Document } from '@element-plus/icons-vue' import { useDictStore } from '@/store/modules/dict' @@ -316,6 +326,7 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns' } from '@/components/business/system-view-dialog/index.vue' import { fetchLeaveList, + fetchExportLeaves, fetchCreateLeave, fetchUpdateLeave, fetchDeleteLeave, @@ -351,6 +362,20 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns' const filterForm = reactive({ keyword: '', status: '' }) const quickSearchKeyword = ref('') + function getLeaveExportFilterParams() { + const quickKeyword = quickSearchKeyword.value?.trim() + return { + keyword: quickKeyword || filterForm.keyword || undefined, + status: filterForm.status || undefined + } + } + + const leaveExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('leaveTypeLabel')) cols.unshift('leaveTypeLabel') + return cols + }) + const dialogVisible = ref(false) const isEdit = ref(false) const editingId = ref('')