From dbba755fd8c0d4df481d94a2d64228f64247cf17 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 21 Aug 2026 18:34:22 +0800 Subject: [PATCH] feat(bx): add list Excel export for reimbursement module pages Wire ListExportDropdown on advance/reimburse/payment/expense/view/wallet lists, with matching POST export endpoints. Co-authored-by: Cursor --- backend/app/api/bx/expense_item.py | 71 ++++++- backend/app/api/bx/invoice.py | 49 +++++ backend/app/api/bx/payment.py | 165 +++++++++++++++ backend/app/api/bx/reimburse.py | 189 ++++++++++++++++++ web-admin/src/api/bx/index.ts | 39 ++++ .../src/views/bx/advance/apply/index.vue | 22 ++ .../src/views/bx/advance/approve/index.vue | 44 ++++ .../src/views/bx/advance/issue/index.vue | 41 +++- web-admin/src/views/bx/expense-item/index.vue | 24 +++ .../views/bx/invoice-wallet/admin/index.vue | 11 +- web-admin/src/views/bx/payment/index.vue | 44 ++++ .../src/views/bx/reimburse/apply/index.vue | 22 ++ .../src/views/bx/reimburse/approve/index.vue | 44 ++++ web-admin/src/views/bx/view/all/index.vue | 49 ++++- web-admin/src/views/bx/view/mine/index.vue | 49 ++++- web-admin/src/views/bx/view/todo/index.vue | 49 ++++- 16 files changed, 901 insertions(+), 11 deletions(-) diff --git a/backend/app/api/bx/expense_item.py b/backend/app/api/bx/expense_item.py index 59fca96d9..3bfad5091 100644 --- a/backend/app/api/bx/expense_item.py +++ b/backend/app/api/bx/expense_item.py @@ -1,8 +1,17 @@ -from typing import List +from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel, Field from sqlalchemy.orm import Session +from app.api.bx.export_common import ( + ExportColumn, + ExportValidationError, + BxExportBodyBase, + dump_export_row, + respond_xlsx, + status_label, +) from app.core.bx_auth import BxUserContext, get_bx_current_user from app.core.database import get_db from app.core.tenant_context import get_tenant_no @@ -23,6 +32,66 @@ def get_expense_item_tree( return ApiResponse(data=[ExpenseItemOut.model_validate(i) for i in tree]) + +EXPENSE_ITEM_EXPORT_COLUMNS = [ + ExportColumn(title="项目名称", field="itemName"), + ExportColumn(title="项目编码", field="itemCode"), + ExportColumn(title="层级", field="level"), + ExportColumn(title="子项数", field="childCount"), + ExportColumn(title="排序", field="sortOrder"), + ExportColumn(title="状态", field="status"), +] + + +class ExpenseItemExportBody(BxExportBodyBase): + name: Optional[str] = None + status: Optional[str] = None + + +def _flatten_expense_tree(nodes: list, out: list) -> None: + for n in nodes: + out.append(n) + children = getattr(n, "children", None) or (n.get("children") if isinstance(n, dict) else None) or [] + if children: + _flatten_expense_tree(list(children), out) + + +@router.post("/export") +def export_expense_items( + body: ExpenseItemExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) + tree = ExpenseItemService.get_item_tree(db, tenant_no, user=user) + flat: list = [] + _flatten_expense_tree(list(tree), flat) + outs = [ExpenseItemOut.model_validate(i) for i in flat] + if body.ids: + id_set = {str(i) for i in body.ids} + outs = [o for o in outs if str(o.id) in id_set] + if body.name: + kw = body.name.strip() + outs = [o for o in outs if kw in (o.item_name or "")] + if body.status not in (None, ""): + outs = [o for o in outs if str(o.status) == str(body.status)] + rows = [] + for o in outs: + data = dump_export_row(o, status_fields=()) + data["status"] = status_label(data.get("status")) + # childCount may be computed + if "childCount" not in data: + data["childCount"] = len(getattr(o, "children", None) or []) + rows.append(data) + return respond_xlsx( + db, tenant_no=tenant_no, sheet_title="费用科目", columns=EXPENSE_ITEM_EXPORT_COLUMNS, + rows=rows, filename_stem="费用科目", requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + @router.get("/{item_id}", response_model=ApiResponse[ExpenseItemOut]) def get_expense_item( item_id: str, diff --git a/backend/app/api/bx/invoice.py b/backend/app/api/bx/invoice.py index 47895cd20..394533d57 100644 --- a/backend/app/api/bx/invoice.py +++ b/backend/app/api/bx/invoice.py @@ -5,7 +5,16 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status, Query from sqlalchemy.orm import Session from starlette.datastructures import UploadFile as StarletteUploadFile +from app.api.bx.export_common import ( + EXPORT_MAX_ROWS, + ExportColumn, + ExportValidationError, + BxExportBodyBase, + dump_export_row, + respond_xlsx, +) from app.core.bx_auth import BxUserContext, get_bx_current_user +from app.core.tenant_context import get_tenant_no from app.core.database import get_db from app.schemas.bx.common import ApiResponse, PagedData from app.schemas.bx.invoice import InvoiceOut, LedgerOut, WalletSummaryOut @@ -39,6 +48,46 @@ def list_invoice_wallets( return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)) + +WALLET_EXPORT_COLUMNS = [ + ExportColumn(title="员工", field="ownerUserName"), + ExportColumn(title="总额(元)", field="totalAmount"), + ExportColumn(title="已报销占用(元)", field="usedAmount"), + ExportColumn(title="余额(元)", field="balance"), + ExportColumn(title="发票张数", field="invoiceCount"), +] + + +class WalletExportBody(BxExportBodyBase): + keyword: str = "" + + +@router.post("/invoice-wallets/export") +def export_invoice_wallets( + body: WalletExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) + rows, _ = InvoiceService.list_wallets( + db, user=user, page=1, page_size=EXPORT_MAX_ROWS, keyword=body.keyword or "" + ) + outs = [WalletSummaryOut.model_validate(InvoiceService._summary_to_dict(r)) for r in rows] + if body.ids: + id_set = {str(i) for i in body.ids} + # wallet may key by ownerUserId + outs = [o for o in outs if str(getattr(o, "owner_user_id", None) or getattr(o, "id", "")) in id_set or str(o.model_dump(by_alias=True).get("ownerUserId") or o.model_dump(by_alias=True).get("id") or "") in id_set] + export_rows = [dump_export_row(o, status_fields=()) for o in outs] + return respond_xlsx( + db, tenant_no=tenant_no, sheet_title="发票夹", columns=WALLET_EXPORT_COLUMNS, + rows=export_rows, filename_stem="发票夹汇总", requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + + @router.get("/invoices", response_model=ApiResponse[PagedData[InvoiceOut]]) def list_invoices( page: int = 1, diff --git a/backend/app/api/bx/payment.py b/backend/app/api/bx/payment.py index eabb24eed..57fe9dfb7 100644 --- a/backend/app/api/bx/payment.py +++ b/backend/app/api/bx/payment.py @@ -5,10 +5,20 @@ from typing import Any, List, Optional from urllib.parse import quote from fastapi import APIRouter, Depends, HTTPException, status, Query +from pydantic import Field from fastapi.responses import StreamingResponse from sqlalchemy import or_ from sqlalchemy.orm import Session +from app.api.bx.export_common import ( + EXPORT_MAX_ROWS, + ExportColumn, + ExportValidationError, + BxApplyListExportBody, + dump_export_row, + issue_status_label, + respond_xlsx, +) from app.core.bx_auth import BxUserContext, get_bx_current_user from app.core.database import get_db from app.core.tenant_context import get_tenant_no @@ -251,6 +261,161 @@ def advance_issue_stats( return ApiResponse(data=AdvanceFinanceIssueStatsOut.model_validate(data)) + +PAYMENT_EXPORT_COLUMNS = [ + ExportColumn(title="申请单号", field="applyCode"), + ExportColumn(title="申请时间", field="applyTime"), + ExportColumn(title="申请人", field="applicantName"), + ExportColumn(title="报销主题", field="title"), + ExportColumn(title="金额(元)", field="amount"), + ExportColumn(title="冲抵金额(元)", field="offsetAmount"), + ExportColumn(title="实付金额(元)", field="payAmount"), + ExportColumn(title="处理状态", field="status"), + ExportColumn(title="支付时间", field="payTime"), +] + +ADVANCE_ISSUE_EXPORT_COLUMNS = [ + ExportColumn(title="申请单号", field="applyCode"), + ExportColumn(title="申请时间", field="applyTime"), + ExportColumn(title="申请人", field="applicantName"), + ExportColumn(title="申请原因", field="reason"), + ExportColumn(title="申请金额(元)", field="amount"), + ExportColumn(title="实际发放金额(元)", field="actualAmount"), + ExportColumn(title="冲抵总金额(元)", field="offsetAmount"), + ExportColumn(title="审批通过时间", field="approveTime"), + ExportColumn(title="发放时间", field="issueTime"), + ExportColumn(title="发放人", field="issueUserName"), + ExportColumn(title="发放状态", field="issueStatus"), +] + + +class PaymentExportBody(BxApplyListExportBody): + issue_status: Optional[str] = Field(default=None, alias="issueStatus") + + +@router.post("/pending/export") +def export_pending_payments( + body: BxApplyListExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = _tenant(db, user) + filters = [BxReimburseApply.status == "approved"] + if body.keyword: + filters.append( + or_( + BxReimburseApply.apply_code.contains(body.keyword), + BxReimburseApply.applicant_name.contains(body.keyword), + ) + ) + if body.applicant_id: + filters.append(BxReimburseApply.applicant_id == str(body.applicant_id).strip()) + if body.ids: + filters.append(BxReimburseApply.id.in_([str(i) for i in body.ids])) + append_create_time_filters( + filters, BxReimburseApply, + apply_date=body.apply_date, apply_date_from=body.apply_date_from, apply_date_to=body.apply_date_to, + ) + rows, _ = ReimburseService.get_paged( + db, tenant_no, page=1, page_size=EXPORT_MAX_ROWS, filters=filters, user=user + ) + outs = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows)) + export_rows = [dump_export_row(o) for o in outs] + return respond_xlsx( + db, tenant_no=tenant_no, sheet_title="待付款", columns=PAYMENT_EXPORT_COLUMNS, + rows=export_rows, filename_stem="报销待付款", requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + +@router.post("/processed/export") +def export_processed_payments( + body: BxApplyListExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = _tenant(db, user) + filters = [BxReimburseApply.status.in_(["processed", "paid", "offset"])] + if body.keyword: + filters.append( + or_( + BxReimburseApply.apply_code.contains(body.keyword), + BxReimburseApply.applicant_name.contains(body.keyword), + ) + ) + if body.applicant_id: + filters.append(BxReimburseApply.applicant_id == str(body.applicant_id).strip()) + if body.ids: + filters.append(BxReimburseApply.id.in_([str(i) for i in body.ids])) + append_create_time_filters( + filters, BxReimburseApply, + apply_date=body.apply_date, apply_date_from=body.apply_date_from, apply_date_to=body.apply_date_to, + ) + rows, _ = ReimburseService.get_paged( + db, tenant_no, page=1, page_size=EXPORT_MAX_ROWS, filters=filters, user=user + ) + outs = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows)) + export_rows = [dump_export_row(o) for o in outs] + return respond_xlsx( + db, tenant_no=tenant_no, sheet_title="已付款", columns=PAYMENT_EXPORT_COLUMNS, + rows=export_rows, filename_stem="报销已付款", requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + +@router.post("/advance-issues/export-xlsx") +def export_advance_issues_xlsx( + body: PaymentExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = _tenant(db, user) + filters = [] + issue_status = body.issue_status + if issue_status == "pending": + filters.append(BxAdvanceApply.status == "approved") + filters.append(or_(BxAdvanceApply.issue_status.is_(None), BxAdvanceApply.issue_status != "issued")) + elif issue_status == "issued": + filters.append(BxAdvanceApply.status == "issued") + else: + filters.append(or_(BxAdvanceApply.status == "approved", BxAdvanceApply.status == "issued")) + if body.keyword: + filters.append( + or_( + BxAdvanceApply.apply_code.contains(body.keyword), + BxAdvanceApply.applicant_name.contains(body.keyword), + ) + ) + if body.applicant_id: + filters.append(BxAdvanceApply.applicant_id == str(body.applicant_id).strip()) + if body.ids: + filters.append(BxAdvanceApply.id.in_([str(i) for i in body.ids])) + append_create_time_filters( + filters, BxAdvanceApply, + apply_date=body.apply_date, apply_date_from=body.apply_date_from, apply_date_to=body.apply_date_to, + ) + rows, _ = AdvanceService.get_paged( + db, tenant_no, page=1, page_size=EXPORT_MAX_ROWS, filters=filters, user=user + ) + out_rows = [AdvanceIssueOut.model_validate(r) for r in rows] + export_rows = [] + for o in out_rows: + data = dump_export_row(o, status_fields=()) + data["issueStatus"] = issue_status_label(getattr(o, "issue_status", None) or data.get("issueStatus")) + export_rows.append(data) + return respond_xlsx( + db, tenant_no=tenant_no, sheet_title="备用金发放", columns=ADVANCE_ISSUE_EXPORT_COLUMNS, + rows=export_rows, filename_stem="备用金发放", requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + @router.get("/advance-issues/export") def export_advance_issues( issue_status: Optional[str] = None, diff --git a/backend/app/api/bx/reimburse.py b/backend/app/api/bx/reimburse.py index 1e6af9595..6aaeb1234 100644 --- a/backend/app/api/bx/reimburse.py +++ b/backend/app/api/bx/reimburse.py @@ -8,6 +8,14 @@ from fastapi.responses import StreamingResponse from sqlalchemy import or_ from sqlalchemy.orm import Session +from app.api.bx.export_common import ( + EXPORT_MAX_ROWS, + ExportColumn, + ExportValidationError, + BxApplyListExportBody, + dump_export_row, + respond_xlsx, +) from app.core.bx_auth import BxUserContext, get_bx_current_user from app.core.database import get_db from app.core.tenant_context import get_tenant_no @@ -42,6 +50,54 @@ def _tenant(db: Session, user: BxUserContext) -> str: return get_tenant_no(db, user.to_tenant_user_dict()) +REIMBURSE_EXPORT_COLUMNS = [ + ExportColumn(title="申请单号", field="applyCode"), + ExportColumn(title="申请时间", field="applyTime"), + ExportColumn(title="申请人", field="applicantName"), + ExportColumn(title="报销主题", field="title"), + ExportColumn(title="费用科目", field="expenseName"), + ExportColumn(title="金额(元)", field="amount"), + ExportColumn(title="状态", field="status"), + ExportColumn(title="当前节点", field="currentNode"), +] + + +def _reimburse_list_filters(body: BxApplyListExportBody, db: Session, tenant_no: str) -> list: + filters = [] + if body.status: + filters.append(BxReimburseApply.status == body.status) + if body.keyword: + filters.append( + or_( + BxReimburseApply.apply_code.contains(body.keyword), + BxReimburseApply.applicant_name.contains(body.keyword), + BxReimburseApply.title.contains(body.keyword), + ) + ) + if body.expense_id: + filters.append(BxReimburseApply.expense_id == str(body.expense_id).strip()) + expense_filter = ReimburseService.build_expense_name_condition(db, tenant_no, body.expense_name) + if expense_filter is not None: + filters.append(expense_filter) + if body.applicant_id: + filters.append(BxReimburseApply.applicant_id == str(body.applicant_id).strip()) + if body.ids: + filters.append(BxReimburseApply.id.in_([str(i) for i in body.ids])) + append_create_time_filters( + filters, + BxReimburseApply, + apply_date=body.apply_date, + apply_date_from=body.apply_date_from, + apply_date_to=body.apply_date_to, + ) + return filters + + +def _export_reimburse_rows(db: Session, tenant_no: str, rows) -> list: + out_rows = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows)) + return [dump_export_row(r) for r in out_rows] + + def _build_details_data( db: Session, tenant_no: str, @@ -327,6 +383,139 @@ def approved_by_me( return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)) + +@router.post("/export") +def export_reimburses( + body: BxApplyListExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = _tenant(db, user) + filters = _reimburse_list_filters(body, db, tenant_no) + rows, _ = ReimburseService.get_paged( + db, tenant_no, page=1, page_size=EXPORT_MAX_ROWS, filters=filters, user=user + ) + return respond_xlsx( + db, + tenant_no=tenant_no, + sheet_title="报销申请", + columns=REIMBURSE_EXPORT_COLUMNS, + rows=_export_reimburse_rows(db, tenant_no, rows), + filename_stem="报销申请", + requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + +@router.post("/my/export") +def export_my_reimburses( + body: BxApplyListExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = _tenant(db, user) + filters = [ + or_( + BxReimburseApply.applicant_id == user.id, + BxReimburseApply.create_by == user.id, + ) + ] + filters.extend(_reimburse_list_filters(body, db, tenant_no)) + rows, _ = ReimburseService.get_paged( + db, tenant_no, page=1, page_size=EXPORT_MAX_ROWS, filters=filters, user=user + ) + return respond_xlsx( + db, + tenant_no=tenant_no, + sheet_title="我的报销", + columns=REIMBURSE_EXPORT_COLUMNS, + rows=_export_reimburse_rows(db, tenant_no, rows), + filename_stem="我的报销申请", + requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + +@router.post("/pending-approval/export") +def export_pending_approval_reimburses( + body: BxApplyListExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = _tenant(db, user) + applicant_id = str(body.applicant_id).strip() if body.applicant_id else None + rows, _ = ReimburseService.pending_approval( + db, + user, + tenant_no, + page=1, + page_size=EXPORT_MAX_ROWS, + keyword=body.keyword or "", + applicant_id=applicant_id, + expense_name=body.expense_name, + apply_date=body.apply_date, + apply_date_from=body.apply_date_from, + apply_date_to=body.apply_date_to, + ) + if body.ids: + id_set = {str(i) for i in body.ids} + rows = [r for r in rows if str(r.id) in id_set] + return respond_xlsx( + db, + tenant_no=tenant_no, + sheet_title="报销待审批", + columns=REIMBURSE_EXPORT_COLUMNS, + rows=_export_reimburse_rows(db, tenant_no, rows), + filename_stem="报销待审批", + requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + +@router.post("/approved-by-me/export") +def export_approved_by_me_reimburses( + body: BxApplyListExportBody, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + try: + tenant_no = _tenant(db, user) + applicant_id = str(body.applicant_id).strip() if body.applicant_id else None + rows, _ = ReimburseService.approved_by_me( + db, + user, + tenant_no, + page=1, + page_size=EXPORT_MAX_ROWS, + keyword=body.keyword or "", + applicant_id=applicant_id, + expense_name=body.expense_name, + apply_date=body.apply_date, + apply_date_from=body.apply_date_from, + apply_date_to=body.apply_date_to, + ) + if body.ids: + id_set = {str(i) for i in body.ids} + rows = [r for r in rows if str(r.id) in id_set] + return respond_xlsx( + db, + tenant_no=tenant_no, + sheet_title="报销已审批", + columns=REIMBURSE_EXPORT_COLUMNS, + rows=_export_reimburse_rows(db, tenant_no, rows), + filename_stem="报销已审批", + requested_columns=body.columns, + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + @router.get("/{reimburse_id}", response_model=ApiResponse[ReimburseApplyOut]) def get_reimburse( reimburse_id: str, diff --git a/web-admin/src/api/bx/index.ts b/web-admin/src/api/bx/index.ts index 5977cb330..5b22e40f4 100644 --- a/web-admin/src/api/bx/index.ts +++ b/web-admin/src/api/bx/index.ts @@ -23,6 +23,9 @@ export const expenseItemApi = { }, delete(id: BxId) { return bxRequest.delete(`/expense-items/${id}`) + }, + export(data: Record) { + return bxRequest.post('/expense-items/export', data, { responseType: 'blob' }) } } @@ -63,6 +66,12 @@ export const advanceApi = { my(params: { page: number; pageSize: number; status?: string; keyword?: string } & BxApplyDateFilter) { return bxRequest.get('/advances/my', { params }) }, + exportMy(data: Record) { + return bxRequest.post('/advances/my/export', data, { responseType: 'blob' }) + }, + exportList(data: Record) { + return bxRequest.post('/advances/export', data, { responseType: 'blob' }) + }, myStats() { return bxRequest.get('/advances/my/stats') }, @@ -77,6 +86,9 @@ export const advanceApi = { } & BxApplyDateFilter) { return bxRequest.get('/advances/pending-approval', { params }) }, + exportPendingApproval(data: Record) { + return bxRequest.post('/advances/pending-approval/export', data, { responseType: 'blob' }) + }, pendingApprovalStats() { return bxRequest.get('/advances/pending-approval/stats') }, @@ -88,6 +100,9 @@ export const advanceApi = { } & BxApplyDateFilter) { return bxRequest.get('/advances/approved-by-me', { params }) }, + exportApprovedByMe(data: Record) { + return bxRequest.post('/advances/approved-by-me/export', data, { responseType: 'blob' }) + }, getById(id: BxId) { return bxRequest.get(`/advances/${id}`) }, @@ -135,6 +150,18 @@ export const reimburseApi = { } & BxApplyDateFilter) { return bxRequest.get('/reimburses/', { params }) }, + exportMy(data: Record) { + return bxRequest.post('/reimburses/my/export', data, { responseType: 'blob' }) + }, + exportList(data: Record) { + return bxRequest.post('/reimburses/export', data, { responseType: 'blob' }) + }, + exportPendingApproval(data: Record) { + return bxRequest.post('/reimburses/pending-approval/export', data, { responseType: 'blob' }) + }, + exportApprovedByMe(data: Record) { + return bxRequest.post('/reimburses/approved-by-me/export', data, { responseType: 'blob' }) + }, my(params: { page: number; pageSize: number; status?: string; keyword?: string } & BxApplyDateFilter) { return bxRequest.get('/reimburses/my', { params }) }, @@ -222,6 +249,15 @@ export const paymentApi = { } & BxApplyDateFilter) { return bxRequest.get('/payments/pending', { params }) }, + exportPending(data: Record) { + return bxRequest.post('/payments/pending/export', data, { responseType: 'blob' }) + }, + exportProcessed(data: Record) { + return bxRequest.post('/payments/processed/export', data, { responseType: 'blob' }) + }, + exportAdvanceIssuesXlsx(data: Record) { + return bxRequest.post('/payments/advance-issues/export-xlsx', data, { responseType: 'blob' }) + }, pay(id: BxId, data?: { payRemark?: string; actualAmount?: number }) { return bxRequest.post(`/payments/${id}/pay`, data) }, @@ -297,6 +333,9 @@ export const invoiceWalletApi = { }, list(params: { page: number; pageSize: number; keyword?: string }) { return bxRequest.get('/invoice-wallets', { params }) + }, + export(data: Record) { + return bxRequest.post('/invoice-wallets/export', data, { responseType: 'blob' }) } } diff --git a/web-admin/src/views/bx/advance/apply/index.vue b/web-admin/src/views/bx/advance/apply/index.vue index 13580609c..af5f53c0d 100644 --- a/web-admin/src/views/bx/advance/apply/index.vue +++ b/web-admin/src/views/bx/advance/apply/index.vue @@ -93,6 +93,15 @@ 查看审批流 + + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' @@ -580,6 +590,18 @@ const viewMainKvFields = computed(() => { ] }) const selectedRows = ref([]) + +function getAdvanceApplyExportFilterParams() { + const q = buildSearchQuery() + return { ...q } +} + +const advanceApplyExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + const editingId = ref(null) const flowNodes = ref([]) const flowHistory = ref([]) diff --git a/web-admin/src/views/bx/advance/approve/index.vue b/web-admin/src/views/bx/advance/approve/index.vue index 5fd63c1a8..8d05d2356 100644 --- a/web-admin/src/views/bx/advance/approve/index.vue +++ b/web-admin/src/views/bx/advance/approve/index.vue @@ -68,6 +68,15 @@ 驳回 + + + + import { reactive, ref, computed, onMounted } from 'vue' import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' @@ -597,6 +616,31 @@ const { }) const tabLoaded = reactive({ pending: false, approved: false }) + +function getAdvancePendingExportFilterParams() { + const q: Record = {} + const kw = (pendingQuickSearchKeyword.value.trim() || pendingFilterForm.keyword || '').trim() + if (kw) q.keyword = kw + + buildApplyDateRangeQuery(pendingFilterForm.applyDateRange, q) + return q +} + +function getAdvanceApprovedExportFilterParams() { + const q: Record = {} + const kw = (approvedQuickSearchKeyword.value.trim() || approvedFilterForm.keyword || '').trim() + if (kw) q.keyword = kw + + buildApplyDateRangeQuery(approvedFilterForm.applyDateRange, q) + return q +} + +const advanceApproveExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + const activeTab = ref('pending') const pendingQuickSearchKeyword = ref('') diff --git a/web-admin/src/views/bx/advance/issue/index.vue b/web-admin/src/views/bx/advance/issue/index.vue index 72c73546b..dc975073c 100644 --- a/web-admin/src/views/bx/advance/issue/index.vue +++ b/web-admin/src/views/bx/advance/issue/index.vue @@ -63,9 +63,14 @@ 发放 - - 导出 - + - - 导出 - + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' @@ -744,6 +755,24 @@ useDbInstantSearch( handleSearch ) + +function getAdvanceIssueExportFilterParams() { + const q: Record = { + issueStatus: activeTab.value === 'pending' ? 'pending' : 'issued' + } + const kw = (quickSearchKeyword.value.trim() || filterForm.keyword.trim() || '').trim() + if (kw) q.keyword = kw + if (filterForm.applicantId != null) q.applicantId = filterForm.applicantId + buildApplyDateRangeQuery(filterForm.applyDateRange, q) + return q +} + +const advanceIssueExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + async function handleExport() { loadingMap.export = true try { diff --git a/web-admin/src/views/bx/expense-item/index.vue b/web-admin/src/views/bx/expense-item/index.vue index 740902d04..0ee574f79 100644 --- a/web-admin/src/views/bx/expense-item/index.vue +++ b/web-admin/src/views/bx/expense-item/index.vue @@ -83,6 +83,15 @@ 收起全部 + + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' @@ -433,6 +443,20 @@ const expenseTree = ref([]) const loading = ref(false) const selectedRows = ref([]) +function getExpenseItemExportFilterParams() { + return { + name: appliedSearchForm.name || undefined, + status: appliedSearchForm.status !== '' && appliedSearchForm.status != null ? String(appliedSearchForm.status) : undefined + } +} + +const expenseItemExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('itemName')) cols.unshift('itemName') + return cols +}) + + const formData = reactive({ id: 0 as number, itemName: '', diff --git a/web-admin/src/views/bx/invoice-wallet/admin/index.vue b/web-admin/src/views/bx/invoice-wallet/admin/index.vue index 89c1f3478..f9ca23976 100644 --- a/web-admin/src/views/bx/invoice-wallet/admin/index.vue +++ b/web-admin/src/views/bx/invoice-wallet/admin/index.vue @@ -39,6 +39,14 @@ 代传发票 + @@ -286,7 +294,8 @@ } from '@element-plus/icons-vue' import { ElMessage, ElMessageBox } from 'element-plus' 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 ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import type { StatsRowColumn } from '@/utils/list-column-stats' import BxInvoiceFileSelect from '@/components/bx/base/BxInvoiceFileSelect.vue' diff --git a/web-admin/src/views/bx/payment/index.vue b/web-admin/src/views/bx/payment/index.vue index 46c396486..0699be7ef 100644 --- a/web-admin/src/views/bx/payment/index.vue +++ b/web-admin/src/views/bx/payment/index.vue @@ -67,6 +67,15 @@ 冲抵备用金 + + + + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' @@ -881,6 +900,31 @@ const offsetPayAmount = computed({ } }) + +function getPaymentPendingExportFilterParams() { + const q: Record = {} + const kw = (quickSearchKeyword.value.trim() || filterForm.code.trim() || '').trim() + if (kw) q.keyword = kw + if (filterForm.applicantId != null) q.applicantId = filterForm.applicantId + buildApplyDateRangeQuery(filterForm.applyDateRange, q) + return q +} + +function getPaymentProcessedExportFilterParams() { + const q: Record = {} + const kw = (processedQuickSearchKeyword.value.trim() || processedFilterForm.code.trim() || '').trim() + if (kw) q.keyword = kw + if (processedFilterForm.applicantId != null) q.applicantId = processedFilterForm.applicantId + buildApplyDateRangeQuery(processedFilterForm.applyDateRange, q) + return q +} + +const paymentExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + const activeTab = ref('pending') const tabLoaded = reactive({ pending: true, processed: false }) diff --git a/web-admin/src/views/bx/reimburse/apply/index.vue b/web-admin/src/views/bx/reimburse/apply/index.vue index cc09d627f..1a395adb3 100644 --- a/web-admin/src/views/bx/reimburse/apply/index.vue +++ b/web-admin/src/views/bx/reimburse/apply/index.vue @@ -96,6 +96,15 @@ > 清空列过滤 + + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' @@ -1061,6 +1071,18 @@ const viewProcessKvFields = computed(() => { ] }) const selectedRows = ref([]) + +function getReimburseApplyExportFilterParams() { + const q = buildSearchQuery() + return { ...q } +} + +const reimburseApplyExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + const flowNodes = ref([]) const flowHistory = ref([]) const flowLoading = ref(false) diff --git a/web-admin/src/views/bx/reimburse/approve/index.vue b/web-admin/src/views/bx/reimburse/approve/index.vue index 04b4aaffc..114f62ad0 100644 --- a/web-admin/src/views/bx/reimburse/approve/index.vue +++ b/web-admin/src/views/bx/reimburse/approve/index.vue @@ -95,6 +95,15 @@ 驳回 + + + + import { reactive, ref, computed, onMounted } from 'vue' import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' import { usePersistedListColumns } from '@/composables/usePersistedListColumns' @@ -758,6 +777,31 @@ const { }) const tabLoaded = reactive({ pending: false, approved: false }) + +function getReimbursePendingExportFilterParams() { + const q: Record = {} + const kw = (pendingQuickSearchKeyword.value.trim() || pendingFilterForm.keyword || '').trim() + if (kw) q.keyword = kw + if (pendingFilterForm.expenseName) q.expenseName = pendingFilterForm.expenseName + buildApplyDateRangeQuery(pendingFilterForm.applyDateRange, q) + return q +} + +function getReimburseApprovedExportFilterParams() { + const q: Record = {} + const kw = (approvedQuickSearchKeyword.value.trim() || approvedFilterForm.keyword || '').trim() + if (kw) q.keyword = kw + if (approvedFilterForm.expenseName) q.expenseName = approvedFilterForm.expenseName + buildApplyDateRangeQuery(approvedFilterForm.applyDateRange, q) + return q +} + +const reimburseApproveExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + const activeTab = ref('pending') const menuStore = useMenuStore() diff --git a/web-admin/src/views/bx/view/all/index.vue b/web-admin/src/views/bx/view/all/index.vue index 0cda5d227..ba5063b95 100644 --- a/web-admin/src/views/bx/view/all/index.vue +++ b/web-admin/src/views/bx/view/all/index.vue @@ -82,6 +82,15 @@ @search="handleQuickSearch" @clear="handleQuickSearchClear" /> + + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import type { StatsRowColumn } from '@/utils/list-column-stats' @@ -419,7 +429,44 @@ type DialogKey = 'viewDetail' const loading = ref(false) - const selectedRows = ref([]) + +async function exportBxViewAll(payload: Record) { + const biz = appliedFilter.bizType || filterForm.bizType || '' + const ids: string[] = payload.ids || [] + if (ids.length) { + const selected = selectedRows.value.filter((r: any) => ids.includes(String(r.id))) + const adv = selected.filter((r: any) => r.bizType === '备用金') + const rei = selected.filter((r: any) => r.bizType === '报销单') + if (adv.length && rei.length) { + throw new Error('请选择同一业务类型的记录再导出') + } + if (adv.length) { + return advanceApi.exportList({ ...payload, ids: adv.map((r: any) => String(r.id)) }) + } + return reimburseApi.exportList({ ...payload, ids: rei.map((r: any) => String(r.id)) }) + } + if (biz === '备用金') return advanceApi.exportList(payload) + if (biz === '报销单') return reimburseApi.exportList(payload) + throw new Error('请先筛选业务类型(备用金/报销单)后再导出') +} + +function getBxViewExportFilterParams() { + const q: Record = {} + const kw = (quickSearchKeyword.value.trim() || filterForm.keyword || appliedFilter.keyword || '').trim() + if (kw) q.keyword = kw + const status = filterForm.status || appliedFilter.status + if (status) q.status = status + buildApplyDateRangeQuery(filterForm.applyDateRange || appliedFilter.applyDateRange, q) + return q +} + +const bxViewExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + +const selectedRows = ref([]) const dialogs = reactive>({ viewDetail: false }) diff --git a/web-admin/src/views/bx/view/mine/index.vue b/web-admin/src/views/bx/view/mine/index.vue index af6ff5a3b..ac613a58a 100644 --- a/web-admin/src/views/bx/view/mine/index.vue +++ b/web-admin/src/views/bx/view/mine/index.vue @@ -80,6 +80,15 @@ @search="handleQuickSearch" @clear="handleQuickSearchClear" /> + + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import type { StatsRowColumn } from '@/utils/list-column-stats' @@ -400,7 +410,44 @@ const tableLoading = ref(false) const searchLoading = ref(false) - const selectedRows = ref([]) + +async function exportBxViewMine(payload: Record) { + const biz = appliedFilter.bizType || filterForm.bizType || '' + const ids: string[] = payload.ids || [] + if (ids.length) { + const selected = selectedRows.value.filter((r: any) => ids.includes(String(r.id))) + const adv = selected.filter((r: any) => r.bizType === '备用金') + const rei = selected.filter((r: any) => r.bizType === '报销单') + if (adv.length && rei.length) { + throw new Error('请选择同一业务类型的记录再导出') + } + if (adv.length) { + return advanceApi.exportMy({ ...payload, ids: adv.map((r: any) => String(r.id)) }) + } + return reimburseApi.exportMy({ ...payload, ids: rei.map((r: any) => String(r.id)) }) + } + if (biz === '备用金') return advanceApi.exportMy(payload) + if (biz === '报销单') return reimburseApi.exportMy(payload) + throw new Error('请先筛选业务类型(备用金/报销单)后再导出') +} + +function getBxViewExportFilterParams() { + const q: Record = {} + const kw = (quickSearchKeyword.value.trim() || filterForm.keyword || appliedFilter.keyword || '').trim() + if (kw) q.keyword = kw + const status = filterForm.status || appliedFilter.status + if (status) q.status = status + buildApplyDateRangeQuery(filterForm.applyDateRange || appliedFilter.applyDateRange, q) + return q +} + +const bxViewExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + +const selectedRows = ref([]) const dialogs = reactive>({ viewDetail: false }) diff --git a/web-admin/src/views/bx/view/todo/index.vue b/web-admin/src/views/bx/view/todo/index.vue index da2bfc905..d8ed27624 100644 --- a/web-admin/src/views/bx/view/todo/index.vue +++ b/web-admin/src/views/bx/view/todo/index.vue @@ -96,6 +96,15 @@ 批量驳回 + + import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import type { StatsRowColumn } from '@/utils/list-column-stats' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' @@ -784,7 +794,44 @@ } } - const selectedRows = ref([]) + +async function exportBxViewTodo(payload: Record) { + const biz = appliedFilter.bizType || filterForm.bizType || '' + const ids: string[] = payload.ids || [] + if (ids.length) { + const selected = selectedRows.value.filter((r: any) => ids.includes(String(r.id))) + const adv = selected.filter((r: any) => r.bizType === '备用金') + const rei = selected.filter((r: any) => r.bizType === '报销单') + if (adv.length && rei.length) { + throw new Error('请选择同一业务类型的记录再导出') + } + if (adv.length) { + return advanceApi.exportPendingApproval({ ...payload, ids: adv.map((r: any) => String(r.id)) }) + } + return reimburseApi.exportPendingApproval({ ...payload, ids: rei.map((r: any) => String(r.id)) }) + } + if (biz === '备用金') return advanceApi.exportPendingApproval(payload) + if (biz === '报销单') return reimburseApi.exportPendingApproval(payload) + throw new Error('请先筛选业务类型(备用金/报销单)后再导出') +} + +function getBxViewExportFilterParams() { + const q: Record = {} + const kw = (quickSearchKeyword.value.trim() || filterForm.keyword || appliedFilter.keyword || '').trim() + if (kw) q.keyword = kw + const status = filterForm.status || appliedFilter.status + if (status) q.status = status + buildApplyDateRangeQuery(filterForm.applyDateRange || appliedFilter.applyDateRange, q) + return q +} + +const bxViewExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyCode')) cols.unshift('applyCode') + return cols +}) + +const selectedRows = ref([]) function onSelectionChange(rows: any[]) { selectedRows.value = rows