From 2f7309a45cd0622dde26234c859e559195db25b6 Mon Sep 17 00:00:00 2001 From: jxw Date: Wed, 3 Jun 2026 20:59:39 +0800 Subject: [PATCH] first commit --- CLAUDE.md | 145 + backend/.air.toml | 36 + backend/.dockerignore | 104 + backend/.env.dev | 10 + backend/.env.prod | 13 + backend/.gitignore | 5 + backend/Dockerfile | 27 + backend/Dockerfile.dev | 27 + backend/README_CN.md | 325 + backend/cmd/api/main.go | 95 + backend/configs/config.dev.yaml | 20 + backend/configs/config.prod.yaml | 20 + backend/docker-compose.dev.yml | 64 + backend/docker-compose.yml | 56 + backend/docker-entrypoint.sh | 5 + backend/docs/DEVELOPMENT.md | 432 + backend/go.mod | 61 + backend/go.sum | 141 + backend/internal/dto/error.go | 6 + backend/internal/dto/user.go | 18 + backend/internal/dto/zsp_contract.go | 19 + backend/internal/dto/zsp_contract_detail.go | 50 + backend/internal/dto/zsp_finances.go | 101 + backend/internal/dto/zsp_folder.go | 11 + backend/internal/handler/company_handler.go | 176 + backend/internal/handler/contract_handler.go | 482 + backend/internal/handler/delivery_handler.go | 530 ++ backend/internal/handler/user_handler.go | 76 + .../internal/handler/zsp_contract_handler.go | 539 ++ .../internal/handler/zsp_finances_handler.go | 318 + backend/internal/middleware/auth.go | 48 + backend/internal/model/company.go | 30 + backend/internal/model/contract.go | 25 + backend/internal/model/contract_file.go | 8 + backend/internal/model/contract_folder.go | 33 + backend/internal/model/delivery.go | 115 + backend/internal/model/user.go | 14 + backend/internal/model/zsp_contract.go | 99 + backend/internal/model/zsp_freight_charges.go | 77 + backend/internal/model/zsp_invoice.go | 63 + backend/internal/model/zsp_settlement.go | 42 + .../internal/repository/company_repository.go | 86 + .../repository/contract_batch_repository.go | 65 + .../repository/contract_folder_repository.go | 48 + .../repository/contract_repository.go | 120 + .../repository/delivery_repository.go | 269 + .../internal/repository/user_repository.go | 66 + .../repository/zsp_contract_repository.go | 129 + backend/internal/repository/zsp_finances.go | 182 + backend/internal/server/route.go | 190 + backend/internal/service/company_service.go | 156 + .../service/contract_batch_service.go | 49 + .../service/contract_folder_service.go | 60 + backend/internal/service/contract_service.go | 152 + backend/internal/service/delivery_service.go | 153 + backend/internal/service/user_service.go | 127 + .../internal/service/zsp_contract_service.go | 271 + .../internal/service/zsp_finances_service.go | 335 + backend/pkg/config/config.go | 51 + backend/pkg/database/gorm.go | 30 + backend/pkg/database/init.go | 57 + backend/pkg/database/migrate.go | 39 + backend/pkg/utils/file.go | 45 + backend/readme.md | 366 + backend/scripts/init_database.sql | 11 + backend/test/company_handler_test.go | 496 + backend/test/contract_handler_test.go | 216 + backend/test/delivery_handler_test.go | 375 + backend/test/user_handler_test.go | 177 + backend/test/zsp_contract_handler_test.go | 539 ++ backend/test/zsp_finances_handler_test.go | 676 ++ docs/Git提交规范.md | 114 + docs/api/前端路由文档.md | 70 + docs/api/后端API文档-合同管理.md | 306 + docs/deploy/nginx.conf | 77 + docs/deploy/部署说明.md | 160 + docs/dev/Talk.md | 15 + docs/dev/当前程序不足分析.md | 220 + docs/dev/财务管理-待确认问题.md | 85 + docs/dev/项目开发文档.md | 299 + docs/dev/项目开发要求文档.md | 28 + .../2026-04-25-frontend-api-test-infra.md | 1314 +++ ...26-04-25-frontend-api-test-infra-design.md | 274 + frontend/.browserslistrc | 4 + frontend/.dockerignore | 55 + frontend/.editorconfig | 14 + frontend/.env | 5 + frontend/.env.development | 8 + frontend/.env.production | 17 + frontend/.env.staging | 16 + frontend/.gitignore | 22 + frontend/.husky/commit-msg | 8 + frontend/.husky/common.sh | 9 + frontend/.husky/pre-commit | 10 + frontend/.lintstagedrc | 20 + frontend/.markdownlint.json | 11 + frontend/.npmrc | 4 + frontend/.nvmrc | 1 + frontend/.prettierrc.js | 9 + frontend/.stylelintignore | 4 + frontend/.vscode/extensions.json | 19 + frontend/.vscode/settings.json | 43 + frontend/.vscode/vue3.0.code-snippets | 22 + frontend/.vscode/vue3.2.code-snippets | 17 + frontend/.vscode/vue3.3.code-snippets | 20 + frontend/Dockerfile | 20 + frontend/LICENSE | 21 + frontend/README.en-US.md | 47 + frontend/README.md | 51 + frontend/build/cdn.ts | 55 + frontend/build/compress.ts | 63 + frontend/build/info.ts | 57 + frontend/build/optimize.ts | 29 + frontend/build/plugins.ts | 66 + frontend/build/utils.ts | 113 + frontend/commitlint.config.js | 35 + frontend/coverage/applyDelivery.ts.html | 577 ++ frontend/coverage/base.css | 224 + frontend/coverage/block-navigation.js | 87 + frontend/coverage/company.ts.html | 445 + frontend/coverage/contract.ts.html | 577 ++ frontend/coverage/coverage-final.json | 14 + frontend/coverage/deliveryDetails.ts.html | 772 ++ frontend/coverage/favicon.png | Bin 0 -> 445 bytes frontend/coverage/index.html | 296 + frontend/coverage/inventory.ts.html | 994 ++ frontend/coverage/invoice.ts.html | 721 ++ frontend/coverage/ownershipTransfer.ts.html | 385 + frontend/coverage/prettify.css | 1 + frontend/coverage/prettify.js | 2 + frontend/coverage/reconciliation.ts.html | 388 + frontend/coverage/routes.ts.html | 115 + frontend/coverage/settlement.ts.html | 391 + frontend/coverage/sort-arrow-sprite.png | Bin 0 -> 138 bytes frontend/coverage/sorter.js | 210 + frontend/coverage/user.ts.html | 247 + frontend/coverage/zspContract.ts.html | 766 ++ frontend/coverage/zspFinances.ts.html | 1228 +++ frontend/eslint.config.js | 173 + frontend/index.html | 91 + frontend/mock/applyDelivery.ts | 356 + frontend/mock/asyncRoutes.ts | 69 + frontend/mock/company.ts | 360 + frontend/mock/contract.ts | 162 + frontend/mock/deliveryDetails.ts | 559 ++ frontend/mock/inventory.ts | 864 ++ frontend/mock/login.ts | 42 + frontend/mock/ownershipTransfer.ts | 274 + frontend/mock/refreshToken.ts | 27 + frontend/mock/zhongshangpeng.ts | 265 + frontend/package.json | 168 + frontend/pnpm-lock.yaml | 8142 +++++++++++++++++ frontend/postcss.config.js | 8 + frontend/public/favicon.ico | Bin 0 -> 1270 bytes frontend/public/logo.svg | 1 + frontend/public/platform-config.json | 26 + frontend/src/App.vue | 26 + .../src/api/__tests__/applyDelivery.spec.ts | 221 + frontend/src/api/__tests__/company.spec.ts | 218 + frontend/src/api/__tests__/contract.spec.ts | 390 + .../src/api/__tests__/deliveryDetails.spec.ts | 260 + frontend/src/api/__tests__/inventory.spec.ts | 394 + frontend/src/api/__tests__/invoice.spec.ts | 388 + .../api/__tests__/ownershipTransfer.spec.ts | 186 + .../src/api/__tests__/reconciliation.spec.ts | 235 + frontend/src/api/__tests__/settlement.spec.ts | 234 + frontend/src/api/__tests__/user.spec.ts | 126 + .../src/api/__tests__/zspContract.spec.ts | 420 + .../src/api/__tests__/zspFinances.spec.ts | 517 ++ frontend/src/api/applyDelivery.ts | 164 + frontend/src/api/base.ts | 5 + frontend/src/api/company.ts | 120 + frontend/src/api/contract.ts | 164 + frontend/src/api/deliveryDetails.ts | 229 + frontend/src/api/inventory.ts | 303 + frontend/src/api/invoice.ts | 212 + frontend/src/api/ownershipTransfer.ts | 100 + frontend/src/api/reconciliation.ts | 101 + frontend/src/api/routes.ts | 10 + frontend/src/api/settlement.ts | 102 + frontend/src/api/test-utils.ts | 41 + frontend/src/api/user.ts | 54 + frontend/src/api/utils.ts | 6 + frontend/src/api/zspContract.ts | 227 + frontend/src/api/zspFinances.ts | 381 + frontend/src/assets/iconfont/iconfont.css | 27 + frontend/src/assets/iconfont/iconfont.js | 68 + frontend/src/assets/iconfont/iconfont.json | 30 + frontend/src/assets/iconfont/iconfont.ttf | Bin 0 -> 3904 bytes frontend/src/assets/iconfont/iconfont.woff | Bin 0 -> 2484 bytes frontend/src/assets/iconfont/iconfont.woff2 | Bin 0 -> 2016 bytes frontend/src/assets/login/avatar.svg | 1 + frontend/src/assets/login/bg.png | Bin 0 -> 17468 bytes frontend/src/assets/login/illustration.svg | 1 + frontend/src/assets/status/403.svg | 1 + frontend/src/assets/status/404.svg | 1 + frontend/src/assets/status/500.svg | 1 + frontend/src/assets/svg/back_top.svg | 1 + frontend/src/assets/svg/dark.svg | 1 + frontend/src/assets/svg/day.svg | 1 + frontend/src/assets/svg/enter_outlined.svg | 1 + frontend/src/assets/svg/exit_screen.svg | 1 + frontend/src/assets/svg/full_screen.svg | 1 + frontend/src/assets/svg/keyboard_esc.svg | 1 + frontend/src/assets/svg/system.svg | 1 + frontend/src/assets/table-bar/collapse.svg | 1 + frontend/src/assets/table-bar/drag.svg | 1 + frontend/src/assets/table-bar/expand.svg | 1 + frontend/src/assets/table-bar/refresh.svg | 1 + frontend/src/assets/table-bar/settings.svg | 1 + frontend/src/assets/user.jpg | Bin 0 -> 3694 bytes frontend/src/components/ReAuth/index.ts | 5 + frontend/src/components/ReAuth/src/auth.tsx | 20 + frontend/src/components/ReCol/index.ts | 29 + frontend/src/components/ReDialog/index.ts | 69 + frontend/src/components/ReDialog/index.vue | 206 + frontend/src/components/ReDialog/type.ts | 275 + frontend/src/components/ReIcon/index.ts | 12 + frontend/src/components/ReIcon/src/hooks.ts | 63 + .../src/components/ReIcon/src/iconfont.ts | 47 + .../ReIcon/src/iconifyIconOffline.ts | 47 + .../ReIcon/src/iconifyIconOnline.ts | 31 + .../src/components/ReIcon/src/offlineIcon.ts | 23 + frontend/src/components/ReIcon/src/types.ts | 20 + frontend/src/components/RePerms/index.ts | 5 + frontend/src/components/RePerms/src/perms.tsx | 20 + .../src/components/RePureTableBar/index.ts | 5 + .../src/components/RePureTableBar/src/bar.tsx | 393 + frontend/src/components/ReSegmented/index.ts | 8 + .../src/components/ReSegmented/src/index.css | 156 + .../src/components/ReSegmented/src/index.tsx | 216 + .../src/components/ReSegmented/src/type.ts | 20 + frontend/src/components/ReText/index.ts | 7 + frontend/src/components/ReText/src/index.vue | 69 + .../src/components/business/ConfirmDialog.vue | 110 + .../src/components/business/FormDialog.vue | 331 + .../src/components/business/PageTable.vue | 234 + .../src/components/business/SearchBar.vue | 205 + frontend/src/components/business/index.ts | 48 + frontend/src/config/index.ts | 55 + frontend/src/directives/auth/index.ts | 15 + frontend/src/directives/copy/index.ts | 33 + frontend/src/directives/index.ts | 6 + frontend/src/directives/longpress/index.ts | 63 + frontend/src/directives/optimize/index.ts | 68 + frontend/src/directives/perms/index.ts | 15 + frontend/src/directives/ripple/index.scss | 48 + frontend/src/directives/ripple/index.ts | 229 + .../layout/components/lay-content/index.vue | 215 + .../layout/components/lay-footer/index.vue | 31 + .../src/layout/components/lay-frame/index.vue | 79 + .../layout/components/lay-navbar/index.vue | 135 + .../lay-notice/components/NoticeItem.vue | 177 + .../lay-notice/components/NoticeList.vue | 23 + .../src/layout/components/lay-notice/data.ts | 97 + .../layout/components/lay-notice/index.vue | 96 + .../src/layout/components/lay-panel/index.vue | 145 + .../lay-search/components/SearchFooter.vue | 61 + .../lay-search/components/SearchHistory.vue | 198 + .../components/SearchHistoryItem.vue | 52 + .../lay-search/components/SearchModal.vue | 334 + .../lay-search/components/SearchResult.vue | 113 + .../layout/components/lay-search/index.vue | 21 + .../src/layout/components/lay-search/types.ts | 20 + .../layout/components/lay-setting/index.vue | 631 ++ .../components/lay-sidebar/NavHorizontal.vue | 123 + .../layout/components/lay-sidebar/NavMix.vue | 143 + .../components/lay-sidebar/NavVertical.vue | 137 + .../components/SidebarBreadCrumb.vue | 120 + .../components/SidebarCenterCollapse.vue | 70 + .../components/SidebarExtraIcon.vue | 20 + .../components/SidebarFullScreen.vue | 30 + .../lay-sidebar/components/SidebarItem.vue | 228 + .../components/SidebarLeftCollapse.vue | 69 + .../components/SidebarLinkItem.vue | 32 + .../lay-sidebar/components/SidebarLogo.vue | 72 + .../components/SidebarTopCollapse.vue | 33 + .../lay-tag/components/TagChrome.vue | 33 + .../src/layout/components/lay-tag/index.scss | 371 + .../src/layout/components/lay-tag/index.vue | 684 ++ frontend/src/layout/frame.vue | 91 + frontend/src/layout/hooks/useBoolean.ts | 26 + .../src/layout/hooks/useDataThemeChange.ts | 138 + frontend/src/layout/hooks/useLayout.ts | 58 + frontend/src/layout/hooks/useMultiFrame.ts | 25 + frontend/src/layout/hooks/useNav.ts | 157 + frontend/src/layout/hooks/useTag.ts | 245 + frontend/src/layout/index.vue | 235 + frontend/src/layout/redirect.vue | 24 + frontend/src/layout/types.ts | 92 + frontend/src/main.ts | 64 + frontend/src/plugins/echarts.ts | 44 + frontend/src/plugins/elementPlus.ts | 248 + frontend/src/router/index.ts | 211 + frontend/src/router/modules/changelog.ts | 18 + frontend/src/router/modules/contract.ts | 44 + frontend/src/router/modules/delivery.ts | 54 + frontend/src/router/modules/error.ts | 36 + frontend/src/router/modules/finance.ts | 72 + frontend/src/router/modules/home.ts | 25 + frontend/src/router/modules/overview.ts | 21 + frontend/src/router/modules/purchase.ts | 48 + frontend/src/router/modules/queryReport.ts | 59 + frontend/src/router/modules/remaining.ts | 30 + frontend/src/router/router.md | 86 + frontend/src/router/utils.ts | 434 + frontend/src/store/index.ts | 9 + frontend/src/store/modules/app.ts | 85 + frontend/src/store/modules/epTheme.ts | 49 + frontend/src/store/modules/multiTags.ts | 145 + frontend/src/store/modules/permission.ts | 74 + frontend/src/store/modules/settings.ts | 35 + frontend/src/store/modules/user.ts | 122 + frontend/src/store/types.ts | 47 + frontend/src/store/utils.ts | 28 + frontend/src/style/dark.scss | 182 + frontend/src/style/element-plus.scss | 189 + frontend/src/style/index.scss | 37 + frontend/src/style/login.css | 96 + frontend/src/style/reset.scss | 256 + frontend/src/style/sidebar.scss | 722 ++ frontend/src/style/tailwind.css | 46 + frontend/src/style/theme.scss | 95 + frontend/src/style/transition.scss | 54 + frontend/src/utils/auth.ts | 141 + frontend/src/utils/globalPolyfills.ts | 7 + frontend/src/utils/http/index.ts | 203 + frontend/src/utils/http/types.d.ts | 47 + frontend/src/utils/localforage/index.ts | 109 + frontend/src/utils/localforage/types.d.ts | 166 + frontend/src/utils/message.ts | 89 + frontend/src/utils/mitt.ts | 14 + frontend/src/utils/preventDefault.ts | 28 + frontend/src/utils/print.ts | 223 + frontend/src/utils/progress/index.ts | 17 + frontend/src/utils/propTypes.ts | 39 + frontend/src/utils/responsive.ts | 42 + frontend/src/utils/sso.ts | 59 + frontend/src/utils/tree.ts | 188 + frontend/src/views/error/403.vue | 70 + frontend/src/views/error/404.vue | 70 + frontend/src/views/error/500.vue | 70 + frontend/src/views/login/index.vue | 192 + frontend/src/views/login/register.vue | 96 + frontend/src/views/login/utils/motion.ts | 40 + frontend/src/views/login/utils/rule.ts | 28 + frontend/src/views/login/utils/static.ts | 5 + frontend/src/views/overview/index.vue | 185 + .../src/views/permission/button/index.vue | 99 + .../src/views/permission/button/perms.vue | 109 + frontend/src/views/permission/page/index.vue | 66 + frontend/src/views/system/changelog/index.vue | 134 + .../components/BatchUploadSection.vue | 297 + .../add-contract/components/ContractForm.vue | 177 + .../components/ContractListSection.vue | 419 + .../components/PdfPreviewDialog.vue | 75 + .../contract/add-contract/components/index.ts | 4 + .../contract/add-contract/hooks/index.ts | 4 + .../add-contract/hooks/useBatchUpload.ts | 220 + .../add-contract/hooks/useContractForm.ts | 253 + .../add-contract/hooks/useContractList.ts | 144 + .../add-contract/hooks/useFileUpload.ts | 120 + .../system/contract/add-contract/index.vue | 339 + .../system/contract/add-contract/types.ts | 87 + .../components/ContractDetailsDialog.vue | 124 + .../components/ContractTable.vue | 281 + .../components/DetailFormDialog.vue | 99 + .../components/EditContractDialog.vue | 77 + .../components/FolderFormDialog.vue | 56 + .../components/FolderPanel.vue | 198 + .../components/PdfPreviewDialog.vue | 63 + .../components/SearchPanel.vue | 128 + .../components/UploadDialog.vue | 304 + .../hooks/useContractDetails.ts | 212 + .../hooks/useContractList.ts | 423 + .../business-contract/hooks/useFilePreview.ts | 133 + .../business-contract/hooks/useFolders.ts | 201 + .../business-contract/hooks/useUpload.ts | 193 + .../contract/business-contract/index.vue | 482 + .../contract/business-contract/types.ts | 125 + .../company-info/components/FileTable.vue | 266 + .../components/FolderFormDialog.vue | 78 + .../company-info/components/FolderPanel.vue | 275 + .../company-info/components/UploadDialog.vue | 183 + .../contract/company-info/components/index.ts | 5 + .../contract/company-info/hooks/index.ts | 4 + .../company-info/hooks/useFileList.ts | 330 + .../contract/company-info/hooks/useFolders.ts | 180 + .../contract/company-info/hooks/useUpload.ts | 171 + .../system/contract/company-info/index.vue | 416 + .../system/contract/company-info/types.ts | 123 + .../components/ContractDetailDialog.vue | 320 + .../components/ContractDetailFormDialog.vue | 148 + .../zsp-contract/components/ContractTable.vue | 247 + .../components/EditContractDialog.vue | 87 + .../components/ExcelImportDialog.vue | 130 + .../components/FolderFormDialog.vue | 69 + .../zsp-contract/components/FolderPanel.vue | 207 + .../components/PdfPreviewDialog.vue | 67 + .../zsp-contract/components/SearchBar.vue | 150 + .../zsp-contract/components/UploadDialog.vue | 147 + .../zsp-contract/hooks/useContractDetails.ts | 273 + .../zsp-contract/hooks/useContractList.ts | 306 + .../contract/zsp-contract/hooks/useFolders.ts | 157 + .../contract/zsp-contract/hooks/useUpload.ts | 98 + .../system/contract/zsp-contract/index.vue | 286 + .../system/contract/zsp-contract/types.ts | 138 + .../views/system/delivery/ApplyDelivery.vue | 811 ++ .../views/system/delivery/OtherDelivery.vue | 9 + .../system/delivery/OwnershipTransfer.vue | 742 ++ .../components/DeliverySearchBar.vue | 116 + .../components/DeliveryTable.vue | 347 + .../components/OutboundFormDialog.vue | 437 + .../components/ReceiptConfirmDialog.vue | 137 + .../components/ReceiptHistoryDialog.vue | 136 + .../components/ViewDetailDialog.vue | 237 + .../delivery-details/components/index.ts | 7 + .../delivery/delivery-details/hooks/index.ts | 5 + .../delivery-details/hooks/useActions.ts | 109 + .../delivery-details/hooks/useDeliveryList.ts | 169 + .../delivery-details/hooks/useOutboundForm.ts | 200 + .../hooks/useReceiptConfirm.ts | 177 + .../delivery/delivery-details/index.vue | 272 + .../system/delivery/delivery-details/types.ts | 190 + .../components/InboundFormDialog.vue | 165 + .../components/InboundSearchBar.vue | 135 + .../components/InboundTable.vue | 160 + .../inventory-info/components/StockPanel.vue | 294 + .../inventory-info/components/TabPanel.vue | 87 + .../components/TransferDialog.vue | 181 + .../components/WarehouseCard.vue | 230 + .../components/WarehouseFormDialog.vue | 114 + .../components/WarehousePanel.vue | 140 + .../inventory-info/components/index.ts | 10 + .../delivery/inventory-info/hooks/index.ts | 7 + .../inventory-info/hooks/useInboundForm.ts | 150 + .../inventory-info/hooks/useInboundList.ts | 158 + .../inventory-info/hooks/useSummaryList.ts | 108 + .../inventory-info/hooks/useTransferForm.ts | 177 + .../inventory-info/hooks/useWarehouseForm.ts | 175 + .../inventory-info/hooks/useWarehouseList.ts | 132 + .../system/delivery/inventory-info/index.vue | 580 ++ .../system/delivery/inventory-info/types.ts | 202 + .../views/system/finance/FreightCharges.vue | 485 + frontend/src/views/system/finance/Invoice.vue | 670 ++ .../src/views/system/finance/MiscCharges.vue | 463 + .../views/system/finance/ProfitDetails.vue | 845 ++ .../src/views/system/finance/ServiceFees.vue | 458 + .../src/views/system/finance/Settlement.vue | 689 ++ .../src/views/system/finance/Statement.vue | 635 ++ .../system/purchase/OverseasSupplier.vue | 9 + .../system/purchase/ProcurementBudget.vue | 9 + .../views/system/purchase/PurchaseOrder.vue | 742 ++ .../system/purchase/ShipmentTracking.vue | 9 + .../system/query-report/BusinessDetails.vue | 639 ++ .../system/query-report/ContractExecution.vue | 608 ++ .../system/query-report/OtherDetails.vue | 680 ++ .../system/query-report/OtherReports.vue | 452 + .../system/query-report/ProductBasicInfo.vue | 660 ++ frontend/src/views/welcome/index.vue | 20 + frontend/stylelint.config.js | 88 + frontend/tsconfig.json | 57 + frontend/types/directives.d.ts | 28 + frontend/types/global-components.d.ts | 135 + frontend/types/global.d.ts | 195 + frontend/types/index.d.ts | 80 + frontend/types/router.d.ts | 109 + frontend/types/shims-tsx.d.ts | 24 + frontend/types/shims-vue.d.ts | 11 + frontend/vite.config.ts | 67 + frontend/vitest.config.ts | 29 + scripts/sync-backend.ps1 | 0 scripts/sync-frontend.ps1 | 14 + 473 files changed, 81369 insertions(+) create mode 100755 CLAUDE.md create mode 100755 backend/.air.toml create mode 100755 backend/.dockerignore create mode 100755 backend/.env.dev create mode 100755 backend/.env.prod create mode 100755 backend/.gitignore create mode 100755 backend/Dockerfile create mode 100755 backend/Dockerfile.dev create mode 100755 backend/README_CN.md create mode 100755 backend/cmd/api/main.go create mode 100755 backend/configs/config.dev.yaml create mode 100755 backend/configs/config.prod.yaml create mode 100755 backend/docker-compose.dev.yml create mode 100755 backend/docker-compose.yml create mode 100755 backend/docker-entrypoint.sh create mode 100755 backend/docs/DEVELOPMENT.md create mode 100755 backend/go.mod create mode 100755 backend/go.sum create mode 100755 backend/internal/dto/error.go create mode 100755 backend/internal/dto/user.go create mode 100755 backend/internal/dto/zsp_contract.go create mode 100755 backend/internal/dto/zsp_contract_detail.go create mode 100755 backend/internal/dto/zsp_finances.go create mode 100755 backend/internal/dto/zsp_folder.go create mode 100755 backend/internal/handler/company_handler.go create mode 100755 backend/internal/handler/contract_handler.go create mode 100755 backend/internal/handler/delivery_handler.go create mode 100755 backend/internal/handler/user_handler.go create mode 100755 backend/internal/handler/zsp_contract_handler.go create mode 100755 backend/internal/handler/zsp_finances_handler.go create mode 100755 backend/internal/middleware/auth.go create mode 100755 backend/internal/model/company.go create mode 100755 backend/internal/model/contract.go create mode 100755 backend/internal/model/contract_file.go create mode 100755 backend/internal/model/contract_folder.go create mode 100755 backend/internal/model/delivery.go create mode 100755 backend/internal/model/user.go create mode 100755 backend/internal/model/zsp_contract.go create mode 100755 backend/internal/model/zsp_freight_charges.go create mode 100755 backend/internal/model/zsp_invoice.go create mode 100755 backend/internal/model/zsp_settlement.go create mode 100755 backend/internal/repository/company_repository.go create mode 100755 backend/internal/repository/contract_batch_repository.go create mode 100755 backend/internal/repository/contract_folder_repository.go create mode 100755 backend/internal/repository/contract_repository.go create mode 100755 backend/internal/repository/delivery_repository.go create mode 100755 backend/internal/repository/user_repository.go create mode 100755 backend/internal/repository/zsp_contract_repository.go create mode 100755 backend/internal/repository/zsp_finances.go create mode 100755 backend/internal/server/route.go create mode 100755 backend/internal/service/company_service.go create mode 100755 backend/internal/service/contract_batch_service.go create mode 100755 backend/internal/service/contract_folder_service.go create mode 100755 backend/internal/service/contract_service.go create mode 100755 backend/internal/service/delivery_service.go create mode 100755 backend/internal/service/user_service.go create mode 100755 backend/internal/service/zsp_contract_service.go create mode 100755 backend/internal/service/zsp_finances_service.go create mode 100755 backend/pkg/config/config.go create mode 100755 backend/pkg/database/gorm.go create mode 100755 backend/pkg/database/init.go create mode 100755 backend/pkg/database/migrate.go create mode 100755 backend/pkg/utils/file.go create mode 100755 backend/readme.md create mode 100755 backend/scripts/init_database.sql create mode 100644 backend/test/company_handler_test.go create mode 100755 backend/test/contract_handler_test.go create mode 100755 backend/test/delivery_handler_test.go create mode 100755 backend/test/user_handler_test.go create mode 100644 backend/test/zsp_contract_handler_test.go create mode 100644 backend/test/zsp_finances_handler_test.go create mode 100755 docs/Git提交规范.md create mode 100755 docs/api/前端路由文档.md create mode 100755 docs/api/后端API文档-合同管理.md create mode 100755 docs/deploy/nginx.conf create mode 100755 docs/deploy/部署说明.md create mode 100755 docs/dev/Talk.md create mode 100755 docs/dev/当前程序不足分析.md create mode 100755 docs/dev/财务管理-待确认问题.md create mode 100755 docs/dev/项目开发文档.md create mode 100755 docs/dev/项目开发要求文档.md create mode 100644 docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md create mode 100644 docs/superpowers/specs/2026-04-25-frontend-api-test-infra-design.md create mode 100755 frontend/.browserslistrc create mode 100755 frontend/.dockerignore create mode 100755 frontend/.editorconfig create mode 100755 frontend/.env create mode 100755 frontend/.env.development create mode 100755 frontend/.env.production create mode 100755 frontend/.env.staging create mode 100755 frontend/.gitignore create mode 100755 frontend/.husky/commit-msg create mode 100755 frontend/.husky/common.sh create mode 100755 frontend/.husky/pre-commit create mode 100755 frontend/.lintstagedrc create mode 100755 frontend/.markdownlint.json create mode 100755 frontend/.npmrc create mode 100755 frontend/.nvmrc create mode 100755 frontend/.prettierrc.js create mode 100755 frontend/.stylelintignore create mode 100755 frontend/.vscode/extensions.json create mode 100755 frontend/.vscode/settings.json create mode 100755 frontend/.vscode/vue3.0.code-snippets create mode 100755 frontend/.vscode/vue3.2.code-snippets create mode 100755 frontend/.vscode/vue3.3.code-snippets create mode 100755 frontend/Dockerfile create mode 100755 frontend/LICENSE create mode 100755 frontend/README.en-US.md create mode 100755 frontend/README.md create mode 100755 frontend/build/cdn.ts create mode 100755 frontend/build/compress.ts create mode 100755 frontend/build/info.ts create mode 100755 frontend/build/optimize.ts create mode 100755 frontend/build/plugins.ts create mode 100755 frontend/build/utils.ts create mode 100755 frontend/commitlint.config.js create mode 100644 frontend/coverage/applyDelivery.ts.html create mode 100644 frontend/coverage/base.css create mode 100644 frontend/coverage/block-navigation.js create mode 100644 frontend/coverage/company.ts.html create mode 100644 frontend/coverage/contract.ts.html create mode 100644 frontend/coverage/coverage-final.json create mode 100644 frontend/coverage/deliveryDetails.ts.html create mode 100644 frontend/coverage/favicon.png create mode 100644 frontend/coverage/index.html create mode 100644 frontend/coverage/inventory.ts.html create mode 100644 frontend/coverage/invoice.ts.html create mode 100644 frontend/coverage/ownershipTransfer.ts.html create mode 100644 frontend/coverage/prettify.css create mode 100644 frontend/coverage/prettify.js create mode 100644 frontend/coverage/reconciliation.ts.html create mode 100644 frontend/coverage/routes.ts.html create mode 100644 frontend/coverage/settlement.ts.html create mode 100644 frontend/coverage/sort-arrow-sprite.png create mode 100644 frontend/coverage/sorter.js create mode 100644 frontend/coverage/user.ts.html create mode 100644 frontend/coverage/zspContract.ts.html create mode 100644 frontend/coverage/zspFinances.ts.html create mode 100755 frontend/eslint.config.js create mode 100755 frontend/index.html create mode 100755 frontend/mock/applyDelivery.ts create mode 100755 frontend/mock/asyncRoutes.ts create mode 100755 frontend/mock/company.ts create mode 100755 frontend/mock/contract.ts create mode 100755 frontend/mock/deliveryDetails.ts create mode 100755 frontend/mock/inventory.ts create mode 100755 frontend/mock/login.ts create mode 100755 frontend/mock/ownershipTransfer.ts create mode 100755 frontend/mock/refreshToken.ts create mode 100755 frontend/mock/zhongshangpeng.ts create mode 100755 frontend/package.json create mode 100755 frontend/pnpm-lock.yaml create mode 100755 frontend/postcss.config.js create mode 100755 frontend/public/favicon.ico create mode 100755 frontend/public/logo.svg create mode 100755 frontend/public/platform-config.json create mode 100755 frontend/src/App.vue create mode 100644 frontend/src/api/__tests__/applyDelivery.spec.ts create mode 100644 frontend/src/api/__tests__/company.spec.ts create mode 100644 frontend/src/api/__tests__/contract.spec.ts create mode 100644 frontend/src/api/__tests__/deliveryDetails.spec.ts create mode 100644 frontend/src/api/__tests__/inventory.spec.ts create mode 100644 frontend/src/api/__tests__/invoice.spec.ts create mode 100644 frontend/src/api/__tests__/ownershipTransfer.spec.ts create mode 100644 frontend/src/api/__tests__/reconciliation.spec.ts create mode 100644 frontend/src/api/__tests__/settlement.spec.ts create mode 100644 frontend/src/api/__tests__/user.spec.ts create mode 100644 frontend/src/api/__tests__/zspContract.spec.ts create mode 100644 frontend/src/api/__tests__/zspFinances.spec.ts create mode 100755 frontend/src/api/applyDelivery.ts create mode 100755 frontend/src/api/base.ts create mode 100755 frontend/src/api/company.ts create mode 100755 frontend/src/api/contract.ts create mode 100755 frontend/src/api/deliveryDetails.ts create mode 100755 frontend/src/api/inventory.ts create mode 100755 frontend/src/api/invoice.ts create mode 100755 frontend/src/api/ownershipTransfer.ts create mode 100755 frontend/src/api/reconciliation.ts create mode 100755 frontend/src/api/routes.ts create mode 100755 frontend/src/api/settlement.ts create mode 100644 frontend/src/api/test-utils.ts create mode 100755 frontend/src/api/user.ts create mode 100755 frontend/src/api/utils.ts create mode 100755 frontend/src/api/zspContract.ts create mode 100755 frontend/src/api/zspFinances.ts create mode 100755 frontend/src/assets/iconfont/iconfont.css create mode 100755 frontend/src/assets/iconfont/iconfont.js create mode 100755 frontend/src/assets/iconfont/iconfont.json create mode 100755 frontend/src/assets/iconfont/iconfont.ttf create mode 100755 frontend/src/assets/iconfont/iconfont.woff create mode 100755 frontend/src/assets/iconfont/iconfont.woff2 create mode 100755 frontend/src/assets/login/avatar.svg create mode 100755 frontend/src/assets/login/bg.png create mode 100755 frontend/src/assets/login/illustration.svg create mode 100755 frontend/src/assets/status/403.svg create mode 100755 frontend/src/assets/status/404.svg create mode 100755 frontend/src/assets/status/500.svg create mode 100755 frontend/src/assets/svg/back_top.svg create mode 100755 frontend/src/assets/svg/dark.svg create mode 100755 frontend/src/assets/svg/day.svg create mode 100755 frontend/src/assets/svg/enter_outlined.svg create mode 100755 frontend/src/assets/svg/exit_screen.svg create mode 100755 frontend/src/assets/svg/full_screen.svg create mode 100755 frontend/src/assets/svg/keyboard_esc.svg create mode 100755 frontend/src/assets/svg/system.svg create mode 100755 frontend/src/assets/table-bar/collapse.svg create mode 100755 frontend/src/assets/table-bar/drag.svg create mode 100755 frontend/src/assets/table-bar/expand.svg create mode 100755 frontend/src/assets/table-bar/refresh.svg create mode 100755 frontend/src/assets/table-bar/settings.svg create mode 100755 frontend/src/assets/user.jpg create mode 100755 frontend/src/components/ReAuth/index.ts create mode 100755 frontend/src/components/ReAuth/src/auth.tsx create mode 100755 frontend/src/components/ReCol/index.ts create mode 100755 frontend/src/components/ReDialog/index.ts create mode 100755 frontend/src/components/ReDialog/index.vue create mode 100755 frontend/src/components/ReDialog/type.ts create mode 100755 frontend/src/components/ReIcon/index.ts create mode 100755 frontend/src/components/ReIcon/src/hooks.ts create mode 100755 frontend/src/components/ReIcon/src/iconfont.ts create mode 100755 frontend/src/components/ReIcon/src/iconifyIconOffline.ts create mode 100755 frontend/src/components/ReIcon/src/iconifyIconOnline.ts create mode 100755 frontend/src/components/ReIcon/src/offlineIcon.ts create mode 100755 frontend/src/components/ReIcon/src/types.ts create mode 100755 frontend/src/components/RePerms/index.ts create mode 100755 frontend/src/components/RePerms/src/perms.tsx create mode 100755 frontend/src/components/RePureTableBar/index.ts create mode 100755 frontend/src/components/RePureTableBar/src/bar.tsx create mode 100755 frontend/src/components/ReSegmented/index.ts create mode 100755 frontend/src/components/ReSegmented/src/index.css create mode 100755 frontend/src/components/ReSegmented/src/index.tsx create mode 100755 frontend/src/components/ReSegmented/src/type.ts create mode 100755 frontend/src/components/ReText/index.ts create mode 100755 frontend/src/components/ReText/src/index.vue create mode 100644 frontend/src/components/business/ConfirmDialog.vue create mode 100644 frontend/src/components/business/FormDialog.vue create mode 100644 frontend/src/components/business/PageTable.vue create mode 100644 frontend/src/components/business/SearchBar.vue create mode 100644 frontend/src/components/business/index.ts create mode 100755 frontend/src/config/index.ts create mode 100755 frontend/src/directives/auth/index.ts create mode 100755 frontend/src/directives/copy/index.ts create mode 100755 frontend/src/directives/index.ts create mode 100755 frontend/src/directives/longpress/index.ts create mode 100755 frontend/src/directives/optimize/index.ts create mode 100755 frontend/src/directives/perms/index.ts create mode 100755 frontend/src/directives/ripple/index.scss create mode 100755 frontend/src/directives/ripple/index.ts create mode 100755 frontend/src/layout/components/lay-content/index.vue create mode 100755 frontend/src/layout/components/lay-footer/index.vue create mode 100755 frontend/src/layout/components/lay-frame/index.vue create mode 100755 frontend/src/layout/components/lay-navbar/index.vue create mode 100755 frontend/src/layout/components/lay-notice/components/NoticeItem.vue create mode 100755 frontend/src/layout/components/lay-notice/components/NoticeList.vue create mode 100755 frontend/src/layout/components/lay-notice/data.ts create mode 100755 frontend/src/layout/components/lay-notice/index.vue create mode 100755 frontend/src/layout/components/lay-panel/index.vue create mode 100755 frontend/src/layout/components/lay-search/components/SearchFooter.vue create mode 100755 frontend/src/layout/components/lay-search/components/SearchHistory.vue create mode 100755 frontend/src/layout/components/lay-search/components/SearchHistoryItem.vue create mode 100755 frontend/src/layout/components/lay-search/components/SearchModal.vue create mode 100755 frontend/src/layout/components/lay-search/components/SearchResult.vue create mode 100755 frontend/src/layout/components/lay-search/index.vue create mode 100755 frontend/src/layout/components/lay-search/types.ts create mode 100755 frontend/src/layout/components/lay-setting/index.vue create mode 100755 frontend/src/layout/components/lay-sidebar/NavHorizontal.vue create mode 100755 frontend/src/layout/components/lay-sidebar/NavMix.vue create mode 100755 frontend/src/layout/components/lay-sidebar/NavVertical.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarItem.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarLogo.vue create mode 100755 frontend/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue create mode 100755 frontend/src/layout/components/lay-tag/components/TagChrome.vue create mode 100755 frontend/src/layout/components/lay-tag/index.scss create mode 100755 frontend/src/layout/components/lay-tag/index.vue create mode 100755 frontend/src/layout/frame.vue create mode 100755 frontend/src/layout/hooks/useBoolean.ts create mode 100755 frontend/src/layout/hooks/useDataThemeChange.ts create mode 100755 frontend/src/layout/hooks/useLayout.ts create mode 100755 frontend/src/layout/hooks/useMultiFrame.ts create mode 100755 frontend/src/layout/hooks/useNav.ts create mode 100755 frontend/src/layout/hooks/useTag.ts create mode 100755 frontend/src/layout/index.vue create mode 100755 frontend/src/layout/redirect.vue create mode 100755 frontend/src/layout/types.ts create mode 100755 frontend/src/main.ts create mode 100755 frontend/src/plugins/echarts.ts create mode 100755 frontend/src/plugins/elementPlus.ts create mode 100755 frontend/src/router/index.ts create mode 100755 frontend/src/router/modules/changelog.ts create mode 100755 frontend/src/router/modules/contract.ts create mode 100755 frontend/src/router/modules/delivery.ts create mode 100755 frontend/src/router/modules/error.ts create mode 100755 frontend/src/router/modules/finance.ts create mode 100755 frontend/src/router/modules/home.ts create mode 100755 frontend/src/router/modules/overview.ts create mode 100755 frontend/src/router/modules/purchase.ts create mode 100755 frontend/src/router/modules/queryReport.ts create mode 100755 frontend/src/router/modules/remaining.ts create mode 100755 frontend/src/router/router.md create mode 100755 frontend/src/router/utils.ts create mode 100755 frontend/src/store/index.ts create mode 100755 frontend/src/store/modules/app.ts create mode 100755 frontend/src/store/modules/epTheme.ts create mode 100755 frontend/src/store/modules/multiTags.ts create mode 100755 frontend/src/store/modules/permission.ts create mode 100755 frontend/src/store/modules/settings.ts create mode 100755 frontend/src/store/modules/user.ts create mode 100755 frontend/src/store/types.ts create mode 100755 frontend/src/store/utils.ts create mode 100755 frontend/src/style/dark.scss create mode 100755 frontend/src/style/element-plus.scss create mode 100755 frontend/src/style/index.scss create mode 100755 frontend/src/style/login.css create mode 100755 frontend/src/style/reset.scss create mode 100755 frontend/src/style/sidebar.scss create mode 100755 frontend/src/style/tailwind.css create mode 100755 frontend/src/style/theme.scss create mode 100755 frontend/src/style/transition.scss create mode 100755 frontend/src/utils/auth.ts create mode 100755 frontend/src/utils/globalPolyfills.ts create mode 100755 frontend/src/utils/http/index.ts create mode 100755 frontend/src/utils/http/types.d.ts create mode 100755 frontend/src/utils/localforage/index.ts create mode 100755 frontend/src/utils/localforage/types.d.ts create mode 100755 frontend/src/utils/message.ts create mode 100755 frontend/src/utils/mitt.ts create mode 100755 frontend/src/utils/preventDefault.ts create mode 100755 frontend/src/utils/print.ts create mode 100755 frontend/src/utils/progress/index.ts create mode 100755 frontend/src/utils/propTypes.ts create mode 100755 frontend/src/utils/responsive.ts create mode 100755 frontend/src/utils/sso.ts create mode 100755 frontend/src/utils/tree.ts create mode 100755 frontend/src/views/error/403.vue create mode 100755 frontend/src/views/error/404.vue create mode 100755 frontend/src/views/error/500.vue create mode 100755 frontend/src/views/login/index.vue create mode 100755 frontend/src/views/login/register.vue create mode 100755 frontend/src/views/login/utils/motion.ts create mode 100755 frontend/src/views/login/utils/rule.ts create mode 100755 frontend/src/views/login/utils/static.ts create mode 100755 frontend/src/views/overview/index.vue create mode 100755 frontend/src/views/permission/button/index.vue create mode 100755 frontend/src/views/permission/button/perms.vue create mode 100755 frontend/src/views/permission/page/index.vue create mode 100755 frontend/src/views/system/changelog/index.vue create mode 100644 frontend/src/views/system/contract/add-contract/components/BatchUploadSection.vue create mode 100644 frontend/src/views/system/contract/add-contract/components/ContractForm.vue create mode 100644 frontend/src/views/system/contract/add-contract/components/ContractListSection.vue create mode 100644 frontend/src/views/system/contract/add-contract/components/PdfPreviewDialog.vue create mode 100644 frontend/src/views/system/contract/add-contract/components/index.ts create mode 100644 frontend/src/views/system/contract/add-contract/hooks/index.ts create mode 100644 frontend/src/views/system/contract/add-contract/hooks/useBatchUpload.ts create mode 100644 frontend/src/views/system/contract/add-contract/hooks/useContractForm.ts create mode 100644 frontend/src/views/system/contract/add-contract/hooks/useContractList.ts create mode 100644 frontend/src/views/system/contract/add-contract/hooks/useFileUpload.ts create mode 100644 frontend/src/views/system/contract/add-contract/index.vue create mode 100644 frontend/src/views/system/contract/add-contract/types.ts create mode 100644 frontend/src/views/system/contract/business-contract/components/ContractDetailsDialog.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/ContractTable.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/DetailFormDialog.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/EditContractDialog.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/FolderFormDialog.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/FolderPanel.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/PdfPreviewDialog.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/SearchPanel.vue create mode 100644 frontend/src/views/system/contract/business-contract/components/UploadDialog.vue create mode 100644 frontend/src/views/system/contract/business-contract/hooks/useContractDetails.ts create mode 100644 frontend/src/views/system/contract/business-contract/hooks/useContractList.ts create mode 100644 frontend/src/views/system/contract/business-contract/hooks/useFilePreview.ts create mode 100644 frontend/src/views/system/contract/business-contract/hooks/useFolders.ts create mode 100644 frontend/src/views/system/contract/business-contract/hooks/useUpload.ts create mode 100644 frontend/src/views/system/contract/business-contract/index.vue create mode 100644 frontend/src/views/system/contract/business-contract/types.ts create mode 100644 frontend/src/views/system/contract/company-info/components/FileTable.vue create mode 100644 frontend/src/views/system/contract/company-info/components/FolderFormDialog.vue create mode 100644 frontend/src/views/system/contract/company-info/components/FolderPanel.vue create mode 100644 frontend/src/views/system/contract/company-info/components/UploadDialog.vue create mode 100644 frontend/src/views/system/contract/company-info/components/index.ts create mode 100644 frontend/src/views/system/contract/company-info/hooks/index.ts create mode 100644 frontend/src/views/system/contract/company-info/hooks/useFileList.ts create mode 100644 frontend/src/views/system/contract/company-info/hooks/useFolders.ts create mode 100644 frontend/src/views/system/contract/company-info/hooks/useUpload.ts create mode 100644 frontend/src/views/system/contract/company-info/index.vue create mode 100644 frontend/src/views/system/contract/company-info/types.ts create mode 100644 frontend/src/views/system/contract/zsp-contract/components/ContractDetailDialog.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/ContractDetailFormDialog.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/ContractTable.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/EditContractDialog.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/ExcelImportDialog.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/FolderFormDialog.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/FolderPanel.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/PdfPreviewDialog.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/SearchBar.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/components/UploadDialog.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/hooks/useContractDetails.ts create mode 100644 frontend/src/views/system/contract/zsp-contract/hooks/useContractList.ts create mode 100644 frontend/src/views/system/contract/zsp-contract/hooks/useFolders.ts create mode 100644 frontend/src/views/system/contract/zsp-contract/hooks/useUpload.ts create mode 100644 frontend/src/views/system/contract/zsp-contract/index.vue create mode 100644 frontend/src/views/system/contract/zsp-contract/types.ts create mode 100755 frontend/src/views/system/delivery/ApplyDelivery.vue create mode 100755 frontend/src/views/system/delivery/OtherDelivery.vue create mode 100755 frontend/src/views/system/delivery/OwnershipTransfer.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/components/DeliverySearchBar.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/components/DeliveryTable.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/components/OutboundFormDialog.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/components/ReceiptConfirmDialog.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/components/ReceiptHistoryDialog.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/components/ViewDetailDialog.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/components/index.ts create mode 100644 frontend/src/views/system/delivery/delivery-details/hooks/index.ts create mode 100644 frontend/src/views/system/delivery/delivery-details/hooks/useActions.ts create mode 100644 frontend/src/views/system/delivery/delivery-details/hooks/useDeliveryList.ts create mode 100644 frontend/src/views/system/delivery/delivery-details/hooks/useOutboundForm.ts create mode 100644 frontend/src/views/system/delivery/delivery-details/hooks/useReceiptConfirm.ts create mode 100644 frontend/src/views/system/delivery/delivery-details/index.vue create mode 100644 frontend/src/views/system/delivery/delivery-details/types.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/components/InboundFormDialog.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/InboundSearchBar.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/InboundTable.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/StockPanel.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/TabPanel.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/TransferDialog.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/WarehouseCard.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/WarehouseFormDialog.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/WarehousePanel.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/components/index.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/hooks/index.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/hooks/useInboundForm.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/hooks/useInboundList.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/hooks/useSummaryList.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/hooks/useTransferForm.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/hooks/useWarehouseForm.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/hooks/useWarehouseList.ts create mode 100644 frontend/src/views/system/delivery/inventory-info/index.vue create mode 100644 frontend/src/views/system/delivery/inventory-info/types.ts create mode 100755 frontend/src/views/system/finance/FreightCharges.vue create mode 100755 frontend/src/views/system/finance/Invoice.vue create mode 100755 frontend/src/views/system/finance/MiscCharges.vue create mode 100755 frontend/src/views/system/finance/ProfitDetails.vue create mode 100755 frontend/src/views/system/finance/ServiceFees.vue create mode 100755 frontend/src/views/system/finance/Settlement.vue create mode 100755 frontend/src/views/system/finance/Statement.vue create mode 100755 frontend/src/views/system/purchase/OverseasSupplier.vue create mode 100755 frontend/src/views/system/purchase/ProcurementBudget.vue create mode 100755 frontend/src/views/system/purchase/PurchaseOrder.vue create mode 100755 frontend/src/views/system/purchase/ShipmentTracking.vue create mode 100755 frontend/src/views/system/query-report/BusinessDetails.vue create mode 100755 frontend/src/views/system/query-report/ContractExecution.vue create mode 100755 frontend/src/views/system/query-report/OtherDetails.vue create mode 100755 frontend/src/views/system/query-report/OtherReports.vue create mode 100755 frontend/src/views/system/query-report/ProductBasicInfo.vue create mode 100755 frontend/src/views/welcome/index.vue create mode 100755 frontend/stylelint.config.js create mode 100755 frontend/tsconfig.json create mode 100755 frontend/types/directives.d.ts create mode 100755 frontend/types/global-components.d.ts create mode 100755 frontend/types/global.d.ts create mode 100755 frontend/types/index.d.ts create mode 100755 frontend/types/router.d.ts create mode 100755 frontend/types/shims-tsx.d.ts create mode 100755 frontend/types/shims-vue.d.ts create mode 100755 frontend/vite.config.ts create mode 100644 frontend/vitest.config.ts create mode 100755 scripts/sync-backend.ps1 create mode 100755 scripts/sync-frontend.ps1 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100755 index 0000000..441de8c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +企业供应链合同管理系统 (中尚鹏 / ZSP). Monorepo with a Vue 3 SPA frontend and Go REST API backend, managing contracts, deliveries, inventory, finance, purchasing, and reporting for a supply chain business. + +### 目录结构 + +``` +├── frontend/ # Vue 3 SPA 前端 +├── backend/ # Go + Gin REST API 后端 +├── docs/ # 项目文档 +│ ├── api/ # API 文档 +│ ├── deploy/ # 部署文档 +│ ├── dev/ # 开发文档与需求分析 +│ └── Git提交规范.md # Git 提交规范 +├── scripts/ # 部署脚本 +└── CLAUDE.md # Claude Code 配置文件 +``` + +### 5 大业务模块(21 个子菜单) + +1. **合同管理**(3 个子菜单):业务合同管理、中尚鹏合同管理、公司信息管理 +2. **提货管理**(5 个子菜单):我要提货、提货明细、库存信息、货权转移、其他出库 +3. **财务管理**(7 个子菜单):货代费用、杂费管理、服务费管理、发票管理、结算管理、对账单、利润明细 +4. **采购管理**(4 个子菜单):采购报单、采购运踪明细、国外供应商、采购预算 +5. **查询&报表中心**(5 个子菜单):合同执行情况、营业明细表、其他明细表、商品基础资料、其他报表 + +## Commands + +### Frontend (frontend/) + +```bash +pnpm dev # Start Vite dev server (port 5173, proxies /api to 127.0.0.1:8080) +pnpm build # Production build to frontend/dist/ +pnpm typecheck # tsc --noEmit + vue-tsc --noEmit +pnpm lint:eslint # ESLint check +pnpm lint:prettier # Prettier check +pnpm lint # Run all linters +``` + +### Backend (backend/) + +```bash +go run cmd/api/main.go # Start server (port 8080) +air -c .air.toml # Start with hot-reload +go test ./... # Run all tests +go test ./test/ # Run all handler tests (user + contract + delivery) +go build -o ./tmp/main ./cmd/api/main.go # Build binary +``` + +### Docker + +```bash +docker-compose -f docker-compose.dev.yml up --build # Dev: backend + MySQL + Air +docker-compose --env-file .env.prod up -d # Production +``` + +## Architecture + +### Frontend (Vue 3 + TypeScript + Vite + Pinia + Element Plus) + +Based on [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) Lite Edition (non-i18n). Key directories: + +- `src/api/` — Axios-based API client modules, one file per domain (contract.ts, delivery.ts, finance.ts, etc.) +- `src/assets/` — Static assets (icons, images) +- `src/components/` — Reusable components (ReAuth, ReDialog, ReIcon, etc.) +- `src/config/` — App configuration +- `src/directives/` — Custom Vue directives (auth, copy, ripple, etc.) +- `src/layout/` — App shell with 3 layout modes (vertical/horizontal/mix) +- `src/router/modules/` — Route modules, one per feature domain, loaded dynamically via async routes +- `src/store/modules/` — Pinia stores (user, app, permission, settings, epTheme, multiTags) +- `src/style/` — Global styles (reset, index, sidebar, tailwind) +- `src/utils/` — Utility functions (HTTP client, auth, tree operations, etc.) +- `src/views/` — Page components by feature: login/, system/contract/, delivery/, finance/, purchase/, query-report/ + +### Backend (Go + Gin + GORM + MySQL 8.0) + +Clean layered architecture: + +- `internal/handler/` — HTTP request handling, parameter parsing, response writing +- `internal/service/` — Business logic orchestration +- `internal/repository/` — Data access via GORM queries +- `internal/model/` — GORM model structs with table tags (27 tables via AutoMigrate) +- `internal/dto/` — Request/response DTOs +- `internal/middleware/` — JWT auth middleware (enabled for all routes except /auth/* and static files) +- `internal/server/route.go` — All API route definitions (single source of truth) + +API routes are under `/api/` prefix with per-domain route groups. JWT middleware is applied to all routes except `/auth/register`, `/auth/login`, and static file routes. + +### API Route Groups + +| Group | Prefix | Handler | Description | +|-------|--------|---------|-------------| +| Auth | `/api/auth/register`, `/api/auth/login` | UserHandler | Public: user registration/login | +| Users | `/api/users/:id` | UserHandler | Protected: user info | +| Contract | `/api/contract` | ContractHandler | Business contracts CRUD + folders + batches | +| ZSP Contract | `/api/zsp-contract` | ZSPContractHandler | CRUD + details + Excel import/export | +| Company | `/api/company` | CompanyHandler | Company info management | +| Delivery | `/api/apply-delivery` | DeliveryHandler | Delivery applications | +| Outbound | `/api/delivery-details` | DeliveryHandler | Outbound orders | +| Inventory | `/api/inventory` | DeliveryHandler | Warehouses/inbound/summary | +| Ownership | `/api/ownership-transfer` | DeliveryHandler | Ownership transfer CRUD | +| Finance | `/api/zsp-finances` | ZSPFinancesHandler | Freight/misc/service fees, costs, profits | + +### Key patterns + +- **Backend handler → service → repository** — Each layer calls the next. Handlers are thin; business logic lives in services. Tests use mocked service interfaces. +- **DTOs for API contracts** — Request/response types in `internal/dto/`, not raw models. +- **GORM AutoMigrate** — 27 models auto-migrated on startup; schema changes are additive. +- **Frontend API modules** — Each `src/api/*.ts` file exports typed functions that return Axios promises with the base response wrapper (`Result`). +- **HTTP interceptors** — Auto-attach JWT Authorization header, handle token refresh, NProgress loading bar. +- **Async routes** — Routes are fetched dynamically post-login; each router module exports an array of route configs. +- **3 layout modes** — Adaptive vertical/horizontal/mix layouts with multi-tab + keep-alive caching. +- **No i18n** — All UI text is hardcoded in Chinese. + +## Testing + +- Backend tests live in `backend/test/` and use mocked service interfaces with `httptest.NewRecorder()`. +- Currently 24 tests covering user, contract, and delivery handlers. +- Frontend testing infrastructure is not yet configured. +- Run tests: `cd backend && go test ./test/ -v` + +## Known Issues / Technical Debt + +1. **Dual contract models** — Legacy `Contract` model coexists with new `ZSPContractFile`/`ZSPContractDetail` models. +2. **API URL inconsistency** — Some routes use `/list`, `/create`, `/update/:id` suffixes while others use direct REST patterns. +3. **Test coverage** — User, contract, and delivery handler tests exist (24 tests); finance and ZSP contract handlers still lack tests. +4. **Delivery handler** — Refactored to use service/repository layer (was directly using `*gorm.DB`). The `InventorySummary` method still accesses DB directly through `svc.DB()` for complex aggregate queries. + +## Git Conventions + +- **Commit format**: Conventional Commits with Chinese descriptions. Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`. Scopes: `contract`, `finance`, `delivery`, `inventory`, `invoice`, `api`, `frontend`, `backend`. +- **Branch strategy**: `main` → production, `dev` → development, `feature/xxx` → features, `fix/xxx` → fixes. +- **Husky hooks**: commit-msg (commitlint) + pre-commit (lint-staged). + +## Docs + +Project documentation is in `docs/`: +- `docs/api/` — API documentation and frontend route docs +- `docs/deploy/` — Nginx configuration, Docker deployment, environment requirements +- `docs/dev/` — Requirements analysis, known issues, finance module pending questions +- `docs/Git提交规范.md` — Git commit standards (Conventional Commits) diff --git a/backend/.air.toml b/backend/.air.toml new file mode 100755 index 0000000..aee6de8 --- /dev/null +++ b/backend/.air.toml @@ -0,0 +1,36 @@ +# .air.toml +root = "." +tmp_dir = "tmp" + +[build] +cmd = "go build -o ./tmp/main ./cmd/api/main.go" +bin = "tmp/main" +full_bin = "./tmp/main" +delay = 1000 +exclude_dir = ["assets", "tmp", "vendor", "testdata"] +exclude_file = [] +exclude_regex = ["_test.go"] +exclude_unchanged = false +follow_symlink = false +log = "build-errors.log" +send_interrupt = true +stop_on_error = true + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[log] +time = false + +[misc] +clean_on_exit = true +[watcher] +# 忽略的文件变化 +ignore_list = ["vendor/*", "*.log", ".git/*", ".tmp/*", "node_modules/*"] + +# 设置轮询间隔(适用于 Docker for Mac/Windows) +poll = true +poll_interval = 500 # 毫秒 diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100755 index 0000000..5037247 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,104 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +mysql_data/ +.env.dev \ No newline at end of file diff --git a/backend/.env.dev b/backend/.env.dev new file mode 100755 index 0000000..5d23bd4 --- /dev/null +++ b/backend/.env.dev @@ -0,0 +1,10 @@ +# 开发环境配置 +ENV=development +APP_NAME=myapp-dev +DB_HOST=mysql +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=password +DB_NAME=myapp +# REDIS_HOST=redis +# REDIS_PORT=6379 \ No newline at end of file diff --git a/backend/.env.prod b/backend/.env.prod new file mode 100755 index 0000000..5554fc2 --- /dev/null +++ b/backend/.env.prod @@ -0,0 +1,13 @@ +# 生产环境配置(部署前请修改敏感信息) +ENV=production +APP_NAME=myapp +DB_HOST=mysql +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=password +DB_NAME=myapp + +# Docker Compose 变量(用于 MySQL 等服务的密码替换) +MYSQL_ROOT_PASSWORD=password +MYSQL_DATABASE=myapp +MYSQL_PASSWORD=password diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100755 index 0000000..ad23e6a --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,5 @@ +*.exe +/bin/ +/.github/ +/.continue/ +/data/ \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100755 index 0000000..aa9efa0 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,27 @@ +# 构建阶段 +FROM golang:1.25.4-alpine3.22 AS builder +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go env -w GO111MODULE=on +RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/api + +# 运行阶段 +FROM alpine:latest +WORKDIR /app + +RUN apk add --no-cache ca-certificates tzdata +ENV TZ=Asia/Shanghai + +COPY --from=builder /app/main . +COPY --from=builder /app/configs ./configs + +# 数据目录(上传文件等),运行时挂载 volume +RUN mkdir -p data/contract/uploads data/contract/batch_archive data/ownership data/zsp-contract/files + +EXPOSE 8080 +ENTRYPOINT ["/app/main"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100755 index 0000000..bbc4a37 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,27 @@ +# Dockerfile.dev +FROM golang:1.25.4-alpine3.22 + +# 安装必要的工具 +RUN apk add --no-cache git curl bash + +# 安装 air(Go 1.21+ 版本) +RUN go install github.com/air-verse/air@latest + +# 设置工作目录 - 注意这里用 /workspace,不是 /app +WORKDIR /workspace + +# 复制依赖文件 +COPY go.mod go.sum ./ + +# 下载依赖到 GOPATH +RUN go mod download + +# 暴露端口 +EXPOSE 8080 + +# 设置环境变量让 go 命令能找到依赖 +ENV GOPATH=/go +ENV PATH=$PATH:/go/bin + +# 使用 air 启动应用 +CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/backend/README_CN.md b/backend/README_CN.md new file mode 100755 index 0000000..0d208e8 --- /dev/null +++ b/backend/README_CN.md @@ -0,0 +1,325 @@ +# ZSP 后端管理系统 + +基于 Go + Gin + GORM + MySQL 构建的现代化合同管理与用户认证系统后端API。 + +## 📋 项目简介 + +ZSP 后端是一个用于供应链合同管理的企业级应用系统,提供用户认证、合同管理、文件存储等核心功能。系统采用微服务架构思想,具备高可扩展性和良好的开发体验。 + +### 主要特性 +- 🔐 **JWT 用户认证与授权** +- 📄 **合同全生命周期管理** +- 🐳 **Docker 容器化开发环境** +- 🔄 **热重载开发体验** (Air) +- 📊 **MySQL 数据库支持** +- 🧪 **自动化数据库迁移** +- 🔧 **配置驱动架构** + +## 🏗️ 技术栈 + +### 后端框架 +- **Go 1.25.4** - 高性能编程语言 +- **Gin** - 轻量级 Web 框架 +- **GORM** - ORM 数据库工具 +- **JWT** - 用户认证令牌 + +### 开发工具 +- **Docker & Docker Compose** - 容器化开发环境 +- **Air** - Go 应用热重载工具 +- **MySQL 8.0** - 关系型数据库 + +### 配置管理 +- **Viper** - 配置管理库 +- **YAML 配置文件** - 结构化配置 + +## 📁 项目结构 + +``` +zsp-backend/ +├── cmd/api/ # 应用入口 +│ └── main.go # 主程序入口 +├── configs/ # 配置文件 +│ └── config.dev.yaml # 开发环境配置 +├── internal/ # 内部包 +│ ├── handler/ # HTTP 处理器 +│ │ ├── user_handler.go # 用户相关接口 +│ │ └── contract_handler.go # 合同相关接口 +│ ├── middleware/ # 中间件 +│ │ └── auth.go # 认证中间件 +│ ├── model/ # 数据模型 +│ │ ├── user.go # 用户模型 +│ │ ├── contract.go # 合同模型 +│ │ └── error.go # 错误响应模型 +│ ├── repository/ # 数据访问层 +│ │ ├── user_repository.go +│ │ └── contract_repository.go +│ ├── service/ # 业务逻辑层 +│ │ ├── user_service.go +│ │ └── contract_service.go +│ └── server/ # 服务器配置 +│ └── router.go # 路由配置 +├── pkg/ # 公共包 +│ ├── config/ # 配置加载 +│ └── database/ # 数据库连接 +├── scripts/ # 脚本文件 +│ └── init_database.sql # 数据库初始化脚本 +├── migrations/ # 数据库迁移文件 +├── test/ # 测试文件 +├── web/ # 前端文件(可选) +├── deployments/ # 部署配置 +├── docs/ # 文档 +├── tmp/ # 临时文件 +└── bin/ # 构建输出 +``` + +## 🚀 快速开始 + +### 前提条件 +- Docker & Docker Compose +- Go 1.25.4+ (可选,用于本地开发) + +### 使用 Docker 开发环境(推荐) + +1. **克隆项目** + ```bash + git clone <仓库地址> + cd zsp-backend + ``` + +2. **启动开发环境** + ```bash + docker-compose -f docker-compose.dev.yml up --build + ``` + +3. **访问应用** + - API 服务: http://localhost:8080 + - MySQL 数据库: localhost:3306 + +### 本地开发(不使用 Docker) + +1. **安装依赖** + ```bash + go mod download + ``` + +2. **启动 MySQL 数据库** + ```bash + # 使用 Docker 启动 MySQL + docker run --name mysql-dev -e MYSQL_ROOT_PASSWORD=password \ + -e MYSQL_DATABASE=myapp -p 3306:3306 -d mysql:8.0 + ``` + +3. **运行应用** + ```bash + # 使用 Air 热重载 + air -c .air.toml + + # 或直接运行 + go run cmd/api/main.go + ``` + +## 🔧 配置说明 + +### 配置文件 (`configs/config.dev.yaml`) +```yaml +server: + port: 8080 + mode: "debug" + read_timeout: 30 + write_timeout: 30 + +database: + host: "mysql" + port: 3306 + user: "root" + password: "password" + dbname: "myapp" + max_open_conns: 100 + max_idle_conns: 10 + reset_database: true + +jwt: + secret: "your-jwt-secret-key" + expire_hours: 24 +``` + +## 📖 API 接口文档 + +### 用户认证 + +#### 用户注册 +```http +POST /api/v1/users/register +Content-Type: application/json + +{ + "username": "testuser", + "email": "test@example.com", + "password": "password123" +} +``` + +#### 用户登录 +```http +POST /api/v1/users/login +Content-Type: application/json + +{ + "email": "test@example.com", + "password": "password123" +} +``` + +响应: +```json +{ + "message": "Login successful", + "user": { + "id": 1, + "username": "testuser", + "email": "test@example.com" + }, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +### 合同管理 + +#### 创建合同 +```http +POST /api/v1/contracts +Authorization: Bearer +Content-Type: application/json + +{ + "name": "采购合同", + "contract_type": "采购", + "contract_code": "CG2024001", + "buy_party": "买方公司", + "sell_party": "卖方公司", + "count_party": "计数方", + "unit_price": "1000.00", + "bl_number": "BL123456", + "delivery_time": "2024-12-31", + "pickup_time": "2024-12-30", + "packaging": "标准包装", + "remarks": "备注信息", + "file_path": "/uploads/contract.pdf" +} +``` + +#### 获取合同列表 +```http +GET /api/v1/contracts +Authorization: Bearer +``` + +## 🗄️ 数据模型 + +### 用户表 (users) +| 字段 | 类型 | 描述 | +|------|------|------| +| id | uint | 主键 | +| username | string(50) | 用户名,唯一 | +| email | string(100) | 邮箱,唯一 | +| password | string(255) | 密码哈希 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +### 合同表 (contracts) +| 字段 | 类型 | 描述 | +|------|------|------| +| id | uint | 主键 | +| name | string(100) | 合同名称 | +| contract_type | string(50) | 合同类型 | +| contract_code | string(50) | 合同编码 | +| buy_party | string(100) | 买方 | +| sell_party | string(100) | 卖方 | +| count_party | string(100) | 计数方 | +| unit_price | string(50) | 单价 | +| bl_number | string(50) | 提单号 | +| delivery_time | string(50) | 交付时间 | +| pickup_time | string(50) | 提货时间 | +| packaging | string(50) | 包装方式 | +| remarks | string(255) | 备注 | +| file_path | string(255) | 文件路径 | + +## 🐳 Docker 开发环境 + +### 开发环境配置 (`docker-compose.dev.yml`) +- **应用服务**: Go + Air 热重载 +- **数据库服务**: MySQL 8.0 +- **网络配置**: 自定义网络 `app-network` +- **卷挂载**: 代码实时同步,Go 模块缓存 + +## 🧪 测试 + +### 运行测试 +```bash +# 运行所有测试 +go test ./... + +# 运行特定包测试 +go test ./internal/handler + +# 运行测试并显示覆盖率 +go test -cover ./... +``` + +## 🔄 数据库迁移 + +### 自动迁移 +在开发环境中,设置 `reset_database: true` 会自动执行数据库迁移。 + +### 手动迁移 +1. 创建迁移文件在 `migrations/` 目录 +2. 使用 GORM 的 `AutoMigrate` 或手动执行 SQL + +## 📊 部署 + +### 生产环境构建 +```bash +# 构建生产镜像 +docker build -t zsp-backend-prod . + +# 使用生产配置 +docker run -p 8080:8080 \ + -v /path/to/config.yaml:/app/config.yaml \ + zsp-backend-prod +``` + +## 🛠️ 开发工具 + +### Air 热重载配置 (`.air.toml`) +- 自动检测文件变化并重新编译 +- 排除测试文件和构建目录 +- 自定义构建命令和输出目录 + +### 代码规范 +- 使用 `go fmt` 格式化代码 +- 遵循 Go 官方代码规范 +- 使用有意义的包和函数命名 + +## 🤝 贡献指南 + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/新功能`) +3. 提交更改 (`git commit -m '添加新功能'`) +4. 推送到分支 (`git push origin feature/新功能`) +5. 创建 Pull Request + +## 📄 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 + +## 📞 支持 + +如有问题或建议,请: +1. 查看 Issues +2. 提交新的 Issue +3. 联系项目维护者 + +--- + +**最后更新**: 2024年1月 +**版本**: 1.0.0-dev \ No newline at end of file diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go new file mode 100755 index 0000000..74951ee --- /dev/null +++ b/backend/cmd/api/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "log" + "os" + "os/signal" + "strconv" + "syscall" + + "github.com/rovina/zsp-backend/internal/handler" + "github.com/rovina/zsp-backend/internal/middleware" + "github.com/rovina/zsp-backend/internal/repository" + "github.com/rovina/zsp-backend/internal/server" + "github.com/rovina/zsp-backend/internal/service" + "github.com/rovina/zsp-backend/pkg/config" + "github.com/rovina/zsp-backend/pkg/database" +) + +func main() { + configPath := os.Getenv("CONFIG_PATH") + if configPath == "" { + configPath = "./configs/config.dev.yaml" + } + cfg, err := config.Load(configPath) + if err != nil { + log.Fatal(err) + } + + // 自动创建数据库 + err = database.EnsureDatabase(&cfg.Database) + if err != nil { + log.Fatalf("create db failed: %v", err) + } + + // 连接数据库 + db, err := database.NewDB(&cfg.Database) + if err != nil { + log.Fatal(err) + } + + // 自动建表 + err = database.AutoMigrate(db) + if err != nil { + log.Fatalf("migrate failed: %v", err) + } + + userRepo := repository.NewUserRepository(db) + contractRepo := repository.NewContractRepository(db) + contractFolderRepo := repository.NewContractFolderRepository(db) + contractBatchRepo := repository.NewContractBatchRepository(db) + zspContractRepo := repository.NewZSPContractRepository(db) + userService, err := service.NewUserService(userRepo, cfg.JWT.Secret) + if err != nil { + log.Fatalf("error in userService Create: %s", err.Error()) + return + } + contractService, err := service.NewContractService(contractRepo) + if err != nil { + log.Fatalf("error in contractService Create: %s", err.Error()) + return + } + contractFolderService := service.NewContractFolderService(contractFolderRepo) + contractBatchService := service.NewContractBatchService(contractBatchRepo) + zspContractService, err := service.NewZSPContractService(zspContractRepo) + if err != nil { + log.Fatalf("error in zspContractService Create: %s", err.Error()) + return + } + zspFinancesService := service.NewZSPFinancesService(db) + companyRepo := repository.NewCompanyRepository(db) + companyService := service.NewCompanyService(companyRepo) + + userHandler := handler.NewUserHandler(userService) + contractHandler := handler.NewContractHandler(contractService, contractFolderService, contractBatchService) + zspContractHandler := handler.NewZSPContractHandler(zspContractService) + zspFinancesHandler := handler.NewZSPFinancesHandler(zspFinancesService) + companyHandler := handler.NewCompanyHandler(companyService) + deliveryRepo := repository.NewDeliveryRepository(db) + deliveryService := service.NewDeliveryService(deliveryRepo) + deliveryHandler := handler.NewDeliveryHandler(deliveryService) + authMiddleware := middleware.AuthMiddleware(cfg.JWT.Secret) + + router := server.SetupRouter(userHandler, contractHandler, zspContractHandler, zspFinancesHandler, companyHandler, deliveryHandler, authMiddleware) + + go func() { + log.Printf("Server starting on port %d", cfg.Server.Port) + if err := router.Run(":" + strconv.Itoa(cfg.Server.Port)); err != nil { + log.Fatalf("Failed to start server: %v", err) + } + }() + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") +} diff --git a/backend/configs/config.dev.yaml b/backend/configs/config.dev.yaml new file mode 100755 index 0000000..f468df6 --- /dev/null +++ b/backend/configs/config.dev.yaml @@ -0,0 +1,20 @@ +# configs/config.yaml +server: + port: 8080 + mode: "debug" + read_timeout: 30 + write_timeout: 30 + +database: + host: "mysql" + port: 3306 + user: "root" + password: "password" + dbname: "myapp" + max_open_conns: 100 + max_idle_conns: 10 + reset_database: false + +jwt: + secret: "2Jkh4BdL6TVURttUEXlMeyD7rCLCkVvwdzPnddEIBRs=" + expire_hours: 24 \ No newline at end of file diff --git a/backend/configs/config.prod.yaml b/backend/configs/config.prod.yaml new file mode 100755 index 0000000..720395c --- /dev/null +++ b/backend/configs/config.prod.yaml @@ -0,0 +1,20 @@ +# 生产环境配置 +server: + port: 8080 + mode: "release" + read_timeout: 30 + write_timeout: 30 + +database: + host: "mysql" + port: 3306 + user: "root" + password: "password" + dbname: "myapp" + max_open_conns: 100 + max_idle_conns: 10 + reset_database: false # 生产环境务必关闭,否则重启会清空所有数据 + +jwt: + secret: "2Jkh4BdL6TVURttUEXlMeyD7rCLCkVvwdzPnddEIBRs=" # 生产环境请更换为强密钥 + expire_hours: 24 diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml new file mode 100755 index 0000000..fd93bbe --- /dev/null +++ b/backend/docker-compose.dev.yml @@ -0,0 +1,64 @@ +# 开发环境部署(支持热重载) +# 启动方式: docker-compose -f docker-compose.dev.yml up +version: '3.8' +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + ports: + - "8080:8080" + depends_on: + mysql: + condition: service_healthy + volumes: + # 关键:将宿主机代码挂载到 /workspace,而不是 /app + - .:/workspace + # 缓存 Go 模块到宿主机,避免重复下载 + - go-mod-cache-dev:/go/pkg/mod + - go-build-cache-dev:/root/.cache/go-build + working_dir: /workspace # 设置容器工作目录 + environment: + - GIN_MODE=debug + - GOFLAGS=-mod=readonly + + + # 设置用户为 root 避免权限问题(开发环境) + user: root + # 保持容器运行 + stdin_open: true + tty: true + networks: + - app-network + env_file: + - ./.env.dev + + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: myapp + MYSQL_PASSWORD: password + ports: + - "15782:3306" + volumes: + - mysql_data_dev:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"] + timeout: 20s + retries: 10 + start_period: 30s + networks: + - app-network + +volumes: + mysql_data_dev: + driver: local + go-mod-cache-dev: + driver: local + go-build-cache-dev: + driver: local + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100755 index 0000000..5794bd6 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,56 @@ +# 生产环境部署 +# 启动方式: docker-compose --env-file .env.prod up -d +version: '3.8' +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + depends_on: + mysql: + condition: service_healthy + working_dir: /app + environment: + - GIN_MODE=release + - CONFIG_PATH=/app/configs/config.prod.yaml + volumes: + # 持久化上传文件、合同等业务数据 + - app_data:/app/data + - ./configs:/app/configs + - ./docker-entrypoint.sh:/docker-entrypoint.sh + stdin_open: true + tty: true + networks: + - app-network + env_file: + - ./.env.prod + + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password} + MYSQL_DATABASE: ${MYSQL_DATABASE:-myapp} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-password} + ports: + - "15782:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-password}"] + timeout: 20s + retries: 10 + start_period: 30s + networks: + - app-network + +volumes: + mysql_data: + driver: local + app_data: + driver: local + +networks: + app-network: + driver: bridge diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh new file mode 100755 index 0000000..706f1c3 --- /dev/null +++ b/backend/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +# 确保 data 目录(volume 挂载点)对 appuser 可写 +chown -R appuser:appuser /app/data 2>/dev/null || true +exec su-exec appuser /app/main "$@" diff --git a/backend/docs/DEVELOPMENT.md b/backend/docs/DEVELOPMENT.md new file mode 100755 index 0000000..2aa0de7 --- /dev/null +++ b/backend/docs/DEVELOPMENT.md @@ -0,0 +1,432 @@ +# 开发指南 + +本文档提供 ZSP 后端系统的详细开发说明。 + +## 开发环境设置 + +### 1. 环境准备 + +#### 使用 Docker(推荐) +```bash +# 1. 克隆项目 +git clone +cd zsp-backend + +# 2. 复制环境变量文件 +cp .env.dev.example .env.dev + +# 3. 启动开发环境 +docker-compose -f docker-compose.dev.yml up --build + +# 4. 查看日志 +docker-compose -f docker-compose.dev.yml logs -f app +``` + +#### 本地开发 +```bash +# 1. 安装 Go 1.25.4+ +# 2. 安装 Air +go install github.com/air-verse/air@latest + +# 3. 启动 MySQL +docker run --name mysql-dev \ + -e MYSQL_ROOT_PASSWORD=password \ + -e MYSQL_DATABASE=myapp \ + -p 3306:3306 \ + -d mysql:8.0 + +# 4. 运行应用 +air -c .air.toml +``` + +### 2. 项目结构说明 + +``` +internal/ +├── handler/ # HTTP 请求处理器 +├── middleware/ # 中间件(认证、日志等) +├── model/ # 数据模型定义 +├── repository/ # 数据访问层 +├── service/ # 业务逻辑层 +└── server/ # 服务器配置和路由 +``` + +## 开发流程 + +### 1. 添加新功能模块 + +#### 步骤 1: 创建数据模型 +在 `internal/model/` 目录下创建新的模型文件: + +```go +// internal/model/product.go +package model + +type Product struct { + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:100" json:"name"` + Description string `gorm:"size:255" json:"description"` + Price int `json:"price"` + // ... 其他字段 +} +``` + +#### 步骤 2: 创建 Repository +在 `internal/repository/` 目录下创建数据访问层: + +```go +// internal/repository/product_repository.go +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type ProductRepository interface { + Create(product *model.Product) error + FindByID(id uint) (*model.Product, error) + FindAll() ([]model.Product, error) + Update(product *model.Product) error + Delete(id uint) error +} + +type productRepository struct { + db *gorm.DB +} + +func NewProductRepository(db *gorm.DB) ProductRepository { + return &productRepository{db: db} +} + +// 实现接口方法... +``` + +#### 步骤 3: 创建 Service +在 `internal/service/` 目录下创建业务逻辑层: + +```go +// internal/service/product_service.go +package service + +import ( + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" +) + +type ProductService interface { + CreateProduct(req *model.CreateProductRequest) (*model.Product, error) + GetProduct(id uint) (*model.Product, error) + ListProducts() ([]model.Product, error) +} + +type productService struct { + repo repository.ProductRepository +} + +func NewProductService(repo repository.ProductRepository) ProductService { + return &productService{repo: repo} +} + +// 实现业务逻辑... +``` + +#### 步骤 4: 创建 Handler +在 `internal/handler/` 目录下创建 HTTP 处理器: + +```go +// internal/handler/product_handler.go +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/service" +) + +type ProductHandler struct { + service service.ProductService +} + +func NewProductHandler(service service.ProductService) *ProductHandler { + return &ProductHandler{service: service} +} + +func (h *ProductHandler) CreateProduct(c *gin.Context) { + var req model.CreateProductRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + product, err := h.service.CreateProduct(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, product) +} + +// 其他处理器方法... +``` + +#### 步骤 5: 注册路由 +在 `internal/server/router.go` 中添加路由: + +```go +// internal/server/router.go +func SetupRouter(userHandler *handler.UserHandler, + contractHandler *handler.ContractHandler, + productHandler *handler.ProductHandler, // 新增 + authMiddleware gin.HandlerFunc) *gin.Engine { + router := gin.Default() + + // 公共路由 + public := router.Group("/api/v1") + { + public.POST("/users/register", userHandler.Register) + public.POST("/users/login", userHandler.Login) + } + + // 需要认证的路由 + authorized := router.Group("/api/v1") + authorized.Use(authMiddleware) + { + // 用户相关 + authorized.GET("/users/profile", userHandler.GetProfile) + + // 合同相关 + authorized.POST("/contracts", contractHandler.CreateContract) + authorized.GET("/contracts", contractHandler.GetContracts) + + // 产品相关(新增) + authorized.POST("/products", productHandler.CreateProduct) + authorized.GET("/products", productHandler.GetProducts) + authorized.GET("/products/:id", productHandler.GetProduct) + authorized.PUT("/products/:id", productHandler.UpdateProduct) + authorized.DELETE("/products/:id", productHandler.DeleteProduct) + } + + return router +} +``` + +#### 步骤 6: 在主程序中初始化 +在 `cmd/api/main.go` 中初始化新的组件: + +```go +// cmd/api/main.go +func main() { + // ... 现有代码 + + // 初始化新的 Repository 和 Service + productRepo := repository.NewProductRepository(db) + productService, err := service.NewProductService(productRepo) + if err != nil { + log.Fatalf("error in productService Create: %s", err.Error()) + return + } + + // 创建 Handler + productHandler := handler.NewProductHandler(productService) + + // 更新路由设置 + router := server.SetupRouter(userHandler, contractHandler, productHandler, authMiddleware) + + // ... 剩余代码 +} +``` + +### 2. 数据库迁移 + +#### 自动迁移 +在开发环境中,设置 `reset_database: true` 会自动执行 GORM 的 AutoMigrate。 + +#### 手动创建迁移文件 +```bash +# 创建迁移目录 +mkdir -p migrations + +# 创建迁移文件 +cat > migrations/001_create_products_table.sql << 'EOF' +CREATE TABLE IF NOT EXISTS products ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description VARCHAR(255), + price INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +EOF +``` + +### 3. 测试开发 + +#### 单元测试 +```go +// internal/service/product_service_test.go +package service + +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// 创建 Mock Repository +type mockProductRepository struct { + mock.Mock +} + +func (m *mockProductRepository) Create(product *model.Product) error { + args := m.Called(product) + return args.Error(0) +} + +func TestProductService_CreateProduct(t *testing.T) { + // 测试用例... +} +``` + +#### API 测试 +使用 curl 或 Postman 测试 API: + +```bash +# 测试用户登录 +curl -X POST http://localhost:8080/api/v1/users/login \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"password123"}' + +# 测试创建合同(需要认证) +curl -X POST http://localhost:8080/api/v1/contracts \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "name": "测试合同", + "contract_type": "采购", + "contract_code": "TEST001" + }' +``` + +## 代码规范 + +### 1. 命名约定 +- 包名:小写,单数形式 +- 接口名:以 "er" 结尾(如 Repository, Handler) +- 结构体名:首字母大写 +- 变量名:驼峰式 + +### 2. 错误处理 +```go +// 正确:返回错误 +func GetUser(id uint) (*model.User, error) { + user, err := repo.FindByID(id) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + return user, nil +} + +// 错误:panic 或忽略错误 +func BadExample() { + user, _ := repo.FindByID(1) // 不要忽略错误 + // 或 + panic("something went wrong") // 不要使用 panic +} +``` + +### 3. 日志记录 +```go +import "log" + +// 使用结构化日志 +log.Printf("User %d logged in", userID) +log.Printf("Failed to create contract: %v", err) + +// 在生产环境中考虑使用更高级的日志库 +``` + +## 调试技巧 + +### 1. 使用 Air 热重载 +Air 会自动检测文件变化并重新编译。修改代码后无需手动重启。 + +### 2. 数据库调试 +```bash +# 进入 MySQL 容器 +docker exec -it zsp-backend-mysql-1 mysql -uroot -ppassword myapp + +# 查看表结构 +DESCRIBE users; +DESCRIBE contracts; + +# 查询数据 +SELECT * FROM users; +SELECT * FROM contracts; +``` + +### 3. 查看应用日志 +```bash +# Docker 环境 +docker-compose -f docker-compose.dev.yml logs -f app + +# 查看特定服务的日志 +docker-compose -f docker-compose.dev.yml logs app --tail=100 + +# 本地环境 +tail -f tmp/air.log +``` + +## 常见问题 + +### 1. 数据库连接失败 +**问题**: `Failed to connect to database` +**解决**: +1. 检查 MySQL 容器是否运行: `docker ps | grep mysql` +2. 检查连接配置: `configs/config.dev.yaml` +3. 检查网络: `docker network ls` + +### 2. 热重载不工作 +**问题**: Air 不检测文件变化 +**解决**: +1. 检查 `.air.toml` 配置 +2. 确保文件在挂载的卷中 +3. 重启 Air: `docker-compose restart app` + +### 3. 权限问题 +**问题**: 文件写入权限错误 +**解决**: +```bash +# 在容器内运行 +docker exec -it zsp-backend-app-1 chmod -R 755 /workspace/uploads +``` + +## 性能优化建议 + +### 1. 数据库优化 +- 为常用查询字段添加索引 +- 使用连接池配置 +- 避免 N+1 查询问题 + +### 2. API 优化 +- 实现分页查询 +- 使用缓存(Redis) +- 压缩响应数据 + +### 3. 内存优化 +- 及时关闭数据库连接 +- 使用连接池 +- 避免内存泄漏 + +## 下一步 + +1. ✅ 完成基础框架搭建 +2. ✅ 实现用户认证模块 +3. ✅ 实现合同管理模块 +4. 🔄 添加文件上传功能 +5. 🔄 实现权限控制(RBAC) +6. 🔄 添加 API 文档(Swagger) +7. 🔄 实现监控和日志收集 +8. 🔄 编写完整的测试套件 \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod new file mode 100755 index 0000000..f6c795a --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,61 @@ +module github.com/rovina/zsp-backend + +go 1.25.4 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.10.1 + github.com/spf13/viper v1.21.0 + github.com/xuri/excelize/v2 v2.9.0 + gorm.io/driver/mysql v1.5.2 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect +) + +require ( + github.com/bytedance/sonic v1.13.3 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.18.0 // indirect + golang.org/x/crypto v0.39.0 + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100755 index 0000000..34bd088 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,141 @@ +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend/internal/dto/error.go b/backend/internal/dto/error.go new file mode 100755 index 0000000..f7b99a3 --- /dev/null +++ b/backend/internal/dto/error.go @@ -0,0 +1,6 @@ +package dto + +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` +} diff --git a/backend/internal/dto/user.go b/backend/internal/dto/user.go new file mode 100755 index 0000000..e03d5bc --- /dev/null +++ b/backend/internal/dto/user.go @@ -0,0 +1,18 @@ +package dto + +type CreateUserRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` +} + +type LoginRequest struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` +} + +type UserResponse struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` +} diff --git a/backend/internal/dto/zsp_contract.go b/backend/internal/dto/zsp_contract.go new file mode 100755 index 0000000..0a87006 --- /dev/null +++ b/backend/internal/dto/zsp_contract.go @@ -0,0 +1,19 @@ +package dto + +type ZSPContractCreateRequest struct { + ContractNumber string `json:"contract_number" binding:"required"` + ContractType string `json:"contract_type" binding:"required"` + FolderID uint `json:"folder_id" binding:"required"` + SignDate string `json:"sign_date" binding:"required"` + CompanyName string `json:"company_name" binding:"required"` + Commodity string `json:"commodity" binding:"required"` +} + +type ZSPContractUpdateRequest struct { + ContractNumber string `json:"contractNumber"` + CompanyName string `json:"companyName"` + Commodity string `json:"commodity"` + SignDate string `json:"signDate"` + ContractType string `json:"contractType"` + FolderID uint `json:"folderId"` +} diff --git a/backend/internal/dto/zsp_contract_detail.go b/backend/internal/dto/zsp_contract_detail.go new file mode 100755 index 0000000..a9a257b --- /dev/null +++ b/backend/internal/dto/zsp_contract_detail.go @@ -0,0 +1,50 @@ +package dto + +// ZSPContractDetailCreateRequest 创建合同详情请求 +type ZSPContractDetailCreateRequest struct { + ContractFileID uint `json:"contract_file_id" binding:"required"` + CommodityName string `json:"commodity_name" binding:"required"` + CommodityCode string `json:"commodity_code"` + TotalQuantity float64 `json:"total_quantity" binding:"required"` + Unit string `json:"unit"` + UnitPrice float64 `json:"unit_price" binding:"required"` + DeliveredQty float64 `json:"delivered_qty"` + BillOfLading string `json:"bill_of_lading"` + Remark string `json:"remark"` +} + +// ZSPContractDetailUpdateRequest 更新合同详情请求 +type ZSPContractDetailUpdateRequest struct { + ID uint `json:"id" binding:"required"` + CommodityName string `json:"commodity_name"` + CommodityCode string `json:"commodity_code"` + TotalQuantity float64 `json:"total_quantity"` + Unit string `json:"unit"` + UnitPrice float64 `json:"unit_price"` + DeliveredQty float64 `json:"delivered_qty"` + BillOfLading string `json:"bill_of_lading"` + Remark string `json:"remark"` +} + +// ZSPContractDetailBatchCreateRequest 批量创建合同详情请求 +type ZSPContractDetailBatchCreateRequest struct { + ContractFileID uint `json:"contract_file_id" binding:"required"` + Details []ZSPContractDetailCreateRequest `json:"details" binding:"required"` +} + +// ZSPContractDetailResponse 合同详情响应 +type ZSPContractDetailResponse struct { + ID uint `json:"id"` + ContractFileID uint `json:"contract_file_id"` + CommodityName string `json:"commodity_name"` + CommodityCode string `json:"commodity_code"` + TotalQuantity float64 `json:"total_quantity"` + Unit string `json:"unit"` + UnitPrice float64 `json:"unit_price"` + DeliveredQty float64 `json:"delivered_qty"` + PendingQty float64 `json:"pending_qty"` + BillOfLading string `json:"bill_of_lading"` + Remark string `json:"remark"` + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` +} diff --git a/backend/internal/dto/zsp_finances.go b/backend/internal/dto/zsp_finances.go new file mode 100755 index 0000000..2c75282 --- /dev/null +++ b/backend/internal/dto/zsp_finances.go @@ -0,0 +1,101 @@ +package dto + +// ==================== 货代费用 ==================== + +type ZSPFreightChargesCreateRequest struct { + BillOfLading string `json:"billOfLading"` + ContractNumber string `json:"contractNumber"` + ExpenseItem string `json:"expenseItem" binding:"required"` + Amount float64 `json:"amount" binding:"required"` + Currency string `json:"currency"` + PaymentDate string `json:"paymentDate" binding:"required"` + FreightCompanyName string `json:"freightCompanyName" binding:"required"` + Remark string `json:"remark"` +} + +type ZSPFreightChargesUpdateRequest struct { + BillOfLading string `json:"billOfLading"` + ContractNumber string `json:"contractNumber"` + ExpenseItem string `json:"expenseItem"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + PaymentDate string `json:"paymentDate"` + FreightCompanyName string `json:"freightCompanyName"` + Remark string `json:"remark"` +} + +// ==================== 其他杂费 ==================== + +type ZSPMiscChargesCreateRequest struct { + BillOfLading string `json:"billOfLading"` + ContractNumber string `json:"contractNumber"` + ExpenseItem string `json:"expenseItem" binding:"required"` + Amount float64 `json:"amount" binding:"required"` + Currency string `json:"currency"` + PaymentDate string `json:"paymentDate" binding:"required"` + CompanyName string `json:"companyName"` + Remark string `json:"remark"` +} + +type ZSPMiscChargesUpdateRequest struct { + BillOfLading string `json:"billOfLading"` + ContractNumber string `json:"contractNumber"` + ExpenseItem string `json:"expenseItem"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + PaymentDate string `json:"paymentDate"` + CompanyName string `json:"companyName"` + Remark string `json:"remark"` +} + +// ==================== 服务费 ==================== + +type ZSPServiceFeesCreateRequest struct { + BillOfLading string `json:"billOfLading"` + ContractNumber string `json:"contractNumber"` + Name string `json:"name" binding:"required"` + Amount float64 `json:"amount" binding:"required"` + Currency string `json:"currency"` + Date string `json:"date" binding:"required"` + Remark string `json:"remark"` +} + +type ZSPServiceFeesUpdateRequest struct { + BillOfLading string `json:"billOfLading"` + ContractNumber string `json:"contractNumber"` + Name string `json:"name"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + Date string `json:"date"` + Remark string `json:"remark"` +} + +// ==================== 成本构成 ==================== + +type ZSPCostBreakdownCreateRequest struct { + BusinessID string `json:"businessId" binding:"required"` + BusinessType string `json:"businessType" binding:"required"` +} + +type ZSPCostBreakdownUpdateRequest struct { + FreightChargeTotal float64 `json:"freightChargeTotal"` + MiscChargeTotal float64 `json:"miscChargeTotal"` + ServiceFeeTotal float64 `json:"serviceFeeTotal"` +} + +// ==================== 利润记录 ==================== + +type ZSPProfitRecordCreateRequest struct { + BusinessID string `json:"businessId" binding:"required"` + BusinessType string `json:"businessType" binding:"required"` + Revenue float64 `json:"revenue" binding:"required"` + TotalCost float64 `json:"totalCost" binding:"required"` + RecordDate string `json:"recordDate" binding:"required"` + Remark string `json:"remark"` +} + +type ZSPProfitRecordUpdateRequest struct { + Revenue float64 `json:"revenue"` + TotalCost float64 `json:"totalCost"` + Remark string `json:"remark"` +} diff --git a/backend/internal/dto/zsp_folder.go b/backend/internal/dto/zsp_folder.go new file mode 100755 index 0000000..69c13e5 --- /dev/null +++ b/backend/internal/dto/zsp_folder.go @@ -0,0 +1,11 @@ +package dto + +type ZSPFolderCreateRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} + +type ZSPFolderUpdateRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} diff --git a/backend/internal/handler/company_handler.go b/backend/internal/handler/company_handler.go new file mode 100755 index 0000000..7cd4215 --- /dev/null +++ b/backend/internal/handler/company_handler.go @@ -0,0 +1,176 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/service" +) + +type CompanyHandler struct { + service service.CompanyService +} + +func NewCompanyHandler(s service.CompanyService) *CompanyHandler { + return &CompanyHandler{service: s} +} + +func (h *CompanyHandler) ListFolders(c *gin.Context) { + list, err := h.service.ListFolders() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) +} + +func (h *CompanyHandler) CreateFolder(c *gin.Context) { + var body struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Category string `json:"category"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + f, err := h.service.CreateFolder(body.Name, body.Description, body.Category) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": f}) +} + +func (h *CompanyHandler) UpdateFolder(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + var body struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Category string `json:"category"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + if err := h.service.UpdateFolder(uint(id), body.Name, body.Description, body.Category); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"}) +} + +func (h *CompanyHandler) DeleteFolder(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.service.DeleteFolder(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} + +func (h *CompanyHandler) ListFiles(c *gin.Context) { + var folderID *uint + if idStr := c.Query("folderId"); idStr != "" { + if id, err := strconv.ParseUint(idStr, 10, 32); err == nil { + uid := uint(id) + folderID = &uid + } + } + list, err := h.service.ListFiles(folderID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) +} + +func (h *CompanyHandler) UploadFiles(c *gin.Context) { + form, err := c.MultipartForm() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + files := form.File["files"] + if len(files) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "no files"}) + return + } + + var dataStr string + if v := form.Value["data"]; len(v) > 0 { + dataStr = v[0] + } + var data struct { + CompanyName string `json:"companyName"` + FileCategory string `json:"fileCategory"` + ExpireDate string `json:"expireDate"` + Description string `json:"description"` + FolderID string `json:"folderId"` + } + if dataStr != "" { + _ = json.Unmarshal([]byte(dataStr), &data) + } + + var folderID *uint + if data.FolderID != "" { + if id, err := strconv.ParseUint(data.FolderID, 10, 32); err == nil { + uid := uint(id) + folderID = &uid + } + } + + count, err := h.service.UploadFiles(folderID, data.CompanyName, data.FileCategory, data.ExpireDate, data.Description, files) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"uploadedCount": count}, "message": "uploaded"}) +} + +func (h *CompanyHandler) UpdateFile(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + var body struct { + CompanyName string `json:"companyName"` + FileCategory string `json:"fileCategory"` + ExpireDate string `json:"expireDate"` + Description string `json:"description"` + FolderID *uint `json:"folderId"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + if err := h.service.UpdateFile(uint(id), body.CompanyName, body.FileCategory, body.ExpireDate, body.Description, body.FolderID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"}) +} + +func (h *CompanyHandler) DeleteFile(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.service.DeleteFile(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} diff --git a/backend/internal/handler/contract_handler.go b/backend/internal/handler/contract_handler.go new file mode 100755 index 0000000..ea9614e --- /dev/null +++ b/backend/internal/handler/contract_handler.go @@ -0,0 +1,482 @@ +package handler + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/service" +) + +func (h *ContractHandler) List(c *gin.Context) { + contractType := c.Query("contractType") + list, err := h.service.List(contractType) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) +} + +func (h *ContractHandler) Get(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + contract, err := h.service.GetByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "contract not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": contract}) +} + +func (h *ContractHandler) Update(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + existing, err := h.service.GetByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "contract not found"}) + return + } + var body struct { + Name string `json:"name"` + ContractType string `json:"contractType"` + ContractCode string `json:"contractCode"` + BuyParty string `json:"buyParty"` + SellParty string `json:"sellParty"` + Commodity string `json:"commodity"` + Count float64 `json:"count"` + UnitPrice float64 `json:"unitPrice"` + BLNumber string `json:"blNumber"` + DeliveryTime string `json:"deliveryTime"` + PickUpTime string `json:"pickUpTime"` + Packaging string `json:"packaging"` + Remarks string `json:"remarks"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + existing.Name = body.Name + existing.ContractType = body.ContractType + existing.ContractCode = body.ContractCode + existing.BuyParty = body.BuyParty + existing.SellParty = body.SellParty + existing.Commodity = body.Commodity + existing.Count = body.Count + existing.UnitPrice = body.UnitPrice + existing.BLNumber = body.BLNumber + existing.DeliveryTime = body.DeliveryTime + existing.PickUpTime = body.PickUpTime + existing.Packaging = body.Packaging + existing.Remarks = body.Remarks + existing.UpdateTime = time.Now() + if err := h.service.Update(existing); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"}) +} + +func (h *ContractHandler) UpdateWithFiles(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + + form, err := c.MultipartForm() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + + formMap := make(map[string]string) + for k, v := range form.Value { + if len(v) > 0 { + formMap[k] = v[0] + } + } + + keepFileIds := parseUintSlice(formMap["keepFileIds"]) + newFileHeaders := form.File["files"] + + if len(keepFileIds) == 0 && len(newFileHeaders) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "至少需要保留一个合同扫描件"}) + return + } + + var newFileModels []model.ContractFile + path := "./data/contract/uploads" + os.MkdirAll(path, os.ModePerm) + + for _, fh := range newFileHeaders { + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), fh.Filename) + savePath := filepath.Join(path, filename) + if err := c.SaveUploadedFile(fh, savePath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + newFileModels = append(newFileModels, model.ContractFile{ + Name: fh.Filename, + URL: "uploads/" + filename, + }) + } + + if len(keepFileIds)+len(newFileModels) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "至少需要保留一个合同扫描件"}) + return + } + + if err := h.service.UpdateWithFiles(uint(id), keepFileIds, newFileModels, formMap); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"}) +} + +func parseUintSlice(s string) []uint { + if s == "" { + return nil + } + parts := strings.Split(s, ",") + var result []uint + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if v, err := strconv.ParseUint(p, 10, 32); err == nil { + result = append(result, uint(v)) + } + } + return result +} + +func (h *ContractHandler) Delete(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.service.Delete(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} + +type ContractHandler struct { + service service.ContractService + folderService service.ContractFolderService + batchService service.ContractBatchService +} + +func NewContractHandler( + service service.ContractService, + folderService service.ContractFolderService, + batchService service.ContractBatchService, +) *ContractHandler { + return &ContractHandler{ + service: service, + folderService: folderService, + batchService: batchService, + } +} + +func (h *ContractHandler) Upload(c *gin.Context) { + + var contract model.Contract + + contract.Name = c.PostForm("name") + contract.ContractType = c.PostForm("contractType") + contract.ContractCode = c.PostForm("contractCode") + contract.BuyParty = c.PostForm("buyParty") + contract.SellParty = c.PostForm("sellParty") + contract.Commodity = c.PostForm("commodity") + contract.BLNumber = c.PostForm("BLNumber") + contract.DeliveryTime = c.PostForm("deliveryTime") + contract.PickUpTime = c.PostForm("pickUpTime") + contract.Packaging = c.PostForm("packaging") + contract.Remarks = c.PostForm("remarks") + + if countStr := c.PostForm("count"); countStr != "" { + if v, err := strconv.ParseFloat(countStr, 64); err == nil { + contract.Count = v + } + } + if priceStr := c.PostForm("unitPrice"); priceStr != "" { + if v, err := strconv.ParseFloat(priceStr, 64); err == nil { + contract.UnitPrice = v + } + } + + contract.CreateTime = time.Now() + contract.UpdateTime = time.Now() + + form, err := c.MultipartForm() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + + files := form.File["files"] + + if len(files) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "no files"}) + return + } + path := "./data/contract/uploads" + os.MkdirAll(path, os.ModePerm) + + var fileModels []model.ContractFile + + for _, file := range files { + + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename) + savePath := filepath.Join(path, filename) + + if err := c.SaveUploadedFile(file, savePath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + + fileModels = append(fileModels, model.ContractFile{ + Name: file.Filename, + URL: "uploads/" + filename, + }) + } + + if err := h.service.CreateContract(&contract, fileModels); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "upload success", + }) +} + +func (h *ContractHandler) BatchUpload(c *gin.Context) { + path := c.PostForm("path") + if path == "" { + + c.JSON(http.StatusBadRequest, gin.H{"error": "path required"}) + return + } + + form, err := c.MultipartForm() + if err != nil { + + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid form"}) + return + } + + files := form.File["files"] + if len(files) == 0 { + + c.JSON(http.StatusBadRequest, gin.H{"error": "no files"}) + return + } + + saved, err := h.service.BatchSaveFiles(path, files) + if err != nil { + + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "count": saved, + }) +} + +// ========== 批量归档文件夹 ========== + +func (h *ContractHandler) ListFolders(c *gin.Context) { + list, err := h.folderService.List() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) +} + +func (h *ContractHandler) CreateFolder(c *gin.Context) { + var body struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + f, err := h.folderService.Create(body.Name, body.Description) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": f}) +} + +func (h *ContractHandler) GetFolder(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + f, err := h.folderService.GetByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": f}) +} + +func (h *ContractHandler) UpdateFolder(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + var body struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + if err := h.folderService.Update(uint(id), body.Name, body.Description); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"}) +} + +func (h *ContractHandler) DeleteFolder(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.folderService.Delete(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} + +// ========== 批量上传记录 ========== + +func (h *ContractHandler) ListBatches(c *gin.Context) { + var folderID *uint + if idStr := c.Query("folderId"); idStr != "" { + if id, err := strconv.ParseUint(idStr, 10, 32); err == nil { + uid := uint(id) + folderID = &uid + } + } + list, err := h.batchService.List(folderID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) +} + +func (h *ContractHandler) CreateBatch(c *gin.Context) { + folderIDStr := c.PostForm("folderId") + batchName := c.PostForm("batchName") + remark := c.PostForm("remark") + if folderIDStr == "" || batchName == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "folderId and batchName required"}) + return + } + folderID, err := strconv.ParseUint(folderIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid folderId"}) + return + } + form, err := c.MultipartForm() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + files := form.File["files"] + if len(files) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "no files"}) + return + } + path := "./data/contract/batch_archive" + os.MkdirAll(path, os.ModePerm) + var fileModels []model.ContractBatchFile + for _, file := range files { + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename) + savePath := filepath.Join(path, filename) + if err := c.SaveUploadedFile(file, savePath); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + fileModels = append(fileModels, model.ContractBatchFile{ + Name: file.Filename, + URL: "batch_archive/" + filename, + FileSize: file.Size, + }) + } + b, err := h.batchService.Create(uint(folderID), batchName, remark, fileModels) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": b, "message": "created"}) +} + +func (h *ContractHandler) GetBatch(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + b, err := h.batchService.GetByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": b}) +} + +func (h *ContractHandler) DeleteBatch(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.batchService.Delete(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} diff --git a/backend/internal/handler/delivery_handler.go b/backend/internal/handler/delivery_handler.go new file mode 100755 index 0000000..87a77a3 --- /dev/null +++ b/backend/internal/handler/delivery_handler.go @@ -0,0 +1,530 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/service" + "github.com/rovina/zsp-backend/pkg/utils" +) + +type DeliveryHandler struct { + svc service.DeliveryService +} + +func NewDeliveryHandler(svc service.DeliveryService) *DeliveryHandler { + return &DeliveryHandler{svc: svc} +} + +// ========== 我要提货 /api/apply-delivery ========== +func (h *DeliveryHandler) ListApply(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + orderNumber := c.Query("orderNumber") + blNumber := c.Query("blNumber") + status := c.Query("status") + list, total, err := h.svc.ListApply( page, pageSize, orderNumber, blNumber, status) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total, "page": page, "pageSize": pageSize}}) +} + +func (h *DeliveryHandler) GetApply(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetApplyByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) CreateApply(c *gin.Context) { + var body model.DeliveryApply + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + body.OrderNumber = fmt.Sprintf("DA%d", time.Now().UnixNano()/1e6) + body.Status = "pending" + body.CreateTime = time.Now() + body.UpdateTime = time.Now() + if err := h.svc.CreateApply( &body); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"id": body.ID, "orderNumber": body.OrderNumber}}) +} + +func (h *DeliveryHandler) UpdateApply(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetApplyByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + if err := c.ShouldBindJSON(e); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + e.UpdateTime = time.Now() + if err := h.svc.UpdateApply( e); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) CancelApply(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetApplyByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + e.Status = "cancelled" + e.UpdateTime = time.Now() + if err := h.svc.UpdateApply( e); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) DeleteApply(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.svc.DeleteApply( uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} + +// ========== 提货明细 /api/delivery-details ========== +func (h *DeliveryHandler) ListOutbound(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + outboundNumber := c.Query("outboundNumber") + blNumber := c.Query("blNumber") + status := c.Query("outboundStatus") + list, total, err := h.svc.ListOutbound( page, pageSize, outboundNumber, blNumber, status) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total, "page": page, "pageSize": pageSize}}) +} + +func (h *DeliveryHandler) GetOutbound(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetOutboundByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) CreateOutbound(c *gin.Context) { + var body model.DeliveryOutbound + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + body.OutboundNumber = fmt.Sprintf("OB%d", time.Now().UnixNano()/1e6) + body.OutboundStatus = "pending" + body.ReceiptStatus = "pending" + body.CreateTime = time.Now() + body.UpdateTime = time.Now() + if err := h.svc.CreateOutbound( &body); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": body}) +} + +func (h *DeliveryHandler) UpdateOutbound(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetOutboundByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + if err := c.ShouldBindJSON(e); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + e.UpdateTime = time.Now() + if err := h.svc.UpdateOutbound( e); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) DeleteOutbound(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.svc.DeleteOutbound( uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} + +// ========== 库存 /api/inventory ========== +func (h *DeliveryHandler) ListWarehouses(c *gin.Context) { + list, err := h.svc.ListWarehouses() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) +} + +func (h *DeliveryHandler) CreateWarehouse(c *gin.Context) { + var body model.Warehouse + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + body.Status = "active" + body.CreateTime = time.Now() + body.UpdateTime = time.Now() + if err := h.svc.CreateWarehouse( &body); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": body}) +} + +func (h *DeliveryHandler) UpdateWarehouse(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetWarehouseByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + if err := c.ShouldBindJSON(e); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + e.UpdateTime = time.Now() + if err := h.svc.UpdateWarehouse( e); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) DeleteWarehouse(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.svc.DeleteWarehouse( uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} + +func (h *DeliveryHandler) ListInbound(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + warehouse := c.Query("warehouse") + commodity := c.Query("commodity") + list, total, err := h.svc.ListInbound( page, pageSize, warehouse, commodity) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total, "page": page, "pageSize": pageSize}}) +} + +func (h *DeliveryHandler) CreateInbound(c *gin.Context) { + var body model.InventoryInbound + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + body.InboundNumber = fmt.Sprintf("IN%d", time.Now().UnixNano()/1e6) + body.CreateTime = time.Now() + body.UpdateTime = time.Now() + if err := h.svc.CreateInbound( &body); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": body}) +} + +func (h *DeliveryHandler) UpdateInbound(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetInboundByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + if err := c.ShouldBindJSON(e); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + e.UpdateTime = time.Now() + if err := h.svc.UpdateInbound( e); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) DeleteInbound(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.svc.DeleteInbound( uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} + +// InventorySummary 库存汇总:入库 - 出库 - 货权转移 = 库存 +func (h *DeliveryHandler) InventorySummary(c *gin.Context) { + warehouse := c.Query("warehouse") + commodity := c.Query("commodity") + + type row struct { + Warehouse string `json:"warehouse"` + Commodity string `json:"commodity"` + TotalInbound float64 `json:"totalInbound"` + TotalOutbound float64 `json:"totalOutbound"` + TotalTransfer float64 `json:"totalTransfer"` + CurrentStock float64 `json:"currentStock"` + Unit string `json:"unit"` + } + + var inSum []struct { + Warehouse string + Commodity string + Sum float64 + } + qIn := h.svc.DB().Model(&model.InventoryInbound{}).Select("warehouse, commodity, SUM(inbound_weight) as sum").Group("warehouse, commodity") + if warehouse != "" { + qIn = qIn.Where("warehouse = ?", warehouse) + } + if commodity != "" { + qIn = qIn.Where("commodity LIKE ?", "%"+commodity+"%") + } + qIn.Scan(&inSum) + + var outSum []struct { + Warehouse string + Commodity string + Sum float64 + } + qOut := h.svc.DB().Model(&model.DeliveryOutbound{}).Select("warehouse, commodity, SUM(quantity) as sum").Group("warehouse, commodity") + if warehouse != "" { + qOut = qOut.Where("warehouse = ?", warehouse) + } + if commodity != "" { + qOut = qOut.Where("commodity LIKE ?", "%"+commodity+"%") + } + qOut.Scan(&outSum) + + var trSum []struct { + Warehouse string + Commodity string + Sum float64 + } + qTr := h.svc.DB().Model(&model.OwnershipTransfer{}). + Select("ownership_transfers.warehouse, ownership_transfers.commodity, COALESCE((SELECT SUM(quantity) FROM ownership_transfer_bls WHERE transfer_id = ownership_transfers.id), 0) as sum"). + Group("ownership_transfers.warehouse, ownership_transfers.commodity") + if warehouse != "" { + qTr = qTr.Where("warehouse = ?", warehouse) + } + if commodity != "" { + qTr = qTr.Where("commodity LIKE ?", "%"+commodity+"%") + } + qTr.Scan(&trSum) + + m := make(map[string]*row) + for _, r := range inSum { + key := r.Warehouse + "|" + r.Commodity + m[key] = &row{Warehouse: r.Warehouse, Commodity: r.Commodity, TotalInbound: r.Sum, Unit: "吨"} + } + for _, r := range outSum { + key := r.Warehouse + "|" + r.Commodity + if m[key] == nil { + m[key] = &row{Warehouse: r.Warehouse, Commodity: r.Commodity, Unit: "吨"} + } + m[key].TotalOutbound = r.Sum + } + for _, r := range trSum { + key := r.Warehouse + "|" + r.Commodity + if m[key] == nil { + m[key] = &row{Warehouse: r.Warehouse, Commodity: r.Commodity, Unit: "吨"} + } + m[key].TotalTransfer = r.Sum + } + var result []row + for _, v := range m { + v.CurrentStock = v.TotalInbound - v.TotalOutbound - v.TotalTransfer + result = append(result, *v) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": result}) +} + +// ========== 货权转移 /api/ownership-transfer ========== +func (h *DeliveryHandler) ListOwnership(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + transferNumber := c.Query("transferNumber") + blNumber := c.Query("blNumber") + status := c.Query("status") + list, total, err := h.svc.ListOwnership( page, pageSize, transferNumber, blNumber, status) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total}}) +} + +func (h *DeliveryHandler) GetOwnership(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + e, err := h.svc.GetOwnershipByID( uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": e}) +} + +func (h *DeliveryHandler) CreateOwnership(c *gin.Context) { + var body model.OwnershipTransfer + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + body.TransferNumber = fmt.Sprintf("OT%d", time.Now().UnixNano()/1e6) + body.Status = "draft" + body.CreateTime = time.Now() + body.UpdateTime = time.Now() + if err := h.svc.CreateOwnership( &body); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": body}) +} + +func (h *DeliveryHandler) CreateOwnershipWithFiles(c *gin.Context) { + transferDate := c.PostForm("transferDate") + commodity := c.PostForm("commodity") + warehouse := c.PostForm("warehouse") + transferor := c.PostForm("transferor") + transferee := c.PostForm("transferee") + remark := c.PostForm("remark") + + // Parse dynamic BL items from JSON string field "blItems" + var blItems []model.OwnershipTransferBL + if blItemsJSON := c.PostForm("blItems"); blItemsJSON != "" { + json.Unmarshal([]byte(blItemsJSON), &blItems) + } + + body := model.OwnershipTransfer{ + TransferNumber: fmt.Sprintf("OT%d", time.Now().UnixNano()/1e6), + TransferDate: transferDate, + Commodity: commodity, + Warehouse: warehouse, + Transferor: transferor, + Transferee: transferee, + Remark: remark, + BLItems: blItems, + Status: "draft", + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + if err := h.svc.CreateOwnership( &body); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + form, _ := c.MultipartForm() + files := form.File["files"] + basePath := filepath.Join(".", "data", "ownership") + if len(files) > 0 { + _ = os.MkdirAll(basePath, os.ModePerm) + for _, fh := range files { + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), fh.Filename) + savePath := filepath.Join(basePath, filename) + if err := utils.SaveUploadedFile(fh, savePath); err != nil { + continue + } + fileRec := model.OwnershipTransferFile{TransferID: body.ID, Name: fh.Filename, URL: "ownership/" + filename} + _ = h.svc.CreateOwnershipFiles( []model.OwnershipTransferFile{fileRec}) + } + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": body}) +} + +func (h *DeliveryHandler) DeleteOwnership(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"}) + return + } + if err := h.svc.DeleteOwnership( uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"}) +} diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go new file mode 100755 index 0000000..f30ecee --- /dev/null +++ b/backend/internal/handler/user_handler.go @@ -0,0 +1,76 @@ +// internal/handler/user_handler.go +package handler + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/service" +) + +type UserHandler struct { + userService service.UserService +} + +func NewUserHandler(userService service.UserService) *UserHandler { + return &UserHandler{userService: userService} +} + +func (h *UserHandler) Register(c *gin.Context) { + var req dto.CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("bind json error: %s", err.Error())}) + return + } + user, err := h.userService.Register(&req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "message": "User registered successfully", + "user": user, + }) +} + +func (h *UserHandler) Login(c *gin.Context) { + var req dto.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, token, err := h.userService.Login(&req) + fmt.Println(user) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Login successful", + "user": user, + "token": token, + }) +} + +func (h *UserHandler) GetUser(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + user, err := h.userService.GetUserByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"user": user}) +} diff --git a/backend/internal/handler/zsp_contract_handler.go b/backend/internal/handler/zsp_contract_handler.go new file mode 100755 index 0000000..eaf9965 --- /dev/null +++ b/backend/internal/handler/zsp_contract_handler.go @@ -0,0 +1,539 @@ +package handler + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/service" + "github.com/xuri/excelize/v2" +) + +type ZSPContractHandler struct { + service service.ZSPContractService +} + +func NewZSPContractHandler(service service.ZSPContractService) *ZSPContractHandler { + return &ZSPContractHandler{service: service} +} + +// zspContracts.POST("/folders", zspContractHandler.CreateFolder) +// zspContracts.GET("/folders", zspContractHandler.GetFolders) +// zspContracts.GET("/folders/:id", zspContractHandler.GetFolder) +// zspContracts.DELETE("/folders/:id", zspContractHandler.DeleteFolder) +// zspContracts.POST("/files", zspContractHandler.UploadFile) +// zspContracts.GET("/files/:id", zspContractHandler.GetFile) +// zspContracts.GET("/files", zspContractHandler.ListFiles) +// zspContracts.DELETE("/files/:id", zspContractHandler.DeleteFile) + +func (h *ZSPContractHandler) CreateFolder(c *gin.Context) { + var req dto.ZSPFolderCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.CreateFolder(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "folder created"}) +} + +func (h *ZSPContractHandler) GetFolders(c *gin.Context) { + folders, err := h.service.GetFolders() + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "folders retrieved", "data": folders}) +} + +func (h *ZSPContractHandler) GetFolder(c *gin.Context) { + folderID := c.Param("id") + // 这里可以调用 service 获取单个文件夹的详情 + folder, err := h.service.GetFolderById(folderID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "folder retrieved", "data": folder}) +} + +func (h *ZSPContractHandler) DeleteFolder(c *gin.Context) { + folderID := c.Param("id") + err := h.service.DeleteFolderById(folderID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "folder deleted"}) +} + +func (h *ZSPContractHandler) UpdateFolder(c *gin.Context) { + folderID := c.Param("id") + var req dto.ZSPFolderUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + // 这里可以调用 service 更新文件夹的信息 + err := h.service.UpdateFolderById(folderID, &req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "folder updated"}) +} + +func (h *ZSPContractHandler) UploadContract(c *gin.Context) { + var req dto.ZSPContractCreateRequest + // FormData 方式上传文件,参数在 form 中 + req.Commodity = c.PostForm("commodity") + req.SignDate = c.PostForm("signDate") + req.ContractType = c.PostForm("contractType") + folderIDStr := c.PostForm("folderId") + if folderIDStr == "" { + c.JSON(400, gin.H{"success": false, "message": "请选择合同文件夹"}) + return + } + folderID, err := strconv.ParseUint(folderIDStr, 10, 64) + if err != nil { + c.JSON(400, gin.H{"success": false, "message": "invalid folderId"}) + return + } + req.FolderID = uint(folderID) + req.ContractNumber = c.PostForm("contractNumber") + req.CompanyName = c.PostForm("companyName") + + file, err := c.FormFile("file") + + if err != nil { + c.JSON(400, gin.H{"success": false, "message": "file is required"}) + return + } + err = h.service.UploadContract(&req, file) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract uploaded"}) + +} + +func (h *ZSPContractHandler) GetContract(c *gin.Context) { + contractID := c.Param("id") + // 这里可以调用 service 获取单个合同的详情 + contract, err := h.service.GetContractById(contractID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract retrieved", "data": contract}) +} + +func (h *ZSPContractHandler) ListContracts(c *gin.Context) { + folderID := c.Query("folderId") + contracts, err := h.service.ListContracts(folderID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contracts retrieved", "data": contracts}) +} + +func (h *ZSPContractHandler) DeleteContract(c *gin.Context) { + contractID := c.Param("id") + err := h.service.DeleteContractById(contractID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract deleted"}) +} + +func (h *ZSPContractHandler) UpdateContract(c *gin.Context) { + contractID := c.Param("id") + var req dto.ZSPContractUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.UpdateContractById(contractID, &req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract updated"}) +} + +// ============ 合同详情相关Handler ============ + +// GetContractWithDetails 获取合同及其详情列表 +func (h *ZSPContractHandler) GetContractWithDetails(c *gin.Context) { + contractID := c.Param("id") + contract, err := h.service.GetContractWithDetails(contractID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract with details retrieved", "data": contract}) +} + +// ListContractsWithDetails 获取合同列表(包含详情) +func (h *ZSPContractHandler) ListContractsWithDetails(c *gin.Context) { + folderID := c.Query("folderId") + contracts, err := h.service.ListContractsWithDetails(folderID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contracts with details retrieved", "data": contracts}) +} + +// CreateContractDetail 创建合同详情 +func (h *ZSPContractHandler) CreateContractDetail(c *gin.Context) { + var req dto.ZSPContractDetailCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + + err := h.service.CreateContractDetail(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + + c.JSON(200, gin.H{"success": true, "message": "contract detail created"}) +} + +// UpdateContractDetail 更新合同详情 +func (h *ZSPContractHandler) UpdateContractDetail(c *gin.Context) { + var req dto.ZSPContractDetailUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + + err := h.service.UpdateContractDetail(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + + c.JSON(200, gin.H{"success": true, "message": "contract detail updated"}) +} + +// GetContractDetail 获取合同详情 +func (h *ZSPContractHandler) GetContractDetail(c *gin.Context) { + detailID := c.Param("id") + detail, err := h.service.GetContractDetailById(detailID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract detail retrieved", "data": detail}) +} + +// ListContractDetails 获取合同详情列表 +func (h *ZSPContractHandler) ListContractDetails(c *gin.Context) { + contractFileID := c.Param("contractFileId") + details, err := h.service.ListContractDetails(contractFileID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract details retrieved", "data": details}) +} + +// DeleteContractDetail 删除合同详情 +func (h *ZSPContractHandler) DeleteContractDetail(c *gin.Context) { + detailID := c.Param("id") + err := h.service.DeleteContractDetailById(detailID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "contract detail deleted"}) +} + +// BatchCreateContractDetails 批量创建合同详情 +func (h *ZSPContractHandler) BatchCreateContractDetails(c *gin.Context) { + var req dto.ZSPContractDetailBatchCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + + err := h.service.BatchCreateContractDetails(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + + c.JSON(200, gin.H{"success": true, "message": fmt.Sprintf("%d contract details created", len(req.Details))}) +} + +// ImportContractDetailsFromExcel 从Excel导入合同详情 +func (h *ZSPContractHandler) ImportContractDetailsFromExcel(c *gin.Context) { + contractFileIDStr := c.PostForm("contractFileId") + if contractFileIDStr == "" { + c.JSON(400, gin.H{"success": false, "message": "contractFileId is required"}) + return + } + + contractFileID, err := strconv.ParseUint(contractFileIDStr, 10, 64) + if err != nil { + c.JSON(400, gin.H{"success": false, "message": "invalid contractFileId"}) + return + } + + file, err := c.FormFile("file") + if err != nil { + c.JSON(400, gin.H{"success": false, "message": "file is required"}) + return + } + + // 打开上传的Excel文件 + src, err := file.Open() + if err != nil { + c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to open file: %v", err)}) + return + } + defer src.Close() + + // 使用excelize读取Excel文件 + f, err := excelize.OpenReader(src) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to read Excel file: %v", err)}) + return + } + defer f.Close() + + // 获取第一个sheet的名称 + sheets := f.GetSheetList() + if len(sheets) == 0 { + c.JSON(400, gin.H{"success": false, "message": "Excel file is empty"}) + return + } + sheetName := sheets[0] + + // 读取所有行数据 + rows, err := f.GetRows(sheetName) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to read rows: %v", err)}) + return + } + + // 检查是否有数据 + if len(rows) <= 1 { + c.JSON(400, gin.H{"success": false, "message": "Excel file has no data rows"}) + return + } + + // 解析数据(假设第一行是表头) + var details []dto.ZSPContractDetailCreateRequest + for i, row := range rows { + // 跳过表头 + if i == 0 { + continue + } + + // 确保至少有基本列 + if len(row) < 6 { + continue + } + + detail := dto.ZSPContractDetailCreateRequest{ + ContractFileID: uint(contractFileID), + } + + // 商品名称 (第1列) + detail.CommodityName = strings.TrimSpace(row[0]) + + // 单位 (第2列) + detail.Unit = strings.TrimSpace(row[1]) + if detail.Unit == "" { + detail.Unit = "吨" + } + + // 总量 (第3列) + if row[2] != "" { + detail.TotalQuantity, err = strconv.ParseFloat(strings.TrimSpace(row[2]), 64) + if err != nil { + c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("invalid total quantity at row %d: %v", i+1, err)}) + return + } + } + + // 单价 (第4列) + if row[3] != "" { + detail.UnitPrice, err = strconv.ParseFloat(strings.TrimSpace(row[3]), 64) + if err != nil { + c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("invalid unit price at row %d: %v", i+1, err)}) + return + } + } + + // 已提货数量 (第5列) + if row[4] != "" { + detail.DeliveredQty, err = strconv.ParseFloat(strings.TrimSpace(row[4]), 64) + if err != nil { + c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("invalid delivered quantity at row %d: %v", i+1, err)}) + return + } + } + + // 提单号 (第6列) + if len(row) > 5 { + detail.BillOfLading = strings.TrimSpace(row[5]) + } + + // 备注 (第7列) + if len(row) > 6 { + detail.Remark = strings.TrimSpace(row[6]) + } + + // 验证必填字段 + if detail.CommodityName == "" { + c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("commodity name is required at row %d", i+1)}) + return + } + if detail.TotalQuantity == 0 { + c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("total quantity is required at row %d", i+1)}) + return + } + if detail.UnitPrice == 0 { + c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("unit price is required at row %d", i+1)}) + return + } + + details = append(details, detail) + } + + // 批量创建合同详情 + if len(details) == 0 { + c.JSON(400, gin.H{"success": false, "message": "no valid data found in Excel file"}) + return + } + + batchReq := &dto.ZSPContractDetailBatchCreateRequest{ + ContractFileID: uint(contractFileID), + Details: details, + } + + err = h.service.BatchCreateContractDetails(batchReq) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to create contract details: %v", err)}) + return + } + + c.JSON(200, gin.H{"success": true, "message": fmt.Sprintf("successfully imported %d contract details", len(details))}) +} + +// ExportContractDetailsToExcel 导出合同详情到Excel +func (h *ZSPContractHandler) ExportContractDetailsToExcel(c *gin.Context) { + contractFileID := c.Param("id") + details, err := h.service.ListContractDetails(contractFileID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + + // 创建新的Excel文件 + f := excelize.NewFile() + sheetName := "Sheet1" + + // 设置表头 + headers := []string{"商品名称", "单位", "总量", "单价", "已提货数量", "应提货数量", "提单号", "备注"} + for i, header := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheetName, cell, header) + + // 设置表头样式(加粗、背景色) + style, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{ + Bold: true, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"#E0E0E0"}, + Pattern: 1, + }, + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + }, + }) + f.SetCellStyle(sheetName, cell, cell, style) + } + + // 填充数据 + for i, detail := range details { + row := i + 2 + + // 商品名称 + cell, _ := excelize.CoordinatesToCellName(1, row) + f.SetCellValue(sheetName, cell, detail.CommodityName) + + // 单位 + cell, _ = excelize.CoordinatesToCellName(2, row) + f.SetCellValue(sheetName, cell, detail.Unit) + + // 总量 + cell, _ = excelize.CoordinatesToCellName(3, row) + f.SetCellValue(sheetName, cell, detail.TotalQuantity) + + // 单价 + cell, _ = excelize.CoordinatesToCellName(4, row) + f.SetCellValue(sheetName, cell, detail.UnitPrice) + + // 已提货数量 + cell, _ = excelize.CoordinatesToCellName(5, row) + f.SetCellValue(sheetName, cell, detail.DeliveredQty) + + // 应提货数量 + cell, _ = excelize.CoordinatesToCellName(6, row) + f.SetCellValue(sheetName, cell, detail.PendingQty) + + // 提单号 + cell, _ = excelize.CoordinatesToCellName(7, row) + f.SetCellValue(sheetName, cell, detail.BillOfLading) + + // 备注 + cell, _ = excelize.CoordinatesToCellName(8, row) + f.SetCellValue(sheetName, cell, detail.Remark) + } + + // 设置列宽 + f.SetColWidth(sheetName, "A", "A", 20) // 商品名称 + f.SetColWidth(sheetName, "B", "B", 10) // 单位 + f.SetColWidth(sheetName, "C", "C", 12) // 总量 + f.SetColWidth(sheetName, "D", "D", 12) // 单价 + f.SetColWidth(sheetName, "E", "E", 12) // 已提货数量 + f.SetColWidth(sheetName, "F", "F", 12) // 应提货数量 + f.SetColWidth(sheetName, "G", "G", 20) // 提单号 + f.SetColWidth(sheetName, "H", "H", 30) // 备注 + + // 使用buffer保存Excel数据 + var buf bytes.Buffer + err = f.Write(&buf) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to write Excel buffer: %v", err)}) + return + } + + // 关闭文件 + f.Close() + + // 设置响应头 + c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=contract_details_%s.xlsx", contractFileID)) + + // 返回Excel文件 + c.Data(200, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes()) +} diff --git a/backend/internal/handler/zsp_finances_handler.go b/backend/internal/handler/zsp_finances_handler.go new file mode 100755 index 0000000..4caac94 --- /dev/null +++ b/backend/internal/handler/zsp_finances_handler.go @@ -0,0 +1,318 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/service" +) + +type ZSPFinancesHandler struct { + service service.ZSPFinancesService +} + +func NewZSPFinancesHandler(service service.ZSPFinancesService) *ZSPFinancesHandler { + return &ZSPFinancesHandler{service: service} +} + +// ============ 货代费用 ============ +func (h *ZSPFinancesHandler) CreateFreightCharge(c *gin.Context) { + var req dto.ZSPFreightChargesCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.CreateFreightCharge(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "freight charge created"}) +} + +func (h *ZSPFinancesHandler) GetFreightCharge(c *gin.Context) { + id := c.Param("id") + charge, err := h.service.GetFreightCharge(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "freight charge retrieved", "data": charge}) +} + +func (h *ZSPFinancesHandler) ListFreightCharges(c *gin.Context) { + charges, err := h.service.ListFreightCharges() + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "freight charges retrieved", "data": charges}) +} + +func (h *ZSPFinancesHandler) UpdateFreightCharge(c *gin.Context) { + id := c.Param("id") + var req dto.ZSPFreightChargesUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.UpdateFreightCharge(id, &req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "freight charge updated"}) +} + +func (h *ZSPFinancesHandler) DeleteFreightCharge(c *gin.Context) { + id := c.Param("id") + err := h.service.DeleteFreightCharge(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "freight charge deleted"}) +} + +// ============ 杂费 ============ +func (h *ZSPFinancesHandler) CreateMiscCharge(c *gin.Context) { + var req dto.ZSPMiscChargesCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.CreateMiscCharge(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "misc charge created"}) +} + +func (h *ZSPFinancesHandler) GetMiscCharge(c *gin.Context) { + id := c.Param("id") + charge, err := h.service.GetMiscCharge(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "misc charge retrieved", "data": charge}) +} + +func (h *ZSPFinancesHandler) ListMiscCharges(c *gin.Context) { + charges, err := h.service.ListMiscCharges() + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "misc charges retrieved", "data": charges}) +} + +func (h *ZSPFinancesHandler) UpdateMiscCharge(c *gin.Context) { + id := c.Param("id") + var req dto.ZSPMiscChargesUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.UpdateMiscCharge(id, &req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "misc charge updated"}) +} + +func (h *ZSPFinancesHandler) DeleteMiscCharge(c *gin.Context) { + id := c.Param("id") + err := h.service.DeleteMiscCharge(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "misc charge deleted"}) +} + +// ============ 服务费 ============ +func (h *ZSPFinancesHandler) CreateServiceFee(c *gin.Context) { + var req dto.ZSPServiceFeesCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.CreateServiceFee(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "service fee created"}) +} + +func (h *ZSPFinancesHandler) GetServiceFee(c *gin.Context) { + id := c.Param("id") + fee, err := h.service.GetServiceFee(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "service fee retrieved", "data": fee}) +} + +func (h *ZSPFinancesHandler) ListServiceFees(c *gin.Context) { + fees, err := h.service.ListServiceFees() + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "service fees retrieved", "data": fees}) +} + +func (h *ZSPFinancesHandler) UpdateServiceFee(c *gin.Context) { + id := c.Param("id") + var req dto.ZSPServiceFeesUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.UpdateServiceFee(id, &req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "service fee updated"}) +} + +func (h *ZSPFinancesHandler) DeleteServiceFee(c *gin.Context) { + id := c.Param("id") + err := h.service.DeleteServiceFee(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "service fee deleted"}) +} + +// ============ 成本构成 ============ +func (h *ZSPFinancesHandler) CreateCostBreakdown(c *gin.Context) { + var req dto.ZSPCostBreakdownCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.CreateCostBreakdown(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "cost breakdown created"}) +} + +func (h *ZSPFinancesHandler) GetCostBreakdown(c *gin.Context) { + id := c.Param("id") + breakdown, err := h.service.GetCostBreakdown(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "cost breakdown retrieved", "data": breakdown}) +} + +func (h *ZSPFinancesHandler) ListCostBreakdowns(c *gin.Context) { + businessID := c.Query("businessId") + breakdowns, err := h.service.ListCostBreakdowns(businessID) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "cost breakdowns retrieved", "data": breakdowns}) +} + +func (h *ZSPFinancesHandler) UpdateCostBreakdown(c *gin.Context) { + id := c.Param("id") + var req dto.ZSPCostBreakdownUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.UpdateCostBreakdown(id, &req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "cost breakdown updated"}) +} + +func (h *ZSPFinancesHandler) DeleteCostBreakdown(c *gin.Context) { + id := c.Param("id") + err := h.service.DeleteCostBreakdown(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "cost breakdown deleted"}) +} + +// ============ 利润记录 ============ +func (h *ZSPFinancesHandler) CreateProfitRecord(c *gin.Context) { + var req dto.ZSPProfitRecordCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.CreateProfitRecord(&req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "profit record created"}) +} + +func (h *ZSPFinancesHandler) GetProfitRecord(c *gin.Context) { + id := c.Param("id") + record, err := h.service.GetProfitRecord(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "profit record retrieved", "data": record}) +} + +func (h *ZSPFinancesHandler) ListProfitRecords(c *gin.Context) { + filters := make(map[string]string) + if c.Query("businessId") != "" { + filters["businessId"] = c.Query("businessId") + } + if c.Query("businessType") != "" { + filters["businessType"] = c.Query("businessType") + } + records, err := h.service.ListProfitRecords(filters) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "profit records retrieved", "data": records}) +} + +func (h *ZSPFinancesHandler) UpdateProfitRecord(c *gin.Context) { + id := c.Param("id") + var req dto.ZSPProfitRecordUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"success": false, "message": err.Error()}) + return + } + err := h.service.UpdateProfitRecord(id, &req) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "profit record updated"}) +} + +func (h *ZSPFinancesHandler) DeleteProfitRecord(c *gin.Context) { + id := c.Param("id") + err := h.service.DeleteProfitRecord(id) + if err != nil { + c.JSON(500, gin.H{"success": false, "message": err.Error()}) + return + } + c.JSON(200, gin.H{"success": true, "message": "profit record deleted"}) +} diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go new file mode 100755 index 0000000..cd03a63 --- /dev/null +++ b/backend/internal/middleware/auth.go @@ -0,0 +1,48 @@ +// internal/middleware/auth.go +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" +) + +type Claims struct { + UserID uint `json:"user_id"` + jwt.RegisteredClaims +} + +func AuthMiddleware(jwtSecret string) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + c.Abort() + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == authHeader { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token is required"}) + c.Abort() + return + } + + claims := &Claims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { + return []byte(jwtSecret), nil + }) + + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + c.Set("userID", claims.ID) // 从token中解析出的用户ID + c.Next() + } +} diff --git a/backend/internal/model/company.go b/backend/internal/model/company.go new file mode 100755 index 0000000..f363c40 --- /dev/null +++ b/backend/internal/model/company.go @@ -0,0 +1,30 @@ +package model + +import "time" + +// CompanyFolder 公司信息文件夹 +type CompanyFolder struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Description string `gorm:"size:500" json:"description"` + Category string `gorm:"size:50" json:"category"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + + Files []CompanyFile `gorm:"foreignKey:FolderID" json:"files"` +} + +// CompanyFile 公司文件(营业执照、开票信息等) +type CompanyFile struct { + ID uint `gorm:"primaryKey" json:"id"` + FolderID uint `gorm:"index" json:"folderId"` + FileName string `gorm:"size:255;not null" json:"fileName"` + FileType string `gorm:"size:50" json:"fileType"` + FileSize int64 `json:"fileSize"` + UploadTime time.Time `json:"uploadTime"` + CompanyName string `gorm:"size:100" json:"companyName"` + FileCategory string `gorm:"size:50" json:"fileCategory"` // license, tax, organization, bank, invoice, charter, legal, other + ExpireDate *time.Time `json:"expireDate"` + Description string `gorm:"size:500" json:"description"` + FileURL string `gorm:"size:500" json:"fileUrl"` +} diff --git a/backend/internal/model/contract.go b/backend/internal/model/contract.go new file mode 100755 index 0000000..3c68b87 --- /dev/null +++ b/backend/internal/model/contract.go @@ -0,0 +1,25 @@ +package model + +import "time" + +type Contract struct { + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:100" json:"name"` + ContractType string `gorm:"size:50" json:"contractType"` + ContractCode string `gorm:"size:50" json:"contractCode"` + BuyParty string `gorm:"size:100" json:"buyParty"` + SellParty string `gorm:"size:100" json:"sellParty"` + Commodity string `gorm:"size:100" json:"commodity"` + Count float64 `gorm:"type:decimal(16,4);default:0" json:"count"` + UnitPrice float64 `gorm:"type:decimal(16,4);default:0" json:"unitPrice"` + BLNumber string `gorm:"size:500" json:"blNumber"` + DeliveryTime string `gorm:"size:50" json:"deliveryTime"` + PickUpTime string `gorm:"size:50" json:"pickUpTime"` + Packaging string `gorm:"size:50" json:"packaging"` + Remarks string `gorm:"size:255" json:"remarks"` + + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + + Files []ContractFile `gorm:"foreignKey:ContractID" json:"files"` +} diff --git a/backend/internal/model/contract_file.go b/backend/internal/model/contract_file.go new file mode 100755 index 0000000..2300fb6 --- /dev/null +++ b/backend/internal/model/contract_file.go @@ -0,0 +1,8 @@ +package model + +type ContractFile struct { + ID uint `gorm:"primarykey" json:"id"` + ContractID uint `gorm:"index" json:"contractId"` + Name string `gorm:"size:255" json:"name"` + URL string `gorm:"size:500" json:"url"` +} diff --git a/backend/internal/model/contract_folder.go b/backend/internal/model/contract_folder.go new file mode 100755 index 0000000..87e695d --- /dev/null +++ b/backend/internal/model/contract_folder.go @@ -0,0 +1,33 @@ +package model + +import "time" + +// ContractFolder 业务合同批量归档文件夹(与 Contract 共用体系,用于归档批量上传) +type ContractFolder struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Description string `gorm:"size:500" json:"description"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// ContractBatch 一次批量上传记录(归属某文件夹) +type ContractBatch struct { + ID uint `gorm:"primaryKey" json:"id"` + FolderID uint `gorm:"index;not null" json:"folderId"` + BatchName string `gorm:"size:200;not null" json:"batchName"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + + Files []ContractBatchFile `gorm:"foreignKey:BatchID" json:"files"` +} + +// ContractBatchFile 批量上传中的单个文件 +type ContractBatchFile struct { + ID uint `gorm:"primaryKey" json:"id"` + BatchID uint `gorm:"index;not null" json:"batchId"` + Name string `gorm:"size:255;not null" json:"name"` + URL string `gorm:"size:500;not null" json:"url"` + FileSize int64 `gorm:"default:0" json:"fileSize"` +} diff --git a/backend/internal/model/delivery.go b/backend/internal/model/delivery.go new file mode 100755 index 0000000..02c2234 --- /dev/null +++ b/backend/internal/model/delivery.go @@ -0,0 +1,115 @@ +package model + +import "time" + +// DeliveryApply 我要提货 - 提货委托 +type DeliveryApply struct { + ID uint `gorm:"primaryKey" json:"id"` + OrderNumber string `gorm:"size:50;uniqueIndex" json:"orderNumber"` + BLNumber string `gorm:"size:100" json:"blNumber"` + BLNumber2 string `gorm:"size:100" json:"blNumber2"` + LicensePlate string `gorm:"size:20" json:"licensePlate"` + DriverName string `gorm:"size:50" json:"driverName"` + DriverPhone string `gorm:"size:20" json:"driverPhone"` + DriverIdCard string `gorm:"size:30" json:"driverIdCard"` + Commodity string `gorm:"size:50" json:"commodity"` + Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"` + Unit string `gorm:"size:20" json:"unit"` + DeliveryDate string `gorm:"size:20" json:"deliveryDate"` + DeliveryAddress string `gorm:"size:200" json:"deliveryAddress"` + Warehouse string `gorm:"size:100" json:"warehouse"` + Status string `gorm:"size:20;default:pending" json:"status"` // pending, approved, in_progress, completed, cancelled + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// DeliveryOutbound 提货明细 - 出库单 +type DeliveryOutbound struct { + ID uint `gorm:"primaryKey" json:"id"` + OutboundNumber string `gorm:"size:50;uniqueIndex" json:"outboundNumber"` + DeliveryOrderNumber string `gorm:"size:50" json:"deliveryOrderNumber"` + BLNumber string `gorm:"size:100" json:"blNumber"` + LicensePlate string `gorm:"size:20" json:"licensePlate"` + DriverName string `gorm:"size:50" json:"driverName"` + DriverPhone string `gorm:"size:20" json:"driverPhone"` + Commodity string `gorm:"size:50" json:"commodity"` + CommodityCode string `gorm:"size:30" json:"commodityCode"` + Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"` + Unit string `gorm:"size:20" json:"unit"` + OutboundDate string `gorm:"size:20" json:"outboundDate"` + Warehouse string `gorm:"size:100" json:"warehouse"` + Operator string `gorm:"size:50" json:"operator"` + OutboundStatus string `gorm:"size:20;default:pending" json:"outboundStatus"` + ReceiptStatus string `gorm:"size:20;default:pending" json:"receiptStatus"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// Warehouse 仓库 +type Warehouse struct { + ID uint `gorm:"primaryKey" json:"id"` + Code string `gorm:"size:30;uniqueIndex" json:"code"` + Name string `gorm:"size:100" json:"name"` + Address string `gorm:"size:200" json:"address"` + ContactPerson string `gorm:"size:50" json:"contactPerson"` + ContactPhone string `gorm:"size:20" json:"contactPhone"` + Status string `gorm:"size:20;default:active" json:"status"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// InventoryInbound 入库单 +type InventoryInbound struct { + ID uint `gorm:"primaryKey" json:"id"` + InboundNumber string `gorm:"size:50;uniqueIndex" json:"inboundNumber"` + Warehouse string `gorm:"size:100" json:"warehouse"` + Commodity string `gorm:"size:50" json:"commodity"` + CommodityCode string `gorm:"size:30" json:"commodityCode"` + BLNumber string `gorm:"size:100" json:"blNumber"` + BLWeight float64 `gorm:"type:decimal(16,4)" json:"blWeight"` + InboundWeight float64 `gorm:"type:decimal(16,4)" json:"inboundWeight"` + Owner string `gorm:"size:100" json:"owner"` + ContractNumber string `gorm:"size:50" json:"contractNumber"` + InboundDate string `gorm:"size:20" json:"inboundDate"` + Operator string `gorm:"size:50" json:"operator"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// OwnershipTransfer 货权转移 +type OwnershipTransfer struct { + ID uint `gorm:"primaryKey" json:"id"` + TransferNumber string `gorm:"size:50;uniqueIndex" json:"transferNumber"` + TransferDate string `gorm:"size:20" json:"transferDate"` + Commodity string `gorm:"size:50" json:"commodity"` + Warehouse string `gorm:"size:100" json:"warehouse"` + Transferor string `gorm:"size:100" json:"transferor"` + Transferee string `gorm:"size:100" json:"transferee"` + Status string `gorm:"size:20;default:draft" json:"status"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + + BLItems []OwnershipTransferBL `gorm:"foreignKey:TransferID" json:"blItems"` + Files []OwnershipTransferFile `gorm:"foreignKey:TransferID" json:"fileList"` +} + +// OwnershipTransferBL 货权转移 - 提单明细(支持动态多条) +type OwnershipTransferBL struct { + ID uint `gorm:"primaryKey" json:"id"` + TransferID uint `gorm:"index;not null" json:"transferId"` + BLNumber string `gorm:"size:100" json:"blNumber"` + Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"` +} + +// OwnershipTransferFile 货权转移附件 +type OwnershipTransferFile struct { + ID uint `gorm:"primaryKey" json:"id"` + TransferID uint `gorm:"index" json:"transferId"` + Name string `gorm:"size:255" json:"name"` + URL string `gorm:"size:500" json:"url"` +} diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go new file mode 100755 index 0000000..5b71847 --- /dev/null +++ b/backend/internal/model/user.go @@ -0,0 +1,14 @@ +package model + +import ( + "time" +) + +type User struct { + ID uint `gorm:"primarykey" json:"id"` + Username string `gorm:"uniqueIndex;size:50" json:"username"` + Email string `gorm:"uniqueIndex;size:100" json:"email"` + Password string `gorm:"size:255" json:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/backend/internal/model/zsp_contract.go b/backend/internal/model/zsp_contract.go new file mode 100755 index 0000000..29d5b7e --- /dev/null +++ b/backend/internal/model/zsp_contract.go @@ -0,0 +1,99 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +// { +// id: "1", +// fileName: "上游合同-铜矿采购-2024-001.pdf", +// fileType: "pdf", +// fileSize: "2.3MB", +// uploadTime: "2024-01-20", +// folderId: "1", +// contractNumber: "SC2024001", +// companyName: "中尚鹏贸易有限公司", +// commodity: "铜矿", +// signDate: "2024-01-15", +// contractType: "upstream" +// } + +type ZSPContractFolder struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100" json:"name"` + Count int `json:"count"` + CreateTime time.Time `json:"createTime"` + Description string `gorm:"size:255" json:"description"` + + Files []ZSPContractFile `gorm:"foreignKey:FolderID" json:"files"` +} + +type ZSPContractFile struct { + ID uint `gorm:"primaryKey" json:"id"` + FileName string `gorm:"size:255" json:"fileName"` + FileURL string `gorm:"size:500" json:"fileUrl"` + FileType string `gorm:"size:50" json:"fileType"` + FileSize int64 `json:"fileSize"` + UploadTime time.Time `json:"uploadTime"` + + FolderID uint `gorm:"index" json:"folderId"` + Folder ZSPContractFolder `gorm:"foreignKey:FolderID" json:"folder"` + + ContractNumber string `gorm:"size:50" json:"contractNumber"` + CompanyName string `gorm:"size:100" json:"companyName"` + Commodity string `gorm:"size:100" json:"commodity"` + SignDate time.Time `json:"signDate"` + ContractType string `gorm:"size:50" json:"contractType"` + + // 合同详情 + Details []ZSPContractDetail `gorm:"foreignKey:ContractFileID" json:"details"` +} + +// ZSPContractDetail 合同详情表(商品明细) +type ZSPContractDetail struct { + ID uint `gorm:"primaryKey" json:"id"` + ContractFileID uint `gorm:"index;not null" json:"contractFileId"` + ContractFile ZSPContractFile `gorm:"foreignKey:ContractFileID" json:"contractFile"` + + // 商品信息 + CommodityName string `gorm:"size:100;not null" json:"commodityName"` // 商品名称 + CommodityCode string `gorm:"size:50" json:"commodityCode"` // 商品编码 + + // 数量信息 + TotalQuantity float64 `gorm:"not null" json:"totalQuantity"` // 合同总量 + Unit string `gorm:"size:20;default:'吨'" json:"unit"` // 单位 + + // 价格信息 + UnitPrice float64 `gorm:"not null" json:"unitPrice"` // 单价 + + // 提货信息 + DeliveredQty float64 `gorm:"default:0" json:"deliveredQty"` // 已提货数量 + PendingQty float64 `gorm:"default:0" json:"pendingQty"` // 应提货数量(自动计算 = 总量 - 已提货) + + // 提单信息 + BillOfLading string `gorm:"size:100" json:"billOfLading"` // 提单号 + + // 备注 + Remark string `gorm:"size:500" json:"remark"` + + // 时间戳 + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// BeforeCreate GORM钩子,在创建前自动计算应提货数量 +func (d *ZSPContractDetail) BeforeCreate(tx *gorm.DB) error { + d.PendingQty = d.TotalQuantity - d.DeliveredQty + d.CreateTime = time.Now() + d.UpdateTime = time.Now() + return nil +} + +// BeforeUpdate GORM钩子,在更新前自动计算应提货数量 +func (d *ZSPContractDetail) BeforeUpdate(tx *gorm.DB) error { + d.PendingQty = d.TotalQuantity - d.DeliveredQty + d.UpdateTime = time.Now() + return nil +} diff --git a/backend/internal/model/zsp_freight_charges.go b/backend/internal/model/zsp_freight_charges.go new file mode 100755 index 0000000..85d2b5e --- /dev/null +++ b/backend/internal/model/zsp_freight_charges.go @@ -0,0 +1,77 @@ +package model + +import "time" + +// ZSPFreightCharges 货代费用表 +type ZSPFreightCharges struct { + ID uint `gorm:"primaryKey" json:"id"` + BillOfLading string `gorm:"size:100;index" json:"billOfLading"` + ContractNumber string `gorm:"size:100;index" json:"contractNumber"` + ExpenseItem string `gorm:"size:200;not null" json:"expenseItem"` + Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + PaymentDate time.Time `gorm:"not null" json:"paymentDate"` + FreightCompanyName string `gorm:"size:200;not null" json:"freightCompanyName"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// ZSPMiscCharges 其他杂费表 +type ZSPMiscCharges struct { + ID uint `gorm:"primaryKey" json:"id"` + BillOfLading string `gorm:"size:100;index" json:"billOfLading"` + ContractNumber string `gorm:"size:100;index" json:"contractNumber"` + ExpenseItem string `gorm:"size:200;not null" json:"expenseItem"` + Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + PaymentDate time.Time `gorm:"not null" json:"paymentDate"` + CompanyName string `gorm:"size:200" json:"companyName"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// ZSPServiceFees 服务费表 +type ZSPServiceFees struct { + ID uint `gorm:"primaryKey" json:"id"` + BillOfLading string `gorm:"size:100;index" json:"billOfLading"` + ContractNumber string `gorm:"size:100;index" json:"contractNumber"` + Name string `gorm:"size:100;not null" json:"name"` + Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + Date time.Time `gorm:"not null" json:"date"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// ZSPCostBreakdown 成本构成明细表 +type ZSPCostBreakdown struct { + ID uint `gorm:"primaryKey" json:"id"` + BusinessID string `gorm:"size:100;not null;index" json:"businessId"` + BusinessType string `gorm:"size:50;not null;index" json:"businessType"` + TotalCost float64 `gorm:"type:decimal(16,4);not null" json:"totalCost"` + + FreightChargeTotal float64 `gorm:"type:decimal(16,4);default:0" json:"freightChargeTotal"` + MiscChargeTotal float64 `gorm:"type:decimal(16,4);default:0" json:"miscChargeTotal"` + ServiceFeeTotal float64 `gorm:"type:decimal(16,4);default:0" json:"serviceFeeTotal"` + + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// ZSPProfitRecord 利润记录表 +type ZSPProfitRecord struct { + ID uint `gorm:"primaryKey" json:"id"` + BusinessID string `gorm:"size:100;not null;index" json:"businessId"` + BusinessType string `gorm:"size:50;not null;index" json:"businessType"` + Revenue float64 `gorm:"type:decimal(16,4);not null" json:"revenue"` + TotalCost float64 `gorm:"type:decimal(16,4);not null" json:"totalCost"` + Profit float64 `gorm:"type:decimal(16,4);not null" json:"profit"` + ProfitRate float64 `gorm:"type:decimal(8,4)" json:"profitRate"` + RecordDate time.Time `gorm:"not null" json:"recordDate"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} diff --git a/backend/internal/model/zsp_invoice.go b/backend/internal/model/zsp_invoice.go new file mode 100755 index 0000000..856bc7f --- /dev/null +++ b/backend/internal/model/zsp_invoice.go @@ -0,0 +1,63 @@ +package model + +import "time" + +// ZSPUpstreamInvoice 上游发票表 +type ZSPUpstreamInvoice struct { + ID uint `gorm:"primaryKey" json:"id"` + InvoiceNumber string `gorm:"size:100;uniqueIndex;not null" json:"invoice_number"` + ContractNumber string `gorm:"size:100;index" json:"contract_number"` + BillOfLading string `gorm:"size:100;index" json:"bill_of_lading"` + SupplierName string `gorm:"size:200;not null" json:"supplier_name"` + Commodity string `gorm:"size:100" json:"commodity"` + Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"` + TaxAmount float64 `gorm:"type:decimal(16,4);default:0" json:"tax_amount"` + TotalAmount float64 `gorm:"type:decimal(16,4);not null" json:"total_amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + InvoiceDate time.Time `gorm:"not null" json:"invoice_date"` + Status string `gorm:"size:20;default:'pending'" json:"status"` + FilePath string `gorm:"size:500" json:"file_path"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` +} + +// ZSPDownstreamInvoice 下游发票表 +type ZSPDownstreamInvoice struct { + ID uint `gorm:"primaryKey" json:"id"` + InvoiceNumber string `gorm:"size:100;uniqueIndex;not null" json:"invoice_number"` + ContractNumber string `gorm:"size:100;index" json:"contract_number"` + BillOfLading string `gorm:"size:100;index" json:"bill_of_lading"` + CustomerName string `gorm:"size:200;not null" json:"customer_name"` + Commodity string `gorm:"size:100" json:"commodity"` + Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"` + TaxAmount float64 `gorm:"type:decimal(16,4);default:0" json:"tax_amount"` + TotalAmount float64 `gorm:"type:decimal(16,4);not null" json:"total_amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + InvoiceDate time.Time `gorm:"not null" json:"invoice_date"` + Status string `gorm:"size:20;default:'pending'" json:"status"` + FilePath string `gorm:"size:500" json:"file_path"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` +} + +// ZSPFreightMiscInvoice 货代杂费发票表 +type ZSPFreightMiscInvoice struct { + ID uint `gorm:"primaryKey" json:"id"` + InvoiceNumber string `gorm:"size:100;uniqueIndex;not null" json:"invoice_number"` + ContractNumber string `gorm:"size:100;index" json:"contract_number"` + BillOfLading string `gorm:"size:100;index" json:"bill_of_lading"` + CompanyName string `gorm:"size:200;not null" json:"company_name"` + ExpenseItem string `gorm:"size:200" json:"expense_item"` + Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"` + TaxAmount float64 `gorm:"type:decimal(16,4);default:0" json:"tax_amount"` + TotalAmount float64 `gorm:"type:decimal(16,4);not null" json:"total_amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + InvoiceDate time.Time `gorm:"not null" json:"invoice_date"` + Status string `gorm:"size:20;default:'pending'" json:"status"` + FilePath string `gorm:"size:500" json:"file_path"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` +} diff --git a/backend/internal/model/zsp_settlement.go b/backend/internal/model/zsp_settlement.go new file mode 100755 index 0000000..d92a5ce --- /dev/null +++ b/backend/internal/model/zsp_settlement.go @@ -0,0 +1,42 @@ +package model + +import "time" + +// ZSPSettlement 结算单表 +type ZSPSettlement struct { + ID uint `gorm:"primaryKey" json:"id"` + SettlementNumber string `gorm:"size:100;uniqueIndex;not null" json:"settlement_number"` + ContractNumber string `gorm:"size:100;index;not null" json:"contract_number"` + ContractType string `gorm:"size:50" json:"contract_type"` + CompanyName string `gorm:"size:200;not null" json:"company_name"` + Commodity string `gorm:"size:100" json:"commodity"` + Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"` + UnitPrice float64 `gorm:"type:decimal(16,4)" json:"unit_price"` + Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + SettlementDate time.Time `gorm:"not null" json:"settlement_date"` + Status string `gorm:"size:20;default:'pending'" json:"status"` + FilePath string `gorm:"size:500" json:"file_path"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` +} + +// ZSPReconciliation 对账单表 +type ZSPReconciliation struct { + ID uint `gorm:"primaryKey" json:"id"` + ReconciliationNumber string `gorm:"size:100;uniqueIndex;not null" json:"reconciliation_number"` + ContractNumber string `gorm:"size:100;index" json:"contract_number"` + CompanyName string `gorm:"size:200;not null" json:"company_name"` + PeriodStart time.Time `json:"period_start"` + PeriodEnd time.Time `json:"period_end"` + TotalAmount float64 `gorm:"type:decimal(16,4)" json:"total_amount"` + PaidAmount float64 `gorm:"type:decimal(16,4)" json:"paid_amount"` + UnpaidAmount float64 `gorm:"type:decimal(16,4)" json:"unpaid_amount"` + Currency string `gorm:"size:10;default:'CNY'" json:"currency"` + Status string `gorm:"size:20;default:'draft'" json:"status"` + FilePath string `gorm:"size:500" json:"file_path"` + Remark string `gorm:"size:500" json:"remark"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` +} diff --git a/backend/internal/repository/company_repository.go b/backend/internal/repository/company_repository.go new file mode 100755 index 0000000..bb5a4ca --- /dev/null +++ b/backend/internal/repository/company_repository.go @@ -0,0 +1,86 @@ +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type CompanyRepository interface { + CreateFolder(f *model.CompanyFolder) error + ListFolders() ([]model.CompanyFolder, error) + GetFolderByID(id uint) (*model.CompanyFolder, error) + UpdateFolder(f *model.CompanyFolder) error + DeleteFolder(id uint) error + + CreateFile(f *model.CompanyFile) error + ListFiles(folderID *uint) ([]model.CompanyFile, error) + GetFileByID(id uint) (*model.CompanyFile, error) + UpdateFile(f *model.CompanyFile) error + DeleteFile(id uint) error +} + +type companyRepository struct { + db *gorm.DB +} + +func NewCompanyRepository(db *gorm.DB) CompanyRepository { + return &companyRepository{db: db} +} + +func (r *companyRepository) CreateFolder(f *model.CompanyFolder) error { + return r.db.Create(f).Error +} + +func (r *companyRepository) ListFolders() ([]model.CompanyFolder, error) { + var list []model.CompanyFolder + err := r.db.Order("create_time DESC").Find(&list).Error + return list, err +} + +func (r *companyRepository) GetFolderByID(id uint) (*model.CompanyFolder, error) { + var f model.CompanyFolder + err := r.db.First(&f, id).Error + if err != nil { + return nil, err + } + return &f, nil +} + +func (r *companyRepository) UpdateFolder(f *model.CompanyFolder) error { + return r.db.Save(f).Error +} + +func (r *companyRepository) DeleteFolder(id uint) error { + return r.db.Delete(&model.CompanyFolder{}, id).Error +} + +func (r *companyRepository) CreateFile(f *model.CompanyFile) error { + return r.db.Create(f).Error +} + +func (r *companyRepository) ListFiles(folderID *uint) ([]model.CompanyFile, error) { + var list []model.CompanyFile + q := r.db.Order("upload_time DESC") + if folderID != nil && *folderID > 0 { + q = q.Where("folder_id = ?", *folderID) + } + err := q.Find(&list).Error + return list, err +} + +func (r *companyRepository) GetFileByID(id uint) (*model.CompanyFile, error) { + var f model.CompanyFile + err := r.db.First(&f, id).Error + if err != nil { + return nil, err + } + return &f, nil +} + +func (r *companyRepository) UpdateFile(f *model.CompanyFile) error { + return r.db.Save(f).Error +} + +func (r *companyRepository) DeleteFile(id uint) error { + return r.db.Delete(&model.CompanyFile{}, id).Error +} diff --git a/backend/internal/repository/contract_batch_repository.go b/backend/internal/repository/contract_batch_repository.go new file mode 100755 index 0000000..a69cd4e --- /dev/null +++ b/backend/internal/repository/contract_batch_repository.go @@ -0,0 +1,65 @@ +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type ContractBatchRepository interface { + List(folderID *uint) ([]model.ContractBatch, error) + GetByID(id uint) (*model.ContractBatch, error) + Create(b *model.ContractBatch, files []model.ContractBatchFile) error + Delete(id uint) error +} + +type contractBatchRepository struct { + db *gorm.DB +} + +func NewContractBatchRepository(db *gorm.DB) ContractBatchRepository { + return &contractBatchRepository{db: db} +} + +func (r *contractBatchRepository) List(folderID *uint) ([]model.ContractBatch, error) { + var list []model.ContractBatch + query := r.db.Model(&model.ContractBatch{}).Order("create_time DESC") + if folderID != nil && *folderID > 0 { + query = query.Where("folder_id = ?", *folderID) + } + err := query.Preload("Files").Find(&list).Error + return list, err +} + +func (r *contractBatchRepository) GetByID(id uint) (*model.ContractBatch, error) { + var b model.ContractBatch + if err := r.db.Preload("Files").First(&b, id).Error; err != nil { + return nil, err + } + return &b, nil +} + +func (r *contractBatchRepository) Create(b *model.ContractBatch, files []model.ContractBatchFile) error { + return r.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(b).Error; err != nil { + return err + } + for i := range files { + files[i].BatchID = b.ID + } + if len(files) > 0 { + if err := tx.Create(&files).Error; err != nil { + return err + } + } + return nil + }) +} + +func (r *contractBatchRepository) Delete(id uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("batch_id = ?", id).Delete(&model.ContractBatchFile{}).Error; err != nil { + return err + } + return tx.Delete(&model.ContractBatch{}, id).Error + }) +} diff --git a/backend/internal/repository/contract_folder_repository.go b/backend/internal/repository/contract_folder_repository.go new file mode 100755 index 0000000..2406ede --- /dev/null +++ b/backend/internal/repository/contract_folder_repository.go @@ -0,0 +1,48 @@ +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type ContractFolderRepository interface { + List() ([]model.ContractFolder, error) + GetByID(id uint) (*model.ContractFolder, error) + Create(f *model.ContractFolder) error + Update(f *model.ContractFolder) error + Delete(id uint) error +} + +type contractFolderRepository struct { + db *gorm.DB +} + +func NewContractFolderRepository(db *gorm.DB) ContractFolderRepository { + return &contractFolderRepository{db: db} +} + +func (r *contractFolderRepository) List() ([]model.ContractFolder, error) { + var list []model.ContractFolder + err := r.db.Order("create_time DESC").Find(&list).Error + return list, err +} + +func (r *contractFolderRepository) GetByID(id uint) (*model.ContractFolder, error) { + var f model.ContractFolder + if err := r.db.First(&f, id).Error; err != nil { + return nil, err + } + return &f, nil +} + +func (r *contractFolderRepository) Create(f *model.ContractFolder) error { + return r.db.Create(f).Error +} + +func (r *contractFolderRepository) Update(f *model.ContractFolder) error { + return r.db.Save(f).Error +} + +func (r *contractFolderRepository) Delete(id uint) error { + return r.db.Delete(&model.ContractFolder{}, id).Error +} diff --git a/backend/internal/repository/contract_repository.go b/backend/internal/repository/contract_repository.go new file mode 100755 index 0000000..c7ba1c1 --- /dev/null +++ b/backend/internal/repository/contract_repository.go @@ -0,0 +1,120 @@ +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type ContractRepository interface { + CreateWithFiles(contract *model.Contract, files []model.ContractFile) error + CreateFiles(files []model.ContractFile) error + BatchInsert(files []model.ContractFile) error + List(contractType string) ([]model.Contract, error) + GetByID(id uint) (*model.Contract, error) + Update(contract *model.Contract) error + Delete(id uint) error + DeleteFilesByContractIDExcept(contractID uint, keepIDs []uint) error + UpdateWithFiles(contract *model.Contract, keepFileIDs []uint, newFiles []model.ContractFile) error +} + +type contractRepository struct { + db *gorm.DB +} + +func NewContractRepository(db *gorm.DB) ContractRepository { + return &contractRepository{db: db} +} + +func (r *contractRepository) CreateFiles(files []model.ContractFile) error { + return r.db.Create(&files).Error +} + +func (r *contractRepository) CreateWithFiles(contract *model.Contract, files []model.ContractFile) error { + return r.db.Transaction(func(tx *gorm.DB) error { + + if err := tx.Create(contract).Error; err != nil { + return err + } + + for i := range files { + files[i].ContractID = contract.ID + } + + if len(files) > 0 { + if err := tx.Create(&files).Error; err != nil { + return err + } + } + + return nil + }) +} + +func (r *contractRepository) BatchInsert(files []model.ContractFile) error { + return r.db.Create(&files).Error +} + +func (r *contractRepository) List(contractType string) ([]model.Contract, error) { + var list []model.Contract + query := r.db.Model(&model.Contract{}).Order("create_time DESC") + if contractType != "" { + query = query.Where("contract_type = ?", contractType) + } + err := query.Preload("Files").Find(&list).Error + return list, err +} + +func (r *contractRepository) GetByID(id uint) (*model.Contract, error) { + var c model.Contract + err := r.db.Preload("Files").First(&c, id).Error + if err != nil { + return nil, err + } + return &c, nil +} + +func (r *contractRepository) Update(contract *model.Contract) error { + return r.db.Save(contract).Error +} + +func (r *contractRepository) Delete(id uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("contract_id = ?", id).Delete(&model.ContractFile{}).Error; err != nil { + return err + } + return tx.Delete(&model.Contract{}, id).Error + }) +} + +func (r *contractRepository) DeleteFilesByContractIDExcept(contractID uint, keepIDs []uint) error { + if len(keepIDs) == 0 { + return r.db.Where("contract_id = ?", contractID).Delete(&model.ContractFile{}).Error + } + return r.db.Where("contract_id = ? AND id NOT IN ?", contractID, keepIDs).Delete(&model.ContractFile{}).Error +} + +func (r *contractRepository) UpdateWithFiles(contract *model.Contract, keepFileIDs []uint, newFiles []model.ContractFile) error { + return r.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(contract).Error; err != nil { + return err + } + if len(keepFileIDs) == 0 { + if err := tx.Where("contract_id = ?", contract.ID).Delete(&model.ContractFile{}).Error; err != nil { + return err + } + } else { + if err := tx.Where("contract_id = ? AND id NOT IN ?", contract.ID, keepFileIDs).Delete(&model.ContractFile{}).Error; err != nil { + return err + } + } + for i := range newFiles { + newFiles[i].ContractID = contract.ID + } + if len(newFiles) > 0 { + if err := tx.Create(&newFiles).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/backend/internal/repository/delivery_repository.go b/backend/internal/repository/delivery_repository.go new file mode 100755 index 0000000..a41b7bd --- /dev/null +++ b/backend/internal/repository/delivery_repository.go @@ -0,0 +1,269 @@ +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +// ----- 我要提货 ----- +func CreateDeliveryApply(db *gorm.DB, e *model.DeliveryApply) error { return db.Create(e).Error } +func ListDeliveryApply(db *gorm.DB, page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) { + var list []model.DeliveryApply + q := db.Model(&model.DeliveryApply{}) + if orderNumber != "" { + q = q.Where("order_number LIKE ?", "%"+orderNumber+"%") + } + if blNumber != "" { + q = q.Where("bl_number LIKE ?", "%"+blNumber+"%") + } + if status != "" { + q = q.Where("status = ?", status) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + offset := (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + if pageSize <= 0 { + pageSize = 10 + } + err := q.Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error + return list, total, err +} +func GetDeliveryApplyByID(db *gorm.DB, id uint) (*model.DeliveryApply, error) { + var e model.DeliveryApply + if err := db.First(&e, id).Error; err != nil { + return nil, err + } + return &e, nil +} +func UpdateDeliveryApply(db *gorm.DB, e *model.DeliveryApply) error { return db.Save(e).Error } +func DeleteDeliveryApply(db *gorm.DB, id uint) error { return db.Delete(&model.DeliveryApply{}, id).Error } + +// ----- 提货明细/出库单 ----- +func CreateDeliveryOutbound(db *gorm.DB, e *model.DeliveryOutbound) error { return db.Create(e).Error } +func ListDeliveryOutbound(db *gorm.DB, page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) { + var list []model.DeliveryOutbound + q := db.Model(&model.DeliveryOutbound{}) + if outboundNumber != "" { + q = q.Where("outbound_number LIKE ?", "%"+outboundNumber+"%") + } + if blNumber != "" { + q = q.Where("bl_number LIKE ?", "%"+blNumber+"%") + } + if status != "" { + q = q.Where("outbound_status = ?", status) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + offset := (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + if pageSize <= 0 { + pageSize = 10 + } + err := q.Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error + return list, total, err +} +func GetDeliveryOutboundByID(db *gorm.DB, id uint) (*model.DeliveryOutbound, error) { + var e model.DeliveryOutbound + if err := db.First(&e, id).Error; err != nil { + return nil, err + } + return &e, nil +} +func UpdateDeliveryOutbound(db *gorm.DB, e *model.DeliveryOutbound) error { return db.Save(e).Error } +func DeleteDeliveryOutbound(db *gorm.DB, id uint) error { return db.Delete(&model.DeliveryOutbound{}, id).Error } + +// ----- 仓库 ----- +func CreateWarehouse(db *gorm.DB, e *model.Warehouse) error { return db.Create(e).Error } +func ListWarehouses(db *gorm.DB) ([]model.Warehouse, error) { var list []model.Warehouse; err := db.Order("create_time DESC").Find(&list).Error; return list, err } +func GetWarehouseByID(db *gorm.DB, id uint) (*model.Warehouse, error) { + var e model.Warehouse + if err := db.First(&e, id).Error; err != nil { + return nil, err + } + return &e, nil +} +func UpdateWarehouse(db *gorm.DB, e *model.Warehouse) error { return db.Save(e).Error } +func DeleteWarehouse(db *gorm.DB, id uint) error { return db.Delete(&model.Warehouse{}, id).Error } + +// ----- 入库单 ----- +func CreateInventoryInbound(db *gorm.DB, e *model.InventoryInbound) error { return db.Create(e).Error } +func ListInventoryInbound(db *gorm.DB, page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) { + var list []model.InventoryInbound + q := db.Model(&model.InventoryInbound{}) + if warehouse != "" { + q = q.Where("warehouse = ?", warehouse) + } + if commodity != "" { + q = q.Where("commodity LIKE ?", "%"+commodity+"%") + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + offset := (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + if pageSize <= 0 { + pageSize = 10 + } + err := q.Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error + return list, total, err +} +func GetInventoryInboundByID(db *gorm.DB, id uint) (*model.InventoryInbound, error) { + var e model.InventoryInbound + if err := db.First(&e, id).Error; err != nil { + return nil, err + } + return &e, nil +} +func UpdateInventoryInbound(db *gorm.DB, e *model.InventoryInbound) error { return db.Save(e).Error } +func DeleteInventoryInbound(db *gorm.DB, id uint) error { return db.Delete(&model.InventoryInbound{}, id).Error } + +// ----- 货权转移 ----- +func CreateOwnershipTransfer(db *gorm.DB, e *model.OwnershipTransfer) error { + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(e).Error; err != nil { + return err + } + // Set TransferID for BL items after main record is created + for i := range e.BLItems { + e.BLItems[i].TransferID = e.ID + } + if len(e.BLItems) > 0 { + return tx.Create(&e.BLItems).Error + } + return nil + }) +} +func ListOwnershipTransfer(db *gorm.DB, page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) { + var list []model.OwnershipTransfer + q := db.Model(&model.OwnershipTransfer{}) + if transferNumber != "" { + q = q.Where("transfer_number LIKE ?", "%"+transferNumber+"%") + } + if blNumber != "" { + q = q.Where("id IN (SELECT transfer_id FROM ownership_transfer_bls WHERE bl_number LIKE ?)", "%"+blNumber+"%") + } + if status != "" { + q = q.Where("status = ?", status) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + offset := (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + if pageSize <= 0 { + pageSize = 10 + } + err := q.Preload("Files").Preload("BLItems").Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error + return list, total, err +} +func GetOwnershipTransferByID(db *gorm.DB, id uint) (*model.OwnershipTransfer, error) { + var e model.OwnershipTransfer + if err := db.Preload("Files").Preload("BLItems").First(&e, id).Error; err != nil { + return nil, err + } + return &e, nil +} +func UpdateOwnershipTransfer(db *gorm.DB, e *model.OwnershipTransfer) error { + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(e).Error; err != nil { + return err + } + // Replace BL items: delete old, insert new + if err := tx.Where("transfer_id = ?", e.ID).Delete(&model.OwnershipTransferBL{}).Error; err != nil { + return err + } + for i := range e.BLItems { + e.BLItems[i].ID = 0 + e.BLItems[i].TransferID = e.ID + } + if len(e.BLItems) > 0 { + return tx.Create(&e.BLItems).Error + } + return nil + }) +} +func DeleteOwnershipTransfer(db *gorm.DB, id uint) error { + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("transfer_id = ?", id).Delete(&model.OwnershipTransferFile{}).Error; err != nil { + return err + } + if err := tx.Where("transfer_id = ?", id).Delete(&model.OwnershipTransferBL{}).Error; err != nil { + return err + } + return tx.Delete(&model.OwnershipTransfer{}, id).Error + }) +} +func CreateOwnershipTransferFiles(db *gorm.DB, files []model.OwnershipTransferFile) error { + if len(files) == 0 { + return nil + } + return db.Create(&files).Error +} + +// DeliveryRepository wraps *gorm.DB and exposes all delivery-related repository methods +type DeliveryRepository struct { + db *gorm.DB +} + +func NewDeliveryRepository(db *gorm.DB) *DeliveryRepository { + return &DeliveryRepository{db: db} +} + +func (r *DeliveryRepository) CreateDeliveryApply(e *model.DeliveryApply) error { return CreateDeliveryApply(r.db, e) } +func (r *DeliveryRepository) ListDeliveryApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) { + return ListDeliveryApply(r.db, page, pageSize, orderNumber, blNumber, status) +} +func (r *DeliveryRepository) GetDeliveryApplyByID(id uint) (*model.DeliveryApply, error) { return GetDeliveryApplyByID(r.db, id) } +func (r *DeliveryRepository) UpdateDeliveryApply(e *model.DeliveryApply) error { return UpdateDeliveryApply(r.db, e) } +func (r *DeliveryRepository) DeleteDeliveryApply(id uint) error { return DeleteDeliveryApply(r.db, id) } + +func (r *DeliveryRepository) CreateDeliveryOutbound(e *model.DeliveryOutbound) error { return CreateDeliveryOutbound(r.db, e) } +func (r *DeliveryRepository) ListDeliveryOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) { + return ListDeliveryOutbound(r.db, page, pageSize, outboundNumber, blNumber, status) +} +func (r *DeliveryRepository) GetDeliveryOutboundByID(id uint) (*model.DeliveryOutbound, error) { return GetDeliveryOutboundByID(r.db, id) } +func (r *DeliveryRepository) UpdateDeliveryOutbound(e *model.DeliveryOutbound) error { return UpdateDeliveryOutbound(r.db, e) } +func (r *DeliveryRepository) DeleteDeliveryOutbound(id uint) error { return DeleteDeliveryOutbound(r.db, id) } + +func (r *DeliveryRepository) CreateWarehouse(e *model.Warehouse) error { return CreateWarehouse(r.db, e) } +func (r *DeliveryRepository) ListWarehouses() ([]model.Warehouse, error) { return ListWarehouses(r.db) } +func (r *DeliveryRepository) GetWarehouseByID(id uint) (*model.Warehouse, error) { return GetWarehouseByID(r.db, id) } +func (r *DeliveryRepository) UpdateWarehouse(e *model.Warehouse) error { return UpdateWarehouse(r.db, e) } +func (r *DeliveryRepository) DeleteWarehouse(id uint) error { return DeleteWarehouse(r.db, id) } + +func (r *DeliveryRepository) CreateInventoryInbound(e *model.InventoryInbound) error { return CreateInventoryInbound(r.db, e) } +func (r *DeliveryRepository) ListInventoryInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) { + return ListInventoryInbound(r.db, page, pageSize, warehouse, commodity) +} +func (r *DeliveryRepository) GetInventoryInboundByID(id uint) (*model.InventoryInbound, error) { return GetInventoryInboundByID(r.db, id) } +func (r *DeliveryRepository) UpdateInventoryInbound(e *model.InventoryInbound) error { return UpdateInventoryInbound(r.db, e) } +func (r *DeliveryRepository) DeleteInventoryInbound(id uint) error { return DeleteInventoryInbound(r.db, id) } + +func (r *DeliveryRepository) CreateOwnershipTransfer(e *model.OwnershipTransfer) error { return CreateOwnershipTransfer(r.db, e) } +func (r *DeliveryRepository) ListOwnershipTransfer(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) { + return ListOwnershipTransfer(r.db, page, pageSize, transferNumber, blNumber, status) +} +func (r *DeliveryRepository) GetOwnershipTransferByID(id uint) (*model.OwnershipTransfer, error) { return GetOwnershipTransferByID(r.db, id) } +func (r *DeliveryRepository) UpdateOwnershipTransfer(e *model.OwnershipTransfer) error { return UpdateOwnershipTransfer(r.db, e) } +func (r *DeliveryRepository) DeleteOwnershipTransfer(id uint) error { return DeleteOwnershipTransfer(r.db, id) } +func (r *DeliveryRepository) CreateOwnershipTransferFiles(files []model.OwnershipTransferFile) error { + return CreateOwnershipTransferFiles(r.db, files) +} + +// DB returns the underlying *gorm.DB for complex queries (e.g. InventorySummary) +func (r *DeliveryRepository) DB() *gorm.DB { return r.db } diff --git a/backend/internal/repository/user_repository.go b/backend/internal/repository/user_repository.go new file mode 100755 index 0000000..c5a8607 --- /dev/null +++ b/backend/internal/repository/user_repository.go @@ -0,0 +1,66 @@ +// internal/repository/user_repository.go +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type UserRepository interface { + Create(user *model.User) error + FindByID(id uint) (*model.User, error) + FindByEmail(email string) (*model.User, error) + FindByUsername(username string) (*model.User, error) + Update(user *model.User) error + Delete(id uint) error +} + +type userRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{db: db} +} + +func (r *userRepository) Create(user *model.User) error { + return r.db.Create(user).Error +} + +func (r *userRepository) FindByID(id uint) (*model.User, error) { + var user model.User + err := r.db.First(&user, id).Error + return &user, err +} + +func (r *userRepository) FindByUsername(username string) (*model.User, error) { + var user model.User + err := r.db.Where("username = ?", username).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} +func (r *userRepository) FindByEmail(email string) (*model.User, error) { + var user model.User + err := r.db.Where("email = ?", email).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + + return &user, nil +} + +func (r *userRepository) Update(user *model.User) error { + return r.db.Save(user).Error +} + +func (r *userRepository) Delete(id uint) error { + return r.db.Delete(&model.User{}, id).Error +} diff --git a/backend/internal/repository/zsp_contract_repository.go b/backend/internal/repository/zsp_contract_repository.go new file mode 100755 index 0000000..b1d86ce --- /dev/null +++ b/backend/internal/repository/zsp_contract_repository.go @@ -0,0 +1,129 @@ +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type ZSPContractRepository interface { + CreateFolder(folder *model.ZSPContractFolder) error + GetFolders() ([]model.ZSPContractFolder, error) + GetFolderById(folderID string) (*model.ZSPContractFolder, error) + DeleteFolderById(folderID string) error + UpdateFolder(folder *model.ZSPContractFolder) error + + CreateContract(contract *model.ZSPContractFile) error + GetContractById(contractID string) (*model.ZSPContractFile, error) + ListContracts(folderID string) ([]model.ZSPContractFile, error) + ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) + DeleteContractById(contractID string) error + UpdateContract(contract *model.ZSPContractFile) error + + // 合同详情相关 + CreateContractDetail(detail *model.ZSPContractDetail) error + GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) + ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) + UpdateContractDetail(detail *model.ZSPContractDetail) error + DeleteContractDetailById(detailID string) error + BatchCreateContractDetails(details []model.ZSPContractDetail) error +} + +type zspContractRepository struct { + db *gorm.DB +} + +func NewZSPContractRepository(db *gorm.DB) ZSPContractRepository { + return &zspContractRepository{db: db} +} + +func (r *zspContractRepository) CreateFolder(folder *model.ZSPContractFolder) error { + // 这里可以根据实际需求创建一个 Folder 模型并保存到数据库 + return r.db.Create(folder).Error +} + +func (r *zspContractRepository) GetFolders() ([]model.ZSPContractFolder, error) { + var folders []model.ZSPContractFolder + err := r.db.Find(&folders).Error + return folders, err +} + +func (r *zspContractRepository) GetFolderById(folderID string) (*model.ZSPContractFolder, error) { + var folder model.ZSPContractFolder + err := r.db.First(&folder, folderID).Error + return &folder, err +} + +func (r *zspContractRepository) DeleteFolderById(folderID string) error { + return r.db.Delete(&model.ZSPContractFolder{}, folderID).Error +} + +func (r *zspContractRepository) UpdateFolder(folder *model.ZSPContractFolder) error { + return r.db.Save(folder).Error +} + +func (r *zspContractRepository) CreateContract(contract *model.ZSPContractFile) error { + return r.db.Create(contract).Error +} + +func (r *zspContractRepository) GetContractById(contractID string) (*model.ZSPContractFile, error) { + var contract model.ZSPContractFile + err := r.db.First(&contract, contractID).Error + return &contract, err +} + +func (r *zspContractRepository) ListContracts(folderID string) ([]model.ZSPContractFile, error) { + var contracts []model.ZSPContractFile + err := r.db.Where("folder_id = ?", folderID).Find(&contracts).Error + return contracts, err +} + +func (r *zspContractRepository) DeleteContractById(contractID string) error { + return r.db.Delete(&model.ZSPContractFile{}, contractID).Error +} + +func (r *zspContractRepository) UpdateContract(contract *model.ZSPContractFile) error { + return r.db.Save(contract).Error +} + +func (r *zspContractRepository) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) { + var contracts []model.ZSPContractFile + query := r.db.Preload("Details") + if folderID != "" { + query = query.Where("folder_id = ?", folderID) + } + err := query.Find(&contracts).Error + return contracts, err +} + +// ============ 合同详情相关方法 ============ + +func (r *zspContractRepository) CreateContractDetail(detail *model.ZSPContractDetail) error { + return r.db.Create(detail).Error +} + +func (r *zspContractRepository) GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) { + var detail model.ZSPContractDetail + err := r.db.First(&detail, detailID).Error + return &detail, err +} + +func (r *zspContractRepository) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) { + var details []model.ZSPContractDetail + err := r.db.Where("contract_file_id = ?", contractFileID).Find(&details).Error + return details, err +} + +func (r *zspContractRepository) UpdateContractDetail(detail *model.ZSPContractDetail) error { + return r.db.Save(detail).Error +} + +func (r *zspContractRepository) DeleteContractDetailById(detailID string) error { + return r.db.Delete(&model.ZSPContractDetail{}, detailID).Error +} + +func (r *zspContractRepository) BatchCreateContractDetails(details []model.ZSPContractDetail) error { + if len(details) == 0 { + return nil + } + return r.db.Create(&details).Error +} diff --git a/backend/internal/repository/zsp_finances.go b/backend/internal/repository/zsp_finances.go new file mode 100755 index 0000000..a7042b6 --- /dev/null +++ b/backend/internal/repository/zsp_finances.go @@ -0,0 +1,182 @@ +package repository + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +type ZSPFinancesRepository interface { + CreateFreightCharge(charge *model.ZSPFreightCharges) error + GetFreightCharge(id string) (*model.ZSPFreightCharges, error) + ListFreightCharges() ([]model.ZSPFreightCharges, error) + UpdateFreightCharge(charge *model.ZSPFreightCharges) error + DeleteFreightCharge(id string) error + + CreateMiscCharge(charge *model.ZSPMiscCharges) error + GetMiscCharge(id string) (*model.ZSPMiscCharges, error) + ListMiscCharges() ([]model.ZSPMiscCharges, error) + UpdateMiscCharge(charge *model.ZSPMiscCharges) error + DeleteMiscCharge(id string) error + + CreateServiceFee(fee *model.ZSPServiceFees) error + GetServiceFee(id string) (*model.ZSPServiceFees, error) + ListServiceFees() ([]model.ZSPServiceFees, error) + UpdateServiceFee(fee *model.ZSPServiceFees) error + DeleteServiceFee(id string) error + + CreateCostBreakdown(breakdown *model.ZSPCostBreakdown) error + GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) + ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) + UpdateCostBreakdown(breakdown *model.ZSPCostBreakdown) error + DeleteCostBreakdown(id string) error + + CreateProfitRecord(record *model.ZSPProfitRecord) error + GetProfitRecord(id string) (*model.ZSPProfitRecord, error) + ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) + UpdateProfitRecord(id string, req *model.ZSPProfitRecord) error + DeleteProfitRecord(id string) error +} + +type zspFinancesRepository struct { + db *gorm.DB +} + +func NewZSPFinancesRepository(db *gorm.DB) ZSPFinancesRepository { + return &zspFinancesRepository{db: db} +} + +// ============ 货代费用 ============ +func (r *zspFinancesRepository) CreateFreightCharge(charge *model.ZSPFreightCharges) error { + return r.db.Create(charge).Error +} + +func (r *zspFinancesRepository) GetFreightCharge(id string) (*model.ZSPFreightCharges, error) { + var charge model.ZSPFreightCharges + err := r.db.First(&charge, id).Error + return &charge, err +} + +func (r *zspFinancesRepository) ListFreightCharges() ([]model.ZSPFreightCharges, error) { + var charges []model.ZSPFreightCharges + err := r.db.Order("create_time desc").Find(&charges).Error + return charges, err +} + +func (r *zspFinancesRepository) UpdateFreightCharge(charge *model.ZSPFreightCharges) error { + return r.db.Save(charge).Error +} + +func (r *zspFinancesRepository) DeleteFreightCharge(id string) error { + return r.db.Delete(&model.ZSPFreightCharges{}, id).Error +} + +// ============ 杂费 ============ +func (r *zspFinancesRepository) CreateMiscCharge(charge *model.ZSPMiscCharges) error { + return r.db.Create(charge).Error +} + +func (r *zspFinancesRepository) GetMiscCharge(id string) (*model.ZSPMiscCharges, error) { + var charge model.ZSPMiscCharges + err := r.db.First(&charge, id).Error + return &charge, err +} + +func (r *zspFinancesRepository) ListMiscCharges() ([]model.ZSPMiscCharges, error) { + var charges []model.ZSPMiscCharges + err := r.db.Order("create_time desc").Find(&charges).Error + return charges, err +} + +func (r *zspFinancesRepository) UpdateMiscCharge(charge *model.ZSPMiscCharges) error { + return r.db.Save(charge).Error +} + +func (r *zspFinancesRepository) DeleteMiscCharge(id string) error { + return r.db.Delete(&model.ZSPMiscCharges{}, id).Error +} + +// ============ 服务费 ============ +func (r *zspFinancesRepository) CreateServiceFee(fee *model.ZSPServiceFees) error { + return r.db.Create(fee).Error +} + +func (r *zspFinancesRepository) GetServiceFee(id string) (*model.ZSPServiceFees, error) { + var fee model.ZSPServiceFees + err := r.db.First(&fee, id).Error + return &fee, err +} + +func (r *zspFinancesRepository) ListServiceFees() ([]model.ZSPServiceFees, error) { + var fees []model.ZSPServiceFees + err := r.db.Order("create_time desc").Find(&fees).Error + return fees, err +} + +func (r *zspFinancesRepository) UpdateServiceFee(fee *model.ZSPServiceFees) error { + return r.db.Save(fee).Error +} + +func (r *zspFinancesRepository) DeleteServiceFee(id string) error { + return r.db.Delete(&model.ZSPServiceFees{}, id).Error +} + +// ============ 成本构成 ============ +func (r *zspFinancesRepository) CreateCostBreakdown(breakdown *model.ZSPCostBreakdown) error { + return r.db.Create(breakdown).Error +} + +func (r *zspFinancesRepository) GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) { + var breakdown model.ZSPCostBreakdown + err := r.db.First(&breakdown, id).Error + return &breakdown, err +} + +func (r *zspFinancesRepository) ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) { + var breakdowns []model.ZSPCostBreakdown + query := r.db.Model(&model.ZSPCostBreakdown{}) + if businessID != "" { + query = query.Where("business_id = ?", businessID) + } + err := query.Order("create_time desc").Find(&breakdowns).Error + return breakdowns, err +} + +func (r *zspFinancesRepository) UpdateCostBreakdown(breakdown *model.ZSPCostBreakdown) error { + return r.db.Save(breakdown).Error +} + +func (r *zspFinancesRepository) DeleteCostBreakdown(id string) error { + return r.db.Delete(&model.ZSPCostBreakdown{}, id).Error +} + +// ============ 利润记录 ============ +func (r *zspFinancesRepository) CreateProfitRecord(record *model.ZSPProfitRecord) error { + return r.db.Create(record).Error +} + +func (r *zspFinancesRepository) GetProfitRecord(id string) (*model.ZSPProfitRecord, error) { + var record model.ZSPProfitRecord + err := r.db.First(&record, id).Error + return &record, err +} + +func (r *zspFinancesRepository) ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) { + var records []model.ZSPProfitRecord + query := r.db.Model(&model.ZSPProfitRecord{}) + if filters["businessId"] != "" { + query = query.Where("business_id = ?", filters["businessId"]) + } + if filters["businessType"] != "" { + query = query.Where("business_type = ?", filters["businessType"]) + } + err := query.Order("create_time desc").Find(&records).Error + return records, err +} + +func (r *zspFinancesRepository) UpdateProfitRecord(id string, req *model.ZSPProfitRecord) error { + return r.db.Save(req).Error +} + +func (r *zspFinancesRepository) DeleteProfitRecord(id string) error { + return r.db.Delete(&model.ZSPProfitRecord{}, id).Error +} diff --git a/backend/internal/server/route.go b/backend/internal/server/route.go new file mode 100755 index 0000000..b633ca7 --- /dev/null +++ b/backend/internal/server/route.go @@ -0,0 +1,190 @@ +package server + +import ( + "path/filepath" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/handler" +) + +func SetupRouter(userHandler *handler.UserHandler, + contractHandler *handler.ContractHandler, + zspContractHandler *handler.ZSPContractHandler, + zspFinancesHandler *handler.ZSPFinancesHandler, + companyHandler *handler.CompanyHandler, + deliveryHandler *handler.DeliveryHandler, + authMiddleware gin.HandlerFunc, +) *gin.Engine { + router := gin.Default() + router.Use(gin.Logger()) + router.Use(gin.Recovery()) + // 允许跨域,并放行 Authorization 等请求头(解决前端直连 8080 时的 CORS 报错) + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: false, + MaxAge: 12 * 3600, + })) + // user routes + api := router.Group("/api") + { + router.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) + auth := api.Group("/auth") + { + auth.POST("/register", userHandler.Register) + auth.POST("/login", userHandler.Login) + } + // 静态文件资源(公开访问,供前端预览/下载) + api.Static("/contract-files", filepath.Join(".", "data", "contract")) + api.Static("/zsp-contract-files", filepath.Join(".", "data", "zsp-contract")) + api.Static("/company-files", filepath.Join(".", "data", "company")) + + // 以下所有路由需要 JWT 认证 + authorized := api.Group("/") + authorized.Use(authMiddleware) + { + users := authorized.Group("/users") + { + users.GET("/:id", userHandler.GetUser) + } + + contracts := authorized.Group("/contract") + { + contracts.GET("", contractHandler.List) + contracts.POST("/upload", contractHandler.Upload) + contracts.POST("/batch-upload", contractHandler.BatchUpload) + contracts.GET("/folders", contractHandler.ListFolders) + contracts.POST("/folders", contractHandler.CreateFolder) + contracts.GET("/folders/:id", contractHandler.GetFolder) + contracts.PUT("/folders/:id", contractHandler.UpdateFolder) + contracts.DELETE("/folders/:id", contractHandler.DeleteFolder) + contracts.GET("/batches", contractHandler.ListBatches) + contracts.POST("/batches", contractHandler.CreateBatch) + contracts.GET("/batches/:id", contractHandler.GetBatch) + contracts.DELETE("/batches/:id", contractHandler.DeleteBatch) + contracts.POST("/:id/update-files", contractHandler.UpdateWithFiles) + contracts.GET("/:id", contractHandler.Get) + contracts.PUT("/:id", contractHandler.Update) + contracts.DELETE("/:id", contractHandler.Delete) + } + + zspContracts := authorized.Group("/zsp-contract") + { + zspContracts.POST("/folders", zspContractHandler.CreateFolder) + zspContracts.GET("/folders", zspContractHandler.GetFolders) + zspContracts.GET("/folders/:id", zspContractHandler.GetFolder) + zspContracts.DELETE("/folders/:id", zspContractHandler.DeleteFolder) + zspContracts.PUT("/folders/:id", zspContractHandler.UpdateFolder) + + zspContracts.POST("/contracts", zspContractHandler.UploadContract) + zspContracts.GET("/contracts/:id", zspContractHandler.GetContractWithDetails) + zspContracts.GET("/contracts", zspContractHandler.ListContractsWithDetails) + zspContracts.PUT("/contracts/:id", zspContractHandler.UpdateContract) + zspContracts.DELETE("/contracts/:id", zspContractHandler.DeleteContract) + + zspContracts.POST("/contracts/:id/details", zspContractHandler.CreateContractDetail) + zspContracts.GET("/contracts/:id/details", zspContractHandler.ListContractDetails) + zspContracts.POST("/details/batch", zspContractHandler.BatchCreateContractDetails) + zspContracts.GET("/details/:id", zspContractHandler.GetContractDetail) + zspContracts.PUT("/details/:id", zspContractHandler.UpdateContractDetail) + zspContracts.DELETE("/details/:id", zspContractHandler.DeleteContractDetail) + zspContracts.POST("/contracts/:id/details/import", zspContractHandler.ImportContractDetailsFromExcel) + zspContracts.GET("/contracts/:id/details/export", zspContractHandler.ExportContractDetailsToExcel) + } + + company := authorized.Group("/company") + { + company.GET("/folders", companyHandler.ListFolders) + company.POST("/folders", companyHandler.CreateFolder) + company.PUT("/folders/:id", companyHandler.UpdateFolder) + company.DELETE("/folders/:id", companyHandler.DeleteFolder) + company.GET("/files", companyHandler.ListFiles) + company.POST("/files/upload", companyHandler.UploadFiles) + company.PUT("/files/:id", companyHandler.UpdateFile) + company.DELETE("/files/:id", companyHandler.DeleteFile) + } + + // 提货管理 (RESTful) + applyDelivery := authorized.Group("/apply-delivery") + { + applyDelivery.GET("", deliveryHandler.ListApply) + applyDelivery.GET("/:id", deliveryHandler.GetApply) + applyDelivery.POST("", deliveryHandler.CreateApply) + applyDelivery.PUT("/:id", deliveryHandler.UpdateApply) + applyDelivery.PUT("/:id/cancel", deliveryHandler.CancelApply) + applyDelivery.DELETE("/:id", deliveryHandler.DeleteApply) + } + deliveryDetails := authorized.Group("/delivery-details") + { + deliveryDetails.GET("", deliveryHandler.ListOutbound) + deliveryDetails.GET("/:id", deliveryHandler.GetOutbound) + deliveryDetails.POST("", deliveryHandler.CreateOutbound) + deliveryDetails.PUT("/:id", deliveryHandler.UpdateOutbound) + deliveryDetails.DELETE("/:id", deliveryHandler.DeleteOutbound) + } + inventory := authorized.Group("/inventory") + { + inventory.GET("/warehouses", deliveryHandler.ListWarehouses) + inventory.POST("/warehouses", deliveryHandler.CreateWarehouse) + inventory.PUT("/warehouses/:id", deliveryHandler.UpdateWarehouse) + inventory.DELETE("/warehouses/:id", deliveryHandler.DeleteWarehouse) + inventory.GET("/inbound", deliveryHandler.ListInbound) + inventory.POST("/inbound", deliveryHandler.CreateInbound) + inventory.PUT("/inbound/:id", deliveryHandler.UpdateInbound) + inventory.DELETE("/inbound/:id", deliveryHandler.DeleteInbound) + inventory.GET("/summary", deliveryHandler.InventorySummary) + } + ownershipTransfer := authorized.Group("/ownership-transfer") + { + ownershipTransfer.GET("", deliveryHandler.ListOwnership) + ownershipTransfer.GET("/:id", deliveryHandler.GetOwnership) + ownershipTransfer.POST("", deliveryHandler.CreateOwnership) + ownershipTransfer.POST("/with-files", deliveryHandler.CreateOwnershipWithFiles) + ownershipTransfer.DELETE("/:id", deliveryHandler.DeleteOwnership) + } + + zspFinances := authorized.Group("/zsp-finances") + { + // 货代费用 + zspFinances.POST("/freight-charges", zspFinancesHandler.CreateFreightCharge) + zspFinances.GET("/freight-charges/:id", zspFinancesHandler.GetFreightCharge) + zspFinances.GET("/freight-charges", zspFinancesHandler.ListFreightCharges) + zspFinances.PUT("/freight-charges/:id", zspFinancesHandler.UpdateFreightCharge) + zspFinances.DELETE("/freight-charges/:id", zspFinancesHandler.DeleteFreightCharge) + + // 杂费 + zspFinances.POST("/misc-charges", zspFinancesHandler.CreateMiscCharge) + zspFinances.GET("/misc-charges/:id", zspFinancesHandler.GetMiscCharge) + zspFinances.GET("/misc-charges", zspFinancesHandler.ListMiscCharges) + zspFinances.PUT("/misc-charges/:id", zspFinancesHandler.UpdateMiscCharge) + zspFinances.DELETE("/misc-charges/:id", zspFinancesHandler.DeleteMiscCharge) + + // 服务费 + zspFinances.POST("/service-fees", zspFinancesHandler.CreateServiceFee) + zspFinances.GET("/service-fees/:id", zspFinancesHandler.GetServiceFee) + zspFinances.GET("/service-fees", zspFinancesHandler.ListServiceFees) + zspFinances.PUT("/service-fees/:id", zspFinancesHandler.UpdateServiceFee) + zspFinances.DELETE("/service-fees/:id", zspFinancesHandler.DeleteServiceFee) + + // 成本构成 + zspFinances.POST("/cost-breakdowns", zspFinancesHandler.CreateCostBreakdown) + zspFinances.GET("/cost-breakdowns/:id", zspFinancesHandler.GetCostBreakdown) + zspFinances.GET("/cost-breakdowns", zspFinancesHandler.ListCostBreakdowns) + zspFinances.PUT("/cost-breakdowns/:id", zspFinancesHandler.UpdateCostBreakdown) + zspFinances.DELETE("/cost-breakdowns/:id", zspFinancesHandler.DeleteCostBreakdown) + + // 利润记录 + zspFinances.POST("/profit-records", zspFinancesHandler.CreateProfitRecord) + zspFinances.GET("/profit-records/:id", zspFinancesHandler.GetProfitRecord) + zspFinances.GET("/profit-records", zspFinancesHandler.ListProfitRecords) + zspFinances.PUT("/profit-records/:id", zspFinancesHandler.UpdateProfitRecord) + zspFinances.DELETE("/profit-records/:id", zspFinancesHandler.DeleteProfitRecord) + } + } // end authorized + + } // end api + return router +} diff --git a/backend/internal/service/company_service.go b/backend/internal/service/company_service.go new file mode 100755 index 0000000..33f49d5 --- /dev/null +++ b/backend/internal/service/company_service.go @@ -0,0 +1,156 @@ +package service + +import ( + "fmt" + "mime/multipart" + "os" + "path/filepath" + "time" + + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" + "github.com/rovina/zsp-backend/pkg/utils" +) + +type CompanyService interface { + CreateFolder(name, description, category string) (*model.CompanyFolder, error) + ListFolders() ([]model.CompanyFolder, error) + GetFolderByID(id uint) (*model.CompanyFolder, error) + UpdateFolder(id uint, name, description, category string) error + DeleteFolder(id uint) error + + UploadFiles(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error) + ListFiles(folderID *uint) ([]model.CompanyFile, error) + GetFileByID(id uint) (*model.CompanyFile, error) + UpdateFile(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error + DeleteFile(id uint) error +} + +type companyService struct { + repo repository.CompanyRepository +} + +func NewCompanyService(repo repository.CompanyRepository) CompanyService { + return &companyService{repo: repo} +} + +func (s *companyService) CreateFolder(name, description, category string) (*model.CompanyFolder, error) { + f := &model.CompanyFolder{ + Name: name, + Description: description, + Category: category, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + if err := s.repo.CreateFolder(f); err != nil { + return nil, err + } + return f, nil +} + +func (s *companyService) ListFolders() ([]model.CompanyFolder, error) { + return s.repo.ListFolders() +} + +func (s *companyService) GetFolderByID(id uint) (*model.CompanyFolder, error) { + return s.repo.GetFolderByID(id) +} + +func (s *companyService) UpdateFolder(id uint, name, description, category string) error { + f, err := s.repo.GetFolderByID(id) + if err != nil { + return err + } + f.Name = name + f.Description = description + f.Category = category + f.UpdateTime = time.Now() + return s.repo.UpdateFolder(f) +} + +func (s *companyService) DeleteFolder(id uint) error { + return s.repo.DeleteFolder(id) +} + +func (s *companyService) UploadFiles(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error) { + basePath := "./data/company/files" + if err := os.MkdirAll(basePath, os.ModePerm); err != nil { + return 0, err + } + + var expire *time.Time + if expireDate != "" { + if t, err := time.Parse("2006-01-02", expireDate); err == nil { + expire = &t + } + } + + count := 0 + for _, fh := range files { + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), fh.Filename) + savePath := filepath.Join(basePath, filename) + if err := utils.SaveUploadedFile(fh, savePath); err != nil { + return count, err + } + + ext := filepath.Ext(fh.Filename) + if len(ext) > 0 { + ext = ext[1:] + } + + file := &model.CompanyFile{ + FolderID: 0, + FileName: fh.Filename, + FileType: ext, + FileSize: fh.Size, + UploadTime: time.Now(), + CompanyName: companyName, + FileCategory: fileCategory, + ExpireDate: expire, + Description: description, + FileURL: "files/" + filename, + } + if folderID != nil && *folderID > 0 { + file.FolderID = *folderID + } + + if err := s.repo.CreateFile(file); err != nil { + return count, err + } + count++ + } + return count, nil +} + +func (s *companyService) ListFiles(folderID *uint) ([]model.CompanyFile, error) { + return s.repo.ListFiles(folderID) +} + +func (s *companyService) GetFileByID(id uint) (*model.CompanyFile, error) { + return s.repo.GetFileByID(id) +} + +func (s *companyService) UpdateFile(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error { + f, err := s.repo.GetFileByID(id) + if err != nil { + return err + } + f.CompanyName = companyName + f.FileCategory = fileCategory + f.Description = description + if expireDate != "" { + if t, err := time.Parse("2006-01-02", expireDate); err == nil { + f.ExpireDate = &t + } + } else { + f.ExpireDate = nil + } + if folderID != nil { + f.FolderID = *folderID + } + return s.repo.UpdateFile(f) +} + +func (s *companyService) DeleteFile(id uint) error { + return s.repo.DeleteFile(id) +} diff --git a/backend/internal/service/contract_batch_service.go b/backend/internal/service/contract_batch_service.go new file mode 100755 index 0000000..6f676f4 --- /dev/null +++ b/backend/internal/service/contract_batch_service.go @@ -0,0 +1,49 @@ +package service + +import ( + "time" + + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" +) + +type ContractBatchService interface { + List(folderID *uint) ([]model.ContractBatch, error) + GetByID(id uint) (*model.ContractBatch, error) + Create(folderID uint, batchName, remark string, files []model.ContractBatchFile) (*model.ContractBatch, error) + Delete(id uint) error +} + +type contractBatchService struct { + repo repository.ContractBatchRepository +} + +func NewContractBatchService(repo repository.ContractBatchRepository) ContractBatchService { + return &contractBatchService{repo: repo} +} + +func (s *contractBatchService) List(folderID *uint) ([]model.ContractBatch, error) { + return s.repo.List(folderID) +} + +func (s *contractBatchService) GetByID(id uint) (*model.ContractBatch, error) { + return s.repo.GetByID(id) +} + +func (s *contractBatchService) Create(folderID uint, batchName, remark string, files []model.ContractBatchFile) (*model.ContractBatch, error) { + b := &model.ContractBatch{ + FolderID: folderID, + BatchName: batchName, + Remark: remark, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + if err := s.repo.Create(b, files); err != nil { + return nil, err + } + return s.repo.GetByID(b.ID) +} + +func (s *contractBatchService) Delete(id uint) error { + return s.repo.Delete(id) +} diff --git a/backend/internal/service/contract_folder_service.go b/backend/internal/service/contract_folder_service.go new file mode 100755 index 0000000..e48ec3f --- /dev/null +++ b/backend/internal/service/contract_folder_service.go @@ -0,0 +1,60 @@ +package service + +import ( + "time" + + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" +) + +type ContractFolderService interface { + List() ([]model.ContractFolder, error) + GetByID(id uint) (*model.ContractFolder, error) + Create(name, description string) (*model.ContractFolder, error) + Update(id uint, name, description string) error + Delete(id uint) error +} + +type contractFolderService struct { + repo repository.ContractFolderRepository +} + +func NewContractFolderService(repo repository.ContractFolderRepository) ContractFolderService { + return &contractFolderService{repo: repo} +} + +func (s *contractFolderService) List() ([]model.ContractFolder, error) { + return s.repo.List() +} + +func (s *contractFolderService) GetByID(id uint) (*model.ContractFolder, error) { + return s.repo.GetByID(id) +} + +func (s *contractFolderService) Create(name, description string) (*model.ContractFolder, error) { + f := &model.ContractFolder{ + Name: name, + Description: description, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + if err := s.repo.Create(f); err != nil { + return nil, err + } + return f, nil +} + +func (s *contractFolderService) Update(id uint, name, description string) error { + f, err := s.repo.GetByID(id) + if err != nil { + return err + } + f.Name = name + f.Description = description + f.UpdateTime = time.Now() + return s.repo.Update(f) +} + +func (s *contractFolderService) Delete(id uint) error { + return s.repo.Delete(id) +} diff --git a/backend/internal/service/contract_service.go b/backend/internal/service/contract_service.go new file mode 100755 index 0000000..2c81fa1 --- /dev/null +++ b/backend/internal/service/contract_service.go @@ -0,0 +1,152 @@ +package service + +import ( + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" +) + +type ContractService interface { + CreateContract(contract *model.Contract, files []model.ContractFile) error + SaveFiles(files []model.ContractFile) error + BatchSaveFiles( + basePath string, + files []*multipart.FileHeader, + ) (int, error) + List(contractType string) ([]model.Contract, error) + GetByID(id uint) (*model.Contract, error) + Update(contract *model.Contract) error + UpdateWithFiles(id uint, keepFileIDs []uint, newFileModels []model.ContractFile, form map[string]string) error + Delete(id uint) error +} + +type contractService struct { + repo repository.ContractRepository +} + +func NewContractService(repo repository.ContractRepository) (ContractService, error) { + return &contractService{repo: repo}, nil +} + +func (s *contractService) CreateContract(contract *model.Contract, files []model.ContractFile) error { + return s.repo.CreateWithFiles(contract, files) +} + +func (s *contractService) SaveFiles(files []model.ContractFile) error { + return s.repo.CreateFiles(files) +} + +func (s *contractService) BatchSaveFiles( + basePath string, + files []*multipart.FileHeader, +) (int, error) { + + // 相对路径根目录 + root := filepath.Join("./data/contract/batch", basePath) + + if err := os.MkdirAll(root, os.ModePerm); err != nil { + return 0, err + } + + count := 0 + + for _, file := range files { + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename) + savePath := filepath.Join(root, filename) + + if err := saveUploadedFile(file, savePath); err != nil { + return count, err + } + + count++ + } + + return count, nil +} + +func (s *contractService) List(contractType string) ([]model.Contract, error) { + return s.repo.List(contractType) +} + +func (s *contractService) GetByID(id uint) (*model.Contract, error) { + return s.repo.GetByID(id) +} + +func (s *contractService) Update(contract *model.Contract) error { + return s.repo.Update(contract) +} + +func (s *contractService) UpdateWithFiles(id uint, keepFileIDs []uint, newFileModels []model.ContractFile, form map[string]string) error { + contract, err := s.repo.GetByID(id) + if err != nil { + return err + } + // 更新合同元信息 + get := func(k string) string { return form[k] } + if v := get("name"); v != "" { + contract.Name = v + } + if v := get("contractType"); v != "" { + contract.ContractType = v + } + if v := get("contractCode"); v != "" { + contract.ContractCode = v + } + if v := get("buyParty"); v != "" { + contract.BuyParty = v + } + if v := get("sellParty"); v != "" { + contract.SellParty = v + } + if v := get("commodity"); v != "" { + contract.Commodity = v + } + if v := get("count"); v != "" { + if n, e := strconv.ParseFloat(v, 64); e == nil { + contract.Count = n + } + } + if v := get("unitPrice"); v != "" { + if n, e := strconv.ParseFloat(v, 64); e == nil { + contract.UnitPrice = n + } + } + if v := get("blNumber"); v != "" { + contract.BLNumber = v + } + contract.DeliveryTime = get("deliveryTime") + contract.PickUpTime = get("pickUpTime") + contract.Packaging = get("packaging") + contract.Remarks = get("remarks") + contract.UpdateTime = time.Now() + + return s.repo.UpdateWithFiles(contract, keepFileIDs, newFileModels) +} + +func (s *contractService) Delete(id uint) error { + return s.repo.Delete(id) +} + +func saveUploadedFile(file *multipart.FileHeader, dst string) error { + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, src) + return err +} diff --git a/backend/internal/service/delivery_service.go b/backend/internal/service/delivery_service.go new file mode 100755 index 0000000..5782df0 --- /dev/null +++ b/backend/internal/service/delivery_service.go @@ -0,0 +1,153 @@ +package service + +import ( + "time" + + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" + "gorm.io/gorm" +) + +type DeliveryService interface { + // 我要提货 + ListApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) + GetApplyByID(id uint) (*model.DeliveryApply, error) + CreateApply(e *model.DeliveryApply) error + UpdateApply(e *model.DeliveryApply) error + DeleteApply(id uint) error + + // 出库单 + ListOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) + GetOutboundByID(id uint) (*model.DeliveryOutbound, error) + CreateOutbound(e *model.DeliveryOutbound) error + UpdateOutbound(e *model.DeliveryOutbound) error + DeleteOutbound(id uint) error + + // 仓库 + ListWarehouses() ([]model.Warehouse, error) + GetWarehouseByID(id uint) (*model.Warehouse, error) + CreateWarehouse(e *model.Warehouse) error + UpdateWarehouse(e *model.Warehouse) error + DeleteWarehouse(id uint) error + + // 入库 + ListInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) + GetInboundByID(id uint) (*model.InventoryInbound, error) + CreateInbound(e *model.InventoryInbound) error + UpdateInbound(e *model.InventoryInbound) error + DeleteInbound(id uint) error + + // 货权转移 + ListOwnership(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) + GetOwnershipByID(id uint) (*model.OwnershipTransfer, error) + CreateOwnership(e *model.OwnershipTransfer) error + DeleteOwnership(id uint) error + CreateOwnershipFiles(files []model.OwnershipTransferFile) error + + // DB for complex queries (InventorySummary) + DB() *gorm.DB +} + +type deliveryService struct { + repo *repository.DeliveryRepository +} + +func NewDeliveryService(repo *repository.DeliveryRepository) DeliveryService { + return &deliveryService{repo: repo} +} + +func (s *deliveryService) DB() *gorm.DB { return s.repo.DB() } + +func (s *deliveryService) ListApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) { + return s.repo.ListDeliveryApply(page, pageSize, orderNumber, blNumber, status) +} +func (s *deliveryService) GetApplyByID(id uint) (*model.DeliveryApply, error) { + return s.repo.GetDeliveryApplyByID(id) +} +func (s *deliveryService) CreateApply(e *model.DeliveryApply) error { + e.CreateTime = time.Now() + e.UpdateTime = time.Now() + return s.repo.CreateDeliveryApply(e) +} +func (s *deliveryService) UpdateApply(e *model.DeliveryApply) error { + e.UpdateTime = time.Now() + return s.repo.UpdateDeliveryApply(e) +} +func (s *deliveryService) DeleteApply(id uint) error { + return s.repo.DeleteDeliveryApply(id) +} + +func (s *deliveryService) ListOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) { + return s.repo.ListDeliveryOutbound(page, pageSize, outboundNumber, blNumber, status) +} +func (s *deliveryService) GetOutboundByID(id uint) (*model.DeliveryOutbound, error) { + return s.repo.GetDeliveryOutboundByID(id) +} +func (s *deliveryService) CreateOutbound(e *model.DeliveryOutbound) error { + e.CreateTime = time.Now() + e.UpdateTime = time.Now() + return s.repo.CreateDeliveryOutbound(e) +} +func (s *deliveryService) UpdateOutbound(e *model.DeliveryOutbound) error { + e.UpdateTime = time.Now() + return s.repo.UpdateDeliveryOutbound(e) +} +func (s *deliveryService) DeleteOutbound(id uint) error { + return s.repo.DeleteDeliveryOutbound(id) +} + +func (s *deliveryService) ListWarehouses() ([]model.Warehouse, error) { + return s.repo.ListWarehouses() +} +func (s *deliveryService) GetWarehouseByID(id uint) (*model.Warehouse, error) { + return s.repo.GetWarehouseByID(id) +} +func (s *deliveryService) CreateWarehouse(e *model.Warehouse) error { + e.CreateTime = time.Now() + e.UpdateTime = time.Now() + return s.repo.CreateWarehouse(e) +} +func (s *deliveryService) UpdateWarehouse(e *model.Warehouse) error { + e.UpdateTime = time.Now() + return s.repo.UpdateWarehouse(e) +} +func (s *deliveryService) DeleteWarehouse(id uint) error { + return s.repo.DeleteWarehouse(id) +} + +func (s *deliveryService) ListInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) { + return s.repo.ListInventoryInbound(page, pageSize, warehouse, commodity) +} +func (s *deliveryService) GetInboundByID(id uint) (*model.InventoryInbound, error) { + return s.repo.GetInventoryInboundByID(id) +} +func (s *deliveryService) CreateInbound(e *model.InventoryInbound) error { + e.CreateTime = time.Now() + e.UpdateTime = time.Now() + return s.repo.CreateInventoryInbound(e) +} +func (s *deliveryService) UpdateInbound(e *model.InventoryInbound) error { + e.UpdateTime = time.Now() + return s.repo.UpdateInventoryInbound(e) +} +func (s *deliveryService) DeleteInbound(id uint) error { + return s.repo.DeleteInventoryInbound(id) +} + +func (s *deliveryService) ListOwnership(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) { + return s.repo.ListOwnershipTransfer(page, pageSize, transferNumber, blNumber, status) +} +func (s *deliveryService) GetOwnershipByID(id uint) (*model.OwnershipTransfer, error) { + return s.repo.GetOwnershipTransferByID(id) +} +func (s *deliveryService) CreateOwnership(e *model.OwnershipTransfer) error { + e.CreateTime = time.Now() + e.UpdateTime = time.Now() + return s.repo.CreateOwnershipTransfer(e) +} +func (s *deliveryService) DeleteOwnership(id uint) error { + return s.repo.DeleteOwnershipTransfer(id) +} +func (s *deliveryService) CreateOwnershipFiles(files []model.OwnershipTransferFile) error { + return s.repo.CreateOwnershipTransferFiles(files) +} diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go new file mode 100755 index 0000000..371912c --- /dev/null +++ b/backend/internal/service/user_service.go @@ -0,0 +1,127 @@ +// internal/service/user_service.go +package service + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" + "golang.org/x/crypto/bcrypt" +) + +type Claims struct { + UserID uint `json:"user_id"` + jwt.RegisteredClaims +} + +type UserService interface { + Register(req *dto.CreateUserRequest) (*dto.UserResponse, error) + Login(req *dto.LoginRequest) (*dto.UserResponse, string, error) + GetUserByID(id uint) (*dto.UserResponse, error) +} + +type userService struct { + userRepo repository.UserRepository + jwtSecret []byte // 改为字节切片 +} + +func NewUserService(userRepo repository.UserRepository, jwtSecretStr string) (*userService, error) { + // 直接使用字符串作为密钥,不再解析 PEM + return &userService{ + userRepo: userRepo, + jwtSecret: []byte(jwtSecretStr), + }, nil +} + +func (s *userService) Register(req *dto.CreateUserRequest) (*dto.UserResponse, error) { + // 检查邮箱是否已存在 + existingUser, err := s.userRepo.FindByEmail(req.Email) + if err != nil { + return nil, fmt.Errorf("database error: %v", err) + } + if existingUser != nil { + return nil, errors.New("email already exists") + } + + existingUser, err = s.userRepo.FindByUsername(req.Username) + if err != nil { + return nil, fmt.Errorf("database error: %v", err) + } + if existingUser != nil { + return nil, errors.New("username already exists") + } + + // 加密密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + user := &model.User{ + Username: req.Username, + Email: req.Email, + Password: string(hashedPassword), + } + + if err := s.userRepo.Create(user); err != nil { + return nil, err + } + + return &dto.UserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + }, nil +} + +func (s *userService) Login(req *dto.LoginRequest) (*dto.UserResponse, string, error) { + user, err := s.userRepo.FindByEmail(req.Email) + if err != nil { + return nil, "", errors.New("invalid credentials") + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + return nil, "", errors.New("invalid credentials") + } + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := &Claims{ + UserID: user.ID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "zsp-backend", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(s.jwtSecret) + if err != nil { + return nil, "", err + } + + return &dto.UserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + }, tokenString, nil +} + +func (s *userService) GetUserByID(id uint) (*dto.UserResponse, error) { + user, err := s.userRepo.FindByID(id) + if err != nil { + return nil, err + } + + return &dto.UserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + }, nil +} diff --git a/backend/internal/service/zsp_contract_service.go b/backend/internal/service/zsp_contract_service.go new file mode 100755 index 0000000..8cf29a8 --- /dev/null +++ b/backend/internal/service/zsp_contract_service.go @@ -0,0 +1,271 @@ +package service + +import ( + "fmt" + "mime/multipart" + "os" + "path" + "strings" + "time" + + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" +) + +type ZSPContractService interface { + CreateFolder(req *dto.ZSPFolderCreateRequest) error + GetFolders() ([]model.ZSPContractFolder, error) + GetFolderById(folderID string) (*model.ZSPContractFolder, error) + DeleteFolderById(folderID string) error + UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error + + UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error + GetContractById(contractID string) (*model.ZSPContractFile, error) + GetContractWithDetails(contractID string) (*model.ZSPContractFile, error) + ListContracts(folderID string) ([]model.ZSPContractFile, error) + ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) + DeleteContractById(contractID string) error + UpdateContract(contract *model.ZSPContractFile) error + UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error + + // 合同详情相关 + CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error + UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error + GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) + ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) + DeleteContractDetailById(detailID string) error + BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error +} + +type zspContractService struct { + repo repository.ZSPContractRepository +} + +func NewZSPContractService(repo repository.ZSPContractRepository) (ZSPContractService, error) { + return &zspContractService{repo: repo}, nil +} + +func (s *zspContractService) CreateFolder(req *dto.ZSPFolderCreateRequest) error { + var folder model.ZSPContractFolder + folder.Name = req.Name + folder.Description = req.Description + folder.Count = 0 + folder.CreateTime = time.Now() + + return s.repo.CreateFolder(&folder) +} + +func (s *zspContractService) GetFolders() ([]model.ZSPContractFolder, error) { + folders, err := s.repo.GetFolders() + if err != nil { + return nil, err + } + return folders, nil +} + +func (s *zspContractService) GetFolderById(folderID string) (*model.ZSPContractFolder, error) { + return s.repo.GetFolderById(folderID) +} + +func (s *zspContractService) DeleteFolderById(folderID string) error { + return s.repo.DeleteFolderById(folderID) +} + +func (s *zspContractService) UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error { + folder, err := s.repo.GetFolderById(folderID) + if err != nil { + return err + } + + folder.Name = req.Name + folder.Description = req.Description + return s.repo.UpdateFolder(folder) +} + +func (s *zspContractService) UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error { + timeNow := time.Now() + var contract model.ZSPContractFile + contract.FolderID = req.FolderID + contract.Commodity = req.Commodity + contract.CompanyName = req.CompanyName + contract.ContractNumber = req.ContractNumber + contract.ContractType = req.ContractType + contract.FileName = fileHeader.Filename + // 提取扩展名便于前端展示(如 pdf、jpg) + if ext := path.Ext(fileHeader.Filename); ext != "" { + contract.FileType = strings.TrimPrefix(strings.ToLower(ext), ".") + } else { + contract.FileType = fileHeader.Header.Get("Content-Type") + } + if req.SignDate == "" { + return fmt.Errorf("签订日期为必填项") + } + signDate, err := time.Parse("2006-01-02", req.SignDate) + if err != nil { + return fmt.Errorf("签订日期格式不正确,请使用 YYYY-MM-DD: %w", err) + } + contract.SignDate = signDate + contract.UploadTime = timeNow + + // 确保目录存在后保存文件 + saveDir := "./data/zsp-contract/files" + if err := os.MkdirAll(saveDir, os.ModePerm); err != nil { + return fmt.Errorf("create upload dir: %w", err) + } + storedName := fmt.Sprintf("%d_%s", timeNow.UnixNano(), fileHeader.Filename) + savePath := path.Join(saveDir, storedName) + + if err := saveUploadedFile(fileHeader, savePath); err != nil { + return err + } + contract.FileURL = "files/" + storedName + contract.FileSize = fileHeader.Size + return s.repo.CreateContract(&contract) +} + +func (s *zspContractService) GetContractById(contractID string) (*model.ZSPContractFile, error) { + return s.repo.GetContractById(contractID) +} + +func (s *zspContractService) ListContracts(folderID string) ([]model.ZSPContractFile, error) { + return s.repo.ListContracts(folderID) +} + +func (s *zspContractService) DeleteContractById(contractID string) error { + return s.repo.DeleteContractById(contractID) +} + +func (s *zspContractService) UpdateContract(contract *model.ZSPContractFile) error { + return s.repo.UpdateContract(contract) +} + +func (s *zspContractService) UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error { + contract, err := s.repo.GetContractById(contractID) + if err != nil { + return err + } + if req.ContractNumber != "" { + contract.ContractNumber = req.ContractNumber + } + if req.CompanyName != "" { + contract.CompanyName = req.CompanyName + } + if req.Commodity != "" { + contract.Commodity = req.Commodity + } + if req.ContractType != "" { + contract.ContractType = req.ContractType + } + if req.FolderID > 0 { + contract.FolderID = req.FolderID + } + if req.SignDate != "" { + signDate, err := time.Parse("2006-01-02", req.SignDate) + if err != nil { + return fmt.Errorf("签订日期格式不正确: %w", err) + } + contract.SignDate = signDate + } + return s.repo.UpdateContract(contract) +} + +func (s *zspContractService) GetContractWithDetails(contractID string) (*model.ZSPContractFile, error) { + contract, err := s.repo.GetContractById(contractID) + if err != nil { + return nil, err + } + // 获取详情列表 + details, err := s.repo.ListContractDetails(contractID) + if err != nil { + return nil, err + } + contract.Details = details + return contract, nil +} + +func (s *zspContractService) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) { + return s.repo.ListContractsWithDetails(folderID) +} + +// ============ 合同详情相关方法 ============ + +func (s *zspContractService) CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error { + var detail model.ZSPContractDetail + detail.ContractFileID = req.ContractFileID + detail.CommodityName = req.CommodityName + detail.CommodityCode = req.CommodityCode + detail.TotalQuantity = req.TotalQuantity + detail.Unit = req.Unit + detail.UnitPrice = req.UnitPrice + detail.DeliveredQty = req.DeliveredQty + detail.BillOfLading = req.BillOfLading + detail.Remark = req.Remark + + // PendingQty 会在 BeforeCreate 钩子中自动计算 + return s.repo.CreateContractDetail(&detail) +} + +func (s *zspContractService) UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error { + detail, err := s.repo.GetContractDetailById(fmt.Sprintf("%d", req.ID)) + if err != nil { + return err + } + + if req.CommodityName != "" { + detail.CommodityName = req.CommodityName + } + if req.CommodityCode != "" { + detail.CommodityCode = req.CommodityCode + } + if req.TotalQuantity > 0 { + detail.TotalQuantity = req.TotalQuantity + } + if req.Unit != "" { + detail.Unit = req.Unit + } + if req.UnitPrice > 0 { + detail.UnitPrice = req.UnitPrice + } + // DeliveredQty 可以为 0,所以需要特别处理 + detail.DeliveredQty = req.DeliveredQty + if req.BillOfLading != "" { + detail.BillOfLading = req.BillOfLading + } + detail.Remark = req.Remark + + // PendingQty 会在 BeforeUpdate 钩子中自动计算 + return s.repo.UpdateContractDetail(detail) +} + +func (s *zspContractService) GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) { + return s.repo.GetContractDetailById(detailID) +} + +func (s *zspContractService) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) { + return s.repo.ListContractDetails(contractFileID) +} + +func (s *zspContractService) DeleteContractDetailById(detailID string) error { + return s.repo.DeleteContractDetailById(detailID) +} + +func (s *zspContractService) BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error { + details := make([]model.ZSPContractDetail, 0, len(req.Details)) + for _, detailReq := range req.Details { + detail := model.ZSPContractDetail{ + ContractFileID: req.ContractFileID, + CommodityName: detailReq.CommodityName, + CommodityCode: detailReq.CommodityCode, + TotalQuantity: detailReq.TotalQuantity, + Unit: detailReq.Unit, + UnitPrice: detailReq.UnitPrice, + DeliveredQty: detailReq.DeliveredQty, + BillOfLading: detailReq.BillOfLading, + Remark: detailReq.Remark, + } + // PendingQty 会在 BeforeCreate 钩子中自动计算 + details = append(details, detail) + } + return s.repo.BatchCreateContractDetails(details) +} diff --git a/backend/internal/service/zsp_finances_service.go b/backend/internal/service/zsp_finances_service.go new file mode 100755 index 0000000..019839e --- /dev/null +++ b/backend/internal/service/zsp_finances_service.go @@ -0,0 +1,335 @@ +package service + +import ( + "time" + + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/repository" + "gorm.io/gorm" +) + +type ZSPFinancesService interface { + CreateFreightCharge(req *dto.ZSPFreightChargesCreateRequest) error + GetFreightCharge(id string) (*model.ZSPFreightCharges, error) + ListFreightCharges() ([]model.ZSPFreightCharges, error) + UpdateFreightCharge(id string, req *dto.ZSPFreightChargesUpdateRequest) error + DeleteFreightCharge(id string) error + + CreateMiscCharge(req *dto.ZSPMiscChargesCreateRequest) error + GetMiscCharge(id string) (*model.ZSPMiscCharges, error) + ListMiscCharges() ([]model.ZSPMiscCharges, error) + UpdateMiscCharge(id string, req *dto.ZSPMiscChargesUpdateRequest) error + DeleteMiscCharge(id string) error + + CreateServiceFee(req *dto.ZSPServiceFeesCreateRequest) error + GetServiceFee(id string) (*model.ZSPServiceFees, error) + ListServiceFees() ([]model.ZSPServiceFees, error) + UpdateServiceFee(id string, req *dto.ZSPServiceFeesUpdateRequest) error + DeleteServiceFee(id string) error + + CreateCostBreakdown(req *dto.ZSPCostBreakdownCreateRequest) error + GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) + ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) + UpdateCostBreakdown(id string, req *dto.ZSPCostBreakdownUpdateRequest) error + DeleteCostBreakdown(id string) error + + CreateProfitRecord(req *dto.ZSPProfitRecordCreateRequest) error + GetProfitRecord(id string) (*model.ZSPProfitRecord, error) + ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) + UpdateProfitRecord(id string, req *dto.ZSPProfitRecordUpdateRequest) error + DeleteProfitRecord(id string) error +} + +type zspFinancesService struct { + repo repository.ZSPFinancesRepository +} + +func NewZSPFinancesService(db *gorm.DB) ZSPFinancesService { + return &zspFinancesService{repo: repository.NewZSPFinancesRepository(db)} +} + +// ============ 货代费用 ============ +func (s *zspFinancesService) CreateFreightCharge(req *dto.ZSPFreightChargesCreateRequest) error { + currency := req.Currency + if currency == "" { + currency = "CNY" + } + charge := model.ZSPFreightCharges{ + BillOfLading: req.BillOfLading, + ContractNumber: req.ContractNumber, + ExpenseItem: req.ExpenseItem, + Amount: req.Amount, + Currency: currency, + PaymentDate: parseDate(req.PaymentDate), + FreightCompanyName: req.FreightCompanyName, + Remark: req.Remark, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + return s.repo.CreateFreightCharge(&charge) +} + +func (s *zspFinancesService) GetFreightCharge(id string) (*model.ZSPFreightCharges, error) { + return s.repo.GetFreightCharge(id) +} + +func (s *zspFinancesService) ListFreightCharges() ([]model.ZSPFreightCharges, error) { + return s.repo.ListFreightCharges() +} + +func (s *zspFinancesService) UpdateFreightCharge(id string, req *dto.ZSPFreightChargesUpdateRequest) error { + charge, err := s.repo.GetFreightCharge(id) + if err != nil { + return err + } + if req.BillOfLading != "" { + charge.BillOfLading = req.BillOfLading + } + if req.ContractNumber != "" { + charge.ContractNumber = req.ContractNumber + } + if req.ExpenseItem != "" { + charge.ExpenseItem = req.ExpenseItem + } + if req.Amount != 0 { + charge.Amount = req.Amount + } + if req.Currency != "" { + charge.Currency = req.Currency + } + if req.PaymentDate != "" { + charge.PaymentDate = parseDate(req.PaymentDate) + } + if req.FreightCompanyName != "" { + charge.FreightCompanyName = req.FreightCompanyName + } + charge.Remark = req.Remark + charge.UpdateTime = time.Now() + return s.repo.UpdateFreightCharge(charge) +} + +func (s *zspFinancesService) DeleteFreightCharge(id string) error { + return s.repo.DeleteFreightCharge(id) +} + +// ============ 杂费 ============ +func (s *zspFinancesService) CreateMiscCharge(req *dto.ZSPMiscChargesCreateRequest) error { + currency := req.Currency + if currency == "" { + currency = "CNY" + } + charge := model.ZSPMiscCharges{ + BillOfLading: req.BillOfLading, + ContractNumber: req.ContractNumber, + ExpenseItem: req.ExpenseItem, + Amount: req.Amount, + Currency: currency, + PaymentDate: parseDate(req.PaymentDate), + CompanyName: req.CompanyName, + Remark: req.Remark, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + return s.repo.CreateMiscCharge(&charge) +} + +func (s *zspFinancesService) GetMiscCharge(id string) (*model.ZSPMiscCharges, error) { + return s.repo.GetMiscCharge(id) +} + +func (s *zspFinancesService) ListMiscCharges() ([]model.ZSPMiscCharges, error) { + return s.repo.ListMiscCharges() +} + +func (s *zspFinancesService) UpdateMiscCharge(id string, req *dto.ZSPMiscChargesUpdateRequest) error { + charge, err := s.repo.GetMiscCharge(id) + if err != nil { + return err + } + if req.BillOfLading != "" { + charge.BillOfLading = req.BillOfLading + } + if req.ContractNumber != "" { + charge.ContractNumber = req.ContractNumber + } + if req.ExpenseItem != "" { + charge.ExpenseItem = req.ExpenseItem + } + if req.Amount != 0 { + charge.Amount = req.Amount + } + if req.Currency != "" { + charge.Currency = req.Currency + } + if req.PaymentDate != "" { + charge.PaymentDate = parseDate(req.PaymentDate) + } + if req.CompanyName != "" { + charge.CompanyName = req.CompanyName + } + charge.Remark = req.Remark + charge.UpdateTime = time.Now() + return s.repo.UpdateMiscCharge(charge) +} + +func (s *zspFinancesService) DeleteMiscCharge(id string) error { + return s.repo.DeleteMiscCharge(id) +} + +// ============ 服务费 ============ +func (s *zspFinancesService) CreateServiceFee(req *dto.ZSPServiceFeesCreateRequest) error { + currency := req.Currency + if currency == "" { + currency = "CNY" + } + fee := model.ZSPServiceFees{ + BillOfLading: req.BillOfLading, + ContractNumber: req.ContractNumber, + Name: req.Name, + Amount: req.Amount, + Currency: currency, + Date: parseDate(req.Date), + Remark: req.Remark, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + return s.repo.CreateServiceFee(&fee) +} + +func (s *zspFinancesService) GetServiceFee(id string) (*model.ZSPServiceFees, error) { + return s.repo.GetServiceFee(id) +} + +func (s *zspFinancesService) ListServiceFees() ([]model.ZSPServiceFees, error) { + return s.repo.ListServiceFees() +} + +func (s *zspFinancesService) UpdateServiceFee(id string, req *dto.ZSPServiceFeesUpdateRequest) error { + fee, err := s.repo.GetServiceFee(id) + if err != nil { + return err + } + if req.BillOfLading != "" { + fee.BillOfLading = req.BillOfLading + } + if req.ContractNumber != "" { + fee.ContractNumber = req.ContractNumber + } + if req.Name != "" { + fee.Name = req.Name + } + if req.Amount != 0 { + fee.Amount = req.Amount + } + if req.Currency != "" { + fee.Currency = req.Currency + } + if req.Date != "" { + fee.Date = parseDate(req.Date) + } + fee.Remark = req.Remark + fee.UpdateTime = time.Now() + return s.repo.UpdateServiceFee(fee) +} + +func (s *zspFinancesService) DeleteServiceFee(id string) error { + return s.repo.DeleteServiceFee(id) +} + +// ============ 成本构成 ============ +func (s *zspFinancesService) CreateCostBreakdown(req *dto.ZSPCostBreakdownCreateRequest) error { + breakdown := model.ZSPCostBreakdown{ + BusinessID: req.BusinessID, + BusinessType: req.BusinessType, + TotalCost: 0, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + return s.repo.CreateCostBreakdown(&breakdown) +} + +func (s *zspFinancesService) GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) { + return s.repo.GetCostBreakdown(id) +} + +func (s *zspFinancesService) ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) { + return s.repo.ListCostBreakdowns(businessID) +} + +func (s *zspFinancesService) UpdateCostBreakdown(id string, req *dto.ZSPCostBreakdownUpdateRequest) error { + breakdown, err := s.repo.GetCostBreakdown(id) + if err != nil { + return err + } + breakdown.FreightChargeTotal = req.FreightChargeTotal + breakdown.MiscChargeTotal = req.MiscChargeTotal + breakdown.ServiceFeeTotal = req.ServiceFeeTotal + breakdown.TotalCost = req.FreightChargeTotal + req.MiscChargeTotal + req.ServiceFeeTotal + breakdown.UpdateTime = time.Now() + return s.repo.UpdateCostBreakdown(breakdown) +} + +func (s *zspFinancesService) DeleteCostBreakdown(id string) error { + return s.repo.DeleteCostBreakdown(id) +} + +// ============ 利润记录 ============ +func (s *zspFinancesService) CreateProfitRecord(req *dto.ZSPProfitRecordCreateRequest) error { + record := model.ZSPProfitRecord{ + BusinessID: req.BusinessID, + BusinessType: req.BusinessType, + Revenue: req.Revenue, + TotalCost: req.TotalCost, + Profit: req.Revenue - req.TotalCost, + ProfitRate: calculateProfitRate(req.Revenue, req.TotalCost), + RecordDate: parseDate(req.RecordDate), + Remark: req.Remark, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + return s.repo.CreateProfitRecord(&record) +} + +func (s *zspFinancesService) GetProfitRecord(id string) (*model.ZSPProfitRecord, error) { + return s.repo.GetProfitRecord(id) +} + +func (s *zspFinancesService) ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) { + return s.repo.ListProfitRecords(filters) +} + +func (s *zspFinancesService) UpdateProfitRecord(id string, req *dto.ZSPProfitRecordUpdateRequest) error { + record, err := s.repo.GetProfitRecord(id) + if err != nil { + return err + } + record.Revenue = req.Revenue + record.TotalCost = req.TotalCost + record.Profit = req.Revenue - req.TotalCost + record.ProfitRate = calculateProfitRate(req.Revenue, req.TotalCost) + record.Remark = req.Remark + record.UpdateTime = time.Now() + return s.repo.UpdateProfitRecord(id, record) +} + +func (s *zspFinancesService) DeleteProfitRecord(id string) error { + return s.repo.DeleteProfitRecord(id) +} + +// ============ 辅助函数 ============ + +func parseDate(dateStr string) time.Time { + t, err := time.Parse("2006-01-02", dateStr) + if err != nil { + return time.Time{} + } + return t +} + +func calculateProfitRate(revenue, totalCost float64) float64 { + if revenue == 0 { + return 0 + } + return (revenue - totalCost) / revenue * 100 +} diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go new file mode 100755 index 0000000..bc8654f --- /dev/null +++ b/backend/pkg/config/config.go @@ -0,0 +1,51 @@ +package config + +import ( + "log" + + "github.com/spf13/viper" +) + +type Config struct { + Server ServerConfig `mapstructure:"server"` + Database DatabaseConfig `mapstructure:"database"` + JWT JWTConfig `mapstructure:"jwt"` +} + +type ServerConfig struct { + Port int `mapstructure:"port"` + Mode string `mapstructure:"mode"` + ReadTimeout int `mapstructure:"read_timeout"` + WriteTimeout int `mapstructure:"write_timeout"` +} + +type DatabaseConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + DBName string `mapstructure:"dbname"` + MaxOpenConns int `mapstructure:"max_open_conns"` + MaxIdleConns int `mapstructure:"max_idle_conns"` + ResetDatabase bool `mapstructure:"reset_database"` +} + +type JWTConfig struct { + Secret string `mapstructure:"secret"` + ExpireHours int `mapstructure:"expire_hours"` +} + +func Load(configPath string) (*Config, error) { + viper.SetConfigFile(configPath) + viper.AutomaticEnv() + if err := viper.ReadInConfig(); err != nil { + return nil, err + } + var config Config + if err := viper.Unmarshal(&config); err != nil { + return nil, err + } + + log.Printf("Config loaded successfully from %s", configPath) + return &config, nil +} diff --git a/backend/pkg/database/gorm.go b/backend/pkg/database/gorm.go new file mode 100755 index 0000000..aeb94a0 --- /dev/null +++ b/backend/pkg/database/gorm.go @@ -0,0 +1,30 @@ +package database + +import ( + "fmt" + + "github.com/rovina/zsp-backend/pkg/config" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func NewDB(cfg *config.DatabaseConfig) (*gorm.DB, error) { + dsn := fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName, + ) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + return nil, err + } + + sqlDB, _ := db.DB() + sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) + sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) + + return db, nil +} diff --git a/backend/pkg/database/init.go b/backend/pkg/database/init.go new file mode 100755 index 0000000..2f82f3c --- /dev/null +++ b/backend/pkg/database/init.go @@ -0,0 +1,57 @@ +package database + +import ( + "database/sql" + "fmt" + "log" + + "github.com/rovina/zsp-backend/pkg/config" +) + +func CheckDatabaseConnection(cfg *config.DatabaseConfig) error { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName) + log.Println("start conn db:", dsn) + db, err := sql.Open("mysql", dsn) + if err != nil { + return err + } + defer db.Close() + + return db.Ping() +} + +func EnsureDatabase(cfg *config.DatabaseConfig) error { + // 不指定数据库连接 + dsn := fmt.Sprintf( + "%s:%s@tcp(%s:%d)/?charset=utf8mb4&parseTime=True&loc=Local", + cfg.User, cfg.Password, cfg.Host, cfg.Port, + ) + + db, err := sql.Open("mysql", dsn) + if err != nil { + return err + } + defer db.Close() + + // 创建数据库 + if cfg.ResetDatabase { + sql := fmt.Sprintf("DROP DATABASE IF EXISTS `%s`;", cfg.DBName) + _, err = db.Exec(sql) + if err != nil { + return err + } + } + sql := fmt.Sprintf( + "CREATE DATABASE IF NOT EXISTS `%s` DEFAULT CHARSET utf8mb4;", + cfg.DBName, + ) + + _, err = db.Exec(sql) + if err != nil { + return err + } + + log.Println("Database ensured:", cfg.DBName) + return nil +} diff --git a/backend/pkg/database/migrate.go b/backend/pkg/database/migrate.go new file mode 100755 index 0000000..c9690f6 --- /dev/null +++ b/backend/pkg/database/migrate.go @@ -0,0 +1,39 @@ +package database + +import ( + "github.com/rovina/zsp-backend/internal/model" + "gorm.io/gorm" +) + +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &model.User{}, + &model.Contract{}, + &model.ContractFile{}, + &model.ZSPContractFolder{}, + &model.ZSPContractFile{}, + &model.ZSPContractDetail{}, + &model.ZSPFreightCharges{}, + &model.ZSPMiscCharges{}, + &model.ZSPServiceFees{}, + &model.ZSPCostBreakdown{}, + &model.ZSPProfitRecord{}, + &model.ZSPUpstreamInvoice{}, + &model.ZSPDownstreamInvoice{}, + &model.ZSPFreightMiscInvoice{}, + &model.ZSPSettlement{}, + &model.ZSPReconciliation{}, + &model.ContractFolder{}, + &model.ContractBatch{}, + &model.ContractBatchFile{}, + &model.CompanyFolder{}, + &model.CompanyFile{}, + &model.DeliveryApply{}, + &model.DeliveryOutbound{}, + &model.Warehouse{}, + &model.InventoryInbound{}, + &model.OwnershipTransfer{}, + &model.OwnershipTransferBL{}, + &model.OwnershipTransferFile{}, + ) +} diff --git a/backend/pkg/utils/file.go b/backend/pkg/utils/file.go new file mode 100755 index 0000000..5713b56 --- /dev/null +++ b/backend/pkg/utils/file.go @@ -0,0 +1,45 @@ +package utils + +import ( + "fmt" + "io" + "mime/multipart" + "os" +) + +func FormatFileSize(i int64) string { + const ( + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 + TB = GB * 1024 + ) + switch { + case i >= TB: + return fmt.Sprintf("%.2fTB", float64(i)/float64(TB)) + case i >= GB: + return fmt.Sprintf("%.2fGB", float64(i)/float64(GB)) + case i >= MB: + return fmt.Sprintf("%.2fMB", float64(i)/float64(MB)) + case i >= KB: + return fmt.Sprintf("%.2fKB", float64(i)/float64(KB)) + default: + return fmt.Sprintf("%dB", i) + } +} + +// SaveUploadedFile 保存 multipart 上传的文件到指定路径 +func SaveUploadedFile(fh *multipart.FileHeader, dst string) error { + src, err := fh.Open() + if err != nil { + return err + } + defer src.Close() + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, src) + return err +} diff --git a/backend/readme.md b/backend/readme.md new file mode 100755 index 0000000..be79a72 --- /dev/null +++ b/backend/readme.md @@ -0,0 +1,366 @@ +# ZSP 后端管理系统 + +基于 Go + Gin + GORM + MySQL 构建的现代化合同管理与用户认证系统后端API。 + +## 📋 项目概述 + +ZSP 后端是一个用于供应链合同管理的企业级应用系统,提供用户认证、合同管理、文件存储等核心功能。系统采用微服务架构思想,具备高可扩展性和良好的开发体验。 + +### 主要特性 +- 🔐 **JWT 用户认证与授权** +- 📄 **合同全生命周期管理** +- 🐳 **Docker 容器化开发环境** +- 🔄 **热重载开发体验** (Air) +- 📊 **MySQL 数据库支持** +- 🧪 **自动化数据库迁移** +- 🔧 **配置驱动架构** + +## 🏗️ 技术栈 + +### 后端框架 +- **Go 1.25.4** - 高性能编程语言 +- **Gin** - 轻量级 Web 框架 +- **GORM** - ORM 数据库工具 +- **JWT** - 用户认证令牌 + +### 开发工具 +- **Docker & Docker Compose** - 容器化开发环境 +- **Air** - Go 应用热重载工具 +- **MySQL 8.0** - 关系型数据库 + +### 配置管理 +- **Viper** - 配置管理库 +- **YAML 配置文件** - 结构化配置 + +## 📁 项目结构 + +``` +zsp-backend/ +├── cmd/api/ # 应用入口 +│ └── main.go # 主程序入口 +├── configs/ # 配置文件 +│ └── config.dev.yaml # 开发环境配置 +├── internal/ # 内部包 +│ ├── handler/ # HTTP 处理器 +│ │ ├── user_handler.go # 用户相关接口 +│ │ └── contract_handler.go # 合同相关接口 +│ ├── middleware/ # 中间件 +│ │ └── auth.go # 认证中间件 +│ ├── model/ # 数据模型 +│ │ ├── user.go # 用户模型 +│ │ ├── contract.go # 合同模型 +│ │ └── error.go # 错误响应模型 +│ ├── repository/ # 数据访问层 +│ │ ├── user_repository.go +│ │ └── contract_repository.go +│ ├── service/ # 业务逻辑层 +│ │ ├── user_service.go +│ │ └── contract_service.go +│ └── server/ # 服务器配置 +│ └── router.go # 路由配置 +├── pkg/ # 公共包 +│ ├── config/ # 配置加载 +│ └── database/ # 数据库连接 +├── scripts/ # 脚本文件 +│ └── init_database.sql # 数据库初始化脚本 +├── migrations/ # 数据库迁移文件 +├── test/ # 测试文件 +├── web/ # 前端文件(可选) +├── deployments/ # 部署配置 +├── docs/ # 文档 +├── tmp/ # 临时文件 +└── bin/ # 构建输出 +``` + +## 🚀 快速开始 + +### 前提条件 +- Docker & Docker Compose +- Go 1.25.4+ (可选,用于本地开发) + +### 使用 Docker 开发环境(推荐) + +1. **克隆项目** + ```bash + git clone + cd zsp-backend + ``` + +2. **启动开发环境** + ```bash + docker-compose -f docker-compose.dev.yml up --build + ``` + +3. **访问应用** + - API 服务: http://localhost:8080 + - MySQL 数据库: localhost:3306 + +### 本地开发(不使用 Docker) + +1. **安装依赖** + ```bash + go mod download + ``` + +2. **启动 MySQL 数据库** + ```bash + # 使用 Docker 启动 MySQL + docker run --name mysql-dev -e MYSQL_ROOT_PASSWORD=password \ + -e MYSQL_DATABASE=myapp -p 3306:3306 -d mysql:8.0 + ``` + +3. **运行应用** + ```bash + # 使用 Air 热重载 + air -c .air.toml + + # 或直接运行 + go run cmd/api/main.go + ``` + +## 🔧 配置说明 + +### 环境变量 +创建 `.env.dev` 文件(参考 `.env.dev.example`): +```env +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=password +DB_NAME=myapp + +# JWT 配置 +JWT_SECRET=your-secret-key +JWT_EXPIRE_HOURS=24 +``` + +### 配置文件 (`configs/config.dev.yaml`) +```yaml +server: + port: 8080 + mode: "debug" + read_timeout: 30 + write_timeout: 30 + +database: + host: "mysql" + port: 3306 + user: "root" + password: "password" + dbname: "myapp" + max_open_conns: 100 + max_idle_conns: 10 + reset_database: true + +jwt: + secret: "your-jwt-secret-key" + expire_hours: 24 +``` + +## 📖 API 文档 + +### 用户认证 + +#### 用户注册 +```http +POST /api/v1/users/register +Content-Type: application/json + +{ + "username": "testuser", + "email": "test@example.com", + "password": "password123" +} +``` + +#### 用户登录 +```http +POST /api/v1/users/login +Content-Type: application/json + +{ + "email": "test@example.com", + "password": "password123" +} +``` + +响应: +```json +{ + "message": "Login successful", + "user": { + "id": 1, + "username": "testuser", + "email": "test@example.com" + }, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +### 合同管理 + +#### 创建合同 +```http +POST /api/v1/contracts +Authorization: Bearer +Content-Type: application/json + +{ + "name": "采购合同", + "contract_type": "采购", + "contract_code": "CG2024001", + "buy_party": "买方公司", + "sell_party": "卖方公司", + "count_party": "计数方", + "unit_price": "1000.00", + "bl_number": "BL123456", + "delivery_time": "2024-12-31", + "pickup_time": "2024-12-30", + "packaging": "标准包装", + "remarks": "备注信息", + "file_path": "/uploads/contract.pdf" +} +``` + +#### 获取合同列表 +```http +GET /api/v1/contracts +Authorization: Bearer +``` + +## 🗄️ 数据模型 + +### 用户表 (users) +| 字段 | 类型 | 描述 | +|------|------|------| +| id | uint | 主键 | +| username | string(50) | 用户名,唯一 | +| email | string(100) | 邮箱,唯一 | +| password | string(255) | 密码哈希 | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +### 合同表 (contracts) +| 字段 | 类型 | 描述 | +|------|------|------| +| id | uint | 主键 | +| name | string(100) | 合同名称 | +| contract_type | string(50) | 合同类型 | +| contract_code | string(50) | 合同编码 | +| buy_party | string(100) | 买方 | +| sell_party | string(100) | 卖方 | +| count_party | string(100) | 计数方 | +| unit_price | string(50) | 单价 | +| bl_number | string(50) | 提单号 | +| delivery_time | string(50) | 交付时间 | +| pickup_time | string(50) | 提货时间 | +| packaging | string(50) | 包装方式 | +| remarks | string(255) | 备注 | +| file_path | string(255) | 文件路径 | + +## 🐳 Docker 开发环境 + +### 开发环境配置 (`docker-compose.dev.yml`) +- **应用服务**: Go + Air 热重载 +- **数据库服务**: MySQL 8.0 +- **网络配置**: 自定义网络 `app-network` +- **卷挂载**: 代码实时同步,Go 模块缓存 + +### 构建自定义镜像 +```bash +# 构建开发镜像 +docker build -f Dockerfile.dev -t zsp-backend-dev . + +# 运行容器 +docker run -p 8080:8080 -v $(pwd):/workspace zsp-backend-dev +``` + +## 🧪 测试 + +### 运行测试 +```bash +# 运行所有测试 +go test ./... + +# 运行特定包测试 +go test ./internal/handler + +# 运行测试并显示覆盖率 +go test -cover ./... +``` + +### 测试数据库 +测试环境使用独立的数据库 `myapp_test`,通过初始化脚本自动创建。 + +## 🔄 数据库迁移 + +### 自动迁移 +在开发环境中,设置 `reset_database: true` 会自动执行数据库迁移: +```go +if cfg.Database.ResetDatabase { + err = database.AutoMigrate(db) + // ... +} +``` + +### 手动迁移 +1. 创建迁移文件在 `migrations/` 目录 +2. 使用 GORM 的 `AutoMigrate` 或手动执行 SQL + +## 📊 部署 + +### 生产环境构建 +```bash +# 构建生产镜像 +docker build -t zsp-backend-prod . + +# 使用生产配置 +docker run -p 8080:8080 \ + -v /path/to/config.yaml:/app/config.yaml \ + zsp-backend-prod +``` + +### 环境变量配置 +生产环境建议使用环境变量覆盖配置文件: +```bash +export DB_HOST=production-db +export DB_PASSWORD=secure-password +export JWT_SECRET=production-secret +``` + +## 🛠️ 开发工具 + +### Air 热重载配置 (`.air.toml`) +- 自动检测文件变化并重新编译 +- 排除测试文件和构建目录 +- 自定义构建命令和输出目录 + +### 代码规范 +- 使用 `go fmt` 格式化代码 +- 遵循 Go 官方代码规范 +- 使用有意义的包和函数命名 + +## 🤝 贡献指南 + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 创建 Pull Request + +## 📄 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 + +## 📞 支持 + +如有问题或建议,请: +1. 查看 [Issues](https://github.com/rovina/zsp-backend/issues) +2. 提交新的 Issue +3. 联系项目维护者 + +--- + +**最后更新**: 2024年1月 +**版本**: 1.0.0-dev \ No newline at end of file diff --git a/backend/scripts/init_database.sql b/backend/scripts/init_database.sql new file mode 100755 index 0000000..a10bc81 --- /dev/null +++ b/backend/scripts/init_database.sql @@ -0,0 +1,11 @@ +CREATE DATABASE IF NOT EXISTS `myapp_dev` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE IF NOT EXISTS `myapp_test` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +--- 创建专用用户 + +CREATE USER IF NOT EXISTS 'myapp_user'@'%' IDENTIFIED BY 'devpassword'; +GRANT ALL PRIVILEGES ON `myapp_dev`.* TO 'myapp_user'@'%'; +GRANT ALL PRIVILEGES ON `myapp_test`.* TO 'myapp_user'@'%'; +FLUSH PRIVILEGES; + +USE `myapp_dev`; diff --git a/backend/test/company_handler_test.go b/backend/test/company_handler_test.go new file mode 100644 index 0000000..a4e545f --- /dev/null +++ b/backend/test/company_handler_test.go @@ -0,0 +1,496 @@ +package test + +import ( + "bytes" + "encoding/json" + "errors" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/handler" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/service" +) + +// mockCompanyService implements service.CompanyService for testing +type mockCompanyService struct { + createFolderFn func(name, description, category string) (*model.CompanyFolder, error) + listFoldersFn func() ([]model.CompanyFolder, error) + getFolderByIDFn func(id uint) (*model.CompanyFolder, error) + updateFolderFn func(id uint, name, description, category string) error + deleteFolderFn func(id uint) error + + uploadFilesFn func(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error) + listFilesFn func(folderID *uint) ([]model.CompanyFile, error) + getFileByIDFn func(id uint) (*model.CompanyFile, error) + updateFileFn func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error + deleteFileFn func(id uint) error +} + +func (m *mockCompanyService) CreateFolder(name, description, category string) (*model.CompanyFolder, error) { + return m.createFolderFn(name, description, category) +} +func (m *mockCompanyService) ListFolders() ([]model.CompanyFolder, error) { + return m.listFoldersFn() +} +func (m *mockCompanyService) GetFolderByID(id uint) (*model.CompanyFolder, error) { + return m.getFolderByIDFn(id) +} +func (m *mockCompanyService) UpdateFolder(id uint, name, description, category string) error { + return m.updateFolderFn(id, name, description, category) +} +func (m *mockCompanyService) DeleteFolder(id uint) error { + return m.deleteFolderFn(id) +} + +func (m *mockCompanyService) UploadFiles(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error) { + return m.uploadFilesFn(folderID, companyName, fileCategory, expireDate, description, files) +} +func (m *mockCompanyService) ListFiles(folderID *uint) ([]model.CompanyFile, error) { + return m.listFilesFn(folderID) +} +func (m *mockCompanyService) GetFileByID(id uint) (*model.CompanyFile, error) { + return m.getFileByIDFn(id) +} +func (m *mockCompanyService) UpdateFile(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error { + return m.updateFileFn(id, companyName, fileCategory, expireDate, description, folderID) +} +func (m *mockCompanyService) DeleteFile(id uint) error { + return m.deleteFileFn(id) +} + +var _ service.CompanyService = (*mockCompanyService)(nil) + +func setupCompanyRouter(h *handler.CompanyHandler) *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + api := r.Group("/api") + { + company := api.Group("/company") + { + company.GET("/folders", h.ListFolders) + company.POST("/folders", h.CreateFolder) + company.PUT("/folders/:id", h.UpdateFolder) + company.DELETE("/folders/:id", h.DeleteFolder) + + company.GET("/files", h.ListFiles) + company.POST("/files/upload", h.UploadFiles) + company.PUT("/files/:id", h.UpdateFile) + company.DELETE("/files/:id", h.DeleteFile) + } + } + return r +} + +// ========== Folder Tests ========== + +func TestCompanyListFolders_Success(t *testing.T) { + mock := &mockCompanyService{ + listFoldersFn: func() ([]model.CompanyFolder, error) { + return []model.CompanyFolder{ + {ID: 1, Name: "营业执照文件夹", Category: "license"}, + {ID: 2, Name: "开票信息文件夹", Category: "invoice"}, + }, nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/company/folders", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestCompanyListFolders_Error(t *testing.T) { + mock := &mockCompanyService{ + listFoldersFn: func() ([]model.CompanyFolder, error) { + return nil, errors.New("database error") + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/company/folders", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestCompanyCreateFolder_Success(t *testing.T) { + mock := &mockCompanyService{ + createFolderFn: func(name, description, category string) (*model.CompanyFolder, error) { + return &model.CompanyFolder{ID: 1, Name: name, Description: description, Category: category}, nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"name":"测试文件夹","description":"测试描述","category":"license"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/company/folders", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestCompanyCreateFolder_BadRequest(t *testing.T) { + mock := &mockCompanyService{ + createFolderFn: func(name, description, category string) (*model.CompanyFolder, error) { + return nil, nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/company/folders", bytes.NewReader([]byte("{}"))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for missing required field, got %d", w.Code) + } +} + +func TestCompanyCreateFolder_Error(t *testing.T) { + mock := &mockCompanyService{ + createFolderFn: func(name, description, category string) (*model.CompanyFolder, error) { + return nil, errors.New("folder already exists") + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"name":"测试文件夹","description":"测试描述","category":"license"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/company/folders", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestCompanyUpdateFolder_Success(t *testing.T) { + mock := &mockCompanyService{ + updateFolderFn: func(id uint, name, description, category string) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"name":"更新文件夹","description":"更新描述","category":"invoice"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/company/folders/1", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestCompanyUpdateFolder_InvalidID(t *testing.T) { + mock := &mockCompanyService{ + updateFolderFn: func(id uint, name, description, category string) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"name":"更新文件夹","description":"更新描述","category":"invoice"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/company/folders/invalid", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for invalid id, got %d", w.Code) + } +} + +func TestCompanyUpdateFolder_Error(t *testing.T) { + mock := &mockCompanyService{ + updateFolderFn: func(id uint, name, description, category string) error { + return errors.New("folder not found") + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"name":"更新文件夹","description":"更新描述","category":"invoice"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/company/folders/999", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestCompanyDeleteFolder_Success(t *testing.T) { + mock := &mockCompanyService{ + deleteFolderFn: func(id uint) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/company/folders/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestCompanyDeleteFolder_InvalidID(t *testing.T) { + mock := &mockCompanyService{ + deleteFolderFn: func(id uint) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/company/folders/invalid", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for invalid id, got %d", w.Code) + } +} + +func TestCompanyDeleteFolder_Error(t *testing.T) { + mock := &mockCompanyService{ + deleteFolderFn: func(id uint) error { + return errors.New("folder not found") + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/company/folders/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +// ========== File Tests ========== + +func TestCompanyListFiles_Success(t *testing.T) { + mock := &mockCompanyService{ + listFilesFn: func(folderID *uint) ([]model.CompanyFile, error) { + return []model.CompanyFile{ + {ID: 1, FileName: "营业执照.pdf", CompanyName: "公司A", FileCategory: "license"}, + {ID: 2, FileName: "开票信息.pdf", CompanyName: "公司A", FileCategory: "invoice"}, + }, nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/company/files", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestCompanyListFiles_WithFolderID(t *testing.T) { + mock := &mockCompanyService{ + listFilesFn: func(folderID *uint) ([]model.CompanyFile, error) { + if folderID != nil && *folderID != 1 { + t.Errorf("expected folderID 1, got %v", folderID) + } + return []model.CompanyFile{ + {ID: 1, FileName: "营业执照.pdf", FolderID: 1}, + }, nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/company/files?folderId=1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestCompanyListFiles_Error(t *testing.T) { + mock := &mockCompanyService{ + listFilesFn: func(folderID *uint) ([]model.CompanyFile, error) { + return nil, errors.New("database error") + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/company/files", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestCompanyUpdateFile_Success(t *testing.T) { + mock := &mockCompanyService{ + updateFileFn: func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"companyName":"公司B","fileCategory":"license","expireDate":"2025-12-31","description":"更新描述"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/company/files/1", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestCompanyUpdateFile_InvalidID(t *testing.T) { + mock := &mockCompanyService{ + updateFileFn: func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"companyName":"公司B","fileCategory":"license"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/company/files/invalid", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for invalid id, got %d", w.Code) + } +} + +func TestCompanyUpdateFile_Error(t *testing.T) { + mock := &mockCompanyService{ + updateFileFn: func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error { + return errors.New("file not found") + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + body := `{"companyName":"公司B","fileCategory":"license"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/company/files/999", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestCompanyDeleteFile_Success(t *testing.T) { + mock := &mockCompanyService{ + deleteFileFn: func(id uint) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/company/files/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestCompanyDeleteFile_InvalidID(t *testing.T) { + mock := &mockCompanyService{ + deleteFileFn: func(id uint) error { + return nil + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/company/files/invalid", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for invalid id, got %d", w.Code) + } +} + +func TestCompanyDeleteFile_Error(t *testing.T) { + mock := &mockCompanyService{ + deleteFileFn: func(id uint) error { + return errors.New("file not found") + }, + } + h := handler.NewCompanyHandler(mock) + router := setupCompanyRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/company/files/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} \ No newline at end of file diff --git a/backend/test/contract_handler_test.go b/backend/test/contract_handler_test.go new file mode 100755 index 0000000..5a9f5a2 --- /dev/null +++ b/backend/test/contract_handler_test.go @@ -0,0 +1,216 @@ +package test + +import ( + "encoding/json" + "errors" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/handler" + "github.com/rovina/zsp-backend/internal/model" +) + +// ----- mock contract services ----- +type mockContractService struct { + listFn func(contractType string) ([]model.Contract, error) + getByIDFn func(id uint) (*model.Contract, error) +} + +func (m *mockContractService) CreateContract(contract *model.Contract, files []model.ContractFile) error { return nil } +func (m *mockContractService) SaveFiles(files []model.ContractFile) error { return nil } +func (m *mockContractService) BatchSaveFiles(basePath string, files []*multipart.FileHeader) (int, error) { return 0, nil } +func (m *mockContractService) List(contractType string) ([]model.Contract, error) { return m.listFn(contractType) } +func (m *mockContractService) GetByID(id uint) (*model.Contract, error) { return m.getByIDFn(id) } +func (m *mockContractService) Update(contract *model.Contract) error { return nil } +func (m *mockContractService) UpdateWithFiles(id uint, keepFileIDs []uint, newFileModels []model.ContractFile, form map[string]string) error { + return nil +} +func (m *mockContractService) Delete(id uint) error { return nil } + +type mockContractFolderService struct { + listFn func() ([]model.ContractFolder, error) + createFn func(name, description string) (*model.ContractFolder, error) + getByIDFn func(id uint) (*model.ContractFolder, error) + updateFn func(id uint, name, description string) error + deleteFn func(id uint) error +} + +func (m *mockContractFolderService) List() ([]model.ContractFolder, error) { return m.listFn() } +func (m *mockContractFolderService) Create(name, description string) (*model.ContractFolder, error) { return m.createFn(name, description) } +func (m *mockContractFolderService) GetByID(id uint) (*model.ContractFolder, error) { return m.getByIDFn(id) } +func (m *mockContractFolderService) Update(id uint, name, description string) error { return m.updateFn(id, name, description) } +func (m *mockContractFolderService) Delete(id uint) error { return m.deleteFn(id) } + +type mockContractBatchService struct { + listFn func(folderID *uint) ([]model.ContractBatch, error) + getByIDFn func(id uint) (*model.ContractBatch, error) + deleteFn func(id uint) error +} + +func (m *mockContractBatchService) List(folderID *uint) ([]model.ContractBatch, error) { return m.listFn(folderID) } +func (m *mockContractBatchService) GetByID(id uint) (*model.ContractBatch, error) { return m.getByIDFn(id) } +func (m *mockContractBatchService) Create(folderID uint, batchName, remark string, files []model.ContractBatchFile) (*model.ContractBatch, error) { + return nil, nil +} +func (m *mockContractBatchService) Delete(id uint) error { return m.deleteFn(id) } + +func setupContractRouter(h *handler.ContractHandler) *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + api := r.Group("/api") + { + contracts := api.Group("/contract") + { + contracts.GET("", h.List) + contracts.GET("/folders", h.ListFolders) + contracts.POST("/folders", h.CreateFolder) + contracts.GET("/folders/:id", h.GetFolder) + contracts.PUT("/folders/:id", h.UpdateFolder) + contracts.DELETE("/folders/:id", h.DeleteFolder) + contracts.GET("/:id", h.Get) + } + } + return r +} + +func TestContractList_Success(t *testing.T) { + mockContract := &mockContractService{ + listFn: func(contractType string) ([]model.Contract, error) { + return []model.Contract{ + {ID: 1, Name: "合同1", ContractCode: "C001"}, + {ID: 2, Name: "合同2", ContractCode: "C002"}, + }, nil + }, + } + mockFolder := &mockContractFolderService{ + listFn: func() ([]model.ContractFolder, error) { return nil, nil }, + } + mockBatch := &mockContractBatchService{ + listFn: func(folderID *uint) ([]model.ContractBatch, error) { return nil, nil }, + } + + h := handler.NewContractHandler(mockContract, mockFolder, mockBatch) + router := setupContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/contract", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestContractGet_Success(t *testing.T) { + mockContract := &mockContractService{ + getByIDFn: func(id uint) (*model.Contract, error) { + return &model.Contract{ID: 1, Name: "合同1", ContractCode: "C001"}, nil + }, + } + mockFolder := &mockContractFolderService{ + listFn: func() ([]model.ContractFolder, error) { return nil, nil }, + } + mockBatch := &mockContractBatchService{ + listFn: func(folderID *uint) ([]model.ContractBatch, error) { return nil, nil }, + } + + h := handler.NewContractHandler(mockContract, mockFolder, mockBatch) + router := setupContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/contract/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestContractGet_NotFound(t *testing.T) { + mockContract := &mockContractService{ + getByIDFn: func(id uint) (*model.Contract, error) { + return nil, errors.New("contract not found") + }, + } + mockFolder := &mockContractFolderService{ + listFn: func() ([]model.ContractFolder, error) { return nil, nil }, + } + mockBatch := &mockContractBatchService{ + listFn: func(folderID *uint) ([]model.ContractBatch, error) { return nil, nil }, + } + + h := handler.NewContractHandler(mockContract, mockFolder, mockBatch) + router := setupContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/contract/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status 404, got %d", w.Code) + } +} + +func TestContractCreateFolder_Success(t *testing.T) { + mockContract := &mockContractService{} + mockFolder := &mockContractFolderService{ + createFn: func(name, description string) (*model.ContractFolder, error) { + return &model.ContractFolder{ID: 1, Name: name, Description: description}, nil + }, + } + mockBatch := &mockContractBatchService{} + + h := handler.NewContractHandler(mockContract, mockFolder, mockBatch) + router := setupContractRouter(h) + + body := `{"name":"测试文件夹","description":"测试描述"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contract/folders", nil) + req.Body = http.NoBody + _ = body // body is unused in this simple test; the handler uses ShouldBindJSON + router.ServeHTTP(w, req) + + // Without valid JSON body, this should return 400 + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for missing body, got %d", w.Code) + } +} + +func TestContractListFolders_Success(t *testing.T) { + mockContract := &mockContractService{} + mockFolder := &mockContractFolderService{ + listFn: func() ([]model.ContractFolder, error) { + return []model.ContractFolder{ + {ID: 1, Name: "文件夹1"}, + {ID: 2, Name: "文件夹2"}, + }, nil + }, + } + mockBatch := &mockContractBatchService{} + + h := handler.NewContractHandler(mockContract, mockFolder, mockBatch) + router := setupContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/contract/folders", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} diff --git a/backend/test/delivery_handler_test.go b/backend/test/delivery_handler_test.go new file mode 100755 index 0000000..b4e7158 --- /dev/null +++ b/backend/test/delivery_handler_test.go @@ -0,0 +1,375 @@ +package test + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/handler" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/service" + "gorm.io/gorm" +) + +// mockDeliveryService implements service.DeliveryService for testing +type mockDeliveryService struct { + listApplyFn func(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) + getApplyByIDFn func(id uint) (*model.DeliveryApply, error) + createApplyFn func(e *model.DeliveryApply) error + deleteApplyFn func(id uint) error + listOutboundFn func(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) + getOutboundByIDFn func(id uint) (*model.DeliveryOutbound, error) + createOutboundFn func(e *model.DeliveryOutbound) error + deleteOutboundFn func(id uint) error + listWarehousesFn func() ([]model.Warehouse, error) + createWarehouseFn func(e *model.Warehouse) error + deleteWarehouseFn func(id uint) error + listInboundFn func(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) + createInboundFn func(e *model.InventoryInbound) error + deleteInboundFn func(id uint) error + listOwnershipFn func(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) + deleteOwnershipFn func(id uint) error + dbFn func() *gorm.DB +} + +func (m *mockDeliveryService) ListApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) { + return m.listApplyFn(page, pageSize, orderNumber, blNumber, status) +} +func (m *mockDeliveryService) GetApplyByID(id uint) (*model.DeliveryApply, error) { return m.getApplyByIDFn(id) } +func (m *mockDeliveryService) CreateApply(e *model.DeliveryApply) error { return m.createApplyFn(e) } +func (m *mockDeliveryService) UpdateApply(e *model.DeliveryApply) error { return nil } +func (m *mockDeliveryService) DeleteApply(id uint) error { return m.deleteApplyFn(id) } + +func (m *mockDeliveryService) ListOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) { + return m.listOutboundFn(page, pageSize, outboundNumber, blNumber, status) +} +func (m *mockDeliveryService) GetOutboundByID(id uint) (*model.DeliveryOutbound, error) { return m.getOutboundByIDFn(id) } +func (m *mockDeliveryService) CreateOutbound(e *model.DeliveryOutbound) error { return m.createOutboundFn(e) } +func (m *mockDeliveryService) UpdateOutbound(e *model.DeliveryOutbound) error { return nil } +func (m *mockDeliveryService) DeleteOutbound(id uint) error { return m.deleteOutboundFn(id) } + +func (m *mockDeliveryService) ListWarehouses() ([]model.Warehouse, error) { return m.listWarehousesFn() } +func (m *mockDeliveryService) GetWarehouseByID(id uint) (*model.Warehouse, error) { return nil, nil } +func (m *mockDeliveryService) CreateWarehouse(e *model.Warehouse) error { return m.createWarehouseFn(e) } +func (m *mockDeliveryService) UpdateWarehouse(e *model.Warehouse) error { return nil } +func (m *mockDeliveryService) DeleteWarehouse(id uint) error { return m.deleteWarehouseFn(id) } + +func (m *mockDeliveryService) ListInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) { + return m.listInboundFn(page, pageSize, warehouse, commodity) +} +func (m *mockDeliveryService) GetInboundByID(id uint) (*model.InventoryInbound, error) { return nil, nil } +func (m *mockDeliveryService) CreateInbound(e *model.InventoryInbound) error { return m.createInboundFn(e) } +func (m *mockDeliveryService) UpdateInbound(e *model.InventoryInbound) error { return nil } +func (m *mockDeliveryService) DeleteInbound(id uint) error { return m.deleteInboundFn(id) } + +func (m *mockDeliveryService) ListOwnership(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) { + return m.listOwnershipFn(page, pageSize, transferNumber, blNumber, status) +} +func (m *mockDeliveryService) GetOwnershipByID(id uint) (*model.OwnershipTransfer, error) { return nil, nil } +func (m *mockDeliveryService) CreateOwnership(e *model.OwnershipTransfer) error { return nil } +func (m *mockDeliveryService) DeleteOwnership(id uint) error { return m.deleteOwnershipFn(id) } +func (m *mockDeliveryService) CreateOwnershipFiles(files []model.OwnershipTransferFile) error { return nil } +func (m *mockDeliveryService) DB() *gorm.DB { return nil } + +var _ service.DeliveryService = (*mockDeliveryService)(nil) + +func setupDeliveryRouter(h *handler.DeliveryHandler) *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + api := r.Group("/api") + { + applyDelivery := api.Group("/apply-delivery") + { + applyDelivery.GET("", h.ListApply) + applyDelivery.POST("", h.CreateApply) + applyDelivery.DELETE("/:id", h.DeleteApply) + } + deliveryDetails := api.Group("/delivery-details") + { + deliveryDetails.GET("", h.ListOutbound) + deliveryDetails.POST("", h.CreateOutbound) + deliveryDetails.DELETE("/:id", h.DeleteOutbound) + } + inventory := api.Group("/inventory") + { + inventory.GET("/warehouses", h.ListWarehouses) + inventory.POST("/warehouses", h.CreateWarehouse) + inventory.DELETE("/warehouses/:id", h.DeleteWarehouse) + inventory.POST("/inbound", h.CreateInbound) + inventory.DELETE("/inbound/:id", h.DeleteInbound) + } + ownershipTransfer := api.Group("/ownership-transfer") + { + ownershipTransfer.GET("", h.ListOwnership) + ownershipTransfer.DELETE("/:id", h.DeleteOwnership) + } + } + return r +} + +// ========== 我要提货 /api/apply-delivery ========== + +func TestDeliveryListApply_Success(t *testing.T) { + mock := &mockDeliveryService{ + listApplyFn: func(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) { + return []model.DeliveryApply{ + {ID: 1, OrderNumber: "DA001", Status: "pending"}, + {ID: 2, OrderNumber: "DA002", Status: "approved"}, + }, 2, nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/apply-delivery?page=1&pageSize=10", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestDeliveryCreateApply_Success(t *testing.T) { + mock := &mockDeliveryService{ + createApplyFn: func(e *model.DeliveryApply) error { + e.ID = 1 + e.OrderNumber = "DA1234567890" + return nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + body := `{"blNumber":"BL001","commodity":"燕麦","quantity":100}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/apply-delivery", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryDeleteApply_Success(t *testing.T) { + mock := &mockDeliveryService{ + deleteApplyFn: func(id uint) error { return nil }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/apply-delivery/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryDeleteApply_NotFound(t *testing.T) { + mock := &mockDeliveryService{ + deleteApplyFn: func(id uint) error { return errors.New("not found") }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/apply-delivery/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +// ========== 提货明细 /api/delivery-details ========== + +func TestDeliveryListOutbound_Success(t *testing.T) { + mock := &mockDeliveryService{ + listOutboundFn: func(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) { + return []model.DeliveryOutbound{ + {ID: 1, OutboundNumber: "OB001", OutboundStatus: "completed"}, + }, 1, nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/delivery-details?page=1&pageSize=10", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryCreateOutbound_Success(t *testing.T) { + mock := &mockDeliveryService{ + createOutboundFn: func(e *model.DeliveryOutbound) error { + e.ID = 1 + e.OutboundNumber = "OB1234567890" + return nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + body := `{"blNumber":"BL001","warehouse":"A库","quantity":50}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/delivery-details", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryDeleteOutbound_Success(t *testing.T) { + mock := &mockDeliveryService{ + deleteOutboundFn: func(id uint) error { return nil }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/delivery-details/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +// ========== 库存 /api/inventory ========== + +func TestDeliveryListWarehouses_Success(t *testing.T) { + mock := &mockDeliveryService{ + listWarehousesFn: func() ([]model.Warehouse, error) { + return []model.Warehouse{ + {ID: 1, Name: "A库", Status: "active"}, + {ID: 2, Name: "B库", Status: "active"}, + }, nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/inventory/warehouses", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryCreateWarehouse_Success(t *testing.T) { + mock := &mockDeliveryService{ + createWarehouseFn: func(e *model.Warehouse) error { + e.ID = 1 + return nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + body := `{"name":"C库","location":"广州"}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/inventory/warehouses", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryDeleteWarehouse_Success(t *testing.T) { + mock := &mockDeliveryService{ + deleteWarehouseFn: func(id uint) error { return nil }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/inventory/warehouses/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryCreateInbound_Success(t *testing.T) { + mock := &mockDeliveryService{ + createInboundFn: func(e *model.InventoryInbound) error { + e.ID = 1 + e.InboundNumber = "IN1234567890" + return nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + body := `{"warehouse":"A库","commodity":"燕麦","inboundWeight":1000}` + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/inventory/inbound", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +// ========== 货权转移 /api/ownership-transfer ========== + +func TestDeliveryListOwnership_Success(t *testing.T) { + mock := &mockDeliveryService{ + listOwnershipFn: func(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) { + return []model.OwnershipTransfer{ + {ID: 1, TransferNumber: "OT001", Status: "draft"}, + }, 1, nil + }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/ownership-transfer?page=1&pageSize=10", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestDeliveryDeleteOwnership_Success(t *testing.T) { + mock := &mockDeliveryService{ + deleteOwnershipFn: func(id uint) error { return nil }, + } + h := handler.NewDeliveryHandler(mock) + router := setupDeliveryRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/ownership-transfer/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} diff --git a/backend/test/user_handler_test.go b/backend/test/user_handler_test.go new file mode 100755 index 0000000..1cef1a8 --- /dev/null +++ b/backend/test/user_handler_test.go @@ -0,0 +1,177 @@ +package test + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/handler" +) + +// mockUserService implements service.UserService for testing +type mockUserService struct { + registerFn func(req *dto.CreateUserRequest) (*dto.UserResponse, error) + loginFn func(req *dto.LoginRequest) (*dto.UserResponse, string, error) + getUserByIDFn func(id uint) (*dto.UserResponse, error) +} + +func (m *mockUserService) Register(req *dto.CreateUserRequest) (*dto.UserResponse, error) { + return m.registerFn(req) +} +func (m *mockUserService) Login(req *dto.LoginRequest) (*dto.UserResponse, string, error) { + return m.loginFn(req) +} +func (m *mockUserService) GetUserByID(id uint) (*dto.UserResponse, error) { + return m.getUserByIDFn(id) +} + +func setupUserRouter(h *handler.UserHandler) *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + api := r.Group("/api") + { + auth := api.Group("/auth") + { + auth.POST("/register", h.Register) + auth.POST("/login", h.Login) + } + users := api.Group("/users") + { + users.GET("/:id", h.GetUser) + } + } + return r +} + +func TestUserRegister_Success(t *testing.T) { + mock := &mockUserService{ + registerFn: func(req *dto.CreateUserRequest) (*dto.UserResponse, error) { + return &dto.UserResponse{ID: 1, Username: "testuser", Email: "test@example.com"}, nil + }, + } + h := handler.NewUserHandler(mock) + router := setupUserRouter(h) + + body, _ := json.Marshal(dto.CreateUserRequest{Username: "testuser", Email: "test@example.com", Password: "password123"}) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/register", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Errorf("expected status 201, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["message"] != "User registered successfully" { + t.Errorf("unexpected message: %v", resp["message"]) + } +} + +func TestUserRegister_DuplicateEmail(t *testing.T) { + mock := &mockUserService{ + registerFn: func(req *dto.CreateUserRequest) (*dto.UserResponse, error) { + return nil, errors.New("email already exists") + }, + } + h := handler.NewUserHandler(mock) + router := setupUserRouter(h) + + body, _ := json.Marshal(dto.CreateUserRequest{Username: "testuser", Email: "existing@example.com", Password: "password123"}) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/register", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400, got %d", w.Code) + } +} + +func TestUserLogin_Success(t *testing.T) { + mock := &mockUserService{ + loginFn: func(req *dto.LoginRequest) (*dto.UserResponse, string, error) { + return &dto.UserResponse{ID: 1, Username: "testuser", Email: "test@example.com"}, "test-token", nil + }, + } + h := handler.NewUserHandler(mock) + router := setupUserRouter(h) + + body, _ := json.Marshal(dto.LoginRequest{Email: "test@example.com", Password: "password123"}) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["token"] != "test-token" { + t.Errorf("expected token 'test-token', got %v", resp["token"]) + } +} + +func TestUserLogin_InvalidCredentials(t *testing.T) { + mock := &mockUserService{ + loginFn: func(req *dto.LoginRequest) (*dto.UserResponse, string, error) { + return nil, "", errors.New("invalid credentials") + }, + } + h := handler.NewUserHandler(mock) + router := setupUserRouter(h) + + body, _ := json.Marshal(dto.LoginRequest{Email: "wrong@example.com", Password: "wrong"}) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("expected status 401, got %d", w.Code) + } +} + +func TestUserGet_Success(t *testing.T) { + mock := &mockUserService{ + getUserByIDFn: func(id uint) (*dto.UserResponse, error) { + return &dto.UserResponse{ID: 1, Username: "testuser", Email: "test@example.com"}, nil + }, + } + h := handler.NewUserHandler(mock) + router := setupUserRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/users/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestUserGet_NotFound(t *testing.T) { + mock := &mockUserService{ + getUserByIDFn: func(id uint) (*dto.UserResponse, error) { + return nil, errors.New("user not found") + }, + } + h := handler.NewUserHandler(mock) + router := setupUserRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/users/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("expected status 404, got %d", w.Code) + } +} diff --git a/backend/test/zsp_contract_handler_test.go b/backend/test/zsp_contract_handler_test.go new file mode 100644 index 0000000..b3cd693 --- /dev/null +++ b/backend/test/zsp_contract_handler_test.go @@ -0,0 +1,539 @@ +package test + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/handler" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/service" + "mime/multipart" +) + +// mockZSPContractService implements service.ZSPContractService for testing +type mockZSPContractService struct { + createFolderFn func(req *dto.ZSPFolderCreateRequest) error + getFoldersFn func() ([]model.ZSPContractFolder, error) + getFolderByIdFn func(folderID string) (*model.ZSPContractFolder, error) + deleteFolderByIdFn func(folderID string) error + updateFolderByIdFn func(folderID string, req *dto.ZSPFolderUpdateRequest) error + uploadContractFn func(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error + getContractByIdFn func(contractID string) (*model.ZSPContractFile, error) + getContractWithDetailsFn func(contractID string) (*model.ZSPContractFile, error) + listContractsFn func(folderID string) ([]model.ZSPContractFile, error) + listContractsWithDetailsFn func(folderID string) ([]model.ZSPContractFile, error) + deleteContractByIdFn func(contractID string) error + updateContractByIdFn func(contractID string, req *dto.ZSPContractUpdateRequest) error + createContractDetailFn func(req *dto.ZSPContractDetailCreateRequest) error + updateContractDetailFn func(req *dto.ZSPContractDetailUpdateRequest) error + getContractDetailByIdFn func(detailID string) (*model.ZSPContractDetail, error) + listContractDetailsFn func(contractFileID string) ([]model.ZSPContractDetail, error) + deleteContractDetailByIdFn func(detailID string) error + batchCreateContractDetailsFn func(req *dto.ZSPContractDetailBatchCreateRequest) error +} + +func (m *mockZSPContractService) CreateFolder(req *dto.ZSPFolderCreateRequest) error { + return m.createFolderFn(req) +} +func (m *mockZSPContractService) GetFolders() ([]model.ZSPContractFolder, error) { + return m.getFoldersFn() +} +func (m *mockZSPContractService) GetFolderById(folderID string) (*model.ZSPContractFolder, error) { + return m.getFolderByIdFn(folderID) +} +func (m *mockZSPContractService) DeleteFolderById(folderID string) error { + return m.deleteFolderByIdFn(folderID) +} +func (m *mockZSPContractService) UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error { + return m.updateFolderByIdFn(folderID, req) +} +func (m *mockZSPContractService) UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error { + return m.uploadContractFn(req, fileHeader) +} +func (m *mockZSPContractService) GetContractById(contractID string) (*model.ZSPContractFile, error) { + return m.getContractByIdFn(contractID) +} +func (m *mockZSPContractService) GetContractWithDetails(contractID string) (*model.ZSPContractFile, error) { + return m.getContractWithDetailsFn(contractID) +} +func (m *mockZSPContractService) ListContracts(folderID string) ([]model.ZSPContractFile, error) { + return m.listContractsFn(folderID) +} +func (m *mockZSPContractService) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) { + return m.listContractsWithDetailsFn(folderID) +} +func (m *mockZSPContractService) DeleteContractById(contractID string) error { + return m.deleteContractByIdFn(contractID) +} +func (m *mockZSPContractService) UpdateContract(contract *model.ZSPContractFile) error { + return nil +} +func (m *mockZSPContractService) UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error { + return m.updateContractByIdFn(contractID, req) +} +func (m *mockZSPContractService) CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error { + return m.createContractDetailFn(req) +} +func (m *mockZSPContractService) UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error { + return m.updateContractDetailFn(req) +} +func (m *mockZSPContractService) GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) { + return m.getContractDetailByIdFn(detailID) +} +func (m *mockZSPContractService) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) { + return m.listContractDetailsFn(contractFileID) +} +func (m *mockZSPContractService) DeleteContractDetailById(detailID string) error { + return m.deleteContractDetailByIdFn(detailID) +} +func (m *mockZSPContractService) BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error { + return m.batchCreateContractDetailsFn(req) +} + +var _ service.ZSPContractService = (*mockZSPContractService)(nil) + +func setupZSPContractRouter(h *handler.ZSPContractHandler) *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + api := r.Group("/api") + { + zsp := api.Group("/zsp-contract") + { + zsp.POST("/folders", h.CreateFolder) + zsp.GET("/folders", h.GetFolders) + zsp.GET("/folders/:id", h.GetFolder) + zsp.DELETE("/folders/:id", h.DeleteFolder) + zsp.PUT("/folders/:id", h.UpdateFolder) + + zsp.POST("/contracts", h.UploadContract) + zsp.GET("/contracts/:id", h.GetContractWithDetails) + zsp.GET("/contracts", h.ListContractsWithDetails) + zsp.PUT("/contracts/:id", h.UpdateContract) + zsp.DELETE("/contracts/:id", h.DeleteContract) + + zsp.POST("/contracts/:id/details", h.CreateContractDetail) + zsp.GET("/contracts/:id/details", h.ListContractDetails) + zsp.POST("/details/batch", h.BatchCreateContractDetails) + zsp.GET("/details/:id", h.GetContractDetail) + zsp.PUT("/details/:id", h.UpdateContractDetail) + zsp.DELETE("/details/:id", h.DeleteContractDetail) + } + } + return r +} + +// ========== Folder Tests ========== + +func TestZSPContractCreateFolder_Success(t *testing.T) { + mock := &mockZSPContractService{ + createFolderFn: func(req *dto.ZSPFolderCreateRequest) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + body, _ := json.Marshal(dto.ZSPFolderCreateRequest{Name: "test folder", Description: "test desc"}) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-contract/folders", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestZSPContractCreateFolder_BadRequest(t *testing.T) { + mock := &mockZSPContractService{ + createFolderFn: func(req *dto.ZSPFolderCreateRequest) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-contract/folders", bytes.NewReader([]byte("{}"))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for missing required field, got %d", w.Code) + } +} + +func TestZSPContractGetFolders_Success(t *testing.T) { + mock := &mockZSPContractService{ + getFoldersFn: func() ([]model.ZSPContractFolder, error) { + return []model.ZSPContractFolder{ + {ID: 1, Name: "folder1"}, + {ID: 2, Name: "folder2"}, + }, nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/folders", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestZSPContractGetFolders_Error(t *testing.T) { + mock := &mockZSPContractService{ + getFoldersFn: func() ([]model.ZSPContractFolder, error) { + return nil, errors.New("database error") + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/folders", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestZSPContractGetFolder_Success(t *testing.T) { + mock := &mockZSPContractService{ + getFolderByIdFn: func(folderID string) (*model.ZSPContractFolder, error) { + return &model.ZSPContractFolder{ID: 1, Name: "test folder"}, nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/folders/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractDeleteFolder_Success(t *testing.T) { + mock := &mockZSPContractService{ + deleteFolderByIdFn: func(folderID string) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-contract/folders/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractDeleteFolder_Error(t *testing.T) { + mock := &mockZSPContractService{ + deleteFolderByIdFn: func(folderID string) error { + return errors.New("folder not found") + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-contract/folders/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestZSPContractUpdateFolder_Success(t *testing.T) { + mock := &mockZSPContractService{ + updateFolderByIdFn: func(folderID string, req *dto.ZSPFolderUpdateRequest) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + body, _ := json.Marshal(dto.ZSPFolderUpdateRequest{Name: "updated name", Description: "updated desc"}) + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/zsp-contract/folders/1", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +// ========== Contract Tests ========== + +func TestZSPContractListContracts_Success(t *testing.T) { + mock := &mockZSPContractService{ + listContractsWithDetailsFn: func(folderID string) ([]model.ZSPContractFile, error) { + return []model.ZSPContractFile{ + {ID: 1, ContractNumber: "C001", CompanyName: "公司A"}, + {ID: 2, ContractNumber: "C002", CompanyName: "公司B"}, + }, nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts?folderId=1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestZSPContractGetContract_Success(t *testing.T) { + mock := &mockZSPContractService{ + getContractWithDetailsFn: func(contractID string) (*model.ZSPContractFile, error) { + return &model.ZSPContractFile{ID: 1, ContractNumber: "C001", CompanyName: "公司A"}, nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractGetContract_Error(t *testing.T) { + mock := &mockZSPContractService{ + getContractWithDetailsFn: func(contractID string) (*model.ZSPContractFile, error) { + return nil, errors.New("contract not found") + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +func TestZSPContractDeleteContract_Success(t *testing.T) { + mock := &mockZSPContractService{ + deleteContractByIdFn: func(contractID string) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-contract/contracts/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractUpdateContract_Success(t *testing.T) { + mock := &mockZSPContractService{ + updateContractByIdFn: func(contractID string, req *dto.ZSPContractUpdateRequest) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + body, _ := json.Marshal(dto.ZSPContractUpdateRequest{ContractNumber: "C001-Updated"}) + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/zsp-contract/contracts/1", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +// ========== Contract Detail Tests ========== + +func TestZSPContractCreateContractDetail_Success(t *testing.T) { + mock := &mockZSPContractService{ + createContractDetailFn: func(req *dto.ZSPContractDetailCreateRequest) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + body, _ := json.Marshal(dto.ZSPContractDetailCreateRequest{ + ContractFileID: 1, + CommodityName: "燕麦", + TotalQuantity: 100, + UnitPrice: 500.0, + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-contract/contracts/1/details", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractListContractDetails_Success(t *testing.T) { + mock := &mockZSPContractService{ + listContractDetailsFn: func(contractFileID string) ([]model.ZSPContractDetail, error) { + return []model.ZSPContractDetail{ + {ID: 1, CommodityName: "燕麦", TotalQuantity: 100}, + {ID: 2, CommodityName: "大豆", TotalQuantity: 200}, + }, nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts/1/details", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestZSPContractGetContractDetail_Success(t *testing.T) { + mock := &mockZSPContractService{ + getContractDetailByIdFn: func(detailID string) (*model.ZSPContractDetail, error) { + return &model.ZSPContractDetail{ID: 1, CommodityName: "燕麦", TotalQuantity: 100}, nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-contract/details/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractDeleteContractDetail_Success(t *testing.T) { + mock := &mockZSPContractService{ + deleteContractDetailByIdFn: func(detailID string) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-contract/details/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractUpdateContractDetail_Success(t *testing.T) { + mock := &mockZSPContractService{ + updateContractDetailFn: func(req *dto.ZSPContractDetailUpdateRequest) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + body, _ := json.Marshal(dto.ZSPContractDetailUpdateRequest{ + ID: 1, + CommodityName: "燕麦-更新", + TotalQuantity: 150, + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/zsp-contract/details/1", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPContractBatchCreateContractDetails_Success(t *testing.T) { + mock := &mockZSPContractService{ + batchCreateContractDetailsFn: func(req *dto.ZSPContractDetailBatchCreateRequest) error { + return nil + }, + } + h := handler.NewZSPContractHandler(mock) + router := setupZSPContractRouter(h) + + details := []dto.ZSPContractDetailCreateRequest{ + {ContractFileID: 1, CommodityName: "燕麦", TotalQuantity: 100, UnitPrice: 500.0}, + {ContractFileID: 1, CommodityName: "大豆", TotalQuantity: 200, UnitPrice: 400.0}, + } + body, _ := json.Marshal(dto.ZSPContractDetailBatchCreateRequest{ + ContractFileID: 1, + Details: details, + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-contract/details/batch", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} \ No newline at end of file diff --git a/backend/test/zsp_finances_handler_test.go b/backend/test/zsp_finances_handler_test.go new file mode 100644 index 0000000..8d27b3a --- /dev/null +++ b/backend/test/zsp_finances_handler_test.go @@ -0,0 +1,676 @@ +package test + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rovina/zsp-backend/internal/dto" + "github.com/rovina/zsp-backend/internal/handler" + "github.com/rovina/zsp-backend/internal/model" + "github.com/rovina/zsp-backend/internal/service" + "time" +) + +// mockZSPFinancesService implements service.ZSPFinancesService for testing +type mockZSPFinancesService struct { + createFreightChargeFn func(req *dto.ZSPFreightChargesCreateRequest) error + getFreightChargeFn func(id string) (*model.ZSPFreightCharges, error) + listFreightChargesFn func() ([]model.ZSPFreightCharges, error) + updateFreightChargeFn func(id string, req *dto.ZSPFreightChargesUpdateRequest) error + deleteFreightChargeFn func(id string) error + + createMiscChargeFn func(req *dto.ZSPMiscChargesCreateRequest) error + getMiscChargeFn func(id string) (*model.ZSPMiscCharges, error) + listMiscChargesFn func() ([]model.ZSPMiscCharges, error) + updateMiscChargeFn func(id string, req *dto.ZSPMiscChargesUpdateRequest) error + deleteMiscChargeFn func(id string) error + + createServiceFeeFn func(req *dto.ZSPServiceFeesCreateRequest) error + getServiceFeeFn func(id string) (*model.ZSPServiceFees, error) + listServiceFeesFn func() ([]model.ZSPServiceFees, error) + updateServiceFeeFn func(id string, req *dto.ZSPServiceFeesUpdateRequest) error + deleteServiceFeeFn func(id string) error + + createCostBreakdownFn func(req *dto.ZSPCostBreakdownCreateRequest) error + getCostBreakdownFn func(id string) (*model.ZSPCostBreakdown, error) + listCostBreakdownsFn func(businessID string) ([]model.ZSPCostBreakdown, error) + updateCostBreakdownFn func(id string, req *dto.ZSPCostBreakdownUpdateRequest) error + deleteCostBreakdownFn func(id string) error + + createProfitRecordFn func(req *dto.ZSPProfitRecordCreateRequest) error + getProfitRecordFn func(id string) (*model.ZSPProfitRecord, error) + listProfitRecordsFn func(filters map[string]string) ([]model.ZSPProfitRecord, error) + updateProfitRecordFn func(id string, req *dto.ZSPProfitRecordUpdateRequest) error + deleteProfitRecordFn func(id string) error +} + +func (m *mockZSPFinancesService) CreateFreightCharge(req *dto.ZSPFreightChargesCreateRequest) error { + return m.createFreightChargeFn(req) +} +func (m *mockZSPFinancesService) GetFreightCharge(id string) (*model.ZSPFreightCharges, error) { + return m.getFreightChargeFn(id) +} +func (m *mockZSPFinancesService) ListFreightCharges() ([]model.ZSPFreightCharges, error) { + return m.listFreightChargesFn() +} +func (m *mockZSPFinancesService) UpdateFreightCharge(id string, req *dto.ZSPFreightChargesUpdateRequest) error { + return m.updateFreightChargeFn(id, req) +} +func (m *mockZSPFinancesService) DeleteFreightCharge(id string) error { + return m.deleteFreightChargeFn(id) +} + +func (m *mockZSPFinancesService) CreateMiscCharge(req *dto.ZSPMiscChargesCreateRequest) error { + return m.createMiscChargeFn(req) +} +func (m *mockZSPFinancesService) GetMiscCharge(id string) (*model.ZSPMiscCharges, error) { + return m.getMiscChargeFn(id) +} +func (m *mockZSPFinancesService) ListMiscCharges() ([]model.ZSPMiscCharges, error) { + return m.listMiscChargesFn() +} +func (m *mockZSPFinancesService) UpdateMiscCharge(id string, req *dto.ZSPMiscChargesUpdateRequest) error { + return m.updateMiscChargeFn(id, req) +} +func (m *mockZSPFinancesService) DeleteMiscCharge(id string) error { + return m.deleteMiscChargeFn(id) +} + +func (m *mockZSPFinancesService) CreateServiceFee(req *dto.ZSPServiceFeesCreateRequest) error { + return m.createServiceFeeFn(req) +} +func (m *mockZSPFinancesService) GetServiceFee(id string) (*model.ZSPServiceFees, error) { + return m.getServiceFeeFn(id) +} +func (m *mockZSPFinancesService) ListServiceFees() ([]model.ZSPServiceFees, error) { + return m.listServiceFeesFn() +} +func (m *mockZSPFinancesService) UpdateServiceFee(id string, req *dto.ZSPServiceFeesUpdateRequest) error { + return m.updateServiceFeeFn(id, req) +} +func (m *mockZSPFinancesService) DeleteServiceFee(id string) error { + return m.deleteServiceFeeFn(id) +} + +func (m *mockZSPFinancesService) CreateCostBreakdown(req *dto.ZSPCostBreakdownCreateRequest) error { + return m.createCostBreakdownFn(req) +} +func (m *mockZSPFinancesService) GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) { + return m.getCostBreakdownFn(id) +} +func (m *mockZSPFinancesService) ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) { + return m.listCostBreakdownsFn(businessID) +} +func (m *mockZSPFinancesService) UpdateCostBreakdown(id string, req *dto.ZSPCostBreakdownUpdateRequest) error { + return m.updateCostBreakdownFn(id, req) +} +func (m *mockZSPFinancesService) DeleteCostBreakdown(id string) error { + return m.deleteCostBreakdownFn(id) +} + +func (m *mockZSPFinancesService) CreateProfitRecord(req *dto.ZSPProfitRecordCreateRequest) error { + return m.createProfitRecordFn(req) +} +func (m *mockZSPFinancesService) GetProfitRecord(id string) (*model.ZSPProfitRecord, error) { + return m.getProfitRecordFn(id) +} +func (m *mockZSPFinancesService) ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) { + return m.listProfitRecordsFn(filters) +} +func (m *mockZSPFinancesService) UpdateProfitRecord(id string, req *dto.ZSPProfitRecordUpdateRequest) error { + return m.updateProfitRecordFn(id, req) +} +func (m *mockZSPFinancesService) DeleteProfitRecord(id string) error { + return m.deleteProfitRecordFn(id) +} + +var _ service.ZSPFinancesService = (*mockZSPFinancesService)(nil) + +func setupZSPFinancesRouter(h *handler.ZSPFinancesHandler) *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + api := r.Group("/api") + { + finances := api.Group("/zsp-finances") + { + // Freight charges + finances.POST("/freight-charges", h.CreateFreightCharge) + finances.GET("/freight-charges/:id", h.GetFreightCharge) + finances.GET("/freight-charges", h.ListFreightCharges) + finances.PUT("/freight-charges/:id", h.UpdateFreightCharge) + finances.DELETE("/freight-charges/:id", h.DeleteFreightCharge) + + // Misc charges + finances.POST("/misc-charges", h.CreateMiscCharge) + finances.GET("/misc-charges/:id", h.GetMiscCharge) + finances.GET("/misc-charges", h.ListMiscCharges) + finances.PUT("/misc-charges/:id", h.UpdateMiscCharge) + finances.DELETE("/misc-charges/:id", h.DeleteMiscCharge) + + // Service fees + finances.POST("/service-fees", h.CreateServiceFee) + finances.GET("/service-fees/:id", h.GetServiceFee) + finances.GET("/service-fees", h.ListServiceFees) + finances.PUT("/service-fees/:id", h.UpdateServiceFee) + finances.DELETE("/service-fees/:id", h.DeleteServiceFee) + + // Cost breakdowns + finances.POST("/cost-breakdowns", h.CreateCostBreakdown) + finances.GET("/cost-breakdowns/:id", h.GetCostBreakdown) + finances.GET("/cost-breakdowns", h.ListCostBreakdowns) + finances.PUT("/cost-breakdowns/:id", h.UpdateCostBreakdown) + finances.DELETE("/cost-breakdowns/:id", h.DeleteCostBreakdown) + + // Profit records + finances.POST("/profit-records", h.CreateProfitRecord) + finances.GET("/profit-records/:id", h.GetProfitRecord) + finances.GET("/profit-records", h.ListProfitRecords) + finances.PUT("/profit-records/:id", h.UpdateProfitRecord) + finances.DELETE("/profit-records/:id", h.DeleteProfitRecord) + } + } + return r +} + +// ========== Freight Charges Tests ========== + +func TestZSPFinancesCreateFreightCharge_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + createFreightChargeFn: func(req *dto.ZSPFreightChargesCreateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + body, _ := json.Marshal(dto.ZSPFreightChargesCreateRequest{ + ExpenseItem: "运费", + Amount: 1000.0, + PaymentDate: "2024-01-15", + FreightCompanyName: "货代公司A", + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-finances/freight-charges", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestZSPFinancesCreateFreightCharge_BadRequest(t *testing.T) { + mock := &mockZSPFinancesService{ + createFreightChargeFn: func(req *dto.ZSPFreightChargesCreateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-finances/freight-charges", bytes.NewReader([]byte("{}"))) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400 for missing required fields, got %d", w.Code) + } +} + +func TestZSPFinancesGetFreightCharge_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + getFreightChargeFn: func(id string) (*model.ZSPFreightCharges, error) { + return &model.ZSPFreightCharges{ + ID: 1, + ExpenseItem: "运费", + Amount: 1000.0, + FreightCompanyName: "货代公司A", + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/freight-charges/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesListFreightCharges_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + listFreightChargesFn: func() ([]model.ZSPFreightCharges, error) { + return []model.ZSPFreightCharges{ + {ID: 1, ExpenseItem: "运费", Amount: 1000.0}, + {ID: 2, ExpenseItem: "港杂费", Amount: 500.0}, + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/freight-charges", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestZSPFinancesUpdateFreightCharge_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + updateFreightChargeFn: func(id string, req *dto.ZSPFreightChargesUpdateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + body, _ := json.Marshal(dto.ZSPFreightChargesUpdateRequest{ + Amount: 1500.0, + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/zsp-finances/freight-charges/1", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesDeleteFreightCharge_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + deleteFreightChargeFn: func(id string) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-finances/freight-charges/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesDeleteFreightCharge_Error(t *testing.T) { + mock := &mockZSPFinancesService{ + deleteFreightChargeFn: func(id string) error { + return errors.New("record not found") + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-finances/freight-charges/999", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d", w.Code) + } +} + +// ========== Misc Charges Tests ========== + +func TestZSPFinancesCreateMiscCharge_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + createMiscChargeFn: func(req *dto.ZSPMiscChargesCreateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + body, _ := json.Marshal(dto.ZSPMiscChargesCreateRequest{ + ExpenseItem: "报关费", + Amount: 200.0, + PaymentDate: "2024-01-15", + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-finances/misc-charges", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesListMiscCharges_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + listMiscChargesFn: func() ([]model.ZSPMiscCharges, error) { + return []model.ZSPMiscCharges{ + {ID: 1, ExpenseItem: "报关费", Amount: 200.0}, + {ID: 2, ExpenseItem: "仓储费", Amount: 300.0}, + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/misc-charges", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesDeleteMiscCharge_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + deleteMiscChargeFn: func(id string) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-finances/misc-charges/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +// ========== Service Fees Tests ========== + +func TestZSPFinancesCreateServiceFee_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + createServiceFeeFn: func(req *dto.ZSPServiceFeesCreateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + body, _ := json.Marshal(dto.ZSPServiceFeesCreateRequest{ + Name: "代理服务费", + Amount: 500.0, + Date: "2024-01-15", + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-finances/service-fees", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesListServiceFees_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + listServiceFeesFn: func() ([]model.ZSPServiceFees, error) { + return []model.ZSPServiceFees{ + {ID: 1, Name: "代理服务费", Amount: 500.0}, + {ID: 2, Name: "咨询费", Amount: 300.0}, + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/service-fees", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesDeleteServiceFee_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + deleteServiceFeeFn: func(id string) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-finances/service-fees/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +// ========== Cost Breakdown Tests ========== + +func TestZSPFinancesCreateCostBreakdown_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + createCostBreakdownFn: func(req *dto.ZSPCostBreakdownCreateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + body, _ := json.Marshal(dto.ZSPCostBreakdownCreateRequest{ + BusinessID: "B001", + BusinessType: "contract", + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-finances/cost-breakdowns", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesListCostBreakdowns_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + listCostBreakdownsFn: func(businessID string) ([]model.ZSPCostBreakdown, error) { + return []model.ZSPCostBreakdown{ + {ID: 1, BusinessID: "B001", BusinessType: "contract", TotalCost: 1000.0}, + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/cost-breakdowns?businessId=B001", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesDeleteCostBreakdown_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + deleteCostBreakdownFn: func(id string) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-finances/cost-breakdowns/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +// ========== Profit Record Tests ========== + +func TestZSPFinancesCreateProfitRecord_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + createProfitRecordFn: func(req *dto.ZSPProfitRecordCreateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + body, _ := json.Marshal(dto.ZSPProfitRecordCreateRequest{ + BusinessID: "B001", + BusinessType: "contract", + Revenue: 2000.0, + TotalCost: 1500.0, + RecordDate: "2024-01-15", + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/zsp-finances/profit-records", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesGetProfitRecord_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + getProfitRecordFn: func(id string) (*model.ZSPProfitRecord, error) { + return &model.ZSPProfitRecord{ + ID: 1, + BusinessID: "B001", + BusinessType: "contract", + Revenue: 2000.0, + TotalCost: 1500.0, + Profit: 500.0, + RecordDate: time.Now(), + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesListProfitRecords_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + listProfitRecordsFn: func(filters map[string]string) ([]model.ZSPProfitRecord, error) { + return []model.ZSPProfitRecord{ + {ID: 1, BusinessID: "B001", Revenue: 2000.0, Profit: 500.0}, + {ID: 2, BusinessID: "B002", Revenue: 3000.0, Profit: 800.0}, + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + if resp["success"] != true { + t.Errorf("expected success true, got %v", resp["success"]) + } +} + +func TestZSPFinancesListProfitRecords_WithFilters(t *testing.T) { + mock := &mockZSPFinancesService{ + listProfitRecordsFn: func(filters map[string]string) ([]model.ZSPProfitRecord, error) { + if filters["businessId"] != "B001" { + t.Errorf("expected businessId filter B001, got %v", filters["businessId"]) + } + return []model.ZSPProfitRecord{ + {ID: 1, BusinessID: "B001", Revenue: 2000.0, Profit: 500.0}, + }, nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records?businessId=B001&businessType=contract", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesUpdateProfitRecord_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + updateProfitRecordFn: func(id string, req *dto.ZSPProfitRecordUpdateRequest) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + body, _ := json.Marshal(dto.ZSPProfitRecordUpdateRequest{ + Revenue: 2500.0, + TotalCost: 1600.0, + }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/zsp-finances/profit-records/1", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} + +func TestZSPFinancesDeleteProfitRecord_Success(t *testing.T) { + mock := &mockZSPFinancesService{ + deleteProfitRecordFn: func(id string) error { + return nil + }, + } + h := handler.NewZSPFinancesHandler(mock) + router := setupZSPFinancesRouter(h) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/api/zsp-finances/profit-records/1", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", w.Code) + } +} \ No newline at end of file diff --git a/docs/Git提交规范.md b/docs/Git提交规范.md new file mode 100755 index 0000000..a17390a --- /dev/null +++ b/docs/Git提交规范.md @@ -0,0 +1,114 @@ +# Git 提交规范文档 + +> 本文档规范项目中的 Git 提交信息格式,便于团队协作与版本追溯。 + +--- + +## 一、提交信息格式 + +采用 **Conventional Commits** 规范,格式如下: + +``` +<类型>[可选范围]: <简短描述> + +[可选的详细说明] + +[可选的脚注] +``` + +### 1.1 类型(type) + +| 类型 | 说明 | +| ---------- | ------------------------ | +| `feat` | 新功能 | +| `fix` | 缺陷修复 | +| `docs` | 文档变更(README、说明等) | +| `style` | 代码格式(空格、缩进等,不影响逻辑) | +| `refactor` | 重构( neither feat nor fix) | +| `perf` | 性能优化 | +| `test` | 测试相关 | +| `chore` | 构建、工具、依赖等 | + +### 1.2 可选范围(scope) + +标明影响模块,便于检索,例如: + +- `contract` - 合同管理 +- `finance` - 财务管理 +- `delivery` - 提货管理 +- `inventory` - 库存 +- `invoice` - 发票 +- `api` - 后端 API +- `frontend` - 前端 +- `backend` - 后端 + +--- + +## 二、提交示例 + +### 2.1 新功能 + +``` +feat(contract): 新增合同批量上传功能 +feat(finance): 结算管理支持导出 Excel +``` + +### 2.2 修复 + +``` +fix(api): 修复合同查询接口分页参数错误 +fix(frontend): 修复发票列表筛选不生效 +``` + +### 2.3 文档 + +``` +docs: 补充财务管理模块待确认问题 +docs(api): 更新合同管理 API 文档 +``` + +### 2.4 重构与优化 + +``` +refactor(invoice): 抽取发票上传公共逻辑 +perf(contract): 优化合同列表查询性能 +``` + +### 2.5 其他 + +``` +style: 统一前端代码缩进 +chore: 升级 Go 依赖版本 +test(finance): 补充结算单单元测试 +``` + +--- + +## 三、书写建议 + +1. **标题**:简洁明了,控制在 50 字符内;使用中文,动词开头。 +2. **一条提交做一件事**:功能、修复、格式尽量分开提交。 +3. **说明可省略**:简单变更可不写正文,复杂逻辑建议补充说明。 +4. **关联需求/问题**:如有 Jira/飞书等,可在脚注中写 `Refs: #123`。 + +--- + +## 四、反例(应避免) + +| 不良示例 | 问题说明 | +| ---------------- | ---------------------- | +| `update` | 类型不明确,描述含糊 | +| `修改了一些东西` | 没有说明修改了什么 | +| `fix bug` | 未说明修复的是哪个 bug | +| `WIP` | 未完成工作不宜直接合并 | + +--- + +## 五、工作流简要 + +- **main**:生产分支,稳定版本 +- **dev**:开发主分支 +- **feature/xxx**:功能开发分支 +- **fix/xxx**:缺陷修复分支 + +提交前请先在 `dev` 或对应 feature 分支上开发,完成后再合并到目标分支。 diff --git a/docs/api/前端路由文档.md b/docs/api/前端路由文档.md new file mode 100755 index 0000000..5618957 --- /dev/null +++ b/docs/api/前端路由文档.md @@ -0,0 +1,70 @@ +# 中尚鹏管理系统 - 前端路由文档 + +``` +作者: rovina +最近修订: 2026/3/2 +``` + +--- + +## 一、路由结构概览 + +前端基于 Vue 3 + Vue Router,采用扁平化二级路由结构。路由模块位于 `frontend/pure-admin-thin/src/router/modules/`。 + +--- + +## 二、合同管理模块路由 + +| 路径 | 名称 | 组件 | 说明 | +|------|------|------|------| +| `/system/contract` | 合同管理 | - | 父级重定向到业务合同管理 | +| `/system/contract/add-contract/index` | AddContract | AddContract.vue | **业务合同管理**:单合同录入、批量上传归档 | +| `/system/contract/zhongshangpeng/index` | ZhongshangpengContract | ZhongshangpengContract.vue | **中尚鹏合同管理**:公司合同文件夹、合同文件、详情表格 | +| `/system/contract/company-info/index` | CompanyInfo | CompanyInfo.vue | **公司信息管理** | + +### 2.1 业务合同管理(AddContract) + +- **单合同管理**:左侧按合同类型筛选(全部/采购/上游/下游),右侧合同列表,支持新增、编辑、删除 +- **批量上传**:左侧批量归档文件夹,右侧批量记录列表,支持新建文件夹、批量上传文件 + +### 2.2 中尚鹏合同管理(ZhongshangpengContract) + +- 文件夹归类、合同上传、合同详情(商品/数量/单价/已提货/未提货等) +- Excel 导入导出合同详情 +- **前后端已对接**:数据从 `/api/zsp-contract` 接口加载 + +### 2.3 公司信息管理(CompanyInfo) + +- 管理公司相关文件(营业执照、税务登记证、开票信息等) +- 文件夹分类、文件上传、查看、下载、删除 +- **前后端已对接**:数据从 `/api/company` 接口加载 + +--- + +## 三、其他模块路由(简要) + +| 模块 | 路径前缀 | 说明 | +|------|----------|------| +| 首页 | `/`, `/welcome` | 首页 | +| 提货管理 | `/system/delivery/*` | 我要提货、提货明细、库存、货权转移 | +| 财务管理 | `/system/finance/*` | 货代费用、杂费、服务费、成本构成、利润、结算、发票、对账单 | +| 查询报表 | `/system/query-report/*` | 合同执行、营业明细、其它明细、商品基础资料 | +| 采购管理 | `/system/purchase/*` | 采购报单、采购运踪、国外供应商、采购预算 | +| 更新看板 | `/system/changelog/index` | **更新内容看板**:展示系统更新记录 | + +--- + +## 四、路由配置说明 + +- 路由定义:`src/router/modules/contract.ts` 等 +- 自动导入:`src/router/index.ts` 通过 `import.meta.glob` 自动加载 `modules/**/*.ts`(排除 remaining.ts) +- 菜单渲染:`constantMenus` 用于侧边栏菜单展示 + +--- + +## 五、API 请求前缀 + +合同相关接口统一使用 `/api` 前缀,经 Vite 代理转发至后端: + +- 开发环境:`/api/*` → `http://127.0.0.1:8080/api/*` +- 生产环境:需配置 Nginx 或同类反向代理将 `/api` 转发至后端服务 diff --git a/docs/api/后端API文档-合同管理.md b/docs/api/后端API文档-合同管理.md new file mode 100755 index 0000000..4df18e3 --- /dev/null +++ b/docs/api/后端API文档-合同管理.md @@ -0,0 +1,306 @@ +# 中尚鹏管理系统 - 后端 API 文档(合同管理 + 提货管理) + +``` +作者: rovina +最近修订: 2026/3/2 +说明: 含合同管理、中尚鹏合同、公司信息、提货管理(我要提货/提货明细/库存/货权转移)。 +``` + +--- + +## 一、基础说明 + +- **Base URL**:`http://{host}:8080/api` +- **数据格式**:JSON(除文件上传为 multipart/form-data) +- **通用响应**:`{ "success": boolean, "message": string, "data"?: T }` +- **认证方式**:除 `/auth/login`、`/auth/register` 及静态文件路由外,所有 API 需在请求头中携带 `Authorization: Bearer `(JWT token 从登录接口获取) + +--- + +## 二、业务合同管理 API(/api/contract) + +### 2.1 单合同 CRUD + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/contract` | 获取合同列表 | +| GET | `/contract/:id` | 获取单个合同详情 | +| POST | `/contract/upload` | 上传合同(表单 + 文件) | +| PUT | `/contract/:id` | 更新合同(仅表单字段) | +| DELETE | `/contract/:id` | 删除合同 | + +#### GET /contract + +**Query 参数**:`contractType`(可选):1=采购 2=上游 3=下游 + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "name": "合同名称", + "contractType": "1", + "contractCode": "HT001", + "buyParty": "甲方", + "sellParty": "乙方", + "commodity": "商品", + "count": 100, + "unitPrice": 10.5, + "blNumber": "BL001;BL002", + "deliveryTime": "", + "pickUpTime": "", + "packaging": "", + "remarks": "", + "createTime": "2026-03-02T10:00:00Z", + "updateTime": "2026-03-02T10:00:00Z", + "files": [ + { "id": 1, "name": "合同.pdf", "url": "uploads/123_合同.pdf" } + ] + } + ] +} +``` + +**文件 URL 说明**:`url` 为相对路径,完整访问地址为 `GET /api/contract-files/{url}`,例如 `/api/contract-files/uploads/123_合同.pdf`。 + +#### POST /contract/upload + +**Content-Type**:`multipart/form-data` + +**表单字段**: +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | string | 是 | 合同名称 | +| contractType | string | 是 | 1/2/3 | +| contractCode | string | 是 | 合同编号 | +| buyParty | string | 是 | 甲方 | +| sellParty | string | 是 | 乙方 | +| commodity | string | 是 | 商品 | +| count | string | 是 | 数量 | +| unitPrice | string | 是 | 单价 | +| BLNumber | string | 是 | 提单号,最多 15 个,分号分隔 | +| deliveryTime | string | 否 | 交货时间 | +| pickUpTime | string | 否 | 提货时间 | +| packaging | string | 否 | 包装 | +| remarks | string | 否 | 备注 | +| files | File[] | 是 | 合同扫描件(PDF/图片,单文件≤10MB) | + +--- + +### 2.2 批量归档(文件夹 + 批量记录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/contract/folders` | 获取批量归档文件夹列表 | +| POST | `/contract/folders` | 新建文件夹 | +| GET | `/contract/folders/:id` | 获取单个文件夹 | +| PUT | `/contract/folders/:id` | 更新文件夹 | +| DELETE | `/contract/folders/:id` | 删除文件夹 | +| GET | `/contract/batches` | 获取批量记录列表 | +| POST | `/contract/batches` | 批量上传 | +| GET | `/contract/batches/:id` | 获取批量记录详情 | +| DELETE | `/contract/batches/:id` | 删除批量记录 | + +#### POST /contract/folders + +**请求体**: +```json +{ + "name": "文件夹名称", + "description": "描述(可选)" +} +``` + +#### GET /contract/batches + +**Query 参数**:`folderId`(可选):按文件夹筛选 + +#### POST /contract/batches + +**Content-Type**:`multipart/form-data` + +**表单字段**: +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| folderId | string | 是 | 归档文件夹 ID | +| batchName | string | 是 | 批次名称 | +| remark | string | 否 | 备注 | +| files | File[] | 是 | 批量文件 | + +--- + +### 2.3 合同文件静态资源(供前端预览/下载) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/contract-files/uploads/{filename}` | 单合同附件 | +| GET | `/api/contract-files/batch_archive/{filename}` | 批量归档文件 | +| GET | `/api/zsp-contract-files/files/{filename}` | 中尚鹏合同文件 | + +--- + +## 三、中尚鹏合同管理 API(/api/zsp-contract) + +与业务合同管理为两套独立体系,用于公司合同(上游/下游/外商采购/货代物流等)。 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/zsp-contract/folders` | 获取合同文件夹列表 | +| POST | `/zsp-contract/folders` | 创建文件夹 | +| GET | `/zsp-contract/folders/:id` | 获取单个文件夹 | +| PUT | `/zsp-contract/folders/:id` | 更新文件夹 | +| DELETE | `/zsp-contract/folders/:id` | 删除文件夹 | +| GET | `/zsp-contract/contracts` | 获取合同列表(含详情,可选 folderId) | +| POST | `/zsp-contract/contracts` | 上传合同文件 | +| GET | `/zsp-contract/contracts/:id` | 获取单个合同详情 | +| DELETE | `/zsp-contract/contracts/:id` | 删除合同 | +| POST | `/zsp-contract/contracts/:id/details` | 创建合同详情 | +| GET | `/zsp-contract/contracts/:id/details` | 获取合同详情列表 | +| PUT | `/zsp-contract/details/:id` | 更新合同详情 | +| DELETE | `/zsp-contract/details/:id` | 删除合同详情 | +| POST | `/zsp-contract/details/batch` | 批量创建合同详情 | +| POST | `/zsp-contract/contracts/:id/details/import` | Excel 导入合同详情 | +| GET | `/zsp-contract/contracts/:id/details/export` | 导出合同详情到 Excel | + +--- + +## 四、公司信息管理 API(/api/company) + +管理公司相关文件(营业执照、开票信息等)。 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/company/folders` | 获取文件夹列表 | +| POST | `/company/folders` | 创建文件夹 | +| PUT | `/company/folders/:id` | 更新文件夹 | +| DELETE | `/company/folders/:id` | 删除文件夹 | +| GET | `/company/files` | 获取文件列表(可选 folderId) | +| POST | `/company/files/upload` | 上传公司文件 | +| DELETE | `/company/files/:id` | 删除文件 | + +#### POST /company/files/upload + +**Content-Type**:`multipart/form-data` + +**表单字段**: +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| files | File[] | 是 | 文件列表 | +| data | string | 否 | JSON 字符串,含 companyName、fileCategory、expireDate、description、folderId | + +**文件访问**:`GET /api/company-files/{fileUrl}`,其中 fileUrl 为接口返回的 fileUrl 字段(如 `files/xxx.pdf`)。 + +--- + +## 五、提货管理 API + +提货管理包含四块:**我要提货**(提货委托)、**提货明细**(出库单)、**库存**(仓库/入库单/库存汇总)、**货权转移**。以下路径均需在 Base URL 下加 `/api` 前缀(与合同等接口一致),且需登录态。 + +### 5.1 我要提货(/api/apply-delivery) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/apply-delivery/list` | 分页列表 | +| GET | `/apply-delivery/detail/:id` | 单条详情 | +| POST | `/apply-delivery/create` | 新增 | +| PUT | `/apply-delivery/update/:id` | 更新 | +| PUT | `/apply-delivery/cancel/:id` | 取消(状态改为 cancelled) | +| DELETE | `/apply-delivery/:id` | 删除 | + +**GET /apply-delivery/list** +Query:`page`、`pageSize`、`orderNumber`、`blNumber`、`status`(可选)。 + +**响应**:`data: { items: DeliveryApply[], total, page, pageSize }`。 +单条字段:id, orderNumber, blNumber, licensePlate, driverName, driverPhone, driverIdCard, commodity, quantity, unit, deliveryDate, deliveryAddress, warehouse, status, remark, createTime, updateTime。 +status:pending / approved / in_progress / completed / cancelled。 + +**POST /apply-delivery/create** +Body(JSON):blNumber, licensePlate, driverName, driverPhone, driverIdCard, commodity, quantity, unit, deliveryDate, deliveryAddress, warehouse, remark。 +服务端自动生成 orderNumber、status=pending、createTime/updateTime。 + +--- + +### 5.2 提货明细(/api/delivery-details) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/delivery-details/list` | 分页列表 | +| GET | `/delivery-details/detail/:id` | 单条详情 | +| POST | `/delivery-details/create` | 新增出库单 | +| PUT | `/delivery-details/update/:id` | 更新 | +| DELETE | `/delivery-details/:id` | 删除 | + +**GET /delivery-details/list** +Query:`page`、`pageSize`、`outboundNumber`、`blNumber`、`outboundStatus`(可选)。 + +**响应**:`data: { items: DeliveryOutbound[], total, page, pageSize }`。 +单条字段:id, outboundNumber, deliveryOrderNumber, blNumber, licensePlate, driverName, driverPhone, commodity, commodityCode, quantity, unit, outboundDate, warehouse, operator, outboundStatus, receiptStatus, remark, createTime, updateTime。 + +--- + +### 5.3 库存(/api/inventory) + +#### 仓库 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/inventory/warehouses` | 仓库列表(不分页) | +| POST | `/inventory/warehouses/create` | 新增仓库 | +| PUT | `/inventory/warehouses/update/:id` | 更新 | +| DELETE | `/inventory/warehouses/delete/:id` | 删除 | + +仓库字段:id, code, name, address, contactPerson, contactPhone, status, remark, createTime, updateTime。 + +#### 入库单 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/inventory/inbound/list` | 分页列表 | +| POST | `/inventory/inbound/create` | 新增入库单 | +| PUT | `/inventory/inbound/update/:id` | 更新 | +| DELETE | `/inventory/inbound/delete/:id` | 删除 | + +**GET /inventory/inbound/list** +Query:`page`、`pageSize`、`warehouse`、`commodity`(可选)。 +入库单字段:id, inboundNumber, warehouse, commodity, commodityCode, blNumber, blWeight, inboundWeight, owner, contractNumber, inboundDate, operator, remark, createTime, updateTime。 + +#### 库存汇总 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/inventory/summary` | 按仓库+商品汇总:入库 - 出库 - 货权转移 = 当前库存 | + +Query:`warehouse`、`commodity`(可选)。 +响应:`data` 为汇总列表,含仓库、商品、入库量、出库量、货权转移量、当前库存等。 + +--- + +### 5.4 货权转移(/api/ownership-transfer) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/ownership-transfer/list` | 分页列表 | +| GET | `/ownership-transfer/detail/:id` | 单条详情(含附件列表) | +| POST | `/ownership-transfer/create` | 新增(仅表单) | +| POST | `/ownership-transfer/create-with-files` | 新增(表单 + 附件) | +| DELETE | `/ownership-transfer/:id` | 删除 | + +**GET /ownership-transfer/list** +Query:分页及筛选(如 transferNumber, blNumber, status, transferor, transferee, startDate, endDate 等,以实际后端为准)。 + +**POST /ownership-transfer/create-with-files** +Content-Type:`multipart/form-data`。 +表单字段:transferDate, blNumber, quantity, commodity, warehouse, transferor, transferee, remark;files:附件。 +货权转移单字段:id, transferNumber, transferDate, blNumber, quantity, commodity, warehouse, transferor, transferee, status, remark, fileList, createTime, updateTime。 +附件存于服务端 `./data/ownership/`,返回的 fileList 中 url 为相对路径(如 `ownership/xxx`)。若需通过 HTTP 访问,可配置静态路由(如 `/api/ownership-files/` 指向该目录),并在文档中说明。 + +--- + +## 六、错误码与状态 + +- `200`:成功 +- `400`:请求参数错误 +- `404`:资源不存在 +- `500`:服务器内部错误 diff --git a/docs/deploy/nginx.conf b/docs/deploy/nginx.conf new file mode 100755 index 0000000..93f3acf --- /dev/null +++ b/docs/deploy/nginx.conf @@ -0,0 +1,77 @@ +# 中尚鹏管理系统 - Nginx 生产配置 +# 放置于 /etc/nginx/conf.d/zsp.conf 或 /etc/nginx/sites-enabled/zsp +# +# 前置条件: +# 1. 前端构建产物位于 /var/www/zsp/dist +# 2. 后端服务运行于 127.0.0.1:8080 +# 3. 后端静态文件目录 /var/www/zsp/data 可读 + +server { + listen 80; + server_name your-domain.com; + + # 前端静态资源 + root /var/www/zsp/dist; + index index.html; + + # Gzip 压缩 + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; + gzip_min_length 1024; + + # 前端 SPA 路由:所有非文件/非 API 请求返回 index.html + location / { + try_files $uri $uri/ /index.html; + } + + # API 反向代理到后端 + location /api/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 大文件上传支持(合同附件等) + client_max_body_size 100m; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # 健康检查端点 + location /health { + proxy_pass http://127.0.0.1:8080/health; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + access_log off; + } + + # 静态文件缓存(合同附件、公司文件等) + location /api/contract-files/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + expires 7d; + add_header Cache-Control "public, immutable"; + } + + location /api/zsp-contract-files/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + expires 7d; + add_header Cache-Control "public, immutable"; + } + + location /api/company-files/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + expires 7d; + add_header Cache-Control "public, immutable"; + } + + # 错误页面 + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/docs/deploy/部署说明.md b/docs/deploy/部署说明.md new file mode 100755 index 0000000..5db339d --- /dev/null +++ b/docs/deploy/部署说明.md @@ -0,0 +1,160 @@ +# 中尚鹏管理系统 - 部署说明 + +``` +作者: rovina +最近修订: 2026/3/2 +``` + +--- + +## 一、环境要求 + +| 组件 | 版本 | +|------|------| +| Node.js | 18+ | +| pnpm | 8+ | +| Go | 1.21+ | +| MySQL | 8.0+ | + +--- + +## 二、后端部署 + +### 2.1 配置文件 + +复制并修改 `backend/configs/config.dev.yaml`: + +```yaml +server: + port: 8080 + mode: "release" # 生产环境建议 release + read_timeout: 30 + write_timeout: 30 + +database: + host: "127.0.0.1" # 或 MySQL 服务地址 + port: 3306 + user: "root" + password: "your_password" + dbname: "myapp" + max_open_conns: 100 + max_idle_conns: 10 + reset_database: false # 生产务必 false + +jwt: + secret: "your_jwt_secret" + expire_hours: 24 +``` + +### 2.2 启动 + +```bash +cd backend + +# 指定配置文件(可选,默认 ./configs/config.dev.yaml) +export CONFIG_PATH=./configs/config.prod.yaml + +go run ./cmd/api +# 或编译后运行 +go build -o zsp-api ./cmd/api && ./zsp-api +``` + +### 2.3 数据目录 + +- 单合同附件:`./data/contract/uploads/` +- 批量归档:`./data/contract/batch_archive/` + +首次运行会自动创建,需确保进程有写权限。 + +--- + +## 三、前端部署 + +### 3.1 开发环境 + +```bash +cd frontend +pnpm install +pnpm dev +``` + +前端默认端口 8848(可改 `.env.development` 中 VITE_PORT),API 请求 `/api/*` 会代理到 `http://127.0.0.1:8080`。 + +### 3.2 生产构建 + +```bash +cd frontend +pnpm install +pnpm build +``` + +产物在 `dist/`,需配合 Nginx 等 Web 服务器部署。 + +### 3.3 为什么打包部署后访问不到 8080 的数据? + +打包后前端是**静态资源**,在**用户浏览器**里运行。请求会发到「打开页面时的那台服务器的同源地址」: + +- 开发时:页面来自 `http://127.0.0.1:5173`,Vite 把 `/api` 代理到本机 8080,所以能访问。 +- 部署后:页面来自 `http://你的服务器:80`,浏览器只会把 `/api` 发到 `http://你的服务器:80/api`,**不会**自动发到 8080。 + +因此必须让**提供前端页面的 Web 服务器**(如 Nginx)把 `/api` 反向代理到本机 8080(或后端容器)。同时前端已统一用**相对路径** `/api/xxx`,不再写死 `127.0.0.1:8080`,这样部署后即可通过同源访问到后端。 + +### 3.4 Nginx 配置 + +完整配置文件见 `docs/deploy/nginx.conf`,包含: + +- SPA 路由重写(`try_files $uri /index.html`) +- API 反向代理(`/api` → `127.0.0.1:8080`) +- 大文件上传支持(`client_max_body_size 100m`) +- 静态文件缓存(合同附件 7 天缓存) +- Gzip 压缩 + +--- + +## 四、Docker 部署(可选) + +### 4.1 后端 Dockerfile 示例 + +```dockerfile +FROM golang:1.21-alpine AS builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o zsp-api ./cmd/api + +FROM alpine:latest +WORKDIR /app +COPY --from=builder /app/zsp-api . +COPY --from=builder /app/configs ./configs +RUN mkdir -p data/contract/uploads data/contract/batch_archive +EXPOSE 8080 +CMD ["./zsp-api"] +``` + +### 4.2 运行 + +```bash +# 需挂载配置与数据目录 +docker run -d -p 8080:8080 \ + -v $(pwd)/configs:/app/configs \ + -v $(pwd)/data:/app/data \ + -e CONFIG_PATH=/app/configs/config.prod.yaml \ + zsp-backend +``` + +--- + +## 五、健康检查 + +- 后端:`GET http://{host}:8080/health` 返回 `{"status":"ok"}` +- 合同 API:`GET http://{host}:8080/api/contract` 需返回 JSON + +--- + +## 六、常见问题 + +1. **跨域**:后端已启用 CORS,生产环境若前后端同域可无需额外配置。 +2. **文件访问**:合同文件通过 `/api/contract-files/{path}` 访问,确保 Nginx 将 `/api` 转发至后端。 +3. **数据库**:首次部署需确保 MySQL 已创建 `myapp` 库,表由 GORM AutoMigrate 自动创建。 +4. **JWT 认证**:除 `/api/auth/login`、`/api/auth/register` 外,所有 API 均需 JWT 认证。前端需在登录后存储 token,并在后续请求中通过 `Authorization: Bearer ` 头传递。token 有效期为 24 小时。 diff --git a/docs/dev/Talk.md b/docs/dev/Talk.md new file mode 100755 index 0000000..e8faa43 --- /dev/null +++ b/docs/dev/Talk.md @@ -0,0 +1,15 @@ +# 开发要求 + +和甲方沟通过程中的内容 + +他们其实诉求就是他们跟单人的诉求是这样的,因为我们比如说现在手上有1000吨货,那么这批货客户是要分很长时间才会提完的,可能就几十吨或者100吨,慢慢的提每一次,那么在第一个合同管理栏他。少写了,比如说除了上传合同之外,然后去做就空出来一个excel表格。这个Excel表格呢,他们就手动录入合同里面的一些数据信息数量啊,单价呀,已经提货的数量啊,然后这些东西。然后,系统呢,它那个excel表格就会自动生成海应提货,就是未提的货物对应那个合同有多少吨就会显示出来。然后,显示出来之后呢,合同管理这一栏就跟第四个查询系统这一块儿。就是要有关联起来,比如说关联就是关联合同编号,或者是商品信息,或者是提单号这样。 + +他这个大框架改的,我刚刚电话沟通了,我算是明白了一些,但是就是明天,我让他们上午再去做最后的就是确定下来,因为。就是这个主菜单,肯定是后面不能再改的。然后呢,二级菜单我们每一次做二级菜单,比如说合同管理这一栏要实现什么,在做之前我们再沟通一次。就是我觉得这样就也免得就是。就就麻烦你改来改去,因为我虽然不懂这个东西,但我知道可能大框架他们有变动,那等于你前面的可能就都白做了,你要重新再来。 + + +1、会涉及到多处表格内部分数据关联后生成新的表格数据。2、比如在利润表中的“成本“栏,是根据”货代费用+杂费”表格中数据以及利润表中单独设的“服务费”相加而成。但是能不能实现点“成本”金额,自动显示出这个成本总金额的构成,比如“货代费20万,杂费12万”。 + +问题:现在前端部分有两个问题:第一个:"营业明细表"的具体内容和字段是什么?是按年/月/合同维度汇总的报表?还是某个甲方提供的 Excel 模板? +第二个:合同执行情况中"差款"的计算规则是什么?是 合同金额 − 已结算金额?还是 合同金额 − 银行流水中已收金额?数据分别来自哪些表? +甲方回答:第一个,汇总的报表。 +第二个,等于收(付)款金额-提货数量*提货单价(提货数量需手填,单价可应用合同单价)第一个数据来自于“营业明细表”。第二个数据来自于“提货明细”这个表格。 \ No newline at end of file diff --git a/docs/dev/当前程序不足分析.md b/docs/dev/当前程序不足分析.md new file mode 100755 index 0000000..7c23a9c --- /dev/null +++ b/docs/dev/当前程序不足分析.md @@ -0,0 +1,220 @@ +# 中尚鹏管理系统 - 当前程序不足分析 + +本文档基于对现有前端代码与《项目开发要求文档》的对照,整理当前实现的不足与待确认点。 + +> **更新说明**:以下问题已在后续迭代中修复: +> - JWT 认证中间件已启用(`backend/internal/server/route.go`),所有业务 API 需携带 `Authorization: Bearer ` 头 +> - `reset_database` 已设为 `false`(配置文件和部署说明均已更新),重启不再清空数据库 +> - Handler 层测试已编写(`backend/test/`),覆盖用户注册/登录/查询、合同列表/查询/文件夹管理 +> - Delivery Handler 已重构为 Handler → Service → Repository 三层架构,不再直接使用 `*gorm.DB` + +--- + +## 一、与需求文档的菜单对照 + +| 需求文档模块 | 需求三级菜单概要 | 当前前端情况 | +|------------------------|--------------------------------------|--------------| +| 合同管理 | 新增合同 / 中尚鹏合同管理 / 公司信息 | 路由与页面均有 | +| 提货管理 | 我要提货 / 提货明细 / 库存 / 货权转移 / 其它 | 路由与页面均有 | +| 财务管理 | 结算 / 发票 / 利润明细 / 货代杂费+其它 / 对账单 | 路由与页面均有 | +| 查询+报表中心 | 合同执行 / 营业明细表 / 其它明细表 / 商品基础资料 / 其它 | 路由与页面均有 | +| 采购管理 | 采购报单 / 采购运踪 / 国外供应商 / 采购预算 | 路由与页面均有 | +| 权限管理 | 1 / 2(需求未展开) | 现有 permission 页面/按钮 | + +--- + +## 二、功能与业务层面的不足 + +### 2.1 合同管理 + +- **新增合同** + - 已实现:单合同录入、批量上传、提单号最多 15 个校验。 + - 不足/待确认:需求中“录入合同内容中相关数据的表格(会涉及简单的公式,我们会给出)”是否已按贵方提供的公式在表格中实现?若未提供公式,当前表格是否有明确占位或说明? + +- **中尚鹏合同管理** + - 已实现:文件夹归类、上传、按合同编号/公司名/商品/提单号等搜索。 + - 不足:需求要求“通过文件名包含商品、公司名、合同编号”进行搜索。当前是否对上传文件的**文件名**做解析并写入可搜索字段?若未解析文件名,则与“通过三要素搜索对应合同”的预期存在差距。 + +- **公司信息管理** + - 已存在页面,需确认:是否仅做“公司相关文件存储(如营业执照、开票信息)”即可,有无额外字段或审批流程要求。 + +### 2.2 提货管理 + +- **我要提货** + - 已实现:新增/变更/取消提货委托,以及模板下载等。 + - 不足/待确认:提货单是否已完全按贵方提供的**提货单模板**(含车牌号、提单号、司机信息、数量、商品品类等)生成与展示?模板若后续变更,是否有统一维护方式? + +- **提货明细** + - 需求:新增出库单、查看出库明细、收货确认单,且“录入数据的表格(后期我们给模板)”。 + - 不足:当前是否已接入“后期给的模板”?若未接入,表格结构与字段是否已与业务方对齐? + +- **库存信息** + - 需求公式:**入库表格中数量 − 新增出库单中数量 − 货权转移数量 = 库存数量**。 + - 不足:该公式是否在**前端展示与后端计算**中都已严格按此逻辑实现?库存明细是否按“仓库+商品+货主”等维度正确汇总? + +- **货权转移** + - 需求字段:货转日期、提单号、数量、商品、库方、货权出让方、货权接收方等。 + - 不足:当前“货权转移”相关表格与表单字段是否已全部包含上述字段,且与扫描件录入需求一致? + +### 2.3 财务管理 + +- **结算管理** + - 需求:新增结算单(导出下游合同结算单)、按单号查询、上传已结算单备份。 + - 不足:导出下游合同结算单的**数据来源与模板**是否已定稿?上传备份后的检索与展示是否满足“按单号查询”的完整流程? + +- **发票管理** + - 需求:上游发票、下游发票、货代杂费发票的上传及“对应表格录入发票对应信息”。 + - 不足:发票表格的字段是否与后续“待开发票情况”“已开票明细表”等统计完全一致,便于关联与报表使用? + +- **利润明细** + - 需求:“根据我们提供的 excel 表格创建一个表格(需要关联其他表格中数据 + 设置好栏目以及函数,后续自行手动录入)”。 + - 不足:当前利润明细页是否已**关联合同/结算/发票/货代杂费等其它表数据**?栏目与公式是否已按贵方提供的 Excel 定稿?若 Excel 尚未提供,当前页面是占位还是已有临时结构? + +- **货代费用 + 其他杂费** + - 需求字段:提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名。 + - 不足:当前表单与列表是否已完全覆盖上述字段,并与“利润明细”“历史清关成本”等可关联? + +- **对账单** + - 需求文档中“5.对账单”无具体内容描述。 + - 不足:对账单的业务范围、字段、与结算/发票的关系是否已有口头或补充文档说明?当前页面若存在,是否仅为占位? + +### 2.4 查询 + 报表中心 + +- **查询上下游客户合同执行情况** + - 需求:按合同编号/公司名/商品/提单号/日期查询,且**可查询到**: + - 该合同**剩余未提数量**(与“新增合同”表格关联); + - **差款**及**待开发票情况**(与“发票管理”关联); + - 支持**上传 Excel 表格(银行流水)**。 + - 不足: + - 当前“合同执行”页的**付款状态**为前端写死的 `"unpaid"`,并未与真实结算/发票数据关联,无法体现真实“差款”与“待开发票”。 + - “剩余未提数量”是否已与“新增合同”及出库/货权转移数据打通并一致? + - “上传银行流水”功能是否已实现?若已实现,银行流水与合同、收付款的关联规则是否明确? + +- **营业明细表** + - 需求:① 2025 年中尚鹏业务明细表。 + - 不足:当前“营业明细表”路由对应的是 **BusinessDetails.vue**,从代码看更偏向“合同文件/营业相关合同”的列表与搜索,与需求中的“2025 年业务明细表”可能不是同一张表。需确认:**营业明细表** 究竟是“按年的业务汇总表”,还是“合同维度明细表”,以便与菜单命名和实现对齐。 + +- **其它明细表** + - 需求:① 已开票明细表 ② 货代杂费明细表 ③ 历史清关成本明细表。 + - 不足:当前“其它明细表”为单一列表(OtherDetails),未在菜单或页面内拆分为上述**三个子表**。三个表的字段与数据来源是否已定义?是否需要在同一页用 Tab 区分,还是独立三个页面? + +- **商品基础资料** + - 需求:燕麦/大麦/豌豆/亚麻籽/葵粕/菜籽粕等,可批量上传扫描件或文档。 + - 不足:当前是否已按“商品类型”或“品类”做分类归档与展示?批量上传后的检索方式是否明确? + +### 2.5 采购管理 + +- **采购报单、采购运踪明细、国外供应商** + - 不足:需求为“批量上传扫描件 + 录入扫描件内容的表格”或“根据所提供的 excel 在系统中创建表格”。当前各页是否已接入贵方提供的**模板/Excel 结构**?若未提供,当前表格是否为暂定结构? + +- **采购预算** + - 需求:“已有单独的预算测算系统,最后按该预算系统功能等添加到我们系统中(这个最后有时间慢慢弄)”。 + - 不足:当前采购预算页是占位还是已有部分对接?与“单独预算系统”的对接方式(单点登录、接口、嵌入等)是否有初步方案? + +### 2.6 权限管理 + +- 需求文档仅写“1”“2”,无具体说明。 +- 不足:当前为通用“页面权限 + 按钮权限”结构。贵方是否需要**角色名、数据范围(如按公司/部门)、审批流**等具体需求?若有,需补充说明以便后续设计。 + +--- + +## 三、技术与实现层面的不足 + +### 3.1 API 与后端 + +- **双套 API 基址** + - 合同模块(如 `contract/upload`、`contract/batch-upload`)通过 `baseUrlApi` 直接写死 `http://127.0.0.1:8080/api/`。 + - 其余模块(如中尚鹏合同、财务、提货等)通过 `@/utils/http` 走相对路径(通常为 `/api`),在开发环境会经 Vite 代理到 127.0.0.1:8080。 + - 不足:基址不统一,不利于环境切换(开发/测试/生产)与后续维护;若后端实际未部署在 8080 或路径不同,合同相关请求易出错。 + +- **后端存在性** + - 当前仓库内未见后端工程目录,仅前端调用 8080 接口。 + - 不足:需确认后端是否独立仓库或独立服务、接口文档是否齐全、现有前端调用的接口是否已全部实现并联调通过。 + +### 3.2 功能未完成或占位 + +- **合同执行情况** + - 导出功能为“导出功能开发中”提示,未实现真实导出。 + +- **差款与待开发票** + - 未与结算、发票、银行流水等数据打通,无法展示真实“差款”与“待开发票情况”。 + +- **银行流水** + - 需求提到“上传 excel 表格(银行流水)”,当前代码中未检索到“银行流水”相关实现,需确认是否未做或位于其他模块。 + +### 3.3 数据关联与一致性 + +- 需求多处强调“关联其他表格”“设置好栏目以及函数”,例如: + - 利润明细 关联 合同/结算/发票/货代等; + - 合同执行 关联 新增合同、发票管理; + - 库存 = 入库 − 出库 − 货权转移。 +- 不足:当前实现中,这些**跨模块的数据关联与计算**是否已在后端或前端全部落地并校验一致,需要逐项对照需求做确认与测试。 + +--- + +## 四、小结 + +- **菜单与路由**:与需求文档的一、二、三级菜单基本对应,但“营业明细表”与“其它明细表”的页面职责与需求描述需要对齐。 +- **业务逻辑**:合同执行中的“差款”“待开发票”“剩余未提数量”“银行流水”未按需求打通;利润明细、库存公式、对账单、采购/提货模板等依赖贵方提供的表格或公式,需确认是否已提供并已实现。 +- **技术**:API 基址不统一、后端与接口实现情况不透明、部分导出与上传功能仍为占位或未实现。 + +建议在进入下一阶段开发前,先根据下文《需求确认问题清单》与业务方确认,再制定实现与联调计划。 + +--- + +## 五、需求确认问题清单(请产品/业务方确认) + +以下问题基于《项目开发要求文档》整理,便于明确开发范围与优先级,**请逐项确认或补充**。 + +### 合同管理 + +1. **新增合同**:需求中提到“会涉及简单的公式,我们会给出”。公式是否已提供?若已提供,请说明存放位置或文档名;若未提供,当前表格中的计算逻辑是否按临时规则处理即可? +2. **中尚鹏合同管理**:搜索是否必须基于**上传文件的文件名**解析出“商品、公司名、合同编号”? + - 若是,文件名命名规范是什么(例如:`商品_公司名_合同编号.pdf`)?是否需要在系统中做“文件名解析并写入可搜索字段”的功能? +3. **公司信息管理**:除“上传扫描件 + 文件夹归类”外,是否需要维护公司主数据(如公司名、税号、开户行等)?还是仅文件存储即可? + +### 提货管理 + +4. **我要提货**:提货单模板(含车牌号、提单号、司机信息、数量、商品品类等)是否已定稿?若已提供,请说明文件位置;若会变更,变更后由谁在系统中更新? +5. **提货明细**:需求写“录入数据的表格(后期我们给模板)”。模板是否已提供?若未提供,当前表格字段是否可作为正式版使用? +6. **库存信息**:公式“入库 − 出库 − 货权转移 = 库存”是否必须按**仓库 + 商品 + 货主(及合同)**等维度精确到最细粒度?是否有“只按仓库汇总”等简化展示需求? +7. **货权转移**:除需求列出的字段外,是否还有必填或必展示字段?已生效的货权转移函“上传”与“新增货权转移函”是两条独立流程还是需要关联? + +### 财务管理 + +8. **结算管理**:“导出下游合同结算单”的数据来源是哪些表?是否有固定 Excel 模板?若有,请提供模板或字段说明。 +9. **发票管理**:上游/下游/货代杂费发票的“对应表格”字段是否已定稿?是否需要与“已开票明细表”“待开发票情况”完全一致以便关联? +10. **利润明细**:需求写“根据我们提供的 excel 表格创建……关联其他表格中数据 + 设置好栏目以及函数”。 + - Excel 模板是否已提供?若已提供,请说明位置。 + - “其他表格”具体指哪些(合同、结算、发票、货代费用、清关成本等)?关联规则(按合同号、提单号、日期等)是什么? +11. **货代费用 + 其他杂费**:与“利润明细”“历史清关成本明细表”的关联方式是否已明确(例如通过提单号、合同编号、日期)? +12. **对账单**:对账单的业务含义、使用场景、字段与数据来源是否已有说明?若有补充文档或样例,请提供或简述。 + +### 查询 + 报表中心 + +13. **合同执行情况**: + - “差款”的定义是什么(例如:合同金额 − 已收款项)?数据来源是结算单、银行流水还是其它? + - “待开发票情况”是指:已结算但未开票的金额/列表?数据来源是否与发票管理表一致? + - “上传银行流水”的 Excel 格式是否已定?上传后是否需要解析并自动匹配到合同/收付款,还是仅作附件存档? +14. **营业明细表**:“2025 年中尚鹏业务明细表”与当前菜单中的“营业明细表”是否同一张表?若否,请说明需求中的这张表应展示哪些维度(按合同、按客户、按商品、按月份等)。 +15. **其它明细表**:已开票明细表、货代杂费明细表、历史清关成本明细表: + - 三张表是独立页面还是同一页面下三个 Tab/子表? + - 每张表的字段与数据来源是否已定义?历史清关成本的数据从哪里录入或导入? + +### 采购管理 + +16. **采购报单 / 采购运踪明细 / 国外供应商**:各模块的“我们提供的 excel”或“扫描件内容表格”是否已提供?若未提供,是否同意以当前系统中的表格结构为初版,后续再按模板调整? +17. **采购预算**:与“单独的预算测算系统”的对接方式是否有初步方案(例如:链接跳转、单点登录、接口拉取数据、iframe 嵌入等)?优先级是否确认为“最后有时间再弄”? + +### 权限管理 + +18. 需求中“权限管理 1、2”具体指什么?是否需要角色管理、数据权限(按公司/部门)、审批流、操作日志等?若有,请列出清单或提供简单说明。 + +### 其它 + +19. 是否有未在《项目开发要求文档》中体现的隐藏需求或口头约定(例如:某表需要审批后才能生效、某报表需要定时发送邮件等)? +20. 后端服务与接口:当前后端是否已有独立项目或部署地址?前端调用的接口是否有完整文档(如 Swagger/Word)?若部分接口尚未实现,请标明模块或接口路径,便于排期。 + +--- + +*文档生成后请根据实际业务与最新需求增删改,本分析不修改任何代码。* diff --git a/docs/dev/财务管理-待确认问题.md b/docs/dev/财务管理-待确认问题.md new file mode 100755 index 0000000..adc6946 --- /dev/null +++ b/docs/dev/财务管理-待确认问题.md @@ -0,0 +1,85 @@ +# 财务管理模块 - 待确认问题清单 + +> 在实现财务管理前后端前,请逐条回答或标注「暂不考虑」。回答完成后再补充前后端代码。 + +--- + +## 一、结算管理 + +**1.1** 需求写「新增结算单(导出下游合同结算单)」: +「导出下游合同结算单」是指:**(a)** 从系统中已有的下游合同数据(如中尚鹏合同/业务合同)里选一份合同,一键生成并下载一份结算单文档(PDF/Excel)?**(b)** 还是仅在前端提供「结算单表单」,手动录入后保存,再支持导出为文件?**(c)** 其他方式? + +**1.2** 「根据单号查询」:结算单号规则是否由系统自动生成(如 `STL2026030001`)?还是允许用户手动填写单号? + +**1.3** 「上传结算单(已交易且结算完毕的,主要为了备份)」: +上传的结算单是否需要和「已录入的结算单记录」关联(例如上传后关联到某条结算单号)?还是仅作为独立附件存储,按文件夹/时间归类即可? + +**1.4** 结算单「录入扫描件内容数据的表格」除当前模型已有字段(结算单号、合同编号、合同类型、公司名、商品、数量、单价、金额、结算日期、状态、附件路径、备注)外,是否还需要增加或删减字段?若有,请列出。 + +--- + +## 二、发票管理 + +**2.1** 需求为「上游发票(上传)、下游发票(上传)、货代杂费发票(上传)」且已确认三张独立表。 +当前三张表均含:发票号、合同编号、提单号、金额/税额/价税合计、开票日期、状态、附件路径、备注;上游多了「供应商名」、下游多了「客户名」、货代杂费多了「公司名」「费用项目」。 +是否还需要其他字段(如税率、发票类型、购方/销方税号等)? + +**2.2** 发票的「合同编号」是否需要与系统内已有合同(业务合同/中尚鹏合同)做下拉关联选择?还是自由文本即可? + +**2.3** 上传发票后,附件存储方式是否与「合同管理/公司信息」一致:按文件夹归类 + 记录文件路径到该条发票记录的 `file_path`? + +--- + +## 三、利润明细 + +**3.1** 需求为「2025年业务利润表」「关联其他表格中数据 + 设置好栏目以及函数,后续自行手动录入」。 +当前理解:收入来自合同金额,成本 = 货代费用 + 杂费 + 服务费,利润 = 收入 − 成本。 +请确认:**(a)** 利润表是按「年度」一张总表(如 2025 年一整年),**(b)** 还是按「合同/业务单」逐条有对应利润记录?**(c)** 或两者都要(既有年度汇总,又有单笔明细)? + +**3.2** 「栏目以及函数」除「利润 = 收入 − 成本」、以及可能的「利润率 = 利润/收入」外,是否还有其它公式或统计维度(如按商品、按公司汇总)?若甲方尚未提供,是否先只做「收入、成本、利润、利润率」四栏? + +**3.3** 利润明细的数据来源:**(a)** 完全由后端根据合同、货代/杂费/服务费自动汇总计算,前端只读展示;**(b)** 支持在前端表格中手动改写部分单元格(覆盖自动结果);**(c)** 以手动录入为主,仅部分字段可从其他表带出。请选一种或说明组合方式。 + +--- + +## 四、货代费用 + 杂费(与菜单结构) + +**4.1** 需求表里写「4.货代费用+其他杂费」合并为一条二级菜单,但当前前端是「货代费用」「杂费管理」两个并列子菜单。 +是否保持现有两个子菜单(货代费用、杂费管理)不变?还是希望合并为一个「货代费用与杂费」页面,用 Tab 或筛选区分「货代」与「杂费」? + +**4.2** 货代费用、杂费、服务费三者是否都需要「支持上传附件」(如发票扫描件)?当前货代/杂费/服务费模型是否需增加 `file_path` 或类似字段? + +--- + +## 五、对账单 + +**5.1** 需求仅写「5.对账单」,无具体说明。当前后端有 `ZSPReconciliation` 模型(对账单号、合同编号、公司名、账期起止、总金额、已付、未付、状态、附件路径等)。 +请确认对账单的业务含义:**(a)** 与「结算单」一一对应或一对多(如多笔结算汇总成一张对账单)?**(b)** 与结算单无关,是独立与客户/供应商的阶段性对账?**(c)** 其他(请简要说明)。 + +**5.2** 对账单的「总金额 / 已付 / 未付」是否由系统根据结算单或付款记录自动汇总?还是完全手动录入? + +**5.3** 对账单是否也需要「上传已确认的对账单扫描件」作为备份?若需要,是否与结算单一样按「记录 + 附件路径」即可? + +--- + +## 六、接口与路由约定 + +**6.1** 结算、发票、对账单的后端 API 是否统一放在现有 `/api/zsp-finances/` 下,例如: +- 结算:`/api/zsp-finances/settlements`(CRUD) +- 上游/下游/货代杂费发票:`/api/zsp-finances/upstream-invoices`、`downstream-invoices`、`freight-misc-invoices` +- 对账单:`/api/zsp-finances/reconciliations` +是否同意?若有不同路径规范请说明。 + +**6.2** 财务相关列表接口是否需要分页、按结算单号/合同号/公司名/日期范围等筛选?若需要,请列出各模块的常用筛选条件。 + +--- + +## 七、其他 + +**7.1** 财务管理各子模块(结算、发票、利润、货代、杂费、服务费、对账单)的菜单顺序是否与当前前端路由一致(结算 → 发票 → 利润明细 → 货代费用 → 杂费管理 → 服务费管理 → 对账单)?如需调整请说明。 + +**7.2** 是否有「财务数据不可删除、仅可作废/红冲」等合规要求?若有,哪些表需要支持状态为「作废」而不是物理删除? + +--- + +*回答方式:可在每题后直接写答案,或另起文档写「题号 + 答案」。完成后告知,再开始补充前后端实现。* diff --git a/docs/dev/项目开发文档.md b/docs/dev/项目开发文档.md new file mode 100755 index 0000000..f2b7774 --- /dev/null +++ b/docs/dev/项目开发文档.md @@ -0,0 +1,299 @@ +# 项目开发文档 + +``` +作者:rovina +最近修订日期:2026/3/2 +``` + +--- + +## 一、项目技术栈概览 + + +| 层级 | 技术 | 说明 | +| --- | --------------------------------- | --------------------- | +| 前端 | Vue 3 + TypeScript + Element Plus | 基于 pure-admin-thin 模板 | +| 后端 | Go + Gin + GORM | RESTful API,MySQL 存储 | +| 数据库 | MySQL 8 (Docker) | 数据库名 `myapp_dev` | + + +--- + +## 二、后端已实现的路由与模型 + +### 2.1 已有后端路由(`route.go`) + +``` +# 认证 +POST /api/auth/register +POST /api/auth/login +GET /api/users/:id + +# 合同上传(旧版,Contract 模型) +POST /api/contract/upload +POST /api/contract/batch-upload + +# 中尚鹏合同管理(ZSPContract 模型) +POST /api/zsp-contract/folders +GET /api/zsp-contract/folders +GET /api/zsp-contract/folders/:id +PUT /api/zsp-contract/folders/:id +DELETE /api/zsp-contract/folders/:id +POST /api/zsp-contract/contracts (上传合同文件) +GET /api/zsp-contract/contracts (含详情列表) +GET /api/zsp-contract/contracts/:id (含详情) +DELETE /api/zsp-contract/contracts/:id +POST /api/zsp-contract/contracts/:id/details +GET /api/zsp-contract/contracts/:id/details +POST /api/zsp-contract/details/batch +GET /api/zsp-contract/details/:id +PUT /api/zsp-contract/details/:id +DELETE /api/zsp-contract/details/:id +POST /api/zsp-contract/contracts/:id/details/import (Excel导入) +GET /api/zsp-contract/contracts/:id/details/export (Excel导出) + +# 财务管理(ZSPFinances 模型) +CRUD /api/zsp-finances/freight-charges (货代费用) +CRUD /api/zsp-finances/misc-charges (杂费) +CRUD /api/zsp-finances/service-fees (服务费) +CRUD /api/zsp-finances/cost-breakdowns (成本构成) +CRUD /api/zsp-finances/profit-records (利润记录) +``` + +### 2.2 已有后端数据模型 + + +| 模型 | 说明 | AutoMigrate | +| ------------------- | -------------------------- | --------------------- | +| `User` | 用户表 | 是 | +| `Contract` | 合同(旧版,仅名称/编号/甲乙方等基础字段) | 是 | +| `ContractFile` | 合同附件(旧版) | 是 | +| `ZSPContractFolder` | 合同文件夹 | 是 | +| `ZSPContractFile` | 合同文件(含合同编号/公司/商品/签订日期/类型) | 是 | +| `ZSPContractDetail` | 合同详情(商品/数量/单价/已提货/应提货/提单号) | **否,未加入 AutoMigrate** | +| `ZSPFreightCharges` | 货代费用 | **否,未加入 AutoMigrate** | +| `ZSPMiscCharges` | 杂费 | **否,未加入 AutoMigrate** | +| `ZSPServiceFees` | 服务费 | **否,未加入 AutoMigrate** | +| `ZSPCostBreakdown` | 成本构成(关联货代/杂费/服务费) | **否,未加入 AutoMigrate** | +| `ZSPProfitRecord` | 利润记录(关联成本构成) | **否,未加入 AutoMigrate** | + + +### 2.3 后端完全缺失的模块(前端有 API 定义但后端无路由/模型) + + +| 前端 API 文件 | 调用路径前缀 | 后端状态 | +| ---------------------- | ----------------------- | ----------------------------------- | +| `applyDelivery.ts` | `/apply-delivery/`* | 无路由、无模型、无 handler | +| `deliveryDetails.ts` | `/delivery-details/`* | 无路由、无模型、无 handler | +| `inventory.ts` | `/inventory/*` | 无路由、无模型、无 handler | +| `ownershipTrAns1fer.ts` | `/ownership-trAns1fer/*` | 无路由、无模型、无 handler(且前端大部分 API 函数被注释) | +| `company.ts` | `/company/*` | 无路由、无模型、无 handler | +| 结算管理 | 未知 | 无前端 API 文件、无后端 | +| 发票管理 | 未知 | 无前端 API 文件、无后端 | +| 采购管理 | 未知 | 无前端 API 文件、无后端 | +| 查询报表 | 复用 zspContract API | 无独立后端 | + + +--- + +## 三、已发现的技术问题 + +### 3.1 AutoMigrate 不完整 + +`pkg/database/migrate.go` 只注册了 5 个模型,缺少 `ZSPContractDetail` 及所有财务相关模型(共 6 个),后端启动后这些表不会自动创建。 + +### 3.2 双套合同模型 + +后端同时存在 `Contract`(旧版)和 `ZSPContractFile`(新版)两套合同模型: + +- `Contract` 字段:Name/ContractType/ContractCode/BuyParty/SellParty/Count/UnitPrice/BLNumber 等,且 Count、UnitPrice 均为 string 类型。 +- `ZSPContractFile` + `ZSPContractDetail`:更完整,有数值类型的 TotalQuantity/UnitPrice/DeliveredQty/PendingQty。 + +前端 `AddContract.vue` 调用旧版 `contract/upload`;中尚鹏合同管理、合同执行等调用新版 `zsp-contract/*`。 + +### 3.3 Auth 中间件已注释 + +`route.go` 中 `authorized.Use(authMiddleware)` 被注释,所有接口无鉴权保护。 + +### 3.4 API 基址不统一 + +- `contract.ts`:用 `baseUrlApi()` 写死 `http://127.0.0.1:8080/api/`,不走 Vite 代理。 +- 其余 API:用 `@/utils/http`,走 Vite 代理 `/api` -> `127.0.0.1:8080`。 + +### 3.5 财务模型字段与需求不匹配 + +需求中"货代费用"要求字段为:**提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名**。 + +当前后端 `ZSPFreightCharges` 模型仅有:Name、Amount、Currency、Date、Remark,**缺少提单号、上游合同编号、货代公司名等关键业务字段**。 + +--- + +## 四、需要你确认的问题 + +> 以下问题分为 **业务理解** 和 **技术决策** 两类,请逐条回答或标注"暂不考虑"。 + +### A. 合同管理 + +**A1.** 旧版 `Contract` 模型和新版 `ZSPContractFile + ZSPContractDetail` 模型都在使用中。你的计划是: + +- (a) 废弃旧版,把"新增合同"页面也改用新版模型? +- (b) 两套并存,各管各的? +- (c) 其他方案? + +**Ans1: b新增合同部分要注意区分中尚鹏合同管理和新增合同是两套并行的合同管理,在业务需求端新增合同其实应该是合同管理或者叫做业务合同管理,建议改掉,然后中尚鹏合同管理只用负责公司相关的合同就行** + +**A2.** 旧版 `Contract` 的 `Count`(数量)和 `UnitPrice`(单价)字段是 `string` 类型,无法直接参与数值计算。如果保留旧版,是否需要改成数值类型? +**Ans1: 我害怕有浮点计算风险,如果你能解决这个问题也未尝不可。** + +**A3.** Talk 中提到"合同管理里空一个 Excel 表格,手动录入合同数据(数量、单价、已提货数量等),系统自动计算未提货数量"。这个"Excel 表格"是否就是当前 `ZSPContractDetail`(合同详情)表的逐行录入形式?还是甲方期望一个**真正可编辑的类 Excel 界面**(如可拖拽选区、复制粘贴公式等 SpreadSheet 组件)? +**Ans1: 甲方可能希望的是一个表的逐行录入形式类似csv格式的前端显示,但是有并发修改的需求** + +**A4.** 甲方说"公式后续会给出"。目前后端 `ZSPContractDetail.BeforeCreate` 只做了一个公式:`PendingQty = TotalQuantity - DeliveredQty`。除此之外是否还有其他公式需要实现?如果甲方还没给,是否先只保留这一个? +**Ans1: 甲方还没给** + +**A5.** 中尚鹏合同管理的搜索:甲方说"我们会备注好文件名包含商品、公司名、合同编号"。当前后端 `ZSPContractFile` 模型已有独立的 `ContractNumber`、`CompanyName`、`Commodity` 字段。那么搜索逻辑是: + +- (a) 上传时手动填写这三个字段,搜索时按字段查?(当前实现) +- (b) 从文件名自动解析出这三个字段?如果是,文件名格式是什么? +**Ans1: (a)方法** + + +### B. 提货管理 + +**B1.** 提货管理(我要提货、提货明细、库存、货权转移)的后端完全没有实现。前端已定义了 API 类型和调用路径。开发顺序是否按 `我要提货 → 提货明细 → 库存 → 货权转移` 依次实现? +**Ans1: 是的** + +**B2.** "我要提货"需求中说"根据我们提供的提货单模板创建"。模板是否已提供?如果提供了,请告诉我文件位置;如果没有,是否按当前前端 `ApplyDelivery.vue` 的字段结构先做? +**Ans1: 目前还没提供,这个部分先不做** + +**B3.** 库存计算公式:`库存 = 入库重 − 出库数量 − 货权转移数量`。这个计算在后端做(API 返回已算好的库存值),还是前端拿到三个数字自己算? +**Ans1: 这个后端来算吧** + +**B4.** 货权转移前端 `ownershipTrAns1fer.ts` 中大部分 API 函数被注释了(create、print、export、delete、upload),只保留了 `getOwnershipList`。这些注释掉的功能是否都要实现? +**Ans1: 可能是Ai生成的内容,可信度不高,你按照自己的想法来** + +**B5.** 需求中"货权转移"要求字段包含**库方**(warehouse)。这里的"库方"是否就是"仓库管理"中已维护的仓库列表?即两者共用同一套仓库数据? +**Ans1: 公司是一个对接上游公司和下游公司的业务,从上游公司拿货给下游公司这样的,库方一个指的是仓库管理中的仓库列表** + +### C. 财务管理 + +**C1.** 当前后端的 `ZSPFreightCharges`(货代费用)模型字段很少(仅 name/amount/currency/date/remark),需求要求的"提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名"基本都缺。是否需要重新设计这个模型的字段? +**Ans1: 是的,重新设计** + +**C2.** Talk 中提到"利润表中的成本栏 = 货代费用 + 杂费 + 服务费,且点击成本金额能显示构成明细(如货代费 20 万、杂费 12 万)"。当前后端已有 `ZSPCostBreakdown` 模型关联了三种费用。这个设计方向是否正确?是否需要调整? +**Ans1: 是的,重新设计** + +**C3.** 利润明细需要"关联其他表格中数据"。Talk 中提到的"其他表格",你认为具体是指哪些?我目前的理解是: + +- 收入 ← 来自合同金额(ZSPContractDetail.TotalQuantity × UnitPrice) +- 成本 ← 来自货代费用 + 杂费 + 服务费(ZSPCostBreakdown) +- 利润 = 收入 − 成本 +- 这个理解对吗?还是收入来源不同(比如来自结算单)? +**Ans1: 应该是对的** + +**C4.** 结算管理、发票管理、对账单:前端目前没有独立的 API 文件,后端也没有实现。这三个模块的优先级如何?是否排在合同和提货之后? +**Ans1: 需要你来实现** + + +**C5.** 发票管理中"上游/下游/货代杂费发票",这三类发票是否共用一个数据表(通过 type 字段区分),还是需要独立三张表? +**Ans1: 独立三张表** + + +### D. 查询 + 报表中心 + +**D1.** 合同执行情况中"差款"的计算规则是什么?是 `合同金额 − 已结算金额`?还是 `合同金额 − 银行流水中已收金额`?数据分别来自哪些表? +**Ans1: 先不做这一部分** + +**D2.** "待开发票情况"的计算规则是什么?是 `已结算金额 − 已开票金额`? +**Ans1: 是** + +**D3.** "上传银行流水":上传后是否需要**解析 Excel 内容并自动匹配到合同**?还是仅当附件保存,人工查看?如果要解析,Excel 的列格式是什么? +**Ans1: 仅当附件保存,人工查看** + +**D4.** "其它明细表"下有三个子表:已开票明细表、货代杂费明细表、历史清关成本明细表。当前前端只有一个 `OtherDetails.vue`。是在一个页面用 Tab 切换三个子表,还是拆成三个独立页面/路由? +**Ans1: 用 Tab 切换三个子表** + +**D5.** "营业明细表"的具体内容和字段是什么?是按年/月/合同维度汇总的报表?还是某个甲方提供的 Excel 模板? +**Ans1: 先不做这一部分** + + +### E. 采购管理 + +**E1.** 采购管理的四个子模块(采购报单、采购运踪明细、国外供应商、采购预算)目前前端有路由和页面,但前端 API 只有 `PurchaseOrder.vue` 有部分内容,后端完全没有。这些模块的开发优先级是否排在合同/提货/财务之后? +**Ans1: 这是之前用AI生成的前端内容,可信度并不高** + +**E2.** 采购预算需求说"已有单独的预算测算系统"。对接方式你倾向哪种:链接跳转、iframe 嵌入、还是接口拉数据?还是说现阶段完全不做? +**Ans1: 完全不做** + +### F. 权限管理 + +**F1.** 需求中"权限管理 1、2"没有展开。当前系统是否只需要最基础的**角色 + 菜单权限**(如管理员看全部、普通用户看部分菜单)?还是需要更复杂的**数据权限**(如只能看自己创建的合同)或**审批流**? +**Ans1: 只需要最基础的** + +**F2.** 当前后端 Auth 中间件已注释。是否需要在这一阶段启用 JWT 鉴权?还是等功能都做完再加? +**Ans1: 等所有的内容做完了再加吧** + +### G. 整体开发计划 + +**G1.** 你期望的模块开发顺序是什么?我建议按依赖关系排序: + +1. 合同管理(基础数据,其他模块依赖合同编号/提单号) +2. 提货管理(依赖合同) +3. 库存信息(依赖入库/出库/货权转移) +4. 财务管理(依赖合同、提货数据) +5. 查询报表(依赖以上所有数据) +6. 采购管理(相对独立) +7. 权限管理(最后统一加) + +这个顺序你是否同意?或者有其他优先级安排? +**Ans1: 同意** + +**G2.** 甲方 Talk 中说"主菜单后面不能再改,二级菜单每次做之前再沟通一次"。是否意味着我每做完一个二级菜单就要等你确认后再做下一个? +**Ans1: 不需要** + +**G3.** 前端页面大部分已经有了 UI 和模拟逻辑。后续开发的重心是否主要在**后端 API 实现 + 前后端联调**,前端 UI 改动较少? +**Ans1: 前端UI的改动,现在我对新增合同这个菜单并不满意,建议修改为业务合同管理,然后前端UI做的和中尚鹏合同管理一样这些** + +--- + +## 五、Ans 修改完成备注 + +以下为根据你的回复已完成的修改及代码位置说明。 + +**Solve A1(两套合同并存 + 业务合同管理命名)** +- 完成:菜单「新增合同」改为「业务合同管理」;业务合同管理仅负责业务合同,中尚鹏合同管理负责公司相关合同。 +- 参考位置:`frontend/pure-admin-thin/src/router/modules/contract.ts` 中 `meta.title` 改为「业务合同管理」;`frontend/pure-admin-thin/src/views/system/contract/AddContract.vue` 页面标题与描述改为「业务合同管理」「管理业务合同信息、上传合同相关文件、录入合同数据」。 + +**Solve A2(Contract 数量/单价改为数值并避免浮点风险)** +- 完成:旧版 `Contract` 的 `Count`、`UnitPrice` 改为 `decimal(16,4)` 数值类型,后端解析表单字符串为 float64。 +- 参考位置:`backend/go-backend/internal/model/contract.go` 中 `Count`、`UnitPrice` 定义为 `float64` 且 `gorm:"type:decimal(16,4)"`;`backend/go-backend/internal/handler/contract_handler.go` 中 `strconv.ParseFloat` 解析并赋值。 + +**Solve A3、A4、A5** +- 当前保留:合同详情逐行录入形式(ZSPContractDetail);公式仅保留 `PendingQty = TotalQuantity - DeliveredQty`;搜索按手动填写的合同编号/公司名/商品字段查询。无新增代码改动,逻辑维持现状。 + +**Solve C1(货代费用模型重新设计)** +- 完成:`ZSPFreightCharges` 增加提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名等字段。 +- 参考位置:`backend/go-backend/internal/model/zsp_freight_charges.go` 中 `ZSPFreightCharges` 结构体;`internal/dto/zsp_finances.go` 中 Create/Update 请求体;`internal/service/zsp_finances_service.go`、`internal/repository/zsp_finances.go`、`internal/handler/zsp_finances_handler.go` 对应 CRUD;前端 `frontend/pure-admin-thin/src/api/zspFinances.ts` 类型与接口;`frontend/pure-admin-thin/src/views/system/finance/FreightCharges.vue` 表格与表单字段。 + +**Solve C2(成本构成重新设计)** +- 完成:`ZSPCostBreakdown` 改为按金额汇总设计,包含 `FreightChargeTotal`、`MiscChargeTotal`、`ServiceFeeTotal` 及 `TotalCost`,不再用外键关联单条费用记录。 +- 参考位置:`backend/go-backend/internal/model/zsp_freight_charges.go` 中 `ZSPCostBreakdown`;`internal/dto/zsp_finances.go` 中成本构成 DTO;service/repository 中成本构成的计算与更新逻辑;前端 `zspFinances.ts` 中 `ZSPCostBreakdown` 及相关 API。 + +**Solve C3** +- 理解已确认:收入来自合同金额,成本=货代+杂费+服务费,利润=收入−成本。当前后端利润记录与成本构成已按此逻辑实现,无需额外改动。 + +**Solve C4(结算/发票/对账单需实现)** +- 完成:后端新增结算、对账单及三张发票表模型并加入迁移;前端新增对应 API 文件。后端路由与完整 CRUD 待后续补全。 +- 参考位置:`backend/go-backend/internal/model/zsp_invoice.go`(上游/下游/货代杂费发票);`backend/go-backend/internal/model/zsp_settlement.go`(结算、对账单);`backend/go-backend/pkg/database/migrate.go` 中 AutoMigrate 新增上述模型;`frontend/pure-admin-thin/src/api/settlement.ts`、`invoice.ts`、`reconciliation.ts`。 + +**Solve C5(发票独立三张表)** +- 完成:已建 `ZSPUpstreamInvoice`、`ZSPDownstreamInvoice`、`ZSPFreightMiscInvoice` 三张独立表及对应模型与迁移。 +- 参考位置:`backend/go-backend/internal/model/zsp_invoice.go`。 + +**Solve 其他(杂费/服务费字段、API 基址、AutoMigrate)** +- 杂费:`ZSPMiscCharges` 增加提单号、合同编号、费用项目、付款日期、公司名等,见 `zsp_freight_charges.go`、DTO、`MiscCharges.vue`。 +- 服务费:`ZSPServiceFees` 增加提单号、合同编号,见 `zsp_freight_charges.go`、DTO、`ServiceFees.vue`。 +- API 基址统一:业务合同上传改用 `@/utils/http`,走 Vite 代理。参考 `frontend/pure-admin-thin/src/api/contract.ts`(`submitContract`、`batchUploadContracts` 使用 `http.request`)。 +- AutoMigrate 补全:`ZSPContractDetail`、所有财务相关模型、三张发票表、结算、对账单已加入。参考 `backend/go-backend/pkg/database/migrate.go`。 + +*以上为当前已完成的修改与对应代码位置;提货/库存/货权转移、结算与发票的后端路由及利润明细页与新成本结构的联调等,按开发计划后续迭代。* \ No newline at end of file diff --git a/docs/dev/项目开发要求文档.md b/docs/dev/项目开发要求文档.md new file mode 100755 index 0000000..ea9d05b --- /dev/null +++ b/docs/dev/项目开发要求文档.md @@ -0,0 +1,28 @@ +| 中尚鹏管理系统 | 主菜单 | 二级菜单 | 三级菜单 | 内容 | 需要实现的功能 | +| -------------- | ------------- | ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| | 合同管理 | 1.新增合同 | ①新增合同信息②上传合同相关文件 | 合同类型,合同编号,商品,数量,单价,卖方,买方,提单号<一个上游合同可关联的提单号最多15个>(体现在一个表格中);上传合同扫描附件(上传扫描件的文件夹,可以后续新建多个文件夹作为合同归类)。(非必选:提货时间·交货时间·包装(散装/吨袋)·备注) | 可批量上传合同+有一个录入合同内容中相关数据的表格(会涉及简单的公式,我们会给出) | +| | 合同管理 | 2.中尚鹏合同管理 | ①上游合同②下游合同③外商采购合同④货代物流协议⑤其它合同 | 上传合同扫描附件(上传扫描件的文件夹,可以后续新建多个文件夹作为合同归类);提供按照签订时间/合同编号/公司名 的搜索功能(我们会备注好文件名包含商品、公司名、合同编号,我们就通过三个要素搜索对应的合同) | | +| | 合同管理 | 3.公司信息管理 | 主要是公司相关文件存储,营业执照开票信息等 | 上传合同扫描附件(上传扫描件的文件夹,可以后续新建多个文件夹作为合同归类) | | +| | 提货管理 | 1.我要提货 | ①新增提货委托②变更提货委托③取消提货委托④ | 根据我们提供提货单模板创建在系统内,需要新增提货单时,我们手动录入可变信息内容(车牌号、提单号、司机信息、数量、商品品类) | | +| | 提货管理 | 2.提货明细 | ①新增出库单②查看出库明细③收货确认单 | 录入数据的表格(后期我们给模板) | | +| | 提货管理 | 3.库存信息 | ①新增入库单(入库单上传+录入表格)②库存明细(入库表格中数量-新增出库单中数量-货权转移数量=库存数量)③仓库管理(需要录入几个目前固定存放货物的仓库名,且后续可能会增加新的仓库名) | 批量上传扫描件的文件夹;录入扫描件内容数据的表格。表格内容:入库单:仓库,商品,提单号,提单重,入库重,货主,对应上游合同编号。可选项(仓库地址,包装) | | +| | 提货管理 | 4.货权转移 | ①新增货权转移函②上传货权转移函(已生效的)③查询/导出货转函 | 批量上传扫描件的文件夹;录入货权转移函数据的表格内容:货转日期,提单号,数量,商品,库方,货权出让方,货权接收方等 | | +| | 提货管理 | 5.其它 | | | | +| | 财务管理 | 1.结算管理 | ①新增结算单(导出下游合同结算单)②结算单根据单号查询③上传结算单(已交易且结算完毕的,主要为了备份) | 批量上传扫描件的文件夹;录入扫描件内容数据的表格 | | +| | 财务管理 | 2.发票管理 | ①上游发票(上传)②下游发票(上传)③货代杂费发票(上传) | 上传文件;对应表格录入发票对应信息 | | +| | 财务管理 | 3.利润明细 | ①2025年业务利润表 | 根据我们提供的excel表格创建一个表格(需要关联其他表格中数据+设置好栏目以及函数,后续自行手动录入) | | +| | 财务管理 | 4.货代费用+其他杂费 | ①新增货代费用/其它费用 | 货代费用/其它费用:提单号,上游合同编号,费用项目,金额,付款日期,货代/物流公司名 | | +| | 财务管理 | 5.对账单 | | | | +| | 查询+报表中心 | 1.查询上下游客户合同执行情况 | ①查询上下游客户合同执行(可按照合同编号/公司名/商品/提单号/日期 查询)②上传excel表格(银行流水) | 查询上下游客户货款及提货进度 | 可查询到该合同剩余未提数量(与二级菜单中新增合同表格关联)及差款及待开发票情况(与二级菜单中发票管理关联) | +| | 查询+报表中心 | 2.营业明细表 | ①2025年中尚鹏业务明细表 | | | +| | 查询+报表中心 | 3.其它明细表 | ①已开票明细表②货代杂费明细表③历史清关成本明细表 | | | +| | 查询+报表中心 | 4.商品基础资料 | 燕麦/大麦/豌豆/亚麻籽/葵粕/菜籽粕 | 可批量上传扫描件or文档即可 | | +| | 查询+报表中心 | 5.其它 | | | | +| | 采购管理 | 1.采购报单 | ①新增采购报单②变更采购报单 | 批量上传扫描件+录入扫描件中内容的表格 | | +| | 采购管理 | 2.采购运踪明细 | 采购进度明细表(相当于一个共享表格) | 根据我们所提供的excel在系统中创建一个表格 | | +| | 采购管理 | 3.国外供应商 | 国外供应商明细表 | 可批量上传扫描件or文档即可 | | +| | 采购管理 | 4.采购预算 | | 已有单独的预算测算系统,最后按照该预算系统功能等添加到我们系统中(这个最后有时间慢慢弄,不着急) | | +| | 权限管理 | 1 | | | | +| | 权限管理 | 2 | | | | +| | | | | | | + diff --git a/docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md b/docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md new file mode 100644 index 0000000..d0e4f2f --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md @@ -0,0 +1,1314 @@ +# 前端 API 测试基础设施 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 Vue3+TypeScript 前端建立 Vitest 测试框架,Mock Axios 测试 11 个 API 模块 + +**Architecture:** Vitest + vi.mock 模式,测试 API 层请求/响应类型验证,不依赖真实后端 + +**Tech Stack:** Vitest, @vitest/coverage-v8, jsdom, TypeScript + +--- + +## 文件结构 + +| 文件 | 责任 | +|------|------| +| `vitest.config.ts` | Vitest 配置,环境、覆盖率、别名设置 | +| `package.json` | 新增 vitest 依赖和测试脚本 | +| `src/api/test-utils.ts` | Mock 工具函数封装 | +| `src/api/__tests__/contract.spec.ts` | 合同 API 测试 (15-20 cases) | +| `src/api/__tests__/zspContract.spec.ts` | 中尚鹏合同测试 | +| `src/api/__tests__/zspFinances.spec.ts` | 财务模块测试 | +| `src/api/__tests__/applyDelivery.spec.ts` | 提货申请测试 | +| `src/api/__tests__/deliveryDetails.spec.ts` | 出库明细测试 | +| `src/api/__tests__/inventory.spec.ts` | 库存管理测试 | +| `src/api/__tests__/ownershipTransfer.spec.ts` | 货权转移测试 | +| `src/api/__tests__/company.spec.ts` | 公司信息测试 | +| `src/api/__tests__/user.spec.ts` | 用户认证测试 | +| `src/api/__tests__/invoice.spec.ts` | 发票测试 | +| `src/api/__tests__/settlement.spec.ts` | 结算测试 | +| `src/api/__tests__/reconciliation.spec.ts` | 对账单测试 | + +--- + +### Task 1: 基础设施配置 + +**Files:** +- Create: `frontend/vitest.config.ts` +- Modify: `frontend/package.json` + +- [ ] **Step 1: 安装 Vitest 依赖** + +Run in `frontend/`: +```bash +pnpm add -D vitest @vitest/coverage-v8 jsdom @vitest/ui +``` + +Expected: 依赖安装成功 + +- [ ] **Step 2: 创建 vitest.config.ts** + +```typescript +// frontend/vitest.config.ts +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'jsdom', + include: ['src/api/__tests__/**/*.spec.ts'], + globals: true, + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'json'], + include: ['src/api/**/*.ts'], + exclude: ['src/api/__tests__/**', 'src/api/test-utils.ts', 'src/api/base.ts', 'src/api/utils.ts'] + } + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + } +}) +``` + +- [ ] **Step 3: 更新 package.json 添加测试脚本** + +在 `frontend/package.json` 的 `scripts` 中添加: +```json +{ + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:ui": "vitest --ui" + } +} +``` + +- [ ] **Step 4: 验证 Vitest 配置** + +Run: +```bash +cd frontend && pnpm test +``` + +Expected: Vitest 运行,报告 "No test files found" (正常,尚未创建测试) + +- [ ] **Step 5: 创建测试目录结构** + +```bash +mkdir -p frontend/src/api/__tests__ +``` + +--- + +### Task 2: Mock 工具函数 + +**Files:** +- Create: `frontend/src/api/test-utils.ts` + +- [ ] **Step 1: 创建 test-utils.ts** + +```typescript +// frontend/src/api/test-utils.ts +import { vi } from 'vitest' +import type { BaseResponse } from './base' + +/** + * Mock http.request 成功响应 + */ +export function mockHttpSuccess(data: T): BaseResponse { + return { + success: true, + data, + message: 'ok' + } +} + +/** + * Mock http.request 错误响应 + */ +export function mockHttpError(message: string, code = 500): BaseResponse { + return { + success: false, + data: null, + message + } +} + +/** + * 创建 http.request 的 spy mock + * 注意:需要在每个测试文件中 import http 后使用 + */ +export function createHttpMock() { + return { + spyOnRequest: (response: any) => { + const http = require('@/utils/http') + return vi.spyOn(http.http, 'request').mockResolvedValue(response) + }, + spyOnError: (error: any) => { + const http = require('@/utils/http') + return vi.spyOn(http.http, 'request').mockRejectedValue(error) + } + } +} +``` + +- [ ] **Step 2: 验证 test-utils 导入** + +创建临时测试验证导入: +```typescript +// frontend/src/api/__tests__/test-utils.spec.ts +import { describe, it, expect } from 'vitest' +import { mockHttpSuccess, mockHttpError } from '../test-utils' + +describe('test-utils', () => { + it('mockHttpSuccess 应返回正确结构', () => { + const result = mockHttpSuccess({ id: 1 }) + expect(result.success).toBe(true) + expect(result.data).toEqual({ id: 1 }) + }) + + it('mockHttpError 应返回错误结构', () => { + const result = mockHttpError('错误', 500) + expect(result.success).toBe(false) + expect(result.message).toBe('错误') + }) +}) +``` + +Run: +```bash +cd frontend && pnpm test +``` + +Expected: 2 tests pass + +- [ ] **Step 3: 删除临时测试,提交基础设施** + +```bash +rm frontend/src/api/__tests__/test-utils.spec.ts +cd frontend && git add vitest.config.ts package.json pnpm-lock.yaml src/api/test-utils.ts +git commit -m "feat(frontend): add vitest test infrastructure" +``` + +--- + +### Task 3: Contract API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/contract.spec.ts` + +- [ ] **Step 1: 创建 contract.spec.ts 基础结构** + +```typescript +// frontend/src/api/__tests__/contract.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' +import type { BaseResponse } from '../base' +import { + getContractList, + getContract, + submitContract, + batchUploadContracts, + updateContract, + deleteContract, + getContractFolders, + createContractFolder, + deleteContractFolder, + getContractBatches, + createContractBatch, + deleteContractBatch +} from '../contract' + +// Mock http.request +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +describe('Contract API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getContractList', () => { + it('应返回合同列表', async () => { + const mockData = [ + { id: 1, name: '合同1', contractCode: 'C001' }, + { id: 2, name: '合同2', contractCode: 'C002' } + ] + vi.mocked(http.request).mockResolvedValue({ + success: true, + data: mockData + }) + + const result = await getContractList() + expect(http.request).toHaveBeenCalledWith('get', '/api/contract', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toHaveLength(2) + }) + + it('带 contractType 参数应正确传递', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + + await getContractList({ contractType: 'business' }) + expect(http.request).toHaveBeenCalledWith('get', '/api/contract', { + params: { contractType: 'business' } + }) + }) + }) + + describe('getContract', () => { + it('应返回单个合同详情', async () => { + const mockData = { id: 1, name: '合同详情', contractCode: 'C001' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await getContract(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/1') + expect(result.data.id).toBe(1) + }) + }) + + describe('submitContract', () => { + it('应提交 FormData 格式数据', async () => { + const formData = new FormData() + formData.append('name', '新合同') + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'ok' }) + + await submitContract(formData) + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + }) + }) + + describe('batchUploadContracts', () => { + it('应批量上传合同', async () => { + const formData = new FormData() + vi.mocked(http.request).mockResolvedValue({ + success: true, + data: { successCount: 5, failedItems: [] } + }) + + const result = await batchUploadContracts(formData) + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/batch-upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.data.successCount).toBe(5) + }) + }) + + describe('updateContract', () => { + it('应 PUT 更新合同', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + + await updateContract(1, { name: '更新名称' }) + expect(http.request).toHaveBeenCalledWith('put', '/api/contract/1', { + data: { name: '更新名称' } + }) + }) + }) + + describe('deleteContract', () => { + it('应 DELETE 删除合同', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + + await deleteContract(1) + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/1') + }) + }) + + describe('Folder operations', () => { + it('getContractFolders 应返回文件夹列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await getContractFolders() + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/folders') + }) + + it('createContractFolder 应 POST 创建文件夹', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } }) + await createContractFolder({ name: '新文件夹' }) + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/folders', { data: { name: '新文件夹' } }) + }) + + it('deleteContractFolder 应 DELETE 文件夹', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await deleteContractFolder(1) + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/folders/1') + }) + }) + + describe('Batch operations', () => { + it('getContractBatches 应返回批次列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await getContractBatches() + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches', { params: undefined }) + }) + + it('getContractBatches 带 folderId 参数', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await getContractBatches({ folderId: 1 }) + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches', { params: { folderId: 1 } }) + }) + }) +}) +``` + +- [ ] **Step 2: 运行测试验证** + +Run: +```bash +cd frontend && pnpm test src/api/__tests__/contract.spec.ts +``` + +Expected: 所有测试通过 + +- [ ] **Step 3: 提交** + +```bash +cd frontend && git add src/api/__tests__/contract.spec.ts +git commit -m "test(frontend): add contract API tests" +``` + +--- + +### Task 4: User API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/user.spec.ts` + +- [ ] **Step 1: 读取 user.ts 了解 API 结构** + +```typescript +// frontend/src/api/user.ts (参考) +export const login = (data: { username: string; password: string }) => { + return http.request("post", "/login", { data }); +}; +export const register = (data: RegisterData) => { + return http.request("post", "/auth/register", { data }); +}; +``` + +- [ ] **Step 2: 创建 user.spec.ts** + +```typescript +// frontend/src/api/__tests__/user.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +// 动态导入以应用 mock +const { login, register, getUser, refreshToken } = await import('../user') + +describe('User API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('login', () => { + it('应 POST 登录请求', async () => { + vi.mocked(http.request).mockResolvedValue({ + success: true, + data: { accessToken: 'token123', refreshToken: 'refresh123' } + }) + + const result = await login({ username: 'test', password: '123456' }) + expect(http.request).toHaveBeenCalledWith('post', '/login', { + data: { username: 'test', password: '123456' } + }) + expect(result.success).toBe(true) + }) + }) + + describe('register', () => { + it('应 POST 注册请求', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + + await register({ username: 'newuser', email: 'test@example.com', password: '123456' }) + expect(http.request).toHaveBeenCalledWith('post', '/auth/register', { + data: { username: 'newuser', email: 'test@example.com', password: '123456' } + }) + }) + }) + + describe('getUser', () => { + it('应 GET 用户信息', async () => { + vi.mocked(http.request).mockResolvedValue({ + success: true, + data: { id: 1, username: 'test', email: 'test@example.com' } + }) + + const result = await getUser(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/users/1') + expect(result.data.username).toBe('test') + }) + }) + + describe('refreshToken', () => { + it('应 POST 刷新 token', async () => { + vi.mocked(http.request).mockResolvedValue({ + success: true, + data: { accessToken: 'newtoken', refreshToken: 'newrefresh' } + }) + + await refreshToken({ refreshToken: 'oldrefresh' }) + expect(http.request).toHaveBeenCalledWith('post', '/refresh-token', { + data: { refreshToken: 'oldrefresh' } + }) + }) + }) +}) +``` + +- [ ] **Step 3: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/user.spec.ts +``` + +- [ ] **Step 4: 提交** + +```bash +git add src/api/__tests__/user.spec.ts +git commit -m "test(frontend): add user API tests" +``` + +--- + +### Task 5: ZSP Contract API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/zspContract.spec.ts` + +- [ ] **Step 1: 创建 zspContract.spec.ts** + +```typescript +// frontend/src/api/__tests__/zspContract.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../zspContract') + +describe('ZSP Contract API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Folder operations', () => { + it('getFolders 应返回文件夹列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.getFolders() + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/folders') + }) + + it('createFolder 应创建文件夹', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createFolder({ name: '新文件夹', parentId: null }) + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/folders', { + data: { name: '新文件夹', parentId: null } + }) + }) + }) + + describe('Contract operations', () => { + it('getContracts 应返回合同列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.getContracts() + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts') + }) + + it('getContractDetail 应返回合同详情', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } }) + await api.getContractDetail(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1') + }) + + it('uploadContract 应上传合同文件', async () => { + const formData = new FormData() + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.uploadContract(formData) + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + }) + }) + + describe('Detail operations', () => { + it('getContractDetails 应返回明细列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.getContractDetails(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1/details') + }) + + it('importDetailsFromExcel 应导入 Excel', async () => { + const formData = new FormData() + vi.mocked(http.request).mockResolvedValue({ success: true, data: { count: 10 } }) + await api.importDetailsFromExcel(1, formData) + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts/1/details/import', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + }) + + it('exportDetailsToExcel 应导出 Excel', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.exportDetailsToExcel(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1/details/export') + }) + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/zspContract.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/zspContract.spec.ts +git commit -m "test(frontend): add zspContract API tests" +``` + +--- + +### Task 6: ZSP Finances API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/zspFinances.spec.ts` + +- [ ] **Step 1: 创建 zspFinances.spec.ts** + +```typescript +// frontend/src/api/__tests__/zspFinances.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../zspFinances') + +describe('ZSP Finances API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // 货代费用 + describe('Freight Charges', () => { + it('listFreightCharges 应返回列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listFreightCharges() + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/freight-charges') + }) + + it('createFreightCharge 应创建记录', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createFreightCharge({ blNumber: 'BL001', amount: 1000 }) + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-finances/freight-charges', { + data: { blNumber: 'BL001', amount: 1000 } + }) + }) + }) + + // 杂费 + describe('Misc Charges', () => { + it('listMiscCharges 应返回列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listMiscCharges() + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/misc-charges') + }) + }) + + // 服务费 + describe('Service Fees', () => { + it('listServiceFees 应返回列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listServiceFees() + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/service-fees') + }) + }) + + // 成本构成 + describe('Cost Breakdowns', () => { + it('listCostBreakdowns 应返回列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listCostBreakdowns() + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/cost-breakdowns') + }) + }) + + // 利润记录 + describe('Profit Records', () => { + it('listProfitRecords 应返回列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listProfitRecords() + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/profit-records') + }) + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/zspFinances.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/zspFinances.spec.ts +git commit -m "test(frontend): add zspFinances API tests" +``` + +--- + +### Task 7: Apply Delivery API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/applyDelivery.spec.ts` + +- [ ] **Step 1: 创建 applyDelivery.spec.ts** + +```typescript +// frontend/src/api/__tests__/applyDelivery.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../applyDelivery') + +describe('Apply Delivery API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('listApplyDelivery 应返回提货申请列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } }) + await api.listApplyDelivery() + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/list') + }) + + it('getApplyDelivery 应返回详情', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } }) + await api.getApplyDelivery(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/detail/1') + }) + + it('createApplyDelivery 应创建申请', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } }) + await api.createApplyDelivery({ blNumber: 'BL001' }) + expect(http.request).toHaveBeenCalledWith('post', '/api/apply-delivery/create', { + data: { blNumber: 'BL001' } + }) + }) + + it('updateApplyDelivery 应更新申请', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.updateApplyDelivery(1, { status: 'approved' }) + expect(http.request).toHaveBeenCalledWith('put', '/api/apply-delivery/update/1', { + data: { status: 'approved' } + }) + }) + + it('cancelApplyDelivery 应取消申请', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.cancelApplyDelivery(1) + expect(http.request).toHaveBeenCalledWith('put', '/api/apply-delivery/cancel/1') + }) + + it('deleteApplyDelivery 应删除申请', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.deleteApplyDelivery(1) + expect(http.request).toHaveBeenCalledWith('delete', '/api/apply-delivery/1') + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/applyDelivery.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/applyDelivery.spec.ts +git commit -m "test(frontend): add applyDelivery API tests" +``` + +--- + +### Task 8: Delivery Details API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/deliveryDetails.spec.ts` + +- [ ] **Step 1: 创建 deliveryDetails.spec.ts** + +```typescript +// frontend/src/api/__tests__/deliveryDetails.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../deliveryDetails') + +describe('Delivery Details API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('listDeliveryDetails 应返回出库明细列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } }) + await api.listDeliveryDetails() + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/list') + }) + + it('getDeliveryDetail 应返回详情', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } }) + await api.getDeliveryDetail(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/detail/1') + }) + + it('createDeliveryDetail 应创建出库单', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createDeliveryDetail({ quantity: 100 }) + expect(http.request).toHaveBeenCalledWith('post', '/api/delivery-details/create', { + data: { quantity: 100 } + }) + }) + + it('updateDeliveryDetail 应更新出库单', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.updateDeliveryDetail(1, { quantity: 200 }) + expect(http.request).toHaveBeenCalledWith('put', '/api/delivery-details/update/1', { + data: { quantity: 200 } + }) + }) + + it('deleteDeliveryDetail 应删除出库单', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.deleteDeliveryDetail(1) + expect(http.request).toHaveBeenCalledWith('delete', '/api/delivery-details/1') + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/deliveryDetails.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/deliveryDetails.spec.ts +git commit -m "test(frontend): add deliveryDetails API tests" +``` + +--- + +### Task 9: Inventory API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/inventory.spec.ts` + +- [ ] **Step 1: 创建 inventory.spec.ts** + +```typescript +// frontend/src/api/__tests__/inventory.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../inventory') + +describe('Inventory API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Warehouse', () => { + it('listWarehouses 应返回仓库列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listWarehouses() + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/warehouses') + }) + + it('createWarehouse 应创建仓库', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createWarehouse({ name: '新仓库' }) + expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/warehouses/create', { + data: { name: '新仓库' } + }) + }) + }) + + describe('Inbound', () => { + it('listInbound 应返回入库列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } }) + await api.listInbound() + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/inbound/list') + }) + + it('createInbound 应创建入库记录', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createInbound({ warehouse: '仓库A', commodity: '大豆' }) + expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/inbound/create', { + data: { warehouse: '仓库A', commodity: '大豆' } + }) + }) + }) + + describe('Summary', () => { + it('inventorySummary 应返回库存汇总', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.inventorySummary() + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/summary') + }) + + it('inventorySummary 带筛选参数', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.inventorySummary({ warehouse: '仓库A' }) + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/summary', { + params: { warehouse: '仓库A' } + }) + }) + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/inventory.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/inventory.spec.ts +git commit -m "test(frontend): add inventory API tests" +``` + +--- + +### Task 10: Ownership Transfer API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/ownershipTransfer.spec.ts` + +- [ ] **Step 1: 创建 ownershipTransfer.spec.ts** + +```typescript +// frontend/src/api/__tests__/ownershipTransfer.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../ownershipTransfer') + +describe('Ownership Transfer API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('listOwnershipTransfers 应返回列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } }) + await api.listOwnershipTransfers() + expect(http.request).toHaveBeenCalledWith('get', '/api/ownership-transfer/list') + }) + + it('getOwnershipTransfer 应返回详情', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } }) + await api.getOwnershipTransfer(1) + expect(http.request).toHaveBeenCalledWith('get', '/api/ownership-transfer/detail/1') + }) + + it('createOwnershipTransfer 应创建货权转移', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createOwnershipTransfer({ transferor: '公司A', transferee: '公司B' }) + expect(http.request).toHaveBeenCalledWith('post', '/api/ownership-transfer/create', { + data: { transferor: '公司A', transferee: '公司B' } + }) + }) + + it('createOwnershipTransferWithFiles 应带文件创建', async () => { + const formData = new FormData() + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createOwnershipTransferWithFiles(formData) + expect(http.request).toHaveBeenCalledWith('post', '/api/ownership-transfer/create-with-files', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + }) + + it('deleteOwnershipTransfer 应删除', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.deleteOwnershipTransfer(1) + expect(http.request).toHaveBeenCalledWith('delete', '/api/ownership-transfer/1') + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/ownershipTransfer.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/ownershipTransfer.spec.ts +git commit -m "test(frontend): add ownershipTransfer API tests" +``` + +--- + +### Task 11: Company API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/company.spec.ts` + +- [ ] **Step 1: 创建 company.spec.ts** + +```typescript +// frontend/src/api/__tests__/company.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../company') + +describe('Company API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Folders', () => { + it('listFolders 应返回文件夹列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listFolders() + expect(http.request).toHaveBeenCalledWith('get', '/api/company/folders') + }) + + it('createFolder 应创建文件夹', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createFolder({ name: '新文件夹' }) + expect(http.request).toHaveBeenCalledWith('post', '/api/company/folders', { + data: { name: '新文件夹' } + }) + }) + }) + + describe('Files', () => { + it('listFiles 应返回文件列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listFiles() + expect(http.request).toHaveBeenCalledWith('get', '/api/company/files') + }) + + it('uploadFiles 应上传文件', async () => { + const formData = new FormData() + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.uploadFiles(formData) + expect(http.request).toHaveBeenCalledWith('post', '/api/company/files/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + }) + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/company.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/company.spec.ts +git commit -m "test(frontend): add company API tests" +``` + +--- + +### Task 12: Invoice API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/invoice.spec.ts` + +- [ ] **Step 1: 创建 invoice.spec.ts** + +```typescript +// frontend/src/api/__tests__/invoice.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../invoice') + +describe('Invoice API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('listInvoices 应返回发票列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listInvoices() + expect(http.request).toHaveBeenCalled() + }) + + it('createInvoice 应创建发票', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createInvoice({ invoiceNumber: 'INV001', amount: 10000 }) + expect(http.request).toHaveBeenCalled() + }) + + it('updateInvoice 应更新发票', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.updateInvoice(1, { amount: 20000 }) + expect(http.request).toHaveBeenCalled() + }) + + it('deleteInvoice 应删除发票', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.deleteInvoice(1) + expect(http.request).toHaveBeenCalled() + }) +}) +``` + +- [ ] **Step 2: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/invoice.spec.ts +``` + +- [ ] **Step 3: 提交** + +```bash +git add src/api/__tests__/invoice.spec.ts +git commit -m "test(frontend): add invoice API tests" +``` + +--- + +### Task 13: Settlement & Reconciliation API 测试 + +**Files:** +- Create: `frontend/src/api/__tests__/settlement.spec.ts` +- Create: `frontend/src/api/__tests__/reconciliation.spec.ts` + +- [ ] **Step 1: 创建 settlement.spec.ts** + +```typescript +// frontend/src/api/__tests__/settlement.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../settlement') + +describe('Settlement API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('listSettlements 应返回结算单列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listSettlements() + expect(http.request).toHaveBeenCalled() + }) + + it('createSettlement 应创建结算单', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createSettlement({ contractCode: 'C001' }) + expect(http.request).toHaveBeenCalled() + }) +}) +``` + +- [ ] **Step 2: 创建 reconciliation.spec.ts** + +```typescript +// frontend/src/api/__tests__/reconciliation.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../reconciliation') + +describe('Reconciliation API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('listReconciliations 应返回对账单列表', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, data: [] }) + await api.listReconciliations() + expect(http.request).toHaveBeenCalled() + }) + + it('createReconciliation 应创建对账单', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true }) + await api.createReconciliation({ companyName: '公司A' }) + expect(http.request).toHaveBeenCalled() + }) +}) +``` + +- [ ] **Step 3: 运行测试** + +```bash +cd frontend && pnpm test src/api/__tests__/settlement.spec.ts src/api/__tests__/reconciliation.spec.ts +``` + +- [ ] **Step 4: 提交** + +```bash +git add src/api/__tests__/settlement.spec.ts src/api/__tests__/reconciliation.spec.ts +git commit -m "test(frontend): add settlement and reconciliation API tests" +``` + +--- + +### Task 14: 运行完整测试套件并验证覆盖率 + +**Files:** +- Run tests + +- [ ] **Step 1: 运行所有 API 测试** + +```bash +cd frontend && pnpm test +``` + +Expected: 所有测试通过 + +- [ ] **Step 2: 运行覆盖率报告** + +```bash +cd frontend && pnpm test:coverage +``` + +Expected: 覆盖率 > 80% + +- [ ] **Step 3: 查看覆盖率 HTML 报告** + +```bash +open frontend/coverage/index.html +``` + +- [ ] **Step 4: 最终提交** + +```bash +git add -A +git commit -m "feat(frontend): complete API test infrastructure with vitest" +``` + +--- + +## Self-Review + +### Spec Coverage Check +| Spec Requirement | Task | +|------------------|------| +| Vitest 配置 | Task 1 | +| package.json 脚本 | Task 1 | +| Mock 工具函数 | Task 2 | +| 11 API 模块测试 | Tasks 3-13 | +| 覆盖率报告 | Task 14 | + +### Placeholder Scan +- 无 TBD/TODO 占位符 +- 所有代码步骤包含完整实现 +- 所有命令包含预期输出 + +### Type Consistency +- `http.request` mock 在所有测试文件中一致使用 `vi.mock('@/utils/http')` +- 响应结构统一为 `{ success: boolean, data?: any, message?: string }` + +--- + +Plan complete and saved to `docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md`. Two execution options: + +**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration + +**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints + +**Which approach?** \ No newline at end of file diff --git a/docs/superpowers/specs/2026-04-25-frontend-api-test-infra-design.md b/docs/superpowers/specs/2026-04-25-frontend-api-test-infra-design.md new file mode 100644 index 0000000..3e724c8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-25-frontend-api-test-infra-design.md @@ -0,0 +1,274 @@ +--- +name: 前端 API 测试基础设施 +description: 为 Vue3+TypeScript 前端建立 Vitest 测试框架,Mock Axios 测试 11 个 API 模块 +created: 2026-04-25 +--- + +# 前端 API 测试基础设施设计文档 + +## 一、背景与目标 + +### 当前状态 + +- 前端基于 Vue 3 + TypeScript + Vite + Axios +- 11 个 API 模块 (`src/api/*.ts`) 封装 HTTP 请求 +- **测试完全缺失**:无 vitest 配置,无 .spec/.test 文件 + +### 目标 + +建立前端 API 层测试基础设施,确保: +1. API 请求参数/响应类型正确 +2. 错误处理逻辑覆盖 +3. 前后端契约一致性验证 +4. CI/CD 集成准备 + +## 二、技术选型 + +| 项目 | 选择 | 原因 | +|------|------|------| +| 测试框架 | Vitest | Vite 生态原生支持,配置简单,ESM 原生 | +| Mock 工具 | vi.mock (Vitest 内置) | 无额外依赖,Axios mock 简洁 | +| 断言库 | Vitest expect | 内置,支持 TypeScript 类型推断 | +| 覆盖率 | @vitest/coverage-v8 | Istanbul 覆盖率报告 | + +## 三、架构设计 + +### 目录结构 + +``` +frontend/ +├── vitest.config.ts # Vitest 配置文件 +├── src/api/ +│ ├── __tests__/ # API 测试目录 +│ │ ├── contract.spec.ts # 合同 API 测试 +│ │ ├── zspContract.spec.ts # 中尚鹏合同 +│ │ ├── zspFinances.spec.ts # 财务模块 +│ │ ├── applyDelivery.spec.ts # 提货申请 +│ │ ├── deliveryDetails.spec.ts +│ │ ├── inventory.spec.ts +│ │ ├── ownershipTransfer.spec.ts +│ │ ├── company.spec.ts +│ │ ├── user.spec.ts +│ │ ├── invoice.spec.ts +│ │ ├── settlement.spec.ts +│ │ └── reconciliation.spec.ts +│ └── test-utils.ts # Mock 工具函数 +└── package.json # 新增 vitest 相关依赖 +``` + +### Vitest 配置 + +```typescript +// vitest.config.ts +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'jsdom', + include: ['src/api/__tests__/**/*.spec.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + include: ['src/api/**/*.ts'], + exclude: ['src/api/__tests__/**', 'src/api/test-utils.ts'] + } + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + } +}) +``` + +### Mock 工具函数 + +```typescript +// src/api/test-utils.ts +import { vi } from 'vitest' +import { http } from '@/utils/http' + +/** + * Mock http.request 方法 + * @param response 模拟响应数据 + * @param status HTTP 状态码 (默认 200) + */ +export function mockHttpRequest(response: any, status = 200) { + vi.spyOn(http, 'request').mockResolvedValue({ + success: true, + data: response, + status + }) +} + +/** + * Mock HTTP 错误响应 + * @param message 错误信息 + * @param status HTTP 状态码 + */ +export function mockHttpError(message: string, status: number) { + vi.spyOn(http, 'request').mockRejectedValue({ + success: false, + message, + status + }) +} + +/** + * 清除所有 mock + */ +export function clearMocks() { + vi.restoreAllMocks() +} +``` + +## 四、测试模式 + +每个 API 模块测试遵循统一模式: + +### 4.1 请求成功测试 + +```typescript +describe('getContractList', () => { + it('应返回合同列表', async () => { + const mockData = [{ id: 1, name: '合同1' }] + mockHttpRequest(mockData) + + const result = await getContractList() + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) +}) +``` + +### 4.2 参数验证测试 + +```typescript +it('带 contractType 参数应正确传递', async () => { + mockHttpRequest([]) + + await getContractList({ contractType: 'business' }) + expect(http.request).toHaveBeenCalledWith('get', '/api/contract', { + params: { contractType: 'business' } + }) +}) +``` + +### 4.3 错误处理测试 + +```typescript +describe('错误处理', () => { + it('401 未授权应抛出错误', async () => { + mockHttpError('Unauthorized', 401) + + await expect(getContractList()).rejects.toMatchObject({ + status: 401 + }) + }) + + it('500 服务器错误应抛出错误', async () => { + mockHttpError('Internal Server Error', 500) + + await expect(getContractList()).rejects.toMatchObject({ + status: 500 + }) + }) +}) +``` + +### 4.4 类型验证 + +```typescript +it('响应应符合 BusinessContract 类型', async () => { + const mockData: BusinessContract[] = [ + { + id: 1, + name: '测试合同', + contractType: 'business', + contractCode: 'C001', + // ... 其他必填字段 + } + ] + mockHttpRequest(mockData) + + const result = await getContractList() + // TypeScript 编译时类型检查,运行时验证结构 + expect(result.data[0]).toHaveProperty('id') + expect(result.data[0]).toHaveProperty('name') +}) +``` + +## 五、API 模块测试清单 + +| 模块 | 文件 | 主要 API 函数 | 预估测试数 | +|------|------|---------------|------------| +| Contract | `contract.spec.ts` | getContractList, getContract, submitContract, batchUploadContracts, updateContract, deleteContract + folders/batches | 15-20 | +| ZSP Contract | `zspContract.spec.ts` | getFolders, getContracts, uploadContract, createDetail, importExcel, exportExcel | 10-15 | +| ZSP Finances | `zspFinances.spec.ts` | CRUD for freight/misc/service/cost/profit | 12-15 | +| Apply Delivery | `applyDelivery.spec.ts` | list, get, create, update, cancel, delete | 10 | +| Delivery Details | `deliveryDetails.spec.ts` | list, get, create, update, delete | 8 | +| Inventory | `inventory.spec.ts` | warehouses, inbound, summary | 8 | +| Ownership Transfer | `ownershipTransfer.spec.ts` | list, get, create, createWithFiles, delete | 8 | +| Company | `company.spec.ts` | folders, files CRUD | 8 | +| User | `user.spec.ts` | login, register, getUser, refreshToken | 8 | +| Invoice | `invoice.spec.ts` | 上游/下游/货代发票 CRUD | 10 | +| Settlement | `settlement.spec.ts` | CRUD | 6 | +| Reconciliation | `reconciliation.spec.ts` | 对账单 CRUD | 6 | + +**总计预估:90-110 个测试用例** + +## 六、执行命令 + +```bash +# 安装依赖 +pnpm add -D vitest @vitest/coverage-v8 jsdom + +# 运行测试 +pnpm test + +# 运行覆盖率 +pnpm test:coverage + +# 监听模式 +pnpm test:watch +``` + +### package.json 新增脚本 + +```json +{ + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + } +} +``` + +## 七、CI/CD 集成预留 + +后续可添加 GitHub Actions 或其他 CI 配置: + +```yaml +# .github/workflows/test.yml (预留) +- name: Run frontend tests + run: cd frontend && pnpm test:coverage +``` + +## 八、验收标准 + +1. 所有 11 个 API 模块有对应 `.spec.ts` 文件 +2. 测试覆盖率 > 80%(API 模块代码行覆盖) +3. 所有测试通过,无 TypeScript 编译错误 +4. `pnpm test` 命令可用 +5. 覆盖率报告生成 (`coverage/index.html`) + +## 九、后续扩展方向 + +- 组件测试 (`src/components/`) +- 页面集成测试 (`src/views/`) +- E2E 测试 (Playwright) +- 真实后端集成测试层 \ No newline at end of file diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100755 index 0000000..bb4e051 --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1,4 @@ +> 1% +last 2 versions +not dead +not ie 11 \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100755 index 0000000..e16e074 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,55 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +node_modules +.DS_Store +dist +dist-ssr +*.local +.eslintcache +report.html + +yarn.lock +npm-debug.log* +.pnpm-error.log* +.pnpm-debug.log +tests/**/coverage/ + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +tsconfig.tsbuildinfo diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100755 index 0000000..9a0e57f --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/frontend/.env b/frontend/.env new file mode 100755 index 0000000..e9c8c06 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,5 @@ +# 平台本地运行端口号 +VITE_PORT = 12345 + +# 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置) +VITE_HIDE_HOME = true diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100755 index 0000000..6edc6a4 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,8 @@ +# 平台本地运行端口号(若 12345 无权限可改用 5173) +VITE_PORT = 5173 + +# 开发环境读取配置文件路径 +VITE_PUBLIC_PATH = / + +# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") +VITE_ROUTER_HISTORY = "hash" diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100755 index 0000000..6fd16f0 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,17 @@ +# 线上环境平台打包路径 +VITE_PUBLIC_PATH = / + +# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") +VITE_ROUTER_HISTORY = "hash" + +# 是否在打包时使用cdn替换本地库 替换 true 不替换 false +VITE_CDN = false + +# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) +# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) +# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) +VITE_COMPRESSION = "none" + +# 生产环境 API 地址(可选)。不填则默认请求「当前访问的域名:8080」,前后端同机时无需 Nginx 代理 /api +# 示例:VITE_API_BASE_URL = http://129.204.148.31:8080 +# VITE_API_BASE_URL = \ No newline at end of file diff --git a/frontend/.env.staging b/frontend/.env.staging new file mode 100755 index 0000000..56b237c --- /dev/null +++ b/frontend/.env.staging @@ -0,0 +1,16 @@ +# 预发布也需要生产环境的行为 +# https://cn.vitejs.dev/guide/env-and-mode.html#modes +# NODE_ENV = development + +VITE_PUBLIC_PATH = / + +# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") +VITE_ROUTER_HISTORY = "hash" + +# 是否在打包时使用cdn替换本地库 替换 true 不替换 false +VITE_CDN = true + +# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) +# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) +# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) +VITE_COMPRESSION = "none" diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100755 index 0000000..7d23fc0 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,22 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.eslintcache +report.html +vite.config.*.timestamp* + +yarn.lock +npm-debug.log* +.pnpm-error.log* +.pnpm-debug.log +tests/**/coverage/ + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/frontend/.husky/commit-msg b/frontend/.husky/commit-msg new file mode 100755 index 0000000..f134830 --- /dev/null +++ b/frontend/.husky/commit-msg @@ -0,0 +1,8 @@ +#!/bin/sh + +# shellcheck source=./_/husky.sh +. "$(dirname "$0")/_/husky.sh" + +PATH="/usr/local/bin:$PATH" + +npx --no-install commitlint --edit "$1" \ No newline at end of file diff --git a/frontend/.husky/common.sh b/frontend/.husky/common.sh new file mode 100755 index 0000000..350839d --- /dev/null +++ b/frontend/.husky/common.sh @@ -0,0 +1,9 @@ +#!/bin/sh +command_exists () { + command -v "$1" >/dev/null 2>&1 +} + +# Workaround for Windows 10, Git Bash and Pnpm +if command_exists winpty && test -t 1; then + exec < /dev/tty +fi diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit new file mode 100755 index 0000000..61b0397 --- /dev/null +++ b/frontend/.husky/pre-commit @@ -0,0 +1,10 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" +. "$(dirname "$0")/common.sh" + +[ -n "$CI" ] && exit 0 + +PATH="/usr/local/bin:$PATH" + +# Perform lint check on files in the staging area through .lintstagedrc configuration +pnpm exec lint-staged \ No newline at end of file diff --git a/frontend/.lintstagedrc b/frontend/.lintstagedrc new file mode 100755 index 0000000..60ce5a1 --- /dev/null +++ b/frontend/.lintstagedrc @@ -0,0 +1,20 @@ +{ + "*.{js,jsx,ts,tsx}": [ + "prettier --cache --ignore-unknown --write", + "eslint --cache --fix" + ], + "{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [ + "prettier --cache --write--parser json" + ], + "package.json": ["prettier --cache --write"], + "*.vue": [ + "prettier --write", + "eslint --cache --fix", + "stylelint --fix --allow-empty-input" + ], + "*.{css,scss,html}": [ + "prettier --cache --ignore-unknown --write", + "stylelint --fix --allow-empty-input" + ], + "*.md": ["prettier --cache --ignore-unknown --write"] +} diff --git a/frontend/.markdownlint.json b/frontend/.markdownlint.json new file mode 100755 index 0000000..e985e09 --- /dev/null +++ b/frontend/.markdownlint.json @@ -0,0 +1,11 @@ +{ + "default": true, + "MD003": false, + "MD033": false, + "MD013": false, + "MD001": false, + "MD025": false, + "MD024": false, + "MD007": { "indent": 4 }, + "no-hard-tabs": false +} diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100755 index 0000000..fb2c1b5 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,4 @@ +shell-emulator=true +shamefully-hoist=true +enable-pre-post-scripts=false +strict-peer-dependencies=false \ No newline at end of file diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100755 index 0000000..c5ddcef --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v22.14.0 \ No newline at end of file diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js new file mode 100755 index 0000000..5e00251 --- /dev/null +++ b/frontend/.prettierrc.js @@ -0,0 +1,9 @@ +// @ts-check + +/** @type {import("prettier").Config} */ +export default { + bracketSpacing: true, + singleQuote: false, + arrowParens: "avoid", + trailingComma: "none" +}; diff --git a/frontend/.stylelintignore b/frontend/.stylelintignore new file mode 100755 index 0000000..b03887c --- /dev/null +++ b/frontend/.stylelintignore @@ -0,0 +1,4 @@ +/dist/* +/public/* +public/* +src/style/reset.scss \ No newline at end of file diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100755 index 0000000..6cfd7ef --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "christian-kohler.path-intellisense", + "warmthsea.vscode-custom-code-color", + "vscode-icons-team.vscode-icons", + "davidanson.vscode-markdownlint", + "ms-azuretools.vscode-docker", + "stylelint.vscode-stylelint", + "bradlc.vscode-tailwindcss", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "redhat.vscode-yaml", + "csstools.postcss", + "mikestead.dotenv", + "eamodio.gitlens", + "antfu.iconify", + "Vue.volar" + ] +} \ No newline at end of file diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100755 index 0000000..b2cff11 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,43 @@ +{ + "editor.formatOnType": true, + "editor.formatOnSave": true, + "[vue]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.tabSize": 2, + "editor.formatOnPaste": true, + "editor.guides.bracketPairs": "active", + "files.autoSave": "afterDelay", + "git.confirmSync": false, + "workbench.startupEditor": "newUntitledFile", + "editor.suggestSelection": "first", + "editor.acceptSuggestionOnCommitCharacter": false, + "css.lint.propertyIgnoredDueToDisplay": "ignore", + "editor.quickSuggestions": { + "other": true, + "comments": true, + "strings": true + }, + "files.associations": { + "editor.snippetSuggestions": "top" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "iconify.excludes": [ + "el" + ], + "vscodeCustomCodeColor.highlightValue": [ + "v-loading", + "v-auth", + "v-copy", + "v-longpress", + "v-optimize", + "v-perms", + "v-ripple" + ], + "vscodeCustomCodeColor.highlightValueColor": "#b392f0", +} \ No newline at end of file diff --git a/frontend/.vscode/vue3.0.code-snippets b/frontend/.vscode/vue3.0.code-snippets new file mode 100755 index 0000000..8670fd2 --- /dev/null +++ b/frontend/.vscode/vue3.0.code-snippets @@ -0,0 +1,22 @@ +{ + "Vue3.0快速生成模板": { + "scope": "vue", + "prefix": "Vue3.0", + "body": [ + "\n", + "\n", + "", + "$2" + ], + "description": "Vue3.0" + } +} diff --git a/frontend/.vscode/vue3.2.code-snippets b/frontend/.vscode/vue3.2.code-snippets new file mode 100755 index 0000000..20056fe --- /dev/null +++ b/frontend/.vscode/vue3.2.code-snippets @@ -0,0 +1,17 @@ +{ + "Vue3.2+快速生成模板": { + "scope": "vue", + "prefix": "Vue3.2+", + "body": [ + "\n", + "\n", + "", + "$2" + ], + "description": "Vue3.2+" + } +} diff --git a/frontend/.vscode/vue3.3.code-snippets b/frontend/.vscode/vue3.3.code-snippets new file mode 100755 index 0000000..054add2 --- /dev/null +++ b/frontend/.vscode/vue3.3.code-snippets @@ -0,0 +1,20 @@ +{ + "Vue3.3+defineOptions快速生成模板": { + "scope": "vue", + "prefix": "Vue3.3+", + "body": [ + "\n", + "\n", + "", + "$2" + ], + "description": "Vue3.3+defineOptions快速生成模板" + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100755 index 0000000..5d7efa8 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20-alpine as build-stage + +WORKDIR /app +RUN corepack enable +RUN corepack prepare pnpm@latest --activate + +RUN npm config set registry https://registry.npmmirror.com + +COPY .npmrc package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm build + +FROM nginx:stable-alpine as production-stage + +COPY --from=build-stage /app/dist /usr/share/nginx/html +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/LICENSE b/frontend/LICENSE new file mode 100755 index 0000000..fdda130 --- /dev/null +++ b/frontend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-present, pure-admin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/frontend/README.en-US.md b/frontend/README.en-US.md new file mode 100755 index 0000000..255511d --- /dev/null +++ b/frontend/README.en-US.md @@ -0,0 +1,47 @@ +

vue-pure-admin Lite Edition(no i18n version)

+ +[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE) + +**English** | [中文](./README.md) + +## Introduce + +The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb` + +## `js` version + +[Click me to view js version](https://pure-admin.cn/pages/js/) + +## `max` version + +[Click me to view the max version](https://pure-admin.cn/pages/max/) + +## Supporting video + +[Click me to view UI design](https://www.bilibili.com/video/BV17g411T7rq) +[Click me to view the rapid development tutorial](https://www.bilibili.com/video/BV1kg411v7QT) + +## Nanny-level documents + +[Click me to view vue-pure-admin documentation](https://pure-admin.cn/) +[Click me to view @pureadmin/utils documentation](https://pure-admin-utils.netlify.app) + +## Quality service, software outsourcing, sponsorship support + +[Click me to view details](https://pure-admin.cn/pages/service/) + +## Preview + +[Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login) + +## Maintainer + +[xiaoxian521](https://github.com/xiaoxian521) + +## ⚠️ Attention + +The Lite version does not accept any issues and prs. If you have any questions, please go to the full version [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) to mention, thank you! + +## License + +[MIT © 2020-present, pure-admin](./LICENSE) diff --git a/frontend/README.md b/frontend/README.md new file mode 100755 index 0000000..4df83fd --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,51 @@ +

vue-pure-admin精简版(非国际化版本)

+ +[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE) + +**中文** | [English](./README.en-US.md) + +## 介绍 + +精简版是基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb` + +## 版本选择 + +当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n) + +## `js` 版本 + +[点我查看 js 版本](https://pure-admin.cn/pages/js/) + +## `max` 版本 + +[点我查看 max 版本](https://pure-admin.cn/pages/max/) + +## 配套视频 + +[点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq) +[点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT) + +## 配套保姆级文档 + +[点我查看 vue-pure-admin 文档](https://pure-admin.cn/) +[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app) + +## 优质服务、软件外包、赞助支持 + +[点我查看详情](https://pure-admin.cn/pages/service/) + +## 预览 + +[查看预览](https://pure-admin-thin.netlify.app/#/login) + +## 维护者 + +[xiaoxian521](https://github.com/xiaoxian521) + +## ⚠️ 注意 + +精简版不接受任何 `issues` 和 `pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢! + +## 许可证 + +[MIT © 2020-present, pure-admin](./LICENSE) diff --git a/frontend/build/cdn.ts b/frontend/build/cdn.ts new file mode 100755 index 0000000..c56e4ad --- /dev/null +++ b/frontend/build/cdn.ts @@ -0,0 +1,55 @@ +import { Plugin as importToCDN } from "vite-plugin-cdn-import"; + +/** + * @description 打包时采用`cdn`模式,仅限外网使用(默认不采用,如果需要采用cdn模式,请在 .env.production 文件,将 VITE_CDN 设置成true) + * 平台采用国内cdn:https://www.bootcdn.cn,当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com + * 注意:上面提到的仅限外网使用也不是完全肯定的,如果你们公司内网部署的有相关js、css文件,也可以将下面配置对应改一下,整一套内网版cdn + */ +export const cdn = importToCDN({ + //(prodUrl解释: name: 对应下面modules的name,version: 自动读取本地package.json中dependencies依赖中对应包的版本号,path: 对应下面modules的path,当然也可写完整路径,会替换prodUrl) + prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}", + modules: [ + { + name: "vue", + var: "Vue", + path: "vue.global.prod.min.js" + }, + { + name: "vue-router", + var: "VueRouter", + path: "vue-router.global.min.js" + }, + // 项目中没有直接安装vue-demi,但是pinia用到了,所以需要在引入pinia前引入vue-demi(https://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77) + { + name: "vue-demi", + var: "VueDemi", + path: "index.iife.min.js" + }, + { + name: "pinia", + var: "Pinia", + path: "pinia.iife.min.js" + }, + { + name: "element-plus", + var: "ElementPlus", + path: "index.full.min.js", + css: "index.min.css" + }, + { + name: "axios", + var: "axios", + path: "axios.min.js" + }, + { + name: "dayjs", + var: "dayjs", + path: "dayjs.min.js" + }, + { + name: "echarts", + var: "echarts", + path: "echarts.min.js" + } + ] +}); diff --git a/frontend/build/compress.ts b/frontend/build/compress.ts new file mode 100755 index 0000000..523a25e --- /dev/null +++ b/frontend/build/compress.ts @@ -0,0 +1,63 @@ +import type { Plugin } from "vite"; +import { isArray } from "@pureadmin/utils"; +import compressPlugin from "vite-plugin-compression"; + +export const configCompressPlugin = ( + compress: ViteCompression +): Plugin | Plugin[] => { + if (compress === "none") return null; + + const gz = { + // 生成的压缩包后缀 + ext: ".gz", + // 体积大于threshold才会被压缩 + threshold: 0, + // 默认压缩.js|mjs|json|css|html后缀文件,设置成true,压缩全部文件 + filter: () => true, + // 压缩后是否删除原始文件 + deleteOriginFile: false + }; + const br = { + ext: ".br", + algorithm: "brotliCompress", + threshold: 0, + filter: () => true, + deleteOriginFile: false + }; + + const codeList = [ + { k: "gzip", v: gz }, + { k: "brotli", v: br }, + { k: "both", v: [gz, br] } + ]; + + const plugins: Plugin[] = []; + + codeList.forEach(item => { + if (compress.includes(item.k)) { + if (compress.includes("clear")) { + if (isArray(item.v)) { + item.v.forEach(vItem => { + plugins.push( + compressPlugin(Object.assign(vItem, { deleteOriginFile: true })) + ); + }); + } else { + plugins.push( + compressPlugin(Object.assign(item.v, { deleteOriginFile: true })) + ); + } + } else { + if (isArray(item.v)) { + item.v.forEach(vItem => { + plugins.push(compressPlugin(vItem)); + }); + } else { + plugins.push(compressPlugin(item.v)); + } + } + } + }); + + return plugins; +}; diff --git a/frontend/build/info.ts b/frontend/build/info.ts new file mode 100755 index 0000000..e3ef06e --- /dev/null +++ b/frontend/build/info.ts @@ -0,0 +1,57 @@ +import type { Plugin } from "vite"; +import gradient from "gradient-string"; +import { getPackageSize } from "./utils"; +import dayjs, { type Dayjs } from "dayjs"; +import duration from "dayjs/plugin/duration"; +import boxen, { type Options as BoxenOptions } from "boxen"; +dayjs.extend(duration); + +const welcomeMessage = gradient(["cyan", "magenta"]).multiline( + `您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://pure-admin.cn\nhttps://pure-admin-utils.netlify.app` +); + +const boxenOptions: BoxenOptions = { + padding: 0.5, + borderColor: "cyan", + borderStyle: "round" +}; + +export function viteBuildInfo(): Plugin { + let config: { command: string }; + let startTime: Dayjs; + let endTime: Dayjs; + let outDir: string; + return { + name: "vite:buildInfo", + configResolved(resolvedConfig) { + config = resolvedConfig; + outDir = resolvedConfig.build?.outDir ?? "dist"; + }, + buildStart() { + console.log(boxen(welcomeMessage, boxenOptions)); + if (config.command === "build") { + startTime = dayjs(new Date()); + } + }, + closeBundle() { + if (config.command === "build") { + endTime = dayjs(new Date()); + getPackageSize({ + folder: outDir, + callback: (size: string) => { + console.log( + boxen( + gradient(["cyan", "magenta"]).multiline( + `🎉 恭喜打包完成(总用时${dayjs + .duration(endTime.diff(startTime)) + .format("mm分ss秒")},打包后的大小为${size})` + ), + boxenOptions + ) + ); + } + }); + } + } + }; +} diff --git a/frontend/build/optimize.ts b/frontend/build/optimize.ts new file mode 100755 index 0000000..dcdeae5 --- /dev/null +++ b/frontend/build/optimize.ts @@ -0,0 +1,29 @@ +/** + * 此文件作用于 `vite.config.ts` 的 `optimizeDeps.include` 依赖预构建配置项 + * 依赖预构建,`vite` 启动时会将下面 include 里的模块,编译成 esm 格式并缓存到 node_modules/.vite 文件夹,页面加载到对应模块时如果浏览器有缓存就读取浏览器缓存,如果没有会读取本地缓存并按需加载 + * 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里,否则会遇到开发环境切换页面卡顿的问题(vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存 + * 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite + */ +const include = [ + "qs", + "mitt", + "dayjs", + "axios", + "pinia", + "vue-types", + "js-cookie", + "vue-tippy", + "pinyin-pro", + "sortablejs", + "@vueuse/core", + "@pureadmin/utils", + "responsive-storage" +]; + +/** + * 在预构建中强制排除的依赖项 + * 温馨提示:平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好 + */ +const exclude = ["@iconify/json"]; + +export { include, exclude }; diff --git a/frontend/build/plugins.ts b/frontend/build/plugins.ts new file mode 100755 index 0000000..93f5981 --- /dev/null +++ b/frontend/build/plugins.ts @@ -0,0 +1,66 @@ +import { cdn } from "./cdn"; +import vue from "@vitejs/plugin-vue"; +import { viteBuildInfo } from "./info"; +import svgLoader from "vite-svg-loader"; +import Icons from "unplugin-icons/vite"; +import type { PluginOption } from "vite"; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import tailwindcss from "@tailwindcss/vite"; +import { configCompressPlugin } from "./compress"; +import removeNoMatch from "vite-plugin-router-warn"; +import { visualizer } from "rollup-plugin-visualizer"; +import removeConsole from "vite-plugin-remove-console"; +import { codeInspectorPlugin } from "code-inspector-plugin"; +import { vitePluginFakeServer } from "vite-plugin-fake-server"; + +export function getPluginsList( + VITE_CDN: boolean, + VITE_COMPRESSION: ViteCompression +): PluginOption[] { + const lifecycle = process.env.npm_lifecycle_event; + return [ + tailwindcss(), + vue(), + // jsx、tsx语法支持 + vueJsx(), + /** + * 在页面上按住组合键时,鼠标在页面移动即会在 DOM 上出现遮罩层并显示相关信息,点击一下将自动打开 IDE 并将光标定位到元素对应的代码位置 + * Mac 默认组合键 Option + Shift + * Windows 默认组合键 Alt + Shift + * 更多用法看 https://inspector.fe-dev.cn/guide/start.html + */ + codeInspectorPlugin({ + bundler: "vite", + hideConsole: true + }), + viteBuildInfo(), + /** + * 开发环境下移除非必要的vue-router动态路由警告No match found for location with path + * 非必要具体看 https://github.com/vuejs/router/issues/521 和 https://github.com/vuejs/router/issues/359 + * vite-plugin-router-warn只在开发环境下启用,只处理vue-router文件并且只在服务启动或重启时运行一次,性能消耗可忽略不计 + */ + removeNoMatch(), + // mock支持 + vitePluginFakeServer({ + logger: false, + include: "mock", + infixName: false, + enableProd: true + }), + // svg组件化支持 + svgLoader(), + // 自动按需加载图标 + Icons({ + compiler: "vue3", + scale: 1 + }), + VITE_CDN ? cdn : null, + configCompressPlugin(VITE_COMPRESSION), + // 线上环境删除console + removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }), + // 打包分析 + lifecycle === "report" + ? visualizer({ open: true, brotliSize: true, filename: "report.html" }) + : (null as any) + ]; +} diff --git a/frontend/build/utils.ts b/frontend/build/utils.ts new file mode 100755 index 0000000..2647bc0 --- /dev/null +++ b/frontend/build/utils.ts @@ -0,0 +1,113 @@ +import dayjs from "dayjs"; +import { readdir, stat } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import { sum, formatBytes } from "@pureadmin/utils"; +import { + name, + version, + engines, + dependencies, + devDependencies +} from "../package.json"; + +/** 启动`node`进程时所在工作目录的绝对路径 */ +const root: string = process.cwd(); + +/** + * @description 根据可选的路径片段生成一个新的绝对路径 + * @param dir 路径片段,默认`build` + * @param metaUrl 模块的完整`url`,如果在`build`目录外调用必传`import.meta.url` + */ +const pathResolve = (dir = ".", metaUrl = import.meta.url) => { + // 当前文件目录的绝对路径 + const currentFileDir = dirname(fileURLToPath(metaUrl)); + // build 目录的绝对路径 + const buildDir = resolve(currentFileDir, "build"); + // 解析的绝对路径 + const resolvedPath = resolve(currentFileDir, dir); + // 检查解析的绝对路径是否在 build 目录内 + if (resolvedPath.startsWith(buildDir)) { + // 在 build 目录内,返回当前文件路径 + return fileURLToPath(metaUrl); + } + // 不在 build 目录内,返回解析后的绝对路径 + return resolvedPath; +}; + +/** 设置别名 */ +const alias: Record = { + "@": pathResolve("../src"), + "@build": pathResolve() +}; + +/** 平台的名称、版本、运行所需的`node`和`pnpm`版本、依赖、最后构建时间的类型提示 */ +const __APP_INFO__ = { + pkg: { name, version, engines, dependencies, devDependencies }, + lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss") +}; + +/** 处理环境变量 */ +const wrapperEnv = (envConf: Recordable): ViteEnv => { + // 默认值 + const ret: ViteEnv = { + VITE_PORT: 8848, + VITE_PUBLIC_PATH: "", + VITE_ROUTER_HISTORY: "", + VITE_CDN: false, + VITE_HIDE_HOME: "false", + VITE_COMPRESSION: "none" + }; + + for (const envName of Object.keys(envConf)) { + let realName = envConf[envName].replace(/\\n/g, "\n"); + realName = + realName === "true" ? true : realName === "false" ? false : realName; + + if (envName === "VITE_PORT") { + realName = Number(realName); + } + ret[envName] = realName; + if (typeof realName === "string") { + process.env[envName] = realName; + } else if (typeof realName === "object") { + process.env[envName] = JSON.stringify(realName); + } + } + return ret; +}; + +const fileListTotal: number[] = []; + +/** 获取指定文件夹中所有文件的总大小 */ +const getPackageSize = options => { + const { folder = "dist", callback, format = true } = options; + readdir(folder, (err, files: string[]) => { + if (err) throw err; + let count = 0; + const checkEnd = () => { + if (++count === files.length) { + callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal)); + } + }; + files.forEach((item: string) => { + stat(`${folder}/${item}`, async (err, stats) => { + if (err) throw err; + if (stats.isFile()) { + fileListTotal.push(stats.size); + checkEnd(); + } else if (stats.isDirectory()) { + getPackageSize({ + folder: `${folder}/${item}/`, + callback: checkEnd + }); + } + }); + }); + if (files.length === 0) { + callback(0); + } + }); +}; + +export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize }; diff --git a/frontend/commitlint.config.js b/frontend/commitlint.config.js new file mode 100755 index 0000000..083440f --- /dev/null +++ b/frontend/commitlint.config.js @@ -0,0 +1,35 @@ +// @ts-check + +/** @type {import("@commitlint/types").UserConfig} */ +export default { + ignores: [commit => commit.includes("init")], + extends: ["@commitlint/config-conventional"], + rules: { + "body-leading-blank": [2, "always"], + "footer-leading-blank": [1, "always"], + "header-max-length": [2, "always", 108], + "subject-empty": [2, "never"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + [ + "feat", + "fix", + "perf", + "style", + "docs", + "test", + "refactor", + "build", + "ci", + "chore", + "revert", + "wip", + "workflow", + "types", + "release" + ] + ] + } +}; diff --git a/frontend/coverage/applyDelivery.ts.html b/frontend/coverage/applyDelivery.ts.html new file mode 100644 index 0000000..bbd0a8d --- /dev/null +++ b/frontend/coverage/applyDelivery.ts.html @@ -0,0 +1,577 @@ + + + + + + Code coverage report for applyDelivery.ts + + + + + + + + + +
+
+

All files applyDelivery.ts

+
+ +
+ 100% + Statements + 16/16 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +2x +  +  +  + 
import { http } from "@/utils/http";
+ 
+// ============ 类型定义 ============
+/** 提货委托项 */
+export type DeliveryItem = {
+  id: string;
+  orderNumber: string; // 提货委托单号
+  blNumber: string; // 提单号1
+  blNumber2: string; // 提单号2
+  licensePlate: string; // 车牌号
+  driverName: string; // 司机姓名
+  driverPhone: string; // 司机电话
+  driverIdCard: string; // 司机身份证号
+  commodity: string; // 商品品类
+  quantity: number; // 数量
+  unit: string; // 单位
+  deliveryDate: string; // 提货日期
+  deliveryAddress: string; // 提货地址
+  warehouse: string; // 仓库
+  status: "pending" | "approved" | "in_progress" | "completed" | "cancelled"; // 状态
+  createTime: string; // 创建时间
+  updateTime: string; // 更新时间
+  remark?: string; // 备注
+};
+ 
+/** 提货委托表单数据 */
+export type DeliveryFormData = {
+  blNumber: string; // 提单号1
+  blNumber2?: string; // 提单号2
+  licensePlate: string; // 车牌号
+  driverName: string; // 司机姓名
+  driverPhone: string; // 司机电话
+  driverIdCard: string; // 司机身份证号
+  commodity: string; // 商品品类
+  quantity: number; // 数量
+  unit: string; // 单位
+  deliveryDate: string; // 提货日期
+  deliveryAddress: string; // 提货地址
+  warehouse: string; // 仓库
+  remark?: string; // 备注
+};
+ 
+/** API基础响应 */
+export type BaseResponse<T = any> = {
+  success: boolean;
+  message: string;
+  data?: T;
+};
+ 
+/** 提货委托列表响应 */
+export type DeliveryListResponse = BaseResponse<{
+  items: DeliveryItem[];
+  total: number;
+  page: number;
+  pageSize: number;
+}>;
+ 
+/** 提货委托详情响应 */
+export type DeliveryDetailResponse = BaseResponse<DeliveryItem>;
+ 
+/** 创建提货委托响应 */
+export type CreateDeliveryResponse = BaseResponse<{
+  id: string;
+  orderNumber: string;
+}>;
+ 
+/** 更新提货委托响应 */
+export type UpdateDeliveryResponse = BaseResponse<DeliveryItem>;
+ 
+/** 取消提货委托响应 */
+export type CancelDeliveryResponse = BaseResponse<DeliveryItem>;
+ 
+/** 下载模板响应 */
+export type DownloadTemplateResponse = BaseResponse<{
+  fileName: string;
+  url: string;
+}>;
+ 
+/** 导出列表响应 */
+export type ExportListResponse = BaseResponse<{
+  fileName: string;
+  url: string;
+}>;
+ 
+// ============ API函数 ============
+/** 获取提货委托列表 */
+export const getDeliveryList = (params?: {
+  page?: number;
+  pageSize?: number;
+  orderNumber?: string;
+  blNumber?: string;
+  blNumber2?: string;
+  licensePlate?: string;
+  driverName?: string;
+  status?: string;
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<DeliveryListResponse>("get", "/api/apply-delivery/list", {
+    params
+  });
+};
+ 
+/** 获取提货委托详情 */
+export const getDeliveryDetail = (id: string) => {
+  return http.request<DeliveryDetailResponse>(
+    "get",
+    `/api/apply-delivery/detail/${id}`
+  );
+};
+ 
+/** 新增提货委托 */
+export const createDelivery = (data: DeliveryFormData) => {
+  return http.request<CreateDeliveryResponse>(
+    "post",
+    "/api/apply-delivery/create",
+    { data }
+  );
+};
+ 
+/** 更新提货委托 */
+export const updateDelivery = (id: string, data: Partial<DeliveryFormData>) => {
+  return http.request<UpdateDeliveryResponse>(
+    "put",
+    `/api/apply-delivery/update/${id}`,
+    { data }
+  );
+};
+ 
+/** 取消提货委托 */
+export const cancelDelivery = (id: string) => {
+  return http.request<CancelDeliveryResponse>(
+    "put",
+    `/api/apply-delivery/cancel/${id}`
+  );
+};
+ 
+/** 下载提货单模板 */
+export const downloadDeliveryTemplate = () => {
+  return http.request<DownloadTemplateResponse>(
+    "get",
+    "/api/apply-delivery/template/download"
+  );
+};
+ 
+/** 删除提货委托 */
+export const deleteDelivery = (id: string) => {
+  return http.request<BaseResponse>("delete", `/api/apply-delivery/${id}`);
+};
+ 
+/** 导出提货委托列表 */
+export const exportDeliveryList = (params?: {
+  orderNumber?: string;
+  blNumber?: string;
+  licensePlate?: string;
+  driverName?: string;
+  status?: string;
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<ExportListResponse>("get", "/api/apply-delivery/export", {
+    params
+  });
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/base.css b/frontend/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/frontend/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/frontend/coverage/block-navigation.js b/frontend/coverage/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/frontend/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/frontend/coverage/company.ts.html b/frontend/coverage/company.ts.html new file mode 100644 index 0000000..56ad9df --- /dev/null +++ b/frontend/coverage/company.ts.html @@ -0,0 +1,445 @@ + + + + + + Code coverage report for company.ts + + + + + + + + + +
+
+

All files company.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 100% + Lines + 18/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +2x +  +  +  +1x +2x +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +1x +1x +  +  +  +1x +1x +  +  +  +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+// ============ 类型定义 ============
+/** 公司文件夹 */
+export type CompanyFolder = {
+  id: string;
+  name: string;
+  count: number;
+  createTime: string;
+  description?: string;
+  category?: string;
+};
+ 
+/** 公司文件 */
+export type CompanyFile = {
+  id: string;
+  fileName: string;
+  fileType: string;
+  fileSize: string;
+  uploadTime: string;
+  folderId: string;
+  companyName?: string;
+  fileCategory: string;
+  expireDate?: string;
+  description?: string;
+};
+ 
+/** API基础响应 */
+ 
+/** 文件夹列表响应 */
+export type FoldersResponse = BaseResponse<CompanyFolder[]>;
+ 
+/** 文件列表响应 */
+export type FilesResponse = BaseResponse<CompanyFile[]>;
+ 
+/** 上传文件响应 */
+export type UploadFileResponse = BaseResponse<{
+  uploadedCount: number;
+}>;
+ 
+/** 创建/更新文件夹请求 */
+export type FolderRequest = {
+  name: string;
+  description?: string;
+  category?: string;
+};
+ 
+// ============ API函数 ============
+/** 获取文件夹列表 */
+export const getCompanyFolders = () => {
+  return http.request<FoldersResponse>("get", "/api/company/folders");
+};
+ 
+/** 获取文件列表 */
+export const getCompanyFiles = (params?: { folderId?: string }) => {
+  return http.request<FilesResponse>("get", "/api/company/files", { params });
+};
+ 
+/** 创建文件夹 */
+export const createCompanyFolder = (data: FolderRequest) => {
+  return http.request<BaseResponse<CompanyFolder>>("post", "/api/company/folders", {
+    data
+  });
+};
+ 
+/** 更新文件夹 */
+export const updateCompanyFolder = (
+  id: string,
+  data: Partial<FolderRequest>
+) => {
+  return http.request<BaseResponse<CompanyFolder>>(
+    "put",
+    `/api/company/folders/${id}`,
+    { data }
+  );
+};
+ 
+/** 删除文件夹 */
+export const deleteCompanyFolder = (id: string) => {
+  return http.request<BaseResponse>("delete", `/api/company/folders/${id}`);
+};
+ 
+/** 上传公司文件 */
+export const uploadCompanyFile = (data: FormData) => {
+  return http.request<UploadFileResponse>("post", "/api/company/files/upload", {
+    data,
+    headers: {
+      "Content-Type": "multipart/form-data"
+    }
+  });
+};
+ 
+/** 更新公司文件元信息 */
+export const updateCompanyFile = (
+  id: string,
+  data: {
+    companyName: string;
+    fileCategory: string;
+    expireDate?: string;
+    description?: string;
+    folderId?: number | null;
+  }
+) => {
+  return http.request<BaseResponse>("put", `/api/company/files/${id}`, {
+    data
+  });
+};
+ 
+/** 删除公司文件 */
+export const deleteCompanyFile = (id: string) => {
+  return http.request<BaseResponse>("delete", `/api/company/files/${id}`);
+};
+ 
+/** 下载公司文件(文件 URL 为 /api/company-files/{fileUrl}) */
+export const downloadCompanyFile = (id: string) => {
+  return http.request<BaseResponse<{ fileName: string; url: string }>>(
+    "get",
+    `/api/company/files/${id}/download`
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/contract.ts.html b/frontend/coverage/contract.ts.html new file mode 100644 index 0000000..6866f0b --- /dev/null +++ b/frontend/coverage/contract.ts.html @@ -0,0 +1,577 @@ + + + + + + Code coverage report for contract.ts + + + + + + + + + +
+
+

All files contract.ts

+
+ +
+ 100% + Statements + 30/30 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 15/15 +
+ + +
+ 100% + Lines + 30/30 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +2x +  +  +  +1x +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +1x +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +1x +  +  +  +2x +  +  +  +  +1x +  +  +  +2x +  +  +1x +2x +  +  +1x +2x +  +  +  +  +1x +2x +  +  +  +1x +1x +  +  +  +  +  +1x +2x +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+ 
+/** 业务合同(含附件列表) */
+export interface BusinessContract {
+  id: number;
+  name: string;
+  contractType: string;
+  contractCode: string;
+  buyParty: string;
+  sellParty: string;
+  commodity: string;
+  count: number;
+  unitPrice: number;
+  blNumber: string;
+  deliveryTime: string;
+  pickUpTime: string;
+  packaging: string;
+  remarks: string;
+  createTime: string;
+  updateTime: string;
+  files?: { id: number; name: string; url: string }[];
+}
+ 
+/** 提交合同后后端返回的结果 */
+export type ContractInitResult = {
+  success: boolean;
+  message: string;
+};
+ 
+/** 获取业务合同列表 */
+export const getContractList = (params?: { contractType?: string }) => {
+  return http.request<BaseResponse<BusinessContract[]>>("get", "/api/contract", {
+    params
+  });
+};
+ 
+/** 获取单个业务合同详情 */
+export const getContract = (id: number | string) => {
+  return http.request<BaseResponse<BusinessContract>>("get", `/api/contract/${id}`);
+};
+ 
+/** 提交合同(POST) */
+export const submitContract = (data: FormData) => {
+  return http.request<ContractInitResult>("post", "/api/contract/upload", {
+    data,
+    headers: { "Content-Type": "multipart/form-data" }
+  });
+};
+ 
+export type BatchUploadResult = {
+  success: boolean;
+  message: string;
+  data?: {
+    successCount: number;
+    failedItems: Array<{ name: string; reason: string }>;
+  };
+};
+ 
+/** 批量上传合同(POST) */
+export const batchUploadContracts = (data: FormData) => {
+  return http.request<BatchUploadResult>("post", "/api/contract/batch-upload", {
+    data,
+    headers: { "Content-Type": "multipart/form-data" }
+  });
+};
+ 
+/** 更新业务合同(含表单字段和合同扫描件增删) */
+export const updateContractWithFiles = (id: number | string, data: FormData) => {
+  return http.request<BaseResponse>("post", `/api/contract/${id}/update-files`, {
+    data,
+    headers: { "Content-Type": "multipart/form-data" }
+  });
+};
+ 
+/** 更新业务合同(仅更新表单字段,不包含附件) */
+export const updateContract = (
+  id: number | string,
+  data: Partial<Omit<BusinessContract, "id" | "createTime" | "updateTime" | "files">>
+) => {
+  return http.request<BaseResponse>("put", `/api/contract/${id}`, { data });
+};
+ 
+/** 删除业务合同 */
+export const deleteContract = (id: number | string) => {
+  return http.request<BaseResponse>("delete", `/api/contract/${id}`);
+};
+ 
+// ========== 批量上传归档(与业务合同共用体系) ==========
+ 
+/** 批量归档文件夹 */
+export interface ContractFolderItem {
+  id: number;
+  name: string;
+  description: string;
+  createTime: string;
+  updateTime: string;
+}
+ 
+/** 批量上传中的单个文件 */
+export interface ContractBatchFileItem {
+  id: number;
+  batchId: number;
+  name: string;
+  url: string;
+  fileSize: number;
+}
+ 
+/** 一次批量上传记录 */
+export interface ContractBatchItem {
+  id: number;
+  folderId: number;
+  batchName: string;
+  remark: string;
+  createTime: string;
+  updateTime: string;
+  files?: ContractBatchFileItem[];
+}
+ 
+export const getContractFolders = () => {
+  return http.request<BaseResponse<ContractFolderItem[]>>("get", "/api/contract/folders");
+};
+ 
+export const createContractFolder = (data: {
+  name: string;
+  description?: string;
+}) => {
+  return http.request<BaseResponse<ContractFolderItem>>("post", "/api/contract/folders", {
+    data
+  });
+};
+ 
+export const updateContractFolder = (
+  id: number | string,
+  data: { name: string; description?: string }
+) => {
+  return http.request<BaseResponse>("put", `/api/contract/folders/${id}`, { data });
+};
+ 
+export const deleteContractFolder = (id: number | string) => {
+  return http.request<BaseResponse>("delete", `/api/contract/folders/${id}`);
+};
+ 
+export const getContractBatches = (params?: { folderId?: number }) => {
+  return http.request<BaseResponse<ContractBatchItem[]>>("get", "/api/contract/batches", {
+    params
+  });
+};
+ 
+export const getContractBatch = (id: number | string) => {
+  return http.request<BaseResponse<ContractBatchItem>>("get", `/api/contract/batches/${id}`);
+};
+ 
+/** 批量上传:提交字段 folderId, batchName, remark, files[] */
+export const createContractBatch = (formData: FormData) => {
+  return http.request<BaseResponse<ContractBatchItem>>("post", "/api/contract/batches", {
+    data: formData,
+    headers: { "Content-Type": "multipart/form-data" }
+  });
+};
+ 
+export const deleteContractBatch = (id: number | string) => {
+  return http.request<BaseResponse>("delete", `/api/contract/batches/${id}`);
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/coverage-final.json b/frontend/coverage/coverage-final.json new file mode 100644 index 0000000..df3414c --- /dev/null +++ b/frontend/coverage/coverage-final.json @@ -0,0 +1,14 @@ +{"/home/rovina/work/zsp-project/frontend/src/api/applyDelivery.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/applyDelivery.ts","statementMap":{"0":{"start":{"line":87,"column":31},"end":{"line":102,"column":null}},"1":{"start":{"line":99,"column":2},"end":{"line":101,"column":null}},"2":{"start":{"line":105,"column":33},"end":{"line":110,"column":null}},"3":{"start":{"line":106,"column":2},"end":{"line":109,"column":null}},"4":{"start":{"line":113,"column":30},"end":{"line":119,"column":null}},"5":{"start":{"line":114,"column":2},"end":{"line":118,"column":null}},"6":{"start":{"line":122,"column":30},"end":{"line":128,"column":null}},"7":{"start":{"line":123,"column":2},"end":{"line":127,"column":null}},"8":{"start":{"line":131,"column":30},"end":{"line":136,"column":null}},"9":{"start":{"line":132,"column":2},"end":{"line":135,"column":null}},"10":{"start":{"line":139,"column":40},"end":{"line":144,"column":null}},"11":{"start":{"line":140,"column":2},"end":{"line":143,"column":null}},"12":{"start":{"line":147,"column":30},"end":{"line":149,"column":null}},"13":{"start":{"line":148,"column":2},"end":{"line":148,"column":null}},"14":{"start":{"line":152,"column":34},"end":{"line":164,"column":null}},"15":{"start":{"line":161,"column":2},"end":{"line":163,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":87,"column":31},"end":{"line":87,"column":32}},"loc":{"start":{"line":98,"column":6},"end":{"line":102,"column":null}},"line":98},"1":{"name":"(anonymous_1)","decl":{"start":{"line":105,"column":33},"end":{"line":105,"column":34}},"loc":{"start":{"line":105,"column":49},"end":{"line":110,"column":null}},"line":105},"2":{"name":"(anonymous_2)","decl":{"start":{"line":113,"column":30},"end":{"line":113,"column":31}},"loc":{"start":{"line":113,"column":58},"end":{"line":119,"column":null}},"line":113},"3":{"name":"(anonymous_3)","decl":{"start":{"line":122,"column":30},"end":{"line":122,"column":31}},"loc":{"start":{"line":122,"column":79},"end":{"line":128,"column":null}},"line":122},"4":{"name":"(anonymous_4)","decl":{"start":{"line":131,"column":30},"end":{"line":131,"column":31}},"loc":{"start":{"line":131,"column":46},"end":{"line":136,"column":null}},"line":131},"5":{"name":"(anonymous_5)","decl":{"start":{"line":139,"column":40},"end":{"line":139,"column":46}},"loc":{"start":{"line":139,"column":46},"end":{"line":144,"column":null}},"line":139},"6":{"name":"(anonymous_6)","decl":{"start":{"line":147,"column":30},"end":{"line":147,"column":31}},"loc":{"start":{"line":147,"column":46},"end":{"line":149,"column":null}},"line":147},"7":{"name":"(anonymous_7)","decl":{"start":{"line":152,"column":34},"end":{"line":152,"column":35}},"loc":{"start":{"line":160,"column":6},"end":{"line":164,"column":null}},"line":160}},"branchMap":{},"s":{"0":1,"1":3,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":2},"f":{"0":3,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":2},"b":{},"meta":{"lastBranch":0,"lastFunction":8,"lastStatement":16,"seen":{"s:87:31:102:Infinity":0,"f:87:31:87:32":0,"s:99:2:101:Infinity":1,"s:105:33:110:Infinity":2,"f:105:33:105:34":1,"s:106:2:109:Infinity":3,"s:113:30:119:Infinity":4,"f:113:30:113:31":2,"s:114:2:118:Infinity":5,"s:122:30:128:Infinity":6,"f:122:30:122:31":3,"s:123:2:127:Infinity":7,"s:131:30:136:Infinity":8,"f:131:30:131:31":4,"s:132:2:135:Infinity":9,"s:139:40:144:Infinity":10,"f:139:40:139:46":5,"s:140:2:143:Infinity":11,"s:147:30:149:Infinity":12,"f:147:30:147:31":6,"s:148:2:148:Infinity":13,"s:152:34:164:Infinity":14,"f:152:34:152:35":7,"s:161:2:163:Infinity":15}}} +,"/home/rovina/work/zsp-project/frontend/src/api/company.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/company.ts","statementMap":{"0":{"start":{"line":50,"column":33},"end":{"line":52,"column":null}},"1":{"start":{"line":51,"column":2},"end":{"line":51,"column":null}},"2":{"start":{"line":55,"column":31},"end":{"line":57,"column":null}},"3":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"4":{"start":{"line":60,"column":35},"end":{"line":64,"column":null}},"5":{"start":{"line":61,"column":2},"end":{"line":63,"column":null}},"6":{"start":{"line":67,"column":35},"end":{"line":76,"column":null}},"7":{"start":{"line":71,"column":2},"end":{"line":75,"column":null}},"8":{"start":{"line":79,"column":35},"end":{"line":81,"column":null}},"9":{"start":{"line":80,"column":2},"end":{"line":80,"column":null}},"10":{"start":{"line":84,"column":33},"end":{"line":91,"column":null}},"11":{"start":{"line":85,"column":2},"end":{"line":90,"column":null}},"12":{"start":{"line":94,"column":33},"end":{"line":107,"column":null}},"13":{"start":{"line":104,"column":2},"end":{"line":106,"column":null}},"14":{"start":{"line":110,"column":33},"end":{"line":112,"column":null}},"15":{"start":{"line":111,"column":2},"end":{"line":111,"column":null}},"16":{"start":{"line":115,"column":35},"end":{"line":120,"column":null}},"17":{"start":{"line":116,"column":2},"end":{"line":119,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":50,"column":33},"end":{"line":50,"column":39}},"loc":{"start":{"line":50,"column":39},"end":{"line":52,"column":null}},"line":50},"1":{"name":"(anonymous_1)","decl":{"start":{"line":55,"column":31},"end":{"line":55,"column":32}},"loc":{"start":{"line":55,"column":67},"end":{"line":57,"column":null}},"line":55},"2":{"name":"(anonymous_2)","decl":{"start":{"line":60,"column":35},"end":{"line":60,"column":36}},"loc":{"start":{"line":60,"column":60},"end":{"line":64,"column":null}},"line":60},"3":{"name":"(anonymous_3)","decl":{"start":{"line":67,"column":35},"end":{"line":67,"column":null}},"loc":{"start":{"line":70,"column":5},"end":{"line":76,"column":null}},"line":70},"4":{"name":"(anonymous_4)","decl":{"start":{"line":79,"column":35},"end":{"line":79,"column":36}},"loc":{"start":{"line":79,"column":51},"end":{"line":81,"column":null}},"line":79},"5":{"name":"(anonymous_5)","decl":{"start":{"line":84,"column":33},"end":{"line":84,"column":34}},"loc":{"start":{"line":84,"column":53},"end":{"line":91,"column":null}},"line":84},"6":{"name":"(anonymous_6)","decl":{"start":{"line":94,"column":33},"end":{"line":94,"column":null}},"loc":{"start":{"line":103,"column":5},"end":{"line":107,"column":null}},"line":103},"7":{"name":"(anonymous_7)","decl":{"start":{"line":110,"column":33},"end":{"line":110,"column":34}},"loc":{"start":{"line":110,"column":49},"end":{"line":112,"column":null}},"line":110},"8":{"name":"(anonymous_8)","decl":{"start":{"line":115,"column":35},"end":{"line":115,"column":36}},"loc":{"start":{"line":115,"column":51},"end":{"line":120,"column":null}},"line":115}},"branchMap":{},"s":{"0":1,"1":1,"2":1,"3":2,"4":1,"5":2,"6":1,"7":2,"8":1,"9":1,"10":1,"11":1,"12":1,"13":3,"14":1,"15":1,"16":1,"17":1},"f":{"0":1,"1":2,"2":2,"3":2,"4":1,"5":1,"6":3,"7":1,"8":1},"b":{},"meta":{"lastBranch":0,"lastFunction":9,"lastStatement":18,"seen":{"s:50:33:52:Infinity":0,"f:50:33:50:39":0,"s:51:2:51:Infinity":1,"s:55:31:57:Infinity":2,"f:55:31:55:32":1,"s:56:2:56:Infinity":3,"s:60:35:64:Infinity":4,"f:60:35:60:36":2,"s:61:2:63:Infinity":5,"s:67:35:76:Infinity":6,"f:67:35:67:Infinity":3,"s:71:2:75:Infinity":7,"s:79:35:81:Infinity":8,"f:79:35:79:36":4,"s:80:2:80:Infinity":9,"s:84:33:91:Infinity":10,"f:84:33:84:34":5,"s:85:2:90:Infinity":11,"s:94:33:107:Infinity":12,"f:94:33:94:Infinity":6,"s:104:2:106:Infinity":13,"s:110:33:112:Infinity":14,"f:110:33:110:34":7,"s:111:2:111:Infinity":15,"s:115:35:120:Infinity":16,"f:115:35:115:36":8,"s:116:2:119:Infinity":17}}} +,"/home/rovina/work/zsp-project/frontend/src/api/contract.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/contract.ts","statementMap":{"0":{"start":{"line":32,"column":31},"end":{"line":36,"column":null}},"1":{"start":{"line":33,"column":2},"end":{"line":35,"column":null}},"2":{"start":{"line":39,"column":27},"end":{"line":41,"column":null}},"3":{"start":{"line":40,"column":2},"end":{"line":40,"column":null}},"4":{"start":{"line":44,"column":30},"end":{"line":49,"column":null}},"5":{"start":{"line":45,"column":2},"end":{"line":48,"column":null}},"6":{"start":{"line":61,"column":36},"end":{"line":66,"column":null}},"7":{"start":{"line":62,"column":2},"end":{"line":65,"column":null}},"8":{"start":{"line":69,"column":39},"end":{"line":74,"column":null}},"9":{"start":{"line":70,"column":2},"end":{"line":73,"column":null}},"10":{"start":{"line":77,"column":30},"end":{"line":82,"column":null}},"11":{"start":{"line":81,"column":2},"end":{"line":81,"column":null}},"12":{"start":{"line":85,"column":30},"end":{"line":87,"column":null}},"13":{"start":{"line":86,"column":2},"end":{"line":86,"column":null}},"14":{"start":{"line":120,"column":34},"end":{"line":122,"column":null}},"15":{"start":{"line":121,"column":2},"end":{"line":121,"column":null}},"16":{"start":{"line":124,"column":36},"end":{"line":131,"column":null}},"17":{"start":{"line":128,"column":2},"end":{"line":130,"column":null}},"18":{"start":{"line":133,"column":36},"end":{"line":138,"column":null}},"19":{"start":{"line":137,"column":2},"end":{"line":137,"column":null}},"20":{"start":{"line":140,"column":36},"end":{"line":142,"column":null}},"21":{"start":{"line":141,"column":2},"end":{"line":141,"column":null}},"22":{"start":{"line":144,"column":34},"end":{"line":148,"column":null}},"23":{"start":{"line":145,"column":2},"end":{"line":147,"column":null}},"24":{"start":{"line":150,"column":32},"end":{"line":152,"column":null}},"25":{"start":{"line":151,"column":2},"end":{"line":151,"column":null}},"26":{"start":{"line":155,"column":35},"end":{"line":160,"column":null}},"27":{"start":{"line":156,"column":2},"end":{"line":159,"column":null}},"28":{"start":{"line":162,"column":35},"end":{"line":164,"column":null}},"29":{"start":{"line":163,"column":2},"end":{"line":163,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":32,"column":31},"end":{"line":32,"column":32}},"loc":{"start":{"line":32,"column":71},"end":{"line":36,"column":null}},"line":32},"1":{"name":"(anonymous_1)","decl":{"start":{"line":39,"column":27},"end":{"line":39,"column":28}},"loc":{"start":{"line":39,"column":52},"end":{"line":41,"column":null}},"line":39},"2":{"name":"(anonymous_2)","decl":{"start":{"line":44,"column":30},"end":{"line":44,"column":31}},"loc":{"start":{"line":44,"column":50},"end":{"line":49,"column":null}},"line":44},"3":{"name":"(anonymous_3)","decl":{"start":{"line":61,"column":36},"end":{"line":61,"column":37}},"loc":{"start":{"line":61,"column":56},"end":{"line":66,"column":null}},"line":61},"4":{"name":"(anonymous_4)","decl":{"start":{"line":69,"column":39},"end":{"line":69,"column":40}},"loc":{"start":{"line":69,"column":80},"end":{"line":74,"column":null}},"line":69},"5":{"name":"(anonymous_5)","decl":{"start":{"line":77,"column":30},"end":{"line":77,"column":null}},"loc":{"start":{"line":80,"column":5},"end":{"line":82,"column":null}},"line":80},"6":{"name":"(anonymous_6)","decl":{"start":{"line":85,"column":30},"end":{"line":85,"column":31}},"loc":{"start":{"line":85,"column":55},"end":{"line":87,"column":null}},"line":85},"7":{"name":"(anonymous_7)","decl":{"start":{"line":120,"column":34},"end":{"line":120,"column":40}},"loc":{"start":{"line":120,"column":40},"end":{"line":122,"column":null}},"line":120},"8":{"name":"(anonymous_8)","decl":{"start":{"line":124,"column":36},"end":{"line":124,"column":37}},"loc":{"start":{"line":127,"column":6},"end":{"line":131,"column":null}},"line":127},"9":{"name":"(anonymous_9)","decl":{"start":{"line":133,"column":36},"end":{"line":133,"column":null}},"loc":{"start":{"line":136,"column":5},"end":{"line":138,"column":null}},"line":136},"10":{"name":"(anonymous_10)","decl":{"start":{"line":140,"column":36},"end":{"line":140,"column":37}},"loc":{"start":{"line":140,"column":61},"end":{"line":142,"column":null}},"line":140},"11":{"name":"(anonymous_11)","decl":{"start":{"line":144,"column":34},"end":{"line":144,"column":35}},"loc":{"start":{"line":144,"column":70},"end":{"line":148,"column":null}},"line":144},"12":{"name":"(anonymous_12)","decl":{"start":{"line":150,"column":32},"end":{"line":150,"column":33}},"loc":{"start":{"line":150,"column":57},"end":{"line":152,"column":null}},"line":150},"13":{"name":"(anonymous_13)","decl":{"start":{"line":155,"column":35},"end":{"line":155,"column":36}},"loc":{"start":{"line":155,"column":59},"end":{"line":160,"column":null}},"line":155},"14":{"name":"(anonymous_14)","decl":{"start":{"line":162,"column":35},"end":{"line":162,"column":36}},"loc":{"start":{"line":162,"column":60},"end":{"line":164,"column":null}},"line":162}},"branchMap":{},"s":{"0":1,"1":2,"2":1,"3":2,"4":1,"5":2,"6":1,"7":2,"8":1,"9":1,"10":1,"11":2,"12":1,"13":2,"14":1,"15":1,"16":1,"17":2,"18":1,"19":2,"20":1,"21":2,"22":1,"23":2,"24":1,"25":2,"26":1,"27":1,"28":1,"29":2},"f":{"0":2,"1":2,"2":2,"3":2,"4":1,"5":2,"6":2,"7":1,"8":2,"9":2,"10":2,"11":2,"12":2,"13":1,"14":2},"b":{},"meta":{"lastBranch":0,"lastFunction":15,"lastStatement":30,"seen":{"s:32:31:36:Infinity":0,"f:32:31:32:32":0,"s:33:2:35:Infinity":1,"s:39:27:41:Infinity":2,"f:39:27:39:28":1,"s:40:2:40:Infinity":3,"s:44:30:49:Infinity":4,"f:44:30:44:31":2,"s:45:2:48:Infinity":5,"s:61:36:66:Infinity":6,"f:61:36:61:37":3,"s:62:2:65:Infinity":7,"s:69:39:74:Infinity":8,"f:69:39:69:40":4,"s:70:2:73:Infinity":9,"s:77:30:82:Infinity":10,"f:77:30:77:Infinity":5,"s:81:2:81:Infinity":11,"s:85:30:87:Infinity":12,"f:85:30:85:31":6,"s:86:2:86:Infinity":13,"s:120:34:122:Infinity":14,"f:120:34:120:40":7,"s:121:2:121:Infinity":15,"s:124:36:131:Infinity":16,"f:124:36:124:37":8,"s:128:2:130:Infinity":17,"s:133:36:138:Infinity":18,"f:133:36:133:Infinity":9,"s:137:2:137:Infinity":19,"s:140:36:142:Infinity":20,"f:140:36:140:37":10,"s:141:2:141:Infinity":21,"s:144:34:148:Infinity":22,"f:144:34:144:35":11,"s:145:2:147:Infinity":23,"s:150:32:152:Infinity":24,"f:150:32:150:33":12,"s:151:2:151:Infinity":25,"s:155:35:160:Infinity":26,"f:155:35:155:36":13,"s:156:2:159:Infinity":27,"s:162:35:164:Infinity":28,"f:162:35:162:36":14,"s:163:2:163:Infinity":29}}} +,"/home/rovina/work/zsp-project/frontend/src/api/deliveryDetails.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/deliveryDetails.ts","statementMap":{"0":{"start":{"line":132,"column":34},"end":{"line":150,"column":null}},"1":{"start":{"line":145,"column":2},"end":{"line":149,"column":null}},"2":{"start":{"line":153,"column":33},"end":{"line":158,"column":null}},"3":{"start":{"line":154,"column":2},"end":{"line":157,"column":null}},"4":{"start":{"line":161,"column":35},"end":{"line":167,"column":null}},"5":{"start":{"line":162,"column":2},"end":{"line":166,"column":null}},"6":{"start":{"line":170,"column":35},"end":{"line":179,"column":null}},"7":{"start":{"line":174,"column":2},"end":{"line":178,"column":null}},"8":{"start":{"line":182,"column":30},"end":{"line":188,"column":null}},"9":{"start":{"line":183,"column":2},"end":{"line":187,"column":null}},"10":{"start":{"line":191,"column":33},"end":{"line":196,"column":null}},"11":{"start":{"line":192,"column":2},"end":{"line":195,"column":null}},"12":{"start":{"line":199,"column":40},"end":{"line":204,"column":null}},"13":{"start":{"line":200,"column":2},"end":{"line":203,"column":null}},"14":{"start":{"line":207,"column":37},"end":{"line":221,"column":null}},"15":{"start":{"line":218,"column":2},"end":{"line":220,"column":null}},"16":{"start":{"line":224,"column":34},"end":{"line":229,"column":null}},"17":{"start":{"line":225,"column":2},"end":{"line":228,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":132,"column":34},"end":{"line":132,"column":35}},"loc":{"start":{"line":144,"column":6},"end":{"line":150,"column":null}},"line":144},"1":{"name":"(anonymous_1)","decl":{"start":{"line":153,"column":33},"end":{"line":153,"column":34}},"loc":{"start":{"line":153,"column":49},"end":{"line":158,"column":null}},"line":153},"2":{"name":"(anonymous_2)","decl":{"start":{"line":161,"column":35},"end":{"line":161,"column":36}},"loc":{"start":{"line":161,"column":68},"end":{"line":167,"column":null}},"line":161},"3":{"name":"(anonymous_3)","decl":{"start":{"line":170,"column":35},"end":{"line":170,"column":null}},"loc":{"start":{"line":173,"column":5},"end":{"line":179,"column":null}},"line":173},"4":{"name":"(anonymous_4)","decl":{"start":{"line":182,"column":30},"end":{"line":182,"column":31}},"loc":{"start":{"line":182,"column":76},"end":{"line":188,"column":null}},"line":182},"5":{"name":"(anonymous_5)","decl":{"start":{"line":191,"column":33},"end":{"line":191,"column":34}},"loc":{"start":{"line":191,"column":49},"end":{"line":196,"column":null}},"line":191},"6":{"name":"(anonymous_6)","decl":{"start":{"line":199,"column":40},"end":{"line":199,"column":46}},"loc":{"start":{"line":199,"column":46},"end":{"line":204,"column":null}},"line":199},"7":{"name":"(anonymous_7)","decl":{"start":{"line":207,"column":37},"end":{"line":207,"column":38}},"loc":{"start":{"line":217,"column":6},"end":{"line":221,"column":null}},"line":217},"8":{"name":"(anonymous_8)","decl":{"start":{"line":224,"column":34},"end":{"line":224,"column":35}},"loc":{"start":{"line":224,"column":50},"end":{"line":229,"column":null}},"line":224}},"branchMap":{},"s":{"0":1,"1":2,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":2,"10":1,"11":1,"12":1,"13":1,"14":1,"15":2,"16":1,"17":1},"f":{"0":2,"1":1,"2":1,"3":1,"4":2,"5":1,"6":1,"7":2,"8":1},"b":{},"meta":{"lastBranch":0,"lastFunction":9,"lastStatement":18,"seen":{"s:132:34:150:Infinity":0,"f:132:34:132:35":0,"s:145:2:149:Infinity":1,"s:153:33:158:Infinity":2,"f:153:33:153:34":1,"s:154:2:157:Infinity":3,"s:161:35:167:Infinity":4,"f:161:35:161:36":2,"s:162:2:166:Infinity":5,"s:170:35:179:Infinity":6,"f:170:35:170:Infinity":3,"s:174:2:178:Infinity":7,"s:182:30:188:Infinity":8,"f:182:30:182:31":4,"s:183:2:187:Infinity":9,"s:191:33:196:Infinity":10,"f:191:33:191:34":5,"s:192:2:195:Infinity":11,"s:199:40:204:Infinity":12,"f:199:40:199:46":6,"s:200:2:203:Infinity":13,"s:207:37:221:Infinity":14,"f:207:37:207:38":7,"s:218:2:220:Infinity":15,"s:224:34:229:Infinity":16,"f:224:34:224:35":8,"s:225:2:228:Infinity":17}}} +,"/home/rovina/work/zsp-project/frontend/src/api/inventory.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/inventory.ts","statementMap":{"0":{"start":{"line":164,"column":32},"end":{"line":179,"column":null}},"1":{"start":{"line":176,"column":2},"end":{"line":178,"column":null}},"2":{"start":{"line":182,"column":34},"end":{"line":187,"column":null}},"3":{"start":{"line":183,"column":2},"end":{"line":186,"column":null}},"4":{"start":{"line":190,"column":38},"end":{"line":196,"column":null}},"5":{"start":{"line":191,"column":2},"end":{"line":195,"column":null}},"6":{"start":{"line":199,"column":38},"end":{"line":208,"column":null}},"7":{"start":{"line":203,"column":2},"end":{"line":207,"column":null}},"8":{"start":{"line":211,"column":38},"end":{"line":216,"column":null}},"9":{"start":{"line":212,"column":2},"end":{"line":215,"column":null}},"10":{"start":{"line":219,"column":32},"end":{"line":227,"column":null}},"11":{"start":{"line":224,"column":2},"end":{"line":226,"column":null}},"12":{"start":{"line":230,"column":31},"end":{"line":236,"column":null}},"13":{"start":{"line":231,"column":2},"end":{"line":235,"column":null}},"14":{"start":{"line":239,"column":31},"end":{"line":248,"column":null}},"15":{"start":{"line":243,"column":2},"end":{"line":247,"column":null}},"16":{"start":{"line":251,"column":31},"end":{"line":256,"column":null}},"17":{"start":{"line":252,"column":2},"end":{"line":255,"column":null}},"18":{"start":{"line":259,"column":35},"end":{"line":266,"column":null}},"19":{"start":{"line":263,"column":2},"end":{"line":265,"column":null}},"20":{"start":{"line":269,"column":35},"end":{"line":275,"column":null}},"21":{"start":{"line":270,"column":2},"end":{"line":274,"column":null}},"22":{"start":{"line":278,"column":43},"end":{"line":283,"column":null}},"23":{"start":{"line":279,"column":2},"end":{"line":282,"column":null}},"24":{"start":{"line":286,"column":35},"end":{"line":293,"column":null}},"25":{"start":{"line":290,"column":2},"end":{"line":292,"column":null}},"26":{"start":{"line":296,"column":36},"end":{"line":303,"column":null}},"27":{"start":{"line":297,"column":2},"end":{"line":302,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":164,"column":32},"end":{"line":164,"column":33}},"loc":{"start":{"line":175,"column":6},"end":{"line":179,"column":null}},"line":175},"1":{"name":"(anonymous_1)","decl":{"start":{"line":182,"column":34},"end":{"line":182,"column":35}},"loc":{"start":{"line":182,"column":50},"end":{"line":187,"column":null}},"line":182},"2":{"name":"(anonymous_2)","decl":{"start":{"line":190,"column":38},"end":{"line":190,"column":39}},"loc":{"start":{"line":190,"column":65},"end":{"line":196,"column":null}},"line":190},"3":{"name":"(anonymous_3)","decl":{"start":{"line":199,"column":38},"end":{"line":199,"column":null}},"loc":{"start":{"line":202,"column":5},"end":{"line":208,"column":null}},"line":202},"4":{"name":"(anonymous_4)","decl":{"start":{"line":211,"column":38},"end":{"line":211,"column":39}},"loc":{"start":{"line":211,"column":54},"end":{"line":216,"column":null}},"line":211},"5":{"name":"(anonymous_5)","decl":{"start":{"line":219,"column":32},"end":{"line":219,"column":33}},"loc":{"start":{"line":223,"column":6},"end":{"line":227,"column":null}},"line":223},"6":{"name":"(anonymous_6)","decl":{"start":{"line":230,"column":31},"end":{"line":230,"column":32}},"loc":{"start":{"line":230,"column":60},"end":{"line":236,"column":null}},"line":230},"7":{"name":"(anonymous_7)","decl":{"start":{"line":239,"column":31},"end":{"line":239,"column":null}},"loc":{"start":{"line":242,"column":5},"end":{"line":248,"column":null}},"line":242},"8":{"name":"(anonymous_8)","decl":{"start":{"line":251,"column":31},"end":{"line":251,"column":32}},"loc":{"start":{"line":251,"column":47},"end":{"line":256,"column":null}},"line":251},"9":{"name":"(anonymous_9)","decl":{"start":{"line":259,"column":35},"end":{"line":259,"column":36}},"loc":{"start":{"line":262,"column":6},"end":{"line":266,"column":null}},"line":262},"10":{"name":"(anonymous_10)","decl":{"start":{"line":269,"column":35},"end":{"line":269,"column":36}},"loc":{"start":{"line":269,"column":68},"end":{"line":275,"column":null}},"line":269},"11":{"name":"(anonymous_11)","decl":{"start":{"line":278,"column":43},"end":{"line":278,"column":49}},"loc":{"start":{"line":278,"column":49},"end":{"line":283,"column":null}},"line":278},"12":{"name":"(anonymous_12)","decl":{"start":{"line":286,"column":35},"end":{"line":286,"column":36}},"loc":{"start":{"line":289,"column":6},"end":{"line":293,"column":null}},"line":289},"13":{"name":"(anonymous_13)","decl":{"start":{"line":296,"column":36},"end":{"line":296,"column":37}},"loc":{"start":{"line":296,"column":56},"end":{"line":303,"column":null}},"line":296}},"branchMap":{},"s":{"0":1,"1":3,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":2,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":2,"20":1,"21":1,"22":1,"23":1,"24":1,"25":2,"26":1,"27":1},"f":{"0":3,"1":1,"2":1,"3":1,"4":1,"5":2,"6":1,"7":1,"8":1,"9":2,"10":1,"11":1,"12":2,"13":1},"b":{},"meta":{"lastBranch":0,"lastFunction":14,"lastStatement":28,"seen":{"s:164:32:179:Infinity":0,"f:164:32:164:33":0,"s:176:2:178:Infinity":1,"s:182:34:187:Infinity":2,"f:182:34:182:35":1,"s:183:2:186:Infinity":3,"s:190:38:196:Infinity":4,"f:190:38:190:39":2,"s:191:2:195:Infinity":5,"s:199:38:208:Infinity":6,"f:199:38:199:Infinity":3,"s:203:2:207:Infinity":7,"s:211:38:216:Infinity":8,"f:211:38:211:39":4,"s:212:2:215:Infinity":9,"s:219:32:227:Infinity":10,"f:219:32:219:33":5,"s:224:2:226:Infinity":11,"s:230:31:236:Infinity":12,"f:230:31:230:32":6,"s:231:2:235:Infinity":13,"s:239:31:248:Infinity":14,"f:239:31:239:Infinity":7,"s:243:2:247:Infinity":15,"s:251:31:256:Infinity":16,"f:251:31:251:32":8,"s:252:2:255:Infinity":17,"s:259:35:266:Infinity":18,"f:259:35:259:36":9,"s:263:2:265:Infinity":19,"s:269:35:275:Infinity":20,"f:269:35:269:36":10,"s:270:2:274:Infinity":21,"s:278:43:283:Infinity":22,"f:278:43:278:49":11,"s:279:2:282:Infinity":23,"s:286:35:293:Infinity":24,"f:286:35:286:36":12,"s:290:2:292:Infinity":25,"s:296:36:303:Infinity":26,"f:296:36:296:37":13,"s:297:2:302:Infinity":27}}} +,"/home/rovina/work/zsp-project/frontend/src/api/invoice.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/invoice.ts","statementMap":{"0":{"start":{"line":39,"column":35},"end":{"line":44,"column":null}},"1":{"start":{"line":40,"column":2},"end":{"line":43,"column":null}},"2":{"start":{"line":46,"column":37},"end":{"line":54,"column":null}},"3":{"start":{"line":49,"column":2},"end":{"line":53,"column":null}},"4":{"start":{"line":56,"column":37},"end":{"line":61,"column":null}},"5":{"start":{"line":57,"column":2},"end":{"line":60,"column":null}},"6":{"start":{"line":63,"column":41},"end":{"line":72,"column":null}},"7":{"start":{"line":67,"column":2},"end":{"line":71,"column":null}},"8":{"start":{"line":109,"column":37},"end":{"line":114,"column":null}},"9":{"start":{"line":110,"column":2},"end":{"line":113,"column":null}},"10":{"start":{"line":116,"column":39},"end":{"line":124,"column":null}},"11":{"start":{"line":119,"column":2},"end":{"line":123,"column":null}},"12":{"start":{"line":126,"column":39},"end":{"line":131,"column":null}},"13":{"start":{"line":127,"column":2},"end":{"line":130,"column":null}},"14":{"start":{"line":133,"column":43},"end":{"line":142,"column":null}},"15":{"start":{"line":137,"column":2},"end":{"line":141,"column":null}},"16":{"start":{"line":179,"column":38},"end":{"line":184,"column":null}},"17":{"start":{"line":180,"column":2},"end":{"line":183,"column":null}},"18":{"start":{"line":186,"column":40},"end":{"line":194,"column":null}},"19":{"start":{"line":189,"column":2},"end":{"line":193,"column":null}},"20":{"start":{"line":196,"column":40},"end":{"line":201,"column":null}},"21":{"start":{"line":197,"column":2},"end":{"line":200,"column":null}},"22":{"start":{"line":203,"column":44},"end":{"line":212,"column":null}},"23":{"start":{"line":207,"column":2},"end":{"line":211,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":39,"column":35},"end":{"line":39,"column":41}},"loc":{"start":{"line":39,"column":41},"end":{"line":44,"column":null}},"line":39},"1":{"name":"(anonymous_1)","decl":{"start":{"line":46,"column":37},"end":{"line":46,"column":null}},"loc":{"start":{"line":48,"column":5},"end":{"line":54,"column":null}},"line":48},"2":{"name":"(anonymous_2)","decl":{"start":{"line":56,"column":37},"end":{"line":56,"column":38}},"loc":{"start":{"line":56,"column":62},"end":{"line":61,"column":null}},"line":56},"3":{"name":"(anonymous_3)","decl":{"start":{"line":63,"column":41},"end":{"line":63,"column":null}},"loc":{"start":{"line":66,"column":5},"end":{"line":72,"column":null}},"line":66},"4":{"name":"(anonymous_4)","decl":{"start":{"line":109,"column":37},"end":{"line":109,"column":43}},"loc":{"start":{"line":109,"column":43},"end":{"line":114,"column":null}},"line":109},"5":{"name":"(anonymous_5)","decl":{"start":{"line":116,"column":39},"end":{"line":116,"column":null}},"loc":{"start":{"line":118,"column":5},"end":{"line":124,"column":null}},"line":118},"6":{"name":"(anonymous_6)","decl":{"start":{"line":126,"column":39},"end":{"line":126,"column":40}},"loc":{"start":{"line":126,"column":64},"end":{"line":131,"column":null}},"line":126},"7":{"name":"(anonymous_7)","decl":{"start":{"line":133,"column":43},"end":{"line":133,"column":null}},"loc":{"start":{"line":136,"column":5},"end":{"line":142,"column":null}},"line":136},"8":{"name":"(anonymous_8)","decl":{"start":{"line":179,"column":38},"end":{"line":179,"column":44}},"loc":{"start":{"line":179,"column":44},"end":{"line":184,"column":null}},"line":179},"9":{"name":"(anonymous_9)","decl":{"start":{"line":186,"column":40},"end":{"line":186,"column":null}},"loc":{"start":{"line":188,"column":5},"end":{"line":194,"column":null}},"line":188},"10":{"name":"(anonymous_10)","decl":{"start":{"line":196,"column":40},"end":{"line":196,"column":41}},"loc":{"start":{"line":196,"column":65},"end":{"line":201,"column":null}},"line":196},"11":{"name":"(anonymous_11)","decl":{"start":{"line":203,"column":44},"end":{"line":203,"column":null}},"loc":{"start":{"line":206,"column":5},"end":{"line":212,"column":null}},"line":206}},"branchMap":{},"s":{"0":1,"1":1,"2":1,"3":2,"4":1,"5":2,"6":1,"7":2,"8":1,"9":1,"10":1,"11":2,"12":1,"13":2,"14":1,"15":2,"16":1,"17":1,"18":1,"19":2,"20":1,"21":2,"22":1,"23":2},"f":{"0":1,"1":2,"2":2,"3":2,"4":1,"5":2,"6":2,"7":2,"8":1,"9":2,"10":2,"11":2},"b":{},"meta":{"lastBranch":0,"lastFunction":12,"lastStatement":24,"seen":{"s:39:35:44:Infinity":0,"f:39:35:39:41":0,"s:40:2:43:Infinity":1,"s:46:37:54:Infinity":2,"f:46:37:46:Infinity":1,"s:49:2:53:Infinity":3,"s:56:37:61:Infinity":4,"f:56:37:56:38":2,"s:57:2:60:Infinity":5,"s:63:41:72:Infinity":6,"f:63:41:63:Infinity":3,"s:67:2:71:Infinity":7,"s:109:37:114:Infinity":8,"f:109:37:109:43":4,"s:110:2:113:Infinity":9,"s:116:39:124:Infinity":10,"f:116:39:116:Infinity":5,"s:119:2:123:Infinity":11,"s:126:39:131:Infinity":12,"f:126:39:126:40":6,"s:127:2:130:Infinity":13,"s:133:43:142:Infinity":14,"f:133:43:133:Infinity":7,"s:137:2:141:Infinity":15,"s:179:38:184:Infinity":16,"f:179:38:179:44":8,"s:180:2:183:Infinity":17,"s:186:40:194:Infinity":18,"f:186:40:186:Infinity":9,"s:189:2:193:Infinity":19,"s:196:40:201:Infinity":20,"f:196:40:196:41":10,"s:197:2:200:Infinity":21,"s:203:44:212:Infinity":22,"f:203:44:203:Infinity":11,"s:207:2:211:Infinity":23}}} +,"/home/rovina/work/zsp-project/frontend/src/api/ownershipTransfer.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/ownershipTransfer.ts","statementMap":{"0":{"start":{"line":43,"column":39},"end":{"line":80,"column":null}},"1":{"start":{"line":54,"column":21},"end":{"line":54,"column":null}},"2":{"start":{"line":54,"column":49},"end":{"line":54,"column":79}},"3":{"start":{"line":56,"column":2},"end":{"line":74,"column":null}},"4":{"start":{"line":57,"column":21},"end":{"line":57,"column":null}},"5":{"start":{"line":58,"column":4},"end":{"line":58,"column":null}},"6":{"start":{"line":59,"column":4},"end":{"line":59,"column":null}},"7":{"start":{"line":60,"column":4},"end":{"line":60,"column":null}},"8":{"start":{"line":61,"column":4},"end":{"line":61,"column":null}},"9":{"start":{"line":62,"column":4},"end":{"line":62,"column":null}},"10":{"start":{"line":63,"column":4},"end":{"line":63,"column":null}},"11":{"start":{"line":64,"column":4},"end":{"line":64,"column":null}},"12":{"start":{"line":64,"column":22},"end":{"line":64,"column":null}},"13":{"start":{"line":65,"column":4},"end":{"line":65,"column":null}},"14":{"start":{"line":65,"column":31},"end":{"line":65,"column":61}},"15":{"start":{"line":66,"column":4},"end":{"line":73,"column":null}},"16":{"start":{"line":75,"column":2},"end":{"line":79,"column":null}},"17":{"start":{"line":83,"column":39},"end":{"line":85,"column":null}},"18":{"start":{"line":84,"column":2},"end":{"line":84,"column":null}},"19":{"start":{"line":88,"column":32},"end":{"line":100,"column":null}},"20":{"start":{"line":95,"column":2},"end":{"line":99,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":43,"column":39},"end":{"line":43,"column":40}},"loc":{"start":{"line":52,"column":6},"end":{"line":80,"column":null}},"line":52},"1":{"name":"(anonymous_1)","decl":{"start":{"line":54,"column":41},"end":{"line":54,"column":49}},"loc":{"start":{"line":54,"column":49},"end":{"line":54,"column":79}},"line":54},"2":{"name":"(anonymous_2)","decl":{"start":{"line":65,"column":23},"end":{"line":65,"column":31}},"loc":{"start":{"line":65,"column":31},"end":{"line":65,"column":61}},"line":65},"3":{"name":"(anonymous_3)","decl":{"start":{"line":83,"column":39},"end":{"line":83,"column":40}},"loc":{"start":{"line":83,"column":55},"end":{"line":85,"column":null}},"line":83},"4":{"name":"(anonymous_4)","decl":{"start":{"line":88,"column":32},"end":{"line":88,"column":33}},"loc":{"start":{"line":94,"column":6},"end":{"line":100,"column":null}},"line":94}},"branchMap":{"0":{"loc":{"start":{"line":54,"column":49},"end":{"line":54,"column":79}},"type":"binary-expr","locations":[{"start":{"line":54,"column":49},"end":{"line":54,"column":66}},{"start":{"line":54,"column":66},"end":{"line":54,"column":79}}],"line":54},"1":{"loc":{"start":{"line":56,"column":2},"end":{"line":74,"column":null}},"type":"if","locations":[{"start":{"line":56,"column":2},"end":{"line":74,"column":null}},{"start":{},"end":{}}],"line":56},"2":{"loc":{"start":{"line":64,"column":4},"end":{"line":64,"column":null}},"type":"if","locations":[{"start":{"line":64,"column":4},"end":{"line":64,"column":null}},{"start":{},"end":{}}],"line":64},"3":{"loc":{"start":{"line":78,"column":52},"end":{"line":78,"column":71}},"type":"binary-expr","locations":[{"start":{"line":78,"column":52},"end":{"line":78,"column":68}},{"start":{"line":78,"column":68},"end":{"line":78,"column":71}}],"line":78}},"s":{"0":1,"1":3,"2":4,"3":3,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":2,"17":1,"18":1,"19":1,"20":3},"f":{"0":3,"1":4,"2":1,"3":1,"4":3},"b":{"0":[4,1],"1":[1,2],"2":[1,0],"3":[2,1]},"meta":{"lastBranch":4,"lastFunction":5,"lastStatement":21,"seen":{"s:43:39:80:Infinity":0,"f:43:39:43:40":0,"s:54:21:54:Infinity":1,"f:54:41:54:49":1,"s:54:49:54:79":2,"b:54:49:54:66:54:66:54:79":0,"b:56:2:74:Infinity:undefined:undefined:undefined:undefined":1,"s:56:2:74:Infinity":3,"s:57:21:57:Infinity":4,"s:58:4:58:Infinity":5,"s:59:4:59:Infinity":6,"s:60:4:60:Infinity":7,"s:61:4:61:Infinity":8,"s:62:4:62:Infinity":9,"s:63:4:63:Infinity":10,"b:64:4:64:Infinity:undefined:undefined:undefined:undefined":2,"s:64:4:64:Infinity":11,"s:64:22:64:Infinity":12,"s:65:4:65:Infinity":13,"f:65:23:65:31":2,"s:65:31:65:61":14,"s:66:4:73:Infinity":15,"s:75:2:79:Infinity":16,"b:78:52:78:68:78:68:78:71":3,"s:83:39:85:Infinity":17,"f:83:39:83:40":3,"s:84:2:84:Infinity":18,"s:88:32:100:Infinity":19,"f:88:32:88:33":4,"s:95:2:99:Infinity":20}}} +,"/home/rovina/work/zsp-project/frontend/src/api/reconciliation.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/reconciliation.ts","statementMap":{"0":{"start":{"line":45,"column":34},"end":{"line":55,"column":null}},"1":{"start":{"line":50,"column":2},"end":{"line":54,"column":null}},"2":{"start":{"line":57,"column":33},"end":{"line":62,"column":null}},"3":{"start":{"line":58,"column":2},"end":{"line":61,"column":null}},"4":{"start":{"line":64,"column":36},"end":{"line":72,"column":null}},"5":{"start":{"line":67,"column":2},"end":{"line":71,"column":null}},"6":{"start":{"line":74,"column":36},"end":{"line":83,"column":null}},"7":{"start":{"line":78,"column":2},"end":{"line":82,"column":null}},"8":{"start":{"line":85,"column":36},"end":{"line":90,"column":null}},"9":{"start":{"line":86,"column":2},"end":{"line":89,"column":null}},"10":{"start":{"line":92,"column":40},"end":{"line":101,"column":null}},"11":{"start":{"line":96,"column":2},"end":{"line":100,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":45,"column":34},"end":{"line":45,"column":35}},"loc":{"start":{"line":49,"column":6},"end":{"line":55,"column":null}},"line":49},"1":{"name":"(anonymous_1)","decl":{"start":{"line":57,"column":33},"end":{"line":57,"column":34}},"loc":{"start":{"line":57,"column":58},"end":{"line":62,"column":null}},"line":57},"2":{"name":"(anonymous_2)","decl":{"start":{"line":64,"column":36},"end":{"line":64,"column":null}},"loc":{"start":{"line":66,"column":5},"end":{"line":72,"column":null}},"line":66},"3":{"name":"(anonymous_3)","decl":{"start":{"line":74,"column":36},"end":{"line":74,"column":null}},"loc":{"start":{"line":77,"column":5},"end":{"line":83,"column":null}},"line":77},"4":{"name":"(anonymous_4)","decl":{"start":{"line":85,"column":36},"end":{"line":85,"column":37}},"loc":{"start":{"line":85,"column":61},"end":{"line":90,"column":null}},"line":85},"5":{"name":"(anonymous_5)","decl":{"start":{"line":92,"column":40},"end":{"line":92,"column":null}},"loc":{"start":{"line":95,"column":5},"end":{"line":101,"column":null}},"line":95}},"branchMap":{},"s":{"0":1,"1":2,"2":1,"3":2,"4":1,"5":2,"6":1,"7":2,"8":1,"9":2,"10":1,"11":2},"f":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":2},"b":{},"meta":{"lastBranch":0,"lastFunction":6,"lastStatement":12,"seen":{"s:45:34:55:Infinity":0,"f:45:34:45:35":0,"s:50:2:54:Infinity":1,"s:57:33:62:Infinity":2,"f:57:33:57:34":1,"s:58:2:61:Infinity":3,"s:64:36:72:Infinity":4,"f:64:36:64:Infinity":2,"s:67:2:71:Infinity":5,"s:74:36:83:Infinity":6,"f:74:36:74:Infinity":3,"s:78:2:82:Infinity":7,"s:85:36:90:Infinity":8,"f:85:36:85:37":4,"s:86:2:89:Infinity":9,"s:92:40:101:Infinity":10,"f:92:40:92:Infinity":5,"s:96:2:100:Infinity":11}}} +,"/home/rovina/work/zsp-project/frontend/src/api/routes.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/routes.ts","statementMap":{"0":{"start":{"line":8,"column":30},"end":{"line":10,"column":null}},"1":{"start":{"line":9,"column":2},"end":{"line":9,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":8,"column":30},"end":{"line":8,"column":36}},"loc":{"start":{"line":8,"column":36},"end":{"line":10,"column":null}},"line":8}},"branchMap":{},"s":{"0":0,"1":0},"f":{"0":0},"b":{},"meta":{"lastBranch":0,"lastFunction":1,"lastStatement":2,"seen":{"s:8:30:10:Infinity":0,"f:8:30:8:36":0,"s:9:2:9:Infinity":1}}} +,"/home/rovina/work/zsp-project/frontend/src/api/settlement.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/settlement.ts","statementMap":{"0":{"start":{"line":51,"column":30},"end":{"line":61,"column":null}},"1":{"start":{"line":56,"column":2},"end":{"line":60,"column":null}},"2":{"start":{"line":63,"column":29},"end":{"line":68,"column":null}},"3":{"start":{"line":64,"column":2},"end":{"line":67,"column":null}},"4":{"start":{"line":70,"column":32},"end":{"line":76,"column":null}},"5":{"start":{"line":71,"column":2},"end":{"line":75,"column":null}},"6":{"start":{"line":78,"column":32},"end":{"line":87,"column":null}},"7":{"start":{"line":82,"column":2},"end":{"line":86,"column":null}},"8":{"start":{"line":89,"column":32},"end":{"line":94,"column":null}},"9":{"start":{"line":90,"column":2},"end":{"line":93,"column":null}},"10":{"start":{"line":96,"column":36},"end":{"line":102,"column":null}},"11":{"start":{"line":97,"column":2},"end":{"line":101,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":51,"column":30},"end":{"line":51,"column":31}},"loc":{"start":{"line":55,"column":6},"end":{"line":61,"column":null}},"line":55},"1":{"name":"(anonymous_1)","decl":{"start":{"line":63,"column":29},"end":{"line":63,"column":30}},"loc":{"start":{"line":63,"column":54},"end":{"line":68,"column":null}},"line":63},"2":{"name":"(anonymous_2)","decl":{"start":{"line":70,"column":32},"end":{"line":70,"column":33}},"loc":{"start":{"line":70,"column":70},"end":{"line":76,"column":null}},"line":70},"3":{"name":"(anonymous_3)","decl":{"start":{"line":78,"column":32},"end":{"line":78,"column":null}},"loc":{"start":{"line":81,"column":5},"end":{"line":87,"column":null}},"line":81},"4":{"name":"(anonymous_4)","decl":{"start":{"line":89,"column":32},"end":{"line":89,"column":33}},"loc":{"start":{"line":89,"column":57},"end":{"line":94,"column":null}},"line":89},"5":{"name":"(anonymous_5)","decl":{"start":{"line":96,"column":36},"end":{"line":96,"column":37}},"loc":{"start":{"line":96,"column":77},"end":{"line":102,"column":null}},"line":96}},"branchMap":{},"s":{"0":1,"1":2,"2":1,"3":2,"4":1,"5":2,"6":1,"7":2,"8":1,"9":2,"10":1,"11":2},"f":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":2},"b":{},"meta":{"lastBranch":0,"lastFunction":6,"lastStatement":12,"seen":{"s:51:30:61:Infinity":0,"f:51:30:51:31":0,"s:56:2:60:Infinity":1,"s:63:29:68:Infinity":2,"f:63:29:63:30":1,"s:64:2:67:Infinity":3,"s:70:32:76:Infinity":4,"f:70:32:70:33":2,"s:71:2:75:Infinity":5,"s:78:32:87:Infinity":6,"f:78:32:78:Infinity":3,"s:82:2:86:Infinity":7,"s:89:32:94:Infinity":8,"f:89:32:89:33":4,"s:90:2:93:Infinity":9,"s:96:36:102:Infinity":10,"f:96:36:96:37":5,"s:97:2:101:Infinity":11}}} +,"/home/rovina/work/zsp-project/frontend/src/api/user.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/user.ts","statementMap":{"0":{"start":{"line":43,"column":24},"end":{"line":45,"column":null}},"1":{"start":{"line":44,"column":2},"end":{"line":44,"column":null}},"2":{"start":{"line":48,"column":31},"end":{"line":50,"column":null}},"3":{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},"4":{"start":{"line":52,"column":27},"end":{"line":54,"column":null}},"5":{"start":{"line":53,"column":2},"end":{"line":53,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":43,"column":24},"end":{"line":43,"column":25}},"loc":{"start":{"line":43,"column":43},"end":{"line":45,"column":null}},"line":43},"1":{"name":"(anonymous_1)","decl":{"start":{"line":48,"column":31},"end":{"line":48,"column":32}},"loc":{"start":{"line":48,"column":50},"end":{"line":50,"column":null}},"line":48},"2":{"name":"(anonymous_2)","decl":{"start":{"line":52,"column":27},"end":{"line":52,"column":28}},"loc":{"start":{"line":52,"column":46},"end":{"line":54,"column":null}},"line":52}},"branchMap":{},"s":{"0":1,"1":3,"2":1,"3":2,"4":1,"5":2},"f":{"0":3,"1":2,"2":2},"b":{},"meta":{"lastBranch":0,"lastFunction":3,"lastStatement":6,"seen":{"s:43:24:45:Infinity":0,"f:43:24:43:25":0,"s:44:2:44:Infinity":1,"s:48:31:50:Infinity":2,"f:48:31:48:32":1,"s:49:2:49:Infinity":3,"s:52:27:54:Infinity":4,"f:52:27:52:28":2,"s:53:2:53:Infinity":5}}} +,"/home/rovina/work/zsp-project/frontend/src/api/zspContract.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/zspContract.ts","statementMap":{"0":{"start":{"line":86,"column":37},"end":{"line":91,"column":null}},"1":{"start":{"line":87,"column":2},"end":{"line":90,"column":null}},"2":{"start":{"line":94,"column":39},"end":{"line":99,"column":null}},"3":{"start":{"line":98,"column":2},"end":{"line":98,"column":null}},"4":{"start":{"line":102,"column":39},"end":{"line":107,"column":null}},"5":{"start":{"line":106,"column":2},"end":{"line":106,"column":null}},"6":{"start":{"line":110,"column":39},"end":{"line":112,"column":null}},"7":{"start":{"line":111,"column":2},"end":{"line":111,"column":null}},"8":{"start":{"line":117,"column":31},"end":{"line":123,"column":null}},"9":{"start":{"line":118,"column":2},"end":{"line":122,"column":null}},"10":{"start":{"line":126,"column":30},"end":{"line":131,"column":null}},"11":{"start":{"line":127,"column":2},"end":{"line":130,"column":null}},"12":{"start":{"line":134,"column":33},"end":{"line":139,"column":null}},"13":{"start":{"line":135,"column":2},"end":{"line":138,"column":null}},"14":{"start":{"line":142,"column":33},"end":{"line":144,"column":null}},"15":{"start":{"line":143,"column":2},"end":{"line":143,"column":null}},"16":{"start":{"line":147,"column":33},"end":{"line":161,"column":null}},"17":{"start":{"line":158,"column":2},"end":{"line":160,"column":null}},"18":{"start":{"line":166,"column":37},"end":{"line":171,"column":null}},"19":{"start":{"line":167,"column":2},"end":{"line":170,"column":null}},"20":{"start":{"line":174,"column":36},"end":{"line":179,"column":null}},"21":{"start":{"line":175,"column":2},"end":{"line":178,"column":null}},"22":{"start":{"line":182,"column":39},"end":{"line":187,"column":null}},"23":{"start":{"line":186,"column":2},"end":{"line":186,"column":null}},"24":{"start":{"line":190,"column":39},"end":{"line":192,"column":null}},"25":{"start":{"line":191,"column":2},"end":{"line":191,"column":null}},"26":{"start":{"line":195,"column":39},"end":{"line":197,"column":null}},"27":{"start":{"line":196,"column":2},"end":{"line":196,"column":null}},"28":{"start":{"line":200,"column":45},"end":{"line":204,"column":null}},"29":{"start":{"line":203,"column":2},"end":{"line":203,"column":null}},"30":{"start":{"line":207,"column":49},"end":{"line":219,"column":null}},"31":{"start":{"line":211,"column":2},"end":{"line":218,"column":null}},"32":{"start":{"line":222,"column":47},"end":{"line":227,"column":null}},"33":{"start":{"line":223,"column":2},"end":{"line":226,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":86,"column":37},"end":{"line":86,"column":43}},"loc":{"start":{"line":86,"column":43},"end":{"line":91,"column":null}},"line":86},"1":{"name":"(anonymous_1)","decl":{"start":{"line":94,"column":39},"end":{"line":94,"column":40}},"loc":{"start":{"line":97,"column":6},"end":{"line":99,"column":null}},"line":97},"2":{"name":"(anonymous_2)","decl":{"start":{"line":102,"column":39},"end":{"line":102,"column":null}},"loc":{"start":{"line":105,"column":5},"end":{"line":107,"column":null}},"line":105},"3":{"name":"(anonymous_3)","decl":{"start":{"line":110,"column":39},"end":{"line":110,"column":40}},"loc":{"start":{"line":110,"column":64},"end":{"line":112,"column":null}},"line":110},"4":{"name":"(anonymous_4)","decl":{"start":{"line":117,"column":31},"end":{"line":117,"column":32}},"loc":{"start":{"line":117,"column":54},"end":{"line":123,"column":null}},"line":117},"5":{"name":"(anonymous_5)","decl":{"start":{"line":126,"column":30},"end":{"line":126,"column":31}},"loc":{"start":{"line":126,"column":55},"end":{"line":131,"column":null}},"line":126},"6":{"name":"(anonymous_6)","decl":{"start":{"line":134,"column":33},"end":{"line":134,"column":34}},"loc":{"start":{"line":134,"column":57},"end":{"line":139,"column":null}},"line":134},"7":{"name":"(anonymous_7)","decl":{"start":{"line":142,"column":33},"end":{"line":142,"column":34}},"loc":{"start":{"line":142,"column":58},"end":{"line":144,"column":null}},"line":142},"8":{"name":"(anonymous_8)","decl":{"start":{"line":147,"column":33},"end":{"line":147,"column":null}},"loc":{"start":{"line":157,"column":5},"end":{"line":161,"column":null}},"line":157},"9":{"name":"(anonymous_9)","decl":{"start":{"line":166,"column":37},"end":{"line":166,"column":38}},"loc":{"start":{"line":166,"column":74},"end":{"line":171,"column":null}},"line":166},"10":{"name":"(anonymous_10)","decl":{"start":{"line":174,"column":36},"end":{"line":174,"column":37}},"loc":{"start":{"line":174,"column":61},"end":{"line":179,"column":null}},"line":174},"11":{"name":"(anonymous_11)","decl":{"start":{"line":182,"column":39},"end":{"line":182,"column":null}},"loc":{"start":{"line":185,"column":5},"end":{"line":187,"column":null}},"line":185},"12":{"name":"(anonymous_12)","decl":{"start":{"line":190,"column":39},"end":{"line":190,"column":40}},"loc":{"start":{"line":190,"column":102},"end":{"line":192,"column":null}},"line":190},"13":{"name":"(anonymous_13)","decl":{"start":{"line":195,"column":39},"end":{"line":195,"column":40}},"loc":{"start":{"line":195,"column":64},"end":{"line":197,"column":null}},"line":195},"14":{"name":"(anonymous_14)","decl":{"start":{"line":200,"column":45},"end":{"line":200,"column":null}},"loc":{"start":{"line":202,"column":5},"end":{"line":204,"column":null}},"line":202},"15":{"name":"(anonymous_15)","decl":{"start":{"line":207,"column":49},"end":{"line":207,"column":null}},"loc":{"start":{"line":210,"column":5},"end":{"line":219,"column":null}},"line":210},"16":{"name":"(anonymous_16)","decl":{"start":{"line":222,"column":47},"end":{"line":222,"column":48}},"loc":{"start":{"line":222,"column":84},"end":{"line":227,"column":null}},"line":222}},"branchMap":{"0":{"loc":{"start":{"line":121,"column":14},"end":{"line":121,"column":43}},"type":"cond-expr","locations":[{"start":{"line":121,"column":25},"end":{"line":121,"column":40}},{"start":{"line":121,"column":40},"end":{"line":121,"column":43}}],"line":121}},"s":{"0":1,"1":1,"2":1,"3":2,"4":1,"5":2,"6":1,"7":2,"8":1,"9":2,"10":1,"11":2,"12":1,"13":1,"14":1,"15":2,"16":1,"17":2,"18":1,"19":2,"20":1,"21":2,"22":1,"23":2,"24":1,"25":2,"26":1,"27":2,"28":1,"29":1,"30":1,"31":2,"32":1,"33":2},"f":{"0":1,"1":2,"2":2,"3":2,"4":2,"5":2,"6":1,"7":2,"8":2,"9":2,"10":2,"11":2,"12":2,"13":2,"14":1,"15":2,"16":2},"b":{"0":[1,1]},"meta":{"lastBranch":1,"lastFunction":17,"lastStatement":34,"seen":{"s:86:37:91:Infinity":0,"f:86:37:86:43":0,"s:87:2:90:Infinity":1,"s:94:39:99:Infinity":2,"f:94:39:94:40":1,"s:98:2:98:Infinity":3,"s:102:39:107:Infinity":4,"f:102:39:102:Infinity":2,"s:106:2:106:Infinity":5,"s:110:39:112:Infinity":6,"f:110:39:110:40":3,"s:111:2:111:Infinity":7,"s:117:31:123:Infinity":8,"f:117:31:117:32":4,"s:118:2:122:Infinity":9,"b:121:25:121:40:121:40:121:43":0,"s:126:30:131:Infinity":10,"f:126:30:126:31":5,"s:127:2:130:Infinity":11,"s:134:33:139:Infinity":12,"f:134:33:134:34":6,"s:135:2:138:Infinity":13,"s:142:33:144:Infinity":14,"f:142:33:142:34":7,"s:143:2:143:Infinity":15,"s:147:33:161:Infinity":16,"f:147:33:147:Infinity":8,"s:158:2:160:Infinity":17,"s:166:37:171:Infinity":18,"f:166:37:166:38":9,"s:167:2:170:Infinity":19,"s:174:36:179:Infinity":20,"f:174:36:174:37":10,"s:175:2:178:Infinity":21,"s:182:39:187:Infinity":22,"f:182:39:182:Infinity":11,"s:186:2:186:Infinity":23,"s:190:39:192:Infinity":24,"f:190:39:190:40":12,"s:191:2:191:Infinity":25,"s:195:39:197:Infinity":26,"f:195:39:195:40":13,"s:196:2:196:Infinity":27,"s:200:45:204:Infinity":28,"f:200:45:200:Infinity":14,"s:203:2:203:Infinity":29,"s:207:49:219:Infinity":30,"f:207:49:207:Infinity":15,"s:211:2:218:Infinity":31,"s:222:47:227:Infinity":32,"f:222:47:222:48":16,"s:223:2:226:Infinity":33}}} +,"/home/rovina/work/zsp-project/frontend/src/api/zspFinances.ts": {"path":"/home/rovina/work/zsp-project/frontend/src/api/zspFinances.ts","statementMap":{"0":{"start":{"line":167,"column":33},"end":{"line":172,"column":null}},"1":{"start":{"line":168,"column":2},"end":{"line":171,"column":null}},"2":{"start":{"line":174,"column":32},"end":{"line":179,"column":null}},"3":{"start":{"line":175,"column":2},"end":{"line":178,"column":null}},"4":{"start":{"line":181,"column":35},"end":{"line":187,"column":null}},"5":{"start":{"line":182,"column":2},"end":{"line":186,"column":null}},"6":{"start":{"line":189,"column":35},"end":{"line":198,"column":null}},"7":{"start":{"line":193,"column":2},"end":{"line":197,"column":null}},"8":{"start":{"line":200,"column":35},"end":{"line":205,"column":null}},"9":{"start":{"line":201,"column":2},"end":{"line":204,"column":null}},"10":{"start":{"line":209,"column":30},"end":{"line":214,"column":null}},"11":{"start":{"line":210,"column":2},"end":{"line":213,"column":null}},"12":{"start":{"line":216,"column":29},"end":{"line":221,"column":null}},"13":{"start":{"line":217,"column":2},"end":{"line":220,"column":null}},"14":{"start":{"line":223,"column":32},"end":{"line":229,"column":null}},"15":{"start":{"line":224,"column":2},"end":{"line":228,"column":null}},"16":{"start":{"line":231,"column":32},"end":{"line":240,"column":null}},"17":{"start":{"line":235,"column":2},"end":{"line":239,"column":null}},"18":{"start":{"line":242,"column":32},"end":{"line":247,"column":null}},"19":{"start":{"line":243,"column":2},"end":{"line":246,"column":null}},"20":{"start":{"line":251,"column":30},"end":{"line":256,"column":null}},"21":{"start":{"line":252,"column":2},"end":{"line":255,"column":null}},"22":{"start":{"line":258,"column":29},"end":{"line":263,"column":null}},"23":{"start":{"line":259,"column":2},"end":{"line":262,"column":null}},"24":{"start":{"line":265,"column":32},"end":{"line":271,"column":null}},"25":{"start":{"line":266,"column":2},"end":{"line":270,"column":null}},"26":{"start":{"line":273,"column":32},"end":{"line":282,"column":null}},"27":{"start":{"line":277,"column":2},"end":{"line":281,"column":null}},"28":{"start":{"line":284,"column":32},"end":{"line":289,"column":null}},"29":{"start":{"line":285,"column":2},"end":{"line":288,"column":null}},"30":{"start":{"line":293,"column":33},"end":{"line":299,"column":null}},"31":{"start":{"line":294,"column":2},"end":{"line":298,"column":null}},"32":{"start":{"line":301,"column":32},"end":{"line":306,"column":null}},"33":{"start":{"line":302,"column":2},"end":{"line":305,"column":null}},"34":{"start":{"line":308,"column":35},"end":{"line":314,"column":null}},"35":{"start":{"line":309,"column":2},"end":{"line":313,"column":null}},"36":{"start":{"line":316,"column":35},"end":{"line":325,"column":null}},"37":{"start":{"line":320,"column":2},"end":{"line":324,"column":null}},"38":{"start":{"line":327,"column":35},"end":{"line":332,"column":null}},"39":{"start":{"line":328,"column":2},"end":{"line":331,"column":null}},"40":{"start":{"line":336,"column":32},"end":{"line":348,"column":null}},"41":{"start":{"line":340,"column":41},"end":{"line":340,"column":null}},"42":{"start":{"line":341,"column":2},"end":{"line":341,"column":null}},"43":{"start":{"line":341,"column":18},"end":{"line":341,"column":null}},"44":{"start":{"line":342,"column":2},"end":{"line":342,"column":null}},"45":{"start":{"line":342,"column":20},"end":{"line":342,"column":null}},"46":{"start":{"line":343,"column":2},"end":{"line":347,"column":null}},"47":{"start":{"line":350,"column":31},"end":{"line":355,"column":null}},"48":{"start":{"line":351,"column":2},"end":{"line":354,"column":null}},"49":{"start":{"line":357,"column":34},"end":{"line":363,"column":null}},"50":{"start":{"line":358,"column":2},"end":{"line":362,"column":null}},"51":{"start":{"line":365,"column":34},"end":{"line":374,"column":null}},"52":{"start":{"line":369,"column":2},"end":{"line":373,"column":null}},"53":{"start":{"line":376,"column":34},"end":{"line":381,"column":null}},"54":{"start":{"line":377,"column":2},"end":{"line":380,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":167,"column":33},"end":{"line":167,"column":39}},"loc":{"start":{"line":167,"column":39},"end":{"line":172,"column":null}},"line":167},"1":{"name":"(anonymous_1)","decl":{"start":{"line":174,"column":32},"end":{"line":174,"column":33}},"loc":{"start":{"line":174,"column":57},"end":{"line":179,"column":null}},"line":174},"2":{"name":"(anonymous_2)","decl":{"start":{"line":181,"column":35},"end":{"line":181,"column":36}},"loc":{"start":{"line":181,"column":76},"end":{"line":187,"column":null}},"line":181},"3":{"name":"(anonymous_3)","decl":{"start":{"line":189,"column":35},"end":{"line":189,"column":null}},"loc":{"start":{"line":192,"column":5},"end":{"line":198,"column":null}},"line":192},"4":{"name":"(anonymous_4)","decl":{"start":{"line":200,"column":35},"end":{"line":200,"column":36}},"loc":{"start":{"line":200,"column":60},"end":{"line":205,"column":null}},"line":200},"5":{"name":"(anonymous_5)","decl":{"start":{"line":209,"column":30},"end":{"line":209,"column":36}},"loc":{"start":{"line":209,"column":36},"end":{"line":214,"column":null}},"line":209},"6":{"name":"(anonymous_6)","decl":{"start":{"line":216,"column":29},"end":{"line":216,"column":30}},"loc":{"start":{"line":216,"column":54},"end":{"line":221,"column":null}},"line":216},"7":{"name":"(anonymous_7)","decl":{"start":{"line":223,"column":32},"end":{"line":223,"column":33}},"loc":{"start":{"line":223,"column":70},"end":{"line":229,"column":null}},"line":223},"8":{"name":"(anonymous_8)","decl":{"start":{"line":231,"column":32},"end":{"line":231,"column":null}},"loc":{"start":{"line":234,"column":5},"end":{"line":240,"column":null}},"line":234},"9":{"name":"(anonymous_9)","decl":{"start":{"line":242,"column":32},"end":{"line":242,"column":33}},"loc":{"start":{"line":242,"column":57},"end":{"line":247,"column":null}},"line":242},"10":{"name":"(anonymous_10)","decl":{"start":{"line":251,"column":30},"end":{"line":251,"column":36}},"loc":{"start":{"line":251,"column":36},"end":{"line":256,"column":null}},"line":251},"11":{"name":"(anonymous_11)","decl":{"start":{"line":258,"column":29},"end":{"line":258,"column":30}},"loc":{"start":{"line":258,"column":54},"end":{"line":263,"column":null}},"line":258},"12":{"name":"(anonymous_12)","decl":{"start":{"line":265,"column":32},"end":{"line":265,"column":33}},"loc":{"start":{"line":265,"column":70},"end":{"line":271,"column":null}},"line":265},"13":{"name":"(anonymous_13)","decl":{"start":{"line":273,"column":32},"end":{"line":273,"column":null}},"loc":{"start":{"line":276,"column":5},"end":{"line":282,"column":null}},"line":276},"14":{"name":"(anonymous_14)","decl":{"start":{"line":284,"column":32},"end":{"line":284,"column":33}},"loc":{"start":{"line":284,"column":57},"end":{"line":289,"column":null}},"line":284},"15":{"name":"(anonymous_15)","decl":{"start":{"line":293,"column":33},"end":{"line":293,"column":34}},"loc":{"start":{"line":293,"column":58},"end":{"line":299,"column":null}},"line":293},"16":{"name":"(anonymous_16)","decl":{"start":{"line":301,"column":32},"end":{"line":301,"column":33}},"loc":{"start":{"line":301,"column":57},"end":{"line":306,"column":null}},"line":301},"17":{"name":"(anonymous_17)","decl":{"start":{"line":308,"column":35},"end":{"line":308,"column":36}},"loc":{"start":{"line":308,"column":76},"end":{"line":314,"column":null}},"line":308},"18":{"name":"(anonymous_18)","decl":{"start":{"line":316,"column":35},"end":{"line":316,"column":null}},"loc":{"start":{"line":319,"column":5},"end":{"line":325,"column":null}},"line":319},"19":{"name":"(anonymous_19)","decl":{"start":{"line":327,"column":35},"end":{"line":327,"column":36}},"loc":{"start":{"line":327,"column":60},"end":{"line":332,"column":null}},"line":327},"20":{"name":"(anonymous_20)","decl":{"start":{"line":336,"column":32},"end":{"line":336,"column":null}},"loc":{"start":{"line":339,"column":5},"end":{"line":348,"column":null}},"line":339},"21":{"name":"(anonymous_21)","decl":{"start":{"line":350,"column":31},"end":{"line":350,"column":32}},"loc":{"start":{"line":350,"column":56},"end":{"line":355,"column":null}},"line":350},"22":{"name":"(anonymous_22)","decl":{"start":{"line":357,"column":34},"end":{"line":357,"column":35}},"loc":{"start":{"line":357,"column":74},"end":{"line":363,"column":null}},"line":357},"23":{"name":"(anonymous_23)","decl":{"start":{"line":365,"column":34},"end":{"line":365,"column":null}},"loc":{"start":{"line":368,"column":5},"end":{"line":374,"column":null}},"line":368},"24":{"name":"(anonymous_24)","decl":{"start":{"line":376,"column":34},"end":{"line":376,"column":35}},"loc":{"start":{"line":376,"column":59},"end":{"line":381,"column":null}},"line":376}},"branchMap":{"0":{"loc":{"start":{"line":297,"column":14},"end":{"line":297,"column":47}},"type":"cond-expr","locations":[{"start":{"line":297,"column":27},"end":{"line":297,"column":44}},{"start":{"line":297,"column":44},"end":{"line":297,"column":47}}],"line":297},"1":{"loc":{"start":{"line":341,"column":2},"end":{"line":341,"column":null}},"type":"if","locations":[{"start":{"line":341,"column":2},"end":{"line":341,"column":null}},{"start":{},"end":{}}],"line":341},"2":{"loc":{"start":{"line":342,"column":2},"end":{"line":342,"column":null}},"type":"if","locations":[{"start":{"line":342,"column":2},"end":{"line":342,"column":null}},{"start":{},"end":{}}],"line":342}},"s":{"0":1,"1":1,"2":1,"3":2,"4":1,"5":1,"6":1,"7":2,"8":1,"9":2,"10":1,"11":1,"12":1,"13":2,"14":1,"15":1,"16":1,"17":2,"18":1,"19":2,"20":1,"21":1,"22":1,"23":2,"24":1,"25":1,"26":1,"27":2,"28":1,"29":2,"30":1,"31":2,"32":1,"33":2,"34":1,"35":1,"36":1,"37":2,"38":1,"39":2,"40":1,"41":3,"42":3,"43":2,"44":3,"45":1,"46":3,"47":1,"48":2,"49":1,"50":1,"51":1,"52":2,"53":1,"54":2},"f":{"0":1,"1":2,"2":1,"3":2,"4":2,"5":1,"6":2,"7":1,"8":2,"9":2,"10":1,"11":2,"12":1,"13":2,"14":2,"15":2,"16":2,"17":1,"18":2,"19":2,"20":3,"21":2,"22":1,"23":2,"24":2},"b":{"0":[1,1],"1":[2,1],"2":[1,2]},"meta":{"lastBranch":3,"lastFunction":25,"lastStatement":55,"seen":{"s:167:33:172:Infinity":0,"f:167:33:167:39":0,"s:168:2:171:Infinity":1,"s:174:32:179:Infinity":2,"f:174:32:174:33":1,"s:175:2:178:Infinity":3,"s:181:35:187:Infinity":4,"f:181:35:181:36":2,"s:182:2:186:Infinity":5,"s:189:35:198:Infinity":6,"f:189:35:189:Infinity":3,"s:193:2:197:Infinity":7,"s:200:35:205:Infinity":8,"f:200:35:200:36":4,"s:201:2:204:Infinity":9,"s:209:30:214:Infinity":10,"f:209:30:209:36":5,"s:210:2:213:Infinity":11,"s:216:29:221:Infinity":12,"f:216:29:216:30":6,"s:217:2:220:Infinity":13,"s:223:32:229:Infinity":14,"f:223:32:223:33":7,"s:224:2:228:Infinity":15,"s:231:32:240:Infinity":16,"f:231:32:231:Infinity":8,"s:235:2:239:Infinity":17,"s:242:32:247:Infinity":18,"f:242:32:242:33":9,"s:243:2:246:Infinity":19,"s:251:30:256:Infinity":20,"f:251:30:251:36":10,"s:252:2:255:Infinity":21,"s:258:29:263:Infinity":22,"f:258:29:258:30":11,"s:259:2:262:Infinity":23,"s:265:32:271:Infinity":24,"f:265:32:265:33":12,"s:266:2:270:Infinity":25,"s:273:32:282:Infinity":26,"f:273:32:273:Infinity":13,"s:277:2:281:Infinity":27,"s:284:32:289:Infinity":28,"f:284:32:284:33":14,"s:285:2:288:Infinity":29,"s:293:33:299:Infinity":30,"f:293:33:293:34":15,"s:294:2:298:Infinity":31,"b:297:27:297:44:297:44:297:47":0,"s:301:32:306:Infinity":32,"f:301:32:301:33":16,"s:302:2:305:Infinity":33,"s:308:35:314:Infinity":34,"f:308:35:308:36":17,"s:309:2:313:Infinity":35,"s:316:35:325:Infinity":36,"f:316:35:316:Infinity":18,"s:320:2:324:Infinity":37,"s:327:35:332:Infinity":38,"f:327:35:327:36":19,"s:328:2:331:Infinity":39,"s:336:32:348:Infinity":40,"f:336:32:336:Infinity":20,"s:340:41:340:Infinity":41,"b:341:2:341:Infinity:undefined:undefined:undefined:undefined":1,"s:341:2:341:Infinity":42,"s:341:18:341:Infinity":43,"b:342:2:342:Infinity:undefined:undefined:undefined:undefined":2,"s:342:2:342:Infinity":44,"s:342:20:342:Infinity":45,"s:343:2:347:Infinity":46,"s:350:31:355:Infinity":47,"f:350:31:350:32":21,"s:351:2:354:Infinity":48,"s:357:34:363:Infinity":49,"f:357:34:357:35":22,"s:358:2:362:Infinity":50,"s:365:34:374:Infinity":51,"f:365:34:365:Infinity":23,"s:369:2:373:Infinity":52,"s:376:34:381:Infinity":53,"f:376:34:376:35":24,"s:377:2:380:Infinity":54}}} +} diff --git a/frontend/coverage/deliveryDetails.ts.html b/frontend/coverage/deliveryDetails.ts.html new file mode 100644 index 0000000..0e01350 --- /dev/null +++ b/frontend/coverage/deliveryDetails.ts.html @@ -0,0 +1,772 @@ + + + + + + Code coverage report for deliveryDetails.ts + + + + + + + + + +
+
+

All files deliveryDetails.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 100% + Lines + 18/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +1x +1x +  +  +  +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+// ============ 类型定义 ============
+/** 提货明细项 */
+export type DeliveryDetail = {
+  id: string;
+  outboundNumber: string; // 出库单号
+  deliveryOrderNumber: string; // 提货委托单号
+  blNumber: string; // 提单号
+  licensePlate: string; // 车牌号
+  driverName: string; // 司机姓名
+  driverPhone: string; // 司机电话
+  commodity: string; // 商品品类
+  commodityCode: string; // 商品编码
+  batchNumber: string; // 批次号
+  quantity: number; // 出库数量
+  unit: string; // 单位
+  unitPrice: number; // 单价
+  totalAmount: number; // 总金额
+  outboundDate: string; // 出库日期
+  outboundTime: string; // 出库时间
+  warehouse: string; // 仓库
+  location: string; // 库位
+  operator: string; // 操作员
+  outboundStatus: "pending" | "in_progress" | "completed" | "cancelled"; // 出库状态
+  receiptStatus: "pending" | "partial" | "completed" | "discrepancy"; // 收货状态
+  receivedQuantity: number; // 已收货数量
+  receiptDate: string; // 收货日期
+  receiver: string; // 收货人
+  remark?: string; // 备注
+  createTime: string; // 创建时间
+  updateTime: string; // 更新时间
+};
+ 
+/** 出库单表单数据 */
+export type OutboundOrderFormData = {
+  deliveryOrderNumber: string; // 提货委托单号
+  blNumber: string; // 提单号
+  licensePlate: string; // 车牌号
+  driverName: string; // 司机姓名
+  driverPhone: string; // 司机电话
+  items: OutboundItem[]; // 出库明细项
+  outboundDate: string; // 出库日期
+  warehouse: string; // 仓库
+  operator: string; // 操作员
+  remark?: string; // 备注
+};
+ 
+/** 出库明细项 */
+export type OutboundItem = {
+  commodity: string; // 商品品类
+  commodityCode?: string; // 商品编码
+  batchNumber?: string; // 批次号
+  quantity: number; // 数量
+  unit: string; // 单位
+  unitPrice: number; // 单价
+  location?: string; // 库位
+  remark?: string; // 备注
+};
+ 
+/** 收货确认表单数据 */
+export type ReceiptConfirmFormData = {
+  receiptDate: string; // 收货日期
+  receivedQuantity: number; // 实收数量
+  receiver: string; // 收货人
+  discrepancyReason?: string; // 差异原因
+  remark?: string; // 备注
+};
+ 
+/** 收货历史记录 */
+export type ReceiptHistory = {
+  id: string;
+  deliveryDetailId: string;
+  receiptDate: string;
+  receivedQuantity: number;
+  receiver: string;
+  remark?: string;
+  createTime: string;
+};
+ 
+/** API基础响应 */
+ 
+/** 提货明细列表响应 */
+export type DeliveryDetailsResponse = BaseResponse<{
+  items: DeliveryDetail[];
+  total: number;
+  page: number;
+  pageSize: number;
+}>;
+ 
+/** 提货明细详情响应 */
+export type DeliveryDetailResponse = BaseResponse<DeliveryDetail>;
+ 
+/** 创建出库单响应 */
+export type CreateOutboundOrderResponse = BaseResponse<{
+  items: DeliveryDetail[];
+  count: number;
+}>;
+ 
+/** 更新出库单响应 */
+export type UpdateOutboundOrderResponse = BaseResponse<DeliveryDetail>;
+ 
+/** 收货确认响应 */
+export type ConfirmReceiptResponse = BaseResponse<{
+  deliveryDetail: DeliveryDetail;
+  receiptRecord: ReceiptHistory;
+}>;
+ 
+/** 收货历史响应 */
+export type ReceiptHistoryResponse = BaseResponse<ReceiptHistory[]>;
+ 
+/** 下载模板响应 */
+export type DownloadTemplateResponse = BaseResponse<{
+  fileName: string;
+  url: string;
+}>;
+ 
+/** 导出数据响应 */
+export type ExportDataResponse = BaseResponse<{
+  fileName: string;
+  url: string;
+}>;
+ 
+/** 打印提货单响应 */
+export type PrintDeliveryOrderResponse = BaseResponse<{
+  fileName: string;
+  url: string;
+}>;
+ 
+// ============ API函数 ============
+/** 获取提货明细列表 */
+export const getDeliveryDetails = (params?: {
+  page?: number;
+  pageSize?: number;
+  outboundNumber?: string;
+  deliveryOrderNumber?: string;
+  blNumber?: string;
+  licensePlate?: string;
+  commodity?: string;
+  outboundStatus?: string;
+  receiptStatus?: string;
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<DeliveryDetailsResponse>(
+    "get",
+    "/api/delivery-details/list",
+    { params }
+  );
+};
+ 
+/** 获取提货明细详情 */
+export const getDeliveryDetail = (id: string) => {
+  return http.request<DeliveryDetailResponse>(
+    "get",
+    `/api/delivery-details/detail/${id}`
+  );
+};
+ 
+/** 新增出库单 */
+export const createOutboundOrder = (data: OutboundOrderFormData) => {
+  return http.request<CreateOutboundOrderResponse>(
+    "post",
+    "/api/delivery-details/create",
+    { data }
+  );
+};
+ 
+/** 更新出库单 */
+export const updateOutboundOrder = (
+  id: string,
+  data: Partial<OutboundOrderFormData>
+) => {
+  return http.request<UpdateOutboundOrderResponse>(
+    "put",
+    `/api/delivery-details/update/${id}`,
+    { data }
+  );
+};
+ 
+/** 收货确认 */
+export const confirmReceipt = (id: string, data: ReceiptConfirmFormData) => {
+  return http.request<ConfirmReceiptResponse>(
+    "put",
+    `/api/delivery-details/confirm-receipt/${id}`,
+    { data }
+  );
+};
+ 
+/** 获取收货历史记录 */
+export const getReceiptHistory = (id: string) => {
+  return http.request<ReceiptHistoryResponse>(
+    "get",
+    `/api/delivery-details/receipt-history/${id}`
+  );
+};
+ 
+/** 下载出库单模板 */
+export const getOutboundOrderTemplate = () => {
+  return http.request<DownloadTemplateResponse>(
+    "get",
+    "/api/delivery-details/template/download"
+  );
+};
+ 
+/** 导出提货明细 */
+export const exportDeliveryDetails = (params?: {
+  outboundNumber?: string;
+  deliveryOrderNumber?: string;
+  blNumber?: string;
+  licensePlate?: string;
+  commodity?: string;
+  outboundStatus?: string;
+  receiptStatus?: string;
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<ExportDataResponse>("get", "/api/delivery-details/export", {
+    params
+  });
+};
+ 
+/** 打印提货单 */
+export const printDeliveryOrder = (id: string) => {
+  return http.request<PrintDeliveryOrderResponse>(
+    "get",
+    `/api/delivery-details/print/${id}`
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/favicon.png b/frontend/coverage/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 99.27% + Statements + 274/276 +
+ + +
+ 93.75% + Branches + 15/16 +
+ + +
+ 99.23% + Functions + 129/130 +
+ + +
+ 99.26% + Lines + 269/271 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
applyDelivery.ts +
+
100%16/16100%0/0100%8/8100%16/16
company.ts +
+
100%18/18100%0/0100%9/9100%18/18
contract.ts +
+
100%30/30100%0/0100%15/15100%30/30
deliveryDetails.ts +
+
100%18/18100%0/0100%9/9100%18/18
inventory.ts +
+
100%28/28100%0/0100%14/14100%28/28
invoice.ts +
+
100%24/24100%0/0100%12/12100%24/24
ownershipTransfer.ts +
+
100%21/2187.5%7/8100%5/5100%18/18
reconciliation.ts +
+
100%12/12100%0/0100%6/6100%12/12
routes.ts +
+
0%0/2100%0/00%0/10%0/2
settlement.ts +
+
100%12/12100%0/0100%6/6100%12/12
user.ts +
+
100%6/6100%0/0100%3/3100%6/6
zspContract.ts +
+
100%34/34100%2/2100%17/17100%34/34
zspFinances.ts +
+
100%55/55100%6/6100%25/25100%53/53
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/inventory.ts.html b/frontend/coverage/inventory.ts.html new file mode 100644 index 0000000..13525ed --- /dev/null +++ b/frontend/coverage/inventory.ts.html @@ -0,0 +1,994 @@ + + + + + + Code coverage report for inventory.ts + + + + + + + + + +
+
+

All files inventory.ts

+
+ +
+ 100% + Statements + 28/28 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 14/14 +
+ + +
+ 100% + Lines + 28/28 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +  +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  + 
import { http } from "@/utils/http";
+ 
+// ============ 类型定义 ============
+/** 入库单 */
+export type InventoryInbound = {
+  id: string;
+  inboundNumber: string; // 入库单号
+  warehouse: string; // 仓库
+  commodity: string; // 商品
+  commodityCode?: string; // 商品编码
+  blNumber: string; // 提单号
+  blWeight: number; // 提单重
+  inboundWeight: number; // 入库重
+  owner: string; // 货主
+  contractNumber: string; // 对应上游合同编号
+  warehouseAddress?: string; // 仓库地址
+  packaging?: string; // 包装方式
+  inboundDate: string; // 入库日期
+  operator: string; // 操作员
+  remark?: string; // 备注
+  createTime: string; // 创建时间
+  updateTime: string; // 更新时间
+};
+ 
+/** 仓库信息 */
+export type Warehouse = {
+  id: string;
+  code: string; // 仓库编码
+  name: string; // 仓库名称
+  address: string; // 仓库地址
+  contactPerson: string; // 联系人
+  contactPhone: string; // 联系电话
+  capacity: number; // 容量(吨)
+  usedCapacity: number; // 已使用容量
+  status: "active" | "inactive" | "maintenance"; // 状态
+  description?: string; // 描述
+  createTime: string; // 创建时间
+  updateTime: string; // 更新时间
+};
+ 
+/** 库存明细 */
+export type InventoryDetail = {
+  id: string;
+  warehouse: string; // 仓库
+  commodity: string; // 商品
+  totalInbound: number; // 总入库量
+  totalOutbound: number; // 总出库量
+  totalTransfer: number; // 总货权转移量
+  currentStock: number; // 当前库存
+  unit: string; // 单位
+  lastInboundDate?: string; // 最后入库日期
+  lastOutboundDate?: string; // 最后出库日期
+  updateTime: string; // 更新时间
+};
+ 
+/** 货权转移 */
+export type CargoTransfer = {
+  id: string;
+  transferNumber: string; // 货权转移单号
+  fromWarehouse: string; // 转出仓库
+  toWarehouse: string; // 转入仓库
+  commodity: string; // 商品
+  transferWeight: number; // 转移重量
+  transferDate: string; // 转移日期
+  operator: string; // 操作员
+  remark?: string; // 备注
+  createTime: string; // 创建时间
+  updateTime: string; // 更新时间
+};
+ 
+/** 入库单表单数据 */
+export type InboundFormData = {
+  warehouse: string; // 仓库
+  commodity: string; // 商品
+  commodityCode?: string; // 商品编码
+  blNumber: string; // 提单号
+  blWeight: number; // 提单重
+  inboundWeight: number; // 入库重
+  owner: string; // 货主
+  contractNumber: string; // 对应上游合同编号
+  warehouseAddress?: string; // 仓库地址
+  packaging?: string; // 包装方式
+  inboundDate: string; // 入库日期
+  operator: string; // 操作员
+  remark?: string; // 备注
+};
+ 
+/** 仓库表单数据 */
+export type WarehouseFormData = {
+  code: string; // 仓库编码
+  name: string; // 仓库名称
+  address: string; // 仓库地址
+  contactPerson: string; // 联系人
+  contactPhone: string; // 联系电话
+  capacity: number; // 容量
+  description?: string; // 描述
+};
+ 
+/** 货权转移表单数据 */
+export type CargoTransferFormData = {
+  fromWarehouse: string; // 转出仓库
+  toWarehouse: string; // 转入仓库
+  commodity: string; // 商品
+  transferWeight: number; // 转移重量
+  transferDate: string; // 转移日期
+  operator: string; // 操作员
+  remark?: string; // 备注
+};
+ 
+/** API基础响应 */
+export type BaseResponse<T = any> = {
+  success: boolean;
+  message: string;
+  data?: T;
+};
+ 
+/** 入库单列表响应 */
+export type InventoryListResponse = BaseResponse<{
+  items: InventoryInbound[];
+  total: number;
+  page: number;
+  pageSize: number;
+}>;
+ 
+/** 入库单详情响应 */
+export type InventoryDetailResponse = BaseResponse<InventoryInbound>;
+ 
+/** 创建入库单响应 */
+export type CreateInventoryInboundResponse = BaseResponse<InventoryInbound>;
+ 
+/** 仓库列表响应 */
+export type WarehouseListResponse = BaseResponse<Warehouse[]>;
+ 
+/** 创建/更新仓库响应 */
+export type WarehouseResponse = BaseResponse<Warehouse>;
+ 
+/** 库存明细响应 */
+export type InventorySummaryResponse = BaseResponse<InventoryDetail[]>;
+ 
+/** 创建货权转移响应 */
+export type CargoTransferResponse = BaseResponse<CargoTransfer>;
+ 
+/** 下载模板响应 */
+export type DownloadTemplateResponse = BaseResponse<{
+  fileName: string;
+  url: string;
+}>;
+ 
+/** 导出数据响应 */
+export type ExportDataResponse = BaseResponse<{
+  fileName: string;
+  url: string;
+}>;
+ 
+/** 批量上传响应 */
+export type UploadFilesResponse = BaseResponse<{
+  processedCount: number;
+  successCount: number;
+  failedCount: number;
+}>;
+ 
+// ============ API函数 ============
+/** 获取入库单列表 */
+export const getInventoryList = (params?: {
+  page?: number;
+  pageSize?: number;
+  inboundNumber?: string;
+  warehouse?: string;
+  commodity?: string;
+  blNumber?: string;
+  owner?: string;
+  contractNumber?: string;
+  startDate?: string;
+  endDate?: string;
+}) => {
+  return http.request<InventoryListResponse>("get", "/api/inventory/inbound/list", {
+    params
+  });
+};
+ 
+/** 获取入库单详情 */
+export const getInventoryDetail = (id: string) => {
+  return http.request<InventoryDetailResponse>(
+    "get",
+    `/api/inventory/inbound/detail/${id}`
+  );
+};
+ 
+/** 新增入库单 */
+export const createInventoryInbound = (data: InboundFormData) => {
+  return http.request<CreateInventoryInboundResponse>(
+    "post",
+    "/api/inventory/inbound/create",
+    { data }
+  );
+};
+ 
+/** 更新入库单 */
+export const updateInventoryInbound = (
+  id: string,
+  data: Partial<InboundFormData>
+) => {
+  return http.request<CreateInventoryInboundResponse>(
+    "put",
+    `/api/inventory/inbound/update/${id}`,
+    { data }
+  );
+};
+ 
+/** 删除入库单 */
+export const deleteInventoryInbound = (id: string) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/api/inventory/inbound/delete/${id}`
+  );
+};
+ 
+/** 获取仓库列表 */
+export const getWarehouseList = (params?: {
+  name?: string;
+  code?: string;
+  status?: string;
+}) => {
+  return http.request<WarehouseListResponse>("get", "/api/inventory/warehouses", {
+    params
+  });
+};
+ 
+/** 创建仓库 */
+export const createWarehouse = (data: WarehouseFormData) => {
+  return http.request<WarehouseResponse>(
+    "post",
+    "/api/inventory/warehouses/create",
+    { data }
+  );
+};
+ 
+/** 更新仓库 */
+export const updateWarehouse = (
+  id: string,
+  data: Partial<WarehouseFormData>
+) => {
+  return http.request<WarehouseResponse>(
+    "put",
+    `/api/inventory/warehouses/update/${id}`,
+    { data }
+  );
+};
+ 
+/** 删除仓库 */
+export const deleteWarehouse = (id: string) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/api/inventory/warehouses/delete/${id}`
+  );
+};
+ 
+/** 获取库存明细 */
+export const getInventorySummary = (params?: {
+  warehouse?: string;
+  commodity?: string;
+}) => {
+  return http.request<InventorySummaryResponse>("get", "/api/inventory/summary", {
+    params
+  });
+};
+ 
+/** 创建货权转移 */
+export const createCargoTransfer = (data: CargoTransferFormData) => {
+  return http.request<CargoTransferResponse>(
+    "post",
+    "/api/inventory/transfer/create",
+    { data }
+  );
+};
+ 
+/** 下载入库单模板 */
+export const getInventoryInboundTemplate = () => {
+  return http.request<DownloadTemplateResponse>(
+    "get",
+    "/api/inventory/template/download"
+  );
+};
+ 
+/** 导出库存数据 */
+export const exportInventoryList = (params?: {
+  warehouse?: string;
+  commodity?: string;
+}) => {
+  return http.request<ExportDataResponse>("get", "/api/inventory/export", {
+    params
+  });
+};
+ 
+/** 批量上传文件 */
+export const uploadInventoryFiles = (data: FormData) => {
+  return http.request<UploadFilesResponse>("post", "/api/inventory/upload", {
+    data,
+    headers: {
+      "Content-Type": "multipart/form-data"
+    }
+  });
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/invoice.ts.html b/frontend/coverage/invoice.ts.html new file mode 100644 index 0000000..f6c79eb --- /dev/null +++ b/frontend/coverage/invoice.ts.html @@ -0,0 +1,721 @@ + + + + + + Code coverage report for invoice.ts + + + + + + + + + +
+
+

All files invoice.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 12/12 +
+ + +
+ 100% + Lines + 24/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+ 
+// ==================== 上游发票 ====================
+ 
+export type ZSPUpstreamInvoice = {
+  id: number;
+  invoiceNumber: string;
+  contractNumber: string;
+  billOfLading: string;
+  supplierName: string;
+  commodity: string;
+  amount: number;
+  taxAmount: number;
+  totalAmount: number;
+  currency: string;
+  invoiceDate: string;
+  status: string;
+  filePath: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export interface ZSPUpstreamInvoiceCreateRequest {
+  invoiceNumber: string;
+  contractNumber?: string;
+  billOfLading?: string;
+  supplierName: string;
+  commodity?: string;
+  amount: number;
+  taxAmount?: number;
+  totalAmount: number;
+  currency?: string;
+  invoiceDate: string;
+  remark?: string;
+}
+ 
+export const getUpstreamInvoices = () => {
+  return http.request<BaseResponse<ZSPUpstreamInvoice[]>>(
+    "get",
+    "/zsp-finances/upstream-invoices"
+  );
+};
+ 
+export const createUpstreamInvoice = (
+  data: ZSPUpstreamInvoiceCreateRequest
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/upstream-invoices",
+    { data }
+  );
+};
+ 
+export const deleteUpstreamInvoice = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/upstream-invoices/${id}`
+  );
+};
+ 
+export const uploadUpstreamInvoiceFile = (
+  id: string | number,
+  data: FormData
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    `/zsp-finances/upstream-invoices/${id}/upload`,
+    { data, headers: { "Content-Type": "multipart/form-data" } }
+  );
+};
+ 
+// ==================== 下游发票 ====================
+ 
+export type ZSPDownstreamInvoice = {
+  id: number;
+  invoiceNumber: string;
+  contractNumber: string;
+  billOfLading: string;
+  customerName: string;
+  commodity: string;
+  amount: number;
+  taxAmount: number;
+  totalAmount: number;
+  currency: string;
+  invoiceDate: string;
+  status: string;
+  filePath: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export interface ZSPDownstreamInvoiceCreateRequest {
+  invoiceNumber: string;
+  contractNumber?: string;
+  billOfLading?: string;
+  customerName: string;
+  commodity?: string;
+  amount: number;
+  taxAmount?: number;
+  totalAmount: number;
+  currency?: string;
+  invoiceDate: string;
+  remark?: string;
+}
+ 
+export const getDownstreamInvoices = () => {
+  return http.request<BaseResponse<ZSPDownstreamInvoice[]>>(
+    "get",
+    "/zsp-finances/downstream-invoices"
+  );
+};
+ 
+export const createDownstreamInvoice = (
+  data: ZSPDownstreamInvoiceCreateRequest
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/downstream-invoices",
+    { data }
+  );
+};
+ 
+export const deleteDownstreamInvoice = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/downstream-invoices/${id}`
+  );
+};
+ 
+export const uploadDownstreamInvoiceFile = (
+  id: string | number,
+  data: FormData
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    `/zsp-finances/downstream-invoices/${id}/upload`,
+    { data, headers: { "Content-Type": "multipart/form-data" } }
+  );
+};
+ 
+// ==================== 货代杂费发票 ====================
+ 
+export type ZSPFreightMiscInvoice = {
+  id: number;
+  invoiceNumber: string;
+  contractNumber: string;
+  billOfLading: string;
+  companyName: string;
+  expenseItem: string;
+  amount: number;
+  taxAmount: number;
+  totalAmount: number;
+  currency: string;
+  invoiceDate: string;
+  status: string;
+  filePath: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export interface ZSPFreightMiscInvoiceCreateRequest {
+  invoiceNumber: string;
+  contractNumber?: string;
+  billOfLading?: string;
+  companyName: string;
+  expenseItem?: string;
+  amount: number;
+  taxAmount?: number;
+  totalAmount: number;
+  currency?: string;
+  invoiceDate: string;
+  remark?: string;
+}
+ 
+export const getFreightMiscInvoices = () => {
+  return http.request<BaseResponse<ZSPFreightMiscInvoice[]>>(
+    "get",
+    "/zsp-finances/freight-misc-invoices"
+  );
+};
+ 
+export const createFreightMiscInvoice = (
+  data: ZSPFreightMiscInvoiceCreateRequest
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/freight-misc-invoices",
+    { data }
+  );
+};
+ 
+export const deleteFreightMiscInvoice = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/freight-misc-invoices/${id}`
+  );
+};
+ 
+export const uploadFreightMiscInvoiceFile = (
+  id: string | number,
+  data: FormData
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    `/zsp-finances/freight-misc-invoices/${id}/upload`,
+    { data, headers: { "Content-Type": "multipart/form-data" } }
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/ownershipTransfer.ts.html b/frontend/coverage/ownershipTransfer.ts.html new file mode 100644 index 0000000..50e7e19 --- /dev/null +++ b/frontend/coverage/ownershipTransfer.ts.html @@ -0,0 +1,385 @@ + + + + + + Code coverage report for ownershipTransfer.ts + + + + + + + + + +
+
+

All files ownershipTransfer.ts

+
+ +
+ 100% + Statements + 21/21 +
+ + +
+ 87.5% + Branches + 7/8 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 18/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +4x +  +3x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +  +  +  +  +  +  +3x +  +  +  +  +  + 
import { http } from "@/utils/http";
+ 
+// 货权转移类型定义
+export type OwnershipTransfer = {
+  id: string;
+  transferNumber: string; // 转移函编号
+  transferDate: string; // 货转日期
+  commodity: string; // 商品
+  warehouse: string; // 库方
+  transferor: string; // 货权出让方
+  transferee: string; // 货权接收方
+  status: "draft" | "effective" | "void"; // 状态
+  remark: string; // 备注
+  blItems: Array<{
+    // 提单明细(动态列表)
+    id?: number;
+    transferId?: number;
+    blNumber: string; // 提单号
+    quantity: number; // 数量
+  }>;
+  fileList: Array<{
+    name: string;
+    url: string;
+  }>;
+  createdAt: string;
+  updatedAt: string;
+};
+ 
+// API响应基础类型
+export type BaseResponse<T = any> = {
+  success: boolean;
+  message: string;
+  data?: T;
+};
+ 
+// 货权转移列表响应类型
+export type OwnershipListResponse = BaseResponse<{
+  items: OwnershipTransfer[];
+  total: number;
+}>;
+ 
+/** 创建货权转移 */
+export const createOwnershipTransfer = (data: {
+  transferDate: string;
+  commodity: string;
+  blItems: Array<{ blNumber: string; quantity: number }>;
+  warehouse: string;
+  transferor: string;
+  transferee: string;
+  remarks?: string;
+  files?: File[];
+}) => {
+  // 过滤空提单号
+  const validItems = data.blItems.filter(item => item.blNumber || item.quantity);
+ 
+  if (data.files?.length) {
+    const formData = new FormData();
+    formData.append("transferDate", data.transferDate);
+    formData.append("commodity", data.commodity);
+    formData.append("blItems", JSON.stringify(validItems));
+    formData.append("warehouse", data.warehouse);
+    formData.append("transferor", data.transferor);
+    formData.append("transferee", data.transferee);
+    Eif (data.remarks) formData.append("remark", data.remarks);
+    data.files.forEach(file => formData.append("files", file));
+    return http.request<BaseResponse<OwnershipTransfer>>(
+      "post",
+      "/api/ownership-transfer/create-with-files",
+      {
+        data: formData,
+        headers: { "Content-Type": "multipart/form-data" }
+      }
+    );
+  }
+  return http.request<BaseResponse<OwnershipTransfer>>(
+    "post",
+    "/api/ownership-transfer/create",
+    { data: { ...data, blItems: validItems, remark: data.remarks ?? "" } }
+  );
+};
+ 
+/** 删除货权转移 */
+export const deleteOwnershipTransfer = (id: string) => {
+  return http.request<BaseResponse>("delete", `/api/ownership-transfer/${id}`);
+};
+ 
+/** 获取货权转移列表 */
+export const getOwnershipList = (params?: {
+  transferNumber?: string;
+  blNumber?: string;
+  status?: string;
+  page?: number;
+  pageSize?: number;
+}) => {
+  return http.request<OwnershipListResponse>(
+    "get",
+    "/api/ownership-transfer/list",
+    { params }
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/prettify.css b/frontend/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/frontend/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/frontend/coverage/prettify.js b/frontend/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/frontend/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/frontend/coverage/reconciliation.ts.html b/frontend/coverage/reconciliation.ts.html new file mode 100644 index 0000000..430d256 --- /dev/null +++ b/frontend/coverage/reconciliation.ts.html @@ -0,0 +1,388 @@ + + + + + + Code coverage report for reconciliation.ts + + + + + + + + + +
+
+

All files reconciliation.ts

+
+ +
+ 100% + Statements + 12/12 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 12/12 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +  +  +2x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+ 
+export type ZSPReconciliation = {
+  id: number;
+  reconciliationNumber: string;
+  contractNumber: string;
+  companyName: string;
+  periodStart: string;
+  periodEnd: string;
+  totalAmount: number;
+  paidAmount: number;
+  unpaidAmount: number;
+  currency: string;
+  status: string;
+  filePath: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export interface ZSPReconciliationCreateRequest {
+  reconciliationNumber: string;
+  contractNumber?: string;
+  companyName: string;
+  periodStart: string;
+  periodEnd: string;
+  totalAmount: number;
+  paidAmount?: number;
+  currency?: string;
+  remark?: string;
+}
+ 
+export interface ZSPReconciliationUpdateRequest {
+  contractNumber?: string;
+  companyName?: string;
+  periodStart?: string;
+  periodEnd?: string;
+  totalAmount?: number;
+  paidAmount?: number;
+  status?: string;
+  remark?: string;
+}
+ 
+export const getReconciliations = (params?: {
+  contractNumber?: string;
+  companyName?: string;
+  status?: string;
+}) => {
+  return http.request<BaseResponse<ZSPReconciliation[]>>(
+    "get",
+    "/zsp-finances/reconciliations",
+    { params }
+  );
+};
+ 
+export const getReconciliation = (id: string | number) => {
+  return http.request<BaseResponse<ZSPReconciliation>>(
+    "get",
+    `/zsp-finances/reconciliations/${id}`
+  );
+};
+ 
+export const createReconciliation = (
+  data: ZSPReconciliationCreateRequest
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/reconciliations",
+    { data }
+  );
+};
+ 
+export const updateReconciliation = (
+  id: string | number,
+  data: ZSPReconciliationUpdateRequest
+) => {
+  return http.request<BaseResponse>(
+    "put",
+    `/zsp-finances/reconciliations/${id}`,
+    { data }
+  );
+};
+ 
+export const deleteReconciliation = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/reconciliations/${id}`
+  );
+};
+ 
+export const uploadReconciliationFile = (
+  id: string | number,
+  data: FormData
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    `/zsp-finances/reconciliations/${id}/upload`,
+    { data, headers: { "Content-Type": "multipart/form-data" } }
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/routes.ts.html b/frontend/coverage/routes.ts.html new file mode 100644 index 0000000..60bb780 --- /dev/null +++ b/frontend/coverage/routes.ts.html @@ -0,0 +1,115 @@ + + + + + + Code coverage report for routes.ts + + + + + + + + + +
+
+

All files routes.ts

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11  +  +  +  +  +  +  +  +  +  + 
import { http } from "@/utils/http";
+ 
+type Result = {
+  success: boolean;
+  data: Array<any>;
+};
+ 
+export const getAsyncRoutes = () => {
+  return http.request<Result>("get", "/get-async-routes");
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/settlement.ts.html b/frontend/coverage/settlement.ts.html new file mode 100644 index 0000000..8b83cbe --- /dev/null +++ b/frontend/coverage/settlement.ts.html @@ -0,0 +1,391 @@ + + + + + + Code coverage report for settlement.ts + + + + + + + + + +
+
+

All files settlement.ts

+
+ +
+ 100% + Statements + 12/12 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 12/12 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +2x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +2x +  +  +  +  +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+ 
+export type ZSPSettlement = {
+  id: number;
+  settlementNumber: string;
+  contractNumber: string;
+  contractType: string;
+  companyName: string;
+  commodity: string;
+  quantity: number;
+  unitPrice: number;
+  amount: number;
+  currency: string;
+  settlementDate: string;
+  status: string;
+  filePath: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export interface ZSPSettlementCreateRequest {
+  settlementNumber: string;
+  contractNumber: string;
+  contractType?: string;
+  companyName: string;
+  commodity?: string;
+  quantity?: number;
+  unitPrice?: number;
+  amount: number;
+  currency?: string;
+  settlementDate: string;
+  remark?: string;
+}
+ 
+export interface ZSPSettlementUpdateRequest {
+  contractNumber?: string;
+  contractType?: string;
+  companyName?: string;
+  commodity?: string;
+  quantity?: number;
+  unitPrice?: number;
+  amount?: number;
+  currency?: string;
+  settlementDate?: string;
+  status?: string;
+  remark?: string;
+}
+ 
+export const getSettlements = (params?: {
+  contractNumber?: string;
+  settlementNumber?: string;
+  status?: string;
+}) => {
+  return http.request<BaseResponse<ZSPSettlement[]>>(
+    "get",
+    "/zsp-finances/settlements",
+    { params }
+  );
+};
+ 
+export const getSettlement = (id: string | number) => {
+  return http.request<BaseResponse<ZSPSettlement>>(
+    "get",
+    `/zsp-finances/settlements/${id}`
+  );
+};
+ 
+export const createSettlement = (data: ZSPSettlementCreateRequest) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/settlements",
+    { data }
+  );
+};
+ 
+export const updateSettlement = (
+  id: string | number,
+  data: ZSPSettlementUpdateRequest
+) => {
+  return http.request<BaseResponse>(
+    "put",
+    `/zsp-finances/settlements/${id}`,
+    { data }
+  );
+};
+ 
+export const deleteSettlement = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/settlements/${id}`
+  );
+};
+ 
+export const uploadSettlementFile = (id: string | number, data: FormData) => {
+  return http.request<BaseResponse>(
+    "post",
+    `/zsp-finances/settlements/${id}/upload`,
+    { data, headers: { "Content-Type": "multipart/form-data" } }
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/sort-arrow-sprite.png b/frontend/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/frontend/coverage/sorter.js b/frontend/coverage/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/frontend/coverage/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/frontend/coverage/user.ts.html b/frontend/coverage/user.ts.html new file mode 100644 index 0000000..15d3017 --- /dev/null +++ b/frontend/coverage/user.ts.html @@ -0,0 +1,247 @@ + + + + + + Code coverage report for user.ts + + + + + + + + + +
+
+

All files user.ts

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +3x +  +  +  +1x +2x +  +  +1x +2x +  + 
import { http } from "@/utils/http";
+ 
+export type UserResult = {
+  success: boolean;
+  data: {
+    /** 头像 */
+    avatar: string;
+    /** 用户名 */
+    username: string;
+    /** 昵称 */
+    nickname: string;
+    /** 当前登录用户的角色 */
+    roles: Array<string>;
+    /** 按钮级别权限 */
+    permissions: Array<string>;
+    /** `token` */
+    accessToken: string;
+    /** 用于调用刷新`accessToken`的接口时所需的`token` */
+    refreshToken: string;
+    /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
+    expires: Date;
+  };
+};
+ 
+export type RefreshTokenResult = {
+  success: boolean;
+  data: {
+    /** `token` */
+    accessToken: string;
+    /** 用于调用刷新`accessToken`的接口时所需的`token` */
+    refreshToken: string;
+    /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */
+    expires: Date;
+  };
+};
+ 
+export type RegisterResult = {
+  success: boolean;
+  message: string;
+};
+ 
+/** 登录 */
+export const getLogin = (data?: object) => {
+  return http.request<UserResult>("post", "/login", { data });
+};
+ 
+/** 刷新`token` */
+export const refreshTokenApi = (data?: object) => {
+  return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
+};
+ 
+export const registerApi = (data?: object) => {
+  return http.request<RegisterResult>("post", "/api/auth/register", { data });
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/zspContract.ts.html b/frontend/coverage/zspContract.ts.html new file mode 100644 index 0000000..5b312db --- /dev/null +++ b/frontend/coverage/zspContract.ts.html @@ -0,0 +1,766 @@ + + + + + + Code coverage report for zspContract.ts + + + + + + + + + +
+
+

All files zspContract.ts

+
+ +
+ 100% + Statements + 34/34 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 17/17 +
+ + +
+ 100% + Lines + 34/34 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +1x +  +  +  +2x +  +  +  +1x +2x +  +  +  +  +  +1x +2x +  +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +2x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +1x +2x +  +  +  +1x +2x +  +  +  +1x +  +  +1x +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +1x +2x +  +  +  +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+ 
+// 合同文件夹类型
+export type ZSPContractFolderResult = {
+  id: number;
+  name: string;
+  count: number;
+  createTime: string;
+  description: string;
+};
+ 
+// 合同文件类型
+export type ZSPContractFileResult = {
+  id: number;
+  fileName: string;
+  fileUrl?: string;
+  fileType: string;
+  fileSize: number;
+  uploadTime: string;
+ 
+  folderId: number;
+ 
+  contractNumber: string;
+  companyName: string;
+  commodity: string;
+  signDate: string;
+  contractType: string;
+ 
+  // 合同详情列表
+  details?: ZSPContractDetailResult[];
+};
+ 
+// 合同详情类型
+export type ZSPContractDetailResult = {
+  id: number;
+  contractFileId: number;
+  commodityName: string;
+  commodityCode?: string;
+  totalQuantity: number;
+  unit: string;
+  unitPrice: number;
+  deliveredQty: number;
+  pendingQty: number;
+  billOfLading?: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+// 创建合同详情请求
+export interface ZSPContractDetailCreateRequest {
+  contractFileId: number;
+  commodityName: string;
+  commodityCode?: string;
+  totalQuantity: number;
+  unit?: string;
+  unitPrice: number;
+  deliveredQty?: number;
+  billOfLading?: string;
+  remark?: string;
+}
+ 
+// 更新合同详情请求
+export interface ZSPContractDetailUpdateRequest {
+  id: number;
+  commodityName?: string;
+  commodityCode?: string;
+  totalQuantity?: number;
+  unit?: string;
+  unitPrice?: number;
+  deliveredQty?: number;
+  billOfLading?: string;
+  remark?: string;
+}
+ 
+// 批量创建合同详情请求
+export interface ZSPContractDetailBatchCreateRequest {
+  contractFileId: number;
+  details: ZSPContractDetailCreateRequest[];
+}
+ 
+// ==================== 合同文件夹相关 ====================
+ 
+/** 获取合同文件夹列表(GET) */
+export const getZSPContractFolders = () => {
+  return http.request<BaseResponse<ZSPContractFolderResult[]>>(
+    "get",
+    "/api/zsp-contract/folders"
+  );
+};
+ 
+/** 创建合同文件夹(POST) */
+export const createZSPContractFolder = (data: {
+  name: string;
+  description?: string;
+}) => {
+  return http.request<BaseResponse>("post", "/api/zsp-contract/folders", { data });
+};
+ 
+/** 更新合同文件夹(PUT) */
+export const updateZSPContractFolder = (
+  id: string | number,
+  data: { name: string; description?: string }
+) => {
+  return http.request<BaseResponse>("put", `/api/zsp-contract/folders/${id}`, { data });
+};
+ 
+/** 删除合同文件夹(DELETE) */
+export const deleteZSPContractFolder = (id: string | number) => {
+  return http.request<BaseResponse>("delete", `/api/zsp-contract/folders/${id}`);
+};
+ 
+// ==================== 合同文件相关 ====================
+ 
+/** 获取合同列表(含详情)(GET) */
+export const getZSPContracts = (folderId?: string) => {
+  return http.request<BaseResponse<ZSPContractFileResult[]>>(
+    "get",
+    "/api/zsp-contract/contracts",
+    { params: folderId ? { folderId } : {} }
+  );
+};
+ 
+/** 获取单个合同详情(含详情列表)(GET) */
+export const getZSPContract = (id: string | number) => {
+  return http.request<BaseResponse<ZSPContractFileResult>>(
+    "get",
+    `/api/zsp-contract/contracts/${id}`
+  );
+};
+ 
+/** 上传合同文件(POST) */
+export const uploadZSPContract = (formData: FormData) => {
+  return http.request<BaseResponse>("post", "/api/zsp-contract/contracts", {
+    data: formData,
+    headers: { "Content-Type": "multipart/form-data" }
+  });
+};
+ 
+/** 删除合同文件(DELETE) */
+export const deleteZSPContract = (id: string | number) => {
+  return http.request<BaseResponse>("delete", `/api/zsp-contract/contracts/${id}`);
+};
+ 
+/** 更新合同元信息(PUT) */
+export const updateZSPContract = (
+  id: string | number,
+  data: {
+    contractNumber?: string;
+    companyName?: string;
+    commodity?: string;
+    signDate?: string;
+    contractType?: string;
+    folderId?: number;
+  }
+) => {
+  return http.request<BaseResponse>("put", `/api/zsp-contract/contracts/${id}`, {
+    data
+  });
+};
+ 
+// ==================== 合同详情相关 ====================
+ 
+/** 获取合同详情列表(GET) */
+export const getZSPContractDetails = (contractFileId: string | number) => {
+  return http.request<BaseResponse<ZSPContractDetailResult[]>>(
+    "get",
+    `/api/zsp-contract/contracts/${contractFileId}/details`
+  );
+};
+ 
+/** 获取单个合同详情(GET) */
+export const getZSPContractDetail = (id: string | number) => {
+  return http.request<BaseResponse<ZSPContractDetailResult>>(
+    "get",
+    `/api/zsp-contract/details/${id}`
+  );
+};
+ 
+/** 创建合同详情(POST) */
+export const createZSPContractDetail = (
+  contractFileId: string | number,
+  data: ZSPContractDetailCreateRequest
+) => {
+  return http.request<BaseResponse>("post", `/api/zsp-contract/contracts/${contractFileId}/details`, { data });
+};
+ 
+/** 更新合同详情(PUT) */
+export const updateZSPContractDetail = (id: string | number, data: ZSPContractDetailUpdateRequest) => {
+  return http.request<BaseResponse>("put", `/api/zsp-contract/details/${id}`, { data });
+};
+ 
+/** 删除合同详情(DELETE) */
+export const deleteZSPContractDetail = (id: string | number) => {
+  return http.request<BaseResponse>("delete", `/api/zsp-contract/details/${id}`);
+};
+ 
+/** 批量创建合同详情(POST) */
+export const batchCreateZSPContractDetails = (
+  data: ZSPContractDetailBatchCreateRequest
+) => {
+  return http.request<BaseResponse>("post", "/api/zsp-contract/details/batch", { data });
+};
+ 
+/** Excel导入合同详情(POST) */
+export const importZSPContractDetailsFromExcel = (
+  contractFileId: string | number,
+  formData: FormData
+) => {
+  return http.request<BaseResponse>(
+    "post",
+    `/api/zsp-contract/contracts/${contractFileId}/details/import`,
+    {
+      data: formData,
+      headers: { "Content-Type": "multipart/form-data" }
+    }
+  );
+};
+ 
+/** 导出合同详情到Excel(GET) */
+export const exportZSPContractDetailsToExcel = (contractFileId: string | number) => {
+  return http.request<BaseResponse>(
+    "get",
+    `/api/zsp-contract/contracts/${contractFileId}/details/export`
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/zspFinances.ts.html b/frontend/coverage/zspFinances.ts.html new file mode 100644 index 0000000..4af2f3c --- /dev/null +++ b/frontend/coverage/zspFinances.ts.html @@ -0,0 +1,1228 @@ + + + + + + Code coverage report for zspFinances.ts + + + + + + + + + +
+
+

All files zspFinances.ts

+
+ +
+ 100% + Statements + 55/55 +
+ + +
+ 100% + Branches + 6/6 +
+ + +
+ 100% + Functions + 25/25 +
+ + +
+ 100% + Lines + 53/53 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +  +  +1x +  +  +  +3x +3x +3x +3x +  +  +  +  +  +  +1x +2x +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +1x +2x +  +  +  +  + 
import { http } from "@/utils/http";
+import type { BaseResponse } from "./base";
+ 
+// ==================== 类型定义 ====================
+ 
+export type ZSPFreightCharge = {
+  id: number;
+  billOfLading: string;
+  contractNumber: string;
+  expenseItem: string;
+  amount: number;
+  currency: string;
+  paymentDate: string;
+  freightCompanyName: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export type ZSPMiscCharge = {
+  id: number;
+  billOfLading: string;
+  contractNumber: string;
+  expenseItem: string;
+  amount: number;
+  currency: string;
+  paymentDate: string;
+  companyName: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export type ZSPServiceFee = {
+  id: number;
+  billOfLading: string;
+  contractNumber: string;
+  name: string;
+  amount: number;
+  currency: string;
+  date: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export type ZSPCostBreakdown = {
+  id: number;
+  businessId: string;
+  businessType: string;
+  totalCost: number;
+  freightChargeTotal: number;
+  miscChargeTotal: number;
+  serviceFeeTotal: number;
+  createTime: string;
+  updateTime: string;
+};
+ 
+export type ZSPProfitRecord = {
+  id: number;
+  businessId: string;
+  businessType: string;
+  revenue: number;
+  totalCost: number;
+  profit: number;
+  profitRate: number;
+  recordDate: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+};
+ 
+// ==================== 请求类型 ====================
+ 
+export interface ZSPFreightChargeCreateRequest {
+  billOfLading?: string;
+  contractNumber?: string;
+  expenseItem: string;
+  amount: number;
+  currency?: string;
+  paymentDate: string;
+  freightCompanyName: string;
+  remark?: string;
+}
+ 
+export interface ZSPFreightChargeUpdateRequest {
+  billOfLading?: string;
+  contractNumber?: string;
+  expenseItem?: string;
+  amount?: number;
+  currency?: string;
+  paymentDate?: string;
+  freightCompanyName?: string;
+  remark?: string;
+}
+ 
+export interface ZSPMiscChargeCreateRequest {
+  billOfLading?: string;
+  contractNumber?: string;
+  expenseItem: string;
+  amount: number;
+  currency?: string;
+  paymentDate: string;
+  companyName?: string;
+  remark?: string;
+}
+ 
+export interface ZSPMiscChargeUpdateRequest {
+  billOfLading?: string;
+  contractNumber?: string;
+  expenseItem?: string;
+  amount?: number;
+  currency?: string;
+  paymentDate?: string;
+  companyName?: string;
+  remark?: string;
+}
+ 
+export interface ZSPServiceFeeCreateRequest {
+  billOfLading?: string;
+  contractNumber?: string;
+  name: string;
+  amount: number;
+  currency?: string;
+  date: string;
+  remark?: string;
+}
+ 
+export interface ZSPServiceFeeUpdateRequest {
+  billOfLading?: string;
+  contractNumber?: string;
+  name?: string;
+  amount?: number;
+  currency?: string;
+  date?: string;
+  remark?: string;
+}
+ 
+export interface ZSPCostBreakdownCreateRequest {
+  businessId: string;
+  businessType: string;
+}
+ 
+export interface ZSPCostBreakdownUpdateRequest {
+  freightChargeTotal?: number;
+  miscChargeTotal?: number;
+  serviceFeeTotal?: number;
+}
+ 
+export interface ZSPProfitRecordCreateRequest {
+  businessId: string;
+  businessType: string;
+  revenue: number;
+  totalCost: number;
+  recordDate: string;
+  remark?: string;
+}
+ 
+export interface ZSPProfitRecordUpdateRequest {
+  revenue: number;
+  totalCost: number;
+  remark?: string;
+}
+ 
+// ==================== 货代费用 API ====================
+ 
+export const getFreightCharges = () => {
+  return http.request<BaseResponse<ZSPFreightCharge[]>>(
+    "get",
+    "/zsp-finances/freight-charges"
+  );
+};
+ 
+export const getFreightCharge = (id: string | number) => {
+  return http.request<BaseResponse<ZSPFreightCharge>>(
+    "get",
+    `/zsp-finances/freight-charges/${id}`
+  );
+};
+ 
+export const createFreightCharge = (data: ZSPFreightChargeCreateRequest) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/freight-charges",
+    { data }
+  );
+};
+ 
+export const updateFreightCharge = (
+  id: string | number,
+  data: ZSPFreightChargeUpdateRequest
+) => {
+  return http.request<BaseResponse>(
+    "put",
+    `/zsp-finances/freight-charges/${id}`,
+    { data }
+  );
+};
+ 
+export const deleteFreightCharge = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/freight-charges/${id}`
+  );
+};
+ 
+// ==================== 杂费 API ====================
+ 
+export const getMiscCharges = () => {
+  return http.request<BaseResponse<ZSPMiscCharge[]>>(
+    "get",
+    "/zsp-finances/misc-charges"
+  );
+};
+ 
+export const getMiscCharge = (id: string | number) => {
+  return http.request<BaseResponse<ZSPMiscCharge>>(
+    "get",
+    `/zsp-finances/misc-charges/${id}`
+  );
+};
+ 
+export const createMiscCharge = (data: ZSPMiscChargeCreateRequest) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/misc-charges",
+    { data }
+  );
+};
+ 
+export const updateMiscCharge = (
+  id: string | number,
+  data: ZSPMiscChargeUpdateRequest
+) => {
+  return http.request<BaseResponse>(
+    "put",
+    `/zsp-finances/misc-charges/${id}`,
+    { data }
+  );
+};
+ 
+export const deleteMiscCharge = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/misc-charges/${id}`
+  );
+};
+ 
+// ==================== 服务费 API ====================
+ 
+export const getServiceFees = () => {
+  return http.request<BaseResponse<ZSPServiceFee[]>>(
+    "get",
+    "/zsp-finances/service-fees"
+  );
+};
+ 
+export const getServiceFee = (id: string | number) => {
+  return http.request<BaseResponse<ZSPServiceFee>>(
+    "get",
+    `/zsp-finances/service-fees/${id}`
+  );
+};
+ 
+export const createServiceFee = (data: ZSPServiceFeeCreateRequest) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/service-fees",
+    { data }
+  );
+};
+ 
+export const updateServiceFee = (
+  id: string | number,
+  data: ZSPServiceFeeUpdateRequest
+) => {
+  return http.request<BaseResponse>(
+    "put",
+    `/zsp-finances/service-fees/${id}`,
+    { data }
+  );
+};
+ 
+export const deleteServiceFee = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/service-fees/${id}`
+  );
+};
+ 
+// ==================== 成本构成 API ====================
+ 
+export const getCostBreakdowns = (businessId?: string) => {
+  return http.request<BaseResponse<ZSPCostBreakdown[]>>(
+    "get",
+    "/zsp-finances/cost-breakdowns",
+    { params: businessId ? { businessId } : {} }
+  );
+};
+ 
+export const getCostBreakdown = (id: string | number) => {
+  return http.request<BaseResponse<ZSPCostBreakdown>>(
+    "get",
+    `/zsp-finances/cost-breakdowns/${id}`
+  );
+};
+ 
+export const createCostBreakdown = (data: ZSPCostBreakdownCreateRequest) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/cost-breakdowns",
+    { data }
+  );
+};
+ 
+export const updateCostBreakdown = (
+  id: string | number,
+  data: ZSPCostBreakdownUpdateRequest
+) => {
+  return http.request<BaseResponse>(
+    "put",
+    `/zsp-finances/cost-breakdowns/${id}`,
+    { data }
+  );
+};
+ 
+export const deleteCostBreakdown = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/cost-breakdowns/${id}`
+  );
+};
+ 
+// ==================== 利润记录 API ====================
+ 
+export const getProfitRecords = (
+  businessId?: string,
+  businessType?: string
+) => {
+  const params: Record<string, string> = {};
+  if (businessId) params.businessId = businessId;
+  if (businessType) params.businessType = businessType;
+  return http.request<BaseResponse<ZSPProfitRecord[]>>(
+    "get",
+    "/zsp-finances/profit-records",
+    { params }
+  );
+};
+ 
+export const getProfitRecord = (id: string | number) => {
+  return http.request<BaseResponse<ZSPProfitRecord>>(
+    "get",
+    `/zsp-finances/profit-records/${id}`
+  );
+};
+ 
+export const createProfitRecord = (data: ZSPProfitRecordCreateRequest) => {
+  return http.request<BaseResponse>(
+    "post",
+    "/zsp-finances/profit-records",
+    { data }
+  );
+};
+ 
+export const updateProfitRecord = (
+  id: string | number,
+  data: ZSPProfitRecordUpdateRequest
+) => {
+  return http.request<BaseResponse>(
+    "put",
+    `/zsp-finances/profit-records/${id}`,
+    { data }
+  );
+};
+ 
+export const deleteProfitRecord = (id: string | number) => {
+  return http.request<BaseResponse>(
+    "delete",
+    `/zsp-finances/profit-records/${id}`
+  );
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100755 index 0000000..d68ecbb --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,173 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; +import * as parserVue from "vue-eslint-parser"; +import configPrettier from "eslint-config-prettier"; +import pluginPrettier from "eslint-plugin-prettier"; +import { defineConfig, globalIgnores } from "eslint/config"; + +export default defineConfig([ + globalIgnores([ + "**/.*", + "dist/*", + "*.d.ts", + "public/*", + "src/assets/**", + "src/**/iconfont/**" + ]), + { + ...js.configs.recommended, + languageOptions: { + globals: { + // types/index.d.ts + RefType: "readonly", + EmitType: "readonly", + TargetContext: "readonly", + ComponentRef: "readonly", + ElRef: "readonly", + ForDataType: "readonly", + AnyFunction: "readonly", + PropType: "readonly", + Writable: "readonly", + Nullable: "readonly", + NonNullable: "readonly", + Recordable: "readonly", + ReadonlyRecordable: "readonly", + Indexable: "readonly", + DeepPartial: "readonly", + Without: "readonly", + Exclusive: "readonly", + TimeoutHandle: "readonly", + IntervalHandle: "readonly", + Effect: "readonly", + ChangeEvent: "readonly", + WheelEvent: "readonly", + ImportMetaEnv: "readonly", + Fn: "readonly", + PromiseFn: "readonly", + ComponentElRef: "readonly", + parseInt: "readonly", + parseFloat: "readonly" + } + }, + plugins: { + prettier: pluginPrettier + }, + rules: { + ...configPrettier.rules, + ...pluginPrettier.configs.recommended.rules, + "no-debugger": "off", + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_" + } + ], + "prettier/prettier": [ + "error", + { + endOfLine: "auto" + } + ] + } + }, + ...tseslint.config({ + extends: [...tseslint.configs.recommended], + files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"], + rules: { + "@typescript-eslint/no-redeclare": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/prefer-as-const": "warn", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + "@typescript-eslint/no-import-type-side-effects": "error", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/consistent-type-imports": [ + "error", + { disallowTypeAnnotations: false, fixStyle: "inline-type-imports" } + ], + "@typescript-eslint/prefer-literal-enum-member": [ + "error", + { allowBitwiseExpressions: true } + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_" + } + ] + } + }), + { + files: ["**/*.d.ts"], + rules: { + "eslint-comments/no-unlimited-disable": "off", + "import/no-duplicates": "off", + "no-restricted-syntax": "off", + "unused-imports/no-unused-vars": "off" + } + }, + { + files: ["**/*.?([cm])js"], + rules: { + "@typescript-eslint/no-require-imports": "off" + } + }, + { + files: ["**/*.vue"], + languageOptions: { + globals: { + $: "readonly", + $$: "readonly", + $computed: "readonly", + $customRef: "readonly", + $ref: "readonly", + $shallowRef: "readonly", + $toRef: "readonly" + }, + parser: parserVue, + parserOptions: { + ecmaFeatures: { + jsx: true + }, + extraFileExtensions: [".vue"], + parser: tseslint.parser, + sourceType: "module" + } + }, + plugins: { + "@typescript-eslint": tseslint.plugin, + vue: pluginVue + }, + processor: pluginVue.processors[".vue"], + rules: { + ...pluginVue.configs.base.rules, + ...pluginVue.configs.essential.rules, + ...pluginVue.configs.recommended.rules, + "no-undef": "off", + "no-unused-vars": "off", + "vue/no-v-html": "off", + "vue/require-default-prop": "off", + "vue/require-explicit-emits": "off", + "vue/multi-word-component-names": "off", + "vue/no-setup-props-reactivity-loss": "off", + "vue/html-self-closing": [ + "error", + { + html: { + void: "always", + normal: "always", + component: "always" + }, + svg: "always", + math: "always" + } + ] + } + } +]); diff --git a/frontend/index.html b/frontend/index.html new file mode 100755 index 0000000..3151fec --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,91 @@ + + + + + + + + pure-admin-thin + + + + +
+ +
+
+ + + diff --git a/frontend/mock/applyDelivery.ts b/frontend/mock/applyDelivery.ts new file mode 100755 index 0000000..eadd8fa --- /dev/null +++ b/frontend/mock/applyDelivery.ts @@ -0,0 +1,356 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 模拟数据 +const generateOrderNumber = () => { + const date = new Date(); + const year = date.getFullYear().toString().slice(-2); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const random = Math.floor(Math.random() * 1000) + .toString() + .padStart(3, "0"); + return `TH${year}${month}${day}${random}`; +}; + +// 模拟提货委托列表 +const mockDeliveryList = [ + { + id: "1", + orderNumber: generateOrderNumber(), + blNumber: "BL20240001", + licensePlate: "京A12345", + driverName: "张师傅", + driverPhone: "13800138001", + driverIdCard: "110101199001011234", + commodity: "copper_ore", + quantity: 100, + unit: "ton", + deliveryDate: "2024-03-15", + deliveryAddress: "北京市朝阳区仓库1号", + warehouse: "朝阳仓库", + status: "pending", + createTime: "2024-03-10 09:30:00", + updateTime: "2024-03-10 09:30:00", + remark: "第一批铜矿提货" + }, + { + id: "2", + orderNumber: generateOrderNumber(), + blNumber: "BL20240002", + licensePlate: "沪B23456", + driverName: "李师傅", + driverPhone: "13800138002", + driverIdCard: "310101199002022345", + commodity: "aluminum_material", + quantity: 50, + unit: "ton", + deliveryDate: "2024-03-16", + deliveryAddress: "上海市浦东新区仓库2号", + warehouse: "浦东仓库", + status: "approved", + createTime: "2024-03-11 10:15:00", + updateTime: "2024-03-11 10:15:00", + remark: "铝材提货" + }, + { + id: "3", + orderNumber: generateOrderNumber(), + blNumber: "BL20240003", + licensePlate: "粤C34567", + driverName: "王师傅", + driverPhone: "13800138003", + driverIdCard: "440101199003033456", + commodity: "steel", + quantity: 80, + unit: "ton", + deliveryDate: "2024-03-17", + deliveryAddress: "广州市天河区仓库3号", + warehouse: "天河仓库", + status: "in_progress", + createTime: "2024-03-12 14:20:00", + updateTime: "2024-03-12 14:20:00" + }, + { + id: "4", + orderNumber: generateOrderNumber(), + blNumber: "BL20240004", + licensePlate: "浙D45678", + driverName: "赵师傅", + driverPhone: "13800138004", + driverIdCard: "330101199004044567", + commodity: "copper_material", + quantity: 60, + unit: "ton", + deliveryDate: "2024-03-18", + deliveryAddress: "杭州市西湖区仓库4号", + warehouse: "西湖仓库", + status: "completed", + createTime: "2024-03-13 16:45:00", + updateTime: "2024-03-13 16:45:00" + }, + { + id: "5", + orderNumber: generateOrderNumber(), + blNumber: "BL20240005", + licensePlate: "苏E56789", + driverName: "刘师傅", + driverPhone: "13800138005", + driverIdCard: "320101199005055678", + commodity: "iron_ore", + quantity: 120, + unit: "ton", + deliveryDate: "2024-03-19", + deliveryAddress: "南京市鼓楼区仓库5号", + warehouse: "鼓楼仓库", + status: "cancelled", + createTime: "2024-03-14 11:10:00", + updateTime: "2024-03-14 11:10:00", + remark: "因天气原因取消" + } +]; + +export default defineFakeRoute([ + // 获取提货委托列表 + { + url: "/apply-delivery/list", + method: "get", + response: ({ query }) => { + let list = [...mockDeliveryList]; + + // 应用搜索条件 + if (query.orderNumber) { + const orderNumber = Array.isArray(query.orderNumber) + ? query.orderNumber[0] + : query.orderNumber; + list = list.filter(item => item.orderNumber.includes(orderNumber)); + } + if (query.blNumber) { + const blNumber = Array.isArray(query.blNumber) + ? query.blNumber[0] + : query.blNumber; + list = list.filter(item => item.blNumber.includes(blNumber)); + } + if (query.licensePlate) { + const licensePlate = Array.isArray(query.licensePlate) + ? query.licensePlate[0] + : query.licensePlate; + list = list.filter(item => item.licensePlate.includes(licensePlate)); + } + if (query.driverName) { + const driverName = Array.isArray(query.driverName) + ? query.driverName[0] + : query.driverName; + list = list.filter(item => item.driverName.includes(driverName)); + } + if (query.status) { + list = list.filter(item => item.status === query.status); + } + if (query.startDate && query.endDate) { + const startDateStr = Array.isArray(query.startDate) + ? query.startDate[0] + : query.startDate; + const endDateStr = Array.isArray(query.endDate) + ? query.endDate[0] + : query.endDate; + list = list.filter(item => { + const createDate = new Date(item.createTime.split(" ")[0]).getTime(); + const startDate = new Date(startDateStr).getTime(); + const endDate = new Date(endDateStr).getTime(); + return createDate >= startDate && createDate <= endDate; + }); + } + + // 分页处理 + const page = parseInt( + Array.isArray(query.page) + ? query.page[0] + : query.page !== undefined + ? query.page + : "1" + ); + const pageSize = parseInt( + Array.isArray(query.pageSize) + ? query.pageSize[0] + : query.pageSize !== undefined + ? query.pageSize + : "10" + ); + const total = list.length; + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedList = list.slice(startIndex, endIndex); + + return { + success: true, + message: "获取提货委托列表成功", + data: { + items: paginatedList, + total, + page, + pageSize + } + }; + } + }, + + // 获取提货委托详情 + { + url: "/apply-delivery/detail/:id", + method: "get", + response: ({ params }) => { + const item = mockDeliveryList.find(item => item.id === params.id); + + if (item) { + return { + success: true, + message: "获取提货委托详情成功", + data: item + }; + } + + return { + success: false, + message: "提货委托不存在" + }; + } + }, + + // 新增提货委托 + { + url: "/apply-delivery/create", + method: "post", + response: ({ body }) => { + const newDelivery = { + id: (mockDeliveryList.length + 1).toString(), + orderNumber: generateOrderNumber(), + blNumber: body.blNumber, + licensePlate: body.licensePlate, + driverName: body.driverName, + driverPhone: body.driverPhone, + driverIdCard: body.driverIdCard, + commodity: body.commodity, + quantity: body.quantity, + unit: body.unit, + deliveryDate: body.deliveryDate, + deliveryAddress: body.deliveryAddress, + warehouse: body.warehouse, + status: "pending", + createTime: new Date().toISOString().replace("T", " ").substring(0, 19), + updateTime: new Date().toISOString().replace("T", " ").substring(0, 19), + remark: body.remark || "" + }; + + mockDeliveryList.unshift(newDelivery); + + return { + success: true, + message: "提货委托创建成功", + data: { + id: newDelivery.id, + orderNumber: newDelivery.orderNumber + } + }; + } + }, + + // 更新提货委托 + { + url: "/apply-delivery/update/:id", + method: "put", + response: ({ body, params }) => { + const index = mockDeliveryList.findIndex(item => item.id === params.id); + + if (index !== -1) { + mockDeliveryList[index] = { + ...mockDeliveryList[index], + ...body, + updateTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19) + }; + + return { + success: true, + message: "提货委托更新成功", + data: mockDeliveryList[index] + }; + } + + return { + success: false, + message: "提货委托不存在" + }; + } + }, + + // 取消提货委托 + { + url: "/apply-delivery/cancel/:id", + method: "put", + response: ({ params }) => { + const index = mockDeliveryList.findIndex(item => item.id === params.id); + + if (index !== -1) { + if (mockDeliveryList[index].status === "cancelled") { + return { + success: false, + message: "提货委托已取消" + }; + } + + mockDeliveryList[index] = { + ...mockDeliveryList[index], + status: "cancelled", + updateTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19) + }; + + return { + success: true, + message: "提货委托取消成功", + data: mockDeliveryList[index] + }; + } + + return { + success: false, + message: "提货委托不存在" + }; + } + }, + + // 下载提货单模板 + { + url: "/apply-delivery/template/download", + method: "get", + response: () => { + return { + success: true, + message: "获取模板成功", + data: { + fileName: "提货单模板.xlsx", + url: "/templates/delivery-template.xlsx" + } + }; + } + }, + + // 导出提货委托列表 + { + url: "/apply-delivery/export", + method: "get", + response: () => { + return { + success: true, + message: "导出成功", + data: { + fileName: `提货委托列表_${new Date().toISOString().split("T")[0]}.xlsx`, + url: "/exports/delivery-list.xlsx" + } + }; + } + } +]); diff --git a/frontend/mock/asyncRoutes.ts b/frontend/mock/asyncRoutes.ts new file mode 100755 index 0000000..211b6eb --- /dev/null +++ b/frontend/mock/asyncRoutes.ts @@ -0,0 +1,69 @@ +// 模拟后端动态生成路由 +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +/** + * roles:页面级别权限,这里模拟二种 "admin"、"common" + * admin:管理员角色 + * common:普通角色 + */ +const permissionRouter = { + path: "/permission", + meta: { + title: "权限管理", + icon: "ep:lollipop", + rank: 10 + }, + children: [ + { + path: "/permission/page/index", + name: "PermissionPage", + meta: { + title: "页面权限", + roles: ["admin", "common"] + } + }, + { + path: "/permission/button", + meta: { + title: "按钮权限", + roles: ["admin", "common"] + }, + children: [ + { + path: "/permission/button/router", + component: "permission/button/index", + name: "PermissionButtonRouter", + meta: { + title: "路由返回按钮权限", + auths: [ + "permission:btn:add", + "permission:btn:edit", + "permission:btn:delete" + ] + } + }, + { + path: "/permission/button/login", + component: "permission/button/perms", + name: "PermissionButtonLogin", + meta: { + title: "登录接口返回按钮权限" + } + } + ] + } + ] +}; + +export default defineFakeRoute([ + { + url: "/get-async-routes", + method: "get", + response: () => { + return { + success: true, + data: [permissionRouter] + }; + } + } +]); diff --git a/frontend/mock/company.ts b/frontend/mock/company.ts new file mode 100755 index 0000000..6d89d86 --- /dev/null +++ b/frontend/mock/company.ts @@ -0,0 +1,360 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 模拟数据 +const mockFolders = [ + { + id: "1", + name: "营业执照", + count: 8, + createTime: "2024-01-15", + description: "所有公司的营业执照", + category: "license" + }, + { + id: "2", + name: "开票信息", + count: 12, + createTime: "2024-01-20", + description: "公司开票相关信息", + category: "invoice" + }, + { + id: "3", + name: "税务资质", + count: 6, + createTime: "2024-02-10", + description: "税务登记证等相关文件", + category: "tax" + }, + { + id: "4", + name: "银行资料", + count: 4, + createTime: "2024-02-15", + description: "开户许可证等银行资料", + category: "bank" + }, + { + id: "5", + name: "其他资质", + count: 5, + createTime: "2024-03-01", + description: "其他相关资质文件", + category: "other" + }, + { + id: "6", + name: "2023年归档", + count: 25, + createTime: "2023-12-31", + description: "2023年度所有文件" + } +]; + +const mockFiles = [ + { + id: "1", + fileName: "中尚鹏贸易有限公司-营业执照.pdf", + fileType: "pdf", + fileSize: "2.3MB", + uploadTime: "2024-01-20", + folderId: "1", + companyName: "中尚鹏贸易有限公司", + fileCategory: "license", + expireDate: "2027-12-31", + description: "营业执照正本扫描件" + }, + { + id: "2", + fileName: "中尚鹏-开票信息.pdf", + fileType: "pdf", + fileSize: "1.1MB", + uploadTime: "2024-01-25", + folderId: "2", + companyName: "中尚鹏贸易有限公司", + fileCategory: "invoice", + description: "增值税专用发票开票信息" + }, + { + id: "3", + fileName: "税务登记证-中尚鹏.jpg", + fileType: "jpg", + fileSize: "0.8MB", + uploadTime: "2024-02-12", + folderId: "3", + companyName: "中尚鹏贸易有限公司", + fileCategory: "tax", + expireDate: "2025-06-30", + description: "税务登记证扫描件" + }, + { + id: "4", + fileName: "开户许可证-中国银行.pdf", + fileType: "pdf", + fileSize: "1.5MB", + uploadTime: "2024-02-18", + folderId: "4", + companyName: "中尚鹏贸易有限公司", + fileCategory: "bank", + description: "基本户开户许可证" + }, + { + id: "5", + fileName: "公司章程-2024版.pdf", + fileType: "pdf", + fileSize: "2.1MB", + uploadTime: "2024-03-05", + folderId: "5", + companyName: "中尚鹏贸易有限公司", + fileCategory: "charter", + description: "最新版公司章程" + }, + { + id: "6", + fileName: "华东制造有限公司-营业执照.png", + fileType: "png", + fileSize: "1.2MB", + uploadTime: "2024-02-05", + folderId: "1", + companyName: "华东制造有限公司", + fileCategory: "license", + expireDate: "2026-11-30", + description: "合作公司营业执照" + }, + { + id: "7", + fileName: "南方制造-开票信息.pdf", + fileType: "pdf", + fileSize: "0.9MB", + uploadTime: "2024-02-08", + folderId: "2", + companyName: "南方制造有限公司", + fileCategory: "invoice", + description: "合作公司开票信息" + } +]; + +export default defineFakeRoute([ + // 获取文件夹列表 + { + url: "/company/folders", + method: "get", + response: () => { + return { + success: true, + message: "获取文件夹列表成功", + data: mockFolders + }; + } + }, + + // 获取文件列表 + { + url: "/company/files", + method: "get", + response: ({ query }) => { + let files = [...mockFiles]; + + // 根据文件夹ID过滤 + if (query.folderId) { + files = files.filter(file => file.folderId === query.folderId); + } + + return { + success: true, + message: "获取文件列表成功", + data: files + }; + } + }, + + // 创建文件夹 + { + url: "/company/folders", + method: "post", + response: ({ body }) => { + const newFolder = { + id: Date.now().toString(), + name: body.name, + count: 0, + createTime: new Date().toISOString().split("T")[0], + description: body.description, + category: body.category + }; + + mockFolders.push(newFolder); + + return { + success: true, + message: "文件夹创建成功", + data: newFolder + }; + } + }, + + // 更新文件夹 + { + url: "/company/folders/:id", + method: "put", + response: ({ body, params }) => { + const folderIndex = mockFolders.findIndex(f => f.id === params.id); + + if (folderIndex !== -1) { + mockFolders[folderIndex] = { + ...mockFolders[folderIndex], + ...body + }; + + return { + success: true, + message: "文件夹更新成功", + data: mockFolders[folderIndex] + }; + } + + return { + success: false, + message: "文件夹不存在" + }; + } + }, + + // 删除文件夹 + { + url: "/company/folders/:id", + method: "delete", + response: ({ params }) => { + const folderIndex = mockFolders.findIndex(f => f.id === params.id); + + if (folderIndex !== -1) { + // 将文件夹内的文件移动到未分类 + mockFiles.forEach(file => { + if (file.folderId === params.id) { + file.folderId = ""; + } + }); + + mockFolders.splice(folderIndex, 1); + + return { + success: true, + message: "文件夹删除成功" + }; + } + + return { + success: false, + message: "文件夹不存在" + }; + } + }, + + // 上传文件 + { + url: "/company/files/upload", + method: "post", + response: ({ body }) => { + try { + const data = JSON.parse(body.get("data")); + const files = body.getAll("files"); + + // 模拟处理每个文件 + files.forEach((file: any, index: number) => { + const newFile = { + id: (Date.now() + index).toString(), + fileName: file.name, + fileType: file.name.split(".").pop() || "unknown", + fileSize: `${(file.size / 1024 / 1024).toFixed(1)}MB`, + uploadTime: new Date().toISOString().split("T")[0], + folderId: data.folderId || "", + companyName: data.companyName, + fileCategory: data.fileCategory, + expireDate: data.expireDate, + description: data.description + }; + + mockFiles.unshift(newFile); + + // 更新文件夹计数 + if (data.folderId) { + const folder = mockFolders.find(f => f.id === data.folderId); + if (folder) { + folder.count++; + } + } + }); + + return { + success: true, + message: `成功上传 ${files.length} 个文件`, + data: { + uploadedCount: files.length + } + }; + } catch { + return { + success: false, + message: "文件上传失败" + }; + } + } + }, + + // 删除文件 + { + url: "/company/files/:id", + method: "delete", + response: ({ params }) => { + const fileIndex = mockFiles.findIndex(f => f.id === params.id); + + if (fileIndex !== -1) { + const file = mockFiles[fileIndex]; + + // 更新文件夹计数 + if (file.folderId) { + const folder = mockFolders.find(f => f.id === file.folderId); + if (folder && folder.count > 0) { + folder.count--; + } + } + + mockFiles.splice(fileIndex, 1); + + return { + success: true, + message: "文件删除成功" + }; + } + + return { + success: false, + message: "文件不存在" + }; + } + }, + + // 下载文件 + { + url: "/company/files/:id/download", + method: "get", + response: ({ params }) => { + const file = mockFiles.find(f => f.id === params.id); + + if (file) { + return { + success: true, + message: "文件下载成功", + data: { + fileName: file.fileName, + url: `/downloads/${file.fileName}` // 模拟下载链接 + } + }; + } + + return { + success: false, + message: "文件不存在" + }; + } + } +]); diff --git a/frontend/mock/contract.ts b/frontend/mock/contract.ts new file mode 100755 index 0000000..9afe648 --- /dev/null +++ b/frontend/mock/contract.ts @@ -0,0 +1,162 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 模拟数据 + +const mockContracts = [ + { + id: "1", + name: "铜矿采购合同", + contractType: "1", + contractCode: "SC2024001", + buyParty: "中尚鹏贸易有限公司", + sellParty: "矿业公司", + commodity: "铜矿", + count: "1000", + unitPrice: "5000", + BLNumber: "BL001;BL002", + totalAmount: "5000000", + status: "active" + }, + { + id: "2", + name: "铜材销售合同", + contractType: "3", + contractCode: "DX2024001", + buyParty: "华东制造有限公司", + sellParty: "中尚鹏贸易有限公司", + commodity: "铜材", + count: "800", + unitPrice: "5500", + BLNumber: "BL003", + totalAmount: "4400000", + status: "active" + }, + { + id: "3", + name: "铁矿进口合同", + contractType: "2", + contractCode: "FW2024001", + buyParty: "中尚鹏贸易有限公司", + sellParty: "澳大利亚矿业公司", + commodity: "铁矿", + count: "2000", + unitPrice: "800", + BLNumber: "BL004;BL005;BL006", + totalAmount: "1600000", + status: "pending" + } +]; + +export default defineFakeRoute([ + // 提交单个合同 + { + url: "/contracts/upload", + method: "post", + response: ({ body }) => { + const newContract = { + id: Date.now().toString(), + name: body.name, + contractType: body.contractType, + contractCode: body.contractCode, + buyParty: body.buyParty, + sellParty: body.sellParty, + commodity: body.commodity, + count: body.count, + unitPrice: body.unitPrice, + BLNumber: body.BLNumber, + totalAmount: ( + parseFloat(body.count || 0) * parseFloat(body.unitPrice || 0) + ).toFixed(2), + status: "active", + createdAt: new Date().toISOString() + }; + + mockContracts.unshift(newContract); + + return { + success: true, + message: `${body.name} 合同提交成功`, + data: { + id: newContract.id, + createdAt: newContract.createdAt + } + }; + } + }, + + // 批量上传合同 + { + url: "/contracts/batch-upload", + method: "post", + response: ({ body }) => { + const items = body.items || []; + const failedItems = items + .filter((item: any) => item.name && item.name.includes("fail")) + .map((item: any) => ({ + name: item.name, + reason: "Invalid format" + })); + + const successItems = items.filter( + (item: any) => !item.name.includes("fail") + ); + + // 模拟添加成功合同 + successItems.forEach((item: any) => { + mockContracts.push({ + id: Date.now().toString() + Math.random(), + ...item, + status: "active", + createdAt: new Date().toISOString() + }); + }); + + return { + success: true, + message: "批量上传完成", + data: { + successCount: successItems.length, + failedItems + } + }; + } + }, + + // 获取合同列表 + { + url: "/contracts/list", + method: "get", + response: ({ query }) => { + let contracts = [...mockContracts]; + + // 简单筛选逻辑 + if (query.search) { + const search = ( + Array.isArray(query.search) ? query.search[0] : query.search + ).toLowerCase(); + contracts = contracts.filter( + contract => + contract.name.toLowerCase().includes(search) || + contract.contractCode.toLowerCase().includes(search) || + contract.buyParty.toLowerCase().includes(search) || + contract.sellParty.toLowerCase().includes(search) + ); + } + + if (query.contractType) { + contracts = contracts.filter( + contract => contract.contractType === query.contractType + ); + } + + return { + success: true, + message: "获取合同列表成功", + data: { + items: contracts, + total: contracts.length + } + }; + } + } +]); diff --git a/frontend/mock/deliveryDetails.ts b/frontend/mock/deliveryDetails.ts new file mode 100755 index 0000000..06e3ad3 --- /dev/null +++ b/frontend/mock/deliveryDetails.ts @@ -0,0 +1,559 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 生成出库单号 +const generateOutboundNumber = () => { + const date = new Date(); + const year = date.getFullYear().toString().slice(-2); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const random = Math.floor(Math.random() * 1000) + .toString() + .padStart(3, "0"); + return `CK${year}${month}${day}${random}`; +}; + +// 模拟提货明细数据 +const mockDeliveryDetails = [ + { + id: "1", + outboundNumber: generateOutboundNumber(), + deliveryOrderNumber: "TH240310001", + blNumber: "BL20240001", + licensePlate: "京A12345", + driverName: "张师傅", + driverPhone: "13800138001", + commodity: "copper_ore", + commodityCode: "CU-20240310-001", + batchNumber: "BATCH001", + quantity: 100, + unit: "ton", + unitPrice: 5000, + totalAmount: 500000, + outboundDate: "2024-03-10", + outboundTime: "09:30:00", + warehouse: "chaoyang", + location: "A-01-01", + operator: "王管理员", + outboundStatus: "completed", + receiptStatus: "completed", + receivedQuantity: 100, + receiptDate: "2024-03-11", + receiver: "李收货员", + remark: "第一批铜矿出库", + createTime: "2024-03-10 09:30:00", + updateTime: "2024-03-11 10:15:00" + }, + { + id: "2", + outboundNumber: generateOutboundNumber(), + deliveryOrderNumber: "TH240310002", + blNumber: "BL20240002", + licensePlate: "沪B23456", + driverName: "李师傅", + driverPhone: "13800138002", + commodity: "aluminum_material", + commodityCode: "AL-20240310-001", + batchNumber: "BATCH002", + quantity: 50, + unit: "ton", + unitPrice: 15000, + totalAmount: 750000, + outboundDate: "2024-03-10", + outboundTime: "10:45:00", + warehouse: "pudong", + location: "B-02-01", + operator: "王管理员", + outboundStatus: "completed", + receiptStatus: "partial", + receivedQuantity: 30, + receiptDate: "2024-03-12", + receiver: "张收货员", + remark: "铝材出库,分批收货", + createTime: "2024-03-10 10:45:00", + updateTime: "2024-03-12 14:20:00" + }, + { + id: "3", + outboundNumber: generateOutboundNumber(), + deliveryOrderNumber: "TH240311001", + blNumber: "BL20240003", + licensePlate: "粤C34567", + driverName: "王师傅", + driverPhone: "13800138003", + commodity: "steel", + commodityCode: "ST-20240311-001", + batchNumber: "BATCH003", + quantity: 80, + unit: "ton", + unitPrice: 4500, + totalAmount: 360000, + outboundDate: "2024-03-11", + outboundTime: "14:20:00", + warehouse: "tianhe", + location: "C-03-01", + operator: "赵操作员", + outboundStatus: "completed", + receiptStatus: "pending", + receivedQuantity: 0, + receiptDate: "", + receiver: "", + createTime: "2024-03-11 14:20:00", + updateTime: "2024-03-11 14:20:00" + }, + { + id: "4", + outboundNumber: generateOutboundNumber(), + deliveryOrderNumber: "TH240311002", + blNumber: "BL20240004", + licensePlate: "浙D45678", + driverName: "赵师傅", + driverPhone: "13800138004", + commodity: "copper_material", + commodityCode: "CU-M-20240311-001", + batchNumber: "BATCH004", + quantity: 60, + unit: "ton", + unitPrice: 5500, + totalAmount: 330000, + outboundDate: "2024-03-11", + outboundTime: "16:30:00", + warehouse: "xihu", + location: "D-04-01", + operator: "钱操作员", + outboundStatus: "in_progress", + receiptStatus: "pending", + receivedQuantity: 0, + receiptDate: "", + receiver: "", + remark: "正在出库中", + createTime: "2024-03-11 16:30:00", + updateTime: "2024-03-11 16:30:00" + }, + { + id: "5", + outboundNumber: generateOutboundNumber(), + deliveryOrderNumber: "TH240312001", + blNumber: "BL20240005", + licensePlate: "苏E56789", + driverName: "刘师傅", + driverPhone: "13800138005", + commodity: "iron_ore", + commodityCode: "IR-20240312-001", + batchNumber: "BATCH005", + quantity: 120, + unit: "ton", + unitPrice: 800, + totalAmount: 96000, + outboundDate: "2024-03-12", + outboundTime: "09:15:00", + warehouse: "gulou", + location: "E-05-01", + operator: "孙操作员", + outboundStatus: "pending", + receiptStatus: "pending", + receivedQuantity: 0, + receiptDate: "", + receiver: "", + remark: "待出库", + createTime: "2024-03-12 09:15:00", + updateTime: "2024-03-12 09:15:00" + } +]; + +// 收货历史记录 +const mockReceiptHistory = [ + { + id: "1", + deliveryDetailId: "1", + receiptDate: "2024-03-11", + receivedQuantity: 100, + receiver: "李收货员", + remark: "全部收货完成", + createTime: "2024-03-11 10:15:00" + }, + { + id: "2", + deliveryDetailId: "2", + receiptDate: "2024-03-12", + receivedQuantity: 30, + receiver: "张收货员", + remark: "第一批收货", + createTime: "2024-03-12 14:20:00" + } +]; + +export default defineFakeRoute([ + // 获取提货明细列表 + { + url: "/delivery-details/list", + method: "get", + response: ({ query }) => { + let list = [...mockDeliveryDetails]; + + // 应用搜索条件 + if (query.outboundNumber) { + const outboundNumber = Array.isArray(query.outboundNumber) + ? query.outboundNumber[0] + : query.outboundNumber; + list = list.filter(item => + item.outboundNumber.includes(outboundNumber) + ); + } + if (query.deliveryOrderNumber) { + const deliveryOrderNumber = Array.isArray(query.deliveryOrderNumber) + ? query.deliveryOrderNumber[0] + : query.deliveryOrderNumber; + list = list.filter(item => + item.deliveryOrderNumber.includes(deliveryOrderNumber) + ); + } + if (query.blNumber) { + const blNumber = Array.isArray(query.blNumber) + ? query.blNumber[0] + : query.blNumber; + list = list.filter(item => item.blNumber.includes(blNumber)); + } + if (query.licensePlate) { + const licensePlate = Array.isArray(query.licensePlate) + ? query.licensePlate[0] + : query.licensePlate; + list = list.filter(item => item.licensePlate.includes(licensePlate)); + } + if (query.commodity) { + list = list.filter(item => item.commodity === query.commodity); + } + if (query.outboundStatus) { + list = list.filter( + item => item.outboundStatus === query.outboundStatus + ); + } + if (query.receiptStatus) { + list = list.filter(item => item.receiptStatus === query.receiptStatus); + } + if (query.startDate && query.endDate) { + list = list.filter(item => { + const outboundDate = new Date(item.outboundDate).getTime(); + const startDate = new Date( + Array.isArray(query.startDate) + ? query.startDate[0] + : query.startDate + ).getTime(); + const endDate = new Date( + Array.isArray(query.endDate) ? query.endDate[0] : query.endDate + ).getTime(); + return outboundDate >= startDate && outboundDate <= endDate; + }); + } + + // 分页处理 + const page = parseInt( + Array.isArray(query.page) ? query.page[0] : (query.page ?? "1") + ); + const pageSize = parseInt( + Array.isArray(query.pageSize) + ? query.pageSize[0] + : (query.pageSize ?? "10") + ); + const total = list.length; + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedList = list.slice(startIndex, endIndex); + + return { + success: true, + message: "获取提货明细列表成功", + data: { + items: paginatedList, + total, + page, + pageSize + } + }; + } + }, + + // 获取提货明细详情 + { + url: "/delivery-details/detail/:id", + method: "get", + response: ({ params }) => { + const item = mockDeliveryDetails.find(item => item.id === params.id); + + if (item) { + return { + success: true, + message: "获取提货明细详情成功", + data: item + }; + } + + return { + success: false, + message: "提货明细不存在" + }; + } + }, + + // 新增出库单 + { + url: "/delivery-details/create", + method: "post", + response: ({ body }) => { + try { + const items = body.items || []; + + // 为每个出库项创建出库单 + const createdItems = items.map((item: any) => { + const newDeliveryDetail = { + id: (mockDeliveryDetails.length + 1).toString(), + outboundNumber: generateOutboundNumber(), + deliveryOrderNumber: body.deliveryOrderNumber, + blNumber: body.blNumber, + licensePlate: body.licensePlate, + driverName: body.driverName, + driverPhone: body.driverPhone, + commodity: item.commodity, + commodityCode: item.commodityCode || "", + batchNumber: item.batchNumber || "", + quantity: item.quantity, + unit: item.unit, + unitPrice: item.unitPrice, + totalAmount: item.quantity * item.unitPrice, + outboundDate: body.outboundDate, + outboundTime: new Date() + .toTimeString() + .split(" ")[0] + .substring(0, 8), + warehouse: body.warehouse, + location: item.location || "", + operator: body.operator, + outboundStatus: "pending", + receiptStatus: "pending", + receivedQuantity: 0, + receiptDate: "", + receiver: "", + remark: body.remark || item.remark || "", + createTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19), + updateTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19) + }; + + mockDeliveryDetails.unshift(newDeliveryDetail); + return newDeliveryDetail; + }); + + return { + success: true, + message: "出库单创建成功", + data: { + items: createdItems, + count: createdItems.length + } + }; + } catch { + return { + success: false, + message: "出库单创建失败" + }; + } + } + }, + + // 更新出库单 + { + url: "/delivery-details/update/:id", + method: "put", + response: ({ body, params }) => { + const index = mockDeliveryDetails.findIndex( + item => item.id === params.id + ); + + if (index !== -1) { + // 更新出库单信息 + mockDeliveryDetails[index] = { + ...mockDeliveryDetails[index], + ...body, + updateTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19) + }; + + // 如果更新了数量或单价,重新计算总金额 + if (body.quantity || body.unitPrice) { + const quantity = body.quantity || mockDeliveryDetails[index].quantity; + const unitPrice = + body.unitPrice || mockDeliveryDetails[index].unitPrice; + mockDeliveryDetails[index].totalAmount = quantity * unitPrice; + } + + return { + success: true, + message: "出库单更新成功", + data: mockDeliveryDetails[index] + }; + } + + return { + success: false, + message: "出库单不存在" + }; + } + }, + + // 收货确认 + { + url: "/delivery-details/confirm-receipt/:id", + method: "put", + response: ({ body, params }) => { + const index = mockDeliveryDetails.findIndex( + item => item.id === params.id + ); + + if (index !== -1) { + const deliveryDetail = mockDeliveryDetails[index]; + const receivedQuantity = body.receivedQuantity; + + // 计算总收货数量 + const totalReceived = + deliveryDetail.receivedQuantity + receivedQuantity; + + // 更新收货状态 + let newReceiptStatus = "partial"; + if (totalReceived >= deliveryDetail.quantity) { + newReceiptStatus = "completed"; + } else if (totalReceived === 0) { + newReceiptStatus = "pending"; + } + + // 检查是否有差异 + if (totalReceived !== deliveryDetail.quantity && totalReceived > 0) { + newReceiptStatus = "discrepancy"; + } + + // 更新记录 + mockDeliveryDetails[index] = { + ...deliveryDetail, + receivedQuantity: totalReceived, + receiptDate: + body.receiptDate || new Date().toISOString().split("T")[0], + receiver: body.receiver, + receiptStatus: newReceiptStatus, + updateTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19) + }; + + // 添加到收货历史记录 + const newReceiptRecord = { + id: (mockReceiptHistory.length + 1).toString(), + deliveryDetailId: Array.isArray(params.id) ? params.id[0] : params.id, + receiptDate: body.receiptDate, + receivedQuantity: receivedQuantity, + receiver: body.receiver, + remark: body.discrepancyReason || body.remark || "", + createTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19) + }; + + mockReceiptHistory.push(newReceiptRecord); + + return { + success: true, + message: "收货确认成功", + data: { + deliveryDetail: mockDeliveryDetails[index], + receiptRecord: newReceiptRecord + } + }; + } + + return { + success: false, + message: "出库单不存在" + }; + } + }, + + // 获取收货历史记录 + { + url: "/delivery-details/receipt-history/:id", + method: "get", + response: ({ params }) => { + const history = mockReceiptHistory.filter( + record => record.deliveryDetailId === params.id + ); + + return { + success: true, + message: "获取收货历史记录成功", + data: history + }; + } + }, + + // 下载出库单模板 + { + url: "/delivery-details/template/download", + method: "get", + response: () => { + return { + success: true, + message: "获取模板成功", + data: { + fileName: "出库单模板.xlsx", + url: "/templates/outbound-order-template.xlsx" + } + }; + } + }, + + // 导出提货明细 + { + url: "/delivery-details/export", + method: "get", + response: () => { + return { + success: true, + message: "导出成功", + data: { + fileName: `提货明细_${new Date().toISOString().split("T")[0]}.xlsx`, + url: "/exports/delivery-details.xlsx" + } + }; + } + }, + + // 打印提货单 + { + url: "/delivery-details/print/:id", + method: "get", + response: ({ params }) => { + const item = mockDeliveryDetails.find(item => item.id === params.id); + + if (item) { + return { + success: true, + message: "打印任务已创建", + data: { + fileName: `${item.outboundNumber}_提货单.pdf`, + url: `/print/delivery-order-${params.id}.pdf` + } + }; + } + + return { + success: false, + message: "出库单不存在" + }; + } + } +]); diff --git a/frontend/mock/inventory.ts b/frontend/mock/inventory.ts new file mode 100755 index 0000000..1463c2e --- /dev/null +++ b/frontend/mock/inventory.ts @@ -0,0 +1,864 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 生成入库单号 +const generateInboundNumber = () => { + const date = new Date(); + const year = date.getFullYear().toString().slice(-2); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const random = Math.floor(Math.random() * 1000) + .toString() + .padStart(3, "0"); + return `RK${year}${month}${day}${random}`; +}; + +// 生成货权转移单号 +const generateTransferNumber = () => { + const date = new Date(); + const year = date.getFullYear().toString().slice(-2); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const random = Math.floor(Math.random() * 1000) + .toString() + .padStart(3, "0"); + return `ZY${year}${month}${day}${random}`; +}; + +// 模拟数据 +const mockWarehouses = [ + { + id: "1", + code: "WH001", + name: "朝阳仓库", + address: "北京市朝阳区仓库路1号", + contactPerson: "张经理", + contactPhone: "13800138001", + capacity: 5000, + usedCapacity: 3200, + status: "active", + description: "主仓库,存放铜矿和铜材", + createTime: "2024-01-15 09:30:00", + updateTime: "2024-03-10 14:20:00" + }, + { + id: "2", + code: "WH002", + name: "浦东仓库", + address: "上海市浦东新区仓库路2号", + contactPerson: "李主任", + contactPhone: "13800138002", + capacity: 3000, + usedCapacity: 1800, + status: "active", + description: "铝材专用仓库", + createTime: "2024-01-20 10:15:00", + updateTime: "2024-03-11 16:45:00" + }, + { + id: "3", + code: "WH003", + name: "天河仓库", + address: "广州市天河区仓库路3号", + contactPerson: "王主管", + contactPhone: "13800138003", + capacity: 4000, + usedCapacity: 2500, + status: "active", + description: "铁矿和钢材仓库", + createTime: "2024-02-10 14:30:00", + updateTime: "2024-03-12 11:10:00" + }, + { + id: "4", + code: "WH004", + name: "西湖仓库", + address: "杭州市西湖区仓库路4号", + contactPerson: "赵经理", + contactPhone: "13800138004", + capacity: 2000, + usedCapacity: 800, + status: "active", + description: "小型仓库,存放其他商品", + createTime: "2024-02-15 16:20:00", + updateTime: "2024-03-13 09:45:00" + }, + { + id: "5", + code: "WH005", + name: "鼓楼仓库", + address: "南京市鼓楼区仓库路5号", + contactPerson: "刘主任", + contactPhone: "13800138005", + capacity: 3500, + usedCapacity: 1200, + status: "maintenance", + description: "正在维护中,暂停使用", + createTime: "2024-02-20 11:45:00", + updateTime: "2024-03-14 15:30:00" + } +]; + +const mockInventoryInbound = [ + { + id: "1", + inboundNumber: generateInboundNumber(), + warehouse: "1", + commodity: "copper_ore", + commodityCode: "CU-20240310-001", + blNumber: "BL20240001", + blWeight: 100, + inboundWeight: 98.5, + owner: "中尚鹏贸易有限公司", + contractNumber: "SC2024001", + warehouseAddress: "北京市朝阳区仓库路1号", + packaging: "bulk", + inboundDate: "2024-03-10", + operator: "王管理员", + remark: "第一批铜矿入库", + createTime: "2024-03-10 09:30:00", + updateTime: "2024-03-10 09:30:00" + }, + { + id: "2", + inboundNumber: generateInboundNumber(), + warehouse: "2", + commodity: "aluminum_material", + commodityCode: "AL-20240310-001", + blNumber: "BL20240002", + blWeight: 50, + inboundWeight: 49.8, + owner: "中尚鹏贸易有限公司", + contractNumber: "SC2024002", + packaging: "ton_bag", + inboundDate: "2024-03-10", + operator: "李操作员", + remark: "铝材入库", + createTime: "2024-03-10 14:45:00", + updateTime: "2024-03-10 14:45:00" + }, + { + id: "3", + inboundNumber: generateInboundNumber(), + warehouse: "3", + commodity: "steel", + commodityCode: "ST-20240311-001", + blNumber: "BL20240003", + blWeight: 80, + inboundWeight: 79.2, + owner: "华东制造有限公司", + contractNumber: "HC2024001", + warehouseAddress: "广州市天河区仓库路3号", + packaging: "container", + inboundDate: "2024-03-11", + operator: "张操作员", + createTime: "2024-03-11 10:15:00", + updateTime: "2024-03-11 10:15:00" + }, + { + id: "4", + inboundNumber: generateInboundNumber(), + warehouse: "1", + commodity: "copper_material", + commodityCode: "CU-M-20240312-001", + blNumber: "BL20240004", + blWeight: 60, + inboundWeight: 59.5, + owner: "中尚鹏贸易有限公司", + contractNumber: "SC2024003", + packaging: "pallet", + inboundDate: "2024-03-12", + operator: "王管理员", + remark: "铜材入库", + createTime: "2024-03-12 14:20:00", + updateTime: "2024-03-12 14:20:00" + }, + { + id: "5", + inboundNumber: generateInboundNumber(), + warehouse: "4", + commodity: "other", + commodityCode: "OT-20240313-001", + blNumber: "BL20240005", + blWeight: 30, + inboundWeight: 29.8, + owner: "其他公司", + contractNumber: "QT2024001", + packaging: "woven_bag", + inboundDate: "2024-03-13", + operator: "赵操作员", + createTime: "2024-03-13 09:10:00", + updateTime: "2024-03-13 09:10:00" + } +]; + +const mockInventoryDetails = [ + { + id: "1", + warehouse: "1", + commodity: "copper_ore", + totalInbound: 198.5, + totalOutbound: 100, + totalTransfer: 20, + currentStock: 78.5, + unit: "ton", + lastInboundDate: "2024-03-12", + lastOutboundDate: "2024-03-15", + updateTime: "2024-03-15 14:30:00" + }, + { + id: "2", + warehouse: "1", + commodity: "copper_material", + totalInbound: 59.5, + totalOutbound: 30, + totalTransfer: 5, + currentStock: 24.5, + unit: "ton", + lastInboundDate: "2024-03-12", + lastOutboundDate: "2024-03-14", + updateTime: "2024-03-14 11:20:00" + }, + { + id: "3", + warehouse: "2", + commodity: "aluminum_material", + totalInbound: 49.8, + totalOutbound: 20, + totalTransfer: 10, + currentStock: 19.8, + unit: "ton", + lastInboundDate: "2024-03-10", + lastOutboundDate: "2024-03-13", + updateTime: "2024-03-13 16:45:00" + }, + { + id: "4", + warehouse: "3", + commodity: "steel", + totalInbound: 79.2, + totalOutbound: 40, + totalTransfer: 15, + currentStock: 24.2, + unit: "ton", + lastInboundDate: "2024-03-11", + lastOutboundDate: "2024-03-12", + updateTime: "2024-03-12 15:30:00" + }, + { + id: "5", + warehouse: "4", + commodity: "other", + totalInbound: 29.8, + totalOutbound: 10, + totalTransfer: 5, + currentStock: 14.8, + unit: "ton", + lastInboundDate: "2024-03-13", + lastOutboundDate: "2024-03-14", + updateTime: "2024-03-14 09:45:00" + } +]; + +const mockCargoTransfers = [ + { + id: "1", + transferNumber: generateTransferNumber(), + fromWarehouse: "1", + toWarehouse: "2", + commodity: "copper_ore", + transferWeight: 20, + transferDate: "2024-03-15", + operator: "王管理员", + remark: "库存调整", + createTime: "2024-03-15 10:30:00", + updateTime: "2024-03-15 10:30:00" + }, + { + id: "2", + transferNumber: generateTransferNumber(), + fromWarehouse: "2", + toWarehouse: "3", + commodity: "aluminum_material", + transferWeight: 10, + transferDate: "2024-03-14", + operator: "李操作员", + remark: "客户要求转移", + createTime: "2024-03-14 14:20:00", + updateTime: "2024-03-14 14:20:00" + } +]; + +// 辅助函数:获取查询参数的字符串值 +const getStringParam = ( + param: string | string[] | undefined +): string | undefined => { + if (Array.isArray(param)) { + return param[0]; + } + return param; +}; + +// 辅助函数:获取查询参数的数值 +const getNumberParam = ( + param: string | string[] | undefined, + defaultValue: number +): number => { + const str = getStringParam(param); + if (str) { + const num = parseInt(str, 10); + return isNaN(num) ? defaultValue : num; + } + return defaultValue; +}; + +// 辅助函数:获取日期参数的字符串值 +const getDateStringParam = ( + param: string | string[] | undefined +): string | undefined => { + const str = getStringParam(param); + return str; +}; + +// 辅助函数:更新库存汇总 +const updateInventorySummary = ( + warehouse: string, + commodity: string, + inbound: number, + outbound: number, + transfer: number +) => { + let summary = mockInventoryDetails.find( + item => item.warehouse === warehouse && item.commodity === commodity + ); + + const now = new Date().toISOString().replace("T", " ").substring(0, 19); + const today = new Date().toISOString().split("T")[0]; + + if (summary) { + summary.totalInbound += inbound; + summary.totalOutbound += outbound; + summary.totalTransfer += transfer; + summary.currentStock = + summary.totalInbound - summary.totalOutbound - summary.totalTransfer; + summary.updateTime = now; + + if (inbound > 0) { + summary.lastInboundDate = today; + } + if (outbound > 0) { + summary.lastOutboundDate = today; + } + } else { + summary = { + id: (mockInventoryDetails.length + 1).toString(), + warehouse, + commodity, + totalInbound: inbound, + totalOutbound: outbound, + totalTransfer: transfer, + currentStock: inbound - outbound - transfer, + unit: "ton", + lastInboundDate: inbound > 0 ? today : undefined, + lastOutboundDate: outbound > 0 ? today : undefined, + updateTime: now + }; + + mockInventoryDetails.push(summary); + } +}; + +export default defineFakeRoute([ + // 获取入库单列表 + { + url: "/inventory/inbound/list", + method: "get", + response: ({ query }) => { + let list = [...mockInventoryInbound]; + + // 应用搜索条件 + const inboundNumber = getStringParam(query.inboundNumber); + const warehouse = getStringParam(query.warehouse); + const commodity = getStringParam(query.commodity); + const blNumber = getStringParam(query.blNumber); + const owner = getStringParam(query.owner); + const contractNumber = getStringParam(query.contractNumber); + const startDate = getDateStringParam(query.startDate); + const endDate = getDateStringParam(query.endDate); + + if (inboundNumber) { + list = list.filter(item => item.inboundNumber.includes(inboundNumber)); + } + if (warehouse) { + list = list.filter(item => item.warehouse === warehouse); + } + if (commodity) { + list = list.filter(item => item.commodity === commodity); + } + if (blNumber) { + list = list.filter(item => item.blNumber.includes(blNumber)); + } + if (owner) { + list = list.filter(item => item.owner.includes(owner)); + } + if (contractNumber) { + list = list.filter(item => + item.contractNumber.includes(contractNumber) + ); + } + if (startDate && endDate) { + list = list.filter(item => { + const inboundDate = new Date(item.inboundDate).getTime(); + const start = new Date(startDate).getTime(); + const end = new Date(endDate).getTime(); + return inboundDate >= start && inboundDate <= end; + }); + } + + // 分页处理 + const page = getNumberParam(query.page, 1); + const pageSize = getNumberParam(query.pageSize, 10); + const total = list.length; + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedList = list.slice(startIndex, endIndex); + + return { + success: true, + message: "获取入库单列表成功", + data: { + items: paginatedList, + total, + page, + pageSize + } + }; + } + }, + + // 获取入库单详情 + { + url: "/inventory/inbound/detail/:id", + method: "get", + response: ({ params }) => { + const item = mockInventoryInbound.find(item => item.id === params.id); + + if (item) { + return { + success: true, + message: "获取入库单详情成功", + data: item + }; + } + + return { + success: false, + message: "入库单不存在" + }; + } + }, + + // 新增入库单 + { + url: "/inventory/inbound/create", + method: "post", + response: ({ body }) => { + const newInbound = { + id: (mockInventoryInbound.length + 1).toString(), + inboundNumber: generateInboundNumber(), + warehouse: body.warehouse, + commodity: body.commodity, + commodityCode: body.commodityCode || "", + blNumber: body.blNumber, + blWeight: body.blWeight, + inboundWeight: body.inboundWeight, + owner: body.owner, + contractNumber: body.contractNumber, + warehouseAddress: body.warehouseAddress || "", + packaging: body.packaging || "bulk", + inboundDate: body.inboundDate, + operator: body.operator, + remark: body.remark || "", + createTime: new Date().toISOString().replace("T", " ").substring(0, 19), + updateTime: new Date().toISOString().replace("T", " ").substring(0, 19) + }; + + mockInventoryInbound.unshift(newInbound); + + // 更新仓库使用容量 + const warehouse = mockWarehouses.find(w => w.id === body.warehouse); + if (warehouse) { + warehouse.usedCapacity += body.inboundWeight; + warehouse.updateTime = new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19); + } + + // 更新库存明细 + updateInventorySummary( + body.warehouse, + body.commodity, + body.inboundWeight, + 0, + 0 + ); + + return { + success: true, + message: "入库单创建成功", + data: newInbound + }; + } + }, + + // 删除入库单 + { + url: "/inventory/inbound/delete/:id", + method: "delete", + response: ({ params }) => { + const index = mockInventoryInbound.findIndex( + item => item.id === params.id + ); + + if (index !== -1) { + const item = mockInventoryInbound[index]; + + // 恢复仓库使用容量 + const warehouse = mockWarehouses.find(w => w.id === item.warehouse); + if (warehouse) { + warehouse.usedCapacity -= item.inboundWeight; + if (warehouse.usedCapacity < 0) warehouse.usedCapacity = 0; + warehouse.updateTime = new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19); + } + + // 更新库存明细 + updateInventorySummary( + item.warehouse, + item.commodity, + -item.inboundWeight, + 0, + 0 + ); + + mockInventoryInbound.splice(index, 1); + + return { + success: true, + message: "入库单删除成功" + }; + } + + return { + success: false, + message: "入库单不存在" + }; + } + }, + + // 获取仓库列表 + { + url: "/inventory/warehouses", + method: "get", + response: ({ query }) => { + let list = [...mockWarehouses]; + + // 应用搜索条件 + const name = getStringParam(query.name); + const code = getStringParam(query.code); + const status = getStringParam(query.status); + + if (name) { + list = list.filter(item => item.name.includes(name)); + } + if (code) { + list = list.filter(item => item.code.includes(code)); + } + if (status) { + list = list.filter(item => item.status === status); + } + + return { + success: true, + message: "获取仓库列表成功", + data: list + }; + } + }, + + // 创建仓库 + { + url: "/inventory/warehouses/create", + method: "post", + response: ({ body }) => { + const newWarehouse = { + id: (mockWarehouses.length + 1).toString(), + code: body.code, + name: body.name, + address: body.address, + contactPerson: body.contactPerson, + contactPhone: body.contactPhone, + capacity: body.capacity, + usedCapacity: 0, + status: "active", + description: body.description || "", + createTime: new Date().toISOString().replace("T", " ").substring(0, 19), + updateTime: new Date().toISOString().replace("T", " ").substring(0, 19) + }; + + mockWarehouses.push(newWarehouse); + + return { + success: true, + message: "仓库创建成功", + data: newWarehouse + }; + } + }, + + // 更新仓库 + { + url: "/inventory/warehouses/update/:id", + method: "put", + response: ({ body, params }) => { + const index = mockWarehouses.findIndex(item => item.id === params.id); + + if (index !== -1) { + mockWarehouses[index] = { + ...mockWarehouses[index], + ...body, + updateTime: new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19) + }; + + return { + success: true, + message: "仓库更新成功", + data: mockWarehouses[index] + }; + } + + return { + success: false, + message: "仓库不存在" + }; + } + }, + + // 删除仓库 + { + url: "/inventory/warehouses/delete/:id", + method: "delete", + response: ({ params }) => { + const index = mockWarehouses.findIndex(item => item.id === params.id); + + if (index !== -1) { + if (mockWarehouses[index].usedCapacity > 0) { + return { + success: false, + message: "该仓库仍有库存,不能删除" + }; + } + + mockWarehouses.splice(index, 1); + + return { + success: true, + message: "仓库删除成功" + }; + } + + return { + success: false, + message: "仓库不存在" + }; + } + }, + + // 获取库存明细 + { + url: "/inventory/summary", + method: "get", + response: ({ query }) => { + let list = [...mockInventoryDetails]; + + // 应用搜索条件 + const warehouse = getStringParam(query.warehouse); + const commodity = getStringParam(query.commodity); + + if (warehouse) { + list = list.filter(item => item.warehouse === warehouse); + } + if (commodity) { + list = list.filter(item => item.commodity === commodity); + } + + return { + success: true, + message: "获取库存明细成功", + data: list + }; + } + }, + + // 创建货权转移 + { + url: "/inventory/transfer/create", + method: "post", + response: ({ body }) => { + // 检查转出仓库库存 + const fromStock = mockInventoryDetails.find( + item => + item.warehouse === body.fromWarehouse && + item.commodity === body.commodity + ); + + if (!fromStock || fromStock.currentStock < body.transferWeight) { + return { + success: false, + message: "转出仓库库存不足" + }; + } + + // 检查转入仓库容量 + const toWarehouse = mockWarehouses.find(w => w.id === body.toWarehouse); + if ( + toWarehouse && + toWarehouse.usedCapacity + body.transferWeight > toWarehouse.capacity + ) { + return { + success: false, + message: "转入仓库容量不足" + }; + } + + const newTransfer = { + id: (mockCargoTransfers.length + 1).toString(), + transferNumber: generateTransferNumber(), + fromWarehouse: body.fromWarehouse, + toWarehouse: body.toWarehouse, + commodity: body.commodity, + transferWeight: body.transferWeight, + transferDate: body.transferDate, + operator: body.operator, + remark: body.remark || "", + createTime: new Date().toISOString().replace("T", " ").substring(0, 19), + updateTime: new Date().toISOString().replace("T", " ").substring(0, 19) + }; + + mockCargoTransfers.push(newTransfer); + + // 更新转出仓库使用容量 + const fromWarehouse = mockWarehouses.find( + w => w.id === body.fromWarehouse + ); + if (fromWarehouse) { + fromWarehouse.usedCapacity -= body.transferWeight; + if (fromWarehouse.usedCapacity < 0) fromWarehouse.usedCapacity = 0; + fromWarehouse.updateTime = new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19); + } + + // 更新转入仓库使用容量 + const toWarehouse2 = mockWarehouses.find(w => w.id === body.toWarehouse); + if (toWarehouse2) { + toWarehouse2.usedCapacity += body.transferWeight; + toWarehouse2.updateTime = new Date() + .toISOString() + .replace("T", " ") + .substring(0, 19); + } + + // 更新库存明细 + updateInventorySummary( + body.fromWarehouse, + body.commodity, + 0, + 0, + body.transferWeight + ); + updateInventorySummary( + body.toWarehouse, + body.commodity, + body.transferWeight, + 0, + 0 + ); + + return { + success: true, + message: "货权转移成功", + data: newTransfer + }; + } + }, + + // 下载入库单模板 + { + url: "/inventory/template/download", + method: "get", + response: () => { + return { + success: true, + message: "获取模板成功", + data: { + fileName: "入库单模板.xlsx", + url: "/templates/inventory-inbound-template.xlsx" + } + }; + } + }, + + // 导出库存数据 + { + url: "/inventory/export", + method: "get", + response: () => { + return { + success: true, + message: "导出成功", + data: { + fileName: `库存明细_${new Date().toISOString().split("T")[0]}.xlsx`, + url: "/exports/inventory-details.xlsx" + } + }; + } + }, + + // 批量上传文件 + { + url: "/inventory/upload", + method: "post", + response: ({ body }) => { + try { + // const warehouse = getStringParam(body.get("warehouse")); + // const operator = getStringParam(body.get("operator")); + const files = body.getAll("files"); + + // 模拟处理文件 + const processedCount = files.length; + + return { + success: true, + message: "批量上传成功", + data: { + processedCount, + successCount: processedCount, + failedCount: 0 + } + }; + } catch { + return { + success: false, + message: "批量上传失败" + }; + } + } + } +]); diff --git a/frontend/mock/login.ts b/frontend/mock/login.ts new file mode 100755 index 0000000..64519ab --- /dev/null +++ b/frontend/mock/login.ts @@ -0,0 +1,42 @@ +// 根据角色动态生成路由 +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +export default defineFakeRoute([ + { + url: "/login", + method: "post", + response: ({ body }) => { + if (body.username === "admin") { + return { + success: true, + data: { + avatar: "https://avatars.githubusercontent.com/u/44761321", + username: "admin", + nickname: "小铭", + // 一个用户可能有多个角色 + roles: ["admin"], + // 按钮级别权限 + permissions: ["*:*:*"], + accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", + refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", + expires: "2030/10/30 00:00:00" + } + }; + } else { + return { + success: true, + data: { + avatar: "https://avatars.githubusercontent.com/u/52823142", + username: "common", + nickname: "小林", + roles: ["common"], + permissions: ["permission:btn:add", "permission:btn:edit"], + accessToken: "eyJhbGciOiJIUzUxMiJ9.common", + refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", + expires: "2030/10/30 00:00:00" + } + }; + } + } + } +]); diff --git a/frontend/mock/ownershipTransfer.ts b/frontend/mock/ownershipTransfer.ts new file mode 100755 index 0000000..a7ef555 --- /dev/null +++ b/frontend/mock/ownershipTransfer.ts @@ -0,0 +1,274 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 生成货权转移函编号 +const generateTransferNumber = () => { + const date = new Date(); + const year = date.getFullYear().toString().slice(-2); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const random = Math.floor(Math.random() * 1000) + .toString() + .padStart(3, "0"); + return `HTZ${year}${month}${day}${random}`; +}; + +// 模拟数据 +const mockTransferList = [ + { + id: "1", + transferNumber: generateTransferNumber(), + transferDate: "2024-05-10", + blItems: [{ blNumber: "BL20240510001", quantity: 100 }], + commodity: "铜精矿", + warehouse: "上海港", + transferor: "中尚鹏贸易有限公司", + transferee: "华东制造有限公司", + status: "effective", + remark: "", + fileList: [], + createdAt: "2024-05-10T08:30:00", + updatedAt: "2024-05-10T08:30:00" + }, + { + id: "2", + transferNumber: generateTransferNumber(), + transferDate: "2024-05-12", + blItems: [ + { blNumber: "BL20240512005", quantity: 120 }, + { blNumber: "BL20240512006", quantity: 80 } + ], + commodity: "铝精矿", + warehouse: "中储", + transferor: "中尚鹏贸易有限公司", + transferee: "华南金属材料公司", + status: "draft", + remark: "", + fileList: [ + { name: "货权转移函-20240512.pdf", url: "/files/transfer-1.pdf" }, + { name: "附加协议.docx", url: "/files/agreement.docx" } + ], + createdAt: "2024-05-12T14:45:00", + updatedAt: "2024-05-13T09:20:00" + }, + { + id: "3", + transferNumber: generateTransferNumber(), + transferDate: "2024-05-15", + blItems: [{ blNumber: "BL20240515012", quantity: 150 }], + commodity: "铁矿石", + warehouse: "宁波港", + transferor: "中尚鹏贸易有限公司", + transferee: "北方物流集团", + status: "void", + remark: "", + fileList: [ + { name: "货权转移确认函-20240515.pdf", url: "/files/transfer-2.pdf" } + ], + createdAt: "2024-05-15T10:15:00", + updatedAt: "2024-05-18T14:30:00" + }, + { + id: "4", + transferNumber: generateTransferNumber(), + transferDate: "2024-05-18", + blItems: [ + { blNumber: "BL20240518008", quantity: 100 }, + { blNumber: "BL20240518009", quantity: 80 } + ], + commodity: "钢材", + warehouse: "青岛港", + transferor: "华南金属材料公司", + transferee: "中尚鹏贸易有限公司", + status: "effective", + remark: "", + fileList: [{ name: "货转函-QD20240518.pdf", url: "/files/transfer-3.pdf" }], + createdAt: "2024-05-18T11:05:00", + updatedAt: "2024-05-20T09:45:00" + } +]; + +export default defineFakeRoute([ + // 创建货权转移函 + { + url: "/ownership-transfer/create", + method: "post", + response: ({ body }) => { + const newTransfer = { + id: `tr${Date.now()}`, + transferNumber: generateTransferNumber(), + transferDate: body.transferDate, + blItems: body.blItems || [], + commodity: body.commodity, + warehouse: body.warehouse, + transferor: body.transferor, + transferee: body.transferee, + status: "draft", + remark: body.remark || "", + fileList: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + mockTransferList.unshift(newTransfer); + + return { + success: true, + message: "货权转移函创建成功", + data: newTransfer + }; + } + }, + + // 获取货权转移列表 + { + url: "/ownership-transfer/list", + method: "get", + response: ({ query }) => { + let list = [...mockTransferList]; + + const transferNumber = query.transferNumber as string; + const blNumber = query.blNumber as string; + const status = query.status as string; + const transferor = query.transferor as string; + const transferee = query.transferee as string; + const startDate = query.startDate as string; + const endDate = query.endDate as string; + + // 应用搜索过滤 + if (transferNumber) { + list = list.filter(item => + item.transferNumber.includes(transferNumber) + ); + } + if (blNumber) { + list = list.filter(item => + item.blItems?.some(bi => bi.blNumber.includes(blNumber)) + ); + } + if (status) { + list = list.filter(item => item.status === status); + } + if (transferor) { + list = list.filter(item => item.transferor.includes(transferor)); + } + if (transferee) { + list = list.filter(item => item.transferee.includes(transferee)); + } + if (startDate && endDate) { + list = list.filter(item => { + if (!item.transferDate) return false; + const date = new Date(item.transferDate); + const start = new Date(startDate); + const end = new Date(endDate); + end.setDate(end.getDate() + 1); // 包括结束日期 + return date >= start && date < end; + }); + } + + // 分页逻辑 + const page = parseInt(query.page as string) || 1; + const pageSize = parseInt(query.pageSize as string) || 10; + const total = list.length; + const startIndex = (page - 1) * pageSize; + const paginatedList = list.slice(startIndex, startIndex + pageSize); + + return { + success: true, + message: "获取货权转移列表成功", + data: { + items: paginatedList, + total + } + }; + } + }, + + // 打印货权转移函 + { + url: "/ownership-transfer/print/:id", + method: "get", + response: ({ params }) => { + const id = params.id; + const transfer = mockTransferList.find(item => item.id === id); + + if (!transfer) { + return { + success: false, + message: "货权转移函未找到" + }; + } + + // 模拟打印地址 + const url = `/print/ownership-transfer/${id}.pdf`; + + return { + success: true, + message: "生成打印地址成功", + data: { + url + } + }; + } + }, + + // 导出货权转移函列表 + { + url: "/ownership-transfer/export", + method: "get", + response: () => { + // 模拟导出文件地址 + const fileName = `货权转移函列表_${new Date().toISOString().split("T")[0]}.xlsx`; + + return { + success: true, + message: "导出成功", + data: { + fileName, + url: `/exports/ownership-transfer/${fileName}` + } + }; + } + }, + + // 删除货权转移函 + { + url: "/ownership-transfer/delete/:id", + method: "delete", + response: ({ params }) => { + const id = params.id; + const index = mockTransferList.findIndex(item => item.id === id); + + if (index === -1) { + return { + success: false, + message: "货权转移函未找到" + }; + } + + mockTransferList.splice(index, 1); + + return { + success: true, + message: "删除成功" + }; + } + }, + + // 文件上传接口 + { + url: "/ownership-transfer/upload", + method: "post", + response: () => { + // 在实际应用中,这里应该处理文件上传,并返回文件URL + // 此处省略文件上传的实际处理 + + return { + success: true, + message: "文件上传成功", + data: { + fileNames: ["文件1.pdf", "文件2.jpg"] + } + }; + } + } +]); diff --git a/frontend/mock/refreshToken.ts b/frontend/mock/refreshToken.ts new file mode 100755 index 0000000..bed12b0 --- /dev/null +++ b/frontend/mock/refreshToken.ts @@ -0,0 +1,27 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 模拟刷新token接口 +export default defineFakeRoute([ + { + url: "/refresh-token", + method: "post", + response: ({ body }) => { + if (body.refreshToken) { + return { + success: true, + data: { + accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin", + refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh", + // `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。 + expires: "2030/10/30 23:59:59" + } + }; + } else { + return { + success: false, + data: {} + }; + } + } + } +]); diff --git a/frontend/mock/zhongshangpeng.ts b/frontend/mock/zhongshangpeng.ts new file mode 100755 index 0000000..031c41a --- /dev/null +++ b/frontend/mock/zhongshangpeng.ts @@ -0,0 +1,265 @@ +import { defineFakeRoute } from "vite-plugin-fake-server/client"; + +// 模拟数据 +const mockContractFiles = [ + { + id: "1", + fileName: "上游合同-铜矿采购-2024-001.pdf", + fileType: "pdf", + fileSize: "2.3MB", + uploadTime: "2024-01-20", + folderId: "1", + contractNumber: "SC2024001", + companyName: "中尚鹏贸易有限公司", + commodity: "铜矿", + signDate: "2024-01-15", + contractType: "upstream" + }, + { + id: "2", + fileName: "下游合同-铜材销售-DX2024005.pdf", + fileType: "pdf", + fileSize: "1.8MB", + uploadTime: "2024-01-25", + folderId: "2", + contractNumber: "DX2024005", + companyName: "华东制造有限公司", + commodity: "铜材", + signDate: "2024-01-20", + contractType: "downstream" + }, + { + id: "3", + fileName: "外商合同-铁矿进口-FW2024003.pdf", + fileType: "pdf", + fileSize: "3.1MB", + uploadTime: "2024-02-12", + folderId: "3", + contractNumber: "FW2024003", + companyName: "澳大利亚矿业公司", + commodity: "铁矿", + signDate: "2024-02-10", + contractType: "foreign" + }, + { + id: "4", + fileName: "物流协议-海运合同-WL2024008.pdf", + fileType: "pdf", + fileSize: "1.5MB", + uploadTime: "2024-02-18", + folderId: "4", + contractNumber: "WL2024008", + companyName: "中海物流有限公司", + commodity: "物流服务", + signDate: "2024-02-15", + contractType: "logistics" + }, + { + id: "5", + fileName: "其他合同-设备租赁-QT2024002.pdf", + fileType: "pdf", + fileSize: "1.2MB", + uploadTime: "2024-03-05", + folderId: "5", + contractNumber: "QT2024002", + companyName: "设备租赁有限公司", + commodity: "设备租赁", + signDate: "2024-03-01", + contractType: "other" + } +]; + +const mockContractFolders = [ + { + id: "1", + name: "2024年上游合同", + count: 15, + createTime: "2024-01-15", + description: "2024年度所有上游合同" + }, + { + id: "2", + name: "2024年下游合同", + count: 23, + createTime: "2024-01-20", + description: "2024年度所有下游合同" + }, + { + id: "3", + name: "外商采购合同", + count: 8, + createTime: "2024-02-10", + description: "所有外商采购合同" + }, + { + id: "4", + name: "物流协议", + count: 12, + createTime: "2024-02-15", + description: "货代物流相关协议" + }, + { + id: "5", + name: "其他合同", + count: 5, + createTime: "2024-03-01", + description: "其他类型的合同" + } +]; + +export default defineFakeRoute([ + // 获取合同文件夹 + { + url: "/zhongshangpeng/folders", + method: "get", + response: () => { + return { + success: true, + message: "获取文件夹成功", + data: mockContractFolders + }; + } + }, + + // 获取合同文件 + { + url: "/zhongshangpeng/files", + method: "get", + response: ({ query }) => { + let files = [...mockContractFiles]; + + if (query.folderId) { + files = files.filter(file => file.folderId === query.folderId); + } + + if (query.searchType && query.keyword) { + const keyword = ( + Array.isArray(query.keyword) ? query.keyword[0] : query.keyword + ).toLowerCase(); + files = files.filter(file => { + if (query.searchType === "contractNumber") { + return ( + file.contractNumber?.toLowerCase().includes(keyword) || + file.fileName.toLowerCase().includes(keyword) + ); + } else if (query.searchType === "companyName") { + return ( + file.companyName?.toLowerCase().includes(keyword) || + file.fileName.toLowerCase().includes(keyword) + ); + } else if (query.searchType === "commodity") { + return ( + file.commodity?.toLowerCase().includes(keyword) || + file.fileName.toLowerCase().includes(keyword) + ); + } + return true; + }); + } + + if (query.contractType) { + files = files.filter(file => file.contractType === query.contractType); + } + + if (query.signDateStart && query.signDateEnd) { + files = files.filter(file => { + if (!file.signDate) return false; + const fileDate = new Date(file.signDate).getTime(); + const startDate = new Date( + Array.isArray(query.signDateStart) + ? query.signDateStart[0] + : query.signDateStart + ).getTime(); + const endDate = new Date( + Array.isArray(query.signDateEnd) + ? query.signDateEnd[0] + : query.signDateEnd + ).getTime(); + return fileDate >= startDate && fileDate <= endDate; + }); + } + + return { + success: true, + message: "获取文件成功", + data: files + }; + } + }, + + // 上传合同文件 + { + url: "/zhongshangpeng/files/upload", + method: "post", + response: ({ body }) => { + try { + const data = JSON.parse(body.get("data")); + const files = body.getAll("files"); + + files.forEach((file: any, index: number) => { + const newFile = { + id: (Date.now() + index).toString(), + fileName: file.name, + fileType: file.name.split(".").pop() || "unknown", + fileSize: `${(file.size / 1024 / 1024).toFixed(1)}MB`, + uploadTime: new Date().toISOString().split("T")[0], + folderId: data.folderId || "", + contractNumber: data.contractNumber, + companyName: data.companyName, + commodity: data.commodity, + signDate: data.signDate, + contractType: data.contractType + }; + + mockContractFiles.unshift(newFile); + + // 更新文件夹计数 + if (data.folderId) { + const folder = mockContractFolders.find( + f => f.id === data.folderId + ); + if (folder) { + folder.count++; + } + } + }); + + return { + success: true, + message: `成功上传 ${files.length} 个文件`, + data: { + uploadedCount: files.length + } + }; + } catch { + return { + success: false, + message: "文件上传失败" + }; + } + } + }, + + // 创建文件夹 + { + url: "/zhongshangpeng/folders", + method: "post", + response: ({ body }) => { + const newFolder = { + id: Date.now().toString(), + name: body.name, + count: 0, + createTime: new Date().toISOString().split("T")[0], + description: body.description + }; + + mockContractFolders.push(newFolder); + + return { + success: true, + message: "文件夹创建成功", + data: newFolder + }; + } + } +]); diff --git a/frontend/package.json b/frontend/package.json new file mode 100755 index 0000000..3f615fb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,168 @@ +{ + "name": "pure-admin-thin", + "version": "6.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "NODE_OPTIONS=--max-old-space-size=4096 vite", + "serve": "pnpm dev", + "build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build", + "build:staging": "rimraf dist && vite build --mode staging", + "report": "rimraf dist && vite build", + "preview": "vite preview", + "preview:build": "pnpm build && vite preview", + "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck", + "svgo": "svgo -f . -r", + "clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install", + "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix", + "lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"", + "lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/", + "lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint", + "prepare": "husky", + "preinstall": "npx only-allow pnpm", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:ui": "vitest --ui" + }, + "keywords": [ + "pure-admin-thin", + "vue-pure-admin", + "element-plus", + "tailwindcss", + "pure-admin", + "typescript", + "pinia", + "vue3", + "vite", + "esm" + ], + "homepage": "https://github.com/pure-admin/pure-admin-thin", + "repository": { + "type": "git", + "url": "git+https://github.com/pure-admin/pure-admin-thin.git" + }, + "bugs": { + "url": "https://github.com/pure-admin/vue-pure-admin/issues" + }, + "license": "MIT", + "author": { + "name": "xiaoxian521", + "email": "pureadmin@163.com", + "url": "https://github.com/xiaoxian521" + }, + "dependencies": { + "@pureadmin/descriptions": "^1.2.1", + "@pureadmin/table": "^3.2.1", + "@pureadmin/utils": "^2.6.0", + "@vueuse/core": "^13.1.0", + "@vueuse/motion": "^3.0.3", + "animate.css": "^4.1.1", + "axios": "^1.9.0", + "dayjs": "^1.11.13", + "echarts": "^5.6.0", + "element-plus": "^2.9.8", + "js-cookie": "^3.0.5", + "localforage": "^1.10.0", + "mitt": "^3.0.1", + "nprogress": "^0.2.0", + "path-browserify": "^1.0.1", + "pinia": "^3.0.2", + "pinyin-pro": "^3.26.0", + "qs": "^6.14.0", + "responsive-storage": "^2.2.0", + "sortablejs": "^1.15.6", + "vue": "^3.5.13", + "vue-router": "^4.5.0", + "vue-tippy": "^6.7.0", + "vue-types": "^6.0.0" + }, + "devDependencies": { + "@commitlint/cli": "^19.8.0", + "@commitlint/config-conventional": "^19.8.0", + "@commitlint/types": "^19.8.0", + "@eslint/js": "^9.25.1", + "@faker-js/faker": "^9.7.0", + "@iconify/json": "^2.2.331", + "@iconify/vue": "4.2.0", + "@tailwindcss/vite": "^4.1.4", + "@types/js-cookie": "^3.0.6", + "@types/node": "^20.17.30", + "@types/nprogress": "^0.2.3", + "@types/path-browserify": "^1.0.3", + "@types/qs": "^6.9.18", + "@types/sortablejs": "^1.15.8", + "@vitejs/plugin-vue": "^5.2.3", + "@vitejs/plugin-vue-jsx": "^4.1.2", + "@vitest/coverage-v8": "^4.1.5", + "@vitest/ui": "^4.1.5", + "boxen": "^8.0.1", + "code-inspector-plugin": "^0.20.10", + "cssnano": "^7.0.6", + "eslint": "^9.25.1", + "eslint-config-prettier": "^10.1.2", + "eslint-plugin-prettier": "^5.2.6", + "eslint-plugin-vue": "^10.0.0", + "gradient-string": "^3.0.0", + "husky": "^9.1.7", + "jsdom": "^29.0.2", + "lint-staged": "^15.5.1", + "postcss": "^8.5.3", + "postcss-html": "^1.8.0", + "postcss-load-config": "^6.0.1", + "postcss-scss": "^4.0.9", + "prettier": "^3.5.3", + "rimraf": "^6.0.1", + "rollup-plugin-visualizer": "^5.14.0", + "sass": "^1.87.0", + "stylelint": "^16.19.0", + "stylelint-config-recess-order": "^6.0.0", + "stylelint-config-recommended-vue": "^1.6.0", + "stylelint-config-standard-scss": "^14.0.0", + "stylelint-prettier": "^5.0.3", + "stylelint-scss": "^7.0.0", + "svgo": "^3.3.2", + "tailwindcss": "^4.1.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.31.0", + "unplugin-icons": "^22.1.0", + "vite": "^6.3.3", + "vite-plugin-cdn-import": "^1.0.1", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-fake-server": "^2.2.0", + "vite-plugin-remove-console": "^2.2.0", + "vite-plugin-router-warn": "^1.0.0", + "vite-svg-loader": "^5.1.0", + "vitest": "^4.1.5", + "vue-eslint-parser": "^10.1.3", + "vue-tsc": "^2.2.10" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.0.0", + "pnpm": ">=9" + }, + "pnpm": { + "allowedDeprecatedVersions": { + "are-we-there-yet": "*", + "sourcemap-codec": "*", + "lodash.isequal": "*", + "domexception": "*", + "w3c-hr-time": "*", + "inflight": "*", + "npmlog": "*", + "rimraf": "*", + "stable": "*", + "gauge": "*", + "abab": "*", + "glob": "*" + }, + "onlyBuiltDependencies": [ + "@parcel/watcher", + "core-js", + "es5-ext", + "esbuild", + "typeit", + "vue-demi" + ] + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100755 index 0000000..f1dbe86 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,8142 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@pureadmin/descriptions': + specifier: ^1.2.1 + version: 1.2.1(echarts@5.6.0)(element-plus@2.9.8(vue@3.5.13(typescript@5.8.3)))(typescript@5.8.3) + '@pureadmin/table': + specifier: ^3.2.1 + version: 3.2.1(element-plus@2.9.8(vue@3.5.13(typescript@5.8.3)))(typescript@5.8.3) + '@pureadmin/utils': + specifier: ^2.6.0 + version: 2.6.0(echarts@5.6.0)(vue@3.5.13(typescript@5.8.3)) + '@vueuse/core': + specifier: ^13.1.0 + version: 13.1.0(vue@3.5.13(typescript@5.8.3)) + '@vueuse/motion': + specifier: ^3.0.3 + version: 3.0.3(vue@3.5.13(typescript@5.8.3)) + animate.css: + specifier: ^4.1.1 + version: 4.1.1 + axios: + specifier: ^1.9.0 + version: 1.9.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + echarts: + specifier: ^5.6.0 + version: 5.6.0 + element-plus: + specifier: ^2.9.8 + version: 2.9.8(vue@3.5.13(typescript@5.8.3)) + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + localforage: + specifier: ^1.10.0 + version: 1.10.0 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + nprogress: + specifier: ^0.2.0 + version: 0.2.0 + path-browserify: + specifier: ^1.0.1 + version: 1.0.1 + pinia: + specifier: ^3.0.2 + version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) + pinyin-pro: + specifier: ^3.26.0 + version: 3.26.0 + qs: + specifier: ^6.14.0 + version: 6.14.0 + responsive-storage: + specifier: ^2.2.0 + version: 2.2.0 + sortablejs: + specifier: ^1.15.6 + version: 1.15.6 + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.8.3) + vue-router: + specifier: ^4.5.0 + version: 4.5.0(vue@3.5.13(typescript@5.8.3)) + vue-tippy: + specifier: ^6.7.0 + version: 6.7.0(vue@3.5.13(typescript@5.8.3)) + vue-types: + specifier: ^6.0.0 + version: 6.0.0(vue@3.5.13(typescript@5.8.3)) + devDependencies: + '@commitlint/cli': + specifier: ^19.8.0 + version: 19.8.0(@types/node@20.17.30)(typescript@5.8.3) + '@commitlint/config-conventional': + specifier: ^19.8.0 + version: 19.8.0 + '@commitlint/types': + specifier: ^19.8.0 + version: 19.8.0 + '@eslint/js': + specifier: ^9.25.1 + version: 9.25.1 + '@faker-js/faker': + specifier: ^9.7.0 + version: 9.7.0 + '@iconify/json': + specifier: ^2.2.331 + version: 2.2.331 + '@iconify/vue': + specifier: 4.2.0 + version: 4.2.0(vue@3.5.13(typescript@5.8.3)) + '@tailwindcss/vite': + specifier: ^4.1.4 + version: 4.1.4(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: ^20.17.30 + version: 20.17.30 + '@types/nprogress': + specifier: ^0.2.3 + version: 0.2.3 + '@types/path-browserify': + specifier: ^1.0.3 + version: 1.0.3 + '@types/qs': + specifier: ^6.9.18 + version: 6.9.18 + '@types/sortablejs': + specifier: ^1.15.8 + version: 1.15.8 + '@vitejs/plugin-vue': + specifier: ^5.2.3 + version: 5.2.3(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3)) + '@vitejs/plugin-vue-jsx': + specifier: ^4.1.2 + version: 4.1.2(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3)) + '@vitest/coverage-v8': + specifier: ^4.1.5 + version: 4.1.5(vitest@4.1.5) + '@vitest/ui': + specifier: ^4.1.5 + version: 4.1.5(vitest@4.1.5) + boxen: + specifier: ^8.0.1 + version: 8.0.1 + code-inspector-plugin: + specifier: ^0.20.10 + version: 0.20.10 + cssnano: + specifier: ^7.0.6 + version: 7.0.6(postcss@8.5.3) + eslint: + specifier: ^9.25.1 + version: 9.25.1(jiti@2.4.2) + eslint-config-prettier: + specifier: ^10.1.2 + version: 10.1.2(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.2.6(eslint-config-prettier@10.1.2(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))(prettier@3.5.3) + eslint-plugin-vue: + specifier: ^10.0.0 + version: 10.0.0(eslint@9.25.1(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.25.1(jiti@2.4.2))) + gradient-string: + specifier: ^3.0.0 + version: 3.0.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 + jsdom: + specifier: ^29.0.2 + version: 29.0.2 + lint-staged: + specifier: ^15.5.1 + version: 15.5.1 + postcss: + specifier: ^8.5.3 + version: 8.5.3 + postcss-html: + specifier: ^1.8.0 + version: 1.8.0 + postcss-load-config: + specifier: ^6.0.1 + version: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.7.1) + postcss-scss: + specifier: ^4.0.9 + version: 4.0.9(postcss@8.5.3) + prettier: + specifier: ^3.5.3 + version: 3.5.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + rollup-plugin-visualizer: + specifier: ^5.14.0 + version: 5.14.0(rollup@4.40.0) + sass: + specifier: ^1.87.0 + version: 1.87.0 + stylelint: + specifier: ^16.19.0 + version: 16.19.0(typescript@5.8.3) + stylelint-config-recess-order: + specifier: ^6.0.0 + version: 6.0.0(stylelint@16.19.0(typescript@5.8.3)) + stylelint-config-recommended-vue: + specifier: ^1.6.0 + version: 1.6.0(postcss-html@1.8.0)(stylelint@16.19.0(typescript@5.8.3)) + stylelint-config-standard-scss: + specifier: ^14.0.0 + version: 14.0.0(postcss@8.5.3)(stylelint@16.19.0(typescript@5.8.3)) + stylelint-prettier: + specifier: ^5.0.3 + version: 5.0.3(prettier@3.5.3)(stylelint@16.19.0(typescript@5.8.3)) + stylelint-scss: + specifier: ^7.0.0 + version: 7.0.0(stylelint@16.19.0(typescript@5.8.3)) + svgo: + specifier: ^3.3.2 + version: 3.3.2 + tailwindcss: + specifier: ^4.1.4 + version: 4.1.4 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.31.0 + version: 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + unplugin-icons: + specifier: ^22.1.0 + version: 22.1.0(@vue/compiler-sfc@3.5.13) + vite: + specifier: ^6.3.3 + version: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + vite-plugin-cdn-import: + specifier: ^1.0.1 + version: 1.0.1(rollup@4.40.0)(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + vite-plugin-fake-server: + specifier: ^2.2.0 + version: 2.2.0 + vite-plugin-remove-console: + specifier: ^2.2.0 + version: 2.2.0 + vite-plugin-router-warn: + specifier: ^1.0.0 + version: 1.0.0 + vite-svg-loader: + specifier: ^5.1.0 + version: 5.1.0(vue@3.5.13(typescript@5.8.3)) + vitest: + specifier: ^4.1.5 + version: 4.1.5(@types/node@20.17.30)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.0.2)(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + vue-eslint-parser: + specifier: ^10.1.3 + version: 10.1.3(eslint@9.25.1(jiti@2.4.2)) + vue-tsc: + specifier: ^2.2.10 + version: 2.2.10(typescript@5.8.3) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@1.0.0': + resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==} + + '@antfu/utils@8.1.1': + resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.8': + resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.10': + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.0': + resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.0': + resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.27.0': + resolution: {integrity: sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.26.5': + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.26.5': + resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.0': + resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.27.0': + resolution: {integrity: sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.0': + resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.0': + resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@commitlint/cli@19.8.0': + resolution: {integrity: sha512-t/fCrLVu+Ru01h0DtlgHZXbHV2Y8gKocTR5elDOqIRUzQd0/6hpt2VIWOj9b3NDo7y4/gfxeR2zRtXq/qO6iUg==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.8.0': + resolution: {integrity: sha512-9I2kKJwcAPwMoAj38hwqFXG0CzS2Kj+SAByPUQ0SlHTfb7VUhYVmo7G2w2tBrqmOf7PFd6MpZ/a1GQJo8na8kw==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.8.0': + resolution: {integrity: sha512-+r5ZvD/0hQC3w5VOHJhGcCooiAVdynFlCe2d6I9dU+PvXdV3O+fU4vipVg+6hyLbQUuCH82mz3HnT/cBQTYYuA==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.8.0': + resolution: {integrity: sha512-kNiNU4/bhEQ/wutI1tp1pVW1mQ0QbAjfPRo5v8SaxoVV+ARhkB8Wjg3BSseNYECPzWWfg/WDqQGIfV1RaBFQZg==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.8.0': + resolution: {integrity: sha512-fuLeI+EZ9x2v/+TXKAjplBJWI9CNrHnyi5nvUQGQt4WRkww/d95oVRsc9ajpt4xFrFmqMZkd/xBQHZDvALIY7A==} + engines: {node: '>=v18'} + + '@commitlint/format@19.8.0': + resolution: {integrity: sha512-EOpA8IERpQstxwp/WGnDArA7S+wlZDeTeKi98WMOvaDLKbjptuHWdOYYr790iO7kTCif/z971PKPI2PkWMfOxg==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.8.0': + resolution: {integrity: sha512-L2Jv9yUg/I+jF3zikOV0rdiHUul9X3a/oU5HIXhAJLE2+TXTnEBfqYP9G5yMw/Yb40SnR764g4fyDK6WR2xtpw==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.8.0': + resolution: {integrity: sha512-+/NZKyWKSf39FeNpqhfMebmaLa1P90i1Nrb1SrA7oSU5GNN/lksA4z6+ZTnsft01YfhRZSYMbgGsARXvkr/VLQ==} + engines: {node: '>=v18'} + + '@commitlint/load@19.8.0': + resolution: {integrity: sha512-4rvmm3ff81Sfb+mcWT5WKlyOa+Hd33WSbirTVUer0wjS1Hv/Hzr07Uv1ULIV9DkimZKNyOwXn593c+h8lsDQPQ==} + engines: {node: '>=v18'} + + '@commitlint/message@19.8.0': + resolution: {integrity: sha512-qs/5Vi9bYjf+ZV40bvdCyBn5DvbuelhR6qewLE8Bh476F7KnNyLfdM/ETJ4cp96WgeeHo6tesA2TMXS0sh5X4A==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.8.0': + resolution: {integrity: sha512-YNIKAc4EXvNeAvyeEnzgvm1VyAe0/b3Wax7pjJSwXuhqIQ1/t2hD3OYRXb6D5/GffIvaX82RbjD+nWtMZCLL7Q==} + engines: {node: '>=v18'} + + '@commitlint/read@19.8.0': + resolution: {integrity: sha512-6ywxOGYajcxK1y1MfzrOnwsXO6nnErna88gRWEl3qqOOP8MDu/DTeRkGLXBFIZuRZ7mm5yyxU5BmeUvMpNte5w==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.8.0': + resolution: {integrity: sha512-CLanRQwuG2LPfFVvrkTrBR/L/DMy3+ETsgBqW1OvRxmzp/bbVJW0Xw23LnnExgYcsaFtos967lul1CsbsnJlzQ==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.8.0': + resolution: {integrity: sha512-IZ5IE90h6DSWNuNK/cwjABLAKdy8tP8OgGVGbXe1noBEX5hSsu00uRlLu6JuruiXjWJz2dZc+YSw3H0UZyl/mA==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.8.0': + resolution: {integrity: sha512-3CKLUw41Cur8VMjh16y8LcsOaKbmQjAKCWlXx6B0vOUREplp6em9uIVhI8Cv934qiwkbi2+uv+mVZPnXJi1o9A==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.8.0': + resolution: {integrity: sha512-Rphgoc/omYZisoNkcfaBRPQr4myZEHhLPx2/vTXNLjiCw4RgfPR1wEgUpJ9OOmDCiv5ZyIExhprNLhteqH4FuQ==} + engines: {node: '>=v18'} + + '@commitlint/types@19.8.0': + resolution: {integrity: sha512-LRjP623jPyf3Poyfb0ohMj8I3ORyBDOwXAgxxVPbSD0unJuW2mJWeiRfaQinjtccMqC5Wy1HOMfa4btKjbNxbg==} + engines: {node: '>=v18'} + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@csstools/media-query-list-parser@4.0.2': + resolution: {integrity: sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@dual-bundle/import-meta-resolve@4.1.0': + resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==} + + '@element-plus/icons-vue@2.3.1': + resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.3': + resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.3': + resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.3': + resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.3': + resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.3': + resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.3': + resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.3': + resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.3': + resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.3': + resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.3': + resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.3': + resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.3': + resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.3': + resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.3': + resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.3': + resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.3': + resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.3': + resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.25.3': + resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.3': + resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.25.3': + resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.3': + resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.3': + resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.3': + resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.3': + resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.3': + resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.6.1': + resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.20.0': + resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.2.1': + resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.25.1': + resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@faker-js/faker@9.7.0': + resolution: {integrity: sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@floating-ui/core@1.6.9': + resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} + + '@floating-ui/dom@1.6.13': + resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + engines: {node: '>=18.18'} + + '@iconify/json@2.2.331': + resolution: {integrity: sha512-K8hDKDFJ31nF2mERN6Nfxv4peXZfaN4kmZqeBpCRhHA/5/SCp+hx9ID4kLH3lEhBm1eqmXfZjwOC2v7FnaFHYw==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.3.0': + resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + + '@iconify/vue@4.2.0': + resolution: {integrity: sha512-CMynoz9BDWugDO2B7LU/s8L99dHCiqDGCjCki6bhVx5etZhw9x0BTV7wWRdj82jtl1yQTc+QQRcHQmSvUY6R+g==} + peerDependencies: + vue: '>=3' + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/serialize@1.0.3': + resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nuxt/kit@3.16.2': + resolution: {integrity: sha512-K1SAUo2vweTfudKZzjKsZ5YJoxPLTspR5qz5+G61xtZreLpsdpDYfBseqsIAl5VFLJuszeRpWQ01jP9LfQ6Ksw==} + engines: {node: '>=18.12.0'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgr/core@0.2.4': + resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@pureadmin/descriptions@1.2.1': + resolution: {integrity: sha512-7jDJuqz8xnhcmwXdWQnBzOYeX2WK27TRFaVgs9AdiRr+DnKb9W+krHByJwQtxo5lg4qyRh4/IWQGEMfhC2ljeQ==} + peerDependencies: + element-plus: ^2.0.0 + + '@pureadmin/table@3.2.1': + resolution: {integrity: sha512-sqkAQWRDx6X0dADUgrYHvA7QfocF+VB5rPe1yFQWjh7tcHNasthFRDCS5Fmw7JQ0R3h6S7aBK450ye2/TLv4JQ==} + peerDependencies: + element-plus: ^2.0.0 + + '@pureadmin/utils@2.6.0': + resolution: {integrity: sha512-xPncBQ4DZUstKrljsHxr3yuYacEZmvEilSfrZ6vpWNgJtPnViJ24Lf7gl7c1Y1RKYm/kXxjrhI7x69S2oN1Pvg==} + peerDependencies: + echarts: '*' + vue: '*' + peerDependenciesMeta: + echarts: + optional: true + vue: + optional: true + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.40.0': + resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.40.0': + resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.40.0': + resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.40.0': + resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.40.0': + resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.40.0': + resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.40.0': + resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.40.0': + resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.40.0': + resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.40.0': + resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} + cpu: [x64] + os: [win32] + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@tailwindcss/node@4.1.4': + resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==} + + '@tailwindcss/oxide-android-arm64@4.1.4': + resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.4': + resolution: {integrity: sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.4': + resolution: {integrity: sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.4': + resolution: {integrity: sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4': + resolution: {integrity: sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.4': + resolution: {integrity: sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.4': + resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.4': + resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.4': + resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.4': + resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.4': + resolution: {integrity: sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.4': + resolution: {integrity: sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.4': + resolution: {integrity: sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.4': + resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==} + peerDependencies: + vite: ^5.2.0 || ^6 + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/conventional-commits-parser@5.0.1': + resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.16': + resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} + + '@types/node@20.17.30': + resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==} + + '@types/nprogress@0.2.3': + resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} + + '@types/path-browserify@1.0.3': + resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==} + + '@types/qs@6.9.18': + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + + '@types/sortablejs@1.15.8': + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@typescript-eslint/eslint-plugin@8.31.0': + resolution: {integrity: sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.31.0': + resolution: {integrity: sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.31.0': + resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.31.0': + resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.31.0': + resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.31.0': + resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.31.0': + resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.31.0': + resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue-jsx@4.1.2': + resolution: {integrity: sha512-4Rk0GdE0QCdsIkuMmWeg11gmM4x8UmTnZR/LWPm7QJ7+BsK4tq08udrN0isrrWqz5heFy9HLV/7bOLgFS8hUjA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.0.0 + + '@vitejs/plugin-vue@5.2.3': + resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vitest/coverage-v8@4.1.5': + resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==} + peerDependencies: + '@vitest/browser': 4.1.5 + vitest: 4.1.5 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/ui@4.1.5': + resolution: {integrity: sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==} + peerDependencies: + vitest: 4.1.5 + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + + '@volar/language-core@2.4.12': + resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==} + + '@volar/source-map@2.4.12': + resolution: {integrity: sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==} + + '@volar/typescript@2.4.12': + resolution: {integrity: sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==} + + '@vue/babel-helper-vue-transform-on@1.4.0': + resolution: {integrity: sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==} + + '@vue/babel-plugin-jsx@1.4.0': + resolution: {integrity: sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.4.0': + resolution: {integrity: sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.5': + resolution: {integrity: sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==} + + '@vue/devtools-kit@7.7.5': + resolution: {integrity: sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==} + + '@vue/devtools-shared@7.7.5': + resolution: {integrity: sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==} + + '@vue/language-core@2.2.10': + resolution: {integrity: sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vueuse/core@13.1.0': + resolution: {integrity: sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@13.1.0': + resolution: {integrity: sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/motion@3.0.3': + resolution: {integrity: sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==} + peerDependencies: + vue: '>=3.0.0' + + '@vueuse/shared@13.1.0': + resolution: {integrity: sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + animate.css@4.1.1: + resolution: {integrity: sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + birpc@2.3.0: + resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-import@0.0.2: + resolution: {integrity: sha512-XB3T6xlgqJHThyr2luo3pNAVhfN/Y2qFEsblrzUO5QZLpJtesget8jmGDImSairScy80ZKBDVcRdFzTzWv3v8A==} + + c12@3.0.3: + resolution: {integrity: sha512-uC3MacKBb0Z15o5QWCHvHWj5Zv34pGQj9P+iXKSpTuSGFS0KKhUWf4t9AJ+gWjYOdmWCPEGpEzm8sS0iqbpo1w==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + cacheable@1.8.10: + resolution: {integrity: sha512-0ZnbicB/N2R6uziva8l6O6BieBklArWyiGx4GkwAhLKhSHyQtRfM9T1nx7HHuHDKkYB/efJQhz3QJ6x/YqoZzA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001715: + resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.1: + resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + engines: {node: '>=10'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + code-inspector-core@0.20.10: + resolution: {integrity: sha512-nSIn1nKJ58BIKhrr4Kiv39ZyIOFKVD1oxVZVf98CSuKz559llCjcyY0DjI8MzZG6iimw5/myemOxhYrV9jUvDQ==} + + code-inspector-plugin@0.20.10: + resolution: {integrity: sha512-G3aQ+t65N+rJlydPRUoG4vegjQb3seitCXCuNICUMhkDLetdVONLTASePVPCADv+fXl0vyW0hnZzAAxb9UnwOQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + cosmiconfig-typescript-loader@6.1.0: + resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-declaration-sorter@7.2.0: + resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-default@7.0.6: + resolution: {integrity: sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-utils@5.0.0: + resolution: {integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano@7.0.6: + resolution: {integrity: sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + + electron-to-chromium@1.5.142: + resolution: {integrity: sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==} + + element-plus@2.9.8: + resolution: {integrity: sha512-srViUaUdfblBKGMeuEPiXxxKlH5aUmKqEwmhb/At9Sj91DbU6od/jYN1955cTnzt3wTSA7GfnZF7UiRX9sdRHg==} + peerDependencies: + vue: ^3.2.0 + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + errx@0.1.0: + resolution: {integrity: sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@0.4.1: + resolution: {integrity: sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild-code-inspector-plugin@0.20.10: + resolution: {integrity: sha512-sYedVx+EjEnIEvomYJdW93wm5vPLuXer0cwj7kmNA1nnsz1hqF5XVrBheqVAMGMj7kM7erKu0hMLJUz0znpzWQ==} + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.25.3: + resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@10.1.2: + resolution: {integrity: sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.6: + resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@10.0.0: + resolution: {integrity: sha512-XKckedtajqwmaX6u1VnECmZ6xJt+YvlmMzBPZd+/sI3ub2lpYZyFnsyWo7c3nMOQKJQudeyk1lw/JxdgeKT64w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + vue-eslint-parser: ^10.0.0 + + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.25.1: + resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.5: + resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + file-entry-cache@10.0.8: + resolution: {integrity: sha512-FGXHpfmI4XyzbLd3HQ8cbUcsFGohJpZtmQRHr8z8FxxtCe2PcpgIlVLwIgunqjvRmXypBETvwhV4ptJizA+Y1Q==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat-cache@6.1.8: + resolution: {integrity: sha512-R6MaD3nrJAtO7C3QOuS79ficm2pEAy++TgEUD8ii1LVlbcgZ9DtASLkt9B+RZSFCzm7QHDMlXPsqqB6W2Pfr1Q==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + framesync@6.1.2: + resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@11.0.2: + resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==} + engines: {node: 20 || >=22} + hasBin: true + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@14.1.0: + resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} + engines: {node: '>=18'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gradient-string@3.0.0: + resolution: {integrity: sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==} + engines: {node: '>=14'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hey-listen@1.0.8: + resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hookified@1.8.2: + resolution: {integrity: sha512-5nZbBNP44sFCDjSoB//0N7m508APCgbQ4mGGo1KJGBYyCKNHfry1Pvd0JVHZIxjdnqn8nFRBAN/eFB6Rk/4w5w==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.3: + resolution: {integrity: sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + immutable@5.1.1: + resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-from-string@0.0.5: + resolution: {integrity: sha512-z59WIHImWhnGVswc0JoyI10Qn4A8xQw7OKrCFRQHvzGZhhEixX13OtXP9ud3Xjpn16CUoYfh5mTu3tnNODiSAw==} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@4.1.0: + resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} + engines: {node: 20 || >=22} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@29.0.2: + resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + keyv@5.3.3: + resolution: {integrity: sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + knitwork@1.2.0: + resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + + known-css-properties@0.35.0: + resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} + + known-css-properties@0.36.0: + resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + launch-ide@1.0.7: + resolution: {integrity: sha512-wJMTq6U2sVYqxrlp544KQxtl8cHoXFfQa2ivDtKJ6ock2ARneiEHqUFce/NQsnNP1aZNg4OXB6g00oFRvni1/Q==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + + lightningcss-darwin-arm64@1.29.2: + resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.2: + resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.2: + resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.2: + resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.2: + resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.29.2: + resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.29.2: + resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.29.2: + resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.29.2: + resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.2: + resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.2: + resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@15.5.1: + resolution: {integrity: sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.3.2: + resolution: {integrity: sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==} + engines: {node: '>=18.0.0'} + + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + + localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.21.0: + resolution: {integrity: sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ==} + + mdn-data@2.27.0: + resolution: {integrity: sha512-/pUmP9UebM48q5BTqZd0yPnDjyRGhITbKh8cwa6/ZwjuDu8xq+VzmugLF7QNxpdaqqNH3J5nnv3yc8oARv096A==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nypm@0.6.0: + resolution: {integrity: sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path-type@6.0.0: + resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} + engines: {node: '>=18'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia@3.0.2: + resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pinyin-pro@3.26.0: + resolution: {integrity: sha512-HcBZZb0pvm0/JkPhZHWA5Hqp2cWHXrrW/WrV+OtaYYM+kf35ffvZppIUuGmyuQ7gDr1JDJKMkbEE+GN0wfMoGg==} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.1.0: + resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + + popmotion@11.0.5: + resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==} + + portfinder@1.0.36: + resolution: {integrity: sha512-gMKUzCoP+feA7t45moaSx7UniU7PgGN3hA8acAB+3Qn7/js0/lJ07fYZlxt9riE9S3myyxDCyAFzSrLlta0c9g==} + engines: {node: '>= 10.12'} + + postcss-calc@10.1.1: + resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} + engines: {node: ^18.12 || ^20.9 || >=22.0} + peerDependencies: + postcss: ^8.4.38 + + postcss-colormin@7.0.2: + resolution: {integrity: sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-convert-values@7.0.4: + resolution: {integrity: sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-comments@7.0.3: + resolution: {integrity: sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-duplicates@7.0.1: + resolution: {integrity: sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-empty@7.0.0: + resolution: {integrity: sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-overridden@7.0.0: + resolution: {integrity: sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-html@1.8.0: + resolution: {integrity: sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==} + engines: {node: ^12 || >=14} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-merge-longhand@7.0.4: + resolution: {integrity: sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-rules@7.0.4: + resolution: {integrity: sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-font-values@7.0.0: + resolution: {integrity: sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-gradients@7.0.0: + resolution: {integrity: sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-params@7.0.2: + resolution: {integrity: sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-selectors@7.0.4: + resolution: {integrity: sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-charset@7.0.0: + resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-display-values@7.0.0: + resolution: {integrity: sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-positions@7.0.0: + resolution: {integrity: sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-repeat-style@7.0.0: + resolution: {integrity: sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-string@7.0.0: + resolution: {integrity: sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-timing-functions@7.0.0: + resolution: {integrity: sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-unicode@7.0.2: + resolution: {integrity: sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-url@7.0.0: + resolution: {integrity: sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-whitespace@7.0.0: + resolution: {integrity: sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-ordered-values@7.0.1: + resolution: {integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-initial@7.0.2: + resolution: {integrity: sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-transforms@7.0.0: + resolution: {integrity: sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-sorting@8.0.2: + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + + postcss-svgo@7.0.1: + resolution: {integrity: sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==} + engines: {node: ^18.12.0 || ^20.9.0 || >= 18} + peerDependencies: + postcss: ^8.4.31 + + postcss-unique-selectors@7.0.3: + resolution: {integrity: sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + responsive-storage@2.2.0: + resolution: {integrity: sha512-94W5Chr2F5kDBT6J+OCOeJguEkSTDc3jPOUQXYmzNG64DCNl5p7hoBDF7bx7u6EXAEcpUKF64OZR4b7Nn8h/Gg==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + + rollup-plugin-external-globals@0.10.0: + resolution: {integrity: sha512-RXlupZrmn97AaaS5dWnktkjM+Iy+od0E+8L0mUkMIs3iuoUXNJebueQocQKV7Ircd54fSGGmkBaXwNzY05J1yQ==} + peerDependencies: + rollup: ^2.25.0 || ^3.3.0 || ^4.1.4 + + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@4.40.0: + resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.87.0: + resolution: {integrity: sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==} + engines: {node: '>=14.0.0'} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + sortablejs@1.15.6: + resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + + style-value-types@5.1.2: + resolution: {integrity: sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==} + + stylehacks@7.0.4: + resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.31 + + stylelint-config-html@1.1.0: + resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recess-order@6.0.0: + resolution: {integrity: sha512-1KqrttqpIrCYFAVQ1/bbgXo7EvvcjmkxxmnzVr+U66Xr2OlrNZqQ5+44Tmct6grCWY6wGTIBh2tSANqcmwIM2g==} + peerDependencies: + stylelint: '>=16' + + stylelint-config-recommended-scss@14.1.0: + resolution: {integrity: sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==} + engines: {node: '>=18.12.0'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.6.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended-vue@1.6.0: + resolution: {integrity: sha512-syk1adIHvbH2T1OiR/spUK4oQy35PZIDw8Zmc7E0+eVK9Z9SK3tdMpGRT/bgGnAPpMt/WaL9K1u0tlF6xM0sMQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recommended@14.0.1: + resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-config-recommended@16.0.0: + resolution: {integrity: sha512-4RSmPjQegF34wNcK1e1O3Uz91HN8P1aFdFzio90wNK9mjgAI19u5vsU868cVZboKzCaa5XbpvtTzAAGQAxpcXA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.16.0 + + stylelint-config-standard-scss@14.0.0: + resolution: {integrity: sha512-6Pa26D9mHyi4LauJ83ls3ELqCglU6VfCXchovbEqQUiEkezvKdv6VgsIoMy58i00c854wVmOw0k8W5FTpuaVqg==} + engines: {node: '>=18.12.0'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.11.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-standard@36.0.1: + resolution: {integrity: sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-order@6.0.4: + resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 + + stylelint-prettier@5.0.3: + resolution: {integrity: sha512-B6V0oa35ekRrKZlf+6+jA+i50C4GXJ7X1PPmoCqSUoXN6BrNF6NhqqhanvkLjqw2qgvrS0wjdpeC+Tn06KN3jw==} + engines: {node: '>=18.12.0'} + peerDependencies: + prettier: '>=3.0.0' + stylelint: '>=16.0.0' + + stylelint-scss@6.11.1: + resolution: {integrity: sha512-e4rYo0UY+BIMtGeGanghrvHTjcryxgZbyFxUedp8dLFqC4P70aawNdYjRrQxbnKhu3BNr4+lt5e/53tcKXiwFA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.0.2 + + stylelint-scss@7.0.0: + resolution: {integrity: sha512-H88kCC+6Vtzj76NsC8rv6x/LW8slBzIbyeSjsKVlS+4qaEJoDrcJR4L+8JdrR2ORdTscrBzYWiiT2jq6leYR1Q==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^16.8.2 || ^17.0.0 + + stylelint@16.19.0: + resolution: {integrity: sha512-BJzc5mo/ez0H/ZSl3UbxGdkK/s0kFGsF5/k6IGu4z8wJ1qp49WrOS9RxswvcN6HMirt0g/iiJqOwLHTbWv49IQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.11.4: + resolution: {integrity: sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tailwindcss@4.1.4: + resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinygradient@1.1.5: + resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.40.0: + resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==} + engines: {node: '>=16'} + + typescript-eslint@8.31.0: + resolution: {integrity: sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unctx@2.4.1: + resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unimport@4.2.0: + resolution: {integrity: sha512-mYVtA0nmzrysnYnyb3ALMbByJ+Maosee2+WyE0puXl+Xm2bUwPorPaaeZt0ETfuroPOtG8jj1g/qeFZ6buFnag==} + engines: {node: '>=18.12.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-icons@22.1.0: + resolution: {integrity: sha512-ect2ZNtk1Zgwb0NVHd0C1IDW/MV+Jk/xaq4t8o6rYdVS3+L660ZdD5kTSQZvsgdwCvquRw+/wYn75hsweRjoIA==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + svelte: + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + + unplugin-utils@0.2.4: + resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} + engines: {node: '>=18.12.0'} + + unplugin@2.3.2: + resolution: {integrity: sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w==} + engines: {node: '>=18.12.0'} + + untyped@2.0.0: + resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} + hasBin: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-code-inspector-plugin@0.20.10: + resolution: {integrity: sha512-uE5nwooHTi3j1+ZWD4bYydiLGjtY8Nn/be2OEnHyXC0UQv4vM5fsB8V3glszWaQ+ip0yJw+VLtfgf1mVpvf7Mg==} + + vite-plugin-cdn-import@1.0.1: + resolution: {integrity: sha512-lgjLxgwFSKvJLbqjVBirUZ0rQo00GpUGJzRpgQu8RyBw9LA7jaqG6fUMQzBC9qWmTGabPC3iOzwCcoi7PseRAQ==} + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-externals@0.6.2: + resolution: {integrity: sha512-R5oVY8xDJjLXLTs2XDYzvYbc/RTZuIwOx2xcFbYf+/VXB6eJuatDgt8jzQ7kZ+IrgwQhe6tU8U2fTyy72C25CQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-fake-server@2.2.0: + resolution: {integrity: sha512-RP691997Q207nenNMhg7cvYyBXZyjqXwApTBa+a9kHmILgcAU2w4TMRDiAhIU0HPLAAR5MHlWTEpxA9Tbf+v8g==} + + vite-plugin-remove-console@2.2.0: + resolution: {integrity: sha512-qgjh5pz75MdE9Kzs8J0kBwaCfifHV0ezRbB9rpGsIOxam+ilcGV7WOk91vFJXquzRmiKrFh3Hxlh0JJWAmXTbQ==} + + vite-plugin-router-warn@1.0.0: + resolution: {integrity: sha512-jnr7faHJPkKxukBXVpg7Ui1UDqhmxD7xU6JGidq8ivSHTsNAPqzSpPpwW8O1PBP/0+Owq4bLfNNk11drOkz4xA==} + + vite-svg-loader@5.1.0: + resolution: {integrity: sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==} + peerDependencies: + vue: '>=3.2.13' + + vite@6.3.3: + resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@10.1.3: + resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + vue-router@4.5.0: + resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} + peerDependencies: + vue: ^3.2.0 + + vue-tippy@6.7.0: + resolution: {integrity: sha512-e0w6UA+A+J79GhDYNw5xZjGu7Tc2ksYypwF5RjkJVWgAGNSpXkLVNx4gZ8cMUa8FRHqmGAZxN3ue7MeXgbeZAQ==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.2.10: + resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue-types@6.0.0: + resolution: {integrity: sha512-fBgCA4nrBrB8SCU/AN40tFq8HUxLGBvU2ds7a5+SEDse6dYc+TJyvy8mWiwwL8oWIC/aGS/8nTqmhwxApgU5eA==} + engines: {node: '>=14.0.0'} + peerDependencies: + vue: ^3.0.0 + peerDependenciesMeta: + vue: + optional: true + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + webpack-code-inspector-plugin@0.20.10: + resolution: {integrity: sha512-I8mSEVbwMtQ1SSdb9pLK7VHqykobdrLvAgbZSKzrGQUGsmTbLmpVTJVs6EJgV1rsl5aoi1BKgmdr77CqaDSnfA==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.7.1: + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/install-pkg@1.0.0': + dependencies: + package-manager-detector: 0.2.11 + tinyexec: 0.3.2 + + '@antfu/utils@8.1.1': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.8': {} + + '@babel/core@7.26.10': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.0': + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.27.0 + + '@babel/helper-compilation-targets@7.27.0': + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.10) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.27.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.27.0 + + '@babel/helper-plugin-utils@7.26.5': {} + + '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.27.0': + dependencies: + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-typescript@7.27.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.27.0(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/traverse@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@commitlint/cli@19.8.0(@types/node@20.17.30)(typescript@5.8.3)': + dependencies: + '@commitlint/format': 19.8.0 + '@commitlint/lint': 19.8.0 + '@commitlint/load': 19.8.0(@types/node@20.17.30)(typescript@5.8.3) + '@commitlint/read': 19.8.0 + '@commitlint/types': 19.8.0 + tinyexec: 0.3.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + ajv: 8.17.1 + + '@commitlint/ensure@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.8.0': {} + + '@commitlint/format@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + chalk: 5.4.1 + + '@commitlint/is-ignored@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + semver: 7.7.1 + + '@commitlint/lint@19.8.0': + dependencies: + '@commitlint/is-ignored': 19.8.0 + '@commitlint/parse': 19.8.0 + '@commitlint/rules': 19.8.0 + '@commitlint/types': 19.8.0 + + '@commitlint/load@19.8.0(@types/node@20.17.30)(typescript@5.8.3)': + dependencies: + '@commitlint/config-validator': 19.8.0 + '@commitlint/execute-rule': 19.8.0 + '@commitlint/resolve-extends': 19.8.0 + '@commitlint/types': 19.8.0 + chalk: 5.4.1 + cosmiconfig: 9.0.0(typescript@5.8.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@20.17.30)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@19.8.0': {} + + '@commitlint/parse@19.8.0': + dependencies: + '@commitlint/types': 19.8.0 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.8.0': + dependencies: + '@commitlint/top-level': 19.8.0 + '@commitlint/types': 19.8.0 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 0.3.2 + + '@commitlint/resolve-extends@19.8.0': + dependencies: + '@commitlint/config-validator': 19.8.0 + '@commitlint/types': 19.8.0 + global-directory: 4.0.1 + import-meta-resolve: 4.1.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@19.8.0': + dependencies: + '@commitlint/ensure': 19.8.0 + '@commitlint/message': 19.8.0 + '@commitlint/to-lines': 19.8.0 + '@commitlint/types': 19.8.0 + + '@commitlint/to-lines@19.8.0': {} + + '@commitlint/top-level@19.8.0': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.8.0': + dependencies: + '@types/conventional-commits-parser': 5.0.1 + chalk: 5.4.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@3.0.3': {} + + '@csstools/css-tokenizer@4.0.0': {} + + '@csstools/media-query-list-parser@4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@ctrl/tinycolor@3.6.1': {} + + '@dual-bundle/import-meta-resolve@4.1.0': {} + + '@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.8.3))': + dependencies: + vue: 3.5.13(typescript@5.8.3) + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/aix-ppc64@0.25.3': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.25.3': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-arm@0.25.3': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/android-x64@0.25.3': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.25.3': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.25.3': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.25.3': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.25.3': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.25.3': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-arm@0.25.3': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.25.3': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.25.3': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.25.3': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.25.3': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.25.3': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.25.3': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/linux-x64@0.25.3': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.3': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.25.3': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.3': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.25.3': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.25.3': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.25.3': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.25.3': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@esbuild/win32-x64@0.25.3': + optional: true + + '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1(jiti@2.4.2))': + dependencies: + eslint: 9.25.1(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.20.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.1': {} + + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.25.1': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.2.8': + dependencies: + '@eslint/core': 0.13.0 + levn: 0.4.1 + + '@exodus/bytes@1.15.0': {} + + '@faker-js/faker@9.7.0': {} + + '@floating-ui/core@1.6.9': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.6.13': + dependencies: + '@floating-ui/core': 1.6.9 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/utils@0.2.9': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.2': {} + + '@iconify/json@2.2.331': + dependencies: + '@iconify/types': 2.0.0 + pathe: 1.1.2 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.3.0': + dependencies: + '@antfu/install-pkg': 1.0.0 + '@antfu/utils': 8.1.1 + '@iconify/types': 2.0.0 + debug: 4.4.0 + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.1 + mlly: 1.7.4 + transitivePeerDependencies: + - supports-color + + '@iconify/vue@4.2.0(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.13(typescript@5.8.3) + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@keyv/serialize@1.0.3': + dependencies: + buffer: 6.0.3 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nuxt/kit@3.16.2': + dependencies: + c12: 3.0.3 + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + errx: 0.1.0 + exsolve: 1.0.5 + globby: 14.1.0 + ignore: 7.0.3 + jiti: 2.4.2 + klona: 2.0.6 + knitwork: 1.2.0 + mlly: 1.7.4 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.1.0 + scule: 1.3.0 + semver: 7.7.1 + std-env: 3.9.0 + ufo: 1.6.1 + unctx: 2.4.1 + unimport: 4.2.0 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + optional: true + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pkgr/core@0.2.4': {} + + '@polka/url@1.0.0-next.29': {} + + '@popperjs/core@2.11.8': {} + + '@pureadmin/descriptions@1.2.1(echarts@5.6.0)(element-plus@2.9.8(vue@3.5.13(typescript@5.8.3)))(typescript@5.8.3)': + dependencies: + '@element-plus/icons-vue': 2.3.1(vue@3.5.13(typescript@5.8.3)) + '@pureadmin/utils': 2.6.0(echarts@5.6.0)(vue@3.5.13(typescript@5.8.3)) + element-plus: 2.9.8(vue@3.5.13(typescript@5.8.3)) + vue: 3.5.13(typescript@5.8.3) + transitivePeerDependencies: + - echarts + - typescript + + '@pureadmin/table@3.2.1(element-plus@2.9.8(vue@3.5.13(typescript@5.8.3)))(typescript@5.8.3)': + dependencies: + element-plus: 2.9.8(vue@3.5.13(typescript@5.8.3)) + vue: 3.5.13(typescript@5.8.3) + transitivePeerDependencies: + - typescript + + '@pureadmin/utils@2.6.0(echarts@5.6.0)(vue@3.5.13(typescript@5.8.3))': + optionalDependencies: + echarts: 5.6.0 + vue: 3.5.13(typescript@5.8.3) + + '@rollup/pluginutils@5.1.4(rollup@4.40.0)': + dependencies: + '@types/estree': 1.0.7 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.40.0 + + '@rollup/rollup-android-arm-eabi@4.40.0': + optional: true + + '@rollup/rollup-android-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-x64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.40.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.40.0': + optional: true + + '@sindresorhus/merge-streams@2.3.0': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@tailwindcss/node@4.1.4': + dependencies: + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.29.2 + tailwindcss: 4.1.4 + + '@tailwindcss/oxide-android-arm64@4.1.4': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.4': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.4': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.4': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.4': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.4': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.4': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.4': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.4': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.4': + optional: true + + '@tailwindcss/oxide@4.1.4': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.4 + '@tailwindcss/oxide-darwin-arm64': 4.1.4 + '@tailwindcss/oxide-darwin-x64': 4.1.4 + '@tailwindcss/oxide-freebsd-x64': 4.1.4 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.4 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.4 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.4 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.4 + '@tailwindcss/oxide-linux-x64-musl': 4.1.4 + '@tailwindcss/oxide-wasm32-wasi': 4.1.4 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.4 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.4 + + '@tailwindcss/vite@4.1.4(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))': + dependencies: + '@tailwindcss/node': 4.1.4 + '@tailwindcss/oxide': 4.1.4 + tailwindcss: 4.1.4 + vite: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + + '@trysound/sax@0.2.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/conventional-commits-parser@5.0.1': + dependencies: + '@types/node': 20.17.30 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.7': {} + + '@types/js-cookie@3.0.6': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.16 + + '@types/lodash@4.17.16': {} + + '@types/node@20.17.30': + dependencies: + undici-types: 6.19.8 + + '@types/nprogress@0.2.3': {} + + '@types/path-browserify@1.0.3': {} + + '@types/qs@6.9.18': {} + + '@types/sortablejs@1.15.8': {} + + '@types/tinycolor2@1.4.6': {} + + '@types/web-bluetooth@0.0.16': {} + + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.31.0 + '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.0 + eslint: 9.25.1(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.31.0 + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.0 + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.31.0': + dependencies: + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/visitor-keys': 8.31.0 + + '@typescript-eslint/type-utils@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.31.0': {} + + '@typescript-eslint/typescript-estree@8.31.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/visitor-keys': 8.31.0 + debug: 4.4.0 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.31.0 + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.31.0': + dependencies: + '@typescript-eslint/types': 8.31.0 + eslint-visitor-keys: 4.2.0 + + '@vitejs/plugin-vue-jsx@4.1.2(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10) + '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.26.10) + vite: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + vue: 3.5.13(typescript@5.8.3) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue@5.2.3(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))': + dependencies: + vite: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + vue: 3.5.13(typescript@5.8.3) + + '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.5 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@types/node@20.17.30)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.0.2)(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + + '@vitest/expect@4.1.5': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.5(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))': + dependencies: + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + + '@vitest/pretty-format@4.1.5': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.5': + dependencies: + '@vitest/utils': 4.1.5 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.5': {} + + '@vitest/ui@4.1.5(vitest@4.1.5)': + dependencies: + '@vitest/utils': 4.1.5 + fflate: 0.8.2 + flatted: 3.4.2 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@types/node@20.17.30)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.0.2)(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + + '@vitest/utils@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@volar/language-core@2.4.12': + dependencies: + '@volar/source-map': 2.4.12 + + '@volar/source-map@2.4.12': {} + + '@volar/typescript@2.4.12': + dependencies: + '@volar/language-core': 2.4.12 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.4.0': {} + + '@vue/babel-plugin-jsx@1.4.0(@babel/core@7.26.10)': + dependencies: + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + '@vue/babel-helper-vue-transform-on': 1.4.0 + '@vue/babel-plugin-resolve-type': 1.4.0(@babel/core@7.26.10) + '@vue/shared': 3.5.13 + optionalDependencies: + '@babel/core': 7.26.10 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.4.0(@babel/core@7.26.10)': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/parser': 7.27.0 + '@vue/compiler-sfc': 3.5.13 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.27.0 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.27.0 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.3 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.5': + dependencies: + '@vue/devtools-kit': 7.7.5 + + '@vue/devtools-kit@7.7.5': + dependencies: + '@vue/devtools-shared': 7.7.5 + birpc: 2.3.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.5': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@2.2.10(typescript@5.8.3)': + dependencies: + '@volar/language-core': 2.4.12 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + alien-signals: 1.0.13 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.8.3 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.8.3) + + '@vue/shared@3.5.13': {} + + '@vueuse/core@13.1.0(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.1.0 + '@vueuse/shared': 13.1.0(vue@3.5.13(typescript@5.8.3)) + vue: 3.5.13(typescript@5.8.3) + + '@vueuse/core@9.13.0(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.13(typescript@5.8.3)) + vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@13.1.0': {} + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/motion@3.0.3(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@vueuse/core': 13.1.0(vue@3.5.13(typescript@5.8.3)) + '@vueuse/shared': 13.1.0(vue@3.5.13(typescript@5.8.3)) + defu: 6.1.4 + framesync: 6.1.2 + popmotion: 11.0.5 + style-value-types: 5.1.2 + vue: 3.5.13(typescript@5.8.3) + optionalDependencies: + '@nuxt/kit': 3.16.2 + transitivePeerDependencies: + - magicast + + '@vueuse/shared@13.1.0(vue@3.5.13(typescript@5.8.3))': + dependencies: + vue: 3.5.13(typescript@5.8.3) + + '@vueuse/shared@9.13.0(vue@3.5.13(typescript@5.8.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@1.0.13: {} + + animate.css@4.1.1: {} + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + argparse@2.0.1: {} + + array-ify@1.0.0: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + astral-regex@2.0.0: {} + + async-validator@4.2.5: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + base64-js@1.5.1: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + birpc@2.3.0: {} + + boolbase@1.0.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.40.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.4: + dependencies: + caniuse-lite: 1.0.30001715 + electron-to-chromium: 1.5.142 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-import@0.0.2: + dependencies: + get-tsconfig: 4.10.0 + import-from-string: 0.0.5 + + c12@3.0.3: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.5.0 + exsolve: 1.0.5 + giget: 2.0.0 + jiti: 2.4.2 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.0 + rc9: 2.1.2 + optional: true + + cacheable@1.8.10: + dependencies: + hookified: 1.8.2 + keyv: 5.3.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@8.0.0: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.24.4 + caniuse-lite: 1.0.30001715 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001715: {} + + chai@6.2.2: {} + + chalk@4.1.1: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + citty@0.1.6: + dependencies: + consola: 3.4.2 + optional: true + + cli-boxes@3.0.0: {} + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + code-inspector-core@0.20.10: + dependencies: + '@vue/compiler-dom': 3.5.13 + chalk: 4.1.1 + dotenv: 16.5.0 + launch-ide: 1.0.7 + portfinder: 1.0.36 + transitivePeerDependencies: + - supports-color + + code-inspector-plugin@0.20.10: + dependencies: + chalk: 4.1.1 + code-inspector-core: 0.20.10 + dotenv: 16.5.0 + esbuild-code-inspector-plugin: 0.20.10 + vite-code-inspector-plugin: 0.20.10 + webpack-code-inspector-plugin: 0.20.10 + transitivePeerDependencies: + - supports-color + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@13.1.0: {} + + commander@7.2.0: {} + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + consola@3.4.2: + optional: true + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + cosmiconfig-typescript-loader@6.1.0(@types/node@20.17.30)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): + dependencies: + '@types/node': 20.17.30 + cosmiconfig: 9.0.0(typescript@5.8.3) + jiti: 2.4.2 + typescript: 5.8.3 + + cosmiconfig@9.0.0(typescript@5.8.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.8.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-declaration-sorter@7.2.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + css-functions-list@3.2.3: {} + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@7.0.6(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + css-declaration-sorter: 7.2.0(postcss@8.5.3) + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-calc: 10.1.1(postcss@8.5.3) + postcss-colormin: 7.0.2(postcss@8.5.3) + postcss-convert-values: 7.0.4(postcss@8.5.3) + postcss-discard-comments: 7.0.3(postcss@8.5.3) + postcss-discard-duplicates: 7.0.1(postcss@8.5.3) + postcss-discard-empty: 7.0.0(postcss@8.5.3) + postcss-discard-overridden: 7.0.0(postcss@8.5.3) + postcss-merge-longhand: 7.0.4(postcss@8.5.3) + postcss-merge-rules: 7.0.4(postcss@8.5.3) + postcss-minify-font-values: 7.0.0(postcss@8.5.3) + postcss-minify-gradients: 7.0.0(postcss@8.5.3) + postcss-minify-params: 7.0.2(postcss@8.5.3) + postcss-minify-selectors: 7.0.4(postcss@8.5.3) + postcss-normalize-charset: 7.0.0(postcss@8.5.3) + postcss-normalize-display-values: 7.0.0(postcss@8.5.3) + postcss-normalize-positions: 7.0.0(postcss@8.5.3) + postcss-normalize-repeat-style: 7.0.0(postcss@8.5.3) + postcss-normalize-string: 7.0.0(postcss@8.5.3) + postcss-normalize-timing-functions: 7.0.0(postcss@8.5.3) + postcss-normalize-unicode: 7.0.2(postcss@8.5.3) + postcss-normalize-url: 7.0.0(postcss@8.5.3) + postcss-normalize-whitespace: 7.0.0(postcss@8.5.3) + postcss-ordered-values: 7.0.1(postcss@8.5.3) + postcss-reduce-initial: 7.0.2(postcss@8.5.3) + postcss-reduce-transforms: 7.0.0(postcss@8.5.3) + postcss-svgo: 7.0.1(postcss@8.5.3) + postcss-unique-selectors: 7.0.3(postcss@8.5.3) + + cssnano-utils@5.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + cssnano@7.0.6(postcss@8.5.3): + dependencies: + cssnano-preset-default: 7.0.6(postcss@8.5.3) + lilconfig: 3.1.3 + postcss: 8.5.3 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + csstype@3.1.3: {} + + dargs@8.1.0: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + dayjs@1.11.13: {} + + de-indent@1.0.2: {} + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + define-lazy-prop@2.0.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + destr@2.0.5: + optional: true + + detect-libc@1.0.3: + optional: true + + detect-libc@2.0.4: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv@16.5.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + + electron-to-chromium@1.5.142: {} + + element-plus@2.9.8(vue@3.5.13(typescript@5.8.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.1(vue@3.5.13(typescript@5.8.3)) + '@floating-ui/dom': 1.6.13 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.16 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.13(typescript@5.8.3)) + async-validator: 4.2.5 + dayjs: 1.11.13 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.13(typescript@5.8.3) + transitivePeerDependencies: + - '@vue/composition-api' + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + entities@4.5.0: {} + + entities@8.0.0: {} + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + errx@0.1.0: + optional: true + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@0.4.1: {} + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild-code-inspector-plugin@0.20.10: + dependencies: + code-inspector-core: 0.20.10 + transitivePeerDependencies: + - supports-color + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + esbuild@0.25.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.3 + '@esbuild/android-arm': 0.25.3 + '@esbuild/android-arm64': 0.25.3 + '@esbuild/android-x64': 0.25.3 + '@esbuild/darwin-arm64': 0.25.3 + '@esbuild/darwin-x64': 0.25.3 + '@esbuild/freebsd-arm64': 0.25.3 + '@esbuild/freebsd-x64': 0.25.3 + '@esbuild/linux-arm': 0.25.3 + '@esbuild/linux-arm64': 0.25.3 + '@esbuild/linux-ia32': 0.25.3 + '@esbuild/linux-loong64': 0.25.3 + '@esbuild/linux-mips64el': 0.25.3 + '@esbuild/linux-ppc64': 0.25.3 + '@esbuild/linux-riscv64': 0.25.3 + '@esbuild/linux-s390x': 0.25.3 + '@esbuild/linux-x64': 0.25.3 + '@esbuild/netbsd-arm64': 0.25.3 + '@esbuild/netbsd-x64': 0.25.3 + '@esbuild/openbsd-arm64': 0.25.3 + '@esbuild/openbsd-x64': 0.25.3 + '@esbuild/sunos-x64': 0.25.3 + '@esbuild/win32-arm64': 0.25.3 + '@esbuild/win32-ia32': 0.25.3 + '@esbuild/win32-x64': 0.25.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: + optional: true + + eslint-config-prettier@10.1.2(eslint@9.25.1(jiti@2.4.2)): + dependencies: + eslint: 9.25.1(jiti@2.4.2) + + eslint-plugin-prettier@5.2.6(eslint-config-prettier@10.1.2(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))(prettier@3.5.3): + dependencies: + eslint: 9.25.1(jiti@2.4.2) + prettier: 3.5.3 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.4 + optionalDependencies: + eslint-config-prettier: 10.1.2(eslint@9.25.1(jiti@2.4.2)) + + eslint-plugin-vue@10.0.0(eslint@9.25.1(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.25.1(jiti@2.4.2))): + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2)) + eslint: 9.25.1(jiti@2.4.2) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.1 + vue-eslint-parser: 10.1.3(eslint@9.25.1(jiti@2.4.2)) + xml-name-validator: 4.0.0 + + eslint-scope@8.3.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.25.1(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.20.0 + '@eslint/config-helpers': 0.2.1 + '@eslint/core': 0.13.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.25.1 + '@eslint/plugin-kit': 0.2.8 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.2 + '@types/estree': 1.0.7 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + eslint-scope: 8.3.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.7 + + esutils@2.0.3: {} + + eventemitter3@5.0.1: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + expect-type@1.3.0: {} + + exsolve@1.0.5: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.0.6: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fflate@0.8.2: {} + + file-entry-cache@10.0.8: + dependencies: + flat-cache: 6.1.8 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flat-cache@6.1.8: + dependencies: + cacheable: 1.8.10 + flatted: 3.3.3 + hookified: 1.8.2 + + flatted@3.3.3: {} + + flatted@3.4.2: {} + + follow-redirects@1.15.9: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + framesync@6.1.2: + dependencies: + tslib: 2.4.0 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@8.0.1: {} + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.6 + nypm: 0.6.0 + pathe: 2.0.3 + optional: true + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.0.2: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.0 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.15.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@14.1.0: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 7.0.3 + path-type: 6.0.0 + slash: 5.1.0 + unicorn-magic: 0.3.0 + optional: true + + globjoin@0.1.4: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gradient-string@3.0.0: + dependencies: + chalk: 5.4.1 + tinygradient: 1.1.5 + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hey-listen@1.0.8: {} + + hookable@5.5.3: {} + + hookified@1.8.2: {} + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + + html-tags@3.3.1: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + human-signals@5.0.0: {} + + husky@9.1.7: {} + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.3: {} + + immediate@3.0.6: {} + + immutable@5.1.1: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-from-string@0.0.5: + dependencies: + esbuild: 0.24.2 + import-meta-resolve: 4.1.0 + + import-meta-resolve@4.1.0: {} + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + is-arrayish@0.2.1: {} + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-plain-object@5.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.7 + + is-stream@3.0.0: {} + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-what@4.1.16: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@4.1.0: + dependencies: + '@isaacs/cliui': 8.0.2 + + jiti@2.4.2: {} + + js-cookie@3.0.5: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdom@29.0.2: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + keyv@5.3.3: + dependencies: + '@keyv/serialize': 1.0.3 + + kind-of@6.0.3: {} + + klona@2.0.6: + optional: true + + knitwork@1.2.0: + optional: true + + known-css-properties@0.35.0: {} + + known-css-properties@0.36.0: {} + + known-css-properties@0.37.0: {} + + kolorist@1.8.0: {} + + launch-ide@1.0.7: + dependencies: + chalk: 4.1.1 + dotenv: 16.5.0 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.1.1: + dependencies: + immediate: 3.0.6 + + lightningcss-darwin-arm64@1.29.2: + optional: true + + lightningcss-darwin-x64@1.29.2: + optional: true + + lightningcss-freebsd-x64@1.29.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.2: + optional: true + + lightningcss-linux-arm64-gnu@1.29.2: + optional: true + + lightningcss-linux-arm64-musl@1.29.2: + optional: true + + lightningcss-linux-x64-gnu@1.29.2: + optional: true + + lightningcss-linux-x64-musl@1.29.2: + optional: true + + lightningcss-win32-arm64-msvc@1.29.2: + optional: true + + lightningcss-win32-x64-msvc@1.29.2: + optional: true + + lightningcss@1.29.2: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.2 + lightningcss-darwin-x64: 1.29.2 + lightningcss-freebsd-x64: 1.29.2 + lightningcss-linux-arm-gnueabihf: 1.29.2 + lightningcss-linux-arm64-gnu: 1.29.2 + lightningcss-linux-arm64-musl: 1.29.2 + lightningcss-linux-x64-gnu: 1.29.2 + lightningcss-linux-x64-musl: 1.29.2 + lightningcss-win32-arm64-msvc: 1.29.2 + lightningcss-win32-x64-msvc: 1.29.2 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lint-staged@15.5.1: + dependencies: + chalk: 5.4.1 + commander: 13.1.0 + debug: 4.4.0 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.3.2 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.7.1 + transitivePeerDependencies: + - supports-color + + listr2@8.3.2: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.1.0 + quansync: 0.2.10 + + localforage@1.10.0: + dependencies: + lie: 3.1.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.camelcase@4.3.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.truncate@4.4.2: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + lru-cache@11.1.0: {} + + lru-cache@11.3.5: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.1 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + mdn-data@2.12.2: {} + + mdn-data@2.21.0: {} + + mdn-data@2.27.0: {} + + mdn-data@2.27.1: {} + + memoize-one@6.0.0: {} + + meow@12.1.1: {} + + meow@13.2.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mitt@3.0.1: {} + + mlly@1.7.4: + dependencies: + acorn: 8.14.1 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-addon-api@7.1.1: + optional: true + + node-fetch-native@1.6.6: + optional: true + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nypm@0.6.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.1.0 + tinyexec: 0.3.2 + optional: true + + object-inspect@1.13.4: {} + + obug@2.1.1: {} + + ohash@2.0.11: + optional: true + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + package-json-from-dist@1.0.1: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.10 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.1.0 + minipass: 7.1.2 + + path-to-regexp@8.2.0: {} + + path-type@4.0.0: {} + + path-type@6.0.0: + optional: true + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + picomatch@4.0.4: {} + + pidtree@0.6.0: {} + + pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)): + dependencies: + '@vue/devtools-api': 7.7.5 + vue: 3.5.13(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + + pinyin-pro@3.26.0: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + pkg-types@2.1.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.5 + pathe: 2.0.3 + + popmotion@11.0.5: + dependencies: + framesync: 6.1.2 + hey-listen: 1.0.8 + style-value-types: 5.1.2 + tslib: 2.4.0 + + portfinder@1.0.36: + dependencies: + async: 3.2.6 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + postcss-calc@10.1.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-colormin@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-convert-values@7.0.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-discard-comments@7.0.3(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-discard-duplicates@7.0.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-empty@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-overridden@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-html@1.8.0: + dependencies: + htmlparser2: 8.0.2 + js-tokens: 9.0.1 + postcss: 8.5.3 + postcss-safe-parser: 6.0.0(postcss@8.5.3) + + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.7.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.4.2 + postcss: 8.5.3 + yaml: 2.7.1 + + postcss-media-query-parser@0.2.3: {} + + postcss-merge-longhand@7.0.4(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + stylehacks: 7.0.4(postcss@8.5.3) + + postcss-merge-rules@7.0.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@7.0.0(postcss@8.5.3): + dependencies: + colord: 2.9.3 + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-params@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@7.0.4(postcss@8.5.3): + dependencies: + cssesc: 3.0.0 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-normalize-charset@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-normalize-display-values@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-ordered-values@7.0.1(postcss@8.5.3): + dependencies: + cssnano-utils: 5.0.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-reduce-initial@7.0.2(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + caniuse-api: 3.0.0 + postcss: 8.5.3 + + postcss-reduce-transforms@7.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-safe-parser@7.0.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-scss@4.0.9(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sorting@8.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-svgo@7.0.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + svgo: 3.3.2 + + postcss-unique-selectors@7.0.3(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.5.3: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.10: {} + + queue-microtask@1.2.3: {} + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + optional: true + + readdirp@4.1.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + responsive-storage@2.2.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@6.0.1: + dependencies: + glob: 11.0.2 + package-json-from-dist: 1.0.1 + + rollup-plugin-external-globals@0.10.0(rollup@4.40.0): + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.40.0) + estree-walker: 3.0.3 + is-reference: 3.0.3 + magic-string: 0.30.17 + rollup: 4.40.0 + + rollup-plugin-visualizer@5.14.0(rollup@4.40.0): + dependencies: + open: 8.4.2 + picomatch: 4.0.2 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.40.0 + + rollup@4.40.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.40.0 + '@rollup/rollup-android-arm64': 4.40.0 + '@rollup/rollup-darwin-arm64': 4.40.0 + '@rollup/rollup-darwin-x64': 4.40.0 + '@rollup/rollup-freebsd-arm64': 4.40.0 + '@rollup/rollup-freebsd-x64': 4.40.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 + '@rollup/rollup-linux-arm-musleabihf': 4.40.0 + '@rollup/rollup-linux-arm64-gnu': 4.40.0 + '@rollup/rollup-linux-arm64-musl': 4.40.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-musl': 4.40.0 + '@rollup/rollup-linux-s390x-gnu': 4.40.0 + '@rollup/rollup-linux-x64-gnu': 4.40.0 + '@rollup/rollup-linux-x64-musl': 4.40.0 + '@rollup/rollup-win32-arm64-msvc': 4.40.0 + '@rollup/rollup-win32-ia32-msvc': 4.40.0 + '@rollup/rollup-win32-x64-msvc': 4.40.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.87.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.1 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scule@1.3.0: + optional: true + + semver@6.3.1: {} + + semver@7.7.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slash@3.0.0: {} + + slash@5.1.0: + optional: true + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + sortablejs@1.15.6: {} + + source-map-js@1.2.1: {} + + source-map@0.7.4: {} + + sourcemap-codec@1.4.8: {} + + speakingurl@14.0.1: {} + + split2@4.2.0: {} + + stackback@0.0.2: {} + + std-env@3.9.0: + optional: true + + std-env@4.1.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + optional: true + + style-value-types@5.1.2: + dependencies: + hey-listen: 1.0.8 + tslib: 2.4.0 + + stylehacks@7.0.4(postcss@8.5.3): + dependencies: + browserslist: 4.24.4 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + postcss-html: 1.8.0 + stylelint: 16.19.0(typescript@5.8.3) + + stylelint-config-recess-order@6.0.0(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + stylelint: 16.19.0(typescript@5.8.3) + stylelint-order: 6.0.4(stylelint@16.19.0(typescript@5.8.3)) + + stylelint-config-recommended-scss@14.1.0(postcss@8.5.3)(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.3) + stylelint: 16.19.0(typescript@5.8.3) + stylelint-config-recommended: 14.0.1(stylelint@16.19.0(typescript@5.8.3)) + stylelint-scss: 6.11.1(stylelint@16.19.0(typescript@5.8.3)) + optionalDependencies: + postcss: 8.5.3 + + stylelint-config-recommended-vue@1.6.0(postcss-html@1.8.0)(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + postcss-html: 1.8.0 + semver: 7.7.1 + stylelint: 16.19.0(typescript@5.8.3) + stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@16.19.0(typescript@5.8.3)) + stylelint-config-recommended: 16.0.0(stylelint@16.19.0(typescript@5.8.3)) + + stylelint-config-recommended@14.0.1(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + stylelint: 16.19.0(typescript@5.8.3) + + stylelint-config-recommended@16.0.0(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + stylelint: 16.19.0(typescript@5.8.3) + + stylelint-config-standard-scss@14.0.0(postcss@8.5.3)(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + stylelint: 16.19.0(typescript@5.8.3) + stylelint-config-recommended-scss: 14.1.0(postcss@8.5.3)(stylelint@16.19.0(typescript@5.8.3)) + stylelint-config-standard: 36.0.1(stylelint@16.19.0(typescript@5.8.3)) + optionalDependencies: + postcss: 8.5.3 + + stylelint-config-standard@36.0.1(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + stylelint: 16.19.0(typescript@5.8.3) + stylelint-config-recommended: 14.0.1(stylelint@16.19.0(typescript@5.8.3)) + + stylelint-order@6.0.4(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + postcss: 8.5.3 + postcss-sorting: 8.0.2(postcss@8.5.3) + stylelint: 16.19.0(typescript@5.8.3) + + stylelint-prettier@5.0.3(prettier@3.5.3)(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + prettier: 3.5.3 + prettier-linter-helpers: 1.0.0 + stylelint: 16.19.0(typescript@5.8.3) + + stylelint-scss@6.11.1(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.35.0 + mdn-data: 2.21.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + stylelint: 16.19.0(typescript@5.8.3) + + stylelint-scss@7.0.0(stylelint@16.19.0(typescript@5.8.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mdn-data: 2.27.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + stylelint: 16.19.0(typescript@5.8.3) + + stylelint@16.19.0(typescript@5.8.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + '@dual-bundle/import-meta-resolve': 4.1.0 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.8.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.0 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 10.0.8 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.3 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.36.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.3) + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + + symbol-tree@3.2.4: {} + + synckit@0.11.4: + dependencies: + '@pkgr/core': 0.2.4 + tslib: 2.8.1 + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tailwindcss@4.1.4: {} + + tapable@2.2.1: {} + + text-extensions@2.4.0: {} + + through@2.3.8: {} + + tinybench@2.9.0: {} + + tinycolor2@1.6.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.1.1: {} + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinygradient@1.1.5: + dependencies: + '@types/tinycolor2': 1.4.6 + tinycolor2: 1.6.0 + + tinyrainbow@3.1.0: {} + + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tslib@2.3.0: {} + + tslib@2.4.0: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.40.0: {} + + typescript-eslint@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + ufo@1.6.1: {} + + unctx@2.4.1: + dependencies: + acorn: 8.14.1 + estree-walker: 3.0.3 + magic-string: 0.30.17 + unplugin: 2.3.2 + optional: true + + undici-types@6.19.8: {} + + undici@7.25.0: {} + + unicorn-magic@0.1.0: {} + + unicorn-magic@0.3.0: + optional: true + + unimport@4.2.0: + dependencies: + acorn: 8.14.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.1 + magic-string: 0.30.17 + mlly: 1.7.4 + pathe: 2.0.3 + picomatch: 4.0.2 + pkg-types: 2.1.0 + scule: 1.3.0 + strip-literal: 3.0.0 + tinyglobby: 0.2.13 + unplugin: 2.3.2 + unplugin-utils: 0.2.4 + optional: true + + universalify@2.0.1: {} + + unplugin-icons@22.1.0(@vue/compiler-sfc@3.5.13): + dependencies: + '@antfu/install-pkg': 1.0.0 + '@iconify/utils': 2.3.0 + debug: 4.4.0 + local-pkg: 1.1.1 + unplugin: 2.3.2 + optionalDependencies: + '@vue/compiler-sfc': 3.5.13 + transitivePeerDependencies: + - supports-color + + unplugin-utils@0.2.4: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.2 + optional: true + + unplugin@2.3.2: + dependencies: + acorn: 8.14.1 + picomatch: 4.0.2 + webpack-virtual-modules: 0.6.2 + + untyped@2.0.0: + dependencies: + citty: 0.1.6 + defu: 6.1.4 + jiti: 2.4.2 + knitwork: 1.2.0 + scule: 1.3.0 + optional: true + + update-browserslist-db@1.1.3(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite-code-inspector-plugin@0.20.10: + dependencies: + code-inspector-core: 0.20.10 + transitivePeerDependencies: + - supports-color + + vite-plugin-cdn-import@1.0.1(rollup@4.40.0)(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)): + dependencies: + rollup-plugin-external-globals: 0.10.0(rollup@4.40.0) + vite-plugin-externals: 0.6.2(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + transitivePeerDependencies: + - rollup + - vite + + vite-plugin-compression@0.5.1(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)): + dependencies: + chalk: 4.1.2 + debug: 4.4.0 + fs-extra: 10.1.0 + vite: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + transitivePeerDependencies: + - supports-color + + vite-plugin-externals@0.6.2(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)): + dependencies: + acorn: 8.14.1 + es-module-lexer: 0.4.1 + fs-extra: 10.1.0 + magic-string: 0.25.9 + vite: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + + vite-plugin-fake-server@2.2.0: + dependencies: + bundle-import: 0.0.2 + chokidar: 4.0.3 + path-to-regexp: 8.2.0 + picocolors: 1.1.1 + tinyglobby: 0.2.13 + + vite-plugin-remove-console@2.2.0: {} + + vite-plugin-router-warn@1.0.0: {} + + vite-svg-loader@5.1.0(vue@3.5.13(typescript@5.8.3)): + dependencies: + svgo: 3.3.2 + vue: 3.5.13(typescript@5.8.3) + + vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1): + dependencies: + esbuild: 0.25.3 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.40.0 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 20.17.30 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.2 + sass: 1.87.0 + yaml: 2.7.1 + + vitest@4.1.5(@types/node@20.17.30)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.0.2)(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 6.3.3(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.30 + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) + '@vitest/ui': 4.1.5(vitest@4.1.5) + jsdom: 29.0.2 + transitivePeerDependencies: + - msw + + vscode-uri@3.1.0: {} + + vue-demi@0.14.10(vue@3.5.13(typescript@5.8.3)): + dependencies: + vue: 3.5.13(typescript@5.8.3) + + vue-eslint-parser@10.1.3(eslint@9.25.1(jiti@2.4.2)): + dependencies: + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + eslint-scope: 8.3.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.8.3) + + vue-tippy@6.7.0(vue@3.5.13(typescript@5.8.3)): + dependencies: + tippy.js: 6.3.7 + vue: 3.5.13(typescript@5.8.3) + + vue-tsc@2.2.10(typescript@5.8.3): + dependencies: + '@volar/typescript': 2.4.12 + '@vue/language-core': 2.2.10(typescript@5.8.3) + typescript: 5.8.3 + + vue-types@6.0.0(vue@3.5.13(typescript@5.8.3)): + optionalDependencies: + vue: 3.5.13(typescript@5.8.3) + + vue@3.5.13(typescript@5.8.3): + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.8.3)) + '@vue/shared': 3.5.13 + optionalDependencies: + typescript: 5.8.3 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + webpack-code-inspector-plugin@0.20.10: + dependencies: + code-inspector-core: 0.20.10 + transitivePeerDependencies: + - supports-color + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + xml-name-validator@4.0.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@2.7.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + zrender@5.6.1: + dependencies: + tslib: 2.3.0 diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100755 index 0000000..e83ea08 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,8 @@ +// @ts-check + +/** @type {import('postcss-load-config').Config} */ +export default { + plugins: { + ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}) + } +}; diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..bef93d4b01212f57cecb50658e8c9518be13e607 GIT binary patch literal 1270 zcmVPx(ut`KgR9Hu~mu*a2RT#(r=eE7z0C^Jz8!DR##z|jzZ413cjgEl9q1$4Zb4*!4 zXjK@R=~U)K+?GvQvStqBzAS3?L86&PjfvBRDVv}SWNy0Q3kb4JH)snHXnD*f0=Mn$ z3o0k+m;3ym|MR=gIX&lGh6N72)v@-pEvi*6?L;@NDA2N>7h=g4A{MmK2d#^rozi|Cv!*48t1H)C9mNv+tmC;++D_EL^x z@6DML`&inAwjP%aAoDmWjfGk^7AizYYd}|fEhcPA`8fl0Qe(!@*lvqzXzLum3B0NY zKr}HtXI86T`CkF_7H2a9)ykSOTC=xu$!dTs|HMZ)H{F?^!Ji2QB4`JETJPnWqWEm| z$u0Lrdp>6G3mWD9{~5qkO|fon>^OluT0d(XA3lu1&KHrFSAepzM?C5gQJq=K?+*qM zumc9`*a6VA)t^b^m0kGb(yue&&6}%{m8F+TLz)V|00yh$K@g7mD}Ez)3>SJnnhWzh zk8Rr;k(jttk`jC>WC4tIw8)}BbepS}9xTq9G#ii2NIEe1@cSiUC7_Fm)MBFF-h~u3t82qKn z@7hXQnv7?+H-Y20`8^Bx)T~hz$|d;#7%Yyr0c?;HFJE#NeP164wy?2jQzde8@;r15 zXfSD1jo!%~0J+wYY>pHi{wkzQ9>T>RgYPf5r)%+8Rh_R5M>-*$@0JEU0E||9DUoEq zmnaiez3A_<1$}!_5EC1R#+P36(GsZHtl_r?0;nKjZ_2jceH4A?z6c3G6z$$^LqtTR z_cZ*eB&E)vWxve^JUU>o*tY|$Nv=x#=spY%TnrhYzWxQIrrzSKpLUlBy6_KW0gN@y zGGgR&)8ROR`-eJAO#BrxK-0_naeZ`*%sOD@Lb~#ZJb=}yCL-}z^%${Xm+<>HJ3=lV z7k2}8?%XGhfr|GJ${Q5sKcz`bG-#Y?}Xx~7y$8mU}mhK7Mwdfe;TIVyb>k%twUPc zs(^E&;FR%s>CER04+@xw6S^HzZ9QG3RDwUU%Cn4H6O(gRBZR1WsWT z(pkTpFeCuE#FM@5TAH424p^PB*9En*CXnV|1Bj+4zMERj5i$VE1Y;Q>Br%|a#2-y} gnzdo?2K(3i4UB<+FiBYjEC2ui07*qoM6N<$f(X(_s{jB1 literal 0 HcmV?d00001 diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100755 index 0000000..a63d2b1 --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/platform-config.json b/frontend/public/platform-config.json new file mode 100755 index 0000000..372ab8a --- /dev/null +++ b/frontend/public/platform-config.json @@ -0,0 +1,26 @@ +{ + "Version": "0.0.1", + "Title": "中尚鹏管理系统", + "FixedHeader": true, + "HiddenSideBar": false, + "MultiTagsCache": false, + "KeepAlive": true, + "Layout": "vertical", + "Theme": "light", + "DarkMode": false, + "OverallStyle": "light", + "Grey": false, + "Weak": false, + "HideTabs": false, + "HideFooter": false, + "Stretch": false, + "SidebarStatus": true, + "EpThemeColor": "#409EFF", + "ShowLogo": true, + "ShowModel": "smart", + "MenuArrowIconNoTransition": false, + "CachingAsyncRoutes": false, + "TooltipEffect": "light", + "ResponsiveStorageNameSpace": "responsive-", + "MenuSearchHistory": 6 +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100755 index 0000000..6086388 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/api/__tests__/applyDelivery.spec.ts b/frontend/src/api/__tests__/applyDelivery.spec.ts new file mode 100644 index 0000000..28e0cd8 --- /dev/null +++ b/frontend/src/api/__tests__/applyDelivery.spec.ts @@ -0,0 +1,221 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../applyDelivery') + +describe('Apply Delivery API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getDeliveryList ========== + describe('getDeliveryList', () => { + it('should call GET /api/apply-delivery/list without params', async () => { + const mockData = { + items: [ + { id: '1', orderNumber: 'D001', blNumber: 'BL001', blNumber2: 'BL002', licensePlate: 'ABC123', driverName: 'Driver 1', driverPhone: '123456', driverIdCard: 'ID001', commodity: 'Steel', quantity: 100, unit: 'kg', deliveryDate: '2024-01-01', deliveryAddress: 'Address 1', warehouse: 'Warehouse 1', status: 'pending', createTime: '2024-01-01', updateTime: '2024-01-01' } + ], + total: 1, + page: 1, + pageSize: 10 + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getDeliveryList() + + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data?.items).toHaveLength(1) + }) + + it('should call GET /api/apply-delivery/list with params', async () => { + const mockData = { + items: [], + total: 0, + page: 1, + pageSize: 10 + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { page: 2, pageSize: 20, orderNumber: 'D001', status: 'approved' } + const result = await api.getDeliveryList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery', { params }) + expect(result.success).toBe(true) + }) + + it('should call GET /api/apply-delivery/list with search params', async () => { + const mockData = { + items: [], + total: 0, + page: 1, + pageSize: 10 + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { blNumber: 'BL001', driverName: 'Driver', startDate: '2024-01-01', endDate: '2024-12-31' } + const result = await api.getDeliveryList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== getDeliveryDetail ========== + describe('getDeliveryDetail', () => { + it('should call GET /api/apply-delivery/detail/:id', async () => { + const mockData = { + id: '1', + orderNumber: 'D001', + blNumber: 'BL001', + blNumber2: 'BL002', + licensePlate: 'ABC123', + driverName: 'Driver 1', + driverPhone: '123456', + driverIdCard: 'ID001', + commodity: 'Steel', + quantity: 100, + unit: 'kg', + deliveryDate: '2024-01-01', + deliveryAddress: 'Address 1', + warehouse: 'Warehouse 1', + status: 'pending', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getDeliveryDetail('1') + + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/1') + expect(result.success).toBe(true) + expect(result.data?.orderNumber).toBe('D001') + }) + }) + + // ========== createDelivery ========== + describe('createDelivery', () => { + it('should call POST /api/apply-delivery/create with data', async () => { + const mockData = { id: '1', orderNumber: 'D001' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const createData = { + blNumber: 'BL001', + blNumber2: 'BL002', + licensePlate: 'ABC123', + driverName: 'Driver 1', + driverPhone: '123456', + driverIdCard: 'ID001', + commodity: 'Steel', + quantity: 100, + unit: 'kg', + deliveryDate: '2024-01-01', + deliveryAddress: 'Address 1', + warehouse: 'Warehouse 1' + } + const result = await api.createDelivery(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/apply-delivery', { data: createData }) + expect(result.success).toBe(true) + expect(result.data?.orderNumber).toBe('D001') + }) + }) + + // ========== updateDelivery ========== + describe('updateDelivery', () => { + it('should call PUT /api/apply-delivery/update/:id with data', async () => { + const mockData = { + id: '1', + orderNumber: 'D001', + blNumber: 'BL001', + status: 'pending', + createTime: '2024-01-01', + updateTime: '2024-01-02' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const updateData = { quantity: 150, remark: 'Updated quantity' } + const result = await api.updateDelivery('1', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/apply-delivery/1', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== cancelDelivery ========== + describe('cancelDelivery', () => { + it('should call PUT /api/apply-delivery/cancel/:id', async () => { + const mockData = { + id: '1', + orderNumber: 'D001', + status: 'cancelled', + createTime: '2024-01-01', + updateTime: '2024-01-02' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.cancelDelivery('1') + + expect(http.request).toHaveBeenCalledWith('put', '/api/apply-delivery/1/cancel') + expect(result.success).toBe(true) + expect(result.data?.status).toBe('cancelled') + }) + }) + + // ========== downloadDeliveryTemplate ========== + describe('downloadDeliveryTemplate', () => { + it('should call GET /api/apply-delivery/template/download', async () => { + const mockData = { fileName: 'delivery_template.xlsx', url: '/templates/delivery_template.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.downloadDeliveryTemplate() + + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/template/download') + expect(result.success).toBe(true) + expect(result.data?.fileName).toBe('delivery_template.xlsx') + }) + }) + + // ========== deleteDelivery ========== + describe('deleteDelivery', () => { + it('should call DELETE /api/apply-delivery/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteDelivery('1') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/apply-delivery/1') + expect(result.success).toBe(true) + }) + }) + + // ========== exportDeliveryList ========== + describe('exportDeliveryList', () => { + it('should call GET /api/apply-delivery/export without params', async () => { + const mockData = { fileName: 'delivery_export.xlsx', url: '/exports/delivery_export.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.exportDeliveryList() + + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/export', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data?.fileName).toBe('delivery_export.xlsx') + }) + + it('should call GET /api/apply-delivery/export with params', async () => { + const mockData = { fileName: 'delivery_export.xlsx', url: '/exports/delivery_export.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { orderNumber: 'D001', status: 'completed' } + const result = await api.exportDeliveryList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/export', { params }) + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/company.spec.ts b/frontend/src/api/__tests__/company.spec.ts new file mode 100644 index 0000000..0a65956 --- /dev/null +++ b/frontend/src/api/__tests__/company.spec.ts @@ -0,0 +1,218 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../company') + +describe('Company API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getCompanyFolders ========== + describe('getCompanyFolders', () => { + it('should call GET /api/company/folders', async () => { + const mockData = [ + { id: '1', name: 'Folder 1', count: 5, createTime: '2024-01-01', description: 'Desc 1', category: 'cat1' }, + { id: '2', name: 'Folder 2', count: 3, createTime: '2024-01-02', description: 'Desc 2', category: 'cat2' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getCompanyFolders() + + expect(http.request).toHaveBeenCalledWith('get', '/api/company/folders') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + // ========== getCompanyFiles ========== + describe('getCompanyFiles', () => { + it('should call GET /api/company/files without params', async () => { + const mockData = [ + { id: '1', fileName: 'File1.pdf', fileType: 'pdf', fileSize: '1024', uploadTime: '2024-01-01', folderId: '1', companyName: 'Company A', fileCategory: 'contract', expireDate: '2025-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getCompanyFiles() + + expect(http.request).toHaveBeenCalledWith('get', '/api/company/files', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toHaveLength(1) + }) + + it('should call GET /api/company/files with folderId param', async () => { + const mockData = [ + { id: '1', fileName: 'File1.pdf', fileType: 'pdf', fileSize: '1024', uploadTime: '2024-01-01', folderId: '5', fileCategory: 'contract' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getCompanyFiles({ folderId: '5' }) + + expect(http.request).toHaveBeenCalledWith('get', '/api/company/files', { params: { folderId: '5' } }) + expect(result.success).toBe(true) + }) + }) + + // ========== createCompanyFolder ========== + describe('createCompanyFolder', () => { + it('should call POST /api/company/folders with name only', async () => { + const mockData = { id: '1', name: 'New Folder', count: 0, createTime: '2024-01-01', description: '', category: '' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.createCompanyFolder({ name: 'New Folder' }) + + expect(http.request).toHaveBeenCalledWith('post', '/api/company/folders', { data: { name: 'New Folder' } }) + expect(result.success).toBe(true) + }) + + it('should call POST /api/company/folders with name and description', async () => { + const mockData = { id: '1', name: 'New Folder', count: 0, createTime: '2024-01-01', description: 'Test description', category: 'cat1' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.createCompanyFolder({ name: 'New Folder', description: 'Test description', category: 'cat1' }) + + expect(http.request).toHaveBeenCalledWith('post', '/api/company/folders', { + data: { name: 'New Folder', description: 'Test description', category: 'cat1' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateCompanyFolder ========== + describe('updateCompanyFolder', () => { + it('should call PUT /api/company/folders/:id with data', async () => { + const mockData = { id: '1', name: 'Updated Folder', description: 'New desc', category: 'cat2' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.updateCompanyFolder('1', { name: 'Updated Folder', description: 'New desc' }) + + expect(http.request).toHaveBeenCalledWith('put', '/api/company/folders/1', { + data: { name: 'Updated Folder', description: 'New desc' } + }) + expect(result.success).toBe(true) + }) + + it('should call PUT /api/company/folders/:id with partial data', async () => { + const mockData = { id: '1', name: 'Updated' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.updateCompanyFolder('folder-123', { name: 'Updated' }) + + expect(http.request).toHaveBeenCalledWith('put', '/api/company/folders/folder-123', { + data: { name: 'Updated' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteCompanyFolder ========== + describe('deleteCompanyFolder', () => { + it('should call DELETE /api/company/folders/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteCompanyFolder('1') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/company/folders/1') + expect(result.success).toBe(true) + }) + }) + + // ========== uploadCompanyFile ========== + describe('uploadCompanyFile', () => { + it('should call POST /api/company/files/upload with FormData', async () => { + const mockData = { uploadedCount: 1 } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'document.pdf') + formData.append('folderId', '1') + + const result = await api.uploadCompanyFile(formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/company/files/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + expect(result.data?.uploadedCount).toBe(1) + }) + }) + + // ========== updateCompanyFile ========== + describe('updateCompanyFile', () => { + it('should call PUT /api/company/files/:id with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const updateData = { + companyName: 'Updated Company', + fileCategory: 'license', + expireDate: '2025-12-31', + description: 'Updated description' + } + const result = await api.updateCompanyFile('1', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/company/files/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should call PUT /api/company/files/:id with folderId', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const updateData = { + companyName: 'Company', + fileCategory: 'contract', + folderId: 5 + } + const result = await api.updateCompanyFile('file-123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/company/files/file-123', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should call PUT /api/company/files/:id with null folderId', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const updateData = { + companyName: 'Company', + fileCategory: 'contract', + folderId: null + } + const result = await api.updateCompanyFile('1', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/company/files/1', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteCompanyFile ========== + describe('deleteCompanyFile', () => { + it('should call DELETE /api/company/files/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteCompanyFile('1') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/company/files/1') + expect(result.success).toBe(true) + }) + }) + + // ========== downloadCompanyFile ========== + describe('downloadCompanyFile', () => { + it('should call GET /api/company/files/:id/download', async () => { + const mockData = { fileName: 'document.pdf', url: '/api/company-files/document.pdf' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.downloadCompanyFile('1') + + expect(http.request).toHaveBeenCalledWith('get', '/api/company/files/1/download') + expect(result.success).toBe(true) + expect(result.data?.fileName).toBe('document.pdf') + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/contract.spec.ts b/frontend/src/api/__tests__/contract.spec.ts new file mode 100644 index 0000000..47e1d65 --- /dev/null +++ b/frontend/src/api/__tests__/contract.spec.ts @@ -0,0 +1,390 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../contract') + +describe('Contract API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getContractList ========== + describe('getContractList', () => { + it('should call GET /api/contract without params', async () => { + const mockData = [ + { id: 1, name: 'Contract 1', contractType: 'type1' }, + { id: 2, name: 'Contract 2', contractType: 'type2' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContractList() + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /api/contract with contractType param', async () => { + const mockData = [{ id: 1, name: 'Contract 1', contractType: 'sales' }] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContractList({ contractType: 'sales' }) + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract', { params: { contractType: 'sales' } }) + expect(result.success).toBe(true) + }) + }) + + // ========== getContract ========== + describe('getContract', () => { + it('should call GET /api/contract/:id with numeric id', async () => { + const mockData = { id: 1, name: 'Contract 1', contractType: 'sales' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContract(1) + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /api/contract/:id with string id', async () => { + const mockData = { id: 123, name: 'Contract 123', contractType: 'purchase' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContract('123') + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/123') + expect(result.success).toBe(true) + }) + }) + + // ========== submitContract ========== + describe('submitContract', () => { + it('should call POST /api/contract/upload with FormData and multipart headers', async () => { + const formData = new FormData() + formData.append('name', 'Test Contract') + const mockResponse = { success: true, message: 'Upload successful' } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const result = await api.submitContract(formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should return failure message on upload failure', async () => { + const formData = new FormData() + const mockResponse = { success: false, message: 'Upload failed' } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const result = await api.submitContract(formData) + + expect(result.success).toBe(false) + expect(result.message).toBe('Upload failed') + }) + }) + + // ========== batchUploadContracts ========== + describe('batchUploadContracts', () => { + it('should call POST /api/contract/batch-upload with FormData and multipart headers', async () => { + const formData = new FormData() + formData.append('files', new Blob(['content']), 'test.xlsx') + const mockResponse = { + success: true, + message: 'Batch upload complete', + data: { successCount: 5, failedItems: [] } + } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const result = await api.batchUploadContracts(formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/batch-upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + expect(result.data?.successCount).toBe(5) + }) + + it('should handle partial failures in batch upload', async () => { + const formData = new FormData() + const mockResponse = { + success: true, + message: 'Some items failed', + data: { + successCount: 3, + failedItems: [{ name: 'bad.xlsx', reason: 'Invalid format' }] + } + } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const result = await api.batchUploadContracts(formData) + + expect(result.data?.failedItems).toHaveLength(1) + expect(result.data?.failedItems[0].name).toBe('bad.xlsx') + }) + }) + + // ========== updateContract ========== + describe('updateContract', () => { + it('should call PUT /api/contract/:id with partial data', async () => { + const updateData = { name: 'Updated Contract', commodity: 'Steel' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateContract(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/contract/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { unitPrice: 1000 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateContract('abc123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/contract/abc123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateContractWithFiles ========== + describe('updateContractWithFiles', () => { + it('should call POST /api/contract/:id/update-files with FormData', async () => { + const formData = new FormData() + formData.append('name', 'Updated Name') + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated with files' }) + + const result = await api.updateContractWithFiles(1, formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/1/update-files', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteContract ========== + describe('deleteContract', () => { + it('should call DELETE /api/contract/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteContract(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/1') + expect(result.success).toBe(true) + }) + + it('should accept string id for delete', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteContract('xyz789') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/xyz789') + expect(result.success).toBe(true) + }) + }) + + // ========== getContractFolders ========== + describe('getContractFolders', () => { + it('should call GET /api/contract/folders', async () => { + const mockData = [ + { id: 1, name: 'Folder 1', description: 'Desc 1', createTime: '2024-01-01', updateTime: '2024-01-01' }, + { id: 2, name: 'Folder 2', description: 'Desc 2', createTime: '2024-01-02', updateTime: '2024-01-02' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContractFolders() + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/folders') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + // ========== createContractFolder ========== + describe('createContractFolder', () => { + it('should call POST /api/contract/folders with name only', async () => { + const mockData = { id: 1, name: 'New Folder', description: '', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.createContractFolder({ name: 'New Folder' }) + + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/folders', { data: { name: 'New Folder' } }) + expect(result.success).toBe(true) + }) + + it('should call POST /api/contract/folders with name and description', async () => { + const mockData = { id: 1, name: 'New Folder', description: 'Test description', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.createContractFolder({ name: 'New Folder', description: 'Test description' }) + + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/folders', { + data: { name: 'New Folder', description: 'Test description' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateContractFolder ========== + describe('updateContractFolder', () => { + it('should call PUT /api/contract/folders/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateContractFolder(1, { name: 'Updated Folder', description: 'New desc' }) + + expect(http.request).toHaveBeenCalledWith('put', '/api/contract/folders/1', { + data: { name: 'Updated Folder', description: 'New desc' } + }) + expect(result.success).toBe(true) + }) + + it('should accept string id for folder update', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateContractFolder('folder-123', { name: 'Updated' }) + + expect(http.request).toHaveBeenCalledWith('put', '/api/contract/folders/folder-123', { + data: { name: 'Updated' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteContractFolder ========== + describe('deleteContractFolder', () => { + it('should call DELETE /api/contract/folders/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteContractFolder(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/folders/1') + expect(result.success).toBe(true) + }) + + it('should accept string id for folder delete', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteContractFolder('folder-abc') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/folders/folder-abc') + expect(result.success).toBe(true) + }) + }) + + // ========== getContractBatches ========== + describe('getContractBatches', () => { + it('should call GET /api/contract/batches without params', async () => { + const mockData = [ + { id: 1, folderId: 1, batchName: 'Batch 1', remark: '', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContractBatches() + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /api/contract/batches with folderId param', async () => { + const mockData = [ + { id: 1, folderId: 5, batchName: 'Batch in Folder 5', remark: '', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContractBatches({ folderId: 5 }) + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches', { params: { folderId: 5 } }) + expect(result.success).toBe(true) + }) + }) + + // ========== getContractBatch ========== + describe('getContractBatch', () => { + it('should call GET /api/contract/batches/:id', async () => { + const mockData = { + id: 1, + folderId: 1, + batchName: 'Batch 1', + remark: 'Test batch', + createTime: '2024-01-01', + updateTime: '2024-01-01', + files: [{ id: 1, batchId: 1, name: 'file.pdf', url: '/uploads/file.pdf', fileSize: 1024 }] + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContractBatch(1) + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should accept string id for batch', async () => { + const mockData = { id: 123, folderId: 1, batchName: 'Batch 123', remark: '', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getContractBatch('123') + + expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches/123') + expect(result.success).toBe(true) + }) + }) + + // ========== createContractBatch ========== + describe('createContractBatch', () => { + it('should call POST /api/contract/batches with FormData and multipart headers', async () => { + const formData = new FormData() + formData.append('folderId', '1') + formData.append('batchName', 'New Batch') + formData.append('remark', 'Test remark') + formData.append('files', new Blob(['content']), 'test.pdf') + + const mockData = { id: 1, folderId: 1, batchName: 'New Batch', remark: 'Test remark', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.createContractBatch(formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/contract/batches', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + // ========== deleteContractBatch ========== + describe('deleteContractBatch', () => { + it('should call DELETE /api/contract/batches/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteContractBatch(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/batches/1') + expect(result.success).toBe(true) + }) + + it('should accept string id for batch delete', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteContractBatch('batch-xyz') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/batches/batch-xyz') + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/deliveryDetails.spec.ts b/frontend/src/api/__tests__/deliveryDetails.spec.ts new file mode 100644 index 0000000..4198cde --- /dev/null +++ b/frontend/src/api/__tests__/deliveryDetails.spec.ts @@ -0,0 +1,260 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../deliveryDetails') + +describe('Delivery Details API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getDeliveryDetails ========== + describe('getDeliveryDetails', () => { + it('should call GET /api/delivery-details/list without params', async () => { + const mockData = { + items: [ + { + id: '1', + outboundNumber: 'OB001', + deliveryOrderNumber: 'D001', + blNumber: 'BL001', + licensePlate: 'ABC123', + driverName: 'Driver 1', + driverPhone: '123456', + commodity: 'Steel', + commodityCode: 'S001', + batchNumber: 'B001', + quantity: 100, + unit: 'kg', + unitPrice: 50, + totalAmount: 5000, + outboundDate: '2024-01-01', + outboundTime: '10:00', + warehouse: 'Warehouse 1', + location: 'A-1', + operator: 'Operator 1', + outboundStatus: 'pending', + receiptStatus: 'pending', + receivedQuantity: 0, + receiptDate: '', + receiver: '', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ], + total: 1, + page: 1, + pageSize: 10 + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getDeliveryDetails() + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data?.items).toHaveLength(1) + }) + + it('should call GET /api/delivery-details/list with params', async () => { + const mockData = { items: [], total: 0, page: 1, pageSize: 10 } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { page: 2, pageSize: 20, outboundNumber: 'OB001', outboundStatus: 'completed' } + const result = await api.getDeliveryDetails(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== getDeliveryDetail ========== + describe('getDeliveryDetail', () => { + it('should call GET /api/delivery-details/detail/:id', async () => { + const mockData = { + id: '1', + outboundNumber: 'OB001', + deliveryOrderNumber: 'D001', + blNumber: 'BL001', + commodity: 'Steel', + quantity: 100, + outboundStatus: 'pending', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getDeliveryDetail('1') + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/1') + expect(result.success).toBe(true) + expect(result.data?.outboundNumber).toBe('OB001') + }) + }) + + // ========== createOutboundOrder ========== + describe('createOutboundOrder', () => { + it('should call POST /api/delivery-details/create with data', async () => { + const mockData = { + items: [ + { id: '1', outboundNumber: 'OB001', commodity: 'Steel', quantity: 100 } + ], + count: 1 + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const createData = { + deliveryOrderNumber: 'D001', + blNumber: 'BL001', + licensePlate: 'ABC123', + driverName: 'Driver 1', + driverPhone: '123456', + items: [ + { commodity: 'Steel', quantity: 100, unit: 'kg', unitPrice: 50 } + ], + outboundDate: '2024-01-01', + warehouse: 'Warehouse 1', + operator: 'Operator 1' + } + const result = await api.createOutboundOrder(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/delivery-details', { data: createData }) + expect(result.success).toBe(true) + expect(result.data?.count).toBe(1) + }) + }) + + // ========== updateOutboundOrder ========== + describe('updateOutboundOrder', () => { + it('should call PUT /api/delivery-details/update/:id with data', async () => { + const mockData = { + id: '1', + outboundNumber: 'OB001', + quantity: 150, + updateTime: '2024-01-02' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const updateData = { quantity: 150, remark: 'Updated' } + const result = await api.updateOutboundOrder('1', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/delivery-details/1', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== confirmReceipt ========== + describe('confirmReceipt', () => { + it('should call PUT /api/delivery-details/confirm-receipt/:id with data', async () => { + const mockData = { + deliveryDetail: { id: '1', receiptStatus: 'completed', receivedQuantity: 100 }, + receiptRecord: { id: '1', receiptDate: '2024-01-02', receivedQuantity: 100, receiver: 'Receiver 1' } + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const receiptData = { + receiptDate: '2024-01-02', + receivedQuantity: 100, + receiver: 'Receiver 1' + } + const result = await api.confirmReceipt('1', receiptData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/delivery-details/confirm-receipt/1', { data: receiptData }) + expect(result.success).toBe(true) + expect(result.data?.deliveryDetail.receiptStatus).toBe('completed') + }) + + it('should call PUT /api/delivery-details/confirm-receipt/:id with discrepancy reason', async () => { + const mockData = { + deliveryDetail: { id: '1', receiptStatus: 'discrepancy', receivedQuantity: 90 }, + receiptRecord: { id: '1', receiptDate: '2024-01-02', receivedQuantity: 90, remark: 'Discrepancy' } + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const receiptData = { + receiptDate: '2024-01-02', + receivedQuantity: 90, + receiver: 'Receiver 1', + discrepancyReason: 'Damaged goods' + } + const result = await api.confirmReceipt('1', receiptData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/delivery-details/confirm-receipt/1', { data: receiptData }) + expect(result.success).toBe(true) + }) + }) + + // ========== getReceiptHistory ========== + describe('getReceiptHistory', () => { + it('should call GET /api/delivery-details/receipt-history/:id', async () => { + const mockData = [ + { id: '1', deliveryDetailId: '1', receiptDate: '2024-01-02', receivedQuantity: 50, receiver: 'Receiver 1', createTime: '2024-01-02' }, + { id: '2', deliveryDetailId: '1', receiptDate: '2024-01-03', receivedQuantity: 50, receiver: 'Receiver 2', createTime: '2024-01-03' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getReceiptHistory('1') + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/receipt-history/1') + expect(result.success).toBe(true) + expect(result.data).toHaveLength(2) + }) + }) + + // ========== getOutboundOrderTemplate ========== + describe('getOutboundOrderTemplate', () => { + it('should call GET /api/delivery-details/template/download', async () => { + const mockData = { fileName: 'outbound_template.xlsx', url: '/templates/outbound_template.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getOutboundOrderTemplate() + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/template/download') + expect(result.success).toBe(true) + expect(result.data?.fileName).toBe('outbound_template.xlsx') + }) + }) + + // ========== exportDeliveryDetails ========== + describe('exportDeliveryDetails', () => { + it('should call GET /api/delivery-details/export without params', async () => { + const mockData = { fileName: 'delivery_details_export.xlsx', url: '/exports/delivery_details_export.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.exportDeliveryDetails() + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/export', { params: undefined }) + expect(result.success).toBe(true) + }) + + it('should call GET /api/delivery-details/export with params', async () => { + const mockData = { fileName: 'export.xlsx', url: '/exports/export.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { outboundNumber: 'OB001', receiptStatus: 'completed' } + const result = await api.exportDeliveryDetails(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/export', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== printDeliveryOrder ========== + describe('printDeliveryOrder', () => { + it('should call GET /api/delivery-details/print/:id', async () => { + const mockData = { fileName: 'delivery_order.pdf', url: '/prints/delivery_order.pdf' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.printDeliveryOrder('1') + + expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/print/1') + expect(result.success).toBe(true) + expect(result.data?.fileName).toBe('delivery_order.pdf') + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/inventory.spec.ts b/frontend/src/api/__tests__/inventory.spec.ts new file mode 100644 index 0000000..62972eb --- /dev/null +++ b/frontend/src/api/__tests__/inventory.spec.ts @@ -0,0 +1,394 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../inventory') + +describe('Inventory API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getInventoryList ========== + describe('getInventoryList', () => { + it('should call GET /api/inventory/inbound/list without params', async () => { + const mockData = { + items: [ + { + id: '1', + inboundNumber: 'IB001', + warehouse: 'Warehouse 1', + commodity: 'Steel', + commodityCode: 'S001', + blNumber: 'BL001', + blWeight: 100, + inboundWeight: 95, + owner: 'Owner 1', + contractNumber: 'C001', + inboundDate: '2024-01-01', + operator: 'Operator 1', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ], + total: 1, + page: 1, + pageSize: 10 + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getInventoryList() + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/inbound', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data?.items).toHaveLength(1) + }) + + it('should call GET /api/inventory/inbound/list with params', async () => { + const mockData = { items: [], total: 0, page: 1, pageSize: 10 } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { page: 2, pageSize: 20, inboundNumber: 'IB001', warehouse: 'Warehouse 1' } + const result = await api.getInventoryList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/inbound', { params }) + expect(result.success).toBe(true) + }) + + it('should call GET /api/inventory/inbound/list with date range params', async () => { + const mockData = { items: [], total: 0, page: 1, pageSize: 10 } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { startDate: '2024-01-01', endDate: '2024-12-31', commodity: 'Steel' } + const result = await api.getInventoryList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/inbound', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== getInventoryDetail ========== + describe('getInventoryDetail', () => { + it('should call GET /api/inventory/inbound/detail/:id', async () => { + const mockData = { + id: '1', + inboundNumber: 'IB001', + warehouse: 'Warehouse 1', + commodity: 'Steel', + blNumber: 'BL001', + blWeight: 100, + inboundWeight: 95, + owner: 'Owner 1', + contractNumber: 'C001', + inboundDate: '2024-01-01', + operator: 'Operator 1', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getInventoryDetail('1') + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/inbound/1') + expect(result.success).toBe(true) + expect(result.data?.inboundNumber).toBe('IB001') + }) + }) + + // ========== createInventoryInbound ========== + describe('createInventoryInbound', () => { + it('should call POST /api/inventory/inbound/create with data', async () => { + const mockData = { + id: '1', + inboundNumber: 'IB001', + warehouse: 'Warehouse 1', + commodity: 'Steel', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const createData = { + warehouse: 'Warehouse 1', + commodity: 'Steel', + commodityCode: 'S001', + blNumber: 'BL001', + blWeight: 100, + inboundWeight: 95, + owner: 'Owner 1', + contractNumber: 'C001', + inboundDate: '2024-01-01', + operator: 'Operator 1' + } + const result = await api.createInventoryInbound(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/inbound', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateInventoryInbound ========== + describe('updateInventoryInbound', () => { + it('should call PUT /api/inventory/inbound/update/:id with data', async () => { + const mockData = { + id: '1', + inboundNumber: 'IB001', + inboundWeight: 90, + updateTime: '2024-01-02' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const updateData = { inboundWeight: 90, remark: 'Updated' } + const result = await api.updateInventoryInbound('1', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/inventory/inbound/1', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteInventoryInbound ========== + describe('deleteInventoryInbound', () => { + it('should call DELETE /api/inventory/inbound/delete/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteInventoryInbound('1') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/inventory/inbound/1') + expect(result.success).toBe(true) + }) + }) + + // ========== getWarehouseList ========== + describe('getWarehouseList', () => { + it('should call GET /api/inventory/warehouses without params', async () => { + const mockData = [ + { + id: '1', + code: 'WH001', + name: 'Warehouse 1', + address: 'Address 1', + contactPerson: 'Contact 1', + contactPhone: '123456', + capacity: 1000, + usedCapacity: 500, + status: 'active', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getWarehouseList() + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/warehouses', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toHaveLength(1) + }) + + it('should call GET /api/inventory/warehouses with params', async () => { + const mockData = [] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { name: 'Warehouse 1', code: 'WH001', status: 'active' } + const result = await api.getWarehouseList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/warehouses', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== createWarehouse ========== + describe('createWarehouse', () => { + it('should call POST /api/inventory/warehouses/create with data', async () => { + const mockData = { + id: '1', + code: 'WH001', + name: 'Warehouse 1', + address: 'Address 1', + contactPerson: 'Contact 1', + contactPhone: '123456', + capacity: 1000, + status: 'active', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const createData = { + code: 'WH001', + name: 'Warehouse 1', + address: 'Address 1', + contactPerson: 'Contact 1', + contactPhone: '123456', + capacity: 1000 + } + const result = await api.createWarehouse(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/warehouses', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateWarehouse ========== + describe('updateWarehouse', () => { + it('should call PUT /api/inventory/warehouses/update/:id with data', async () => { + const mockData = { + id: '1', + name: 'Updated Warehouse', + capacity: 1500, + updateTime: '2024-01-02' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const updateData = { name: 'Updated Warehouse', capacity: 1500 } + const result = await api.updateWarehouse('1', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/inventory/warehouses/1', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteWarehouse ========== + describe('deleteWarehouse', () => { + it('should call DELETE /api/inventory/warehouses/delete/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteWarehouse('1') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/inventory/warehouses/1') + expect(result.success).toBe(true) + }) + }) + + // ========== getInventorySummary ========== + describe('getInventorySummary', () => { + it('should call GET /api/inventory/summary without params', async () => { + const mockData = [ + { + id: '1', + warehouse: 'Warehouse 1', + commodity: 'Steel', + totalInbound: 1000, + totalOutbound: 500, + totalTransfer: 100, + currentStock: 400, + unit: 'kg', + updateTime: '2024-01-01' + } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getInventorySummary() + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/summary', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toHaveLength(1) + }) + + it('should call GET /api/inventory/summary with params', async () => { + const mockData = [] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { warehouse: 'Warehouse 1', commodity: 'Steel' } + const result = await api.getInventorySummary(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/summary', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== createCargoTransfer ========== + describe('createCargoTransfer', () => { + it('should call POST /api/inventory/transfer/create with data', async () => { + const mockData = { + id: '1', + transferNumber: 'CT001', + fromWarehouse: 'Warehouse 1', + toWarehouse: 'Warehouse 2', + commodity: 'Steel', + transferWeight: 100, + transferDate: '2024-01-01', + operator: 'Operator 1', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const createData = { + fromWarehouse: 'Warehouse 1', + toWarehouse: 'Warehouse 2', + commodity: 'Steel', + transferWeight: 100, + transferDate: '2024-01-01', + operator: 'Operator 1' + } + const result = await api.createCargoTransfer(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/transfer/create', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + // ========== getInventoryInboundTemplate ========== + describe('getInventoryInboundTemplate', () => { + it('should call GET /api/inventory/template/download', async () => { + const mockData = { fileName: 'inventory_template.xlsx', url: '/templates/inventory_template.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getInventoryInboundTemplate() + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/template/download') + expect(result.success).toBe(true) + expect(result.data?.fileName).toBe('inventory_template.xlsx') + }) + }) + + // ========== exportInventoryList ========== + describe('exportInventoryList', () => { + it('should call GET /api/inventory/export without params', async () => { + const mockData = { fileName: 'inventory_export.xlsx', url: '/exports/inventory_export.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.exportInventoryList() + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/export', { params: undefined }) + expect(result.success).toBe(true) + }) + + it('should call GET /api/inventory/export with params', async () => { + const mockData = { fileName: 'export.xlsx', url: '/exports/export.xlsx' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { warehouse: 'Warehouse 1', commodity: 'Steel' } + const result = await api.exportInventoryList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/export', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== uploadInventoryFiles ========== + describe('uploadInventoryFiles', () => { + it('should call POST /api/inventory/upload with FormData', async () => { + const mockData = { processedCount: 5, successCount: 4, failedCount: 1 } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const formData = new FormData() + formData.append('files', new Blob(['content']), 'inventory.xlsx') + + const result = await api.uploadInventoryFiles(formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + expect(result.data?.successCount).toBe(4) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/invoice.spec.ts b/frontend/src/api/__tests__/invoice.spec.ts new file mode 100644 index 0000000..813b0a1 --- /dev/null +++ b/frontend/src/api/__tests__/invoice.spec.ts @@ -0,0 +1,388 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../invoice') + +describe('Invoice API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== Upstream Invoices ========== + describe('getUpstreamInvoices', () => { + it('should call GET /zsp-finances/upstream-invoices', async () => { + const mockData = [ + { + id: 1, + invoiceNumber: 'INV001', + contractNumber: 'C001', + billOfLading: 'BL001', + supplierName: 'Supplier 1', + commodity: 'Steel', + amount: 1000, + taxAmount: 100, + totalAmount: 1100, + currency: 'USD', + invoiceDate: '2024-01-01', + status: 'pending', + filePath: '/files/inv001.pdf', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getUpstreamInvoices() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/upstream-invoices') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + describe('createUpstreamInvoice', () => { + it('should call POST /zsp-finances/upstream-invoices with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + invoiceNumber: 'INV001', + supplierName: 'Supplier 1', + amount: 1000, + totalAmount: 1100, + invoiceDate: '2024-01-01' + } + const result = await api.createUpstreamInvoice(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/upstream-invoices', { data: createData }) + expect(result.success).toBe(true) + }) + + it('should call POST /zsp-finances/upstream-invoices with optional fields', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + invoiceNumber: 'INV002', + contractNumber: 'C002', + billOfLading: 'BL002', + supplierName: 'Supplier 2', + commodity: 'Iron', + amount: 2000, + taxAmount: 200, + totalAmount: 2200, + currency: 'CNY', + invoiceDate: '2024-01-02', + remark: 'Test remark' + } + const result = await api.createUpstreamInvoice(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/upstream-invoices', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteUpstreamInvoice', () => { + it('should call DELETE /zsp-finances/upstream-invoices/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteUpstreamInvoice(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/upstream-invoices/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/upstream-invoices/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteUpstreamInvoice('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/upstream-invoices/123') + expect(result.success).toBe(true) + }) + }) + + describe('uploadUpstreamInvoiceFile', () => { + it('should call POST /zsp-finances/upstream-invoices/:id/upload with FormData', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'invoice.pdf') + + const result = await api.uploadUpstreamInvoiceFile(1, formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/upstream-invoices/1/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should accept string id for upload', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'invoice.pdf') + + const result = await api.uploadUpstreamInvoiceFile('123', formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/upstream-invoices/123/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== Downstream Invoices ========== + describe('getDownstreamInvoices', () => { + it('should call GET /zsp-finances/downstream-invoices', async () => { + const mockData = [ + { + id: 1, + invoiceNumber: 'INV001', + contractNumber: 'C001', + billOfLading: 'BL001', + customerName: 'Customer 1', + commodity: 'Steel', + amount: 1500, + taxAmount: 150, + totalAmount: 1650, + currency: 'USD', + invoiceDate: '2024-01-01', + status: 'pending', + filePath: '/files/inv001.pdf', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getDownstreamInvoices() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/downstream-invoices') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + describe('createDownstreamInvoice', () => { + it('should call POST /zsp-finances/downstream-invoices with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + invoiceNumber: 'INV001', + customerName: 'Customer 1', + amount: 1500, + totalAmount: 1650, + invoiceDate: '2024-01-01' + } + const result = await api.createDownstreamInvoice(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/downstream-invoices', { data: createData }) + expect(result.success).toBe(true) + }) + + it('should call POST /zsp-finances/downstream-invoices with optional fields', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + invoiceNumber: 'INV002', + contractNumber: 'C002', + billOfLading: 'BL002', + customerName: 'Customer 2', + commodity: 'Iron', + amount: 3000, + taxAmount: 300, + totalAmount: 3300, + currency: 'CNY', + invoiceDate: '2024-01-02', + remark: 'Test remark' + } + const result = await api.createDownstreamInvoice(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/downstream-invoices', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteDownstreamInvoice', () => { + it('should call DELETE /zsp-finances/downstream-invoices/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteDownstreamInvoice(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/downstream-invoices/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/downstream-invoices/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteDownstreamInvoice('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/downstream-invoices/123') + expect(result.success).toBe(true) + }) + }) + + describe('uploadDownstreamInvoiceFile', () => { + it('should call POST /zsp-finances/downstream-invoices/:id/upload with FormData', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'invoice.pdf') + + const result = await api.uploadDownstreamInvoiceFile(1, formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/downstream-invoices/1/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should accept string id for upload', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'invoice.pdf') + + const result = await api.uploadDownstreamInvoiceFile('123', formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/downstream-invoices/123/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== Freight Misc Invoices ========== + describe('getFreightMiscInvoices', () => { + it('should call GET /zsp-finances/freight-misc-invoices', async () => { + const mockData = [ + { + id: 1, + invoiceNumber: 'INV001', + contractNumber: 'C001', + billOfLading: 'BL001', + companyName: 'Freight Company', + expenseItem: 'Shipping', + amount: 500, + taxAmount: 50, + totalAmount: 550, + currency: 'USD', + invoiceDate: '2024-01-01', + status: 'pending', + filePath: '/files/inv001.pdf', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getFreightMiscInvoices() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/freight-misc-invoices') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + describe('createFreightMiscInvoice', () => { + it('should call POST /zsp-finances/freight-misc-invoices with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + invoiceNumber: 'INV001', + companyName: 'Freight Company', + amount: 500, + totalAmount: 550, + invoiceDate: '2024-01-01' + } + const result = await api.createFreightMiscInvoice(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/freight-misc-invoices', { data: createData }) + expect(result.success).toBe(true) + }) + + it('should call POST /zsp-finances/freight-misc-invoices with optional fields', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + invoiceNumber: 'INV002', + contractNumber: 'C002', + billOfLading: 'BL002', + companyName: 'Misc Company', + expenseItem: 'Storage', + amount: 300, + taxAmount: 30, + totalAmount: 330, + currency: 'CNY', + invoiceDate: '2024-01-02', + remark: 'Test remark' + } + const result = await api.createFreightMiscInvoice(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/freight-misc-invoices', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteFreightMiscInvoice', () => { + it('should call DELETE /zsp-finances/freight-misc-invoices/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteFreightMiscInvoice(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/freight-misc-invoices/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/freight-misc-invoices/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteFreightMiscInvoice('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/freight-misc-invoices/123') + expect(result.success).toBe(true) + }) + }) + + describe('uploadFreightMiscInvoiceFile', () => { + it('should call POST /zsp-finances/freight-misc-invoices/:id/upload with FormData', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'invoice.pdf') + + const result = await api.uploadFreightMiscInvoiceFile(1, formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/freight-misc-invoices/1/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should accept string id for upload', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'invoice.pdf') + + const result = await api.uploadFreightMiscInvoiceFile('123', formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/freight-misc-invoices/123/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/ownershipTransfer.spec.ts b/frontend/src/api/__tests__/ownershipTransfer.spec.ts new file mode 100644 index 0000000..d3f98e3 --- /dev/null +++ b/frontend/src/api/__tests__/ownershipTransfer.spec.ts @@ -0,0 +1,186 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../ownershipTransfer') + +describe('Ownership Transfer API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== createOwnershipTransfer ========== + describe('createOwnershipTransfer', () => { + it('should call POST /api/ownership-transfer/create with data (without files)', async () => { + const mockData = { + id: '1', + transferNumber: 'OT001', + transferDate: '2024-01-01', + commodity: 'Steel', + warehouse: 'Warehouse 1', + transferor: 'Owner 1', + transferee: 'Owner 2', + status: 'draft', + remark: 'Test remark', + blItems: [{ blNumber: 'BL001', quantity: 100 }], + fileList: [], + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const createData = { + transferDate: '2024-01-01', + commodity: 'Steel', + blItems: [{ blNumber: 'BL001', quantity: 100 }], + warehouse: 'Warehouse 1', + transferor: 'Owner 1', + transferee: 'Owner 2', + remarks: 'Test remark' + } + const result = await api.createOwnershipTransfer(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/ownership-transfer', { + data: { ...createData, blItems: createData.blItems, remark: 'Test remark' } + }) + expect(result.success).toBe(true) + expect(result.data?.transferNumber).toBe('OT001') + }) + + it('should call POST /api/ownership-transfer/create-with-files with FormData (with files)', async () => { + const mockData = { + id: '1', + transferNumber: 'OT001', + transferDate: '2024-01-01', + commodity: 'Steel', + warehouse: 'Warehouse 1', + status: 'draft', + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const file = new File(['content'], 'document.pdf', { type: 'application/pdf' }) + const createData = { + transferDate: '2024-01-01', + commodity: 'Steel', + blItems: [{ blNumber: 'BL001', quantity: 100 }], + warehouse: 'Warehouse 1', + transferor: 'Owner 1', + transferee: 'Owner 2', + remarks: 'Test remark', + files: [file] + } + const result = await api.createOwnershipTransfer(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/ownership-transfer/with-files', { + data: expect.any(FormData), + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should filter empty blItems', async () => { + const mockData = { + id: '1', + transferNumber: 'OT001', + blItems: [{ blNumber: 'BL001', quantity: 100 }], + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const createData = { + transferDate: '2024-01-01', + commodity: 'Steel', + blItems: [ + { blNumber: 'BL001', quantity: 100 }, + { blNumber: '', quantity: 0 } // Empty item should be filtered + ], + warehouse: 'Warehouse 1', + transferor: 'Owner 1', + transferee: 'Owner 2' + } + const result = await api.createOwnershipTransfer(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/ownership-transfer', { + data: expect.objectContaining({ + blItems: [{ blNumber: 'BL001', quantity: 100 }] + }) + }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteOwnershipTransfer ========== + describe('deleteOwnershipTransfer', () => { + it('should call DELETE /api/ownership-transfer/:id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteOwnershipTransfer('1') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/ownership-transfer/1') + expect(result.success).toBe(true) + }) + }) + + // ========== getOwnershipList ========== + describe('getOwnershipList', () => { + it('should call GET /api/ownership-transfer/list without params', async () => { + const mockData = { + items: [ + { + id: '1', + transferNumber: 'OT001', + transferDate: '2024-01-01', + commodity: 'Steel', + warehouse: 'Warehouse 1', + transferor: 'Owner 1', + transferee: 'Owner 2', + status: 'effective', + remark: 'Test', + blItems: [{ blNumber: 'BL001', quantity: 100 }], + fileList: [], + createdAt: '2024-01-01', + updatedAt: '2024-01-01' + } + ], + total: 1 + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getOwnershipList() + + expect(http.request).toHaveBeenCalledWith('get', '/api/ownership-transfer', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data?.items).toHaveLength(1) + }) + + it('should call GET /api/ownership-transfer/list with params', async () => { + const mockData = { items: [], total: 0 } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { transferNumber: 'OT001', blNumber: 'BL001', status: 'effective', page: 1, pageSize: 10 } + const result = await api.getOwnershipList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/ownership-transfer', { params }) + expect(result.success).toBe(true) + }) + + it('should call GET /api/ownership-transfer/list with search params', async () => { + const mockData = { items: [], total: 0 } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { status: 'draft', page: 2, pageSize: 20 } + const result = await api.getOwnershipList(params) + + expect(http.request).toHaveBeenCalledWith('get', '/api/ownership-transfer', { params }) + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/reconciliation.spec.ts b/frontend/src/api/__tests__/reconciliation.spec.ts new file mode 100644 index 0000000..4c9ba1b --- /dev/null +++ b/frontend/src/api/__tests__/reconciliation.spec.ts @@ -0,0 +1,235 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../reconciliation') + +describe('Reconciliation API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getReconciliations ========== + describe('getReconciliations', () => { + it('should call GET /zsp-finances/reconciliations without params', async () => { + const mockData = [ + { + id: 1, + reconciliationNumber: 'REC001', + contractNumber: 'C001', + companyName: 'Company 1', + periodStart: '2024-01-01', + periodEnd: '2024-01-31', + totalAmount: 5000, + paidAmount: 3000, + unpaidAmount: 2000, + currency: 'USD', + status: 'pending', + filePath: '/files/rec001.pdf', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getReconciliations() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/reconciliations', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/reconciliations with params', async () => { + const mockData = [] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { contractNumber: 'C001', companyName: 'Company 1', status: 'pending' } + const result = await api.getReconciliations(params) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/reconciliations', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== getReconciliation ========== + describe('getReconciliation', () => { + it('should call GET /zsp-finances/reconciliations/:id with numeric id', async () => { + const mockData = { + id: 1, + reconciliationNumber: 'REC001', + contractNumber: 'C001', + companyName: 'Company 1', + periodStart: '2024-01-01', + periodEnd: '2024-01-31', + totalAmount: 5000, + paidAmount: 3000, + unpaidAmount: 2000, + currency: 'USD', + status: 'pending', + filePath: '/files/rec001.pdf', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getReconciliation(1) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/reconciliations/1') + expect(result.success).toBe(true) + expect(result.data?.reconciliationNumber).toBe('REC001') + }) + + it('should call GET /zsp-finances/reconciliations/:id with string id', async () => { + const mockData = { + id: 123, + reconciliationNumber: 'REC123', + contractNumber: 'C123', + companyName: 'Company', + periodStart: '2024-01-01', + periodEnd: '2024-01-31', + totalAmount: 1000, + paidAmount: 500, + unpaidAmount: 500, + status: 'completed', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getReconciliation('123') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/reconciliations/123') + expect(result.success).toBe(true) + }) + }) + + // ========== createReconciliation ========== + describe('createReconciliation', () => { + it('should call POST /zsp-finances/reconciliations with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + reconciliationNumber: 'REC001', + companyName: 'Company 1', + periodStart: '2024-01-01', + periodEnd: '2024-01-31', + totalAmount: 5000 + } + const result = await api.createReconciliation(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/reconciliations', { data: createData }) + expect(result.success).toBe(true) + }) + + it('should call POST /zsp-finances/reconciliations with optional fields', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + reconciliationNumber: 'REC002', + contractNumber: 'C002', + companyName: 'Company 2', + periodStart: '2024-02-01', + periodEnd: '2024-02-28', + totalAmount: 6000, + paidAmount: 2000, + currency: 'CNY', + remark: 'Test remark' + } + const result = await api.createReconciliation(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/reconciliations', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateReconciliation ========== + describe('updateReconciliation', () => { + it('should call PUT /zsp-finances/reconciliations/:id with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const updateData = { + totalAmount: 5500, + paidAmount: 3500, + status: 'partial', + remark: 'Updated amounts' + } + const result = await api.updateReconciliation(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/reconciliations/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const updateData = { + companyName: 'Updated Company', + periodStart: '2024-03-01', + periodEnd: '2024-03-31' + } + const result = await api.updateReconciliation('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/reconciliations/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteReconciliation ========== + describe('deleteReconciliation', () => { + it('should call DELETE /zsp-finances/reconciliations/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteReconciliation(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/reconciliations/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/reconciliations/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteReconciliation('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/reconciliations/123') + expect(result.success).toBe(true) + }) + }) + + // ========== uploadReconciliationFile ========== + describe('uploadReconciliationFile', () => { + it('should call POST /zsp-finances/reconciliations/:id/upload with FormData', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'reconciliation.pdf') + + const result = await api.uploadReconciliationFile(1, formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/reconciliations/1/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should accept string id for upload', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'reconciliation.pdf') + + const result = await api.uploadReconciliationFile('123', formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/reconciliations/123/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/settlement.spec.ts b/frontend/src/api/__tests__/settlement.spec.ts new file mode 100644 index 0000000..7716110 --- /dev/null +++ b/frontend/src/api/__tests__/settlement.spec.ts @@ -0,0 +1,234 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../settlement') + +describe('Settlement API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getSettlements ========== + describe('getSettlements', () => { + it('should call GET /zsp-finances/settlements without params', async () => { + const mockData = [ + { + id: 1, + settlementNumber: 'SET001', + contractNumber: 'C001', + contractType: 'sales', + companyName: 'Company 1', + commodity: 'Steel', + quantity: 100, + unitPrice: 50, + amount: 5000, + currency: 'USD', + settlementDate: '2024-01-01', + status: 'pending', + filePath: '/files/set001.pdf', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getSettlements() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/settlements', { params: undefined }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/settlements with params', async () => { + const mockData = [] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const params = { contractNumber: 'C001', settlementNumber: 'SET001', status: 'pending' } + const result = await api.getSettlements(params) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/settlements', { params }) + expect(result.success).toBe(true) + }) + }) + + // ========== getSettlement ========== + describe('getSettlement', () => { + it('should call GET /zsp-finances/settlements/:id with numeric id', async () => { + const mockData = { + id: 1, + settlementNumber: 'SET001', + contractNumber: 'C001', + contractType: 'sales', + companyName: 'Company 1', + commodity: 'Steel', + quantity: 100, + unitPrice: 50, + amount: 5000, + currency: 'USD', + settlementDate: '2024-01-01', + status: 'pending', + filePath: '/files/set001.pdf', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getSettlement(1) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/settlements/1') + expect(result.success).toBe(true) + expect(result.data?.settlementNumber).toBe('SET001') + }) + + it('should call GET /zsp-finances/settlements/:id with string id', async () => { + const mockData = { + id: 123, + settlementNumber: 'SET123', + contractNumber: 'C123', + companyName: 'Company', + amount: 1000, + settlementDate: '2024-01-01', + status: 'completed', + createTime: '2024-01-01', + updateTime: '2024-01-01' + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getSettlement('123') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/settlements/123') + expect(result.success).toBe(true) + }) + }) + + // ========== createSettlement ========== + describe('createSettlement', () => { + it('should call POST /zsp-finances/settlements with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + settlementNumber: 'SET001', + contractNumber: 'C001', + companyName: 'Company 1', + amount: 5000, + settlementDate: '2024-01-01' + } + const result = await api.createSettlement(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/settlements', { data: createData }) + expect(result.success).toBe(true) + }) + + it('should call POST /zsp-finances/settlements with optional fields', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const createData = { + settlementNumber: 'SET002', + contractNumber: 'C002', + contractType: 'purchase', + companyName: 'Company 2', + commodity: 'Iron', + quantity: 200, + unitPrice: 30, + amount: 6000, + currency: 'CNY', + settlementDate: '2024-01-02', + remark: 'Test remark' + } + const result = await api.createSettlement(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/settlements', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateSettlement ========== + describe('updateSettlement', () => { + it('should call PUT /zsp-finances/settlements/:id with data', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const updateData = { + amount: 5500, + status: 'completed', + remark: 'Updated amount' + } + const result = await api.updateSettlement(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/settlements/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const updateData = { + companyName: 'Updated Company', + quantity: 150 + } + const result = await api.updateSettlement('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/settlements/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteSettlement ========== + describe('deleteSettlement', () => { + it('should call DELETE /zsp-finances/settlements/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteSettlement(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/settlements/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/settlements/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteSettlement('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/settlements/123') + expect(result.success).toBe(true) + }) + }) + + // ========== uploadSettlementFile ========== + describe('uploadSettlementFile', () => { + it('should call POST /zsp-finances/settlements/:id/upload with FormData', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'settlement.pdf') + + const result = await api.uploadSettlementFile(1, formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/settlements/1/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should accept string id for upload', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const formData = new FormData() + formData.append('file', new Blob(['content']), 'settlement.pdf') + + const result = await api.uploadSettlementFile('123', formData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/settlements/123/upload', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/user.spec.ts b/frontend/src/api/__tests__/user.spec.ts new file mode 100644 index 0000000..308f21c --- /dev/null +++ b/frontend/src/api/__tests__/user.spec.ts @@ -0,0 +1,126 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../user') + +describe('User API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getLogin ========== + describe('getLogin', () => { + it('should call POST /login with data', async () => { + const mockData = { + avatar: 'avatar.png', + username: 'testuser', + nickname: 'Test User', + roles: ['admin'], + permissions: ['read', 'write'], + accessToken: 'token123', + refreshToken: 'refresh123', + expires: new Date('2024-12-31') + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const loginData = { username: 'testuser', password: 'password123' } + const result = await api.getLogin(loginData) + + expect(http.request).toHaveBeenCalledWith('post', '/login', { data: loginData }) + expect(result.success).toBe(true) + expect(result.data?.username).toBe('testuser') + expect(result.data?.accessToken).toBe('token123') + }) + + it('should call POST /login without data', async () => { + const mockData = { + avatar: '', + username: '', + nickname: '', + roles: [], + permissions: [], + accessToken: '', + refreshToken: '', + expires: new Date() + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getLogin() + + expect(http.request).toHaveBeenCalledWith('post', '/login', { data: undefined }) + expect(result.success).toBe(true) + }) + + it('should return failure on login error', async () => { + vi.mocked(http.request).mockResolvedValue({ success: false, data: undefined }) + + const result = await api.getLogin({ username: 'wrong', password: 'wrong' }) + + expect(result.success).toBe(false) + }) + }) + + // ========== refreshTokenApi ========== + describe('refreshTokenApi', () => { + it('should call POST /refresh-token with data', async () => { + const mockData = { + accessToken: 'newToken123', + refreshToken: 'newRefresh123', + expires: new Date('2025-01-01') + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const refreshData = { refreshToken: 'oldRefresh123' } + const result = await api.refreshTokenApi(refreshData) + + expect(http.request).toHaveBeenCalledWith('post', '/refresh-token', { data: refreshData }) + expect(result.success).toBe(true) + expect(result.data?.accessToken).toBe('newToken123') + }) + + it('should call POST /refresh-token without data', async () => { + const mockData = { + accessToken: 'newToken', + refreshToken: 'newRefresh', + expires: new Date() + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.refreshTokenApi() + + expect(http.request).toHaveBeenCalledWith('post', '/refresh-token', { data: undefined }) + expect(result.success).toBe(true) + }) + }) + + // ========== registerApi ========== + describe('registerApi', () => { + it('should call POST /api/auth/register with data', async () => { + const mockResponse = { success: true, message: 'Registration successful' } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const registerData = { username: 'newuser', password: 'password123', email: 'test@example.com' } + const result = await api.registerApi(registerData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/auth/register', { data: registerData }) + expect(result.success).toBe(true) + expect(result.message).toBe('Registration successful') + }) + + it('should return failure message on registration error', async () => { + const mockResponse = { success: false, message: 'Username already exists' } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const result = await api.registerApi({ username: 'existinguser', password: 'pass' }) + + expect(result.success).toBe(false) + expect(result.message).toBe('Username already exists') + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/zspContract.spec.ts b/frontend/src/api/__tests__/zspContract.spec.ts new file mode 100644 index 0000000..0aac4c8 --- /dev/null +++ b/frontend/src/api/__tests__/zspContract.spec.ts @@ -0,0 +1,420 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../zspContract') + +describe('ZSP Contract API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== getZSPContractFolders ========== + describe('getZSPContractFolders', () => { + it('should call GET /api/zsp-contract/folders', async () => { + const mockData = [ + { id: 1, name: 'Folder 1', count: 5, createTime: '2024-01-01', description: 'Desc 1' }, + { id: 2, name: 'Folder 2', count: 3, createTime: '2024-01-02', description: 'Desc 2' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContractFolders() + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/folders') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + // ========== createZSPContractFolder ========== + describe('createZSPContractFolder', () => { + it('should call POST /api/zsp-contract/folders with name only', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createZSPContractFolder({ name: 'New Folder' }) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/folders', { + data: { name: 'New Folder' } + }) + expect(result.success).toBe(true) + }) + + it('should call POST /api/zsp-contract/folders with name and description', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createZSPContractFolder({ name: 'New Folder', description: 'Test description' }) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/folders', { + data: { name: 'New Folder', description: 'Test description' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateZSPContractFolder ========== + describe('updateZSPContractFolder', () => { + it('should call PUT /api/zsp-contract/folders/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateZSPContractFolder(1, { name: 'Updated Folder', description: 'New desc' }) + + expect(http.request).toHaveBeenCalledWith('put', '/api/zsp-contract/folders/1', { + data: { name: 'Updated Folder', description: 'New desc' } + }) + expect(result.success).toBe(true) + }) + + it('should call PUT /api/zsp-contract/folders/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateZSPContractFolder('folder-123', { name: 'Updated' }) + + expect(http.request).toHaveBeenCalledWith('put', '/api/zsp-contract/folders/folder-123', { + data: { name: 'Updated' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteZSPContractFolder ========== + describe('deleteZSPContractFolder', () => { + it('should call DELETE /api/zsp-contract/folders/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteZSPContractFolder(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/api/zsp-contract/folders/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /api/zsp-contract/folders/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteZSPContractFolder('folder-abc') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/zsp-contract/folders/folder-abc') + expect(result.success).toBe(true) + }) + }) + + // ========== getZSPContracts ========== + describe('getZSPContracts', () => { + it('should call GET /api/zsp-contract/contracts without folderId', async () => { + const mockData = [ + { id: 1, fileName: 'Contract1.pdf', fileType: 'pdf', fileSize: 1024, uploadTime: '2024-01-01', folderId: 1, contractNumber: 'C001', companyName: 'Company A', commodity: 'Steel', signDate: '2024-01-01', contractType: 'sales' }, + { id: 2, fileName: 'Contract2.pdf', fileType: 'pdf', fileSize: 2048, uploadTime: '2024-01-02', folderId: 1, contractNumber: 'C002', companyName: 'Company B', commodity: 'Iron', signDate: '2024-01-02', contractType: 'purchase' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContracts() + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts', { params: {} }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /api/zsp-contract/contracts with folderId', async () => { + const mockData = [ + { id: 1, fileName: 'Contract1.pdf', fileType: 'pdf', fileSize: 1024, uploadTime: '2024-01-01', folderId: 5, contractNumber: 'C001', companyName: 'Company A', commodity: 'Steel', signDate: '2024-01-01', contractType: 'sales' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContracts('5') + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts', { params: { folderId: '5' } }) + expect(result.success).toBe(true) + }) + }) + + // ========== getZSPContract ========== + describe('getZSPContract', () => { + it('should call GET /api/zsp-contract/contracts/:id with numeric id', async () => { + const mockData = { + id: 1, + fileName: 'Contract1.pdf', + fileType: 'pdf', + fileSize: 1024, + uploadTime: '2024-01-01', + folderId: 1, + contractNumber: 'C001', + companyName: 'Company A', + commodity: 'Steel', + signDate: '2024-01-01', + contractType: 'sales', + details: [] + } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContract(1) + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /api/zsp-contract/contracts/:id with string id', async () => { + const mockData = { id: 123, fileName: 'Contract.pdf', fileType: 'pdf', fileSize: 1024, uploadTime: '2024-01-01', folderId: 1, contractNumber: 'C123', companyName: 'Company', commodity: 'Steel', signDate: '2024-01-01', contractType: 'sales' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContract('123') + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/123') + expect(result.success).toBe(true) + }) + }) + + // ========== uploadZSPContract ========== + describe('uploadZSPContract', () => { + it('should call POST /api/zsp-contract/contracts with FormData and multipart headers', async () => { + const formData = new FormData() + formData.append('file', new Blob(['content']), 'contract.pdf') + formData.append('contractNumber', 'C001') + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Upload successful' }) + + const result = await api.uploadZSPContract(formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteZSPContract ========== + describe('deleteZSPContract', () => { + it('should call DELETE /api/zsp-contract/contracts/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteZSPContract(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/api/zsp-contract/contracts/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /api/zsp-contract/contracts/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteZSPContract('contract-abc') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/zsp-contract/contracts/contract-abc') + expect(result.success).toBe(true) + }) + }) + + // ========== updateZSPContract ========== + describe('updateZSPContract', () => { + it('should call PUT /api/zsp-contract/contracts/:id with data', async () => { + const updateData = { contractNumber: 'C002', companyName: 'Updated Company', commodity: 'Updated Steel' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateZSPContract(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/zsp-contract/contracts/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { folderId: 5 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateZSPContract('contract-123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/zsp-contract/contracts/contract-123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== getZSPContractDetails ========== + describe('getZSPContractDetails', () => { + it('should call GET /api/zsp-contract/contracts/:contractFileId/details with numeric id', async () => { + const mockData = [ + { id: 1, contractFileId: 1, commodityName: 'Steel', totalQuantity: 100, unit: 'kg', unitPrice: 50, deliveredQty: 20, pendingQty: 80, createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContractDetails(1) + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1/details') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /api/zsp-contract/contracts/:contractFileId/details with string id', async () => { + const mockData = [] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContractDetails('contract-123') + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/contract-123/details') + expect(result.success).toBe(true) + }) + }) + + // ========== getZSPContractDetail ========== + describe('getZSPContractDetail', () => { + it('should call GET /api/zsp-contract/details/:id with numeric id', async () => { + const mockData = { id: 1, contractFileId: 1, commodityName: 'Steel', totalQuantity: 100, unit: 'kg', unitPrice: 50, deliveredQty: 20, pendingQty: 80, createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContractDetail(1) + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/details/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /api/zsp-contract/details/:id with string id', async () => { + const mockData = { id: 123, contractFileId: 1, commodityName: 'Iron', totalQuantity: 50, unit: 'kg', unitPrice: 30, deliveredQty: 10, pendingQty: 40, createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getZSPContractDetail('123') + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/details/123') + expect(result.success).toBe(true) + }) + }) + + // ========== createZSPContractDetail ========== + describe('createZSPContractDetail', () => { + it('should call POST /api/zsp-contract/contracts/:contractFileId/details with data', async () => { + const createData = { contractFileId: 1, commodityName: 'Steel', totalQuantity: 100, unitPrice: 50 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createZSPContractDetail(1, createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts/1/details', { data: createData }) + expect(result.success).toBe(true) + }) + + it('should accept string contractFileId', async () => { + const createData = { contractFileId: 123, commodityName: 'Iron', totalQuantity: 50, unitPrice: 30 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createZSPContractDetail('123', createData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts/123/details', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + // ========== updateZSPContractDetail ========== + describe('updateZSPContractDetail', () => { + it('should call PUT /api/zsp-contract/details/:id with data', async () => { + const updateData = { id: 1, commodityName: 'Updated Steel', totalQuantity: 150 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateZSPContractDetail(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/zsp-contract/details/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { id: 123, unitPrice: 60 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateZSPContractDetail('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/api/zsp-contract/details/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + // ========== deleteZSPContractDetail ========== + describe('deleteZSPContractDetail', () => { + it('should call DELETE /api/zsp-contract/details/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteZSPContractDetail(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/api/zsp-contract/details/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /api/zsp-contract/details/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteZSPContractDetail('detail-abc') + + expect(http.request).toHaveBeenCalledWith('delete', '/api/zsp-contract/details/detail-abc') + expect(result.success).toBe(true) + }) + }) + + // ========== batchCreateZSPContractDetails ========== + describe('batchCreateZSPContractDetails', () => { + it('should call POST /api/zsp-contract/details/batch with data', async () => { + const batchData = { + contractFileId: 1, + details: [ + { contractFileId: 1, commodityName: 'Steel', totalQuantity: 100, unitPrice: 50 }, + { contractFileId: 1, commodityName: 'Iron', totalQuantity: 50, unitPrice: 30 } + ] + } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Batch created' }) + + const result = await api.batchCreateZSPContractDetails(batchData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/details/batch', { data: batchData }) + expect(result.success).toBe(true) + }) + }) + + // ========== importZSPContractDetailsFromExcel ========== + describe('importZSPContractDetailsFromExcel', () => { + it('should call POST /api/zsp-contract/contracts/:contractFileId/details/import with FormData', async () => { + const formData = new FormData() + formData.append('file', new Blob(['content']), 'import.xlsx') + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Import successful' }) + + const result = await api.importZSPContractDetailsFromExcel(1, formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts/1/details/import', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + + it('should accept string contractFileId for import', async () => { + const formData = new FormData() + formData.append('file', new Blob(['content']), 'import.xlsx') + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Import successful' }) + + const result = await api.importZSPContractDetailsFromExcel('123', formData) + + expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts/123/details/import', { + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) + expect(result.success).toBe(true) + }) + }) + + // ========== exportZSPContractDetailsToExcel ========== + describe('exportZSPContractDetailsToExcel', () => { + it('should call GET /api/zsp-contract/contracts/:contractFileId/details/export with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Export successful' }) + + const result = await api.exportZSPContractDetailsToExcel(1) + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1/details/export') + expect(result.success).toBe(true) + }) + + it('should call GET /api/zsp-contract/contracts/:contractFileId/details/export with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Export successful' }) + + const result = await api.exportZSPContractDetailsToExcel('123') + + expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/123/details/export') + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/__tests__/zspFinances.spec.ts b/frontend/src/api/__tests__/zspFinances.spec.ts new file mode 100644 index 0000000..6aaf713 --- /dev/null +++ b/frontend/src/api/__tests__/zspFinances.spec.ts @@ -0,0 +1,517 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/utils/http' + +vi.mock('@/utils/http', () => ({ + http: { + request: vi.fn() + } +})) + +const api = await import('../zspFinances') + +describe('ZSP Finances API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ========== Freight Charges ========== + describe('getFreightCharges', () => { + it('should call GET /zsp-finances/freight-charges', async () => { + const mockData = [ + { id: 1, billOfLading: 'BL001', contractNumber: 'C001', expenseItem: 'Shipping', amount: 1000, currency: 'USD', paymentDate: '2024-01-01', freightCompanyName: 'FreightCo', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getFreightCharges() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/freight-charges') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + describe('getFreightCharge', () => { + it('should call GET /zsp-finances/freight-charges/:id with numeric id', async () => { + const mockData = { id: 1, billOfLading: 'BL001', contractNumber: 'C001', expenseItem: 'Shipping', amount: 1000, currency: 'USD', paymentDate: '2024-01-01', freightCompanyName: 'FreightCo', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getFreightCharge(1) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/freight-charges/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/freight-charges/:id with string id', async () => { + const mockData = { id: 123, billOfLading: 'BL123', contractNumber: 'C123', expenseItem: 'Shipping', amount: 500, currency: 'USD', paymentDate: '2024-01-01', freightCompanyName: 'FreightCo', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getFreightCharge('123') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/freight-charges/123') + expect(result.success).toBe(true) + }) + }) + + describe('createFreightCharge', () => { + it('should call POST /zsp-finances/freight-charges with data', async () => { + const createData = { expenseItem: 'Shipping', amount: 1000, paymentDate: '2024-01-01', freightCompanyName: 'FreightCo' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createFreightCharge(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/freight-charges', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('updateFreightCharge', () => { + it('should call PUT /zsp-finances/freight-charges/:id with data', async () => { + const updateData = { amount: 1500, remark: 'Updated amount' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateFreightCharge(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/freight-charges/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { expenseItem: 'Updated Shipping' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateFreightCharge('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/freight-charges/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteFreightCharge', () => { + it('should call DELETE /zsp-finances/freight-charges/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteFreightCharge(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/freight-charges/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/freight-charges/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteFreightCharge('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/freight-charges/123') + expect(result.success).toBe(true) + }) + }) + + // ========== Misc Charges ========== + describe('getMiscCharges', () => { + it('should call GET /zsp-finances/misc-charges', async () => { + const mockData = [ + { id: 1, billOfLading: 'BL001', contractNumber: 'C001', expenseItem: 'Storage', amount: 500, currency: 'USD', paymentDate: '2024-01-01', companyName: 'StorageCo', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getMiscCharges() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/misc-charges') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + describe('getMiscCharge', () => { + it('should call GET /zsp-finances/misc-charges/:id with numeric id', async () => { + const mockData = { id: 1, billOfLading: 'BL001', contractNumber: 'C001', expenseItem: 'Storage', amount: 500, currency: 'USD', paymentDate: '2024-01-01', companyName: 'StorageCo', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getMiscCharge(1) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/misc-charges/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/misc-charges/:id with string id', async () => { + const mockData = { id: 123, billOfLading: 'BL123', contractNumber: 'C123', expenseItem: 'Storage', amount: 300, currency: 'USD', paymentDate: '2024-01-01', companyName: 'StorageCo', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getMiscCharge('123') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/misc-charges/123') + expect(result.success).toBe(true) + }) + }) + + describe('createMiscCharge', () => { + it('should call POST /zsp-finances/misc-charges with data', async () => { + const createData = { expenseItem: 'Storage', amount: 500, paymentDate: '2024-01-01', companyName: 'StorageCo' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createMiscCharge(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/misc-charges', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('updateMiscCharge', () => { + it('should call PUT /zsp-finances/misc-charges/:id with data', async () => { + const updateData = { amount: 600, remark: 'Updated amount' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateMiscCharge(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/misc-charges/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { expenseItem: 'Updated Storage' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateMiscCharge('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/misc-charges/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteMiscCharge', () => { + it('should call DELETE /zsp-finances/misc-charges/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteMiscCharge(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/misc-charges/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/misc-charges/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteMiscCharge('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/misc-charges/123') + expect(result.success).toBe(true) + }) + }) + + // ========== Service Fees ========== + describe('getServiceFees', () => { + it('should call GET /zsp-finances/service-fees', async () => { + const mockData = [ + { id: 1, billOfLading: 'BL001', contractNumber: 'C001', name: 'Consulting', amount: 200, currency: 'USD', date: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getServiceFees() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/service-fees') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + }) + + describe('getServiceFee', () => { + it('should call GET /zsp-finances/service-fees/:id with numeric id', async () => { + const mockData = { id: 1, billOfLading: 'BL001', contractNumber: 'C001', name: 'Consulting', amount: 200, currency: 'USD', date: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getServiceFee(1) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/service-fees/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/service-fees/:id with string id', async () => { + const mockData = { id: 123, billOfLading: 'BL123', contractNumber: 'C123', name: 'Consulting', amount: 150, currency: 'USD', date: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getServiceFee('123') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/service-fees/123') + expect(result.success).toBe(true) + }) + }) + + describe('createServiceFee', () => { + it('should call POST /zsp-finances/service-fees with data', async () => { + const createData = { name: 'Consulting', amount: 200, date: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createServiceFee(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/service-fees', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('updateServiceFee', () => { + it('should call PUT /zsp-finances/service-fees/:id with data', async () => { + const updateData = { amount: 250, remark: 'Updated amount' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateServiceFee(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/service-fees/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { name: 'Updated Consulting' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateServiceFee('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/service-fees/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteServiceFee', () => { + it('should call DELETE /zsp-finances/service-fees/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteServiceFee(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/service-fees/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/service-fees/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteServiceFee('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/service-fees/123') + expect(result.success).toBe(true) + }) + }) + + // ========== Cost Breakdowns ========== + describe('getCostBreakdowns', () => { + it('should call GET /zsp-finances/cost-breakdowns without businessId', async () => { + const mockData = [ + { id: 1, businessId: 'B001', businessType: 'contract', totalCost: 1500, freightChargeTotal: 1000, miscChargeTotal: 300, serviceFeeTotal: 200, createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getCostBreakdowns() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/cost-breakdowns', { params: {} }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/cost-breakdowns with businessId', async () => { + const mockData = [ + { id: 1, businessId: 'B001', businessType: 'contract', totalCost: 1500, freightChargeTotal: 1000, miscChargeTotal: 300, serviceFeeTotal: 200, createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getCostBreakdowns('B001') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/cost-breakdowns', { params: { businessId: 'B001' } }) + expect(result.success).toBe(true) + }) + }) + + describe('getCostBreakdown', () => { + it('should call GET /zsp-finances/cost-breakdowns/:id with numeric id', async () => { + const mockData = { id: 1, businessId: 'B001', businessType: 'contract', totalCost: 1500, freightChargeTotal: 1000, miscChargeTotal: 300, serviceFeeTotal: 200, createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getCostBreakdown(1) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/cost-breakdowns/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/cost-breakdowns/:id with string id', async () => { + const mockData = { id: 123, businessId: 'B123', businessType: 'contract', totalCost: 500, freightChargeTotal: 300, miscChargeTotal: 100, serviceFeeTotal: 100, createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getCostBreakdown('123') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/cost-breakdowns/123') + expect(result.success).toBe(true) + }) + }) + + describe('createCostBreakdown', () => { + it('should call POST /zsp-finances/cost-breakdowns with data', async () => { + const createData = { businessId: 'B001', businessType: 'contract' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createCostBreakdown(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/cost-breakdowns', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('updateCostBreakdown', () => { + it('should call PUT /zsp-finances/cost-breakdowns/:id with data', async () => { + const updateData = { freightChargeTotal: 1200, miscChargeTotal: 400 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateCostBreakdown(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/cost-breakdowns/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { serviceFeeTotal: 300 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateCostBreakdown('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/cost-breakdowns/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteCostBreakdown', () => { + it('should call DELETE /zsp-finances/cost-breakdowns/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteCostBreakdown(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/cost-breakdowns/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/cost-breakdowns/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteCostBreakdown('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/cost-breakdowns/123') + expect(result.success).toBe(true) + }) + }) + + // ========== Profit Records ========== + describe('getProfitRecords', () => { + it('should call GET /zsp-finances/profit-records without params', async () => { + const mockData = [ + { id: 1, businessId: 'B001', businessType: 'contract', revenue: 2000, totalCost: 1500, profit: 500, profitRate: 0.25, recordDate: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getProfitRecords() + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/profit-records', { params: {} }) + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/profit-records with businessId', async () => { + const mockData = [ + { id: 1, businessId: 'B001', businessType: 'contract', revenue: 2000, totalCost: 1500, profit: 500, profitRate: 0.25, recordDate: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getProfitRecords('B001') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/profit-records', { params: { businessId: 'B001' } }) + expect(result.success).toBe(true) + }) + + it('should call GET /zsp-finances/profit-records with businessId and businessType', async () => { + const mockData = [ + { id: 1, businessId: 'B001', businessType: 'delivery', revenue: 2000, totalCost: 1500, profit: 500, profitRate: 0.25, recordDate: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + ] + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getProfitRecords('B001', 'delivery') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/profit-records', { params: { businessId: 'B001', businessType: 'delivery' } }) + expect(result.success).toBe(true) + }) + }) + + describe('getProfitRecord', () => { + it('should call GET /zsp-finances/profit-records/:id with numeric id', async () => { + const mockData = { id: 1, businessId: 'B001', businessType: 'contract', revenue: 2000, totalCost: 1500, profit: 500, profitRate: 0.25, recordDate: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getProfitRecord(1) + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/profit-records/1') + expect(result.success).toBe(true) + expect(result.data).toEqual(mockData) + }) + + it('should call GET /zsp-finances/profit-records/:id with string id', async () => { + const mockData = { id: 123, businessId: 'B123', businessType: 'contract', revenue: 1000, totalCost: 800, profit: 200, profitRate: 0.2, recordDate: '2024-01-01', createTime: '2024-01-01', updateTime: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData }) + + const result = await api.getProfitRecord('123') + + expect(http.request).toHaveBeenCalledWith('get', '/zsp-finances/profit-records/123') + expect(result.success).toBe(true) + }) + }) + + describe('createProfitRecord', () => { + it('should call POST /zsp-finances/profit-records with data', async () => { + const createData = { businessId: 'B001', businessType: 'contract', revenue: 2000, totalCost: 1500, recordDate: '2024-01-01' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Created' }) + + const result = await api.createProfitRecord(createData) + + expect(http.request).toHaveBeenCalledWith('post', '/zsp-finances/profit-records', { data: createData }) + expect(result.success).toBe(true) + }) + }) + + describe('updateProfitRecord', () => { + it('should call PUT /zsp-finances/profit-records/:id with data', async () => { + const updateData = { revenue: 2500, totalCost: 1800, remark: 'Updated' } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateProfitRecord(1, updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/profit-records/1', { data: updateData }) + expect(result.success).toBe(true) + }) + + it('should accept string id for update', async () => { + const updateData = { revenue: 1200, totalCost: 900 } + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Updated' }) + + const result = await api.updateProfitRecord('123', updateData) + + expect(http.request).toHaveBeenCalledWith('put', '/zsp-finances/profit-records/123', { data: updateData }) + expect(result.success).toBe(true) + }) + }) + + describe('deleteProfitRecord', () => { + it('should call DELETE /zsp-finances/profit-records/:id with numeric id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteProfitRecord(1) + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/profit-records/1') + expect(result.success).toBe(true) + }) + + it('should call DELETE /zsp-finances/profit-records/:id with string id', async () => { + vi.mocked(http.request).mockResolvedValue({ success: true, message: 'Deleted' }) + + const result = await api.deleteProfitRecord('123') + + expect(http.request).toHaveBeenCalledWith('delete', '/zsp-finances/profit-records/123') + expect(result.success).toBe(true) + }) + }) +}) \ No newline at end of file diff --git a/frontend/src/api/applyDelivery.ts b/frontend/src/api/applyDelivery.ts new file mode 100755 index 0000000..6fba4d8 --- /dev/null +++ b/frontend/src/api/applyDelivery.ts @@ -0,0 +1,164 @@ +import { http } from "@/utils/http"; + +// ============ 类型定义 ============ +/** 提货委托项 */ +export type DeliveryItem = { + id: string; + orderNumber: string; // 提货委托单号 + blNumber: string; // 提单号1 + blNumber2: string; // 提单号2 + licensePlate: string; // 车牌号 + driverName: string; // 司机姓名 + driverPhone: string; // 司机电话 + driverIdCard: string; // 司机身份证号 + commodity: string; // 商品品类 + quantity: number; // 数量 + unit: string; // 单位 + deliveryDate: string; // 提货日期 + deliveryAddress: string; // 提货地址 + warehouse: string; // 仓库 + status: "pending" | "approved" | "in_progress" | "completed" | "cancelled"; // 状态 + createTime: string; // 创建时间 + updateTime: string; // 更新时间 + remark?: string; // 备注 +}; + +/** 提货委托表单数据 */ +export type DeliveryFormData = { + blNumber: string; // 提单号1 + blNumber2?: string; // 提单号2 + licensePlate: string; // 车牌号 + driverName: string; // 司机姓名 + driverPhone: string; // 司机电话 + driverIdCard: string; // 司机身份证号 + commodity: string; // 商品品类 + quantity: number; // 数量 + unit: string; // 单位 + deliveryDate: string; // 提货日期 + deliveryAddress: string; // 提货地址 + warehouse: string; // 仓库 + remark?: string; // 备注 +}; + +/** API基础响应 */ +export type BaseResponse = { + success: boolean; + message: string; + data?: T; +}; + +/** 提货委托列表响应 */ +export type DeliveryListResponse = BaseResponse<{ + items: DeliveryItem[]; + total: number; + page: number; + pageSize: number; +}>; + +/** 提货委托详情响应 */ +export type DeliveryDetailResponse = BaseResponse; + +/** 创建提货委托响应 */ +export type CreateDeliveryResponse = BaseResponse<{ + id: string; + orderNumber: string; +}>; + +/** 更新提货委托响应 */ +export type UpdateDeliveryResponse = BaseResponse; + +/** 取消提货委托响应 */ +export type CancelDeliveryResponse = BaseResponse; + +/** 下载模板响应 */ +export type DownloadTemplateResponse = BaseResponse<{ + fileName: string; + url: string; +}>; + +/** 导出列表响应 */ +export type ExportListResponse = BaseResponse<{ + fileName: string; + url: string; +}>; + +// ============ API函数 ============ +/** 获取提货委托列表 */ +export const getDeliveryList = (params?: { + page?: number; + pageSize?: number; + orderNumber?: string; + blNumber?: string; + blNumber2?: string; + licensePlate?: string; + driverName?: string; + status?: string; + startDate?: string; + endDate?: string; +}) => { + return http.request("get", "/api/apply-delivery", { + params + }); +}; + +/** 获取提货委托详情 */ +export const getDeliveryDetail = (id: string) => { + return http.request( + "get", + `/api/apply-delivery/${id}` + ); +}; + +/** 新增提货委托 */ +export const createDelivery = (data: DeliveryFormData) => { + return http.request( + "post", + "/api/apply-delivery", + { data } + ); +}; + +/** 更新提货委托 */ +export const updateDelivery = (id: string, data: Partial) => { + return http.request( + "put", + `/api/apply-delivery/${id}`, + { data } + ); +}; + +/** 取消提货委托 */ +export const cancelDelivery = (id: string) => { + return http.request( + "put", + `/api/apply-delivery/${id}/cancel` + ); +}; + +/** 下载提货单模板 */ +export const downloadDeliveryTemplate = () => { + return http.request( + "get", + "/api/apply-delivery/template/download" + ); +}; + +/** 删除提货委托 */ +export const deleteDelivery = (id: string) => { + return http.request("delete", `/api/apply-delivery/${id}`); +}; + +/** 导出提货委托列表 */ +export const exportDeliveryList = (params?: { + orderNumber?: string; + blNumber?: string; + licensePlate?: string; + driverName?: string; + status?: string; + startDate?: string; + endDate?: string; +}) => { + return http.request("get", "/api/apply-delivery/export", { + params + }); +}; diff --git a/frontend/src/api/base.ts b/frontend/src/api/base.ts new file mode 100755 index 0000000..37764c1 --- /dev/null +++ b/frontend/src/api/base.ts @@ -0,0 +1,5 @@ +export type BaseResponse = { + success: boolean; + message: string; + data?: T; +}; diff --git a/frontend/src/api/company.ts b/frontend/src/api/company.ts new file mode 100755 index 0000000..d8013ce --- /dev/null +++ b/frontend/src/api/company.ts @@ -0,0 +1,120 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; +// ============ 类型定义 ============ +/** 公司文件夹 */ +export type CompanyFolder = { + id: string; + name: string; + count: number; + createTime: string; + description?: string; + category?: string; +}; + +/** 公司文件 */ +export type CompanyFile = { + id: string; + fileName: string; + fileType: string; + fileSize: string; + uploadTime: string; + folderId: string; + companyName?: string; + fileCategory: string; + expireDate?: string; + description?: string; +}; + +/** API基础响应 */ + +/** 文件夹列表响应 */ +export type FoldersResponse = BaseResponse; + +/** 文件列表响应 */ +export type FilesResponse = BaseResponse; + +/** 上传文件响应 */ +export type UploadFileResponse = BaseResponse<{ + uploadedCount: number; +}>; + +/** 创建/更新文件夹请求 */ +export type FolderRequest = { + name: string; + description?: string; + category?: string; +}; + +// ============ API函数 ============ +/** 获取文件夹列表 */ +export const getCompanyFolders = () => { + return http.request("get", "/api/company/folders"); +}; + +/** 获取文件列表 */ +export const getCompanyFiles = (params?: { folderId?: string }) => { + return http.request("get", "/api/company/files", { params }); +}; + +/** 创建文件夹 */ +export const createCompanyFolder = (data: FolderRequest) => { + return http.request>("post", "/api/company/folders", { + data + }); +}; + +/** 更新文件夹 */ +export const updateCompanyFolder = ( + id: string, + data: Partial +) => { + return http.request>( + "put", + `/api/company/folders/${id}`, + { data } + ); +}; + +/** 删除文件夹 */ +export const deleteCompanyFolder = (id: string) => { + return http.request("delete", `/api/company/folders/${id}`); +}; + +/** 上传公司文件 */ +export const uploadCompanyFile = (data: FormData) => { + return http.request("post", "/api/company/files/upload", { + data, + headers: { + "Content-Type": "multipart/form-data" + } + }); +}; + +/** 更新公司文件元信息 */ +export const updateCompanyFile = ( + id: string, + data: { + companyName: string; + fileCategory: string; + expireDate?: string; + description?: string; + folderId?: number | null; + } +) => { + return http.request("put", `/api/company/files/${id}`, { + data + }); +}; + +/** 删除公司文件 */ +export const deleteCompanyFile = (id: string) => { + return http.request("delete", `/api/company/files/${id}`); +}; + +/** 下载公司文件(文件 URL 为 /api/company-files/{fileUrl}) */ +export const downloadCompanyFile = (id: string) => { + return http.request>( + "get", + `/api/company/files/${id}/download` + ); +}; diff --git a/frontend/src/api/contract.ts b/frontend/src/api/contract.ts new file mode 100755 index 0000000..895442a --- /dev/null +++ b/frontend/src/api/contract.ts @@ -0,0 +1,164 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; + +/** 业务合同(含附件列表) */ +export interface BusinessContract { + id: number; + name: string; + contractType: string; + contractCode: string; + buyParty: string; + sellParty: string; + commodity: string; + count: number; + unitPrice: number; + blNumber: string; + deliveryTime: string; + pickUpTime: string; + packaging: string; + remarks: string; + createTime: string; + updateTime: string; + files?: { id: number; name: string; url: string }[]; +} + +/** 提交合同后后端返回的结果 */ +export type ContractInitResult = { + success: boolean; + message: string; +}; + +/** 获取业务合同列表 */ +export const getContractList = (params?: { contractType?: string }) => { + return http.request>("get", "/api/contract", { + params + }); +}; + +/** 获取单个业务合同详情 */ +export const getContract = (id: number | string) => { + return http.request>("get", `/api/contract/${id}`); +}; + +/** 提交合同(POST) */ +export const submitContract = (data: FormData) => { + return http.request("post", "/api/contract/upload", { + data, + headers: { "Content-Type": "multipart/form-data" } + }); +}; + +export type BatchUploadResult = { + success: boolean; + message: string; + data?: { + successCount: number; + failedItems: Array<{ name: string; reason: string }>; + }; +}; + +/** 批量上传合同(POST) */ +export const batchUploadContracts = (data: FormData) => { + return http.request("post", "/api/contract/batch-upload", { + data, + headers: { "Content-Type": "multipart/form-data" } + }); +}; + +/** 更新业务合同(含表单字段和合同扫描件增删) */ +export const updateContractWithFiles = (id: number | string, data: FormData) => { + return http.request("post", `/api/contract/${id}/update-files`, { + data, + headers: { "Content-Type": "multipart/form-data" } + }); +}; + +/** 更新业务合同(仅更新表单字段,不包含附件) */ +export const updateContract = ( + id: number | string, + data: Partial> +) => { + return http.request("put", `/api/contract/${id}`, { data }); +}; + +/** 删除业务合同 */ +export const deleteContract = (id: number | string) => { + return http.request("delete", `/api/contract/${id}`); +}; + +// ========== 批量上传归档(与业务合同共用体系) ========== + +/** 批量归档文件夹 */ +export interface ContractFolderItem { + id: number; + name: string; + description: string; + createTime: string; + updateTime: string; +} + +/** 批量上传中的单个文件 */ +export interface ContractBatchFileItem { + id: number; + batchId: number; + name: string; + url: string; + fileSize: number; +} + +/** 一次批量上传记录 */ +export interface ContractBatchItem { + id: number; + folderId: number; + batchName: string; + remark: string; + createTime: string; + updateTime: string; + files?: ContractBatchFileItem[]; +} + +export const getContractFolders = () => { + return http.request>("get", "/api/contract/folders"); +}; + +export const createContractFolder = (data: { + name: string; + description?: string; +}) => { + return http.request>("post", "/api/contract/folders", { + data + }); +}; + +export const updateContractFolder = ( + id: number | string, + data: { name: string; description?: string } +) => { + return http.request("put", `/api/contract/folders/${id}`, { data }); +}; + +export const deleteContractFolder = (id: number | string) => { + return http.request("delete", `/api/contract/folders/${id}`); +}; + +export const getContractBatches = (params?: { folderId?: number }) => { + return http.request>("get", "/api/contract/batches", { + params + }); +}; + +export const getContractBatch = (id: number | string) => { + return http.request>("get", `/api/contract/batches/${id}`); +}; + +/** 批量上传:提交字段 folderId, batchName, remark, files[] */ +export const createContractBatch = (formData: FormData) => { + return http.request>("post", "/api/contract/batches", { + data: formData, + headers: { "Content-Type": "multipart/form-data" } + }); +}; + +export const deleteContractBatch = (id: number | string) => { + return http.request("delete", `/api/contract/batches/${id}`); +}; diff --git a/frontend/src/api/deliveryDetails.ts b/frontend/src/api/deliveryDetails.ts new file mode 100755 index 0000000..9169c0f --- /dev/null +++ b/frontend/src/api/deliveryDetails.ts @@ -0,0 +1,229 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; +// ============ 类型定义 ============ +/** 提货明细项 */ +export type DeliveryDetail = { + id: string; + outboundNumber: string; // 出库单号 + deliveryOrderNumber: string; // 提货委托单号 + blNumber: string; // 提单号 + licensePlate: string; // 车牌号 + driverName: string; // 司机姓名 + driverPhone: string; // 司机电话 + commodity: string; // 商品品类 + commodityCode: string; // 商品编码 + batchNumber: string; // 批次号 + quantity: number; // 出库数量 + unit: string; // 单位 + unitPrice: number; // 单价 + totalAmount: number; // 总金额 + outboundDate: string; // 出库日期 + outboundTime: string; // 出库时间 + warehouse: string; // 仓库 + location: string; // 库位 + operator: string; // 操作员 + outboundStatus: "pending" | "in_progress" | "completed" | "cancelled"; // 出库状态 + receiptStatus: "pending" | "partial" | "completed" | "discrepancy"; // 收货状态 + receivedQuantity: number; // 已收货数量 + receiptDate: string; // 收货日期 + receiver: string; // 收货人 + remark?: string; // 备注 + createTime: string; // 创建时间 + updateTime: string; // 更新时间 +}; + +/** 出库单表单数据 */ +export type OutboundOrderFormData = { + deliveryOrderNumber: string; // 提货委托单号 + blNumber: string; // 提单号 + licensePlate: string; // 车牌号 + driverName: string; // 司机姓名 + driverPhone: string; // 司机电话 + items: OutboundItem[]; // 出库明细项 + outboundDate: string; // 出库日期 + warehouse: string; // 仓库 + operator: string; // 操作员 + remark?: string; // 备注 +}; + +/** 出库明细项 */ +export type OutboundItem = { + commodity: string; // 商品品类 + commodityCode?: string; // 商品编码 + batchNumber?: string; // 批次号 + quantity: number; // 数量 + unit: string; // 单位 + unitPrice: number; // 单价 + location?: string; // 库位 + remark?: string; // 备注 +}; + +/** 收货确认表单数据 */ +export type ReceiptConfirmFormData = { + receiptDate: string; // 收货日期 + receivedQuantity: number; // 实收数量 + receiver: string; // 收货人 + discrepancyReason?: string; // 差异原因 + remark?: string; // 备注 +}; + +/** 收货历史记录 */ +export type ReceiptHistory = { + id: string; + deliveryDetailId: string; + receiptDate: string; + receivedQuantity: number; + receiver: string; + remark?: string; + createTime: string; +}; + +/** API基础响应 */ + +/** 提货明细列表响应 */ +export type DeliveryDetailsResponse = BaseResponse<{ + items: DeliveryDetail[]; + total: number; + page: number; + pageSize: number; +}>; + +/** 提货明细详情响应 */ +export type DeliveryDetailResponse = BaseResponse; + +/** 创建出库单响应 */ +export type CreateOutboundOrderResponse = BaseResponse<{ + items: DeliveryDetail[]; + count: number; +}>; + +/** 更新出库单响应 */ +export type UpdateOutboundOrderResponse = BaseResponse; + +/** 收货确认响应 */ +export type ConfirmReceiptResponse = BaseResponse<{ + deliveryDetail: DeliveryDetail; + receiptRecord: ReceiptHistory; +}>; + +/** 收货历史响应 */ +export type ReceiptHistoryResponse = BaseResponse; + +/** 下载模板响应 */ +export type DownloadTemplateResponse = BaseResponse<{ + fileName: string; + url: string; +}>; + +/** 导出数据响应 */ +export type ExportDataResponse = BaseResponse<{ + fileName: string; + url: string; +}>; + +/** 打印提货单响应 */ +export type PrintDeliveryOrderResponse = BaseResponse<{ + fileName: string; + url: string; +}>; + +// ============ API函数 ============ +/** 获取提货明细列表 */ +export const getDeliveryDetails = (params?: { + page?: number; + pageSize?: number; + outboundNumber?: string; + deliveryOrderNumber?: string; + blNumber?: string; + licensePlate?: string; + commodity?: string; + outboundStatus?: string; + receiptStatus?: string; + startDate?: string; + endDate?: string; +}) => { + return http.request( + "get", + "/api/delivery-details", + { params } + ); +}; + +/** 获取提货明细详情 */ +export const getDeliveryDetail = (id: string) => { + return http.request( + "get", + `/api/delivery-details/${id}` + ); +}; + +/** 新增出库单 */ +export const createOutboundOrder = (data: OutboundOrderFormData) => { + return http.request( + "post", + "/api/delivery-details", + { data } + ); +}; + +/** 更新出库单 */ +export const updateOutboundOrder = ( + id: string, + data: Partial +) => { + return http.request( + "put", + `/api/delivery-details/${id}`, + { data } + ); +}; + +/** 收货确认 */ +export const confirmReceipt = (id: string, data: ReceiptConfirmFormData) => { + return http.request( + "put", + `/api/delivery-details/confirm-receipt/${id}`, + { data } + ); +}; + +/** 获取收货历史记录 */ +export const getReceiptHistory = (id: string) => { + return http.request( + "get", + `/api/delivery-details/receipt-history/${id}` + ); +}; + +/** 下载出库单模板 */ +export const getOutboundOrderTemplate = () => { + return http.request( + "get", + "/api/delivery-details/template/download" + ); +}; + +/** 导出提货明细 */ +export const exportDeliveryDetails = (params?: { + outboundNumber?: string; + deliveryOrderNumber?: string; + blNumber?: string; + licensePlate?: string; + commodity?: string; + outboundStatus?: string; + receiptStatus?: string; + startDate?: string; + endDate?: string; +}) => { + return http.request("get", "/api/delivery-details/export", { + params + }); +}; + +/** 打印提货单 */ +export const printDeliveryOrder = (id: string) => { + return http.request( + "get", + `/api/delivery-details/print/${id}` + ); +}; diff --git a/frontend/src/api/inventory.ts b/frontend/src/api/inventory.ts new file mode 100755 index 0000000..ee0454b --- /dev/null +++ b/frontend/src/api/inventory.ts @@ -0,0 +1,303 @@ +import { http } from "@/utils/http"; + +// ============ 类型定义 ============ +/** 入库单 */ +export type InventoryInbound = { + id: string; + inboundNumber: string; // 入库单号 + warehouse: string; // 仓库 + commodity: string; // 商品 + commodityCode?: string; // 商品编码 + blNumber: string; // 提单号 + blWeight: number; // 提单重 + inboundWeight: number; // 入库重 + owner: string; // 货主 + contractNumber: string; // 对应上游合同编号 + warehouseAddress?: string; // 仓库地址 + packaging?: string; // 包装方式 + inboundDate: string; // 入库日期 + operator: string; // 操作员 + remark?: string; // 备注 + createTime: string; // 创建时间 + updateTime: string; // 更新时间 +}; + +/** 仓库信息 */ +export type Warehouse = { + id: string; + code: string; // 仓库编码 + name: string; // 仓库名称 + address: string; // 仓库地址 + contactPerson: string; // 联系人 + contactPhone: string; // 联系电话 + capacity: number; // 容量(吨) + usedCapacity: number; // 已使用容量 + status: "active" | "inactive" | "maintenance"; // 状态 + description?: string; // 描述 + createTime: string; // 创建时间 + updateTime: string; // 更新时间 +}; + +/** 库存明细 */ +export type InventoryDetail = { + id: string; + warehouse: string; // 仓库 + commodity: string; // 商品 + totalInbound: number; // 总入库量 + totalOutbound: number; // 总出库量 + totalTransfer: number; // 总货权转移量 + currentStock: number; // 当前库存 + unit: string; // 单位 + lastInboundDate?: string; // 最后入库日期 + lastOutboundDate?: string; // 最后出库日期 + updateTime: string; // 更新时间 +}; + +/** 货权转移 */ +export type CargoTransfer = { + id: string; + transferNumber: string; // 货权转移单号 + fromWarehouse: string; // 转出仓库 + toWarehouse: string; // 转入仓库 + commodity: string; // 商品 + transferWeight: number; // 转移重量 + transferDate: string; // 转移日期 + operator: string; // 操作员 + remark?: string; // 备注 + createTime: string; // 创建时间 + updateTime: string; // 更新时间 +}; + +/** 入库单表单数据 */ +export type InboundFormData = { + warehouse: string; // 仓库 + commodity: string; // 商品 + commodityCode?: string; // 商品编码 + blNumber: string; // 提单号 + blWeight: number; // 提单重 + inboundWeight: number; // 入库重 + owner: string; // 货主 + contractNumber: string; // 对应上游合同编号 + warehouseAddress?: string; // 仓库地址 + packaging?: string; // 包装方式 + inboundDate: string; // 入库日期 + operator: string; // 操作员 + remark?: string; // 备注 +}; + +/** 仓库表单数据 */ +export type WarehouseFormData = { + code: string; // 仓库编码 + name: string; // 仓库名称 + address: string; // 仓库地址 + contactPerson: string; // 联系人 + contactPhone: string; // 联系电话 + capacity: number; // 容量 + description?: string; // 描述 +}; + +/** 货权转移表单数据 */ +export type CargoTransferFormData = { + fromWarehouse: string; // 转出仓库 + toWarehouse: string; // 转入仓库 + commodity: string; // 商品 + transferWeight: number; // 转移重量 + transferDate: string; // 转移日期 + operator: string; // 操作员 + remark?: string; // 备注 +}; + +/** API基础响应 */ +export type BaseResponse = { + success: boolean; + message: string; + data?: T; +}; + +/** 入库单列表响应 */ +export type InventoryListResponse = BaseResponse<{ + items: InventoryInbound[]; + total: number; + page: number; + pageSize: number; +}>; + +/** 入库单详情响应 */ +export type InventoryDetailResponse = BaseResponse; + +/** 创建入库单响应 */ +export type CreateInventoryInboundResponse = BaseResponse; + +/** 仓库列表响应 */ +export type WarehouseListResponse = BaseResponse; + +/** 创建/更新仓库响应 */ +export type WarehouseResponse = BaseResponse; + +/** 库存明细响应 */ +export type InventorySummaryResponse = BaseResponse; + +/** 创建货权转移响应 */ +export type CargoTransferResponse = BaseResponse; + +/** 下载模板响应 */ +export type DownloadTemplateResponse = BaseResponse<{ + fileName: string; + url: string; +}>; + +/** 导出数据响应 */ +export type ExportDataResponse = BaseResponse<{ + fileName: string; + url: string; +}>; + +/** 批量上传响应 */ +export type UploadFilesResponse = BaseResponse<{ + processedCount: number; + successCount: number; + failedCount: number; +}>; + +// ============ API函数 ============ +/** 获取入库单列表 */ +export const getInventoryList = (params?: { + page?: number; + pageSize?: number; + inboundNumber?: string; + warehouse?: string; + commodity?: string; + blNumber?: string; + owner?: string; + contractNumber?: string; + startDate?: string; + endDate?: string; +}) => { + return http.request("get", "/api/inventory/inbound", { + params + }); +}; + +/** 获取入库单详情 */ +export const getInventoryDetail = (id: string) => { + return http.request( + "get", + `/api/inventory/inbound/${id}` + ); +}; + +/** 新增入库单 */ +export const createInventoryInbound = (data: InboundFormData) => { + return http.request( + "post", + "/api/inventory/inbound", + { data } + ); +}; + +/** 更新入库单 */ +export const updateInventoryInbound = ( + id: string, + data: Partial +) => { + return http.request( + "put", + `/api/inventory/inbound/${id}`, + { data } + ); +}; + +/** 删除入库单 */ +export const deleteInventoryInbound = (id: string) => { + return http.request( + "delete", + `/api/inventory/inbound/${id}` + ); +}; + +/** 获取仓库列表 */ +export const getWarehouseList = (params?: { + name?: string; + code?: string; + status?: string; +}) => { + return http.request("get", "/api/inventory/warehouses", { + params + }); +}; + +/** 创建仓库 */ +export const createWarehouse = (data: WarehouseFormData) => { + return http.request( + "post", + "/api/inventory/warehouses", + { data } + ); +}; + +/** 更新仓库 */ +export const updateWarehouse = ( + id: string, + data: Partial +) => { + return http.request( + "put", + `/api/inventory/warehouses/${id}`, + { data } + ); +}; + +/** 删除仓库 */ +export const deleteWarehouse = (id: string) => { + return http.request( + "delete", + `/api/inventory/warehouses/${id}` + ); +}; + +/** 获取库存明细 */ +export const getInventorySummary = (params?: { + warehouse?: string; + commodity?: string; +}) => { + return http.request("get", "/api/inventory/summary", { + params + }); +}; + +/** 创建货权转移 */ +export const createCargoTransfer = (data: CargoTransferFormData) => { + return http.request( + "post", + "/api/inventory/transfer/create", + { data } + ); +}; + +/** 下载入库单模板 */ +export const getInventoryInboundTemplate = () => { + return http.request( + "get", + "/api/inventory/template/download" + ); +}; + +/** 导出库存数据 */ +export const exportInventoryList = (params?: { + warehouse?: string; + commodity?: string; +}) => { + return http.request("get", "/api/inventory/export", { + params + }); +}; + +/** 批量上传文件 */ +export const uploadInventoryFiles = (data: FormData) => { + return http.request("post", "/api/inventory/upload", { + data, + headers: { + "Content-Type": "multipart/form-data" + } + }); +}; diff --git a/frontend/src/api/invoice.ts b/frontend/src/api/invoice.ts new file mode 100755 index 0000000..3e586fc --- /dev/null +++ b/frontend/src/api/invoice.ts @@ -0,0 +1,212 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; + +// ==================== 上游发票 ==================== + +export type ZSPUpstreamInvoice = { + id: number; + invoiceNumber: string; + contractNumber: string; + billOfLading: string; + supplierName: string; + commodity: string; + amount: number; + taxAmount: number; + totalAmount: number; + currency: string; + invoiceDate: string; + status: string; + filePath: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export interface ZSPUpstreamInvoiceCreateRequest { + invoiceNumber: string; + contractNumber?: string; + billOfLading?: string; + supplierName: string; + commodity?: string; + amount: number; + taxAmount?: number; + totalAmount: number; + currency?: string; + invoiceDate: string; + remark?: string; +} + +export const getUpstreamInvoices = () => { + return http.request>( + "get", + "/zsp-finances/upstream-invoices" + ); +}; + +export const createUpstreamInvoice = ( + data: ZSPUpstreamInvoiceCreateRequest +) => { + return http.request( + "post", + "/zsp-finances/upstream-invoices", + { data } + ); +}; + +export const deleteUpstreamInvoice = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/upstream-invoices/${id}` + ); +}; + +export const uploadUpstreamInvoiceFile = ( + id: string | number, + data: FormData +) => { + return http.request( + "post", + `/zsp-finances/upstream-invoices/${id}/upload`, + { data, headers: { "Content-Type": "multipart/form-data" } } + ); +}; + +// ==================== 下游发票 ==================== + +export type ZSPDownstreamInvoice = { + id: number; + invoiceNumber: string; + contractNumber: string; + billOfLading: string; + customerName: string; + commodity: string; + amount: number; + taxAmount: number; + totalAmount: number; + currency: string; + invoiceDate: string; + status: string; + filePath: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export interface ZSPDownstreamInvoiceCreateRequest { + invoiceNumber: string; + contractNumber?: string; + billOfLading?: string; + customerName: string; + commodity?: string; + amount: number; + taxAmount?: number; + totalAmount: number; + currency?: string; + invoiceDate: string; + remark?: string; +} + +export const getDownstreamInvoices = () => { + return http.request>( + "get", + "/zsp-finances/downstream-invoices" + ); +}; + +export const createDownstreamInvoice = ( + data: ZSPDownstreamInvoiceCreateRequest +) => { + return http.request( + "post", + "/zsp-finances/downstream-invoices", + { data } + ); +}; + +export const deleteDownstreamInvoice = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/downstream-invoices/${id}` + ); +}; + +export const uploadDownstreamInvoiceFile = ( + id: string | number, + data: FormData +) => { + return http.request( + "post", + `/zsp-finances/downstream-invoices/${id}/upload`, + { data, headers: { "Content-Type": "multipart/form-data" } } + ); +}; + +// ==================== 货代杂费发票 ==================== + +export type ZSPFreightMiscInvoice = { + id: number; + invoiceNumber: string; + contractNumber: string; + billOfLading: string; + companyName: string; + expenseItem: string; + amount: number; + taxAmount: number; + totalAmount: number; + currency: string; + invoiceDate: string; + status: string; + filePath: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export interface ZSPFreightMiscInvoiceCreateRequest { + invoiceNumber: string; + contractNumber?: string; + billOfLading?: string; + companyName: string; + expenseItem?: string; + amount: number; + taxAmount?: number; + totalAmount: number; + currency?: string; + invoiceDate: string; + remark?: string; +} + +export const getFreightMiscInvoices = () => { + return http.request>( + "get", + "/zsp-finances/freight-misc-invoices" + ); +}; + +export const createFreightMiscInvoice = ( + data: ZSPFreightMiscInvoiceCreateRequest +) => { + return http.request( + "post", + "/zsp-finances/freight-misc-invoices", + { data } + ); +}; + +export const deleteFreightMiscInvoice = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/freight-misc-invoices/${id}` + ); +}; + +export const uploadFreightMiscInvoiceFile = ( + id: string | number, + data: FormData +) => { + return http.request( + "post", + `/zsp-finances/freight-misc-invoices/${id}/upload`, + { data, headers: { "Content-Type": "multipart/form-data" } } + ); +}; diff --git a/frontend/src/api/ownershipTransfer.ts b/frontend/src/api/ownershipTransfer.ts new file mode 100755 index 0000000..0e286d1 --- /dev/null +++ b/frontend/src/api/ownershipTransfer.ts @@ -0,0 +1,100 @@ +import { http } from "@/utils/http"; + +// 货权转移类型定义 +export type OwnershipTransfer = { + id: string; + transferNumber: string; // 转移函编号 + transferDate: string; // 货转日期 + commodity: string; // 商品 + warehouse: string; // 库方 + transferor: string; // 货权出让方 + transferee: string; // 货权接收方 + status: "draft" | "effective" | "void"; // 状态 + remark: string; // 备注 + blItems: Array<{ + // 提单明细(动态列表) + id?: number; + transferId?: number; + blNumber: string; // 提单号 + quantity: number; // 数量 + }>; + fileList: Array<{ + name: string; + url: string; + }>; + createdAt: string; + updatedAt: string; +}; + +// API响应基础类型 +export type BaseResponse = { + success: boolean; + message: string; + data?: T; +}; + +// 货权转移列表响应类型 +export type OwnershipListResponse = BaseResponse<{ + items: OwnershipTransfer[]; + total: number; +}>; + +/** 创建货权转移 */ +export const createOwnershipTransfer = (data: { + transferDate: string; + commodity: string; + blItems: Array<{ blNumber: string; quantity: number }>; + warehouse: string; + transferor: string; + transferee: string; + remarks?: string; + files?: File[]; +}) => { + // 过滤空提单号 + const validItems = data.blItems.filter(item => item.blNumber || item.quantity); + + if (data.files?.length) { + const formData = new FormData(); + formData.append("transferDate", data.transferDate); + formData.append("commodity", data.commodity); + formData.append("blItems", JSON.stringify(validItems)); + formData.append("warehouse", data.warehouse); + formData.append("transferor", data.transferor); + formData.append("transferee", data.transferee); + if (data.remarks) formData.append("remark", data.remarks); + data.files.forEach(file => formData.append("files", file)); + return http.request>( + "post", + "/api/ownership-transfer/with-files", + { + data: formData, + headers: { "Content-Type": "multipart/form-data" } + } + ); + } + return http.request>( + "post", + "/api/ownership-transfer", + { data: { ...data, blItems: validItems, remark: data.remarks ?? "" } } + ); +}; + +/** 删除货权转移 */ +export const deleteOwnershipTransfer = (id: string) => { + return http.request("delete", `/api/ownership-transfer/${id}`); +}; + +/** 获取货权转移列表 */ +export const getOwnershipList = (params?: { + transferNumber?: string; + blNumber?: string; + status?: string; + page?: number; + pageSize?: number; +}) => { + return http.request( + "get", + "/api/ownership-transfer", + { params } + ); +}; diff --git a/frontend/src/api/reconciliation.ts b/frontend/src/api/reconciliation.ts new file mode 100755 index 0000000..b559b44 --- /dev/null +++ b/frontend/src/api/reconciliation.ts @@ -0,0 +1,101 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; + +export type ZSPReconciliation = { + id: number; + reconciliationNumber: string; + contractNumber: string; + companyName: string; + periodStart: string; + periodEnd: string; + totalAmount: number; + paidAmount: number; + unpaidAmount: number; + currency: string; + status: string; + filePath: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export interface ZSPReconciliationCreateRequest { + reconciliationNumber: string; + contractNumber?: string; + companyName: string; + periodStart: string; + periodEnd: string; + totalAmount: number; + paidAmount?: number; + currency?: string; + remark?: string; +} + +export interface ZSPReconciliationUpdateRequest { + contractNumber?: string; + companyName?: string; + periodStart?: string; + periodEnd?: string; + totalAmount?: number; + paidAmount?: number; + status?: string; + remark?: string; +} + +export const getReconciliations = (params?: { + contractNumber?: string; + companyName?: string; + status?: string; +}) => { + return http.request>( + "get", + "/zsp-finances/reconciliations", + { params } + ); +}; + +export const getReconciliation = (id: string | number) => { + return http.request>( + "get", + `/zsp-finances/reconciliations/${id}` + ); +}; + +export const createReconciliation = ( + data: ZSPReconciliationCreateRequest +) => { + return http.request( + "post", + "/zsp-finances/reconciliations", + { data } + ); +}; + +export const updateReconciliation = ( + id: string | number, + data: ZSPReconciliationUpdateRequest +) => { + return http.request( + "put", + `/zsp-finances/reconciliations/${id}`, + { data } + ); +}; + +export const deleteReconciliation = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/reconciliations/${id}` + ); +}; + +export const uploadReconciliationFile = ( + id: string | number, + data: FormData +) => { + return http.request( + "post", + `/zsp-finances/reconciliations/${id}/upload`, + { data, headers: { "Content-Type": "multipart/form-data" } } + ); +}; diff --git a/frontend/src/api/routes.ts b/frontend/src/api/routes.ts new file mode 100755 index 0000000..6d95ec6 --- /dev/null +++ b/frontend/src/api/routes.ts @@ -0,0 +1,10 @@ +import { http } from "@/utils/http"; + +type Result = { + success: boolean; + data: Array; +}; + +export const getAsyncRoutes = () => { + return http.request("get", "/get-async-routes"); +}; diff --git a/frontend/src/api/settlement.ts b/frontend/src/api/settlement.ts new file mode 100755 index 0000000..cf0e16a --- /dev/null +++ b/frontend/src/api/settlement.ts @@ -0,0 +1,102 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; + +export type ZSPSettlement = { + id: number; + settlementNumber: string; + contractNumber: string; + contractType: string; + companyName: string; + commodity: string; + quantity: number; + unitPrice: number; + amount: number; + currency: string; + settlementDate: string; + status: string; + filePath: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export interface ZSPSettlementCreateRequest { + settlementNumber: string; + contractNumber: string; + contractType?: string; + companyName: string; + commodity?: string; + quantity?: number; + unitPrice?: number; + amount: number; + currency?: string; + settlementDate: string; + remark?: string; +} + +export interface ZSPSettlementUpdateRequest { + contractNumber?: string; + contractType?: string; + companyName?: string; + commodity?: string; + quantity?: number; + unitPrice?: number; + amount?: number; + currency?: string; + settlementDate?: string; + status?: string; + remark?: string; +} + +export const getSettlements = (params?: { + contractNumber?: string; + settlementNumber?: string; + status?: string; +}) => { + return http.request>( + "get", + "/zsp-finances/settlements", + { params } + ); +}; + +export const getSettlement = (id: string | number) => { + return http.request>( + "get", + `/zsp-finances/settlements/${id}` + ); +}; + +export const createSettlement = (data: ZSPSettlementCreateRequest) => { + return http.request( + "post", + "/zsp-finances/settlements", + { data } + ); +}; + +export const updateSettlement = ( + id: string | number, + data: ZSPSettlementUpdateRequest +) => { + return http.request( + "put", + `/zsp-finances/settlements/${id}`, + { data } + ); +}; + +export const deleteSettlement = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/settlements/${id}` + ); +}; + +export const uploadSettlementFile = (id: string | number, data: FormData) => { + return http.request( + "post", + `/zsp-finances/settlements/${id}/upload`, + { data, headers: { "Content-Type": "multipart/form-data" } } + ); +}; diff --git a/frontend/src/api/test-utils.ts b/frontend/src/api/test-utils.ts new file mode 100644 index 0000000..927615e --- /dev/null +++ b/frontend/src/api/test-utils.ts @@ -0,0 +1,41 @@ +// frontend/src/api/test-utils.ts +import { vi } from 'vitest' +import type { BaseResponse } from './base' + +/** + * Mock http.request 成功响应 + */ +export function mockHttpSuccess(data: T): BaseResponse { + return { + success: true, + data, + message: 'ok' + } +} + +/** + * Mock http.request 错误响应 + */ +export function mockHttpError(message: string): BaseResponse { + return { + success: false, + data: null, + message + } +} + +/** + * 创建 http.request 的 spy mock (异步版本) + * 使用动态导入避免 top-level import 的副作用 + */ +export async function createHttpMock() { + const { http } = await import('@/utils/http') + return { + spyOnRequest: (response: BaseResponse) => { + return vi.spyOn(http, 'request').mockResolvedValue(response) + }, + spyOnError: (error: Error) => { + return vi.spyOn(http, 'request').mockRejectedValue(error) + } + } +} \ No newline at end of file diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts new file mode 100755 index 0000000..6241636 --- /dev/null +++ b/frontend/src/api/user.ts @@ -0,0 +1,54 @@ +import { http } from "@/utils/http"; + +export type UserResult = { + success: boolean; + data: { + /** 头像 */ + avatar: string; + /** 用户名 */ + username: string; + /** 昵称 */ + nickname: string; + /** 当前登录用户的角色 */ + roles: Array; + /** 按钮级别权限 */ + permissions: Array; + /** `token` */ + accessToken: string; + /** 用于调用刷新`accessToken`的接口时所需的`token` */ + refreshToken: string; + /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ + expires: Date; + }; +}; + +export type RefreshTokenResult = { + success: boolean; + data: { + /** `token` */ + accessToken: string; + /** 用于调用刷新`accessToken`的接口时所需的`token` */ + refreshToken: string; + /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ + expires: Date; + }; +}; + +export type RegisterResult = { + success: boolean; + message: string; +}; + +/** 登录 */ +export const getLogin = (data?: object) => { + return http.request("post", "/login", { data }); +}; + +/** 刷新`token` */ +export const refreshTokenApi = (data?: object) => { + return http.request("post", "/refresh-token", { data }); +}; + +export const registerApi = (data?: object) => { + return http.request("post", "/api/auth/register", { data }); +}; diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts new file mode 100755 index 0000000..0a31c79 --- /dev/null +++ b/frontend/src/api/utils.ts @@ -0,0 +1,6 @@ +/** + * 生产环境使用相对路径 /api,由 Nginx 或 Vite 代理到后端; + * 避免硬编码 127.0.0.1:8080 导致打包部署后浏览器请求不到本机端口。 + */ +export const baseUrlApi = (url: string) => + url.startsWith("/") ? url : `/api/${url.replace(/^\/+/, "")}`; diff --git a/frontend/src/api/zspContract.ts b/frontend/src/api/zspContract.ts new file mode 100755 index 0000000..8bc4cc9 --- /dev/null +++ b/frontend/src/api/zspContract.ts @@ -0,0 +1,227 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; + +// 合同文件夹类型 +export type ZSPContractFolderResult = { + id: number; + name: string; + count: number; + createTime: string; + description: string; +}; + +// 合同文件类型 +export type ZSPContractFileResult = { + id: number; + fileName: string; + fileUrl?: string; + fileType: string; + fileSize: number; + uploadTime: string; + + folderId: number; + + contractNumber: string; + companyName: string; + commodity: string; + signDate: string; + contractType: string; + + // 合同详情列表 + details?: ZSPContractDetailResult[]; +}; + +// 合同详情类型 +export type ZSPContractDetailResult = { + id: number; + contractFileId: number; + commodityName: string; + commodityCode?: string; + totalQuantity: number; + unit: string; + unitPrice: number; + deliveredQty: number; + pendingQty: number; + billOfLading?: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +// 创建合同详情请求 +export interface ZSPContractDetailCreateRequest { + contractFileId: number; + commodityName: string; + commodityCode?: string; + totalQuantity: number; + unit?: string; + unitPrice: number; + deliveredQty?: number; + billOfLading?: string; + remark?: string; +} + +// 更新合同详情请求 +export interface ZSPContractDetailUpdateRequest { + id: number; + commodityName?: string; + commodityCode?: string; + totalQuantity?: number; + unit?: string; + unitPrice?: number; + deliveredQty?: number; + billOfLading?: string; + remark?: string; +} + +// 批量创建合同详情请求 +export interface ZSPContractDetailBatchCreateRequest { + contractFileId: number; + details: ZSPContractDetailCreateRequest[]; +} + +// ==================== 合同文件夹相关 ==================== + +/** 获取合同文件夹列表(GET) */ +export const getZSPContractFolders = () => { + return http.request>( + "get", + "/api/zsp-contract/folders" + ); +}; + +/** 创建合同文件夹(POST) */ +export const createZSPContractFolder = (data: { + name: string; + description?: string; +}) => { + return http.request("post", "/api/zsp-contract/folders", { data }); +}; + +/** 更新合同文件夹(PUT) */ +export const updateZSPContractFolder = ( + id: string | number, + data: { name: string; description?: string } +) => { + return http.request("put", `/api/zsp-contract/folders/${id}`, { data }); +}; + +/** 删除合同文件夹(DELETE) */ +export const deleteZSPContractFolder = (id: string | number) => { + return http.request("delete", `/api/zsp-contract/folders/${id}`); +}; + +// ==================== 合同文件相关 ==================== + +/** 获取合同列表(含详情)(GET) */ +export const getZSPContracts = (folderId?: string) => { + return http.request>( + "get", + "/api/zsp-contract/contracts", + { params: folderId ? { folderId } : {} } + ); +}; + +/** 获取单个合同详情(含详情列表)(GET) */ +export const getZSPContract = (id: string | number) => { + return http.request>( + "get", + `/api/zsp-contract/contracts/${id}` + ); +}; + +/** 上传合同文件(POST) */ +export const uploadZSPContract = (formData: FormData) => { + return http.request("post", "/api/zsp-contract/contracts", { + data: formData, + headers: { "Content-Type": "multipart/form-data" } + }); +}; + +/** 删除合同文件(DELETE) */ +export const deleteZSPContract = (id: string | number) => { + return http.request("delete", `/api/zsp-contract/contracts/${id}`); +}; + +/** 更新合同元信息(PUT) */ +export const updateZSPContract = ( + id: string | number, + data: { + contractNumber?: string; + companyName?: string; + commodity?: string; + signDate?: string; + contractType?: string; + folderId?: number; + } +) => { + return http.request("put", `/api/zsp-contract/contracts/${id}`, { + data + }); +}; + +// ==================== 合同详情相关 ==================== + +/** 获取合同详情列表(GET) */ +export const getZSPContractDetails = (contractFileId: string | number) => { + return http.request>( + "get", + `/api/zsp-contract/contracts/${contractFileId}/details` + ); +}; + +/** 获取单个合同详情(GET) */ +export const getZSPContractDetail = (id: string | number) => { + return http.request>( + "get", + `/api/zsp-contract/details/${id}` + ); +}; + +/** 创建合同详情(POST) */ +export const createZSPContractDetail = ( + contractFileId: string | number, + data: ZSPContractDetailCreateRequest +) => { + return http.request("post", `/api/zsp-contract/contracts/${contractFileId}/details`, { data }); +}; + +/** 更新合同详情(PUT) */ +export const updateZSPContractDetail = (id: string | number, data: ZSPContractDetailUpdateRequest) => { + return http.request("put", `/api/zsp-contract/details/${id}`, { data }); +}; + +/** 删除合同详情(DELETE) */ +export const deleteZSPContractDetail = (id: string | number) => { + return http.request("delete", `/api/zsp-contract/details/${id}`); +}; + +/** 批量创建合同详情(POST) */ +export const batchCreateZSPContractDetails = ( + data: ZSPContractDetailBatchCreateRequest +) => { + return http.request("post", "/api/zsp-contract/details/batch", { data }); +}; + +/** Excel导入合同详情(POST) */ +export const importZSPContractDetailsFromExcel = ( + contractFileId: string | number, + formData: FormData +) => { + return http.request( + "post", + `/api/zsp-contract/contracts/${contractFileId}/details/import`, + { + data: formData, + headers: { "Content-Type": "multipart/form-data" } + } + ); +}; + +/** 导出合同详情到Excel(GET) */ +export const exportZSPContractDetailsToExcel = (contractFileId: string | number) => { + return http.request( + "get", + `/api/zsp-contract/contracts/${contractFileId}/details/export` + ); +}; diff --git a/frontend/src/api/zspFinances.ts b/frontend/src/api/zspFinances.ts new file mode 100755 index 0000000..1398433 --- /dev/null +++ b/frontend/src/api/zspFinances.ts @@ -0,0 +1,381 @@ +import { http } from "@/utils/http"; +import type { BaseResponse } from "./base"; + +// ==================== 类型定义 ==================== + +export type ZSPFreightCharge = { + id: number; + billOfLading: string; + contractNumber: string; + expenseItem: string; + amount: number; + currency: string; + paymentDate: string; + freightCompanyName: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export type ZSPMiscCharge = { + id: number; + billOfLading: string; + contractNumber: string; + expenseItem: string; + amount: number; + currency: string; + paymentDate: string; + companyName: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export type ZSPServiceFee = { + id: number; + billOfLading: string; + contractNumber: string; + name: string; + amount: number; + currency: string; + date: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +export type ZSPCostBreakdown = { + id: number; + businessId: string; + businessType: string; + totalCost: number; + freightChargeTotal: number; + miscChargeTotal: number; + serviceFeeTotal: number; + createTime: string; + updateTime: string; +}; + +export type ZSPProfitRecord = { + id: number; + businessId: string; + businessType: string; + revenue: number; + totalCost: number; + profit: number; + profitRate: number; + recordDate: string; + remark?: string; + createTime: string; + updateTime: string; +}; + +// ==================== 请求类型 ==================== + +export interface ZSPFreightChargeCreateRequest { + billOfLading?: string; + contractNumber?: string; + expenseItem: string; + amount: number; + currency?: string; + paymentDate: string; + freightCompanyName: string; + remark?: string; +} + +export interface ZSPFreightChargeUpdateRequest { + billOfLading?: string; + contractNumber?: string; + expenseItem?: string; + amount?: number; + currency?: string; + paymentDate?: string; + freightCompanyName?: string; + remark?: string; +} + +export interface ZSPMiscChargeCreateRequest { + billOfLading?: string; + contractNumber?: string; + expenseItem: string; + amount: number; + currency?: string; + paymentDate: string; + companyName?: string; + remark?: string; +} + +export interface ZSPMiscChargeUpdateRequest { + billOfLading?: string; + contractNumber?: string; + expenseItem?: string; + amount?: number; + currency?: string; + paymentDate?: string; + companyName?: string; + remark?: string; +} + +export interface ZSPServiceFeeCreateRequest { + billOfLading?: string; + contractNumber?: string; + name: string; + amount: number; + currency?: string; + date: string; + remark?: string; +} + +export interface ZSPServiceFeeUpdateRequest { + billOfLading?: string; + contractNumber?: string; + name?: string; + amount?: number; + currency?: string; + date?: string; + remark?: string; +} + +export interface ZSPCostBreakdownCreateRequest { + businessId: string; + businessType: string; +} + +export interface ZSPCostBreakdownUpdateRequest { + freightChargeTotal?: number; + miscChargeTotal?: number; + serviceFeeTotal?: number; +} + +export interface ZSPProfitRecordCreateRequest { + businessId: string; + businessType: string; + revenue: number; + totalCost: number; + recordDate: string; + remark?: string; +} + +export interface ZSPProfitRecordUpdateRequest { + revenue: number; + totalCost: number; + remark?: string; +} + +// ==================== 货代费用 API ==================== + +export const getFreightCharges = () => { + return http.request>( + "get", + "/zsp-finances/freight-charges" + ); +}; + +export const getFreightCharge = (id: string | number) => { + return http.request>( + "get", + `/zsp-finances/freight-charges/${id}` + ); +}; + +export const createFreightCharge = (data: ZSPFreightChargeCreateRequest) => { + return http.request( + "post", + "/zsp-finances/freight-charges", + { data } + ); +}; + +export const updateFreightCharge = ( + id: string | number, + data: ZSPFreightChargeUpdateRequest +) => { + return http.request( + "put", + `/zsp-finances/freight-charges/${id}`, + { data } + ); +}; + +export const deleteFreightCharge = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/freight-charges/${id}` + ); +}; + +// ==================== 杂费 API ==================== + +export const getMiscCharges = () => { + return http.request>( + "get", + "/zsp-finances/misc-charges" + ); +}; + +export const getMiscCharge = (id: string | number) => { + return http.request>( + "get", + `/zsp-finances/misc-charges/${id}` + ); +}; + +export const createMiscCharge = (data: ZSPMiscChargeCreateRequest) => { + return http.request( + "post", + "/zsp-finances/misc-charges", + { data } + ); +}; + +export const updateMiscCharge = ( + id: string | number, + data: ZSPMiscChargeUpdateRequest +) => { + return http.request( + "put", + `/zsp-finances/misc-charges/${id}`, + { data } + ); +}; + +export const deleteMiscCharge = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/misc-charges/${id}` + ); +}; + +// ==================== 服务费 API ==================== + +export const getServiceFees = () => { + return http.request>( + "get", + "/zsp-finances/service-fees" + ); +}; + +export const getServiceFee = (id: string | number) => { + return http.request>( + "get", + `/zsp-finances/service-fees/${id}` + ); +}; + +export const createServiceFee = (data: ZSPServiceFeeCreateRequest) => { + return http.request( + "post", + "/zsp-finances/service-fees", + { data } + ); +}; + +export const updateServiceFee = ( + id: string | number, + data: ZSPServiceFeeUpdateRequest +) => { + return http.request( + "put", + `/zsp-finances/service-fees/${id}`, + { data } + ); +}; + +export const deleteServiceFee = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/service-fees/${id}` + ); +}; + +// ==================== 成本构成 API ==================== + +export const getCostBreakdowns = (businessId?: string) => { + return http.request>( + "get", + "/zsp-finances/cost-breakdowns", + { params: businessId ? { businessId } : {} } + ); +}; + +export const getCostBreakdown = (id: string | number) => { + return http.request>( + "get", + `/zsp-finances/cost-breakdowns/${id}` + ); +}; + +export const createCostBreakdown = (data: ZSPCostBreakdownCreateRequest) => { + return http.request( + "post", + "/zsp-finances/cost-breakdowns", + { data } + ); +}; + +export const updateCostBreakdown = ( + id: string | number, + data: ZSPCostBreakdownUpdateRequest +) => { + return http.request( + "put", + `/zsp-finances/cost-breakdowns/${id}`, + { data } + ); +}; + +export const deleteCostBreakdown = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/cost-breakdowns/${id}` + ); +}; + +// ==================== 利润记录 API ==================== + +export const getProfitRecords = ( + businessId?: string, + businessType?: string +) => { + const params: Record = {}; + if (businessId) params.businessId = businessId; + if (businessType) params.businessType = businessType; + return http.request>( + "get", + "/zsp-finances/profit-records", + { params } + ); +}; + +export const getProfitRecord = (id: string | number) => { + return http.request>( + "get", + `/zsp-finances/profit-records/${id}` + ); +}; + +export const createProfitRecord = (data: ZSPProfitRecordCreateRequest) => { + return http.request( + "post", + "/zsp-finances/profit-records", + { data } + ); +}; + +export const updateProfitRecord = ( + id: string | number, + data: ZSPProfitRecordUpdateRequest +) => { + return http.request( + "put", + `/zsp-finances/profit-records/${id}`, + { data } + ); +}; + +export const deleteProfitRecord = (id: string | number) => { + return http.request( + "delete", + `/zsp-finances/profit-records/${id}` + ); +}; diff --git a/frontend/src/assets/iconfont/iconfont.css b/frontend/src/assets/iconfont/iconfont.css new file mode 100755 index 0000000..d25cd6c --- /dev/null +++ b/frontend/src/assets/iconfont/iconfont.css @@ -0,0 +1,27 @@ +@font-face { + font-family: "iconfont"; /* Project id 2208059 */ + src: + url("iconfont.woff2?t=1671895108120") format("woff2"), + url("iconfont.woff?t=1671895108120") format("woff"), + url("iconfont.ttf?t=1671895108120") format("truetype"); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.pure-iconfont-tabs:before { + content: "\e63e"; +} + +.pure-iconfont-logo:before { + content: "\e620"; +} + +.pure-iconfont-new:before { + content: "\e615"; +} diff --git a/frontend/src/assets/iconfont/iconfont.js b/frontend/src/assets/iconfont/iconfont.js new file mode 100755 index 0000000..da95f2a --- /dev/null +++ b/frontend/src/assets/iconfont/iconfont.js @@ -0,0 +1,68 @@ +(window._iconfont_svg_string_2208059 = + ''), + (function (e) { + var t = (t = document.getElementsByTagName("script"))[t.length - 1], + c = t.getAttribute("data-injectcss"), + t = t.getAttribute("data-disable-injectsvg"); + if (!t) { + var n, + l, + i, + o, + a, + h = function (t, c) { + c.parentNode.insertBefore(t, c); + }; + if (c && !e.__iconfont__svg__cssinject__) { + e.__iconfont__svg__cssinject__ = !0; + try { + document.write( + "" + ); + } catch (t) { + if (typeof console !== "undefined") console.log(t); + } + } + (n = function () { + var t, + c = document.createElement("div"); + (c.innerHTML = e._iconfont_svg_string_2208059), + (c = c.getElementsByTagName("svg")[0]) && + ((c.style.position = "absolute"), + (c.style.width = 0), + (c.style.height = 0), + (c.style.overflow = "hidden"), + (c = c), + (t = document.body).firstChild + ? h(c, t.firstChild) + : t.appendChild(c)); + }), + document.addEventListener + ? ~["complete", "loaded", "interactive"].indexOf(document.readyState) + ? setTimeout(n, 0) + : ((l = function () { + document.removeEventListener("DOMContentLoaded", l, !1), n(); + }), + document.addEventListener("DOMContentLoaded", l, !1)) + : document.attachEvent && + ((i = n), + (o = e.document), + (a = !1), + v(), + (o.onreadystatechange = function () { + "complete" == o.readyState && + ((o.onreadystatechange = null), d()); + })); + } + function d() { + a || ((a = !0), i()); + } + function v() { + try { + o.documentElement.doScroll("left"); + } catch (t) { + return void setTimeout(v, 50); + } + d(); + } + })(window); diff --git a/frontend/src/assets/iconfont/iconfont.json b/frontend/src/assets/iconfont/iconfont.json new file mode 100755 index 0000000..ca293f4 --- /dev/null +++ b/frontend/src/assets/iconfont/iconfont.json @@ -0,0 +1,30 @@ +{ + "id": "2208059", + "name": "pure-admin", + "font_family": "iconfont", + "css_prefix_text": "pure-iconfont-", + "description": "pure-admin-iconfont", + "glyphs": [ + { + "icon_id": "20594647", + "name": "Tabs", + "font_class": "tabs", + "unicode": "e63e", + "unicode_decimal": 58942 + }, + { + "icon_id": "22129506", + "name": "PureLogo", + "font_class": "logo", + "unicode": "e620", + "unicode_decimal": 58912 + }, + { + "icon_id": "7795615", + "name": "New", + "font_class": "new", + "unicode": "e615", + "unicode_decimal": 58901 + } + ] +} diff --git a/frontend/src/assets/iconfont/iconfont.ttf b/frontend/src/assets/iconfont/iconfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..82efcf8f7a34c697125264a48d96d149237662a8 GIT binary patch literal 3904 zcmd^CU2Ggz6~5=*`Q7>1UGMIUw~lw$yY)I7N4xf}cQ?(?Hi;cOKSh)@^;4_Van?>6 z+iN-7&>ut-C=w|BiJ%fitprlwfwxNKrwEWx1p-x}FAd2HDmoFt7n8hwK!Nz7DdiY@O^6=A)(Joj|eEy+Ib?@jA7y9$i zADdsETYck41DBxx1J7%VOHVEQ^+5Lp#>Dp-GyZ<2J$E|)<{zE|_Rn}4XJF9Z6JLY; z3q1FpS?)d=g8<(TFvONR^K1Fu80zI-k_v9)K@&e}jq32fSmfNwbdtYbFevL7{ zxY{}2ee5p>k1!ThFu#W(cX4s^B6~!tjA4|kx${y2i^O&6M@~lf-ey*a{nNGI{aOOU z_vW(-n4lVrODitPq;Q`_15H{ZxEb3F^l(1H##oE-sp$t!Fn4LLyTa~~Nd#Bme}_y< zXBX$dA9@mS5=pQS(|T3~>0V(f*7^;84YrrUH~o!O37(v(0@lyC9$Ywdn*Hil>Q*bL z2NQo}mjIJFd~$Ia@XiM+VxjYl?F^^rOwZ>OtkQcY-dHv43Tr?&!U`c$?pA9PwfIEB zY`7kYk*r)YkX+mU`(+0OnCO)O^}dw~A6ffbx$u#{dAC~O6Lx_(y{vo*UY7I}-{b2n ziH_9SarP)%W?l9iyUeb#pQFlNQX46xl3KpFj|y6GqOpgnzBfqHQr8MvnI>A~H~fi4 z)z=#n#k`jEs>7b|C3Tky`Qk{*1F~5!HmaVUYEiA~OOy^+qbVe7K1)UHv9-^f^r-Le=+CE}KD`#rjAgQAig-4z9sTjFMNYyYa{63~Wct!ZG+T~FGnMEg5yy#q z)60-+e&>=9&uml^`jhWW0@rm*u|^V*P?l zi+(Re*87SGHa&*TXT`U0O6A!h^q)~u*S79d(nl|=%|^3PpBO1_-Tr@u(1ditEeU)t zwqF0mgp=70=E+x~Y?NifNr>C};(#cY~UP%tc6K^N}&1`bxdVm~F7=b@877rde zUb|0RERUC}gZmB*w{{(znmszRV{~-K%+cAY$!2SKdhcMhG`G=X~92T`|IMqxgGlZfQ?xHH@qi~YM>i-<4h&cX#9VbzfDa!LIuc2&GYw@~V zEG#Zh$tuvh`TGye9QBiZG1rb5x~g*`Ju_f?1vBC&OM|<|?&ZohO?~{rwoFuipcK`0 zLAGfdgfp6iqT|iE6%mA6jTnM!pb*4OLl^*&DujYjRV+W=A04o`bkj5=0O_hBbZ%Q* z7lsWYN5!n6Dv79~*rjSZ?#C2MLjc{;xKKG4pyE1LI8^ZHq?*yblpVMFla6D6!PHb! zK|o}OQL_sNWWbo$Y-BS?4EN|g_gHyF=!r<}@Ch@f7-lk&h+={d7xaXweq3scsB5}$ zClnkUimYuqj)?(FZ+l)K3<c!|4HMaG)Q(jia}&s*t5Bim@X%B1C^o86I>8ud9}5Wpq^-b5s5CC~D_e zLG4fuSwsc33v?-K=vxE^T?<;w$&FdQp&Lq6lOTk}!7WHdYYFWlvMfW5#LRTDPjJIT z+ay(w5@@i?Fph|2QvJ5BXlMn;?++&|gEX`oOm1@D!@wrhNIYVrZ^*P?V9y+`L2|o|s&}~K2 z&YjaV#nx}gZo=)Hg`Yrq`|yj_4gD_49@4(;7}rC}>mFLXKxjFE`wKU9Tc`o4YUwwF zTyBP>fwFN8@E{dW(F}xFG@qc^}a|MOF$35XysRPq9=2vvw@y0V9)nt z1@exbtl|r*+LJZd_x0om8^zz!kkbO=s{HCzOvEX&ul8hOG4{ir%$du6){_P7zwOBi zu;$F@u}?UnZ07R~t!i`8y-A-AyBSonlX6Jj>=;hpn)MVC;TMKMwRFdx9;&bL~^J zcYDtQy~fT1CpmL$oR#r!)7_{UpjHCp-PxRH7Z9zA9J|OUhi45;%aEpf`L&VB6672j zDCZsr&pgm&b|&yzWxH7g_ttv0@a15g%km$q@RI-Eeb%L5n+lwyDpR_1r_L)&oyCq= JX@3#ee*y;q>;3=$ literal 0 HcmV?d00001 diff --git a/frontend/src/assets/iconfont/iconfont.woff b/frontend/src/assets/iconfont/iconfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..0fdaa0a4174505d3116137ed761c9534b21b05f4 GIT binary patch literal 2484 zcmY*bc{tQtAOFo*W-!^hN|Kpt3E2r{yM)kU*UdT`W`r>^F|v&!W67NfSsU59SsR2z zOhj4YlI-SMO4cY#k~jC~eV_Zs`+c6z`JT_`e80~*=XuWad`_6%`SSn-07Kv@AasyL zO#jCZ=Kmj7PA=yF00IY9LlDlWWQCh{P8!;vW&!d8V2o63gzE>{Tx+r&IV+)YybXt5haUXyrZML=! zh7aObrHp{F5>0CHq@BMx98P3HftXS+y^t;d_0&%)UyB&tri^wLu~EQ zUAp3!RI$-bblgkb(zQ=^4F4vIEWoxp$-+3f; zBF;AsD#fz>%yj*2lqEW?n~J*5xI;onG7{m(h$LG+%&R5oyS``Lx_ye0-Z98~*P@a( zT%S8XCnz_|MgC}cC^Nad6C2gBdu8W_okFEV$^BfLo+pV8k|J}VNa@9yy2*-q&-G;! z8d`{x=&z;{@U2}_CeF^FWvy8S4`2Dit7NmJ-B=O6qTy}GZvB})AR-czwp!n;I*}rd zPKKTBNcQ+MW#IPTAGI#b1bS`El=Xc)#lJq>xFK-riqMfeiU~qM_xAUXMBU{NW3bq3 ztbo#Zj)K8@|LztY*@CPLEu+&6nimZ<#?7RSS9kuS*6k%2;9Q`VUvvAXFqW+8ck!!j z_AfrwMtR+O-(v@FAEmghA1(>}E>+_5wKBG=@j^V@mfEAV zVErbYp;mKDh1!|TihGA#3x5DetM?^DO~0+eD{DLR+Zn5G=U!YH^1sYK;8+)3`BS8J zV8%=N$xx-ZS&5WPdG|(OsOTD%%u2E}KZF5acb8SR)NKw{}dgRsKh;l2j zYN_AYQ`r*7kADj5C%(#_+BhC!k&*^Ozs;C^>{+PSzZ#H#2D^>!l#2H$P-kk~RbbCZ zR=hk+7URH zb>y{Dj+-g|%aL`5F)8B2oZYr_*7r9zLE&`)1sLNHX3`%2%Y4PJ#7q~phfb`MDvS@x zwmlGs-DB5?NT|J*vM-HE`K3+h?&p(%jIq7Mg7W9>@3mh@iuel*E4so?oy zOovul8=p#BD#N81@SCk=_XV2XV@icyEGqU#F+%uv$^2}%Wj52KTW6tIU_^XgXSH4M zaiXN3jl8n(o@>h2@6H2s9H+1(%)!ToZtJGAAC`xhd%Q3Gr^#T|ZcGMQHFwz8yYOi8 zecEY?#Ps>E)5CpLN!KzsA*xtGC~B6^?ooYFA$K;aJpyjn{g!|}Bb6{a z%?V{jn{_*0E%ZhE)h@a35H|EzP?+U01ECA~+~>qETVD1X!mW#*@)F3q+%JYhK*O9uT-@I-xsN{R=idgZ?k_l5P0&-`WSeYMGPMEiBi z*_#pTH66USlFC)F)r{qE#KkRj&FVVZDwOQmLpc?yQtp5ovj|cve&FOdg(_^{c`B}- z!@@|ro00cLIX=MaP&p^jO5@nkHpMkxR+;yLh1zv& z<24(*;=U*z@|X1culIny1>oi*o%*TKxla(2st6Q$zc*11z8hmK#1q29=y}QE73Tw# zY=mU~tv~P}(e^d~3O;X$1RDUX1fhYTk6|$wj9D81umLg98Nq-+A?du4z`uPMtT-MZBN;hzs- zwewc>BW@bjLuW6iG7f)$FA_tNtiwmmc`Td9$AY*m*(bVy&+LSRuT7uS$J!NY@2NcF z6p=G_)7W;=N2&@ES>y-1S0n1zEhYL(@_y0h81bp5v`i$&u?>CLbsWGcYk&V zwg(?6-51o?{8AO4OAMdkES0_s(VZxDAr8JN{!C5X!X36-Ej1d@LuWhRxZURr)1$lk?ccClsDD$6ydJL(rSy5oJs)ffH@KXthF*5KK~W*~3X9qhQpA1oy4pN5fivqhr-VkY97!yvS zIIyecWI*&8`V)ZyPj>fwXIQ2sN}~}XjY>%jkLTNaNf8YB{ZaQ1-4|I*Kyv3~b1<=gpf5dDC!l>m zF3iW@eEYfj|1b|t56Ogr$9yvTCX>U&;vS*UOyWX9R6~SNdF%8}EI^*-9zQLXMVN%= zl1X3VDh_;!K%;g0+WY6X53q47QUhDM#LU3y8@IU?Fx(nSLPDsQYta*ncvnMuNS}B! zem?%&42+~E6Sq1zKhyAa=35pjf&AsZya}{(LJcyOTjq+()KvfE??(DNCiz9FPwJg`#HygM1P~NSgOE@(Q71{+rGmgG9U%5y zIwl^!9CtoR)MduGRXipg5su1Z_uDrM{H6hZNfoKmsJ|*y*V+^mER;BJs&ls zM6%Nk+E=3qHwi_JAV!5To@`)nb{ zsA+^mD=C|3T7;Z+F`1xgi6nKJPzY9HSdC$1Hk~4!SVm=8ZCwOl1OEeF{+E)-*){1hrT95R!!&qv>0wyEq9ZGIdIX#3`YoyyZM8 z)>U=RLljdj(at>2v`5vE@3E{_I1C+3Iw5>-c9xXQs+;7Oi18?4KTI#^QT4Uft{!hQ zgyLJoJCX0=w~E#owzu)U*!ha_#5Tf+yoybV77ooUoVjHd^&D>4#%D_(X9Y6@*`KE4 z+VFGd3oo48!bg022*Hg=1OGhnkazI5ZSUFa){P%$eUkn`%7Z`nXsApW#1h;`Y+&C- zM-m*gDNU?HW6-53ONe*zC3tl@4tl?1hnt(7P21Ukl_=?DG#m9b_GSNLpP~l=`)MIP6N9DS*(^#>U_Ss2F=#MHiXvnDFaw$N;8#uQx;|TKh(mm zxca!ds+8K4E>l|J3h?@tf&YC{%hwNR>NBmF`^|rH-SVkT(`gh;3G6-XNm{Fo$$tKo zjlYyWUK-^$52>D9HvjQ+fsZD(j7qHeeAJ9MGj;{?;I*e@#zFFYIZ6rA`+mbWXs)acuD7-~ik2LzSW7%GeEiKzd3 z#!N#1=ve&;*K!Ue`Mlfr7eQOZvz98MAkPHN2n`D$7=Ajn(rKe0{P|&W=a+Afp&$jQ zz1w!|8Tg;6FnWSxYxegI89g@f_pFs-Ze0AQNYd|h#E*+zTlrID(`sp4Fz(ZUgoUto z!Ap3^y@`){>Hl8{H*mj#j7z{P_;{D!H6HhYD~|D~Ii0)PE@a?bI)=Xkjp~k&RrUw- zDyGDHlf0l`wuW#YWfTUlcZz*TpD2a!cd09l4;$A=$@7 zpb`=30hgt|^dyWWQSjMb5;(k_J6x&i3|qIpekZSzssfTV++Gt86H`gN-A3fJG&3#D zMB2gdN)2OiSGalQW#!QGeo9UbhU=|SDqPviP!T)(9DtJq$BB%eFb^hI)^61ClyJAgedYmXJkIqsbId!;N zUH7+LP(z`JL*k2Fr)1CsuJL?*JE|&17q}pm=2=Q^d$D#Nb;w+C4JQI3BrUUsr1z{j zg*A-to>eE^uoNoo^vrwP|L^ yMK>0RIu*}lIU1d93(1#3g?ar_ee*7YMnW&~cDquE8s$#N*sa)dr=eE>0000mD!%Oi literal 0 HcmV?d00001 diff --git a/frontend/src/assets/login/avatar.svg b/frontend/src/assets/login/avatar.svg new file mode 100755 index 0000000..a63d2b1 --- /dev/null +++ b/frontend/src/assets/login/avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/login/bg.png b/frontend/src/assets/login/bg.png new file mode 100755 index 0000000000000000000000000000000000000000..8cdd3001fc95b63c1d334ab7d717812f4240839e GIT binary patch literal 17468 zcmd6Oc|gtC-~YLFQ3+XQTw_Z}40TgVX}M$>=B8%CEX1u8p+$>I#fKrxM16-D33bhP zgs-g`OH}G(7#V|_v4l$3ShCg~p(J|V=Y7uS-k#_8%=7oJKW6SZ=Y8JG>%E-qV}t+n zDP1f+upoqVnflShPYLNH5F#II))9IhUODXp|LYSs>GQaNsIa(%pqNlHetuMNsP)u{ zpar3yh6c?~TKZe42O*}m;WIvu``lOU9ugH{A4F;F6Cc6qVXohtK!4`D~1@(l^>CbYb{M%VR@upACqMnIGo<>BPxg7_>ZX7RJRzyE`}}BqZ1; zIN3+VEO2mib8~Z0j&>M5dK6HMicN})3rZXn89P|wFflYXBqlsME<7sInsN*Zj*5@- zv;kF_4H3}>wvn+MCqT?0F(}%>(OyYyk_df$|8LTW2m^I&+^3;Neg9X+u``mQLmfU1 zjg5+r2?5WA4W^=^-F;$0gW{rMW<*6Tkzn*+7!?;4yD%!+dh}@fG1hjzK_TIhv}ZU@ z@%43|8W|fG6d4jab)u&YFtHC0pYJ|NIcEH1H&-_&SEZ|?<0PN)%85SX$4^!|I*xXp z;5f!rnmaKnBt9ZEGESO1-#B;tKhCA$5D^WMCx*s^FAJSNIVLK?8m)E@|7TrX|B)Y` zJO7_`8T*fO9Y7fe>f3g{mbxH-=uboFLcR)ZY6%4$5aldOr$cIko!FaPBh}DZsOK3-fdUwMan0rI|uOgmV>WrSI*5cVNLB46D9kl z8d}m(_uX(#b>V^B&@b1pPGMR~7qQXm$}jym(fvgwuN$VzxSqY#gzL5UY=JB>v@4$# z<0j)O`kTuXtW(>gwxcd-eRxn0PBeOqcWcxh$pDL&Atch5tLUFM5Ud=+WrjXCTgc~3 zYgGu9w%lgBD;cuH_k&|BBnhfZgWC)m*;RyqM(^{9&pqB3W*^}Ux0DUmO1S;~Imua< zH7ifz`Elb}?|Iv}Roan$9dy^7B|VZxkmPO>MnOi5Jl+$w4$51b#OW^l;nCRU)mkf`;s)OHGt62uMJ2KNE0cJ1b3CZ-z@}stJP^I>dP$}} z=-Nd*xQ`8y=k3A-`nTqBpPjGTMZK};lEmZigZ17|94q*=KE_a8swR;}st^S8B0l$r4$ zy|gO38z&rCC)YH{c>J7S-&17$~`wXIk@fwcq=Gvh4R zb|Ghbxr=BtYeU{wxqoDLXLB?ufpT4EC+>^%a&rymH7%~YP&dfdkY|&x$GwRCGC(5y z*cc5&3fK*YO!AwgC`&)^NWb9N136=-@Y^bDzO{oj$lXVy!KR+9rSS;th&J(hB96`8 zv~uoZ*qP8jVX4ukhwet3#E%NQ8f{X3o3}1EDPk6%bLVAuNLsF{<>3s(UEP_DDi7rC z$*s!f39k9vSkzi9#L7G#g^F48Fg3RIX0tS7eKWj^#yGH+7~Nti4lwP)Ia&{ss?mr) z7SOU_ujZI2)AQWh>vToNE>okYU(A0U!V7L_&7#RIrwsKg&QOx~p8cF5<;@6H z3EACO76gcVTK^B6NWlY1(@n=bQ$7_9b~WK*L(Xur2QkTMfAlq+)sTQYkW4%nEKNbjlWm z(3G(%vPP=?4P(}>sNXo7gQq|BscF7J`&R8$x>jxwxzyT)7Vn-)#Yc3V)>U+cy})d~ zUhB2PTN}qodjY+-M5r1%t(*86ja5$Qzk=M3m3A_kSyKSZja))J4b(@!9ON}P@6f(` z$xEuIu~E(AMdro=s-8QFmb)IS7-YiK#UonmC7H_bF&%WlUCT^r67u9^p60k4}3z+|Q0GS#0O z#r>c6!z=2Fz@kf>cQ5Cj&71lb@DiJ~dbnJ-AL`OUPQR#9eJ?q(npa%yG6@*f(7YbB za9n>T5nfk6EQ+j-WNfuz0T#@uyl!6o&$b&)H7D+~Vb#IB_l2k$UO2V00%$e|JUPxw z<(Ngb%{yHqdBO1i_wbJBgM&P{7F|)lNRFyr&PBNdbP)?8Jz2|t`@W8_Mm)OpOd|eM z-CPt^y@C_l26V+3TFIdlzI5){_O5T61sf;FtPJa!<4aO(=@`+vm=9gmIpsUFF7oxme&IjX|9=3=)8;6Uym)o*g$$_Od%x2Vax%F$=9 zX7il2`;N4pn$5QeWOomtG~vDAFSkjf|01nU(wZmOn2X+3YqH)C<2p_oOxl)|6dmH; zC0izw71ai{!sWWMQLri))G`zOs6VP$w=wyzQH7gB;WxgNry6A2+`204KGS*6Y?MI$$y}_tA%U#ePO7^YO#K6fZW@L*lj@yMpww_g#n0us`ilvb25CYgr(x(5Gx2S-1c_n?sqSVNI&Ch#y_v;vR(wmUb76NC!V_9! zXA)g{+?J1~*^nl~s;N<$M7J4-tVvV3v`%WQO!P{_P>U_;c19Y~CYb1>jYC67w*@OL znhgOoPp%7!@KJY`5Kpxd){UtRG@yJ6+6hmjok>L}Sz==Qv9ddcAvalKKf}<@o^SwJ%OIbU1 z9odm#2%lIp@%4!~33WQQ)i4%dE+*Z~HMuQaR*YRscH|mIVdfWUS&>o(_xqS!z5*I5 zc@Rfi%EtON*4&p&itZ&FJFlQ!y!n`muWtc+gJN6AjsjT148_O?Bos+!+h{Y{*p;-7 zOHuc+TsOLUJNJ6x1~MnDT}}4kgG;W(R zo~bGRXfDQ|v~WDejTFLr2v1_8_@bX>D?8x@Wm5uB>1h(ZGZOZZ93t^3NX{&#pftg(z*mOdc3qCcuzVGp3WAO<;>ur}u6p9{n3@;-k1jXnWLWMt}PclO-;%_ms4IJcRg_$0S5Z(Fdo= zJ&z(_C&jh(?=4F#td~}jd2O_?BM&~zSj}`6elZtQe*ZV$G;1{`%G&34{P3Vm+9OBLUk6)XAv zF79IzX8EM%zQnY`QI_=+Y#tPpb1`6A(Dp2IfxEFf*ES zhkp+aWSXa^RGcOq63?2GfU1gKjW5yL=*7)^;MvTlU5* zMKV86MZzo&O;9(5vAOhA6R>nJpX@(QmNoq56x}$Ubp9X8vi2qQ4Zg+KYvBWPQT@l1 z?(BvYTi=;6qg3&7O`e*YpdA5R3o=A$|F(A(u{?6~un|-^GAP21w6lqJ3|KERrJw|+ zt&~p8y?jYpgQ4*;Ny{}frjoQWLt_d_+hb@GYG{lnX=fxkuo+J~BaO@l zId@YwiU;|H%P$NlNy~(lyM;g zS2ge;6tm=-hnF{Uq_Fk9)L1z8R8rIZfljKr6}5qt^i`*-7n5fBtGVW3+5P&KesERK zR$O71nOKqg^64Oxc~Z|H=y8x7avB|{tnBO%>$uc4(c z&!TRpzAm%$UsIb<`EpOU!)-2H@rRH1T}UD`bib zPPs`A{9Z0Y-&JE*m0Ps`r8MQ$4wCls!Gy|ru&vv-r~xFc7#dM78uRh|1OKPa)$RTXa?Z~dONYNWO4e$ykS`vuF3 z14rogex-bGVw#0=?Zwo47MrS91&+TQxp4Bbkzf39bWZ(6cn4Jus!c6$2>bp`KbJ^T zwvfy(>!Q&)8d|^5R$Fz)$nqRF=GSxZY?1RgA{okl08i!g&BSs^aH(g5kGi)sGd+V? zdX#N{Rh^bKUGm%juyM}jSFHnr3zw9y;{2AuquAWXGmo^m=&QNYi(XzSFKzvMYabye zWnuZvYp?(QlaHJM;3Aq2ez8{P#{pJs8m3(pkyw*cWp!m6hbQfBM`eCQ&RP}#iuHX( z@p%8^b#R>cBgZtBnXx1TdNaKW>V6nk(v4N!3U60o)ga$njmJG*2jy+!`nB8#W*=8R z+xEtb1+L0luF+WDK34biaci>_r{#6#3pJK=kJXh9%KPTk(`^h)T~amaciMi zXKdvbpU4G^=R7T-_T0?b$JHzWo|*ffguQfrA%XQF%pEs~EibyxGKJk*D`w~nfP7lx zPk`cDhL~{6OjPY#qd{o2?j}nALVLNxt=OeTrQxbcqkTWOD)x_@U%?> zV(~23)lppKEfYz{MayaqOwjM<%zK!LWPgMFaPGQSJDCXU*(OSHXZ^U-H4(a#4RQvS zTMt22Uzfx((|R0GWW<;IIAmXItSETEoOTYLGZG6EE{%{Ey~j*R|CU(hyA`a)+8}%a zEav9DnHAl8wN&et`I3$g%j#am`S~O`CWbStHNE8;;a!pqJ4)SvcXCaUm-Bu_%J7a% zmnvIgYFQU}WVvnOH>YzivdZ%USl#I2$N+t~69gv*vyi9&bTxAhZ7))!Eann=g8qrA z3(J4@{APdS-M&2PGP(=0=lY+%_*GZGO#f%VA83}C3fg!a%>8<9VSi>7TwRgZvunH7 zU*27H8fp9;WD2dvlc-K@sq!G~*}INd`g@IM+Kd=POs`I{!hjbh!0ZF1gHo;>SgqeU z6^X53=^{-hBkOzr^T8V= z^-0O_R=-}6YnXgRI63-U%c`n> zy}PG1b&os(CzXI}RNK5P~bN&P@uEc>d*QrIoew-aRPj zdmiW9wSgzq)aI6Rf4f`Nu5gQdA8oaSNF`}UOJ){tI@{KoOk*9s%7Q6*Ljf^u92Ows}F=QS@F~-wkiFn z9Y#ZXHcZTS`vh@O3)y!&4N^WRHw{j4yhb3MFV<^@L&HbW)Kt-+}D%F6N0e{RMY z6+^&yAtlalvP0im7G?t|&AlguCyV;0gkaFC08YCzbq_waj7;%HZT~YAXtspEofDTZ zj_2N9SP+SS350*G>sQ-nTD^Lvp$6D(q20OA{lgcY?>%Et?{7tojU2fB+B1#cB&wN8 z26~X|*Zn3XI5z)@YP--J5G>6^_=fJ+qRE!n{DUHKV~Yau988tc z_h6};z8>VN1{OwjJnW<2jYUo4N@cVh-*lR0F7Jc$C?QoZXy(%1*_hAbG76^UeV^p_ zFFg*+Z|Fc(6yK+^pZ-!5_TK7L2ZUky5RjPvaZS_e-j_yntj$7<=R!IV&B(%GKR}^< zv;oUr;s9#PPmdxyBtW?;SiKeNp0J0iuQ-x%>G3T6F9$(6LK1gSz3o>lE?*nP#GavA z$K0Kb^+Hc5qdD=l7IU5V&pyrTC+!-^U= z?fmyco$vkZi{|NA26oes*M@`QbMSjf>ja8y^`i;V34;@vT5uI_O2no^+plfZu{GrU z2H;M*^~5rN{%hLE;EmO^;a9m9-STk&s7sWSDa$37H-e6YOr8oj3w4ZK@gA5$=1xb^ znqJh3hZ(pkiqoktD@yI2+*?E-q- z*!cEUaW}_NRk8EC68LM;bp$KY&qO_QYfOc4P#ZR5gecZkUV8jSj73xRb0R5L&2hDz z>LX3(Wz&tj;Ar^d-QTh_VmiB$?vK>JJ;lRVgS=EIU(m7B963p=`Z3Q zt9AV_t&CVZ)Eud(XUaw2^HbpAcCbM8Ible_ZuT?Z!^ZhOAVbI)gWWd(VZ`20E= zL1Jpx?bkL}I3b+(&QQ@h#|A{@-F%v$!cy20Latxj9~R#?aS`jHwLsW&BdQ$kZrFzT z?tSVBO+e){x0gd`Nf(<>P(Lhr{G%saYqMQ?I%4Tj_M>Mh{kDeYx^h5{&=JDwvCom^ zJzCgKwg`5+gy=tA_w1y(qc5VF51Tc}jCJ)>A2tojJBijVE9+SSUaN&1-CV&^Ksk~k zDU|GV-al+t1`{xnhS!a>x360J1ZQuf!AURnAa{IA)A(DXA}xvn=hIw~v#$Y9ic7X4 zI96S&VxJU=Fo#qjwm#dOOFC1Z<#&rQ zd%8;SCpC|FV{n~J>F)K9t~$~kj$WuP36VW@q{Fbdkqq?(-Fc=w_Aq)4CtBZyr- zrJ%hr*Cx^DZWf=jXsxqe@CH=L$0-%GN~qr1vYQmWD`jAJ+TA1<6KyZut&lB0w9|7D zdSUnLnRqe7T0`+`2Vn%Q52}3=K?I@42gs|@c#X0bYxS1tIORWdt3<|F(GMhq5h#9C zp&9$URy!YOZFY^s7#NCCek-vn-8$KQPj$Ka5i_gq+3I@_&>?SZ*X->1LY<7?xMsG zAH*y86(fw3bOc%-7bwrcDjP~8UN^yKE%B1bd^A#3YWwVqHCeAP*Dc|21aoSGlI;;$*1oZ6GrwUL z1|fJ=jkafet~3=G=$gbg&%VIqdbpiNN`BwN6VwaP z_!h+b>E)Ghzkv=&MdO$K-b67FZX?U0G2!J*DKFMe=nGJyJD|0(L2 z*vwistQA*lIv1rU+9UOxxofj7Uj%aE0jD~^#;eVoqjH08jEQo3=UP*+jksVEre6t4 zet_D#U{0l1LY^~M?V;xfa_&+<1yGBd5%jw*lD2y=7tS>dIU5&7*}=0ab^U+`gYrz# zSZ}&zBlk|T>Ho&aN$HoC#iR{jqxa$M1k;6{Tmh5Tk!s*YsKb&rQb$wBV+a4XdK&#b`gg(q! z?S-jy6(i2B3`zabt=fLW2IXPes7|3~KVS4wCsUvVcT^{In{y6*4HV$RE$F?U^j>f_ zzTQ5jM?N^J+(Di8!sQjzu)Mdgc3@~eM!UB>0lP&QApYH`Ioc&?j>?mX+R4LEaL16i zUt|W^2zC;9lcc;Mj!)7!)~2Ar{UK~OU4%A)+dv?Dyfla|kaif4{uz)r=umnw2uK9l zPvC}(M$O}IMwM?vZfYD)y*xqP2@QRY(qC>nx*!q^C3eh|J(WC{wIh-K)B{@>6n%wAO;>;(}b zc;BR>$4;69sdW?;N*2Rsk(g7e5(*h9VRG)_htRsk!y@T>BOiMg5lP`V4_AvIvoyXy z!1NSaQRC_bY!rjHXz3Nm9M_wvxMOR)?k9zr!Wa}dCsCuiBz z-l)vhtZl%DoI}6qhC)5>dXaYArAKurMO-LCQG7L=`!OHaMB)G?4I@eeZ*(xJ1t*DS zNc|!R>Kg|0GIW;Kf$@q&173uBpLNEkVrEhIvv^Ff|#MApstRL60S8w$TvMBqEGe`2%^zreP(_AB909im3DP`!jj? zDf=NJ5!r}H8fJICZ_X+jCNjqN$`HO1St4GVsxVB6AT)SM)l|+YKNSW0=o!)iy2Xmo zK0`+~18SZ^HZV~86ZgfTcccH5J zL)pY+_(THN&KLL(Ni@A#xQ#+oZ>&X5sy0Mo*&XHCI1T0Wkm+>j1b%Dk$wKqJS{X$m z2iZTp02z)_AF`mczodqV{EjfB|9X+8seUx$0Smr6R(y|KoSC&F zo2r+?Ku|NV>zFHgqOxLV#%JRKEfg{x`=0zu$-WNTjaH2jX zE=)pL06HGt`JpfU<_f-~>YbU&d!(1=`J(>9p>aHt8Au&eKv0_;dW9s596N&k2 zoJfV3G9lZT5InYIZiaBj%|SF^tRT5%BisLV z$o)ls4JZI#572URXu2;nNGwgy^2#n0GK!h9M>pR=JFl$MWcNM;dpKW;;<6V?sN^K=4AEE%dk=p}kD z2bXaDuq+|F3rduP0lVeSS*kli<-Z;_{Pf6w72{LetAzfeD%DGzG|dqw9mBY=TT2h4 zgud#)?xNQlzL)dIa1*}8#8+Ko^G4&mF>KxjHV@Y>?T}$KinL+qnMpiyRma#gCngR7 zw%f?2v4wzz>oqoVX%`Luy_gcJrLcki5Sspj(dHgWG~d}#k!)cr!!#5&6esV6mmIKf zLs#lB)kc*2b+6#;FCl~J`xNwUxqz`f1D&Uxfs^S~JM40rJBj?TBf0{T!h~9{~5iv7w;NXzj?@DKPu6=@IV#5WQtrKrGa_uKuf>2 z02LRpk$RDO@GAcMKy4hoog@BTkl@dqSunym^m2gmOwS{Wn6tk!rPaWnUd0pP12tmM z1$A7MzW{aQbc5T|p~xBZ{)=ckZnoNt<<5>J#VD*&TDEMmuhFFuX4>D3J&UB>7!iAa zMHubiZ3}E4^$OL%OAPX#?!oRqQ$_G-PWJwmLjlJ}4WjbJ*k)&VGG-~gw894*0OBmY ziy?OO4Fl-9qGNtzl7GfeAE#m2@c&NI9uNDfh*wCtk5RJlgD=>D75%foiUptOqYkVB zL$|*f!gp&20{to0!x_WQ9$i3zo`&Z-|9>NZZ6OVt77dXLc4juRPTzd>>*-)J^@Puj zz||JZ|1+exc6ylB@NX>EW9`m*sHC#tVTvHmI*{3r%kUYd0mf z#%R#|*jSjr5`spB^4cQwE6&Kfh2~=TiZixjEMvH4U7&Y+B!*M5+@PZApB2KOyjG4O zhrvL*yQ7jJJ1b9p0Xhpk5R~6sFmN!^DbYkES1&`Tkc9SfO@F>dIc{Mj$LS82acNI- zsU68H{Aiu+Qh6{f&L2?Ei=Cmna=nhYF=@}Jj44MF>bmFkjX@|s4T8zot)lk z_Cd4S(^C?TK@G0$BN$9?&?pOPH~nKq(O=A$iiYC)Gqkey`X9PL7gL@5{xe;B1QTTO zeJCD>I{bIv!#Ec%3>4uv(^c1r1_<*2{NU{17t<4daKikN-sliaYhyS4^^dc~(#FDW zG=lWTe?Eh8NN4>|0kn^C@<7+~IsdHzB>$|Tk%5sc;|%qBG1dn<{?9^ \ No newline at end of file diff --git a/frontend/src/assets/status/403.svg b/frontend/src/assets/status/403.svg new file mode 100755 index 0000000..ba3ce29 --- /dev/null +++ b/frontend/src/assets/status/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/status/404.svg b/frontend/src/assets/status/404.svg new file mode 100755 index 0000000..aacb740 --- /dev/null +++ b/frontend/src/assets/status/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/status/500.svg b/frontend/src/assets/status/500.svg new file mode 100755 index 0000000..ea23a37 --- /dev/null +++ b/frontend/src/assets/status/500.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/back_top.svg b/frontend/src/assets/svg/back_top.svg new file mode 100755 index 0000000..f8e6aa0 --- /dev/null +++ b/frontend/src/assets/svg/back_top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/dark.svg b/frontend/src/assets/svg/dark.svg new file mode 100755 index 0000000..b5c4d2d --- /dev/null +++ b/frontend/src/assets/svg/dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/day.svg b/frontend/src/assets/svg/day.svg new file mode 100755 index 0000000..b760034 --- /dev/null +++ b/frontend/src/assets/svg/day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/enter_outlined.svg b/frontend/src/assets/svg/enter_outlined.svg new file mode 100755 index 0000000..ab4f9b6 --- /dev/null +++ b/frontend/src/assets/svg/enter_outlined.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/exit_screen.svg b/frontend/src/assets/svg/exit_screen.svg new file mode 100755 index 0000000..c431a05 --- /dev/null +++ b/frontend/src/assets/svg/exit_screen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/full_screen.svg b/frontend/src/assets/svg/full_screen.svg new file mode 100755 index 0000000..b7452e4 --- /dev/null +++ b/frontend/src/assets/svg/full_screen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/keyboard_esc.svg b/frontend/src/assets/svg/keyboard_esc.svg new file mode 100755 index 0000000..e128594 --- /dev/null +++ b/frontend/src/assets/svg/keyboard_esc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/system.svg b/frontend/src/assets/svg/system.svg new file mode 100755 index 0000000..9ad39a5 --- /dev/null +++ b/frontend/src/assets/svg/system.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/table-bar/collapse.svg b/frontend/src/assets/table-bar/collapse.svg new file mode 100755 index 0000000..0823ae6 --- /dev/null +++ b/frontend/src/assets/table-bar/collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/table-bar/drag.svg b/frontend/src/assets/table-bar/drag.svg new file mode 100755 index 0000000..f477f16 --- /dev/null +++ b/frontend/src/assets/table-bar/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/table-bar/expand.svg b/frontend/src/assets/table-bar/expand.svg new file mode 100755 index 0000000..bb41c35 --- /dev/null +++ b/frontend/src/assets/table-bar/expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/table-bar/refresh.svg b/frontend/src/assets/table-bar/refresh.svg new file mode 100755 index 0000000..140288c --- /dev/null +++ b/frontend/src/assets/table-bar/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/table-bar/settings.svg b/frontend/src/assets/table-bar/settings.svg new file mode 100755 index 0000000..4ecd077 --- /dev/null +++ b/frontend/src/assets/table-bar/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/user.jpg b/frontend/src/assets/user.jpg new file mode 100755 index 0000000000000000000000000000000000000000..a2973ace3367cf7181b470e2814db5a9c06a4533 GIT binary patch literal 3694 zcmV-!4w3OvNk&Fy4gdgGMM6+kP&go34gdfUJpi2nDxd(M06vjGnMtLiq9G@=4G6Fj z31%=YJ_J#Mqzynh+5qkiBt9n)6vG`w5 z59Yt%zw-M=|Ci+3>i=N>Ew5u=yB^RVw5UFe3RiK5ppxFI5hZzrgl1&wg-n3c))Y%AQ zztZ0A)fc1!SiK%ISX~$|)-hs;X=zAAAD@Z*gWVgzm)%g1+`$}ptJ-~!RpF(M+D9~| zyzKiw5rIMAM4W$##xF)FI@aAEmGoC*qlTH*Y#7}@iW_duD(>JV*{zEr22N+LV{FP<1qq}&(tt4p3;yxmOQ!w+`1j={ETfnRl zb*POc9Om`L-8@f+Gk(-A2)n%p1tC~&oQ?yQ38vTpU!MQ)ux#BxhN6FS+Ip3+kZ zo)vP~0RH)Kb6xxU_v?P7m=(NjXJ{Qq)1;v4mM+fSM86gKRbTlS-Q*o$`VsB;freYXMC&Fq|O+`H_g4W7j#I156=zk-lyK(W;g4Pr^fJEUsrmG@~M< z8YJKtA)lr1dC%-VDU&Z@=;T7n$wm_N*ypE#R{lf>W{F^Cc+%9AvK*ifnV3I72I=~y zrX$rW(zQAbZygSnMX7Y;U5>deXIh31`1g-ozrKYzNYUp04RlM>U>Ij}<*ZZho!<05 zYmb%3O)I4=oaZb8Dw2p_yb?=pr8GA){5J)#o6zoeg0xoY9UXILJq>qq^Hr+Y5_Yag zId(Fll_vTlz>?58^}uZksfi)kv{%Pol8y;#(Z3Qjz(#GqGs97bk8XnB4BC(unp=DF zwb@#fdhkZHt?382*X9i4H3( zP%33VLUkL7<7cB&pEAN|2jiP_KpfK{BwK(ZQ;sRM)qUC5H(1&2goq|LPtRZv0uO}(WVRdjlr->H4e|s$?c(t= z0Y+*WC>B3@x61{)IcDz!K7qehBk6sfXnf(5`f*hK;TvIM#Isf=jLMsYL;sj$x!2Y8 z{cbjG*LdoeU*lwo%hL0ALneG>2>`vmeQ(IQ^#t3emMwCnq#_gAJ@P0XY1%L2vwB1J z^X|s=t%IgV-(Qy%kXc93u`9Dk&2Ls3i4mGtR(DUGY_(Zm{2##iZJ{CzQ@OnEfM{Qo ze#f+W8o~d;onaf71-BcWY!b=oo`-r;Nq_}|8ehGiG5*utgI7MMek4CM1hxkxXrlORW=5OfHu(6?{I)XQg87b!kL2!Z zukGavgW)KJ+F`?&ZR$T>*<|}e=^$Y)*j|G&9bU3GMX6GxjN?uRKFarnJ1Ii3(q-FM*cM~QMOv{tbAPN}BsP9Y?3ec^(|(8D>aYFf8<+-HrcvmwDl-?u-Vp zJ|3KvuNMo~Fj-xr9OYGmPg6QI~cU=+S4PG9EoZLla zf~s<=?i7=OIk8oX2%pFnZz4v}0d_Yy_WSVi&tWnP*o6YH(vSfzlb$(ZTy2RS$T+)f z(5r!}zZSDj4mr^-J1`2Yp{${_`5%})c>iJ5iW)1!>1mFAFGx z;B;)cd(7#6vu0zB3Qiz&wE9A&QQy8+YwetHC?0#?zR%)|rIuC^P(^NcLGFLLWvrwT ziSba^I7L^ceM_vWH_7yL!}bLW3DNWJjqs&HWJa>c6e!xK__eysks1GnQI z=f_v}Bj_px0`6cV_hwtJ5)QfC)>1IDGb$J>Vy9^nQaj04!pnQu2D>_Ozw$GD}C-N_mEcRS!<`}D?FWA;{I4@mjmo4iX`{bVhu`N5_P7$ z$*T<)7`5sHvXa}65d`e%prY8yx~xO-i9`QIz@F?0o2dtu#R1kNto!5Rr&u4`(XmBy zEWzm3Sj90g7xrfDLt|ZF4$qYzc0Sq}*PSHT6JDm}trLU{TeU&6#Fqam6qK9*P z=N#HV3O9LNdF|4Kk}aJkxHdJT#X4I;oHW$da#uo4OO6Srw>$?F!``-RN^mw_Yxw1o zxo`|bmxWItG&jIDM3j3qsV{jks(QdvNM(AEOv@?eVx=4w8)k6rnBFTQh?@{8W=HvQ zk>-?8)CHGVg;;uXr{F^8={SY<_^v;05dcUlv@?V@0}i&WN8? z@E5~EBta9?8mDw9)qrV$3OL(`wg?`sH$8rc1(LIi5)6y=vR_*$r<4r|;t)!_qCNEp zF}2c#-d4enAZYOlHcS0MkH5V_;>cd~ETx8-fJK#EXWSaQg!I}N`C=~yR^_y3!@Duh z(oAswXyBc3&2Q>c31feA8*p;L0a89nQ9)??NeirTT=;C8ExeBxwgMzTBU5Pr#G_C} zGE9$H@qU}U)DgzCagb1$xUof9d-id5Bs6=`!!BN#>C#n?d6;ksZSVCO-%~UvJ&#_O z7K93jU4giQlw`I7QvtbdUa&p-I;*l@#2YW?%Ku7!?Gt*r8+9vL{`Ga=mN)Kn1Dh}1 zFFK$=E^uzUi^92o+^tUHv9BwVxin&)&?V)_I#1gC?F^CYC}ZI^4rPOaD>`V-8W`mJ zj)FqAei|k^JBZFZDI5_r$N{6To8A>=DE(nmG4eZ z0Oco+JGokWUkHtBtHHGj|L+S#dTpwVov{G}q#pQd^(dy7@qE`=a28_{C^L8vH zbFj1Kh!|RsnqvSfIg*Y=_P5~OclqFwGl`?fv>G@;%KOeL5AjlIPzeTbrk;ru(H1L0 zd;Isk3}O=|1XHS9xWfK3LT8g=)b>X@PjwP=3)n^4FB>`%!VQ>9+IHe-L z!`KUQU$KxlEY<-h#RJ_gB9-EV)mBh34C%_DsmSnql4?BtvY8Hl^pqD(!_cnWV@K}M zYr8t?Tlh}AngVOKjWx9G3uu{pb{O&yFosG{7Fnp+`9aBx?GId7yL=665}>Y zbn=*9Gkfa|B$iYPJv{zfO!NZx9NZ9*AXlhxm=t>4pfj{%eUw~!G)t0L4-lj)}T~z zuzBX_ { + if (!slots) return null; + return hasAuth(props.value) ? ( + {slots.default?.()} + ) : null; + }; + } +}); diff --git a/frontend/src/components/ReCol/index.ts b/frontend/src/components/ReCol/index.ts new file mode 100755 index 0000000..bdf5aa7 --- /dev/null +++ b/frontend/src/components/ReCol/index.ts @@ -0,0 +1,29 @@ +import { ElCol } from "element-plus"; +import { h, defineComponent } from "vue"; + +// 封装element-plus的el-col组件 +export default defineComponent({ + name: "ReCol", + props: { + value: { + type: Number, + default: 24 + } + }, + render() { + const attrs = this.$attrs; + const val = this.value; + return h( + ElCol, + { + xs: val, + sm: val, + md: val, + lg: val, + xl: val, + ...attrs + }, + { default: () => this.$slots.default() } + ); + } +}); diff --git a/frontend/src/components/ReDialog/index.ts b/frontend/src/components/ReDialog/index.ts new file mode 100755 index 0000000..fd6073f --- /dev/null +++ b/frontend/src/components/ReDialog/index.ts @@ -0,0 +1,69 @@ +import { ref } from "vue"; +import reDialog from "./index.vue"; +import { useTimeoutFn } from "@vueuse/core"; +import { withInstall } from "@pureadmin/utils"; +import type { + EventType, + ArgsType, + DialogProps, + ButtonProps, + DialogOptions +} from "./type"; + +const dialogStore = ref>([]); + +/** 打开弹框 */ +const addDialog = (options: DialogOptions) => { + const open = () => + dialogStore.value.push(Object.assign(options, { visible: true })); + if (options?.openDelay) { + useTimeoutFn(() => { + open(); + }, options.openDelay); + } else { + open(); + } +}; + +/** 关闭弹框 */ +const closeDialog = (options: DialogOptions, index: number, args?: any) => { + dialogStore.value[index].visible = false; + options.closeCallBack && options.closeCallBack({ options, index, args }); + + const closeDelay = options?.closeDelay ?? 200; + useTimeoutFn(() => { + dialogStore.value.splice(index, 1); + }, closeDelay); +}; + +/** + * @description 更改弹框自身属性值 + * @param value 属性值 + * @param key 属性,默认`title` + * @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`) + */ +const updateDialog = (value: any, key = "title", index = 0) => { + dialogStore.value[index][key] = value; +}; + +/** 关闭所有弹框 */ +const closeAllDialog = () => { + dialogStore.value = []; +}; + +/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载 + * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4 + * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L12 + * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22 + */ +const ReDialog = withInstall(reDialog); + +export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions }; +export { + ReDialog, + dialogStore, + addDialog, + closeDialog, + updateDialog, + closeAllDialog +}; diff --git a/frontend/src/components/ReDialog/index.vue b/frontend/src/components/ReDialog/index.vue new file mode 100755 index 0000000..a51b787 --- /dev/null +++ b/frontend/src/components/ReDialog/index.vue @@ -0,0 +1,206 @@ + + + diff --git a/frontend/src/components/ReDialog/type.ts b/frontend/src/components/ReDialog/type.ts new file mode 100755 index 0000000..d477ee1 --- /dev/null +++ b/frontend/src/components/ReDialog/type.ts @@ -0,0 +1,275 @@ +import type { CSSProperties, VNode, Component } from "vue"; + +type DoneFn = (cancel?: boolean) => void; +type EventType = + | "open" + | "close" + | "openAutoFocus" + | "closeAutoFocus" + | "fullscreenCallBack"; +type ArgsType = { + /** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */ + command: "cancel" | "sure" | "close"; +}; +type ButtonType = + | "primary" + | "success" + | "warning" + | "danger" + | "info" + | "text"; + +/** https://element-plus.org/zh-CN/component/dialog.html#attributes */ +type DialogProps = { + /** `Dialog` 的显示与隐藏 */ + visible?: boolean; + /** `Dialog` 的标题 */ + title?: string; + /** `Dialog` 的宽度,默认 `50%` */ + width?: string | number; + /** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */ + fullscreen?: boolean; + /** 是否显示全屏操作图标,默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */ + fullscreenIcon?: boolean; + /** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */ + top?: string; + /** 是否需要遮罩层,默认 `true` */ + modal?: boolean; + /** `Dialog` 自身是否插入至 `body` 元素上。嵌套的 `Dialog` 必须指定该属性并赋值为 `true`,默认 `false` */ + appendToBody?: boolean; + /** 是否在 `Dialog` 出现时将 `body` 滚动锁定,默认 `true` */ + lockScroll?: boolean; + /** `Dialog` 的自定义类名 */ + class?: string; + /** `Dialog` 的自定义样式 */ + style?: CSSProperties; + /** `Dialog` 打开的延时时间,单位毫秒,默认 `0` */ + openDelay?: number; + /** `Dialog` 关闭的延时时间,单位毫秒,默认 `0` */ + closeDelay?: number; + /** 是否可以通过点击 `modal` 关闭 `Dialog`,默认 `true` */ + closeOnClickModal?: boolean; + /** 是否可以通过按下 `ESC` 关闭 `Dialog`,默认 `true` */ + closeOnPressEscape?: boolean; + /** 是否显示关闭按钮,默认 `true` */ + showClose?: boolean; + /** 关闭前的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */ + beforeClose?: (done: DoneFn) => void; + /** 为 `Dialog` 启用可拖拽功能,默认 `false` */ + draggable?: boolean; + /** 是否让 `Dialog` 的 `header` 和 `footer` 部分居中排列,默认 `false` */ + center?: boolean; + /** 是否水平垂直对齐对话框,默认 `false` */ + alignCenter?: boolean; + /** 当关闭 `Dialog` 时,销毁其中的元素,默认 `false` */ + destroyOnClose?: boolean; +}; + +//element-plus.org/zh-CN/component/popconfirm.html#attributes +type Popconfirm = { + /** 标题 */ + title?: string; + /** 确定按钮文字 */ + confirmButtonText?: string; + /** 取消按钮文字 */ + cancelButtonText?: string; + /** 确定按钮类型,默认 `primary` */ + confirmButtonType?: ButtonType; + /** 取消按钮类型,默认 `text` */ + cancelButtonType?: ButtonType; + /** 自定义图标,默认 `QuestionFilled` */ + icon?: string | Component; + /** `Icon` 颜色,默认 `#f90` */ + iconColor?: string; + /** 是否隐藏 `Icon`,默认 `false` */ + hideIcon?: boolean; + /** 关闭时的延迟,默认 `200` */ + hideAfter?: number; + /** 是否将 `popover` 的下拉列表插入至 `body` 元素,默认 `true` */ + teleported?: boolean; + /** 当 `popover` 组件长时间不触发且 `persistent` 属性设置为 `false` 时, `popover` 将会被删除,默认 `false` */ + persistent?: boolean; + /** 弹层宽度,最小宽度 `150px`,默认 `150` */ + width?: string | number; +}; + +type BtnClickDialog = { + options?: DialogOptions; + index?: number; +}; +type BtnClickButton = { + btn?: ButtonProps; + index?: number; +}; +/** https://element-plus.org/zh-CN/component/button.html#button-attributes */ +type ButtonProps = { + /** 按钮文字 */ + label: string; + /** 按钮尺寸 */ + size?: "large" | "default" | "small"; + /** 按钮类型 */ + type?: "primary" | "success" | "warning" | "danger" | "info"; + /** 是否为朴素按钮,默认 `false` */ + plain?: boolean; + /** 是否为文字按钮,默认 `false` */ + text?: boolean; + /** 是否显示文字按钮背景颜色,默认 `false` */ + bg?: boolean; + /** 是否为链接按钮,默认 `false` */ + link?: boolean; + /** 是否为圆角按钮,默认 `false` */ + round?: boolean; + /** 是否为圆形按钮,默认 `false` */ + circle?: boolean; + /** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */ + popconfirm?: Popconfirm; + /** 是否为加载中状态,默认 `false` */ + loading?: boolean; + /** 自定义加载中状态图标组件 */ + loadingIcon?: string | Component; + /** 按钮是否为禁用状态,默认 `false` */ + disabled?: boolean; + /** 图标组件 */ + icon?: string | Component; + /** 是否开启原生 `autofocus` 属性,默认 `false` */ + autofocus?: boolean; + /** 原生 `type` 属性,默认 `button` */ + nativeType?: "button" | "submit" | "reset"; + /** 自动在两个中文字符之间插入空格 */ + autoInsertSpace?: boolean; + /** 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 */ + color?: string; + /** `dark` 模式, 意味着自动设置 `color` 为 `dark` 模式的颜色,默认 `false` */ + dark?: boolean; + /** 自定义元素标签 */ + tag?: string | Component; + /** 点击按钮后触发的回调 */ + btnClick?: ({ + dialog, + button + }: { + /** 当前 `Dialog` 信息 */ + dialog: BtnClickDialog; + /** 当前 `button` 信息 */ + button: BtnClickButton; + }) => void; +}; + +interface DialogOptions extends DialogProps { + /** 内容区组件的 `props`,可通过 `defineProps` 接收 */ + props?: any; + /** 是否隐藏 `Dialog` 按钮操作区的内容 */ + hideFooter?: boolean; + /** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */ + popconfirm?: Popconfirm; + /** 点击确定按钮后是否开启 `loading` 加载动画 */ + sureBtnLoading?: boolean; + /** + * @description 自定义对话框标题的内容渲染器 + * @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8} + */ + headerRenderer?: ({ + close, + titleId, + titleClass + }: { + close: Function; + titleId: string; + titleClass: string; + }) => VNode | Component; + /** 自定义内容渲染器 */ + contentRenderer?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => VNode | Component; + /** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */ + footerRenderer?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => VNode | Component; + /** 自定义底部按钮操作 */ + footerButtons?: Array; + /** `Dialog` 打开后的回调 */ + open?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发) */ + close?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */ + closeCallBack?: ({ + options, + index, + args + }: { + options: DialogOptions; + index: number; + args: any; + }) => void; + /** 点击全屏按钮时的回调 */ + fullscreenCallBack?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** 输入焦点聚焦在 `Dialog` 内容时的回调 */ + openAutoFocus?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** 输入焦点从 `Dialog` 内容失焦时的回调 */ + closeAutoFocus?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** 点击底部取消按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */ + beforeCancel?: ( + done: Function, + { + options, + index + }: { + options: DialogOptions; + index: number; + } + ) => void; + /** 点击底部确定按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */ + beforeSure?: ( + done: Function, + { + options, + index, + closeLoading + }: { + options: DialogOptions; + index: number; + /** 关闭确定按钮的 `loading` 加载动画 */ + closeLoading: Function; + } + ) => void; +} + +export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions }; diff --git a/frontend/src/components/ReIcon/index.ts b/frontend/src/components/ReIcon/index.ts new file mode 100755 index 0000000..ec4ffc2 --- /dev/null +++ b/frontend/src/components/ReIcon/index.ts @@ -0,0 +1,12 @@ +import iconifyIconOffline from "./src/iconifyIconOffline"; +import iconifyIconOnline from "./src/iconifyIconOnline"; +import fontIcon from "./src/iconfont"; + +/** 本地图标组件 */ +const IconifyIconOffline = iconifyIconOffline; +/** 在线图标组件 */ +const IconifyIconOnline = iconifyIconOnline; +/** `iconfont`组件 */ +const FontIcon = fontIcon; + +export { IconifyIconOffline, IconifyIconOnline, FontIcon }; diff --git a/frontend/src/components/ReIcon/src/hooks.ts b/frontend/src/components/ReIcon/src/hooks.ts new file mode 100755 index 0000000..0306373 --- /dev/null +++ b/frontend/src/components/ReIcon/src/hooks.ts @@ -0,0 +1,63 @@ +import type { iconType } from "./types"; +import { h, defineComponent, type Component } from "vue"; +import { FontIcon, IconifyIconOnline, IconifyIconOffline } from "../index"; + +/** + * 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标 + * @see 点击查看文档图标篇 {@link https://pure-admin.cn/pages/icon/} + * @param icon 必传 图标 + * @param attrs 可选 iconType 属性 + * @returns Component + */ +export function useRenderIcon(icon: any, attrs?: iconType): Component { + // iconfont + const ifReg = /^IF-/; + // typeof icon === "function" 属于SVG + if (ifReg.test(icon)) { + // iconfont + const name = icon.split(ifReg)[1]; + const iconName = name.slice( + 0, + name.indexOf(" ") == -1 ? name.length : name.indexOf(" ") + ); + const iconType = name.slice(name.indexOf(" ") + 1, name.length); + return defineComponent({ + name: "FontIcon", + render() { + return h(FontIcon, { + icon: iconName, + iconType, + ...attrs + }); + } + }); + } else if (typeof icon === "function" || typeof icon?.render === "function") { + // svg + return attrs ? h(icon, { ...attrs }) : icon; + } else if (typeof icon === "object") { + return defineComponent({ + name: "OfflineIcon", + render() { + return h(IconifyIconOffline, { + icon: icon, + ...attrs + }); + } + }); + } else { + // 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之 + return defineComponent({ + name: "Icon", + render() { + if (!icon) return; + const IconifyIcon = icon.includes(":") + ? IconifyIconOnline + : IconifyIconOffline; + return h(IconifyIcon, { + icon, + ...attrs + }); + } + }); + } +} diff --git a/frontend/src/components/ReIcon/src/iconfont.ts b/frontend/src/components/ReIcon/src/iconfont.ts new file mode 100755 index 0000000..d7e87e9 --- /dev/null +++ b/frontend/src/components/ReIcon/src/iconfont.ts @@ -0,0 +1,47 @@ +import { h, defineComponent } from "vue"; + +// 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code) +export default defineComponent({ + name: "FontIcon", + props: { + icon: { + type: String, + default: "" + } + }, + render() { + const attrs = this.$attrs; + if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") { + return h( + "i", + { + class: "iconfont", + ...attrs + }, + this.icon + ); + } else if ( + Object.keys(attrs).includes("svg") || + attrs?.iconType === "svg" + ) { + return h( + "svg", + { + class: "icon-svg" + }, + { + default: () => [ + h("use", { + "xlink:href": `#${this.icon}` + }) + ] + } + ); + } else { + return h("i", { + class: `iconfont ${this.icon}`, + ...attrs + }); + } + } +}); diff --git a/frontend/src/components/ReIcon/src/iconifyIconOffline.ts b/frontend/src/components/ReIcon/src/iconifyIconOffline.ts new file mode 100755 index 0000000..7eac8a8 --- /dev/null +++ b/frontend/src/components/ReIcon/src/iconifyIconOffline.ts @@ -0,0 +1,47 @@ +import { h, defineComponent } from "vue"; +import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline"; + +// Iconify Icon在Vue里本地使用(用于内网环境) +export default defineComponent({ + name: "IconifyIconOffline", + components: { IconifyIcon }, + props: { + icon: { + default: null + } + }, + render() { + if (typeof this.icon === "object") addIcon(this.icon, this.icon); + const attrs = this.$attrs; + if (typeof this.icon === "string") { + return h( + IconifyIcon, + { + icon: this.icon, + "aria-hidden": false, + style: attrs?.style + ? Object.assign(attrs.style, { outline: "none" }) + : { outline: "none" }, + ...attrs + }, + { + default: () => [] + } + ); + } else { + return h( + this.icon, + { + "aria-hidden": false, + style: attrs?.style + ? Object.assign(attrs.style, { outline: "none" }) + : { outline: "none" }, + ...attrs + }, + { + default: () => [] + } + ); + } + } +}); diff --git a/frontend/src/components/ReIcon/src/iconifyIconOnline.ts b/frontend/src/components/ReIcon/src/iconifyIconOnline.ts new file mode 100755 index 0000000..88d0315 --- /dev/null +++ b/frontend/src/components/ReIcon/src/iconifyIconOnline.ts @@ -0,0 +1,31 @@ +import { h, defineComponent } from "vue"; +import { Icon as IconifyIcon } from "@iconify/vue"; + +// Iconify Icon在Vue里在线使用(用于外网环境) +export default defineComponent({ + name: "IconifyIconOnline", + components: { IconifyIcon }, + props: { + icon: { + type: String, + default: "" + } + }, + render() { + const attrs = this.$attrs; + return h( + IconifyIcon, + { + icon: `${this.icon}`, + "aria-hidden": false, + style: attrs?.style + ? Object.assign(attrs.style, { outline: "none" }) + : { outline: "none" }, + ...attrs + }, + { + default: () => [] + } + ); + } +}); diff --git a/frontend/src/components/ReIcon/src/offlineIcon.ts b/frontend/src/components/ReIcon/src/offlineIcon.ts new file mode 100755 index 0000000..2e4042c --- /dev/null +++ b/frontend/src/components/ReIcon/src/offlineIcon.ts @@ -0,0 +1,23 @@ +// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 +import { getSvgInfo } from "@pureadmin/utils"; +import { addIcon } from "@iconify/vue/dist/offline"; + +// https://icon-sets.iconify.design/ep/?keyword=ep +import EpHomeFilled from "~icons/ep/home-filled?raw"; + +// https://icon-sets.iconify.design/ri/?keyword=ri +import RiSearchLine from "~icons/ri/search-line?raw"; +import RiInformationLine from "~icons/ri/information-line?raw"; + +const icons = [ + // Element Plus Icon: https://github.com/element-plus/element-plus-icons + ["ep/home-filled", EpHomeFilled], + // Remix Icon: https://github.com/Remix-Design/RemixIcon + ["ri/search-line", RiSearchLine], + ["ri/information-line", RiInformationLine] +]; + +// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标 +icons.forEach(([name, icon]) => { + addIcon(name as string, getSvgInfo(icon as string)); +}); diff --git a/frontend/src/components/ReIcon/src/types.ts b/frontend/src/components/ReIcon/src/types.ts new file mode 100755 index 0000000..92c5314 --- /dev/null +++ b/frontend/src/components/ReIcon/src/types.ts @@ -0,0 +1,20 @@ +export interface iconType { + // iconify (https://docs.iconify.design/icon-components/vue/#properties) + inline?: boolean; + width?: string | number; + height?: string | number; + horizontalFlip?: boolean; + verticalFlip?: boolean; + flip?: string; + rotate?: number | string; + color?: string; + horizontalAlign?: boolean; + verticalAlign?: boolean; + align?: string; + onLoad?: Function; + includes?: Function; + // svg 需要什么SVG属性自行添加 + fill?: string; + // all icon + style?: object; +} diff --git a/frontend/src/components/RePerms/index.ts b/frontend/src/components/RePerms/index.ts new file mode 100755 index 0000000..fe1a215 --- /dev/null +++ b/frontend/src/components/RePerms/index.ts @@ -0,0 +1,5 @@ +import perms from "./src/perms"; + +const Perms = perms; + +export { Perms }; diff --git a/frontend/src/components/RePerms/src/perms.tsx b/frontend/src/components/RePerms/src/perms.tsx new file mode 100755 index 0000000..a22ecce --- /dev/null +++ b/frontend/src/components/RePerms/src/perms.tsx @@ -0,0 +1,20 @@ +import { defineComponent, Fragment } from "vue"; +import { hasPerms } from "@/utils/auth"; + +export default defineComponent({ + name: "Perms", + props: { + value: { + type: undefined, + default: [] + } + }, + setup(props, { slots }) { + return () => { + if (!slots) return null; + return hasPerms(props.value) ? ( + {slots.default?.()} + ) : null; + }; + } +}); diff --git a/frontend/src/components/RePureTableBar/index.ts b/frontend/src/components/RePureTableBar/index.ts new file mode 100755 index 0000000..a4eb927 --- /dev/null +++ b/frontend/src/components/RePureTableBar/index.ts @@ -0,0 +1,5 @@ +import pureTableBar from "./src/bar"; +import { withInstall } from "@pureadmin/utils"; + +/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */ +export const PureTableBar = withInstall(pureTableBar); diff --git a/frontend/src/components/RePureTableBar/src/bar.tsx b/frontend/src/components/RePureTableBar/src/bar.tsx new file mode 100755 index 0000000..78e68e7 --- /dev/null +++ b/frontend/src/components/RePureTableBar/src/bar.tsx @@ -0,0 +1,393 @@ +import Sortable from "sortablejs"; +import { useEpThemeStoreHook } from "@/store/modules/epTheme"; +import { + type PropType, + ref, + unref, + computed, + nextTick, + defineComponent, + getCurrentInstance +} from "vue"; +import { + delay, + cloneDeep, + isBoolean, + isFunction, + getKeyList +} from "@pureadmin/utils"; + +import Fullscreen from "~icons/ri/fullscreen-fill"; +import ExitFullscreen from "~icons/ri/fullscreen-exit-fill"; +import DragIcon from "@/assets/table-bar/drag.svg?component"; +import ExpandIcon from "@/assets/table-bar/expand.svg?component"; +import RefreshIcon from "@/assets/table-bar/refresh.svg?component"; +import SettingIcon from "@/assets/table-bar/settings.svg?component"; +import CollapseIcon from "@/assets/table-bar/collapse.svg?component"; + +const props = { + /** 头部最左边的标题 */ + title: { + type: String, + default: "列表" + }, + /** 对于树形表格,如果想启用展开和折叠功能,传入当前表格的ref即可 */ + tableRef: { + type: Object as PropType + }, + /** 需要展示的列 */ + columns: { + type: Array as PropType, + default: () => [] + }, + isExpandAll: { + type: Boolean, + default: true + }, + tableKey: { + type: [String, Number] as PropType, + default: "0" + } +}; + +export default defineComponent({ + name: "PureTableBar", + props, + emits: ["refresh", "fullscreen"], + setup(props, { emit, slots, attrs }) { + const size = ref("default"); + const loading = ref(false); + const checkAll = ref(true); + const isFullscreen = ref(false); + const isIndeterminate = ref(false); + const instance = getCurrentInstance()!; + const isExpandAll = ref(props.isExpandAll); + const filterColumns = cloneDeep(props?.columns).filter(column => + isBoolean(column?.hide) + ? !column.hide + : !(isFunction(column?.hide) && column?.hide()) + ); + let checkColumnList = getKeyList(cloneDeep(props?.columns), "label"); + const checkedColumns = ref(getKeyList(cloneDeep(filterColumns), "label")); + const dynamicColumns = ref(cloneDeep(props?.columns)); + + const getDropdownItemStyle = computed(() => { + return s => { + return { + background: + s === size.value ? useEpThemeStoreHook().epThemeColor : "", + color: s === size.value ? "#fff" : "var(--el-text-color-primary)" + }; + }; + }); + + const iconClass = computed(() => { + return [ + "text-black", + "dark:text-white", + "duration-100", + "hover:text-primary!", + "cursor-pointer", + "outline-hidden" + ]; + }); + + const topClass = computed(() => { + return [ + "flex", + "justify-between", + "pt-[3px]", + "px-[11px]", + "border-b-[1px]", + "border-solid", + "border-[#dcdfe6]", + "dark:border-[#303030]" + ]; + }); + + function onReFresh() { + loading.value = true; + emit("refresh"); + delay(500).then(() => (loading.value = false)); + } + + function onExpand() { + isExpandAll.value = !isExpandAll.value; + toggleRowExpansionAll(props.tableRef.data, isExpandAll.value); + } + + function onFullscreen() { + isFullscreen.value = !isFullscreen.value; + emit("fullscreen", isFullscreen.value); + } + + function toggleRowExpansionAll(data, isExpansion) { + data.forEach(item => { + props.tableRef.toggleRowExpansion(item, isExpansion); + if (item.children !== undefined && item.children !== null) { + toggleRowExpansionAll(item.children, isExpansion); + } + }); + } + + function handleCheckAllChange(val: boolean) { + checkedColumns.value = val ? checkColumnList : []; + isIndeterminate.value = false; + dynamicColumns.value.map(column => + val ? (column.hide = false) : (column.hide = true) + ); + } + + function handleCheckedColumnsChange(value: string[]) { + checkedColumns.value = value; + const checkedCount = value.length; + checkAll.value = checkedCount === checkColumnList.length; + isIndeterminate.value = + checkedCount > 0 && checkedCount < checkColumnList.length; + } + + function handleCheckColumnListChange(val: boolean, label: string) { + dynamicColumns.value.filter(item => item.label === label)[0].hide = !val; + } + + async function onReset() { + checkAll.value = true; + isIndeterminate.value = false; + dynamicColumns.value = cloneDeep(props?.columns); + checkColumnList = []; + checkColumnList = await getKeyList(cloneDeep(props?.columns), "label"); + checkedColumns.value = getKeyList(cloneDeep(filterColumns), "label"); + } + + const dropdown = { + dropdown: () => ( + + (size.value = "large")} + > + 宽松 + + (size.value = "default")} + > + 默认 + + (size.value = "small")} + > + 紧凑 + + + ) + }; + + /** 列展示拖拽排序 */ + const rowDrop = (event: { preventDefault: () => void }) => { + event.preventDefault(); + nextTick(() => { + const wrapper: HTMLElement = ( + instance?.proxy?.$refs[`GroupRef${unref(props.tableKey)}`] as any + ).$el.firstElementChild; + Sortable.create(wrapper, { + animation: 300, + handle: ".drag-btn", + onEnd: ({ newIndex, oldIndex, item }) => { + const targetThElem = item; + const wrapperElem = targetThElem.parentNode as HTMLElement; + const oldColumn = dynamicColumns.value[oldIndex]; + const newColumn = dynamicColumns.value[newIndex]; + if (oldColumn?.fixed || newColumn?.fixed) { + // 当前列存在fixed属性 则不可拖拽 + const oldThElem = wrapperElem.children[oldIndex] as HTMLElement; + if (newIndex > oldIndex) { + wrapperElem.insertBefore(targetThElem, oldThElem); + } else { + wrapperElem.insertBefore( + targetThElem, + oldThElem ? oldThElem.nextElementSibling : oldThElem + ); + } + return; + } + const currentRow = dynamicColumns.value.splice(oldIndex, 1)[0]; + dynamicColumns.value.splice(newIndex, 0, currentRow); + } + }); + }); + }; + + const isFixedColumn = (label: string) => { + return dynamicColumns.value.filter(item => item.label === label)[0].fixed + ? true + : false; + }; + + const rendTippyProps = (content: string) => { + // https://vue-tippy.netlify.app/props + return { + content, + offset: [0, 18], + duration: [300, 0], + followCursor: true, + hideOnClick: "toggle" + }; + }; + + const reference = { + reference: () => ( + + ) + }; + + return () => ( + <> +
+
+ {slots?.title ? ( + slots.title() + ) : ( +

{props.title}

+ )} +
+ {slots?.buttons ? ( +
{slots.buttons()}
+ ) : null} + {props.tableRef?.size ? ( + <> + onExpand()} + /> + + + ) : null} + onReFresh()} + /> + + + + + + + +
+ handleCheckAllChange(value)} + /> + onReset()}> + 重置 + +
+ +
+ + handleCheckedColumnsChange(value)} + > + + {checkColumnList.map((item, index) => { + return ( +
+ void; + }) => rowDrop(event)} + /> + + handleCheckColumnListChange(value, item) + } + > + + {item} + + +
+ ); + })} +
+
+
+
+
+ + + onFullscreen()} + /> +
+
+ {slots.default({ + size: size.value, + dynamicColumns: dynamicColumns.value + })} +
+ + ); + } +}); diff --git a/frontend/src/components/ReSegmented/index.ts b/frontend/src/components/ReSegmented/index.ts new file mode 100755 index 0000000..9b3ae5a --- /dev/null +++ b/frontend/src/components/ReSegmented/index.ts @@ -0,0 +1,8 @@ +import reSegmented from "./src/index"; +import { withInstall } from "@pureadmin/utils"; + +/** 分段控制器组件 */ +export const ReSegmented = withInstall(reSegmented); + +export default ReSegmented; +export type { OptionsType } from "./src/type"; diff --git a/frontend/src/components/ReSegmented/src/index.css b/frontend/src/components/ReSegmented/src/index.css new file mode 100755 index 0000000..bbf5ae3 --- /dev/null +++ b/frontend/src/components/ReSegmented/src/index.css @@ -0,0 +1,156 @@ +.pure-segmented { + --pure-control-padding-horizontal: 12px; + --pure-control-padding-horizontal-sm: 8px; + --pure-segmented-track-padding: 2px; + --pure-segmented-line-width: 1px; + + --pure-segmented-border-radius-small: 4px; + --pure-segmented-border-radius-base: 6px; + --pure-segmented-border-radius-large: 8px; + + box-sizing: border-box; + display: inline-block; + padding: var(--pure-segmented-track-padding); + font-size: var(--el-font-size-base); + color: rgba(0, 0, 0, 0.65); + background-color: rgb(0 0 0 / 4%); + border-radius: var(--pure-segmented-border-radius-base); +} + +.pure-segmented-block { + display: flex; +} + +.pure-segmented-block .pure-segmented-item { + flex: 1; + min-width: 0; +} + +.pure-segmented-block .pure-segmented-item > .pure-segmented-item-label > span { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* small */ +.pure-segmented.pure-segmented--small { + border-radius: var(--pure-segmented-border-radius-small); +} +.pure-segmented.pure-segmented--small .pure-segmented-item { + border-radius: var(--el-border-radius-small); +} +.pure-segmented.pure-segmented--small .pure-segmented-item > div { + min-height: calc( + var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2 + ); + line-height: calc( + var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2 + ); + padding: 0 + calc( + var(--pure-control-padding-horizontal-sm) - + var(--pure-segmented-line-width) + ); +} + +/* large */ +.pure-segmented.pure-segmented--large { + border-radius: var(--pure-segmented-border-radius-large); +} +.pure-segmented.pure-segmented--large .pure-segmented-item { + border-radius: calc( + var(--el-border-radius-base) + var(--el-border-radius-small) + ); +} +.pure-segmented.pure-segmented--large .pure-segmented-item > div { + min-height: calc( + var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2 + ); + line-height: calc( + var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2 + ); + padding: 0 + calc( + var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width) + ); + font-size: var(--el-font-size-medium); +} + +/* default */ +.pure-segmented-item { + position: relative; + text-align: center; + cursor: pointer; + border-radius: var(--el-border-radius-base); + transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); +} +.pure-segmented .pure-segmented-item > div { + min-height: calc( + var(--el-component-size) - var(--pure-segmented-track-padding) * 2 + ); + line-height: calc( + var(--el-component-size) - var(--pure-segmented-track-padding) * 2 + ); + padding: 0 + calc( + var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width) + ); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.pure-segmented-group { + position: relative; + display: flex; + align-items: stretch; + justify-items: flex-start; + width: 100%; +} + +.pure-segmented-item-selected { + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; + display: none; + width: 0; + height: 100%; + padding: 4px 0; + background-color: #fff; + border-radius: 4px; + box-shadow: + 0 2px 8px -2px rgb(0 0 0 / 5%), + 0 1px 4px -1px rgb(0 0 0 / 7%), + 0 0 1px rgb(0 0 0 / 7%); + transition: + transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1), + width 0.5s cubic-bezier(0.645, 0.045, 0.355, 1); + will-change: transform, width; +} + +.pure-segmented-item > input { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; +} + +.pure-segmented-item-label { + display: flex; + align-items: center; + justify-content: center; +} + +.pure-segmented-item-icon svg { + width: 16px; + height: 16px; +} + +.pure-segmented-item-disabled { + color: rgba(0, 0, 0, 0.25); + cursor: not-allowed; +} diff --git a/frontend/src/components/ReSegmented/src/index.tsx b/frontend/src/components/ReSegmented/src/index.tsx new file mode 100755 index 0000000..4a42fb6 --- /dev/null +++ b/frontend/src/components/ReSegmented/src/index.tsx @@ -0,0 +1,216 @@ +import "./index.css"; +import type { OptionsType } from "./type"; +import { useRenderIcon } from "@/components/ReIcon/src/hooks"; +import { + useDark, + isNumber, + isFunction, + useResizeObserver +} from "@pureadmin/utils"; +import { + type PropType, + h, + ref, + toRef, + watch, + nextTick, + defineComponent, + getCurrentInstance +} from "vue"; + +const props = { + options: { + type: Array, + default: () => [] + }, + /** 默认选中,按照第一个索引为 `0` 的模式,可选(`modelValue`只有传`number`类型时才为响应式) */ + modelValue: { + type: undefined, + require: false, + default: "0" + }, + /** 将宽度调整为父元素宽度 */ + block: { + type: Boolean, + default: false + }, + /** 控件尺寸 */ + size: { + type: String as PropType<"small" | "default" | "large"> + }, + /** 是否全局禁用,默认 `false` */ + disabled: { + type: Boolean, + default: false + }, + /** 当内容发生变化时,设置 `resize` 可使其自适应容器位置 */ + resize: { + type: Boolean, + default: false + } +}; + +export default defineComponent({ + name: "ReSegmented", + props, + emits: ["change", "update:modelValue"], + setup(props, { emit }) { + const width = ref(0); + const translateX = ref(0); + const { isDark } = useDark(); + const initStatus = ref(false); + const curMouseActive = ref(-1); + const segmentedItembg = ref(""); + const instance = getCurrentInstance()!; + const curIndex = isNumber(props.modelValue) + ? toRef(props, "modelValue") + : ref(0); + + function handleChange({ option, index }, event: Event) { + if (props.disabled || option.disabled) return; + event.preventDefault(); + isNumber(props.modelValue) + ? emit("update:modelValue", index) + : (curIndex.value = index); + segmentedItembg.value = ""; + emit("change", { index, option }); + } + + function handleMouseenter({ option, index }, event: Event) { + if (props.disabled) return; + event.preventDefault(); + curMouseActive.value = index; + if (option.disabled || curIndex.value === index) { + segmentedItembg.value = ""; + } else { + segmentedItembg.value = isDark.value + ? "#1f1f1f" + : "rgba(0, 0, 0, 0.06)"; + } + } + + function handleMouseleave(_, event: Event) { + if (props.disabled) return; + event.preventDefault(); + curMouseActive.value = -1; + } + + function handleInit(index = curIndex.value) { + nextTick(() => { + const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef; + if (!curLabelRef) return; + width.value = curLabelRef.clientWidth; + translateX.value = curLabelRef.offsetLeft; + initStatus.value = true; + }); + } + + function handleResizeInit() { + useResizeObserver(".pure-segmented", () => { + nextTick(() => { + handleInit(curIndex.value); + }); + }); + } + + (props.block || props.resize) && handleResizeInit(); + + watch( + () => curIndex.value, + index => { + nextTick(() => { + handleInit(index); + }); + }, + { + immediate: true + } + ); + + watch(() => props.size, handleResizeInit, { + immediate: true + }); + + const rendLabel = () => { + return props.options.map((option, index) => { + return ( + + ); + }); + }; + + return () => ( +
+
+
+ {rendLabel()} +
+
+ ); + } +}); diff --git a/frontend/src/components/ReSegmented/src/type.ts b/frontend/src/components/ReSegmented/src/type.ts new file mode 100755 index 0000000..63b2a90 --- /dev/null +++ b/frontend/src/components/ReSegmented/src/type.ts @@ -0,0 +1,20 @@ +import type { VNode, Component } from "vue"; +import type { iconType } from "@/components/ReIcon/src/types.ts"; + +export interface OptionsType { + /** 文字 */ + label?: string | (() => VNode | Component); + /** + * @description 图标,采用平台内置的 `useRenderIcon` 函数渲染 + * @see {@link 用法参考 https://pure-admin.cn/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks } + */ + icon?: string | Component; + /** 图标属性、样式配置 */ + iconAttrs?: iconType; + /** 值 */ + value?: any; + /** 是否禁用 */ + disabled?: boolean; + /** `tooltip` 提示 */ + tip?: string; +} diff --git a/frontend/src/components/ReText/index.ts b/frontend/src/components/ReText/index.ts new file mode 100755 index 0000000..d4babbd --- /dev/null +++ b/frontend/src/components/ReText/index.ts @@ -0,0 +1,7 @@ +import reText from "./src/index.vue"; +import { withInstall } from "@pureadmin/utils"; + +/** 支持`Tooltip`提示的文本省略组件 */ +export const ReText = withInstall(reText); + +export default ReText; diff --git a/frontend/src/components/ReText/src/index.vue b/frontend/src/components/ReText/src/index.vue new file mode 100755 index 0000000..1717505 --- /dev/null +++ b/frontend/src/components/ReText/src/index.vue @@ -0,0 +1,69 @@ + + + diff --git a/frontend/src/components/business/ConfirmDialog.vue b/frontend/src/components/business/ConfirmDialog.vue new file mode 100644 index 0000000..a53beff --- /dev/null +++ b/frontend/src/components/business/ConfirmDialog.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/business/FormDialog.vue b/frontend/src/components/business/FormDialog.vue new file mode 100644 index 0000000..a592888 --- /dev/null +++ b/frontend/src/components/business/FormDialog.vue @@ -0,0 +1,331 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/business/PageTable.vue b/frontend/src/components/business/PageTable.vue new file mode 100644 index 0000000..b99bdd6 --- /dev/null +++ b/frontend/src/components/business/PageTable.vue @@ -0,0 +1,234 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/business/SearchBar.vue b/frontend/src/components/business/SearchBar.vue new file mode 100644 index 0000000..dff9fdf --- /dev/null +++ b/frontend/src/components/business/SearchBar.vue @@ -0,0 +1,205 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/business/index.ts b/frontend/src/components/business/index.ts new file mode 100644 index 0000000..7590b72 --- /dev/null +++ b/frontend/src/components/business/index.ts @@ -0,0 +1,48 @@ +// 通用业务组件导出 +export { default as SearchBar } from "./SearchBar.vue"; +export { default as ConfirmDialog } from "./ConfirmDialog.vue"; +export { default as FormDialog } from "./FormDialog.vue"; +export { default as PageTable } from "./PageTable.vue"; + +// 类型导出 +export interface SearchField { + key: string; + label: string; + type: "input" | "select" | "date" | "dateRange"; + options?: { label: string; value: string | number }[]; + placeholder?: string; +} + +export interface FormField { + key: string; + label: string; + type: "input" | "textarea" | "select" | "date" | "number" | "upload"; + required?: boolean; + options?: { label: string; value: string | number }[]; + placeholder?: string; + disabled?: boolean; + defaultValue?: any; + span?: number; + appendText?: string; +} + +export interface TableColumn { + key: string; + label: string; + width?: number | string; + minWidth?: number | string; + fixed?: "left" | "right" | boolean; + align?: "left" | "center" | "right"; + sortable?: boolean; + formatter?: (row: any, value: any) => string; + slot?: string; +} + +export interface TableAction { + label: string; + icon?: any; + type?: "primary" | "success" | "warning" | "danger" | "info"; + onClick: (row: any) => void; + visible?: (row: any) => boolean; + disabled?: (row: any) => boolean; +} \ No newline at end of file diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts new file mode 100755 index 0000000..66d9c0a --- /dev/null +++ b/frontend/src/config/index.ts @@ -0,0 +1,55 @@ +import axios from "axios"; +import type { App } from "vue"; + +let config: object = {}; +const { VITE_PUBLIC_PATH } = import.meta.env; + +const setConfig = (cfg?: unknown) => { + config = Object.assign(config, cfg); +}; + +const getConfig = (key?: string): PlatformConfigs => { + if (typeof key === "string") { + const arr = key.split("."); + if (arr && arr.length) { + let data = config; + arr.forEach(v => { + if (data && typeof data[v] !== "undefined") { + data = data[v]; + } else { + data = null; + } + }); + return data; + } + } + return config; +}; + +/** 获取项目动态全局配置 */ +export const getPlatformConfig = async (app: App): Promise => { + app.config.globalProperties.$config = getConfig(); + return axios({ + method: "get", + url: `${VITE_PUBLIC_PATH}platform-config.json` + }) + .then(({ data: config }) => { + let $config = app.config.globalProperties.$config; + // 自动注入系统配置 + if (app && $config && typeof config === "object") { + $config = Object.assign($config, config); + app.config.globalProperties.$config = $config; + // 设置全局配置 + setConfig($config); + } + return $config; + }) + .catch(() => { + throw "请在public文件夹下添加platform-config.json配置文件"; + }); +}; + +/** 本地响应式存储的命名空间 */ +const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace; + +export { getConfig, setConfig, responsiveStorageNameSpace }; diff --git a/frontend/src/directives/auth/index.ts b/frontend/src/directives/auth/index.ts new file mode 100755 index 0000000..f09e65d --- /dev/null +++ b/frontend/src/directives/auth/index.ts @@ -0,0 +1,15 @@ +import { hasAuth } from "@/router/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +export const auth: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding>) { + const { value } = binding; + if (value) { + !hasAuth(value) && el.parentNode?.removeChild(el); + } else { + throw new Error( + "[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\"" + ); + } + } +}; diff --git a/frontend/src/directives/copy/index.ts b/frontend/src/directives/copy/index.ts new file mode 100755 index 0000000..7efe88c --- /dev/null +++ b/frontend/src/directives/copy/index.ts @@ -0,0 +1,33 @@ +import { message } from "@/utils/message"; +import { useEventListener } from "@vueuse/core"; +import { copyTextToClipboard } from "@pureadmin/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +export interface CopyEl extends HTMLElement { + copyValue: string; +} + +/** 文本复制指令(默认双击复制) */ +export const copy: Directive = { + mounted(el: CopyEl, binding: DirectiveBinding) { + const { value } = binding; + if (value) { + el.copyValue = value; + const arg = binding.arg ?? "dblclick"; + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener(el, arg, () => { + const success = copyTextToClipboard(el.copyValue); + success + ? message("复制成功", { type: "success" }) + : message("复制失败", { type: "error" }); + }); + } else { + throw new Error( + '[Directive: copy]: need value! Like v-copy="modelValue"' + ); + } + }, + updated(el: CopyEl, binding: DirectiveBinding) { + el.copyValue = binding.value; + } +}; diff --git a/frontend/src/directives/index.ts b/frontend/src/directives/index.ts new file mode 100755 index 0000000..726e14e --- /dev/null +++ b/frontend/src/directives/index.ts @@ -0,0 +1,6 @@ +export * from "./auth"; +export * from "./copy"; +export * from "./longpress"; +export * from "./optimize"; +export * from "./perms"; +export * from "./ripple"; diff --git a/frontend/src/directives/longpress/index.ts b/frontend/src/directives/longpress/index.ts new file mode 100755 index 0000000..b03fce2 --- /dev/null +++ b/frontend/src/directives/longpress/index.ts @@ -0,0 +1,63 @@ +import { useEventListener } from "@vueuse/core"; +import type { Directive, DirectiveBinding } from "vue"; +import { subBefore, subAfter, isFunction } from "@pureadmin/utils"; + +export const longpress: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const cb = binding.value; + if (cb && isFunction(cb)) { + let timer = null; + let interTimer = null; + let num = 500; + let interNum = null; + const isInter = binding?.arg?.includes(":") ?? false; + + if (isInter) { + num = Number(subBefore(binding.arg, ":")); + interNum = Number(subAfter(binding.arg, ":")); + } else if (binding.arg) { + num = Number(binding.arg); + } + + const clear = () => { + if (timer) { + clearTimeout(timer); + timer = null; + } + if (interTimer) { + clearInterval(interTimer); + interTimer = null; + } + }; + + const onDownInter = (ev: PointerEvent) => { + ev.preventDefault(); + if (interTimer === null) { + interTimer = setInterval(() => cb(), interNum); + } + }; + + const onDown = (ev: PointerEvent) => { + clear(); + ev.preventDefault(); + if (timer === null) { + timer = isInter + ? setTimeout(() => { + cb(); + onDownInter(ev); + }, num) + : setTimeout(() => cb(), num); + } + }; + + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener(el, "pointerdown", onDown); + useEventListener(el, "pointerup", clear); + useEventListener(el, "pointerleave", clear); + } else { + throw new Error( + '[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"' + ); + } + } +}; diff --git a/frontend/src/directives/optimize/index.ts b/frontend/src/directives/optimize/index.ts new file mode 100755 index 0000000..1aac3e6 --- /dev/null +++ b/frontend/src/directives/optimize/index.ts @@ -0,0 +1,68 @@ +import { + isArray, + throttle, + debounce, + isObject, + isFunction +} from "@pureadmin/utils"; +import { useEventListener } from "@vueuse/core"; +import type { Directive, DirectiveBinding } from "vue"; + +export interface OptimizeOptions { + /** 事件名 */ + event: string; + /** 事件触发的方法 */ + fn: (...params: any) => any; + /** 是否立即执行 */ + immediate?: boolean; + /** 防抖或节流的延迟时间(防抖默认:`200`毫秒、节流默认:`1000`毫秒) */ + timeout?: number; + /** 传递的参数 */ + params?: any; +} + +/** 防抖(v-optimize或v-optimize:debounce)、节流(v-optimize:throttle)指令 */ +export const optimize: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const { value } = binding; + const optimizeType = binding.arg ?? "debounce"; + const type = ["debounce", "throttle"].find(t => t === optimizeType); + if (type) { + if (value && value.event && isFunction(value.fn)) { + let params = value?.params; + if (params) { + if (isArray(params) || isObject(params)) { + params = isObject(params) ? Array.of(params) : params; + } else { + throw new Error( + "[Directive: optimize]: `params` must be an array or object" + ); + } + } + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener( + el, + value.event, + type === "debounce" + ? debounce( + params ? () => value.fn(...params) : value.fn, + value?.timeout ?? 200, + value?.immediate ?? false + ) + : throttle( + params ? () => value.fn(...params) : value.fn, + value?.timeout ?? 1000 + ) + ); + } else { + throw new Error( + "[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function" + ); + } + } else { + throw new Error( + "[Directive: optimize]: only `debounce` and `throttle` are supported" + ); + } + } +}; diff --git a/frontend/src/directives/perms/index.ts b/frontend/src/directives/perms/index.ts new file mode 100755 index 0000000..378578f --- /dev/null +++ b/frontend/src/directives/perms/index.ts @@ -0,0 +1,15 @@ +import { hasPerms } from "@/utils/auth"; +import type { Directive, DirectiveBinding } from "vue"; + +export const perms: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding>) { + const { value } = binding; + if (value) { + !hasPerms(value) && el.parentNode?.removeChild(el); + } else { + throw new Error( + "[Directive: perms]: need perms! Like v-perms=\"['btn.add','btn.edit']\"" + ); + } + } +}; diff --git a/frontend/src/directives/ripple/index.scss b/frontend/src/directives/ripple/index.scss new file mode 100755 index 0000000..08ec1cf --- /dev/null +++ b/frontend/src/directives/ripple/index.scss @@ -0,0 +1,48 @@ +/* stylelint-disable-next-line scss/dollar-variable-colon-space-after */ +$ripple-animation-transition-in: + transform 0.4s cubic-bezier(0, 0, 0.2, 1), + opacity 0.2s cubic-bezier(0, 0, 0.2, 1) !default; +$ripple-animation-transition-out: opacity 0.5s cubic-bezier(0, 0, 0.2, 1) !default; +$ripple-animation-visible-opacity: 0.25 !default; + +.v-ripple { + &__container { + position: absolute; + top: 0; + left: 0; + z-index: 0; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; + border-radius: inherit; + contain: strict; + } + + &__animation { + position: absolute; + top: 0; + left: 0; + overflow: hidden; + pointer-events: none; + background: currentcolor; + border-radius: 50%; + opacity: 0; + will-change: transform, opacity; + + &--enter { + opacity: 0; + transition: none; + } + + &--in { + opacity: $ripple-animation-visible-opacity; + transition: $ripple-animation-transition-in; + } + + &--out { + opacity: 0; + transition: $ripple-animation-transition-out; + } + } +} diff --git a/frontend/src/directives/ripple/index.ts b/frontend/src/directives/ripple/index.ts new file mode 100755 index 0000000..140f874 --- /dev/null +++ b/frontend/src/directives/ripple/index.ts @@ -0,0 +1,229 @@ +import "./index.scss"; +import { isObject } from "@pureadmin/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +export interface RippleOptions { + /** 自定义`ripple`颜色,支持`tailwindcss` */ + class?: string; + /** 是否从中心扩散 */ + center?: boolean; + circle?: boolean; +} + +export interface RippleDirectiveBinding + extends Omit { + value?: boolean | { class: string }; + modifiers: { + center?: boolean; + circle?: boolean; + }; +} + +function transform(el: HTMLElement, value: string) { + el.style.transform = value; + el.style.webkitTransform = value; +} + +const calculate = ( + e: PointerEvent, + el: HTMLElement, + value: RippleOptions = {} +) => { + const offset = el.getBoundingClientRect(); + + // 获取点击位置距离 el 的垂直和水平距离 + const localX = e.clientX - offset.left; + const localY = e.clientY - offset.top; + + let radius = 0; + let scale = 0.3; + // 计算点击位置到 el 顶点最远距离,即为圆的最大半径(勾股定理) + if (el._ripple?.circle) { + scale = 0.15; + radius = el.clientWidth / 2; + radius = value.center + ? radius + : radius + Math.sqrt((localX - radius) ** 2 + (localY - radius) ** 2) / 4; + } else { + radius = Math.sqrt(el.clientWidth ** 2 + el.clientHeight ** 2) / 2; + } + + // 中心点坐标 + const centerX = `${(el.clientWidth - radius * 2) / 2}px`; + const centerY = `${(el.clientHeight - radius * 2) / 2}px`; + + // 点击位置坐标 + const x = value.center ? centerX : `${localX - radius}px`; + const y = value.center ? centerY : `${localY - radius}px`; + + return { radius, scale, x, y, centerX, centerY }; +}; + +const ripples = { + show(e: PointerEvent, el: HTMLElement, value: RippleOptions = {}) { + if (!el?._ripple?.enabled) { + return; + } + + // 创建 ripple 元素和 ripple 父元素 + const container = document.createElement("span"); + const animation = document.createElement("span"); + + container.appendChild(animation); + container.className = "v-ripple__container"; + + if (value.class) { + container.className += ` ${value.class}`; + } + + const { radius, scale, x, y, centerX, centerY } = calculate(e, el, value); + + // ripple 圆大小 + const size = `${radius * 2}px`; + + animation.className = "v-ripple__animation"; + animation.style.width = size; + animation.style.height = size; + + el.appendChild(container); + + // 获取目标元素样式表 + const computed = window.getComputedStyle(el); + // 防止 position 被覆盖导致 ripple 位置有问题 + if (computed && computed.position === "static") { + el.style.position = "relative"; + el.dataset.previousPosition = "static"; + } + + animation.classList.add("v-ripple__animation--enter"); + animation.classList.add("v-ripple__animation--visible"); + transform( + animation, + `translate(${x}, ${y}) scale3d(${scale},${scale},${scale})` + ); + animation.dataset.activated = String(performance.now()); + + setTimeout(() => { + animation.classList.remove("v-ripple__animation--enter"); + animation.classList.add("v-ripple__animation--in"); + transform(animation, `translate(${centerX}, ${centerY}) scale3d(1,1,1)`); + }, 0); + }, + + hide(el: HTMLElement | null) { + if (!el?._ripple?.enabled) return; + + const ripples = el.getElementsByClassName("v-ripple__animation"); + + if (ripples.length === 0) return; + const animation = ripples[ripples.length - 1] as HTMLElement; + + if (animation.dataset.isHiding) return; + else animation.dataset.isHiding = "true"; + + const diff = performance.now() - Number(animation.dataset.activated); + const delay = Math.max(250 - diff, 0); + + setTimeout(() => { + animation.classList.remove("v-ripple__animation--in"); + animation.classList.add("v-ripple__animation--out"); + + setTimeout(() => { + const ripples = el.getElementsByClassName("v-ripple__animation"); + if (ripples.length === 1 && el.dataset.previousPosition) { + el.style.position = el.dataset.previousPosition; + delete el.dataset.previousPosition; + } + + if (animation.parentNode?.parentNode === el) + el.removeChild(animation.parentNode); + }, 300); + }, delay); + } +}; + +function isRippleEnabled(value: any): value is true { + return typeof value === "undefined" || !!value; +} + +function rippleShow(e: PointerEvent) { + const value: RippleOptions = {}; + const element = e.currentTarget as HTMLElement | undefined; + + if (!element?._ripple || element._ripple.touched) return; + + value.center = element._ripple.centered; + if (element._ripple.class) { + value.class = element._ripple.class; + } + + ripples.show(e, element, value); +} + +function rippleHide(e: Event) { + const element = e.currentTarget as HTMLElement | null; + if (!element?._ripple) return; + + window.setTimeout(() => { + if (element._ripple) { + element._ripple.touched = false; + } + }); + ripples.hide(element); +} + +function updateRipple( + el: HTMLElement, + binding: RippleDirectiveBinding, + wasEnabled: boolean +) { + const { value, modifiers } = binding; + const enabled = isRippleEnabled(value); + if (!enabled) { + ripples.hide(el); + } + + el._ripple = el._ripple ?? {}; + el._ripple.enabled = enabled; + el._ripple.centered = modifiers.center; + el._ripple.circle = modifiers.circle; + if (isObject(value) && value.class) { + el._ripple.class = value.class; + } + + if (enabled && !wasEnabled) { + el.addEventListener("pointerdown", rippleShow); + el.addEventListener("pointerup", rippleHide); + } else if (!enabled && wasEnabled) { + removeListeners(el); + } +} + +function removeListeners(el: HTMLElement) { + el.removeEventListener("pointerdown", rippleShow); + el.removeEventListener("pointerup", rippleHide); +} + +function mounted(el: HTMLElement, binding: RippleDirectiveBinding) { + updateRipple(el, binding, false); +} + +function unmounted(el: HTMLElement) { + delete el._ripple; + removeListeners(el); +} + +function updated(el: HTMLElement, binding: RippleDirectiveBinding) { + if (binding.value === binding.oldValue) { + return; + } + + const wasEnabled = isRippleEnabled(binding.oldValue); + updateRipple(el, binding, wasEnabled); +} + +export const Ripple: Directive = { + mounted, + unmounted, + updated +}; diff --git a/frontend/src/layout/components/lay-content/index.vue b/frontend/src/layout/components/lay-content/index.vue new file mode 100755 index 0000000..9e9f3db --- /dev/null +++ b/frontend/src/layout/components/lay-content/index.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/frontend/src/layout/components/lay-footer/index.vue b/frontend/src/layout/components/lay-footer/index.vue new file mode 100755 index 0000000..0bda052 --- /dev/null +++ b/frontend/src/layout/components/lay-footer/index.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/layout/components/lay-frame/index.vue b/frontend/src/layout/components/lay-frame/index.vue new file mode 100755 index 0000000..cc8e00a --- /dev/null +++ b/frontend/src/layout/components/lay-frame/index.vue @@ -0,0 +1,79 @@ + + diff --git a/frontend/src/layout/components/lay-navbar/index.vue b/frontend/src/layout/components/lay-navbar/index.vue new file mode 100755 index 0000000..3003b01 --- /dev/null +++ b/frontend/src/layout/components/lay-navbar/index.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/frontend/src/layout/components/lay-notice/components/NoticeItem.vue b/frontend/src/layout/components/lay-notice/components/NoticeItem.vue new file mode 100755 index 0000000..6c8e156 --- /dev/null +++ b/frontend/src/layout/components/lay-notice/components/NoticeItem.vue @@ -0,0 +1,177 @@ + + + + + + diff --git a/frontend/src/layout/components/lay-notice/components/NoticeList.vue b/frontend/src/layout/components/lay-notice/components/NoticeList.vue new file mode 100755 index 0000000..71ca2f3 --- /dev/null +++ b/frontend/src/layout/components/lay-notice/components/NoticeList.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/layout/components/lay-notice/data.ts b/frontend/src/layout/components/lay-notice/data.ts new file mode 100755 index 0000000..0255dcf --- /dev/null +++ b/frontend/src/layout/components/lay-notice/data.ts @@ -0,0 +1,97 @@ +export interface ListItem { + avatar: string; + title: string; + datetime: string; + type: string; + description: string; + status?: "primary" | "success" | "warning" | "info" | "danger"; + extra?: string; +} + +export interface TabItem { + key: string; + name: string; + list: ListItem[]; + emptyText: string; +} + +export const noticesData: TabItem[] = [ + { + key: "1", + name: "通知", + list: [], + emptyText: "暂无通知" + }, + { + key: "2", + name: "消息", + list: [ + { + avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile1.svg", + title: "小铭 评论了你", + description: "诚在于心,信在于行,诚信在于心行合一。", + datetime: "今天", + type: "2" + }, + { + avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile2.svg", + title: "李白 回复了你", + description: "长风破浪会有时,直挂云帆济沧海。", + datetime: "昨天", + type: "2" + }, + { + avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile5.svg", + title: "标题", + description: + "请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容", + datetime: "时间", + type: "2" + } + ], + emptyText: "暂无消息" + }, + { + key: "3", + name: "待办", + list: [ + { + avatar: "", + title: "第三方紧急代码变更", + description: + "小林提交于 2024-05-10,需在 2024-05-11 前完成代码变更任务", + datetime: "", + extra: "马上到期", + status: "danger", + type: "3" + }, + { + avatar: "", + title: "版本发布", + description: "指派小铭于 2024-06-18 前完成更新并发布", + datetime: "", + extra: "已耗时 8 天", + status: "warning", + type: "3" + }, + { + avatar: "", + title: "新功能开发", + description: "开发多租户管理", + datetime: "", + extra: "进行中", + type: "3" + }, + { + avatar: "", + title: "任务名称", + description: "任务需要在 2030-10-30 10:00 前启动", + datetime: "", + extra: "未开始", + status: "info", + type: "3" + } + ], + emptyText: "暂无待办" + } +]; diff --git a/frontend/src/layout/components/lay-notice/index.vue b/frontend/src/layout/components/lay-notice/index.vue new file mode 100755 index 0000000..7e449dc --- /dev/null +++ b/frontend/src/layout/components/lay-notice/index.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/frontend/src/layout/components/lay-panel/index.vue b/frontend/src/layout/components/lay-panel/index.vue new file mode 100755 index 0000000..bbb91a0 --- /dev/null +++ b/frontend/src/layout/components/lay-panel/index.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/frontend/src/layout/components/lay-search/components/SearchFooter.vue b/frontend/src/layout/components/lay-search/components/SearchFooter.vue new file mode 100755 index 0000000..fb423c9 --- /dev/null +++ b/frontend/src/layout/components/lay-search/components/SearchFooter.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/frontend/src/layout/components/lay-search/components/SearchHistory.vue b/frontend/src/layout/components/lay-search/components/SearchHistory.vue new file mode 100755 index 0000000..dcf2d63 --- /dev/null +++ b/frontend/src/layout/components/lay-search/components/SearchHistory.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/frontend/src/layout/components/lay-search/components/SearchHistoryItem.vue b/frontend/src/layout/components/lay-search/components/SearchHistoryItem.vue new file mode 100755 index 0000000..0d82d24 --- /dev/null +++ b/frontend/src/layout/components/lay-search/components/SearchHistoryItem.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/frontend/src/layout/components/lay-search/components/SearchModal.vue b/frontend/src/layout/components/lay-search/components/SearchModal.vue new file mode 100755 index 0000000..f752405 --- /dev/null +++ b/frontend/src/layout/components/lay-search/components/SearchModal.vue @@ -0,0 +1,334 @@ + + + + + diff --git a/frontend/src/layout/components/lay-search/components/SearchResult.vue b/frontend/src/layout/components/lay-search/components/SearchResult.vue new file mode 100755 index 0000000..cbca99e --- /dev/null +++ b/frontend/src/layout/components/lay-search/components/SearchResult.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/frontend/src/layout/components/lay-search/index.vue b/frontend/src/layout/components/lay-search/index.vue new file mode 100755 index 0000000..4e489db --- /dev/null +++ b/frontend/src/layout/components/lay-search/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/layout/components/lay-search/types.ts b/frontend/src/layout/components/lay-search/types.ts new file mode 100755 index 0000000..9cfa97d --- /dev/null +++ b/frontend/src/layout/components/lay-search/types.ts @@ -0,0 +1,20 @@ +interface optionsItem { + path: string; + type: "history" | "collect"; + meta: { + icon?: string; + title?: string; + }; +} + +interface dragItem { + oldIndex: number; + newIndex: number; +} + +interface Props { + value: string; + options: Array; +} + +export type { optionsItem, dragItem, Props }; diff --git a/frontend/src/layout/components/lay-setting/index.vue b/frontend/src/layout/components/lay-setting/index.vue new file mode 100755 index 0000000..a8aa19e --- /dev/null +++ b/frontend/src/layout/components/lay-setting/index.vue @@ -0,0 +1,631 @@ + + + + + diff --git a/frontend/src/layout/components/lay-sidebar/NavHorizontal.vue b/frontend/src/layout/components/lay-sidebar/NavHorizontal.vue new file mode 100755 index 0000000..3f25e62 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/NavHorizontal.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/frontend/src/layout/components/lay-sidebar/NavMix.vue b/frontend/src/layout/components/lay-sidebar/NavMix.vue new file mode 100755 index 0000000..fb820e5 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/NavMix.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/frontend/src/layout/components/lay-sidebar/NavVertical.vue b/frontend/src/layout/components/lay-sidebar/NavVertical.vue new file mode 100755 index 0000000..64b5217 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/NavVertical.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue new file mode 100755 index 0000000..6bb9c60 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue @@ -0,0 +1,120 @@ + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue new file mode 100755 index 0000000..e31fa63 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue new file mode 100755 index 0000000..45d3285 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue new file mode 100755 index 0000000..ddfa27f --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue @@ -0,0 +1,30 @@ + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarItem.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarItem.vue new file mode 100755 index 0000000..452f437 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarItem.vue @@ -0,0 +1,228 @@ + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue new file mode 100755 index 0000000..579c3a9 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue new file mode 100755 index 0000000..2fa5617 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue @@ -0,0 +1,32 @@ + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarLogo.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarLogo.vue new file mode 100755 index 0000000..77b89d4 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarLogo.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue b/frontend/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue new file mode 100755 index 0000000..c37a504 --- /dev/null +++ b/frontend/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/layout/components/lay-tag/components/TagChrome.vue b/frontend/src/layout/components/lay-tag/components/TagChrome.vue new file mode 100755 index 0000000..a958485 --- /dev/null +++ b/frontend/src/layout/components/lay-tag/components/TagChrome.vue @@ -0,0 +1,33 @@ + diff --git a/frontend/src/layout/components/lay-tag/index.scss b/frontend/src/layout/components/lay-tag/index.scss new file mode 100755 index 0000000..1a095d5 --- /dev/null +++ b/frontend/src/layout/components/lay-tag/index.scss @@ -0,0 +1,371 @@ +@keyframes schedule-in-width { + from { + width: 0; + } + + to { + width: 100%; + } +} + +@keyframes schedule-out-width { + from { + width: 100%; + } + + to { + width: 0; + } +} + +.tags-view { + position: relative; + display: flex; + align-items: center; + width: 100%; + font-size: 14px; + color: var(--el-text-color-primary); + background: #fff; + box-shadow: 0 0 1px #888; + + .scroll-item { + position: relative; + display: inline-block; + height: 34px; + padding-left: 6px; + line-height: 34px; + cursor: pointer; + transition: all 0.4s; + + &:not(:first-child) { + padding-right: 24px; + } + + &.chrome-item { + padding-right: 0; + padding-left: 0; + margin-right: -18px; + box-shadow: none; + } + + .el-icon-close { + position: absolute; + top: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + color: var(--el-color-primary); + cursor: pointer; + border-radius: 4px; + transform: translate(0, -50%); + transition: + background-color 0.12s, + color 0.12s; + + &:hover { + color: rgb(0 0 0 / 88%) !important; + background-color: rgb(0 0 0 / 6%); + } + } + } + + .tag-title { + padding: 0 4px; + color: var(--el-text-color-primary); + text-decoration: none; + } + + .scroll-container { + position: relative; + flex: 1; + overflow: hidden; + white-space: nowrap; + + &.chrome-scroll-container { + padding-top: 4px; + + .fixed-tag { + padding: 0 !important; + } + } + + .tab { + position: relative; + float: left; + overflow: visible; + white-space: nowrap; + list-style: none; + + .scroll-item { + transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); + + &:nth-child(1) { + padding: 0 12px; + } + + &.chrome-item { + &:nth-child(1) { + padding: 0; + } + } + } + + .fixed-tag { + padding: 0 12px; + } + } + } + + /* 右键菜单 */ + .contextmenu { + position: absolute; + padding: 5px 0; + margin: 0; + font-size: 13px; + font-weight: normal; + color: var(--el-text-color-primary); + white-space: nowrap; + outline: 0; + list-style-type: none; + background: #fff; + border-radius: 4px; + box-shadow: 0 2px 8px rgb(0 0 0 / 15%); + + li { + display: flex; + align-items: center; + width: 100%; + padding: 7px 12px; + margin: 0; + cursor: pointer; + + &:hover { + color: var(--el-color-primary); + } + + svg { + display: block; + margin-right: 0.5em; + } + } + } +} + +.el-dropdown-menu { + li { + display: flex; + align-items: center; + width: 100%; + margin: 0; + cursor: pointer; + + svg { + display: block; + margin-right: 0.5em; + } + } +} + +.el-dropdown-menu__item:not(.is-disabled):hover { + color: #606266; + background: #f0f0f0; +} + +:deep(.el-dropdown-menu__item) i { + margin-right: 10px; +} + +:deep(.el-dropdown-menu__item--divided) { + margin: 1px 0; +} + +.el-dropdown-menu__item--divided::before { + margin: 0; +} + +.el-dropdown-menu__item.is-disabled { + cursor: not-allowed; +} + +.scroll-item.is-active { + position: relative; + color: #fff; + box-shadow: 0 0 0.7px #888; + + .chrome-tab { + z-index: 10; + } + + .chrome-tab__bg { + color: var(--el-color-primary-light-9) !important; + } + + .tag-title { + color: var(--el-color-primary) !important; + } + + .chrome-close-btn { + color: var(--el-color-primary); + + &:hover { + background-color: var(--el-color-primary); + } + } + + .chrome-tab-divider { + opacity: 0; + } +} + +.arrow-left, +.arrow-right, +.arrow-down { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 34px; + color: var(--el-text-color-primary); + + svg { + width: 20px; + height: 20px; + } +} + +.arrow-left { + box-shadow: 5px 0 5px -6px #ccc; + + &:hover { + cursor: w-resize; + } +} + +.arrow-right { + border-right: 0.5px solid #ccc; + box-shadow: -5px 0 5px -6px #ccc; + + &:hover { + cursor: e-resize; + } +} + +/* 卡片模式下鼠标移入显示蓝色边框 */ +.card-in { + color: var(--el-color-primary); + + .tag-title { + color: var(--el-color-primary); + } +} + +/* 卡片模式下鼠标移出隐藏蓝色边框 */ +.card-out { + color: #666; + border: none; + + .tag-title { + color: #666; + } +} + +/* 灵动模式 */ +.schedule-active { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--el-color-primary); +} + +/* 灵动模式下鼠标移入显示蓝色进度条 */ +.schedule-in { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--el-color-primary); + animation: schedule-in-width 200ms ease-in; +} + +/* 灵动模式下鼠标移出隐藏蓝色进度条 */ +.schedule-out { + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: var(--el-color-primary); + animation: schedule-out-width 200ms ease-in; +} + +/* 谷歌风格的页签 */ +.chrome-tab { + position: relative; + display: inline-flex; + gap: 16px; + align-items: center; + justify-content: center; + padding: 0 24px; + white-space: nowrap; + cursor: pointer; + + .tag-title { + padding: 0; + } + + .chrome-tab-divider { + position: absolute; + right: 7px; + width: 1px; + height: 14px; + background-color: #2b2d2f; + } + + &:hover { + z-index: 10; + + .chrome-tab__bg { + color: #dee1e6; + } + + .tag-title { + color: #1f1f1f; + } + + .chrome-tab-divider { + opacity: 0; + } + } + + .chrome-tab__bg { + position: absolute; + top: 0; + left: 0; + z-index: -10; + width: 100%; + height: 100%; + color: transparent; + pointer-events: none; + } + + .chrome-close-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: #666; + border-radius: 50%; + + &:hover { + color: white; + background-color: #b1b3b8; + } + } +} diff --git a/frontend/src/layout/components/lay-tag/index.vue b/frontend/src/layout/components/lay-tag/index.vue new file mode 100755 index 0000000..ddecb46 --- /dev/null +++ b/frontend/src/layout/components/lay-tag/index.vue @@ -0,0 +1,684 @@ + + + + + diff --git a/frontend/src/layout/frame.vue b/frontend/src/layout/frame.vue new file mode 100755 index 0000000..ee8bd71 --- /dev/null +++ b/frontend/src/layout/frame.vue @@ -0,0 +1,91 @@ + + +